diff options
author | Russ Allbery <rra@cpan.org> | 2022-01-11 20:45:16 -0800 |
---|---|---|
committer | Russ Allbery <rra@cpan.org> | 2022-01-11 20:45:16 -0800 |
commit | e7e6c9a95fb4cd68110ecd51a8ea2b5accc17e78 (patch) | |
tree | 509eb6dd650ff4664238dfc445abfb4b394b0a00 /lib | |
parent | 66d9ae6a41b46481882c10474d92b8633a11e279 (diff) |
Add docknot release command
Add new docknot release command and corresponding App::DocKnot::Release
module to copy a tarball releaes (normally created by docknot dist)
into a release area, update symlinks, archive any previous releases,
and update the .versions file used by docknot spin. DocKnot now
depends on Sort::Versions.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/App/DocKnot/Command.pm | 6 | ||||
-rw-r--r-- | lib/App/DocKnot/Config.pm | 17 | ||||
-rw-r--r-- | lib/App/DocKnot/Dist.pm | 8 | ||||
-rw-r--r-- | lib/App/DocKnot/Release.pm | 268 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Versions.pm | 61 | ||||
-rw-r--r-- | lib/App/DocKnot/Util.pm | 71 |
6 files changed, 414 insertions, 17 deletions
diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm index 48c7240..0068b34 100644 --- a/lib/App/DocKnot/Command.pm +++ b/lib/App/DocKnot/Command.pm @@ -78,6 +78,12 @@ our %COMMANDS = ( options => ['metadata|m=s', 'width|w=i'], maximum => 0, }, + release => { + method => 'release', + module => 'App::DocKnot::Release', + options => ['archivedir|a=s', 'distdir|d=s', 'metadata|m=s'], + maximum => 0, + }, spin => { method => 'spin', module => 'App::DocKnot::Spin', diff --git a/lib/App/DocKnot/Config.pm b/lib/App/DocKnot/Config.pm index 5efbc48..036cfc7 100644 --- a/lib/App/DocKnot/Config.pm +++ b/lib/App/DocKnot/Config.pm @@ -107,7 +107,7 @@ __END__ =for stopwords Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT sublicense CPAN XDG Kwalify -distdir +distdir archivedir =head1 NAME @@ -170,6 +170,16 @@ default) may contain the following keys: =over 4 +=item archivedir + +Specifies the directory into which distribution tarballs are placed by the +C<docknot release> command. The current distribution will be put in a +subdirectory named after the C<distribution.section> key in the package +configuration. Older versions will be moved to the F<ARCHIVE> subdirectory of +I<archivedir>. + +If this is not specified, the B<-a> option to C<docknot release> is mandatory. + =item distdir Specifies the directory into which to build and store distribution tarballs @@ -177,7 +187,8 @@ Specifies the directory into which to build and store distribution tarballs as working directories while the distribution is being built, and the final tarballs are stored in this directory. -If this is not specified, the B<-d> option to C<docknot dist> is mandatory. +If this is not specified, the B<-d> options to C<docknot dist> and C<docknot +release> is mandatory. =item pgp_key @@ -228,7 +239,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/Dist.pm b/lib/App/DocKnot/Dist.pm index 50d3b9b..e78329d 100644 --- a/lib/App/DocKnot/Dist.pm +++ b/lib/App/DocKnot/Dist.pm @@ -262,9 +262,9 @@ sub _sign_tarballs { # Create a new App::DocKnot::Dist object, which will be used for subsequent # calls. # -# $args - Anonymous hash of arguments with the following keys: +# $args_ref - Anonymous hash of arguments with the following keys: # distdir - Path to the directory for distribution tarball -# metadata - Path to the directory containing package metadata +# metadata - Path to the package metadata # perl - Path to Perl to use (default: search the user's PATH) # # Returns: Newly created object @@ -520,7 +520,7 @@ Default: The binary named C<gpg> on the user's PATH. =item metadata -The path to the directory containing metadata for a package. Default: +The path to the metadata for the package on which to operate. Default: F<docs/docknot.yaml> relative to the current directory. =item perl @@ -598,7 +598,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 2019-2021 Russ Allbery <rra@cpan.org> +Copyright 2019-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 new file mode 100644 index 0000000..f2cef26 --- /dev/null +++ b/lib/App/DocKnot/Release.pm @@ -0,0 +1,268 @@ +# Release a distribution tarball for a package. +# +# This is the implementation of the docknot release command, which copies a +# release tarball (normally generated by docknot dist) into a publication +# area, archives old versions, and updates the .versions database for spin. +# +# SPDX-License-Identifier: MIT + +############################################################################## +# Modules and declarations +############################################################################## + +package App::DocKnot::Release 6.00; + +use 5.024; +use autodie; +use warnings; + +use App::DocKnot::Config; +use App::DocKnot::Spin::Versions; +use App::DocKnot::Util qw(latest_tarball); +use Carp qw(croak); +use Path::Tiny qw(path); + +############################################################################## +# Public interface +############################################################################## + +# Create a new App::DocKnot::Release object, which will be used for subsequent +# calls. +# +# $args_ref - Anonymous hash of arguments with the following keys: +# archivedir - Path to the archive directory +# distdir - Path to where docknot dist puts distribution tarballs +# metadata - Path to the package metadata +# +# Returns: Newly created object +# Throws: Text exceptions on invalid package metadata +# Text exceptions on invalid global configuration +# Text exceptions on 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_reader = App::DocKnot::Config->new(\%config_args); + + # Load the global and package configuration. + my $global_config_ref = $config_reader->global_config(); + my $config_ref = $config_reader->config(); + + # Ensure we were given a valid archivedir and distdir arguments if they + # were not set in the global configuration. + my $archivedir = $args_ref->{archivedir} + // $global_config_ref->{archivedir}; + if (!defined($archivedir)) { + croak('archivedir path not given'); + } elsif (!-d $archivedir) { + croak( + "archivedir path $archivedir does not exist or is not a directory", + ); + } + my $distdir = $args_ref->{distdir} // $global_config_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"); + } + + # Build an App::DocKnot::Spin::Versions object if configured with a path + # to a versions database. + my $versions; + if ($global_config_ref->{versions}) { + my $versions_path = path($global_config_ref->{versions}); + $versions = App::DocKnot::Spin::Versions->new($versions_path); + } + + # Create and return the object. + #<<< + my $self = { + archivedir => path($archivedir), + distdir => path($distdir), + package => $config_ref->{name}, + section => $config_ref->{distribution}{section}, + tarname => $config_ref->{distribution}{tarname}, + version_name => $config_ref->{distribution}{version}, + versions => $versions, + }; + #>>> + bless($self, $class); + return $self; +} + +# Release a new version and update .versions if so configured. +# +# Throws: Text exception on any failures +sub release { + my ($self) = @_; + my $tarball_ref = latest_tarball($self->{distdir}, $self->{tarname}); + if (!defined($tarball_ref)) { + croak("no release of $self->{tarname} found in $self->{distdir}"); + } + + # Archive old versions. This is only done if the current version in the + # archive directory is different than the version we're about to release. + # If it is not, we overwrite the version in the archive directory, since + # we assume we're replacing a release. + my $current_path = $self->{archivedir}->child($self->{section}); + my $current_ref = latest_tarball($current_path, $self->{tarname}); + if (defined($current_ref)) { + if ($current_ref->{version} ne $tarball_ref->{version}) { + my $old_root = $self->{archivedir}->child('ARCHIVE'); + my $old_path = $old_root->child($self->{tarname}); + $old_path->mkpath(); + for my $file ($current_ref->{files}->@*) { + $current_path->child($file)->move($old_path->child($file)); + } + } + } + + # Copy the new version into place and update the symlinks. + $current_path->mkpath(); + for my $file ($tarball_ref->{files}->@*) { + $self->{distdir}->child($file)->copy($current_path->child($file)); + my $generic_name = $file; + $generic_name =~ s{ \A (\Q$self->{tarname}\E) - [\d.]+ [.] }{$1.}xms; + my $generic_path = $current_path->child($generic_name); + $generic_path->remove(); + symlink($file, $generic_path); + } + + # Update the .versions file. + if ($self->{versions}) { + my $name = $self->{version_name}; + my $version = $tarball_ref->{version}; + my $date = $tarball_ref->{date}; + $self->{versions}->update_version($name, $version, $date); + } + return; +} + +############################################################################## +# Module return value and documentation +############################################################################## + +1; +__END__ + +=for stopwords +Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT sublicense archivedir distdir + +=head1 Name + +App::DocKnot::Release - Release a distribution tarball + +=head1 SYNOPSIS + + use App::DocKnot::Release; + my $docknot = App::DocKnot::Release->new(); + $docknot->release(); + +=head1 REQUIREMENTS + +Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, +Git::Repository, Path::Tiny, and YAML::XS, all of which are available from +CPAN. + +=head1 DESCRIPTION + +This component of DocKnot releases a distribution tarball (normally created by +C<docknot dist> or App::DocKnot::Dist), maintains a software distribution +directory, and updates a version and release date database. + +=head1 CLASS METHODS + +=over 4 + +=item new(ARGS) + +Create a new App::DocKnot::Release 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 archivedir + +The release area into which to put the distribution tarball. The current +distribution will be put in a subdirectory named after the +C<distribution.section> key in the package configuration. Older versions will +be moved to the F<ARCHIVE> subdirectory of I<archivedir>. Required if not set +in the global configuration file. + +=item distdir + +The directory from which to get the new distribution tarball, normally +generated by C<docknot dist>. The latest version in this directory will be +used. Required if not set in the global configuration file. + +=item metadata + +The path to the metadata for the package on which to operate. Default: +F<docs/docknot.yaml> relative to the current directory. + +=back + +=back + +=head1 INSTANCE METHODS + +=over 4 + +=item release() + +Copy the distribution tarball (in multiple formats, with PGP signatures) into +a release area, updates symlink pointing to the latest version, and move any +old release to an archive area. + +If C<versions> is set in the global configuration file, updates the +F<.versions> file found at that path with the new release version and release +date. See L<App::DocKnot::Spin::Versions> for more information about +F<.versions> files. + +=back + +=head1 AUTHOR + +Russ Allbery <rra@cpan.org> + +=head1 COPYRIGHT AND LICENSE + +Copyright 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 +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)>, L<App::DocKnot::Config>, L<App::DocKnot::Dist>, +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 +L<https://www.eyrie.org/~eagle/software/docknot/>. + +=cut + +# Local Variables: +# copyright-at-end-flag: t +# End: diff --git a/lib/App/DocKnot/Spin/Versions.pm b/lib/App/DocKnot/Spin/Versions.pm index c0e04f4..d417951 100644 --- a/lib/App/DocKnot/Spin/Versions.pm +++ b/lib/App/DocKnot/Spin/Versions.pm @@ -64,6 +64,8 @@ sub _datetime_to_seconds { # Text exception on file parsing errors sub _read_data { my ($self) = @_; + $self->{depends} = {}; + $self->{versions} = {}; my $timestamp; my $lineno = 0; @@ -119,13 +121,7 @@ sub new { my ($class, $path) = @_; # Create an empty object. - #<<< - my $self = { - depends => {}, - path => path($path), - versions => {}, - }; - #>>> + my $self = { path => path($path) }; bless($self, $class); # Parse the file into the newly-created object. @@ -157,6 +153,50 @@ sub release_date { return defined($version) ? $version->[1] : undef; } +# Update the version and release date for a package. Add the change to Git if +# the .versions file is at the top of a Git repository. +# +# $package - Name of the package +# $version - New version +# $timestamp - New release date as seconds since epoch +# +# Throws: Text exception on failure +sub update_version { + my ($self, $package, $version, $timestamp) = @_; + my $date = strftime('%Y-%m-%d', localtime($timestamp)); + my $time = strftime('%H:%M:%S', localtime($timestamp)); + + # 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); + return if $product ne $package; + + # We're going to replace the old version with the new one, but we need + # to space-pad one or the other if they're not the same length. + my $version_string = $version; + while (length($old_version) > length($version_string)) { + $version_string .= q{ }; + } + while (length($old_version) < length($version_string)) { + $old_version .= q{ }; + } + + # Make the replacement. + $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; + $_ = $line; + }; + + # 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->_read_data(); + return; +} + # Return the latest version for a given package. # # $package - Name of the package @@ -274,6 +314,13 @@ PATH, or 0 if no releases affect that file. Return the release date of the latest release of PACKAGE (in UTC), or C<undef> if there is no release information for PACKAGE. +=item update_version(PACKAGE, VERSION, TIMESTAMP) + +Given a new VERSION and TIMESTAMP (in seconds since epoch) for a release of +PACKAGE, update the release information in the F<.versions> file for that +package accordingly. If the F<.versions> file is at the root of a Git +repository, this change will be staged with C<git add>. + =item version(PACKAGE) Return the version of the latest release of PACKAGE, or C<undef> if there is diff --git a/lib/App/DocKnot/Util.pm b/lib/App/DocKnot/Util.pm index 5999b2b..e97be4c 100644 --- a/lib/App/DocKnot/Util.pm +++ b/lib/App/DocKnot/Util.pm @@ -18,8 +18,9 @@ use warnings; use Carp qw(croak); use Exporter qw(import); use List::SomeUtils qw(all); +use Sort::Versions qw(versioncmp); -our @EXPORT_OK = qw(is_newer print_checked print_fh); +our @EXPORT_OK = qw(is_newer latest_tarball print_checked print_fh); ############################################################################## # Public interface @@ -39,6 +40,47 @@ sub is_newer { return all { $file_mtime >= $_ } @others_mtimes; } +# Find the files for a given package with the latest version and return them +# along with some associated metadata. +# +# $path - Path::Tiny path to directory +# $tarname - Name of the tarball before the version component +# +# 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 +sub latest_tarball { + my ($path, $tarname) = @_; + + # Collect the list of matching files and extract their version numbers. + return if !$path->is_dir(); + my $regex = qr{ \A \Q$tarname\E - ([\d.]+) [.] }xms; + my @files = map { $_->basename() } $path->children($regex); + my @versions = map { m{ $regex }xms ? [$1, $_] : () } @files; + return if !@versions; + + # Find the latest version and filter the list of files down to only that + # version. + @versions = reverse(sort { versioncmp($a->[0], $b->[0]) } @versions); + 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, + }; + #<<< +} + # print with error checking. autodie unfortunately can't help us because # print can't be prototyped and hence can't be overridden. # @@ -95,7 +137,8 @@ App::DocKnot::Util - Shared utility functions for other DocKnot modules =head1 REQUIREMENTS -Perl 5.24 or later and the List::SomeUtils module, available from CPAN. +Perl 5.24 or later and the modules List::SomeUtils and Sort::Versions, +available from CPAN. =head1 DESCRIPTION @@ -114,6 +157,28 @@ 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. +=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: + +=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. + +=item version + +The version number extracted from this set of files. + +=back + =item print_checked(ARG[, ARG ...]) The same as print (without a file handle argument), except that it throws a @@ -137,7 +202,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 |