File Coverage

blib/lib/App/DocKnot/Dist.pm
Criterion Covered Total %
statement 82 84 97.6
branch 15 20 75.0
condition 6 6 100.0
subroutine 15 15 100.0
pod 3 3 100.0
total 121 128 94.5


line stmt bran cond sub pod time code
1             # Generate a distribution tarball for a package.
2             #
3             # This is the implementation of the docknot dist command, which determines and
4             # runs the commands necessary to build a distribution tarball for a given
5             # package.
6             #
7             # SPDX-License-Identifier: MIT
8              
9             ##############################################################################
10             # Modules and declarations
11             ##############################################################################
12              
13             package App::DocKnot::Dist 3.01;
14              
15 4     4   137225 use 5.024;
  4         27  
16 4     4   21 use autodie;
  4         8  
  4         37  
17 4     4   18105 use warnings;
  4         8  
  4         142  
18              
19 4     4   1730 use App::DocKnot::Config;
  4         11  
  4         131  
20 4     4   21 use Carp qw(croak);
  4         7  
  4         164  
21 4     4   19 use Cwd qw(getcwd);
  4         7  
  4         158  
22 4     4   1199 use File::Copy qw(move);
  4         5537  
  4         260  
23 4     4   24 use File::Path qw(remove_tree);
  4         7  
  4         494  
24 4     4   2404 use IPC::Run qw(run);
  4         88649  
  4         214  
25 4     4   1364 use IPC::System::Simple qw(systemx);
  4         14655  
  4         3509  
26              
27             # Base commands to run for various types of distributions. Additional
28             # variations may be added depending on additional configuration parameters.
29             #<<<
30             our %COMMANDS = (
31             'Autoconf' => [
32             ['./bootstrap'],
33             ['./configure', 'CC=clang'],
34             ['make', 'warnings'],
35             ['make', 'check'],
36             ['make', 'clean'],
37             ['./configure', 'CC=gcc'],
38             ['make', 'warnings'],
39             ['make', 'check'],
40             ['make', 'clean'],
41             ['make', 'check-cppcheck'],
42             ['make', 'distcheck'],
43             ],
44             'ExtUtils::MakeMaker' => [
45             ['perl', 'Makefile.PL'],
46             ['make', 'disttest'],
47             ['make', 'dist'],
48             ],
49             'Module::Build' => [
50             ['perl', 'Build.PL'],
51             ['./Build', 'disttest'],
52             ['./Build', 'dist'],
53             ],
54             'make' => [
55             ['make', 'dist'],
56             ],
57             );
58             #>>>
59              
60             ##############################################################################
61             # Helper methods
62             ##############################################################################
63              
64             # Given a source directory, a prefix for tarballs and related files (such as
65             # signatures), and a destination directory, move all matching files from the
66             # source directory to the destination directory.
67             #
68             # $self - The App::DocKnot::Dist object
69             # $source_path - The source directory path
70             # $prefix - The tarball file prefix
71             # $dest_path - The destination directory path
72             #
73             # Throws: Text exception if no files are found
74             # Text exception on failure to move a file
75             sub _move_tarballs {
76 1     1   14 my ($self, $source_path, $prefix, $dest_path) = @_;
77              
78             # Find all matching files.
79 1         97 my $pattern = qr{ \A \Q$prefix\E - \d.* [.]tar [.][xg]z \z }xms;
80 1         21 opendir(my $source, $source_path);
81 1         1272 my @files = grep { $_ =~ $pattern } readdir($source);
  15         46  
82 1         7 closedir($source);
83              
84             # Move the files.
85 1         487 for my $file (@files) {
86 1         23 my $source_file = File::Spec->catfile($source_path, $file);
87 1 50       22 move($source_file, $dest_path)
88             or die "cannot move $source_file to $dest_path: $!\n";
89             }
90 1         438 return;
91             }
92              
93             # Given a command with arguments, replace a command of "perl" with the
94             # configured path to Perl, if any. Assumes that the perl configuration
95             # parameter is set in the object and should not be called if this is not true.
96             #
97             # $self - The App::DocKnot::Dist object
98             # $command_ref - Reference to an array representing a command with arguments
99             #
100             # Returns: Reference to an array representing a command with arguments, with
101             # the command replaced with the configured path to Perl if it was
102             # "perl"
103             sub _replace_perl_path {
104 6     6   104 my ($self, $command_ref) = @_;
105 6 100       22 if ($command_ref->[0] ne 'perl') {
106 4         11 return $command_ref;
107             }
108 2         10 my @command = $command_ref->@*;
109 2         8 $command[0] = $self->{perl};
110 2         28 return [@command];
111             }
112              
113             ##############################################################################
114             # Public interface
115             ##############################################################################
116              
117             # Create a new App::DocKnot::Dist object, which will be used for subsequent
118             # calls.
119             #
120             # $class - Class of object ot create
121             # $args - Anonymous hash of arguments with the following keys:
122             # distdir - Path to the directory for distribution tarball
123             # metadata - Path to the directory containing package metadata
124             # perl - Path to Perl to use (default: search the user's PATH)
125             #
126             # Returns: Newly created object
127             # Throws: Text exceptions on invalid metadata directory path
128             # Text exception on missing or invalid distdir argument
129             sub new {
130 7     7 1 12685 my ($class, $args_ref) = @_;
131              
132             # Create the config reader.
133 7         11 my %config_args;
134 7 100       22 if ($args_ref->{metadata}) {
135 4         7 $config_args{metadata} = $args_ref->{metadata};
136             }
137 7         35 my $config = App::DocKnot::Config->new(\%config_args);
138              
139             # Ensure we were given a valid distdir argument.
140 7         13 my $distdir = $args_ref->{distdir};
141 7 50       70 if (!defined($distdir)) {
    50          
142 0         0 croak('distdir path not given');
143             } elsif (!-d $distdir) {
144 0         0 croak("distdir path $distdir does not exist or is not a directory");
145             }
146              
147             # Create and return the object.
148             my $self = {
149             config => $config->config(),
150             distdir => $distdir,
151             perl => $args_ref->{perl},
152 7         31 };
153 7         26 bless($self, $class);
154 7         96 return $self;
155             }
156              
157             # Analyze a source directory and return the list of commands to run to
158             # generate a distribution tarball.
159             #
160             # $self - The App::DocKnot::Dist object
161             #
162             # Returns: List of commands, each of which is a list of strings representing
163             # a command and its arguments
164             sub commands {
165 7     7 1 76 my ($self) = @_;
166 7         29 my $type = $self->{config}{build}{type};
167 7         26 my @commands = map { [@$_] } $COMMANDS{$type}->@*;
  35         88  
168              
169             # Special-case: If a specific path to Perl was configured, use that path
170             # rather than searching for perl in the user's PATH. This is used
171             # primarily by the test suite, which wants to run a Module::Build Build.PL
172             # and thus has to use the same perl binary as the one running the tests.
173 7 100       28 if (defined($self->{perl})) {
174 2         6 @commands = map { $self->_replace_perl_path($_) } @commands;
  6         27  
175             }
176              
177             # Special-case: Autoconf packages with C++ support should also attempt a
178             # build with a C++ compiler.
179 7 100 100     26 if ($type eq 'Autoconf' && $self->{config}{build}{cplusplus}) {
180             #<<<
181 1         14 my @extra = (
182             ['./configure', 'CC=g++'],
183             ['make', 'warnings'],
184             ['make', 'check'],
185             ['make', 'clean'],
186             );
187             #>>>
188 1         4 splice(@commands, 1, 0, @extra);
189             }
190              
191             # Special-case: Autoconf packages with Valgrind support should also run
192             # make check-valgrind.
193 7 100 100     20 if ($type eq 'Autoconf' && $self->{config}{build}{valgrind}) {
194 1         10 splice(@commands, -3, 0, ['make', 'check-valgrind']);
195             }
196              
197 7         31 return @commands;
198             }
199              
200             # Generate a distribution tarball. This assumes it is run from the root
201             # directory of the package to release and that it is a Git repository. It
202             # exports the Git repository, runs the commands to generate the tarball, and
203             # then removes the working tree.
204             #
205             # $self - The App::DocKnot::Dist object
206             #
207             # Throws: Text exception if any of the commands fail
208             sub make_distribution {
209 1     1 1 1044 my ($self) = @_;
210              
211             # Export the Git repository into a new directory.
212 1 50       12 my $source = getcwd() or die "cannot get current directory: $!\n";
213 1         15 my $prefix = $self->{config}{distribution}{tarname};
214 1         6 my @git = ('git', 'archive', "--remote=$source", "--prefix=${prefix}/",
215             'master',);
216 1         3 my @tar = qw(tar xf -);
217 1         6 chdir($self->{distdir});
218 1 50       60 run(\@git, q{|}, \@tar) or die "@git | @tar failed with status $?\n";
219              
220             # Change to that directory and run the configured commands.
221 1         33473 chdir($prefix);
222 1         154 for my $command_ref ($self->commands()) {
223 3         1431846 systemx($command_ref->@*);
224             }
225              
226             # Move the generated tarball to the parent directory.
227 1         258710 $self->_move_tarballs(File::Spec->curdir(), $prefix, File::Spec->updir());
228              
229             # Remove the working tree.
230 1         8 chdir(File::Spec->updir());
231 1         2612 remove_tree($prefix, { safe => 1 });
232 1         28 return;
233             }
234              
235             ##############################################################################
236             # Module return value and documentation
237             ##############################################################################
238              
239             1;
240             __END__