Claude
Skills
Sign in
Back

working-with-dist-zilla

Included with Lifetime
$97 forever

Use when working in a Perl repo containing a dist.ini file, or when the user mentions dzil, Dist::Zilla, or @Author::* PluginBundles.

General

What this skill does


# Working with Dist::Zilla Repositories

## Overview

`Dist::Zilla` (`dzil`) is a meta-tool that builds CPAN distributions from a `dist.ini` declaration. Many of its behaviors are stable across projects but invisible to anyone who hasn't been bitten — there's no "the docs say so" moment, you just learn by getting them wrong. This skill captures those patterns so you apply them on the first pass instead of discovering each one in an implementer/reviewer loop.

**Worked example that surfaced these patterns in sequence:** [oalders/lwp-consolelogger#56](https://github.com/oalders/lwp-consolelogger/pull/56) (Code::TidyAll → precious migration).

## When to Use

Use this skill when:
- The repo contains a `dist.ini` file at its root
- The user mentions `dzil`, `Dist::Zilla`, `@Author::*` bundles, `cpanfile`, or `META.json`
- You are editing prereqs, swapping plugins, or preparing a release
- A `dzil build` output is producing diff noise or unexpected warnings

Don't use when:
- The repo is a plain `ExtUtils::MakeMaker` / `Module::Build` distribution with no `dist.ini`
- Work is unrelated to packaging (e.g. editing only `lib/` source code without touching prereqs or build config)

## Workflow

```dot
digraph dzil {
    "Repo orientation" [shape=box];
    "Editing prereqs?" [shape=diamond];
    "Decide PluginRemover vs RemovePrereqs" [shape=box];
    "Apply edits to dist.ini" [shape=box];
    "Run dzil build" [shape=box];
    "Decide commit vs revert" [shape=box];
    "Run dzil test --release --author" [shape=box];
    "Tests pass?" [shape=diamond];
    "Fix warnings (e.g. .mailmap)" [shape=box];
    "Commit" [shape=box];

    "Repo orientation" -> "Editing prereqs?";
    "Editing prereqs?" -> "Decide PluginRemover vs RemovePrereqs" [label="yes"];
    "Editing prereqs?" -> "Run dzil build" [label="no"];
    "Decide PluginRemover vs RemovePrereqs" -> "Apply edits to dist.ini";
    "Apply edits to dist.ini" -> "Run dzil build";
    "Run dzil build" -> "Decide commit vs revert";
    "Decide commit vs revert" -> "Run dzil test --release --author";
    "Run dzil test --release --author" -> "Tests pass?";
    "Tests pass?" -> "Fix warnings (e.g. .mailmap)" [label="warnings"];
    "Fix warnings (e.g. .mailmap)" -> "Run dzil build";
    "Tests pass?" -> "Commit" [label="clean"];
}
```

## 1. Repo orientation: read the bundle source

`dist.ini` usually starts with a single `[@Author::Foo]` line. The bundle's plugin and prereq-block names are what you'll need to reference for `-remove`. Find the bundle source:

```bash
perldoc -lm Dist::Zilla::PluginBundle::Author::OALDERS
```

Read the `configure` / `bundle_config` sub. The strings you can `-remove` against come from there — both plugin instance names (e.g. `Test::TidyAll`) and named Prereqs blocks (e.g. `'Prereqs' => 'Modules for use with tidyall'`).

## 2. Editing prereqs: PluginRemover vs RemovePrereqs

`Dist::Zilla::Role::PluginBundle::PluginRemover` matches `-remove` values against the plugin **instance name** or the **expanded class**. Class-name removes (`-remove = Test::TidyAll`) work reliably because they match `Dist::Zilla::Plugin::Test::TidyAll` — that string is stable. Instance-name removes against a **named Prereqs block** like:

```perl
[ 'Prereqs' => 'Modules for use with tidyall' => { ... } ]
```

…require you to know the exact moniker the bundle assigned. Bundle authors pick these names ad-hoc, so the same conceptual block is called `'Modules for use with tidyall'` in `@Author::OALDERS` but might be `'TidyAll prereqs'` in another bundle. The moniker may also include spaces, slashes, or the `@Bundle/` prefix, none of which is documented anywhere except the bundle source. **Prefer module-level removal via `[RemovePrereqs]` (next section) — it sidesteps the moniker-guessing problem entirely.**

**Decision tree:**

| Target | Use |
|--------|-----|
| A plugin (e.g. `Test::TidyAll`) | `[@Author::Foo]` with `-remove = Test::TidyAll` |
| Individual modules from a bundle's prereqs | `[RemovePrereqs]` (see below) |
| A named `[Prereqs / NAME]` block | `[RemovePrereqs]` — `-remove` is unreliable |

## 3. `[RemovePrereqs]` syntax: `remove =`, not `-remove =`

`[RemovePrereqs]` uses `remove =` (no leading dash). Using `-remove =` raises `multiple values given for property -remove` from `Config::MVP`. This contradicts the muscle-memory pattern from PluginBundle `-remove`. Correct usage:

```ini
[RemovePrereqs]
remove = Code::TidyAll
remove = Code::TidyAll::Plugin::SortLines::Naturally
remove = Parallel::ForkManager
remove = Test::Vars
```

## 4. Re-adding prereqs: `[Prereqs / PHASE]`

After stripping bundle prereqs, re-add what you still need with a phase-scoped block. The slash syntax sets the phase + relationship in one shot:

```ini
[Prereqs / DevelopRequires]
App::perlvars = 0

[Prereqs / TestRequires]
Test::Deep = 0
```

Use `RuntimeRequires`, `TestRequires`, `DevelopRequires`, `ConfigureRequires` as appropriate.

## 5. What to commit vs revert: the `CopyFilesFromBuild` rule

`dzil build` regenerates `META.json`, `Makefile.PL`, `README.md` (and sometimes others) via `[CopyFilesFromBuild]`. These files are tracked at the repo root but are essentially **snapshots of the last release**. When a feature PR touches `dist.ini`, the regenerated copies show up as ~100-line diffs of prereq lists, MakeMaker plugin version bumps, and dist-version increments — noise without semantic content.

**Rule:** For non-release PRs, commit ONLY `dist.ini` and `cpanfile`. Revert the rest and remove the build dir:

```bash
git checkout -- META.json Makefile.PL README.md
rm -rf <dist-name>-*/ <dist-name>-*.tar.gz
```

The next release commit regenerates them cleanly.

**Caveat:** If the PR IS a release (version bump in `dist.ini` + `Changes` update), then those files SHOULD ship in the commit.

## 6. `cpanfile` sync is automatic

`[CopyFilesFromBuild]` copies whatever filenames the bundle explicitly lists — it doesn't intrinsically know about `cpanfile`. But most modern bundles configure `[CopyFilesFromBuild] copy = cpanfile` (often alongside `META.json`, `Makefile.PL`, etc.), so the `cpanfile` at the repo root is regenerated automatically after `dzil build`. You usually don't need to `cp` manually. Verify with:

```bash
diff -u cpanfile <Dist>-*/cpanfile
```

If they differ, run `dzil build` again — your edit hasn't been reflected yet.

## 7. Keep developer-only tooling configs out of the dist

Linter/formatter config and dev hook scripts matter only to contributors working in the repo; they add nothing to an installed dist. Unless excluded, `[Git::GatherDir]` (which gathers from `git ls-files`) sweeps them into the tarball and ships them to CPAN, where they are dead weight.

**Canonical file list** — the dev-only configs that should never reach the dist:

- `precious.toml`
- `.perltidyrc` / `perltidyrc`
- `.perlcriticrc`
- `perlimports.toml`
- `.tidyallrc` / `tidyall.ini` (legacy `Code::TidyAll`)
- lint / pre-commit hook scripts (e.g. `scripts/pre-commit`)

**Two mechanisms** — pick by whether the file should ever enter the manifest:

| Mechanism | Syntax | What it does | Reach for it when |
|-----------|--------|--------------|-------------------|
| `[Git::GatherDir]` (or `[GatherDir]`) | `exclude_filename = <name>` | Keeps the file out of the gather step entirely — it never enters the manifest | Files that never need to be in the dist (the root config files above) |
| `[PruneFiles]` | `filename = <name>` or `match = <regex>` | Drops a file that was already gathered | Tracked files contributors need locally but that must not ship (e.g. `scripts/pre-commit`, which contributors symlink as a git hook — see the `tune-precious` skill's T6) |

**Practical caveat:** `exclude_filename` only works on a `[Git::GatherDir]` block you actually control in `dist.ini`. When `[Git::GatherDir]` is supplied by an `[@Author::*]` PluginBundle (the common case), you usually can't pass `exclude_filename` to it from `dist.ini` — use the bundle's docume

Related in General