diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/App/DocKnot.pm | 12 | ||||
-rw-r--r-- | lib/App/DocKnot/Command.pm | 3 | ||||
-rw-r--r-- | lib/App/DocKnot/Config.pm | 11 | ||||
-rw-r--r-- | lib/App/DocKnot/Dist.pm | 117 | ||||
-rw-r--r-- | lib/App/DocKnot/Generate.pm | 26 | ||||
-rw-r--r-- | lib/App/DocKnot/Release.pm | 14 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin.pm | 52 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Pointer.pm | 28 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/RSS.pm | 234 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Sitemap.pm | 48 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Thread.pm | 23 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Versions.pm | 9 | ||||
-rw-r--r-- | lib/App/DocKnot/Update.pm | 2 | ||||
-rw-r--r-- | lib/App/DocKnot/Util.pm | 42 |
14 files changed, 315 insertions, 306 deletions
diff --git a/lib/App/DocKnot.pm b/lib/App/DocKnot.pm index c288ed9..947b9d2 100644 --- a/lib/App/DocKnot.pm +++ b/lib/App/DocKnot.pm @@ -11,7 +11,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot 6.01; +package App::DocKnot 7.00; use 5.024; use autodie; @@ -19,8 +19,8 @@ use warnings; use File::BaseDir qw(config_files); use File::ShareDir qw(module_file); -use File::Spec; use Kwalify qw(validate); +use Path::Tiny qw(path); use YAML::XS (); ############################################################################## @@ -50,7 +50,7 @@ sub appdata_path { # If that doesn't work, use the data that came with the module. if (!defined($path)) { - $path = module_file('App::DocKnot', File::Spec->catfile(@path)); + $path = module_file('App::DocKnot', path(@path)->stringify()); } return $path; } @@ -113,8 +113,8 @@ App::DocKnot - Documentation and software release management =head1 REQUIREMENTS -Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, Kwalify, and -YAML::XS, all of which are available from CPAN. +Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, Kwalify, +Path::Tiny, and YAML::XS, all of which are available from CPAN. =head1 DESCRIPTION @@ -154,7 +154,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 2013-2021 Russ Allbery <rra@cpan.org> +Copyright 2013-2022 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 diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm index f032ab6..a0fa90c 100644 --- a/lib/App/DocKnot/Command.pm +++ b/lib/App/DocKnot/Command.pm @@ -10,7 +10,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Command 6.01; +package App::DocKnot::Command 7.00; use 5.024; use autodie; @@ -18,6 +18,7 @@ use warnings; use App::DocKnot::Dist; use App::DocKnot::Generate; +use App::DocKnot::Release; use App::DocKnot::Spin; use App::DocKnot::Spin::RSS; use App::DocKnot::Spin::Thread; diff --git a/lib/App/DocKnot/Config.pm b/lib/App/DocKnot/Config.pm index 6b6716d..597e6cb 100644 --- a/lib/App/DocKnot/Config.pm +++ b/lib/App/DocKnot/Config.pm @@ -9,7 +9,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Config 6.01; +package App::DocKnot::Config 7.00; use 5.024; use autodie; @@ -195,6 +195,12 @@ release> is mandatory. Sign distribution tarballs generated via C<docknot dist> with this PGP key. Equivalent to the B<-p> option to C<docknot dist>. +=item versions + +Path to the F<.versions> file that should be updated by C<docknot release>. A +F<.versions> file records the versions and release dates of software packages. +See L<App::Docknot::Spin::Versions> for more information. + =back =head1 CLASS METHODS @@ -261,7 +267,8 @@ SOFTWARE. =head1 SEE ALSO -L<docknot(1)> +L<docknot(1)>, L<App::DocKnot::Dist>, L<App:DocKnot::Release>, +L<App::DocKnot::Spin::Versions> This module is part of the App-DocKnot distribution. The current version of DocKnot is available from CPAN, or directly from its web site at diff --git a/lib/App/DocKnot/Dist.pm b/lib/App/DocKnot/Dist.pm index ccbeff4..585d243 100644 --- a/lib/App/DocKnot/Dist.pm +++ b/lib/App/DocKnot/Dist.pm @@ -10,19 +10,17 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Dist 6.01; +package App::DocKnot::Dist 7.00; use 5.024; use autodie; use warnings; use App::DocKnot::Config; +use App::DocKnot::Util qw(latest_tarball print_checked); use Archive::Tar (); use Carp qw(croak); -use Cwd qw(getcwd); -use File::Copy qw(move); use File::Find qw(find); -use File::Path qw(remove_tree); use Git::Repository (); use IO::Compress::Xz (); use IO::Uncompress::Gunzip (); @@ -30,6 +28,7 @@ use IPC::Run qw(run); use IPC::System::Simple qw(systemx); use List::SomeUtils qw(lastval); use List::Util qw(any first); +use Path::Tiny qw(path); # Base commands to run for various types of distributions. Additional # variations may be added depending on additional configuration parameters. @@ -113,6 +112,11 @@ sub _expected_dist_files { # Find all files in the source directory, stripping its path from the file # name and excluding (and pruning) anything matching @DIST_IGNORE or in # the distribution/ignore key of the package configuration. + # + # This uses File::Find rather than Path::Iterator::Rule like other parts + # of DocKnot because the ignore patterns are based on the whole path + # relative to the top of the distribution, and that's more annoying to do + # with Path::Iterator::Rule. my $wanted = sub { my $name = $File::Find::name; $name =~ s{ \A \Q$path\E / }{}xms; @@ -126,7 +130,7 @@ sub _expected_dist_files { }; # Generate and return the list of files. - find($wanted, $path); + find($wanted, "$path"); return @files; } @@ -135,31 +139,16 @@ sub _expected_dist_files { # $path - The directory path # $prefix - The tarball file prefix # -# Returns: The full path to the gzip tarball +# Returns: The path to the gzip tarball # Throws: Text exception if no gzip tarball was found sub _find_gzip_tarball { my ($self, $path, $prefix) = @_; - my @files = $self->_find_matching_tarballs($path, $prefix); - my $gzip_file = lastval { m{ [.]tar [.]gz \z }xms } @files; + my $files_ref = latest_tarball($path, $prefix)->{files}; + my $gzip_file = lastval { m{ [.]tar [.]gz \z }xms } $files_ref->@*; if (!defined($gzip_file)) { die "cannot find gzip tarball for $prefix in $path\n"; } - return File::Spec->catfile($path, $gzip_file); -} - -# Find matching tarballs given a directory and a prefix. -# -# $path - The directory path -# $prefix - The tarball file prefix -# -# Returns: All matching files, without the directory name, as a list -sub _find_matching_tarballs { - my ($self, $path, $prefix) = @_; - my $pattern = qr{ \A \Q$prefix\E - \d.* [.]tar [.][xg]z \z }xms; - opendir(my $source, $path); - my @files = grep { $_ =~ $pattern } readdir($source); - closedir($source); - return @files; + return $path->child($gzip_file); } # Given a directory and a prefix for tarballs in that directory, ensure that @@ -172,17 +161,15 @@ sub _find_matching_tarballs { # Throws: Text exception on failure to read or write compressed files. sub _generate_compression_formats { my ($self, $path, $prefix) = @_; - my @files = $self->_find_matching_tarballs($path, $prefix); - if (!any { m{ [.]tar [.]xz \z }xms } @files) { - my $gzip_file = lastval { m{ [.]tar [.]gz \z }xms } @files; - my $xz_file = $gzip_file; - $xz_file =~ s{ [.]gz \z }{.xz}xms; - my $gzip_path = File::Spec->catfile($path, $gzip_file); - my $xz_path = File::Spec->catfile($path, $xz_file); + my $files_ref = latest_tarball($path, $prefix)->{files}; + if (!any { m{ [.]tar [.]xz \z }xms } $files_ref->@*) { + my $gzip_file = lastval { m{ [.]tar [.]gz \z }xms } $files_ref->@*; + my $gzip_path = $path->child($gzip_file); + my $xz_path = $path->child($gzip_path->basename('.gz') . '.xz'); # Open the input and output files. - my $gzip_fh = IO::Uncompress::Gunzip->new($gzip_path); - my $xz_fh = IO::Compress::Xz->new($xz_path); + my $gzip_fh = IO::Uncompress::Gunzip->new("$gzip_path"); + my $xz_fh = IO::Compress::Xz->new("$xz_path"); # Read from the gzip file and write to the xz-compressed file. my $buffer; @@ -207,11 +194,9 @@ sub _generate_compression_formats { # Text exception on failure to move a file sub _move_tarballs { my ($self, $source_path, $prefix, $dest_path) = @_; - my @files = $self->_find_matching_tarballs($source_path, $prefix); - 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"; + my $files_ref = latest_tarball($source_path, $prefix)->{files}; + for my $file ($files_ref->@*) { + $source_path->child($file)->move($dest_path->child($file)); } return; } @@ -244,9 +229,13 @@ sub _replace_perl_path { # Throws: Text exception on failure to sign the file sub _sign_tarballs { my ($self, $path, $prefix) = @_; - my @files = $self->_find_matching_tarballs($path, $prefix); - for my $file (@files) { - my $tarball_path = File::Spec->catdir($path, $file); + my $files_ref = latest_tarball($path, $prefix)->{files}; + for my $file (grep { m{ [.]tar [.][xg]z }xms } $files_ref->@*) { + my $tarball_path = $path->child($file); + my $sig_path = $path->child($tarball_path->basename() . '.asc'); + if ($sig_path->exists()) { + $sig_path->remove(); + } systemx( $self->{gpg}, '--detach-sign', '--armor', '-u', $self->{pgp_key}, $tarball_path, @@ -296,7 +285,7 @@ sub new { #<<< my $self = { config => $config_reader->config(), - distdir => $distdir, + distdir => path($distdir), gpg => $args_ref->{gpg} // 'gpg', perl => $args_ref->{perl}, pgp_key => $args_ref->{pgp_key} // $global_config_ref->{pgp_key}, @@ -318,8 +307,8 @@ sub new { # means all expected files were found) sub check_dist { my ($self, $source, $tarball) = @_; - my @expected = $self->_expected_dist_files(getcwd()); - my %expected = map { $_ => 1 } @expected; + my @expected = $self->_expected_dist_files(path(q{.})); + my %expected = map { ("$_", 1) } @expected; my $archive = Archive::Tar->new($tarball); for my $file ($archive->list_files()) { $file =~ s{ \A [^/]* / }{}xms; @@ -380,20 +369,21 @@ sub make_distribution { my ($self) = @_; # Determine the source directory and the distribution directory name. - my $source = getcwd() or die "cannot get current directory: $!\n"; + my $source = path(q{.})->realpath(); my $prefix = $self->{config}{distribution}{tarname}; # If the distribution directory name already exists, remove it. Automake # may have made parts of it read-only, so be forceful in the removal. - # Note that this does not pass the safe parameter and therefore should not - # be called on attacker-controlled directories. + # Note that this disables safe mode and therefore should not be called on + # attacker-controlled directories. chdir($self->{distdir}); - if (-d $prefix) { - remove_tree($prefix); + my $workdir = path($prefix); + if ($workdir->is_dir()) { + $workdir->remove_tree({ safe => 0 }); } # Export the Git repository into a new directory. - my $repo = Git::Repository->new(work_tree => $source); + my $repo = Git::Repository->new(work_tree => "$source"); my @branches = $repo->run( 'for-each-ref' => '--format=%(refname:short)', 'refs/heads/', ); @@ -408,31 +398,29 @@ sub make_distribution { } # Change to that directory and run the configured commands. - chdir($prefix); + chdir($workdir); 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()); + # Generate additional compression formats if needed. + $self->_generate_compression_formats(path(q{.}), $prefix); - # Remove the working tree. - chdir(File::Spec->updir()); - remove_tree($prefix, { safe => 1 }); + # Move the generated tarballs to the parent directory. + $self->_move_tarballs(path(q{.}), $prefix, $self->{distdir}); - # Generate additional compression formats if needed. - $self->_generate_compression_formats(getcwd(), $prefix); + # Remove the working tree. + chdir($self->{distdir}); + $workdir->remove_tree(); # Check the distribution for any missing files. If there are any, report # them and then fail with an error. - my $tarball = $self->_find_gzip_tarball(getcwd(), $prefix); + my $tarball = $self->_find_gzip_tarball($self->{distdir}, $prefix); chdir($source); my @missing = $self->check_dist($source, $tarball); if (@missing) { - print "Files found in local tree but not in distribution:\n" - or die "cannot print to stdout: $!\n"; - print q{ } . join(qq{\n }, @missing) . "\n" - or die "cannot print to stdout: $!\n"; + print_checked("Files found in local tree but not in distribution:\n"); + print_checked(q{ }, join(qq{\n }, @missing), "\n"); my $count = scalar(@missing); my $files = ($count == 1) ? '1 file' : "$count files"; die "$files missing from distribution\n"; @@ -472,7 +460,8 @@ App::DocKnot::Dist - Prepare a distribution tarball Git, Perl 5.24 or later, and the modules File::BaseDir, File::ShareDir, Git::Repository, IO::Compress::Xz (part of IO-Compress-Lzma), IO::Uncompress::Gunzip (part of IO-Compress), IPC::Run, IPC::System::Simple, -Kwalify, List::SomeUtils, and YAML::XS, all of which are available from CPAN. +Kwalify, List::SomeUtils, Path::Tiny, and YAML::XS, all of which are available +from CPAN. The tools to build whatever type of software distribution is being prepared are also required, since the distribution is built and tested as part of diff --git a/lib/App/DocKnot/Generate.pm b/lib/App/DocKnot/Generate.pm index cab4365..72eb686 100644 --- a/lib/App/DocKnot/Generate.pm +++ b/lib/App/DocKnot/Generate.pm @@ -10,7 +10,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Generate 6.01; +package App::DocKnot::Generate 7.00; use 5.024; use autodie; @@ -18,17 +18,18 @@ use parent qw(App::DocKnot); use warnings; use App::DocKnot::Config; -use App::DocKnot::Util qw(print_fh); use Carp qw(croak); -use Encode qw(encode); +use Path::Tiny qw(path); use Template; use Text::Wrap qw(wrap); # Default output files for specific templates. +#<<< my %DEFAULT_OUTPUT = ( - 'readme' => 'README', + 'readme' => 'README', 'readme-md' => 'README.md', ); +#>>> ############################################################################## # Generator functions @@ -480,11 +481,10 @@ sub generate { $vars{to_text} = $self->_code_for_to_text; $vars{to_thread} = $self->_code_for_to_thread; - # Ensure we were given a valid template. - $template = $self->appdata_path('templates', "${template}.tmpl"); - # Run Template Toolkit processing. - my $tt = Template->new({ ABSOLUTE => 1 }) or croak(Template->error()); + $template = $self->appdata_path('templates', "${template}.tmpl"); + my $tt = Template->new({ ABSOLUTE => 1, ENCODING => 'utf8' }) + or croak(Template->error()); my $result; $tt->process($template, \%vars, \$result) or croak($tt->error); @@ -527,9 +527,7 @@ sub generate_output { # Generate the output. my $data = $self->generate($template); - open(my $outfh, '>', $output); - print_fh($outfh, $output, encode('utf-8', $data)); - close($outfh); + path($output)->spew_utf8($data); return; } @@ -561,8 +559,8 @@ App::DocKnot::Generate - Generate documentation from package metadata =head1 REQUIREMENTS Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, Kwalify, -Template (part of Template Toolkit), and YAML::XS, all of which are available -from CPAN. +Path::Tiny, Template (part of Template Toolkit), and YAML::XS, all of which +are available from CPAN. =head1 DESCRIPTION @@ -661,7 +659,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 2013-2021 Russ Allbery <rra@cpan.org> +Copyright 2013-2022 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 diff --git a/lib/App/DocKnot/Release.pm b/lib/App/DocKnot/Release.pm index b5d8d83..b118281 100644 --- a/lib/App/DocKnot/Release.pm +++ b/lib/App/DocKnot/Release.pm @@ -10,7 +10,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Release 6.01; +package App::DocKnot::Release 7.00; use 5.024; use autodie; @@ -20,6 +20,7 @@ use App::DocKnot::Config; use App::DocKnot::Spin::Versions; use App::DocKnot::Util qw(latest_tarball); use Carp qw(croak); +use List::Util qw(min); use Path::Tiny qw(path); ############################################################################## @@ -122,9 +123,16 @@ sub release { } # Copy the new version into place and update the symlinks. + my @times; $current_path->mkpath(); for my $file ($tarball_ref->{files}->@*) { - $self->{distdir}->child($file)->copy($current_path->child($file)); + my $source = $self->{distdir}->child($file); + my $dest = $current_path->child($file); + $source->copy($dest); + my ($atime, $mtime) = $source->stat()->@[8, 9]; + push(@times, $mtime); + utime($atime, $mtime, $dest) + or die "cannot reset timestamps of $dest: $!\n"; my $generic_name = $file; $generic_name =~ s{ \A (\Q$self->{tarname}\E) - [\d.]+ [.] }{$1.}xms; my $generic_path = $current_path->child($generic_name); @@ -136,7 +144,7 @@ sub release { if ($self->{versions}) { my $name = $self->{version_name}; my $version = $tarball_ref->{version}; - my $date = $tarball_ref->{date}; + my $date = min(@times); $self->{versions}->update_version($name, $version, $date); } return; diff --git a/lib/App/DocKnot/Spin.pm b/lib/App/DocKnot/Spin.pm index 5f4632e..f7a5978 100644 --- a/lib/App/DocKnot/Spin.pm +++ b/lib/App/DocKnot/Spin.pm @@ -11,11 +11,11 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin 6.01; +package App::DocKnot::Spin 7.00; use 5.024; use autodie; -use warnings; +use warnings FATAL => 'utf8'; use App::DocKnot::Spin::Pointer; use App::DocKnot::Spin::RSS; @@ -70,7 +70,7 @@ sub _footer { # Add the end-of-page navbar if we have sitemap information. if ($self->{sitemap} && $self->{output}) { my $page = $out_path->relative($self->{output}); - $output .= join(q{}, $self->{sitemap}->navbar("/$page")) . "\n"; + $output .= join(q{}, $self->{sitemap}->navbar($page)) . "\n"; } # Figure out the modification dates. Use the RCS/CVS Id if available, @@ -325,7 +325,7 @@ sub _read_pointer { } # Return the details. - return ($master, $options, $style); + return (path($master), $options, $style); } # Convert an input path to an output path. @@ -360,8 +360,6 @@ sub _report_action { # # Throws: Text exception on any processing error # autodie exception if files could not be accessed or written -# -## no critic (Subroutines::ProhibitExcessComplexity) sub _process_file { my ($self, $input) = @_; @@ -387,16 +385,12 @@ sub _process_file { $self->_report_action('Creating', $output); $output->mkpath(); } - my $rss_path = path($input, '.rss'); - if ($rss_path->exists()) { - $self->{rss}->generate("$rss_path", "$input"); - } } elsif ($input->basename() =~ m{ [.] spin \z }xms) { my $output = $self->_output_for_file($input, '.spin'); $self->{generated}{"$output"} = 1; - if ($self->{pointer}->is_out_of_date("$input", "$output")) { + if ($self->{pointer}->is_out_of_date($input, $output)) { $self->_report_action('Converting', $output); - $self->{pointer}->spin_pointer("$input", "$output"); + $self->{pointer}->spin_pointer($input, $output); } } elsif ($input->basename() =~ m{ [.] th \z }xms) { my $output = $self->_output_for_file($input, '.th'); @@ -406,12 +400,11 @@ sub _process_file { # a software release. if ($output->exists() && $self->{versions}) { my $relative = $input->relative($self->{source}); - my $time = $self->{versions}->latest_release("$relative"); + my $time = $self->{versions}->latest_release($relative); return - if is_newer("$output", "$input") - && $output->stat()->[9] >= $time; + if is_newer($output, $input) && $output->stat()->[9] >= $time; } else { - return if is_newer("$output", "$input"); + return if is_newer($output, $input); } # The output file is not newer. Respin it. @@ -421,7 +414,7 @@ sub _process_file { my ($extension) = ($input->basename =~ m{ [.] ([^.]+) \z }xms); if (defined($extension) && $rules{$extension}) { my ($name, $sub) = $rules{$extension}->@*; - my $output = $self->_output_for_file($input, $extension); + my $output = $self->_output_for_file($input, q{.} . $extension); $self->{generated}{"$output"} = 1; my ($source, $options, $style) = $self->_read_pointer($input); return if is_newer($output, $input, $source); @@ -430,14 +423,13 @@ sub _process_file { } else { my $output = $self->_output_for_file($input); $self->{generated}{"$output"} = 1; - return if is_newer("$output", "$input"); + return if is_newer($output, $input); $self->_report_action('Updating', $output); $input->copy($output); } } return; } -## use critic # This routine is called for every file in the destination tree in depth-first # order, if the user requested file deletion of files not generated from the @@ -512,7 +504,6 @@ sub spin { # Reset data from a previous run. delete $self->{repository}; - delete $self->{rss}; delete $self->{sitemap}; delete $self->{versions}; @@ -536,7 +527,7 @@ sub spin { # Read metadata from the top of the input directory. my $sitemap_path = $input->child('.sitemap'); if ($sitemap_path->exists()) { - $self->{sitemap} = App::DocKnot::Spin::Sitemap->new("$sitemap_path"); + $self->{sitemap} = App::DocKnot::Spin::Sitemap->new($sitemap_path); } my $versions_path = $input->child('.versions'); if ($versions_path->exists()) { @@ -546,13 +537,14 @@ sub spin { $self->{repository} = Git::Repository->new(work_tree => $input); } - # Create a new RSS generator object. - $self->{rss} = App::DocKnot::Spin::RSS->new({ base => $input }); - - # Process an .rss file at the top of the tree, if present. - my $rss_path = $input->child('.rss'); - if ($rss_path->exists()) { - $self->{rss}->generate("$rss_path", "$input"); + # Process all .rss files in the input tree first. This is done as a + # separate pass because Path::Iterator::Rule appears to not always re-read + # the directory when it's modified during the iteration. + my $rss = App::DocKnot::Spin::RSS->new({ base => $input }); + my $rule = Path::Iterator::Rule->new()->name('.rss'); + my $iter = $rule->iter("$input", { follow_symlinks => 0 }); + while (defined(my $file = $iter->())) { + $rss->generate(path($file), path($file)->parent); } # Create a new thread converter object. @@ -581,9 +573,9 @@ sub spin { #>>> # Process the input tree. - my $rule = Path::Iterator::Rule->new(); + $rule = Path::Iterator::Rule->new(); $rule = $rule->skip($rule->new()->name($self->{excludes}->@*)); - my $iter = $rule->iter("$input", { follow_symlinks => 0 }); + $iter = $rule->iter("$input", { follow_symlinks => 0 }); while (defined(my $file = $iter->())) { $self->_process_file(path($file)); } diff --git a/lib/App/DocKnot/Spin/Pointer.pm b/lib/App/DocKnot/Spin/Pointer.pm index 871e267..a16250c 100644 --- a/lib/App/DocKnot/Spin/Pointer.pm +++ b/lib/App/DocKnot/Spin/Pointer.pm @@ -10,12 +10,12 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin::Pointer 6.01; +package App::DocKnot::Spin::Pointer 7.00; use 5.024; use autodie; use parent qw(App::DocKnot); -use warnings; +use warnings FATAL => 'utf8'; use App::DocKnot::Config; use App::DocKnot::Util qw(is_newer); @@ -66,11 +66,11 @@ sub _spin_markdown { my ($links, $navbar, $style); if ($self->{sitemap}) { my $page = $output->relative($self->{output}); - my @links = $self->{sitemap}->links("/$page"); + my @links = $self->{sitemap}->links($page); if (@links) { $links = join(q{}, @links); } - my @navbar = $self->{sitemap}->navbar("/$page"); + my @navbar = $self->{sitemap}->navbar($page); if (@navbar) { $navbar = join(q{}, @navbar); } @@ -172,7 +172,8 @@ sub new { } # Create and return the object. - my $tt = Template->new({ ABSOLUTE => 1 }) or croak(Template->error()); + my $tt = Template->new({ ABSOLUTE => 1, ENCODING => 'utf8' }) + or croak(Template->error()); #<<< my $self = { output => $args_ref->{output}, @@ -199,7 +200,6 @@ sub new { # Throws: YAML::XS exception on invalid pointer sub is_out_of_date { my ($self, $pointer, $output) = @_; - $pointer = path($pointer); my $data_ref = $self->load_yaml_file($pointer, 'pointer'); my $path = path($data_ref->{path})->absolute($pointer->parent()); if (!$path->exists()) { @@ -218,8 +218,6 @@ sub is_out_of_date { # Text exception on failure to convert the file sub spin_pointer { my ($self, $pointer, $output, $options_ref) = @_; - $pointer = path($pointer); - $output = path($output); my $data_ref = $self->load_yaml_file($pointer, 'pointer'); $data_ref->{options} //= {}; @@ -254,13 +252,17 @@ App::DocKnot::Spin::Pointer - Generate HTML from a pointer to an external file use App::DocKnot::Spin::Pointer; use App::DocKnot::Spin::Sitemap; + use Path::Tiny qw(path); - my $sitemap = App::DocKnot::Spin::Sitemap->new('/input/.sitemap'); + my $sitemap_path = path('/input/.sitemap'); + my $sitemap = App::DocKnot::Spin::Sitemap->new($sitemap_path); my $pointer = App::DocKnot::Spin::Pointer->new({ - output => '/output', + output => path('/output'), sitemap => $sitemap, }); - $pointer->spin_pointer('/input/file.spin', '/output/file.html'); + my $input = path('/input/file.spin'); + my $output = path('/output/file.html'); + $pointer->spin_pointer($input, $output); =head1 REQUIREMENTS @@ -326,11 +328,13 @@ C<sitemap> argument. Returns true if OUTPUT is missing or if it was modified less recently than the modification time of either POINTER or the underlying file that it points to. +Both paths must be Path::Tiny objects. =item spin_pointer(POINTER, OUTPUT) Convert a single pointer file to HTML. POINTER is the path to the pointer -file, and OUTPUT is the path to where to write the output. +file, and OUTPUT is the path to where to write the output. Both paths must +be Path::Tiny objects. =back diff --git a/lib/App/DocKnot/Spin/RSS.pm b/lib/App/DocKnot/Spin/RSS.pm index 3f8fb70..2b0e0fc 100644 --- a/lib/App/DocKnot/Spin/RSS.pm +++ b/lib/App/DocKnot/Spin/RSS.pm @@ -9,19 +9,19 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin::RSS 6.01; +package App::DocKnot::Spin::RSS 7.00; use 5.024; use autodie; -use warnings; +use parent qw(App::DocKnot); +use warnings FATAL => 'utf8'; -use App::DocKnot; use App::DocKnot::Spin::Thread; use App::DocKnot::Util qw(print_checked print_fh); +use Carp qw(croak); use Date::Language (); use Date::Parse qw(str2time); use Path::Tiny qw(path); -use Perl6::Slurp qw(slurp); use POSIX qw(strftime); ############################################################################## @@ -141,7 +141,7 @@ sub _spin_file { # $base - Base path for all output sub _report_action { my ($self, $action, $output) = @_; - my $shortout = $output->relative($self->{base} // path()); + my $shortout = $output->relative($self->{base} // path(q{.})); print_checked("$action .../$shortout\n"); return; } @@ -385,8 +385,6 @@ sub _rss_review { # $entries_ref - Array of entries in the RSS feed sub _rss_output { my ($self, $file, $base, $metadata_ref, $entries_ref) = @_; - my $fh = $file->openw_utf8(); - my $version = '1.25'; # Determine the current date and latest publication date of all of the # entries, published in the obnoxious format used by RSS. @@ -398,35 +396,17 @@ sub _rss_output { $latest = strftime($format, localtime($entries_ref->[0]{date})); } - # Output the RSS header. - print_fh($fh, $file, <<"EOC"); -<?xml version="1.0" encoding="UTF-8"?> -<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> - <channel> - <title>$metadata_ref->{title}</title> - <link>$metadata_ref->{base}</link> - <description>$metadata_ref->{description}</description> - <language>$metadata_ref->{language}</language> - <pubDate>$latest</pubDate> - <lastBuildDate>$now</lastBuildDate> - <generator>DocKnot $App::DocKnot::VERSION</generator> -EOC + # Determine the URL of the RSS file we're generating, if possible. + my $url; if ($metadata_ref->{'rss-base'}) { my $name = $file->basename(); - my $url = $metadata_ref->{'rss-base'} . $name; - print_fh( - $fh, - $file, - qq{ <atom:link href="$url" rel="self"\n}, - qq{ type="application/rss+xml" />\n}, - ); + $url = $metadata_ref->{'rss-base'} . $name; } - print_fh($fh, $file, "\n"); - # Output each entry, formatting the contents of the entry as we go. + # Format the entries. + my @formatted_entries; for my $entry_ref ($entries_ref->@*) { my $date = $lang->strftime($format, [localtime($entry_ref->{date})]); - my $title = _escape($entry_ref->{title}); my $description; if ($entry_ref->{description}) { $description = _escape($entry_ref->{description}); @@ -448,32 +428,39 @@ EOC ( [./\w] [^\"]+ ) \" }{ $1 . _absolute_url($2, $entry_ref->{link}) . qq{\"} }xmsge; - # Optionally add an attribute indicating this is not a permanent link. - # Assume any URL is a permanent link. - my $perma = q{}; - if ($entry_ref->{guid} !~ m{ \A http }xms) { - $perma = ' isPermaLink="false"'; - } - - # Output the entry. - print_fh( - $fh, - $file, - " <item>\n", - " <title>$title</title>\n", - " <link>$entry_ref->{link}</link>\n", - " <description><![CDATA[\n", - $description, - " ]]></description>\n", - " <pubDate>$date</pubDate>\n", - " <guid$perma>$entry_ref->{guid}</guid>\n", - " </item>\n", - ); + # Convert this into an object suitable for the output template. + #<<< + my $formatted_ref = { + date => $date, + description => $description, + guid => $entry_ref->{guid}, + link => $entry_ref->{link}, + title => $entry_ref->{title}, + }; + #>>> + push(@formatted_entries, $formatted_ref); } - # Close the RSS structure. - print_fh($fh, $file, " </channel>\n</rss>\n"); - close($fh); + # Generate the RSS output using the template. + #<<< + my %vars = ( + base => $metadata_ref->{base}, + description => $metadata_ref->{description}, + docknot_version => $App::DocKnot::VERSION, + entries => \@formatted_entries, + language => $metadata_ref->{language}, + latest => $latest, + now => $now, + title => $metadata_ref->{title}, + url => $url, + ); + #>>> + my $result; + $self->{template}->process($self->{templates}{rss}, \%vars, \$result) + or croak($self->{template}->error()); + + # Write the result to the output file. + $file->spew_utf8($result); return; } @@ -488,49 +475,51 @@ EOC # $entries_ref - Entries sub _thread_output { my ($self, $file, $metadata_ref, $entries_ref) = @_; - my $fh = $file->openw_utf8(); - - # Page prefix. - if ($metadata_ref->{'thread-prefix'}) { - print_fh($fh, $file, $metadata_ref->{'thread-prefix'}, "\n"); - } else { - print_fh( - $fh, - $file, - "\\heading[Recent Changes][indent]\n\n", - "\\h1[Recent Changes]\n\n", - ); - } - # Print out each entry. - my $last_month; + # The entries are in a flat list, but we want a two-level list of entries + # by month so that the template can add appropriate month headings. + # Restructure the entry list accordingly. + my (@entries_by_month, $last_month); for my $entry_ref ($entries_ref->@*) { my $month = strftime('%B %Y', localtime($entry_ref->{date})); - - # Put headings before each month. - if (!$last_month || $month ne $last_month) { - print_fh($fh, $file, "\\h2[$month]\n\n"); - $last_month = $month; - } - - # Format each entry. my $date = strftime('%Y-%m-%d', localtime($entry_ref->{date})); - print_fh( - $fh, - $file, - "\\desc[$date \\entity[mdash]\n", - " \\link[$entry_ref->{link}]\n", - " [$entry_ref->{title}]][\n", - ); + + # Copy the entry with a reformatted description. my $description = $entry_ref->{description}; $description =~ s{ ^ }{ }xmsg; $description =~ s{ \\ }{\\\\}xmsg; - print_fh($fh, $file, $description, "]\n\n"); + #<<< + my $formatted_ref = { + date => $date, + description => $description, + link => $entry_ref->{link}, + title => $entry_ref->{title}, + }; + #<<< + + # Add the entry to the appropriate month. + if (!$last_month || $month ne $last_month) { + my $month_ref = { heading => $month, entries => [$formatted_ref] }; + push(@entries_by_month, $month_ref); + $last_month = $month; + } else { + push($entries_by_month[-1]{entries}->@*, $formatted_ref); + } } - # Print out the end of the page. - print_fh($fh, $file, "\\signature\n"); - close($fh); + # Generate the RSS output using the template. + #<<< + my %vars = ( + prefix => $metadata_ref->{'thread-prefix'}, + entries => \@entries_by_month, + ); + #>>> + my $result; + $self->{template}->process($self->{templates}{changes}, \%vars, \$result) + or croak($self->{template}->error()); + + # Write the result to the output file. + $file->spew_utf8($result); return; } @@ -636,14 +625,9 @@ sub _index_review { # $entries_ref - Entries sub _index_output { my ($self, $file, $base, $metadata_ref, $entries_ref) = @_; - my $fh = $file->openw_utf8(); - # Output the prefix. - if ($metadata_ref->{'index-prefix'}) { - print_fh($fh, $file, $metadata_ref->{'index-prefix'}, "\n"); - } - - # Output each entry. + # Format each entry. + my @formatted_entries; for my $entry_ref ($entries_ref->@*) { my @time = localtime($entry_ref->{date}); my $date = strftime('%Y-%m-%d %H:%M', @time); @@ -671,24 +655,33 @@ sub _index_output { ( \\ image \s* \[ ) ( [^\]]+ ) \] }{$1 . _relative_url($2, $metadata_ref->{'index-base'}) . ']' }xmsge; - # Print out the entry. - print_fh( - $fh, - $file, - "\\h2[$day: $entry_ref->{title}]\n\n", - $text, - "\\class(footer)[$date \\entity[mdash]\n", - " \\link[$entry_ref->{link}]\n", - " [Permanent link]]\n\n", - ); + # Add the entry to the list. + #<<< + my $formatted_ref = { + date => $date, + day => $day, + link => $entry_ref->{link}, + title => $entry_ref->{title}, + text => $text, + }; + #>>> + push(@formatted_entries, $formatted_ref); } - # Print out the end of the page. - if ($metadata_ref->{'index-suffix'}) { - print_fh($fh, $file, $metadata_ref->{'index-suffix'}, "\n"); - } - print_fh($fh, $file, "\\signature\n"); - close($fh); + # Generate the RSS output using the template. + #<<< + my %vars = ( + prefix => $metadata_ref->{'index-prefix'}, + suffix => $metadata_ref->{'index-suffix'}, + entries => \@formatted_entries, + ); + #>>> + my $result; + $self->{template}->process($self->{templates}{index}, \%vars, \$result) + or croak($self->{template}->error()); + + # Write the result to the output file. + $file->spew_utf8($result); return; } @@ -706,11 +699,22 @@ sub new { my ($class, $args_ref) = @_; # Create and return the object. + my $base = defined($args_ref->{base}) ? path($args_ref->{base}) : undef; + my $tt = Template->new({ ABSOLUTE => 1, ENCODING => 'utf8' }) + or croak(Template->error()); + #<<< my $self = { - base => defined($args_ref->{base}) ? path($args_ref->{base}) : undef, - spin => App::DocKnot::Spin::Thread->new(), + base => $base, + spin => App::DocKnot::Spin::Thread->new(), + template => $tt, }; bless($self, $class); + $self->{templates} = { + changes => $self->appdata_path('templates', 'changes.tmpl'), + index => $self->appdata_path('templates', 'index.tmpl'), + rss => $self->appdata_path('templates', 'rss.tmpl'), + }; + #>>> return $self; } @@ -722,7 +726,7 @@ sub generate { my ($self, $source, $base) = @_; $source = path($source); $base //= $self->{base}; - $base = defined($base) ? path($base) : path(); + $base = defined($base) ? path($base) : path(q{.}); # Read in the changes. my ($metadata_ref, $changes_ref) = $self->_parse_changes($source); @@ -1082,7 +1086,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 2008, 2010-2012, 2021 Russ Allbery <rra@cpan.org> +Copyright 2008, 2010-2012, 2021-2022 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 diff --git a/lib/App/DocKnot/Spin/Sitemap.pm b/lib/App/DocKnot/Spin/Sitemap.pm index 3cbc97a..884765c 100644 --- a/lib/App/DocKnot/Spin/Sitemap.pm +++ b/lib/App/DocKnot/Spin/Sitemap.pm @@ -12,13 +12,14 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin::Sitemap 6.01; +package App::DocKnot::Spin::Sitemap 7.00; use 5.024; use autodie; use warnings; use List::SomeUtils qw(pairwise); +use Path::Tiny qw(path); ############################################################################## # File parsing @@ -48,7 +49,7 @@ sub _read_data { my @indents; # Parse the file. - open(my $fh, '<', $path); + my $fh = $path->openr_utf8(); while (defined(my $line = <$fh>)) { next if $line =~ m{ \A \s* \# }xms; chomp($line); @@ -67,14 +68,14 @@ sub _read_data { # Regular line. Parse it. my ($spaces, $url, $desc) - = $line =~ m{ \A ([ ]*) ([^\s:]+): \s+ (.+) \z}xms; + = $line =~ m{ \A ([ ]*) /([^\s:]*): \s+ (.+) \z}xms; if (!defined($desc)) { die "invalid line $. in $path\n"; } # Error on duplicate lines. if ($seen{$url}) { - die "duplicate entry for $url in $path (line $.)\n"; + die "duplicate entry for /$url in $path (line $.)\n"; } $seen{$url} = 1; @@ -121,9 +122,9 @@ sub _read_data { # Returns: $desc escaped so that it's safe to interpolate into an attribute sub _escape { my ($desc, $is_attr) = @_; - $desc =~ s{ & }{&}xmsg; - $desc =~ s{ < }{<}xmsg; - $desc =~ s{ > }{>}xmsg; + $desc =~ s{ & }{&}xmsg; + $desc =~ s{ < }{<}xmsg; + $desc =~ s{ > }{>}xmsg; if ($is_attr) { $desc =~ s{ \" }{"}xmsg; } @@ -152,8 +153,8 @@ sub _relative { # If there are the same number of components in both links, the link # should be relative to the current directory. Otherwise, ascend to the # common prefix and then descend to the dest link. - if (@origin == 1 && @dest == 1) { - return length($dest[0]) > 0 ? $dest[0] : q{./}; + if (@origin == 1 && @dest <= 1) { + return (@dest && length($dest[0])) > 0 ? $dest[0] : q{./}; } else { return ('../' x $#origin) . join(q{/}, @dest); } @@ -168,16 +169,21 @@ sub _relative { # The relative URL and description may be undef if missing. sub _page_links { my ($self, $path) = @_; - $path =~ s{ /index[.]html \z }{/}xms; + my $key; + if ($path->basename() eq 'index.html') { + $key = $path->parent() . q{/}; + } else { + $key = "$path"; + } # If the page is not present in the sitemap, return nothing. There are # also no meaningful links to generate for the top page. - return () if ($path eq q{/} || !$self->{links}{$path}); + return () if ($key eq q{/} || !$self->{links}{$key}); # Convert all the links to relative and add the page descriptions. return map { defined ? [_relative($path, $_), $self->{pagedesc}{$_}] : undef } - $self->{links}{$path}->@*; + $self->{links}{$key}->@*; } ############################################################################## @@ -217,7 +223,7 @@ sub new { bless($self, $class); # Parse the file into the newly-created object. - $self->_read_data($path); + $self->_read_data(path($path)); # Return the populated object. return $self; @@ -226,12 +232,12 @@ sub new { # Return the <link> tags for a given output file, suitable for its <head> # section. # -# $path - URL path to the output with leading slash +# $path - Path to the output file relative to the top of the output tree # # Returns: List of lines to add to the <head> section sub links { my ($self, $path) = @_; - my @links = $self->_page_links($path); + my @links = $self->_page_links(path($path)); return () if !@links; # We only care about the first parent, not the rest of the chain to the @@ -259,7 +265,7 @@ sub links { } # Add the link to the top-level page. - my $url = _relative($path, q{/}); + my $url = _relative($path, q{}); push(@output, qq{ <link rel="top" href="$url" />\n}); # Return the results. @@ -268,12 +274,12 @@ sub links { # Return the navigation bar for a given output file. # -# $path - URL path to the output with leading slash +# $path - Path to the output file relative to the top of the output tree # # Returns: List of lines that create the navbar sub navbar { my ($self, $path) = @_; - my ($prev, $next, @parents) = $self->_page_links($path); + my ($prev, $next, @parents) = $self->_page_links(path($path)); return () if !@parents; # Construct the left and right links (previous and next). @@ -329,7 +335,6 @@ sub sitemap { # Build the sitemap as nested unordered lists. for my $page ($self->{sitemap}->@*) { my ($indent, $url, $desc) = $page->@*; - $url =~ s{ \A / }{}xms; # Skip the top page. next if $indent == 0; @@ -383,7 +388,8 @@ App::DocKnot::Spin::Sitemap - Generate page navigation links for spin =head1 REQUIREMENTS -Perl 5.24 or later and List::SomeUtils, which is available from CPAN. +Perl 5.24 or later and the List::SomeUtils and Path::Tiny modules, both of +which are available from CPAN. =head1 DESCRIPTION @@ -464,7 +470,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 1999-2000, 2002-2004, 2008, 2021 Russ Allbery <rra@cpan.org> +Copyright 1999-2000, 2002-2004, 2008, 2021-2022 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 diff --git a/lib/App/DocKnot/Spin/Thread.pm b/lib/App/DocKnot/Spin/Thread.pm index 0d84bd7..1d1ef52 100644 --- a/lib/App/DocKnot/Spin/Thread.pm +++ b/lib/App/DocKnot/Spin/Thread.pm @@ -9,14 +9,15 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin::Thread 6.01; +package App::DocKnot::Spin::Thread 7.00; use 5.024; use autodie; -use warnings; +use warnings FATAL => 'utf8'; use App::DocKnot; use App::DocKnot::Util qw(print_fh); +use Encode qw(decode); use Git::Repository (); use Image::Size qw(html_imgsize); use Path::Tiny qw(path); @@ -1079,7 +1080,7 @@ sub _cmd_heading { # Add <link> tags based on the sitemap. if ($self->{sitemap} && defined($page)) { - my @links = $self->{sitemap}->links("/$page"); + my @links = $self->{sitemap}->links($page); if (@links) { $output .= join(q{}, @links); } @@ -1098,7 +1099,7 @@ sub _cmd_heading { # Add the <body> tag and the navbar (if we have a sitemap). $output .= "\n<body>\n"; if ($self->{sitemap} && defined($page)) { - my @navbar = $self->{sitemap}->navbar("/$page"); + my @navbar = $self->{sitemap}->navbar($page); if (@navbar) { $output .= join(q{}, @navbar); } @@ -1278,7 +1279,7 @@ sub _cmd_signature { # Add the end-of-page navbar if we have sitemap information. if ($self->{sitemap} && $self->{output}) { my $page = $self->{out_path}->relative($self->{output}); - $output .= join(q{}, $self->{sitemap}->navbar("/$page")) . "\n"; + $output .= join(q{}, $self->{sitemap}->navbar($page)) . "\n"; } # Figure out the modification dates. Use the Git repository if available. @@ -1467,10 +1468,10 @@ sub new { sub spin_thread { my ($self, $thread, $input) = @_; my $result; - open(my $out_fh, '>', \$result); + open(my $out_fh, '>:raw:encoding(utf-8)', \$result); $self->_parse_document($thread, $input, $out_fh, undef); close($out_fh); - return $result; + return decode('utf-8', $result); } # Spin a single file of thread to HTML. @@ -1497,7 +1498,7 @@ sub spin_thread_file { $output = path($output)->absolute(); $out_fh = $output->openw_utf8(); } else { - open($out_fh, '>&', 'STDOUT'); + open($out_fh, '>&:raw:encoding(utf-8)', 'STDOUT'); } # Do the work. @@ -1526,9 +1527,9 @@ sub spin_thread_output { my $out_fh; if (defined($output)) { $output = path($output)->absolute(); - $out_fh = $output->filehandle('>'); + $out_fh = $output->openw_utf8(); } else { - open($out_fh, '>&', 'STDOUT'); + open($out_fh, '>&:raw:encoding(utf-8)', 'STDOUT'); } # Do the work. @@ -2097,7 +2098,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 1999-2011, 2013, 2021 Russ Allbery <rra@cpan.org> +Copyright 1999-2011, 2013, 2021-2022 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 diff --git a/lib/App/DocKnot/Spin/Versions.pm b/lib/App/DocKnot/Spin/Versions.pm index 1655ff0..2b6f759 100644 --- a/lib/App/DocKnot/Spin/Versions.pm +++ b/lib/App/DocKnot/Spin/Versions.pm @@ -12,7 +12,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin::Versions 6.01; +package App::DocKnot::Spin::Versions 7.00; use 5.024; use autodie; @@ -168,9 +168,7 @@ sub update_version { # Edits the line for the package to replace the version and release date. my $edit = sub { - my $line = $_; - my ($product, $old_version, $old_date, $old_time) - = split(q{ }, $line); + my ($product, $old_version, $old_date, $old_time) = split(q{ }); return if $product ne $package; # We're going to replace the old version with the new one, but we need @@ -184,6 +182,7 @@ sub update_version { } # Make the replacement. + my $line = $_; $line =~ s{ \Q$old_version\E }{$version_string}xms; $line =~ s{ \Q$old_date\E }{$date}xms; $line =~ s{ \Q$old_time\E }{$time}xms; @@ -192,7 +191,7 @@ sub update_version { # Apply that change to our versions file, and then re-read the contents to # update the internal data structure. - $self->{path}->edit_utf8($edit); + $self->{path}->edit_lines_utf8($edit); $self->_read_data(); return; } diff --git a/lib/App/DocKnot/Update.pm b/lib/App/DocKnot/Update.pm index 78b33de..4b43fba 100644 --- a/lib/App/DocKnot/Update.pm +++ b/lib/App/DocKnot/Update.pm @@ -9,7 +9,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Update 6.01; +package App::DocKnot::Update 7.00; use 5.024; use autodie; diff --git a/lib/App/DocKnot/Util.pm b/lib/App/DocKnot/Util.pm index e5e4014..cffa4f5 100644 --- a/lib/App/DocKnot/Util.pm +++ b/lib/App/DocKnot/Util.pm @@ -9,15 +9,16 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Util 6.01; +package App::DocKnot::Util 7.00; use 5.024; use autodie; -use warnings; +use warnings FATAL => 'utf8'; use Carp qw(croak); use Exporter qw(import); use List::SomeUtils qw(all); +use Path::Tiny qw(path); use Sort::Versions qw(versioncmp); our @EXPORT_OK = qw(is_newer latest_tarball print_checked print_fh); @@ -34,9 +35,9 @@ our @EXPORT_OK = qw(is_newer latest_tarball print_checked print_fh); # Returns: True if $file exists and is newer than @others, false otherwise sub is_newer { my ($file, @others) = @_; - return if !-e $file; - my $file_mtime = (stat($file))[9]; - my @others_mtimes = map { (stat)[9] } @others; + return if !$file->exists(); + my $file_mtime = $file->stat()->[9]; + my @others_mtimes = map { $_->stat()->[9] } @others; return all { $file_mtime >= $_ } @others_mtimes; } @@ -48,7 +49,6 @@ sub is_newer { # # Returns: Anonymous hash with the following keys: # version - Latest version found -# date - Date (in seconds since epoch) of oldest file # files - Array of files for that version # or undef if no matching files were found # Throws: Text exception on any error @@ -68,14 +68,10 @@ sub latest_tarball { my $latest = $versions[0][0]; @files = map { $_->[1] } grep { $_->[0] eq $latest } @versions; - # Find the timestamps of those files. - my @times = sort(map { $path->child($_)->stat()->[9] } @files); - # Return the results. #<<< return { version => $latest, - date => $times[0], files => \@files, }; #<<< @@ -126,19 +122,25 @@ App::DocKnot::Util - Shared utility functions for other DocKnot modules =head1 SYNOPSIS - use App::DocKnot::Util qw(is_newer print_checked print_fh); + use App::DocKnot::Util qw( + is_newer latest_tarball print_checked print_fh + ); + use Path::Tiny qw(path); print_checked('some stdout output'); - if (!is_newer('/output', '/input-1', '/input-2')) { + my @inputs = (path('/input-1'), path('/input-2')); + if (!is_newer(path('/output'), @inputs)) { open(my $fh, '>', '/output'); print_fh($fh, '/output', 'some stuff'); close($fh); } + my $latest_ref = latest_tarball(path('/archive'), 'App-Foo'); + =head1 REQUIREMENTS -Perl 5.24 or later and the modules List::SomeUtils and Sort::Versions, -available from CPAN. +Perl 5.24 or later and the modules List::SomeUtils, Path::Tiny, and +Sort::Versions, available from CPAN. =head1 DESCRIPTION @@ -155,20 +157,18 @@ used if desired. Returns a true value if FILE exists and has a last modified time that is newer or equal to the last modified times of all SOURCE files, and otherwise returns a false value. Used primarily to determine if a given output file is -up-to-date with respect to its source files. +up-to-date with respect to its source files. All paths must be Path::Tiny +objects. =item latest_tarball(PATH, NAME) Returns data including a file list for the latest tarballs (by version number) -for a given software package NAME in the directory PATH. Versions are compared -using Sort::Versions. The return valid is a hash with the following keys: +for a given software package NAME in the directory PATH (which must be a +Path::Tiny object). Versions are compared using Sort::Versions. The return +valid is a hash with the following keys: =over 4 -=item date - -The timestamp of the oldest file for that version, in seconds since epoch. - =item files The list of files found for that version. |