The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Dist::Zilla::Plugin::Hook::Manual - Hook plugin user manual

VERSION

Version v0.8.4, released on 2018-03-15 21:44 UTC.

WHAT?

Dist-Zilla-Plugin-Hook (or just Hook) is a set of Dist-Zilla plugins. Every plugin executes Perl code inlined into dist.ini at particular stage of build process.

This is Hook user manual. Read this if you want to write Dist::Zilla plugin directly in dist.ini.

If you are going to hack or extend Dist-Zilla-Plugin-Hook, read the Dist::Zilla::Role::Hooker module documentation. General topics like getting source, building, installing, bug reporting and some others are covered in the README.

SYNOPSIS

In your dist.ini:

    [Hook/prologue]
        . = # Code to be executed before every hook.
        . = use autodie ':all';
        . = use Path::Tiny;
    [Hook::Role]
        . = # Code of your inline plugin:
        . = $self->log( 'Starting…' );
        . = # …arbitrary Perl code…
        . = …

where Role is one of Hook submodules/Dist::Zilla roles, like BeforeBuild, AfterBuild, FileGatherer, MetaProvider etc. See complete list of supported roles in "List of Modules".

DESCRIPTION

Hook is a set of plugins, like Hook::BeforeBuild and Hook::AfterBuild.

Every Hook plugin (except Hook::Init, which is a bit special, see below) consumes a role with the same name, and implements the method required by the consumed role. When Dist::Zilla invokes the method, it executes the code specified in the plugin's section of dist.ini.

An example: Hook::BeforeBuild plugin consumes BeforeBuild role, this role requires before_build method, which is implemented by the plugin. When Dist::Zilla invokes Hook::BeforeBuild's before_build method, it executes the code from the plugin's section of dist.ini, e. g.:

    name    = Assa
    version = 0.001
    [Hook::BeforeBuild]
        . = $self->log( [ "Building v%s", $dist->version ] );
    ...

and Perl code inlined into [Hook::BeforeBuild] section of dist.ini prints message "Building v0.001" to the log. Such inlined Perl code is called "hook" below.

The same for all other Hook plugins. Only Hook::Init plugin is a bit special: it implements BUILD method. It has two subsequences. First: there is no need in consuming a role to implement BUILD method (and actually there is no role Dist::Zilla::Role::Init). Second, more important: BUILD method is called at very early stage of the build, immediately after reading [Hook::Init] section of dist.ini. This is useful in some circumstances.

Predefined Variables

Inlined Perl code can use following predefined variables:

@_

Arguments of the method, as provided by Dist::Zilla. Self-reference is already shifted to $self (but the first argument is not)!

$arg

The first argument of the method, the same as $_[ 0 ]. If Dist::Zilla does not provide argument, the variable will be set to undef.

$plugin
$self

Reference to the plugin object executing the code.

$dist
$zilla

Reference to Dist::Zilla object, the same as $self->zilla.

$self and $zilla are "standard" variables frequently used in plugin source code. $plugin and $dist variables are defined for conformance with template processing plugins (GenerateFile, Templates, TemplateFiles, MetaResources::Template, etc.). $arg is defined for convenience: in many cases Dist::Zilla passes the only argument to the method (which usually is a HashRef).

Arguments and Return Value

As described in the previous section, arguments provided by Dist::Zilla are passed to the hook (through $self, $arg, and @_ variables).

Return value of the hook code is not ignored but passed back to Dist::Zilla. Dist::Zilla, in turn, often ignores it, but sometimes return value is important, for example, for "provider" plugins: Hook::VersionProvider, Hook::MetaProvider, etc. See documentation on corresponding roles (e. g. Dist::Zilla::Role::VersionProvider, Dist::Zilla::Role::MetaProvider, etc) for description of expected method result.

Passing arguments and return values actually means you can write your own Dist::Zilla plugin which code is not in an external .pm file but inlined directly to dist.ini. Of course, such approach is limited. For example, "inline plugin" cannot define attributes and methods. Anyway the approach is quite convenient for small hacks and prototyping which do not require much coding. See "EXAMPLES" section.

Prologue

If dist.ini contains section [Hook/prologue], the code from this section is executed before every hook. All the predefined variables are available in prologue code too.

Prologue may be used for loading frequently used modules, or for debugging:

    [Hook/prologue]
        . = use autodie ':all';
        . = use Path::Tiny;
        . = use IPC::System::Simple qw{ capture };
        . = $self->log_debug( 'begins' );
    [Hook::BeforeBuild]
        . = # No need in "use autodie" because
        . = # it is specified in prologue.
        . = system( … );

ErrorLogger Role

Every Hook plugin executes Perl code with help from the Hooker role. The latter uses ErrorLogger role internally. As a side effect, ErrorLogger methods are also available to use in hooks:

    [Hook::Role]
        . = $self->log_error( … );
        . = $self->abort_if_error( … );
        . = $self->abort( … );

Multiple Hooks of the Same Role

Use explicit plugin names if you want to have multiple hooks of the same role, e. g.:

    [Hook::AfterRelease/bump version]
        . = my $version = Perl::Version->new( $dist->version );
        . = $version->inc_alpha;
        . = path( $dist->root )->child( 'VERSION' )->spew( $version );

    [Hook::AfterRelease/post-release commit]
        . = system( qw{ hg commit -m Post-release VERSION Changes } );

    [Hook::AfterRelease/push]
        . = system( qw{ hg push } );

    [Hook::AfterRelease/clean]
        . = $zilla->clean();

List of Modules

This is the complete list of Hook modules/roles and methods:

    --------------------- + ----------------------
    Module/Role           | Method
    --------------------- + ----------------------
    AfterBuild            | after_build
    AfterMint             | after_mint
    AfterRelease          | after_release
    BeforeArchive         | before_archive
    BeforeBuild           | before_build
    BeforeMint            | before_mint
    BeforeRelease         | before_release
    FileGatherer          | gather_files
    FileMunger            | munge_files
    FilePruner            | prune_files
    Init                  | BUILD
    InstallTool           | setup_installer
    LicenseProvider       | provide_license
    MetaProvider          | metadata
    ModuleMaker           | make_module
    NameProvider          | provide_name
    PrereqSource          | register_prereqs
    ReleaseStatusProvider | provide_release_status
    Releaser              | release
    VersionProvider       | provide_version
    --------------------- + ----------------------

OPTIONS

.

Yes, the only option recognized by Hook modules is . (dot).

This is multi-value option, i. e. it may be specified multiple time. Each value is a distinct line of Perl code, e. g.:

    . = if ( $dist->is_trial ) {
    . =     $self->log( 'Building trial version' );
    . = };

Beware of caveats, see "dist.ini Parsing".

CAVEATS

dist.ini Parsing

Before code reaches a Hook plugin, it is parsed by Dist::Zilla config file reader (probably, by Config::INI::Reader). Config file reader seems to strip leading and trailing spaces from each value, and treat semicolon preceded by a space as a comment starter. Usually it is not a problem, just put semicolon immediately after statement:

    . = foo(); bar();       # Ok
    . = foo() ; bar() ;     # NOT OK: bar will not be called.

Note that semicolon starts a dist.ini comment even within Perl string:

    . = $str = "one; two";  # Ok
    . = $str = "one ; two"; # NOT OK

And be careful with multi-line strings:

    . = $str = "first line
    . =     indented line"; # Leading spaces will be lost.

WHY?

There is Dist::Zilla::Plugin::Run on CPAN which allows to run Perl code from within dist.ini, why I wrote one more? Let us consider two examples.

The first one executes external commands:

    $cat dist.ini
    name     = RunShell
    abstract = RunShell demo
    version  = 0.001_001
    [Run::BeforeBuild]
        run            = echo "1. begin"
        run_if_release = echo "2. release"
        run_no_release = echo "3. not release"
        run_if_trial   = echo "4. trial"
        run_no_trial   = echo "5. not trial"
        run            = echo "6. end"
    [GenerateFile/Assa.pm]
        filename = lib/Assa.pm
        content  = package Assa; 1;
    [FakeRelease]

    $ dzil build
    [Run::BeforeBuild] executing: echo "1. begin"
    [Run::BeforeBuild] 1. begin
    [Run::BeforeBuild] executing: echo "6. end"
    [Run::BeforeBuild] 6. end
    [Run::BeforeBuild] executing: echo "5. not trial"
    [Run::BeforeBuild] 5. not trial
    [Run::BeforeBuild] executing: echo "3. not release"
    [Run::BeforeBuild] 3. not release
    [DZ] beginning to build RunShell
    [DZ] writing RunShell in RunShell-0.001_001
    [DZ] building archive with Archive::Tar::Wrapper
    [DZ] writing archive to RunShell-0.001_001-TRIAL.tar.gz
    [DZ] built in RunShell-0.001_001

Execution order is err… non-linear. Of course there is an explanation why command were executed in this particular order, but when you are looking at dist.ini it is not obvious. (It is also unclear why Run consider the build is not trial, but it may be just a bug.)

Another example executes Perl code:

    $cat dist.ini
    name     = RunPerl
    abstract = RunPerl demo
    version  = 0.001_001
    [Run::BeforeBuild]
        eval = my $self = shift( @_ );
        eval = my $dist = $self->zilla;
        eval = $self->log( [ '%s v%s', $dist->name, $dist->version ] );
    [GenerateFile/Assa.pm]
        filename = lib/Assa.pm
        content  = package Assa; 1;
    [FakeRelease]

    $ dzil build
    [Run::BeforeBuild] evaluating: my $self = shift( @_ );
    [Run::BeforeBuild] my $dist = $self->zilla;
    [Run::BeforeBuild] $self->log( [ '0.001_001 v', $dist->name, $dist->version ] );
    [Run::BeforeBuild] 0.001_001 v
    [DZ] beginning to build RunPerl
    [DZ] writing RunPerl in RunPerl-0.001_001
    [DZ] building archive with Archive::Tar::Wrapper
    [DZ] writing archive to RunPerl-0.001_001-TRIAL.tar.gz
    [DZ] built in RunPerl-0.001_001

Look at the last message from Run::BeforeBuild plugin. Surprising? Where is the distribution name? Why is the character "v" printed after version number? Ah! %s is a special conversion specifier which was replaced with "something retained for backward compatibility". There is a bunch of other conversion specifiers: %a, %d, %n,%p, %t, %v, %x,… That effectively means I cannot use printf-like functions and hashes, because every percent will be replaced with something or cause error "unknown conversion".

Ok, I can. There is (undocumented!) method to avoid it — every percent sign should be doubled:

    eval = $self->log( [ '%%s v%%s', $dist->name, $dist->version ] );

or

    eval = my %%meta = %%{ $dist->distmeta };

It is simple, but… this is err… not quite Perl. I cannot just cut-n-paste code from a plugin to dist.ini and back.

Let me cite a part of "Philosophy" section of the great Text::Template module:

    When people make a template module like this one, they almost always start by inventing a special syntax for substitutions. For example, they build it so that a string like %%VAR%% is replaced with the value of $VAR. Then they realize the need extra formatting, so they put in some special syntax for formatting. Then they need a loop, so they invent a loop syntax. Pretty soon they have a new little template language.

    This approach has two problems: First, their little language is crippled. If you need to do something the author hasn't thought of, you lose. Second: Who wants to learn another language? You already know Perl, so why not use it?

Look: Run plugin introduced a bunch of dist.ini options: run_if_trial, run_no_trial (BTW, why not run_if_not_trial?), run_if_release, run_no_release, eval, censor_commands, fatal_errors, quiet; a bunch of "conversion specifiers": %a, %d, %n, %p, %v, %t, %x, %s; and bunch of poorly documented rules. It's "a little crippled language", isn't it?

Compared to Run, Hook is designed to be minimalistic: It provides only one option, and it executes only Perl. Of course, when writing a hook you have to keep in mind many rules, but these are well documented Perl rules and (not so well) Dist::Zilla rules, not rules introduced by Hook.

All Run features can be easily implemented with hooks in Perl, for example:

Running external commands:

    . = system( … );

Making errors in external commands fatal:

    . = use autodie ':all';
    . = system( … );

Making errors in Perl code non-fatal:

    . = use Try::Tiny;
    . = try { … };

Checking trial status:

    . = if ( $dist->is_trial ) { … };

Checking release build:

    . = if ( $ENV{ DZIL_RELEASING } ) { … };

The code is a little bit longer than Run counterparts, but it is well-known full-featured Perl.

What if you need to pass to an external command something the Run authors have not thought of? For example, abstract or licence name. There are no conversion specifiers for it, so you lose. But with Hook it is trivial:

    . = system( …, $dist->abstract, …, $dist->license->name, … );

BTW, there are two minor (at the first look) Hook features:

  1. Arguments provided by Dist::Zilla are passes to the hook.

  2. Hook return value is passed back to Dist::Zilla.

These bring a new quality: with Hook you can write inline plugins. For example, a plugin which reads distribution version from an external file:

    [Hook::VersionProvider]
        . = use Path::Tiny; path( 'VERSION' )->slurp;

(Actually, every hook is an inline plugin.) See more in "EXAMPLES" in Dist::Zilla::Plugin::Hook::Manual.

EXAMPLES

Examples below are focused on using Hook, so dist.ini is a primary file in all the examples, and sometimes is the only file of an example. Example module contains single line package Assa; 1; and generated on-the-fly with GenerateFile plugin.

Description Meta Resource

Distribution meta information contains such items as name, version, abstract and many others. All named items are written to META.json (and maybe to META.yml) automatically, all you need is just using MetaJSON and/or MetaYAML plugin(s) in your dist.ini file.

Meta information may also include description — a longer, more complete description of the distribution. However, Dist::Zilla does not provide option to specify description. It could be easily fixed with Hook, though.

dist.ini file:

    name     = Description
    abstract = Hook demo: Set "description" meta info
    version  = v0.0.1
    [Hook::MetaProvider/description]    ; <<<=== Look at this
        ;   MetaProvider's metadata method must return HashRef (or undef).
        ;   Multiple MetaProviders are allowed. Metainfo received from
        ;   all providers will be merged by Dist::Zilla. This
        ;   MetaProvider provides only description.
        ;   See Dist::Zilla::Role::MetaProvider.
        . = { description =>
        . =     "This is not short one-line abstract,
        . =     but more detailed description,
        . =     which spans several lines."
        . = }
    [GenerateFile/Assa.pm]
        filename = lib/Assa.pm
        content  = package Assa; 1;
    [MetaJSON]
    [FakeRelease]

Test::Version adaptive strictness

Test::Version is a great plugin. It creates a test which checks modules in distribution: every module must have $VERSION variable defined, and its value must be a valid version string. There are two notion of "validity": lax and strict. (See "Regular Expressions for Version Parsing" in version::Internals for definitions of lax and strict).

I want to use strict check:

    [Test::Version]
        is_strict = 1

Unfortunately, this does not work for trial releases: any trial release definitely fails the test, because strict check does not allow underscore in version string. Thus, before every trial release I have to reset is_strict option to zero, and return it back to one after release. This is boring and error-prone. I want to have "adaptive strictness": use lax check in case of trial release and strict check otherwise.

Test::Version maintainer Graham Ollis said: This is a good idea! I'll see if I can implement it. However, implementation may take some time. With a little help from Hook, I can easily get achieve adaptive strictness right now.

dist.ini file:

    name     = AdaptiveTestVersion
    abstract = Hook demo: Test::Version adaptive strictness
    version  = 0.001
    [GenerateFile/Assa.pm]
        filename = lib/Assa.pm
        content  = package Assa; 1;
    [Test::Version]                         ; <<<=== Look at this
        is_strict = 0
    [Hook::BeforeBuild/AdaptiveStrictness]  ; <<<=== Look at this
        . = my $tv = $zilla->plugin_named( 'Test::Version' );
        . = $tv->{ is_strict } = $dist->is_trial ? '0' : '1';
    [MetaJSON]
    [FakeRelease]

Template Variables

In a distribution, I have to duplicate the same pieces of information again and again. For example, bug report email and web URLs should be written in [MetaResources] section of dist.ini and in the documentation, like BUGS.pod.

With a help from Templates plugin I can eliminate duplication. If BUGS.pod is a template, I can use email and web URLs defined in dist.ini, e. g.:

    {{$dist->distmeta->{resources}->{bugtracker}->{mailto};}}

Err… This works but requires a lot of typing (so it is typo-prone), and looks ugly. With Hook I can make it not only working, but also elegant. [Hook::Init] section defines few variables in MY package, which can be used in various templates, including documentation and meta resources.

dist.ini file:

    name     = TemplateVariables
    abstract = Hook demo: Define variables for later use in templates.
    version  = v0.0.1
    [Hook::Init/my vars]                ; <<<=== Look at this
        . = package MY;
        . = our $name    = $dist->name;
        . = our $bt_mail = "mailto:bug-$name\@bt.example.org";
        . = our $bt_web  = "https://bt.example.org/display.html?name=$name";
        ;   BTW, Hook::BeforeBuild cannot be used here: it works too late,
        ;   MetaResources::Template will not see the variables.
    [GenerateFile/Assa.pm]
        filename = lib/Assa.pm
        content  = package Assa; 1;
    [GatherDir]
    [PruneCruft]
    [FileFinder::ByName/BUGS.pod]       ; <<<=== Look at this
        file = BUGS.pod
    [Templates]                         ; <<<=== Look at this
        templates = BUGS.pod
    [MetaResources::Template]           ; <<<=== Look at this
        bugtracker.mailto = {{$MY::bt_mail}}
        bugtracker.web    = {{$MY::bt_web}}
        license           = {{$dist->license->url}}
    [MetaJSON]
    [FakeRelease]

BUGS.pod file:

    =head2 Bugs

    The quickest way to report a bug in C<{{$MY::name}}>
    is by sending email to {{$MY::bt_mail}}.

    Bug tracker can be used via
    L<web interface|{{$MY::bt_web}}>.

Version Bumping

I want the version of my distribution is bumped automatically after each release, and automatically assigned version should be trial.

For example: If I released version v0.0.1, the version of the next release should be v0.0.1.1 (see Version::Dotted::Semantic). When I release v0.0.1.1, the next should be v0.0.1.2, the next one — v0.0.1.3 and so on. When I decide it is time to non-trial release, I will set the version to v0.0.2 manually, release it, and will have automatically bumped version v0.0.2.1 for the next release.

This is implemented with three plugins: Hook:VersionProvider, Hook::ReleaseStatusProvider, and Hook::AfterRelease. The first one reads version from external file VERSION which contains only version and nothing more (ok, trailing whitespace is allowed) — it simplifies implementation, because there is no need in parsing dist.ini file. The second plugin lets Dist::Zilla know release status (trial or stable). The third plugin bumps the version after release, and writes bumped version back to the VERSION file.

dist.ini file:

    name     = VersionBumping
    abstract = Hook demo: Bump version after release
    [Hook/prologue]                     ; <<<=== Look at this
        . = use Version::Dotted::Semantic 'qv';
    [Hook::VersionProvider]             ; <<<=== Look at this
        . = $zilla->root->child( 'VERSION' )->slurp =~ s{\s*\z}{}r;
    [Hook::ReleaseStatusProvider]       ; <<<=== Look at this
        . = qv( $zilla->version )->is_trial ? "testing" : "stable";
    [GenerateFile/Assa.pm]
        filename = lib/Assa.pm
        content  = package Assa; 1;
    [MetaJSON]
    [FakeRelease]
    [Hook::AfterRelease/bump version]   ; <<<=== Look at this
        . = my $ver = qv( $dist->version )->bump( 3 );
        . = $zilla->root->child( 'VERSION' )->spew( $ver );
        . = $self->log( [ 'The next release will be %s', "$ver" ] );

VERSION file:

    v0.0.1

Unwanted Dependencies

Data::Printer is a great module, I often use it for debugging. However, sometimes I forget to remove

    use DDP;

from the code and make a release with this unwanted dependency. Hook::BeforeRelease checks the distribution does not have unwanted dependencies. If it does, release will be aborted.

dist.ini file:

    name     = UnwantedDependencies
    abstract = Hook demo: Check the distro does not have unwanted dependencies
    version  = v0.0.1
    [GenerateFile/Assa.pm]
        filename = lib/Assa.pm
        content  = package Assa; use DDP; 1;
    [AutoPrereqs]
    [MetaJSON]
    [Hook::BeforeRelease/unwanted deps] ; <<<=== Look at this
        . = my @modules = qw{ DDP Data::Printer };  # Unwanted modules.
        . = my $prereqs = $dist->distmeta->{ prereqs };
        . = for my $m ( @modules ) {
        . =     for my $s ( qw{ configure develop runtime test } ) {
        . =         if ( exists( $prereqs->{ $s }->{ requires }->{ $m } ) ) {
        . =             $self->log_error( [ '%s found in %s prereqs', $m, $s ] );
        . =         };
        . =     };
        . = };
        . = $self->abort_if_error( 'unwanted dependencies found' );
    [FakeRelease]

Let [=inc::Foo] work in Perl v5.26+.

Starting from Perl v26.0, . is not included into @INC anymore. This breaks Dist::Zilla syntax for plugins which are located in the distribution source tree, e. g.:

    [=inc::Foo]

Being run with Perl v5.26+, dzil complains:

    Required plugin inc::Foo isn't installed.

Dist::Zilla::Plugin::lib was created specially for workarounding this issue. (I said "workarounding" not "solving" because Dist::Zilla::Plugin::lib does not help dzil authordeps --missing to work.)

The same effect can be achieved with Hook::Init one-liner.

dist.ini file:

    name     = NoDotInInc
    abstract = Hook demo: Let [=inc::Foo] work in Perl v5.26+.
    version  = v0.0.1
    [Hook::Init]    ; <<<=== Look at this
        . = use lib $zilla->root->absolute->stringify;
    [=inc::Foo]
    [MetaJSON]
    [FakeRelease]

SEE ALSO

Dist::Zilla
Dist::Zilla::Plugin::Run
Dist::Zilla::Role::Hooker
Dist::Zilla::Role::ErrorLogger
Dist::Zilla::Plugin::Hook

AUTHOR

Van de Bugger <van.de.bugger@gmail.com>

COPYRIGHT AND LICENSE

Copyright (C) 2015, 2016, 2018 Van de Bugger

License GPLv3+: The GNU General Public License version 3 or later <http://www.gnu.org/licenses/gpl-3.0.txt>.

This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.