summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuss Allbery <rra@cpan.org>2019-06-26 12:52:24 -0700
committerRuss Allbery <rra@cpan.org>2019-06-26 12:52:24 -0700
commitca1eb7306bdaaa6524c5796d477ae1e5eb49b43e (patch)
tree8ae9b877397bfa973da1e2ca8fbda4cf95c0643f
parent8582ba05c2de1a8fdd1f59dae0452535abf41297 (diff)
Add docknot dist command
Add new docknot dist command and App::DocKnot::Dist module, which runs appropriate commands to create a distribution tarball.
-rw-r--r--Build.PL19
-rw-r--r--Changes3
-rw-r--r--README2
-rw-r--r--README.md2
-rwxr-xr-xbin/docknot30
-rw-r--r--docs/metadata/requirements2
-rw-r--r--lib/App/DocKnot/Command.pm7
-rw-r--r--lib/App/DocKnot/Dist.pm301
-rwxr-xr-xt/cli/errors.t6
-rwxr-xr-xt/data/dist/Build.PL26
-rw-r--r--t/data/dist/MANIFEST6
-rw-r--r--t/data/dist/MANIFEST.SKIP18
-rw-r--r--t/data/dist/docs/metadata/metadata.json11
-rw-r--r--t/data/dist/lib/Empty.pm57
-rwxr-xr-xt/data/dist/t/api/empty.t.in19
-rw-r--r--t/data/generate/control-archive/metadata/metadata.json1
-rw-r--r--t/data/generate/docknot/output/thread2
-rwxr-xr-xt/dist/basic.t82
-rwxr-xr-xt/dist/commands.t73
19 files changed, 656 insertions, 11 deletions
diff --git a/Build.PL b/Build.PL
index da9557a..d61acc7 100644
--- a/Build.PL
+++ b/Build.PL
@@ -2,7 +2,7 @@
#
# Build script for the docknot application.
#
-# Copyright 2013, 2016, 2018 Russ Allbery <rra@cpan.org>
+# Copyright 2013, 2016, 2018-2019 Russ Allbery <rra@cpan.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
@@ -61,15 +61,16 @@ my $build = Module::Build->new(
},
# Other package relationships.
- configure_requires => { 'Module::Build' => 0.36 },
- test_requires => { 'IPC::System::Simple' => 0 },
+ configure_requires => { 'Module::Build' => 0.36 },
requires => {
- 'File::BaseDir' => 0,
- 'File::ShareDir' => 0,
- 'JSON' => 0,
- 'Perl6::Slurp' => 0,
- 'Template' => 0,
- perl => '5.024',
+ 'File::BaseDir' => 0,
+ 'File::ShareDir' => 0,
+ 'IPC::Run' => 0,
+ 'IPC::System::Simple' => 0,
+ 'JSON' => 0,
+ 'Perl6::Slurp' => 0,
+ 'Template' => 0,
+ perl => '5.024',
},
);
diff --git a/Changes b/Changes
index 78f0576..4159560 100644
--- a/Changes
+++ b/Changes
@@ -5,6 +5,9 @@ DocKnot 3.00 (unreleased)
Separate configuration parsing into a new App::DocKnot::Config module,
used by App::DocKnot::Generate.
+ Add new docknot dist command and App::DocKnot::Dist module, which runs
+ appropriate commands to create a distribution tarball.
+
Move the entry point for command-line commands from App::DocKnot to
App::DocKnot::Command. The App::DocKnot module now only provides some
helper methods to load application data, used by both
diff --git a/README b/README
index 2e33962..42d290a 100644
--- a/README
+++ b/README
@@ -55,6 +55,8 @@ REQUIREMENTS
* File::BaseDir
* File::ShareDir
+ * IPC::Run
+ * IPC::System::Simple
* JSON
* Perl6::Slurp
* Template (part of Template Toolkit)
diff --git a/README.md b/README.md
index 3434917..46defa5 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,8 @@ The following additional Perl modules are required to use it:
* File::BaseDir
* File::ShareDir
+* IPC::Run
+* IPC::System::Simple
* JSON
* Perl6::Slurp
* Template (part of Template Toolkit)
diff --git a/bin/docknot b/bin/docknot
index 957c887..07b0924 100755
--- a/bin/docknot
+++ b/bin/docknot
@@ -22,7 +22,7 @@ __END__
=for stopwords
Allbery DocKnot docknot MERCHANTABILITY NONINFRINGEMENT sublicense subcommand
-subcommands
+subcommands distdir
=head1 NAME
@@ -32,6 +32,8 @@ docknot - Generate human-readable documentation from package metadata
B<docknot> B<-h>
+B<docknot> dist [B<-m> I<metadata>] B<-d> I<distdir>
+
B<docknot> generate [B<-m> I<metadata>] [B<-w> I<width>] I<template> [I<output>]
B<docknot> generate-all [B<-m> I<metadata>] [B<-w> I<width>]
@@ -43,6 +45,12 @@ software packages, organized into subcommands. The supported subcommands are:
=over 4
+=item dist
+
+Build, test, and generate a distribution tarball of the package in the
+current directory. The exact commands used is determined by the package
+metadata (see L<App::DocKnot::Generate> for format documentation).
+
=item generate
Use metadata files and templates to generate human-readable documentation
@@ -71,6 +79,26 @@ Print out usage information and exit.
=back
+=head2 dist
+
+=over 4
+
+=item B<-d> I<distdir>, B<--distdir>=I<distdir>
+
+The directory into which to put the generated distribution tarball. This is
+also used as a working directory for a temporary copy of the package source.
+Required.
+
+=item B<-m> I<metadata>, B<--metadata>=I<metadata>
+
+The path to the metadata files for the package whose distribution tarball is
+being generated. This should be a directory containing all the package
+metadata files required by App::DocKnot. Default: F<docs/metadata> relative
+to the current directory (which is the recommended metadata path for a
+project).
+
+=back
+
=head2 generate
=over 4
diff --git a/docs/metadata/requirements b/docs/metadata/requirements
index 266966e..b618f0c 100644
--- a/docs/metadata/requirements
+++ b/docs/metadata/requirements
@@ -3,6 +3,8 @@ The following additional Perl modules are required to use it:
* File::BaseDir
* File::ShareDir
+* IPC::Run
+* IPC::System::Simple
* JSON
* Perl6::Slurp
* Template (part of Template Toolkit)
diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm
index f7fcf3e..c93e829 100644
--- a/lib/App/DocKnot/Command.pm
+++ b/lib/App/DocKnot/Command.pm
@@ -53,6 +53,13 @@ use Getopt::Long;
# in the option specification for that option). If any of these options
# are not set, an error will be thrown.
our %COMMANDS = (
+ dist => {
+ method => 'make_distribution',
+ module => 'App::DocKnot::Dist',
+ options => ['distdir|d=s', 'metadata|m=s'],
+ maximum => 0,
+ required => ['distdir'],
+ },
generate => {
method => 'generate_output',
module => 'App::DocKnot::Generate',
diff --git a/lib/App/DocKnot/Dist.pm b/lib/App/DocKnot/Dist.pm
new file mode 100644
index 0000000..c852f87
--- /dev/null
+++ b/lib/App/DocKnot/Dist.pm
@@ -0,0 +1,301 @@
+# Generate a distribution tarball for a package.
+#
+# This is the implementation of the docknot dist command, which determines and
+# runs the commands necessary to build a distribution tarball for a given
+# package.
+#
+# SPDX-License-Identifier: MIT
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package App::DocKnot::Dist 2.00;
+
+use 5.024;
+use autodie;
+use warnings;
+
+use App::DocKnot::Config;
+use Carp qw(croak);
+use Cwd qw(getcwd);
+use File::Copy qw(move);
+use File::Path qw(remove_tree);
+use IPC::Run qw(run);
+use IPC::System::Simple qw(systemx);
+
+# Base commands to run for various types of distributions. Additional
+# variations may be added depending on additional configuration parameters.
+#<<<
+our %COMMANDS = (
+ 'Autoconf' => [
+ ['./bootstrap'],
+ ['./configure', 'CC=clang'],
+ ['make', 'clean'],
+ ['make', 'warnings'],
+ ['make', 'check'],
+ ['./configure', 'CC=gcc'],
+ ['make', 'warnings'],
+ ['make', 'check'],
+ ['make', 'clean'],
+ ['make', 'distcheck'],
+ ],
+ 'ExtUtils::MakeMaker' => [
+ ['perl', 'Makefile.PL'],
+ ['make', 'disttest'],
+ ['make', 'dist'],
+ ],
+ 'Module::Build' => [
+ ['perl', 'Build.PL'],
+ ['./Build', 'disttest'],
+ ['./Build', 'dist'],
+ ],
+ 'make' => [
+ ['make', 'dist'],
+ ],
+);
+#>>>
+
+##############################################################################
+# Helper methods
+##############################################################################
+
+# Given a source directory, a prefix for tarballs and related files (such as
+# signatures), and a destination directory, move all matching files from the
+# source directory to the destination directory.
+#
+# $self - The App::DocKnot::Dist object
+# $source_path - The source directory path
+# $prefix - The tarball file prefix
+# $dest_path - The destination directory path
+#
+# Throws: Text exception if no files are found
+# Text exception on failure to move a file
+sub _move_tarballs {
+ my ($self, $source_path, $prefix, $dest_path) = @_;
+
+ # Find all matching files.
+ my $pattern = qr{ \A \Q$prefix\E - \d.* [.]tar [.][xg]z \z }xms;
+ opendir(my $source, $source_path);
+ my @files = grep { $_ =~ $pattern } readdir($source);
+ closedir($source);
+
+ # Move the files.
+ for my $file (@files) {
+ my $source_file = File::Spec->catfile($source_path, $file);
+ move($source_file, $dest_path)
+ or die "cannot move $source_file to $dest_path: $!\n";
+ }
+ return;
+}
+
+##############################################################################
+# Public interface
+##############################################################################
+
+# Create a new App::DocKnot::Dist object, which will be used for subsequent
+# calls.
+#
+# $class - Class of object ot create
+# $args - Anonymous hash of arguments with the following keys:
+# distdir - Path to the directory for distribution tarball
+# metadata - Path to the directory containing package metadata
+#
+# Returns: Newly created object
+# Throws: Text exceptions on invalid metadata directory path
+# Text exception on missing or invalid distdir argument
+sub new {
+ my ($class, $args_ref) = @_;
+
+ # Create the config reader.
+ my %config_args;
+ if ($args_ref->{metadata}) {
+ $config_args{metadata} = $args_ref->{metadata};
+ }
+ my $config = App::DocKnot::Config->new(\%config_args);
+
+ # Ensure we were given a valid distdir argument.
+ my $distdir = $args_ref->{distdir};
+ if (!defined($distdir)) {
+ croak('distdir path not given');
+ } elsif (!-d $distdir) {
+ croak("distdir path $distdir does not exist or is not a directory");
+ }
+
+ # Create and return the object.
+ my $self = {
+ config => $config->config(),
+ distdir => $distdir,
+ };
+ bless($self, $class);
+ return $self;
+}
+
+# Analyze a source directory and return the list of commands to run to
+# generate a distribution tarball.
+#
+# $self - The App::DocKnot::Dist object
+#
+# Returns: List of commands, each of which is a list of strings representing
+# a command and its arguments
+sub commands {
+ my ($self) = @_;
+ return $COMMANDS{ $self->{config}{build}{type} };
+}
+
+# Generate a distribution tarball. This assumes it is run from the root
+# directory of the package to release and that it is a Git repository. It
+# exports the Git repository, runs the commands to generate the tarball, and
+# then removes the working tree.
+#
+# $self - The App::DocKnot::Dist object
+#
+# Throws: Text exception if any of the commands fail
+sub make_distribution {
+ my ($self) = @_;
+
+ # Export the Git repository into a new directory.
+ my $source = getcwd() or die "cannot get current directory: $!\n";
+ my $prefix = $self->{config}{distribution}{tarname};
+ my @git = ('git', 'archive', "--remote=$source", "--prefix=${prefix}/",
+ 'master',);
+ my @tar = qw(tar xf -);
+ chdir($self->{distdir});
+ run(\@git, q{|}, \@tar) or die "@git | @tar failed with status $?\n";
+
+ # Change to that directory and run the configured commands.
+ chdir($prefix);
+ for my $command_ref ($self->commands()->@*) {
+ systemx($command_ref->@*);
+ }
+
+ # Move the generated tarball to the parent directory.
+ $self->_move_tarballs(File::Spec->curdir(), $prefix, File::Spec->updir());
+
+ # Remove the working tree.
+ chdir(File::Spec->updir());
+ remove_tree($prefix, { safe => 1 });
+ return;
+}
+
+##############################################################################
+# Module return value and documentation
+##############################################################################
+
+1;
+__END__
+
+=for stopwords
+Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT sublicense JSON CPAN ARGS
+distdir
+
+=head1 NAME
+
+App::DocKnot::Dist - Prepare a distribution tarball
+
+=head1 SYNOPSIS
+
+ use App::DocKnot::Dist;
+ my $docknot = App::DocKnot::Dist->new({ distdir => '/path/to/dist' });
+ $docknot->make_distribution();
+
+=head1 REQUIREMENTS
+
+Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, IPC::Run,
+IPC::System::Simple, JSON, and Perl6::Slurp, all of which are available from
+CPAN.
+
+=head1 DESCRIPTION
+
+This component of DocKnot generates distribution tarballs for a package. This
+is a bit of an odd inclusion in the DocKnot suite, since it's not about
+generating documentation, but it uses the same configuration and metadata as
+the rest of DocKnot.
+
+Specifically, App::DocKnot::Dist exports the current branch from Git into a
+separate working directory, runs the commands appropriate to create a
+distribution (based on the build system configured in the package metadata),
+and cleans up the working directory.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item new(ARGS)
+
+Create a new App::DocKnot::Dist object. This should be used for all
+subsequent actions. ARGS should be a hash reference with one or more of the
+following keys:
+
+=over 4
+
+=item distdir
+
+The path to the directory into which to put the distribution tarball.
+Required.
+
+=item metadata
+
+The path to the directory containing metadata for a package. Default:
+F<docs/metadata> relative to the current directory.
+
+=back
+
+=back
+
+=head1 INSTANCE METHODS
+
+=over 4
+
+=item commands()
+
+Return the commands that should be run to generate a distribution tarball as a
+reference to an array of arrays. Each included array is a single command.
+This method is provided primarily for testing convenience and is normally just
+an implementation detail of make_distribution().
+
+=item make_distribution()
+
+Generate a distribution tarball in the C<destdir> directory provided to new().
+
+=back
+
+=head1 AUTHOR
+
+Russ Allbery <rra@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2019 Russ Allbery <rra@cpan.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=head1 SEE ALSO
+
+L<docknot(1)>
+
+This module is part of the App-DocKnot distribution. The current version of
+App::DocKnot is available from CPAN, or directly from its web site at
+L<https://www.eyrie.org/~eagle/software/docknot/>.
+
+=cut
+
+# Local Variables:
+# copyright-at-end-flag: t
+# End:
diff --git a/t/cli/errors.t b/t/cli/errors.t
index fc0561e..0943e54 100755
--- a/t/cli/errors.t
+++ b/t/cli/errors.t
@@ -10,7 +10,7 @@ use 5.024;
use autodie;
use warnings;
-use Test::More tests => 10;
+use Test::More tests => 11;
# Load the module.
BEGIN { use_ok('App::DocKnot::Command') }
@@ -60,3 +60,7 @@ eval { $docknot->run('generate', '-m', '/nonexistent', 'readme') };
is_error($@,
'generate: metadata path /nonexistent does not exist or is not a directory'
);
+
+# Check for a missing required argument.
+eval { $docknot->run('dist') };
+is_error($@, 'dist: missing required option --distdir');
diff --git a/t/data/dist/Build.PL b/t/data/dist/Build.PL
new file mode 100755
index 0000000..5d3635e
--- /dev/null
+++ b/t/data/dist/Build.PL
@@ -0,0 +1,26 @@
+#!/usr/bin/perl
+#
+# Build script for an empty module.
+#
+# Used to test constructing distributions for Perl modules. This is a minimum
+# build script, just enough to support the dist action and let tests run.
+#
+# Copyright 2018-2019 Russ Allbery <rra@cpan.org>
+#
+# SPDX-License-Identifier: MIT
+
+use strict;
+use warnings;
+
+use Module::Build;
+
+my $build = Module::Build->new(
+ dist_abstract => 'Empty test module',
+ dist_author => 'Russ Allbery <rra@cpan.org>',
+ license => 'mit',
+ module_name => 'Empty',
+ recursive_test_files => 1,
+ add_to_cleanup => [qw(MANIFEST.bak MYMETA.json.lock cover_db)],
+ configure_requires => { 'Module::Build' => 0.36 },
+);
+$build->create_build_script;
diff --git a/t/data/dist/MANIFEST b/t/data/dist/MANIFEST
new file mode 100644
index 0000000..ab9b270
--- /dev/null
+++ b/t/data/dist/MANIFEST
@@ -0,0 +1,6 @@
+Build.PL
+docs/metadata/metadata.json
+lib/Empty.pm
+MANIFEST This list of files
+MANIFEST.SKIP
+t/api/empty.t
diff --git a/t/data/dist/MANIFEST.SKIP b/t/data/dist/MANIFEST.SKIP
new file mode 100644
index 0000000..f24f967
--- /dev/null
+++ b/t/data/dist/MANIFEST.SKIP
@@ -0,0 +1,18 @@
+# -*- conf -*-
+
+# Avoid generated build files.
+\bblib/
+
+# Avoid Module::Build generated and utility files.
+\bBuild$
+\b_build/
+\bBuild.bat$
+\bBuild.COM$
+\bBUILD.COM$
+\bbuild.com$
+
+# Avoid MYMETA files
+^MYMETA\.
+
+# Avoid archives of this distribution
+\bEmpty-[\d\.\_]+
diff --git a/t/data/dist/docs/metadata/metadata.json b/t/data/dist/docs/metadata/metadata.json
new file mode 100644
index 0000000..8dff14e
--- /dev/null
+++ b/t/data/dist/docs/metadata/metadata.json
@@ -0,0 +1,11 @@
+{
+ "name": "Empty",
+ "version": "1.00",
+ "license": "Expat",
+ "build": {
+ "type": "Module::Build",
+ },
+ "distribution": {
+ "tarname": "Empty",
+ },
+}
diff --git a/t/data/dist/lib/Empty.pm b/t/data/dist/lib/Empty.pm
new file mode 100644
index 0000000..7f75c8f
--- /dev/null
+++ b/t/data/dist/lib/Empty.pm
@@ -0,0 +1,57 @@
+# Empty module for test purposes.
+#
+# This module exists only to trigger the test suite, contain some
+# documentation, and be something to distribute.
+#
+# Copyright 2018-2019 Russ Allbery <rra@cpan.org>
+#
+# SPDX-License-Identifier: MIT
+
+package Empty;
+
+use 5.006;
+use strict;
+use warnings;
+
+# Declare variables that should be set in BEGIN for robustness.
+our $VERSION = '1.00';
+
+# Empty function for testing purposes.
+sub empty_function {
+ return 42;
+}
+
+1;
+__END__
+
+=for stopwords
+Allbery
+
+=head1 NAME
+
+Empty - Empty module for test purposes
+
+=head1 SYNOPSIS
+
+ use Empty;
+
+=head1 DESCRIPTION
+
+An empty module that does nothing, used only for test and example purposes.
+It's intended to be just enough of a Perl module to create a distribution.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item empty_function
+
+Returns 42.
+
+=back
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/t/data/dist/t/api/empty.t.in b/t/data/dist/t/api/empty.t.in
new file mode 100755
index 0000000..8c8ec08
--- /dev/null
+++ b/t/data/dist/t/api/empty.t.in
@@ -0,0 +1,19 @@
+#!/bin/perl
+#
+# Tiny test solely to get coverage information and let tests pass.
+#
+# Copyright 2018-2019 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: MIT
+
+use 5.006;
+use strict;
+use warnings;
+
+use Test::More tests => 2;
+
+# Load the module.
+BEGIN { use_ok('Empty') }
+
+# Test the function.
+is(Empty::empty_function(), 42, 'test_function');
diff --git a/t/data/generate/control-archive/metadata/metadata.json b/t/data/generate/control-archive/metadata/metadata.json
index 58a041e..dd20003 100644
--- a/t/data/generate/control-archive/metadata/metadata.json
+++ b/t/data/generate/control-archive/metadata/metadata.json
@@ -20,6 +20,7 @@
"license": "Expat",
"build": {
"install": false,
+ "type": "make",
},
"support": {
"email": "eagle@eyrie.org",
diff --git a/t/data/generate/docknot/output/thread b/t/data/generate/docknot/output/thread
index ae2bae5..044b7ed 100644
--- a/t/data/generate/docknot/output/thread
+++ b/t/data/generate/docknot/output/thread
@@ -101,6 +101,8 @@ The following additional Perl modules are required to use it:
\bullet(packed)[File::BaseDir]
\bullet(packed)[File::ShareDir]
+\bullet(packed)[IPC::Run]
+\bullet(packed)[IPC::System::Simple]
\bullet(packed)[JSON]
\bullet(packed)[Perl6::Slurp]
\bullet(packed)[Template (part of Template Toolkit)]
diff --git a/t/dist/basic.t b/t/dist/basic.t
new file mode 100755
index 0000000..3bd119f
--- /dev/null
+++ b/t/dist/basic.t
@@ -0,0 +1,82 @@
+#!/usr/bin/perl
+#
+# Basic tests for App::DocKnot::Dist.
+#
+# Copyright 2019 Russ Allbery <rra@cpan.org>
+#
+# SPDX-License-Identifier: MIT
+
+use 5.024;
+use autodie;
+use warnings;
+
+use Cwd qw(getcwd);
+use File::Copy;
+use File::Spec;
+use File::Temp;
+use IPC::Run qw(run);
+use IPC::System::Simple qw(systemx);
+
+use Test::More;
+
+# Find the full path to the test data.
+my $cwd = getcwd() or die "$0: cannot get working directory: $!\n";
+my $dataroot = File::Spec->catfile($cwd, 't', 'data', 'dist');
+
+# Set up a temporary directory, copy all files from the data directory, and
+# commit them. We have to rename the test while we copy it to avoid having it
+# picked up by the main package test suite.
+my $dir = File::Temp->newdir();
+chdir($dir);
+systemx(qw(git init source));
+chdir('source');
+for my $file (qw(Build.PL MANIFEST MANIFEST.SKIP)) {
+ copy(File::Spec->catfile($dataroot, $file), File::Spec->curdir())
+ or die "$0: cannot copy $file: $!\n";
+}
+mkdir('docs');
+mkdir(File::Spec->catfile('docs', 'metadata'));
+my $metadata_path
+ = File::Spec->catfile($dataroot, 'docs', 'metadata', 'metadata.json');
+copy($metadata_path, File::Spec->catfile('docs', 'metadata'))
+ or die "$0: cannot copy docs/metadata/metadata.json: $!\n";
+for my $file (qw(blurb description requirements)) {
+ open(my $fh, '>', File::Spec->catfile('docs', 'metadata', $file));
+ close($fh);
+}
+mkdir('lib');
+copy(File::Spec->catfile($dataroot, 'lib', 'Empty.pm'), 'lib')
+ or die "$0: cannot copy lib/Empty.pm: $!\n";
+mkdir('t');
+my $testdir = File::Spec->catfile('t', 'api');
+mkdir($testdir);
+my $test_path = File::Spec->catfile($dataroot, 't', 'api', 'empty.t.in');
+copy($test_path, File::Spec->catfile($testdir, 'empty.t'))
+ or die "$0: cannot copy t/api/empty.t: $!\n";
+systemx(qw(git add -A .));
+systemx(qw(git commit -m Initial));
+
+# Check whether we have all the necessary tools to set up the test. This test
+# relies on the external git and tar utilities, which may not be available on
+# all systems.
+if (!run(['git', 'archive', 'HEAD'], q{|}, ['tar', 'tf', q{-}])) {
+ plan skip_all => 'git and tar not available';
+} else {
+ plan tests => 2;
+}
+
+# Load the module. Change back to the starting directory for this so that
+# coverage analysis works.
+chdir($cwd);
+require_ok('App::DocKnot::Dist');
+chdir(File::Spec->catfile($dir, 'source'));
+
+# Setup finished. Now we can create a distribution tarball.
+my $distdir = File::Spec->catfile($dir, 'dist');
+mkdir($distdir);
+my $dist = App::DocKnot::Dist->new({ distdir => $distdir });
+$dist->make_distribution();
+ok(-f File::Spec->catfile($distdir, 'Empty-1.00.tar.gz'), 'dist exists');
+
+# Change directories so that the temporary directory can be cleaned up.
+chdir($cwd);
diff --git a/t/dist/commands.t b/t/dist/commands.t
new file mode 100755
index 0000000..e7a77cb
--- /dev/null
+++ b/t/dist/commands.t
@@ -0,0 +1,73 @@
+#!/usr/bin/perl
+#
+# Tests for App::DocKnot::Dist command selection to generate a distribution.
+#
+# Copyright 2019 Russ Allbery <rra@cpan.org>
+#
+# SPDX-License-Identifier: MIT
+
+use 5.024;
+use autodie;
+use warnings;
+
+use File::Spec;
+
+use Test::More tests => 5;
+
+# Load the module.
+BEGIN { use_ok('App::DocKnot::Dist') }
+
+# Use the same test cases that we use for generate, since they represent the
+# same variety of build systems.
+my $dataroot = File::Spec->catfile('t', 'data', 'generate');
+
+# Module::Build distribution (use App::DocKnot itself and default paths).
+my $docknot = App::DocKnot::Dist->new({ distdir => q{.} });
+#<<<
+my $expected = [
+ ['perl', 'Build.PL'],
+ ['./Build', 'disttest'],
+ ['./Build', 'dist'],
+];
+#>>>
+is_deeply($docknot->commands(), $expected, 'Module::Build');
+
+# ExtUtils::MakeMaker distribution.
+my $metadata_path = File::Spec->catfile($dataroot, 'ansicolor', 'metadata');
+$docknot
+ = App::DocKnot::Dist->new({ distdir => q{.}, metadata => $metadata_path });
+#<<<
+$expected = [
+ ['perl', 'Makefile.PL'],
+ ['make', 'disttest'],
+ ['make', 'dist'],
+];
+#>>>
+is_deeply($docknot->commands(), $expected, 'ExtUtils::MakeMaker');
+
+# Autoconf distribution.
+$metadata_path = File::Spec->catfile($dataroot, 'c-tap-harness', 'metadata');
+$docknot
+ = App::DocKnot::Dist->new({ distdir => q{.}, metadata => $metadata_path });
+#<<<
+$expected = [
+ ['./bootstrap'],
+ ['./configure', 'CC=clang'],
+ ['make', 'clean'],
+ ['make', 'warnings'],
+ ['make', 'check'],
+ ['./configure', 'CC=gcc'],
+ ['make', 'warnings'],
+ ['make', 'check'],
+ ['make', 'clean'],
+ ['make', 'distcheck'],
+];
+#>>>
+is_deeply($docknot->commands(), $expected, 'Autoconf');
+
+# Makefile only distribution (make).
+$metadata_path = File::Spec->catfile($dataroot, 'control-archive', 'metadata');
+$docknot
+ = App::DocKnot::Dist->new({ distdir => q{.}, metadata => $metadata_path });
+$expected = [['make', 'dist']];
+is_deeply($docknot->commands(), $expected, 'make');