Commit bbd8488f authored by Rémy Oudompheng's avatar Rémy Oudompheng
Browse files

Update Perl libraries to version of TeXLive 2019 installer

parent 78f00238
# $Id: TLConfig.pm 46841 2018-03-05 16:31:19Z karl $
# $Id: TLConfig.pm 53076 2019-12-10 06:20:44Z preining $
# TeXLive::TLConfig.pm - module exporting configuration values
# Copyright 2007-2018 Norbert Preining
# Copyright 2007-2019 Norbert Preining
# This file is licensed under the GNU General Public License version 2
# or any later version.
package TeXLive::TLConfig;
my $svnrev = '$Revision: 46841 $';
my $svnrev = '$Revision: 53076 $';
my $_modulerevision = ($svnrev =~ m/: ([0-9]+) /) ? $1 : "unknown";
sub module_revision { return $_modulerevision; }
......@@ -22,10 +22,15 @@ BEGIN {
$MetaCategoriesRegexp
$CategoriesRegexp
$DefaultCategory
$DefaultContainerFormat
$DefaultContainerExtension
@AcceptedFallbackDownloaders
%FallbackDownloaderProgram
%FallbackDownloaderArgs
$DefaultCompressorFormat
$CompressorExtRegexp
%Compressors
$InfraLocation
$DatabaseName
$DatabaseLocation
$PackageBackupDir
$BlockSize
$Archive
......@@ -52,7 +57,7 @@ BEGIN {
# the year of our release, will be used in the location of the
# network packages, and in menu names, and other places.
$ReleaseYear = 2018;
$ReleaseYear = 2019;
# users can upgrade from this year to the current year; might be the
# same as the release year, or any number of releases earlier.
......@@ -78,12 +83,17 @@ our $DefaultCategory = "Package";
# relative to a root (e.g., the Master/, or the installation path)
our $InfraLocation = "tlpkg";
our $DatabaseName = "texlive.tlpdb";
our $DatabaseLocation = "$InfraLocation/$DatabaseName";
# location of backups in default autobackup setting (under tlpkg)
our $PackageBackupDir = "$InfraLocation/backups";
# for computing disk usage; this is most common.
our $BlockSize = 4096;
# timeout for network connections (wget, LWP) in seconds
our $NetworkTimeout = 30;
our $Archive = "archive";
our $TeXLiveServerURL = "http://mirror.ctan.org";
# from 2009 on we try to put them all into tlnet directly without any
......@@ -103,9 +113,43 @@ if ($^O =~ /^MSWin/i) {
$CriticalPackagesRegexp = '^(texlive\.infra|tlperl\.win32$)';
}
#
our @AcceptedFallbackDownloaders = qw/curl wget/;
our %FallbackDownloaderProgram = ( 'wget' => 'wget', 'curl' => 'curl');
our %FallbackDownloaderArgs = (
'curl' => ['--user-agent', 'texlive/curl', '--retry', '4', '--retry-delay', '5',
'--fail', '--location',
'--connect-timeout', "$NetworkTimeout", '--silent', '--output'],
'wget' => ['--user-agent=texlive/wget', '--tries=4',
"--timeout=$NetworkTimeout", '-q', '-O'],
);
# the way we package things on the web
our $DefaultContainerFormat = "xz";
our $DefaultContainerExtension = "tar.$DefaultContainerFormat";
our $DefaultCompressorFormat = "xz";
# priority defines which compressor is selected for backups/rollback containers
# less is better
our %Compressors = (
"lz4" => {
"decompress_args" => ["-dcf"],
"compress_args" => ["-zfmq"],
"extension" => "lz4",
"priority" => 10,
},
"gzip" => {
"decompress_args" => ["-dcf"],
"compress_args" => ["-f"],
"extension" => "gz",
"priority" => 20,
},
"xz" => {
"decompress_args" => ["-dcf"],
"compress_args" => ["-zf"],
"extension" => "xz",
"priority" => 30,
},
);
our $CompressorExtRegexp = "("
. join("|", map { $Compressors{$_}{'extension'} } keys %Compressors)
. ")";
# archive (not user) settings.
# these can be overridden by putting them into 00texlive.config.tlpsrc
......@@ -114,7 +158,7 @@ our $DefaultContainerExtension = "tar.$DefaultContainerFormat";
our %TLPDBConfigs = (
"container_split_src_files" => 1,
"container_split_doc_files" => 1,
"container_format" => $DefaultContainerFormat,
"container_format" => $DefaultCompressorFormat,
"minrelease" => $MinRelease,
"release" => $ReleaseYear,
"frozen" => 0,
......@@ -185,17 +229,14 @@ our %TLPDBOptions = (
our %TLPDBSettings = (
"platform" => [ "s", "Main platform for this computer" ],
"available_architectures" => [ "l", "All available/installed architectures" ],
"available_architectures" => [ "l","All available/installed architectures" ],
"usertree" => [ "b", "This tree acts as user tree" ]
);
our $WindowsMainMenuName = "TeX Live $ReleaseYear";
# Comma-separated list of engines which do not exist on all platforms.
our $PartialEngineSupport = "luajittex,mfluajit";
# timeout for network connections (wget, LWP) in seconds
our $NetworkTimeout = 30;
our $PartialEngineSupport = "luahbtex,luajittex,mfluajit";
# Flags for error handling across the scripts and modules
# all fine
......@@ -274,6 +315,16 @@ The subdirectory with various infrastructure files (C<texlive.tlpdb>,
tlpobj files, ...) relative to the root of the installation; currently
C<tlpkg>.
=item C<$TeXLive::TLConfig::DatabaseName>
The name of our so-called database file: C<texlive.tlpdb>. It's just a
plain text file, not any kind of relational or other database.
=item C<$TeXLive::TLConfig::DatabaseLocation>
Concatenation of C<InfraLocation> "/" C<DatabaseName>, i.e.,
C<tlpkg/texlive.tlpdb>.
=item C<$TeXLive::TLConfig::BlockSize>
The assumed block size, currently 4k.
......
# $Id: TLCrypto.pm 46745 2018-02-26 18:16:54Z karl $
# TeXLive::TLcrypto.pm - handle checksums and signatures.
# Copyright 2016-2018 Norbert Preining
# $Id: TLCrypto.pm 52158 2019-09-23 18:04:33Z karl $
# TeXLive::TLCrypto.pm - handle checksums and signatures.
# Copyright 2016-2019 Norbert Preining
# This file is licensed under the GNU General Public License version 2
# or any later version.
......@@ -9,10 +9,10 @@ package TeXLive::TLCrypto;
use Digest::MD5;
use TeXLive::TLConfig;
use TeXLive::TLUtils qw(debug ddebug win32 which platform conv_to_w32_path tlwarn tldie);
use TeXLive::TLUtils qw(debug ddebug win32 which platform
conv_to_w32_path tlwarn tldie);
my $svnrev = '$Revision$';
my $svnrev = '$Revision: 52158 $';
my $_modulerevision = ($svnrev =~ m/: ([0-9]+) /) ? $1 : "unknown";
sub module_revision { return $_modulerevision; }
......@@ -57,16 +57,20 @@ BEGIN {
%VerificationStatusDescription
$VS_VERIFIED $VS_CHECKSUM_ERROR $VS_SIGNATURE_ERROR $VS_CONNECTION_ERROR
$VS_UNSIGNED $VS_GPG_UNAVAILABLE $VS_PUBKEY_MISSING $VS_UNKNOWN
$VS_EXPKEYSIG $VS_REVKEYSIG
);
@EXPORT = qw(
%VerificationStatusDescription
$VS_VERIFIED $VS_CHECKSUM_ERROR $VS_SIGNATURE_ERROR $VS_CONNECTION_ERROR
$VS_UNSIGNED $VS_GPG_UNAVAILABLE $VS_PUBKEY_MISSING $VS_UNKNOWN
$VS_EXPKEYSIG $VS_REVKEYSIG
);
}
=pod
=over 4
=item C<< setup_checksum_method() >>
Tries to find a checksum method: check usability of C<Digest::SHA>,
......@@ -144,7 +148,9 @@ sub tlchecksum {
if (!$::checksum_method) {
setup_checksum_method();
}
tldie("no checksum method available\n") if (!$::checksum_method);
tldie("TLCRYPTO::tlchecksum: no checksum method available\n")
if (!$::checksum_method);
if (-r $file) {
my ($out, $ret);
if ($::checksum_method eq "openssl") {
......@@ -163,10 +169,10 @@ sub tlchecksum {
close(FILE);
$ret = 0;
} else {
tldie("unknown checksum program: $::checksum_method\n");
tldie("TLCRYPTO::tlchecksum: unknown checksum program: $::checksum_method\n");
}
if ($ret != 0) {
tlwarn("tlchecksum: cannot compute checksum: $file\n");
tlwarn("TLCRYPTO::tlchecksum: cannot compute checksum: $file\n");
return "";
}
ddebug("tlchecksum: out = $out\n");
......@@ -182,12 +188,13 @@ sub tlchecksum {
}
debug("tlchecksum($file): ===$cs===\n");
if (length($cs) != 128) {
tlwarn("unexpected output from $::checksum_method: $out\n");
tlwarn("TLCRYPTO::tlchecksum: unexpected output from $::checksum_method:"
. " $out\n");
return "";
}
return $cs;
} else {
tlwarn("tlchecksum: given file not readable: $file\n");
tlwarn("TLCRYPTO::tlchecksum: given file not readable: $file\n");
return "";
}
}
......@@ -230,8 +237,10 @@ C<$VS_CONNECTION_ERROR> on connection error,
C<$VS_UNSIGNED> on missing signature file,
C<$VS_GPG_UNAVAILABLE> if no gpg program is available,
C<$VS_PUBKEY_MISSING> if the pubkey is not available,
C<$VS_CHECKSUM_ERROR> on checksum errors,and
C<$VS_SIGNATURE_ERROR> on signature errors.
C<$VS_CHECKSUM_ERROR> on checksum errors,
C<$VS_EXPKEYSIG> if the signature is good but was made with an expired key,
C<$VS_REVKEYSIG> if the signature is good but was made with a revoked key,
and C<$VS_SIGNATURE_ERROR> on signature errors.
In case of errors returns an informal message as second argument.
=cut
......@@ -250,6 +259,17 @@ sub verify_checksum {
debug("verify_checksum: download did not succeed for $checksum_url\n");
return($VS_CONNECTION_ERROR, "download did not succeed: $checksum_url");
}
# check that we have a non-trivial size for the checksum file
# the size should be at least 128 + 1 + length(filename) > 129
{
my $css = -s $checksum_file;
if ($css <= 128) {
debug("verify_checksum: size of checksum file suspicious: $css\n");
return($VS_CONNECTION_ERROR, "download corrupted: $checksum_url");
}
}
# check the signature
my ($ret, $msg) = verify_signature($checksum_file, $checksum_url);
......@@ -399,6 +419,8 @@ gpg signature in C<$url.asc>.
Returns
$VS_VERIFIED on success,
$VS_REVKEYSIG on good signature but from revoked key,
$VS_EXPKEYSIG on good signature but from expired key,
$VS_UNSIGNED on missing signature file,
$VS_SIGNATURE_ERROR on signature error,
$VS_GPG_UNAVAILABLE if no gpg is available, and
......@@ -416,22 +438,48 @@ sub verify_signature {
my $signature_file
= TeXLive::TLUtils::download_to_temp_or_file($signature_url);
if ($signature_file) {
{
# we expect a signature to be at least
# 30 header line + 30 footer line + 256 > 300
my $sigsize = -s $signature_file;
if ($sigsize < 300) {
debug("cryptographic signature seems to be corrupted (size $sigsize<300): $signature_url, $signature_file\n");
return($VS_UNSIGNED, "cryptographic signature download seems to be corrupted (size $sigsize<300)");
}
}
# check also the first line of the signature file for
# -----BEGIN PGP SIGNATURE-----
{
open my $file, '<', $signature_file;
chomp(my $firstLine = <$file>);
close $file;
if ($firstLine !~ m/^-----BEGIN PGP SIGNATURE-----/) {
debug("cryptographic signature seems to be corrupted (first line not signature): $signature_url, $signature_file, $firstLine\n");
return($VS_UNSIGNED, "cryptographic signature download seems to be corrupted (first line of $signature_url not signature: $firstLine)");
}
}
my ($ret, $out) = gpg_verify_signature($file, $signature_file);
if ($ret == 1) {
if ($ret == $VS_VERIFIED) {
# no need to show the output
debug("cryptographic signature of $url verified\n");
return($VS_VERIFIED);
} elsif ($ret == -1) {
} elsif ($ret == $VS_PUBKEY_MISSING) {
return($VS_PUBKEY_MISSING, $out);
} elsif ($ret == $VS_EXPKEYSIG) {
return($VS_EXPKEYSIG, $out);
} elsif ($ret == $VS_REVKEYSIG) {
return($VS_REVKEYSIG, $out);
} else {
return($VS_SIGNATURE_ERROR, <<GPGERROR);
cryptographic signature verification of
$file
against
$signature_url
failed. Output was
failed. Output was:
$out
Please report to texlive\@tug.org
Please try from a different mirror and/or wait a few minutes
and try again; usually this is because of transient updates.
If problems persist, feel free to report to texlive\@tug.org.
GPGERROR
}
} else {
......@@ -468,19 +516,29 @@ sub gpg_verify_signature {
close($status_fh);
my ($out, $ret)
= TeXLive::TLUtils::run_cmd("$::gpg --status-file \"$status_file\" --verify $sig_quote $file_quote 2>&1");
# read status file
open($status_fd, "<", $status_file) || die("Cannot open status file: $!");
my @status_lines = <$status_fd>;
close($status_fd);
chomp(@status_lines);
debug(join("\n", "STATUS OUTPUT", @status_lines));
if ($ret == 0) {
# verification still might return success but key is expired!
if (grep(/EXPKEYSIG/, @status_lines)) {
return($VS_EXPKEYSIG, "expired key");
}
if (grep(/REVKEYSIG/, @status_lines)) {
return($VS_REVKEYSIG, "revoked key");
}
debug("verification succeeded, output:\n$out\n");
return (1, $out);
return ($VS_VERIFIED, $out);
} else {
open($status_fd, "<", $status_file) || die("Cannot open status file: $!");
while (<$status_fd>) {
if (m/^\[GNUPG:\] NO_PUBKEY (.*)/) {
close($status_fd);
debug("missing pubkey $1\n");
return (-1, "missing pubkey $1");
}
if (grep(/^\[GNUPG:\] NO_PUBKEY (.*)/, @status_lines)) {
debug("missing pubkey $1\n");
return ($VS_PUBKEY_MISSING, "missing pubkey $1");
}
return (0, $out);
# we could do more checks on what is the actual problem here!
return ($VS_SIGNATURE_ERROR, $out);
}
}
......@@ -499,6 +557,9 @@ our $VS_CONNECTION_ERROR = -1;
our $VS_UNSIGNED = -2;
our $VS_GPG_UNAVAILABLE = -3;
our $VS_PUBKEY_MISSING = -4;
our $VS_EXPKEYSIG = -5;
our $VS_EXPSIG = -6;
our $VS_REVKEYSIG = -7;
our $VS_UNKNOWN = -100;
our %VerificationStatusDescription = (
......@@ -509,6 +570,8 @@ our %VerificationStatusDescription = (
$VS_UNSIGNED => 'unsigned',
$VS_GPG_UNAVAILABLE => 'gpg unavailable',
$VS_PUBKEY_MISSING => 'pubkey missing',
$VS_EXPKEYSIG => 'valid signature with expired key',
$VS_EXPSIG => 'valid but expired signature',
$VS_UNKNOWN => 'unknown',
);
......
This diff is collapsed.
# $Id: TLPOBJ.pm 46745 2018-02-26 18:16:54Z karl $
# $Id: TLPOBJ.pm 53204 2019-12-21 23:18:19Z karl $
# TeXLive::TLPOBJ.pm - module for using tlpobj files
# Copyright 2007-2018 Norbert Preining
# Copyright 2007-2019 Norbert Preining
# This file is licensed under the GNU General Public License version 2
# or any later version.
package TeXLive::TLPOBJ;
my $svnrev = '$Revision: 46745 $';
my $svnrev = '$Revision: 53204 $';
my $_modulerevision = ($svnrev =~ m/: ([0-9]+) /) ? $1 : "unknown";
sub module_revision { return $_modulerevision; }
use TeXLive::TLConfig qw($DefaultCategory $CategoriesRegexp
$MetaCategoriesRegexp $InfraLocation
%Compressors $DefaultCompressorFormat
$RelocPrefix $RelocTree);
use TeXLive::TLCrypto;
use TeXLive::TLTREE;
......@@ -118,16 +119,17 @@ sub from_fh {
# do manual parsing
# this is not optimal, but since we support only two tags there
# are not so many cases
if ($rest =~ m/^details="(.*)"\s*$/) {
$self->{'docfiledata'}{$f}{'details'} = $1;
} elsif ($rest =~ m/^language="(.*)"\s*$/) {
$self->{'docfiledata'}{$f}{'language'} = $1;
} elsif ($rest =~ m/^language="(.*)"\s+details="(.*)"\s*$/) {
# Warning: need tp check the double cases first!!!
if ($rest =~ m/^language="(.*)"\s+details="(.*)"\s*$/) {
$self->{'docfiledata'}{$f}{'details'} = $2;
$self->{'docfiledata'}{$f}{'language'} = $1;
} elsif ($rest =~ m/^details="(.*)"\s+language="(.*)"\s*$/) {
$self->{'docfiledata'}{$f}{'details'} = $1;
$self->{'docfiledata'}{$f}{'language'} = $2;
} elsif ($rest =~ m/^details="(.*)"\s*$/) {
$self->{'docfiledata'}{$f}{'details'} = $1;
} elsif ($rest =~ m/^language="(.*)"\s*$/) {
$self->{'docfiledata'}{$f}{'language'} = $1;
} else {
tlwarn("$0: Unparsable tagging in TLPDB line: $line\n");
}
......@@ -261,7 +263,7 @@ sub _recompute_size {
$nrivblocks += int($s/$TeXLive::TLConfig::BlockSize);
$nrivblocks++ if (($s%$TeXLive::TLConfig::BlockSize) > 0);
} else {
printf STDERR "size for $f not defined, strange ...\n";
tlwarn("$0: (TLPOBJ::_recompute_size) size of $type $f undefined?!\n");
}
}
}
......@@ -368,6 +370,7 @@ sub writeout {
}
# writeout all the catalogue keys
foreach my $k (sort keys %{$self->cataloguedata}) {
next if $k eq "date";
print $fd "catalogue-$k ", $self->cataloguedata->{$k}, "\n";
}
}
......@@ -456,8 +459,8 @@ sub as_json {
my %newd;
$newd{'file'} = $f;
if (defined($dfd->{$f})) {
# TODO should we check that there are actually only "details"
# and "language" as key?
# "details" and "language" keys now, but more could be added any time.
# (Such new keys would have to be added in update_from_catalogue.)
for my $k (keys %{$dfd->{$f}}) {
$newd{$k} = $dfd->{$f}->{$k};
}
......@@ -507,7 +510,7 @@ sub replace_reloc_prefix {
}
$self->docfiledata(%newdata);
# if there are bin files they have definitely NOT the
# texmf-dist prefix, so we cannot cancel it anyway
# texmf-dist prefix, so no reloc to replace
}
sub cancel_common_texmf_tree {
......@@ -557,10 +560,19 @@ sub common_texmf_tree {
sub make_container {
my ($self,$type,$instroot,$destdir,$containername,$relative) = @_;
if (($type ne "xz") && ($type ne "tar")) {
die "$0: TLPOBJ supports tar and xz containers, not $type";
my ($self, $type, $instroot, %other) = @_;
my $destdir = ($other{'destdir'} || undef);
my $containername = ($other{'containername'} || undef);
my $relative = ($other{'relative'} || undef);
my $user = ($other{'user'} || undef);
my $copy_instead_of_link = ($other{'copy_instead_of_link'} || undef);
if (!($type eq 'tar' ||
TeXLive::TLUtils::member($type, @{$::progs{'working_compressors'}}))) {
tlwarn "$0: TLPOBJ supports @{$::progs{'working_compressors'}} and tar containers, not $type\n";
tlwarn "$0: falling back to $DefaultCompressorFormat as container type!\n";
$type = $DefaultCompressorFormat;
}
if (!defined($containername)) {
$containername = $self->name;
}
......@@ -621,28 +633,21 @@ sub make_container {
$selfcopy->writeout(\*TMP);
close(TMP);
push(@files, "$tlpobjdir/$self->{'name'}.tlpobj");
$tarname = "$containername.tar";
if ($type eq "tar") {
$containername = $tarname;
} else {
$containername = "$tarname.xz";
}
# Switch to versioned containers
# $tarname = "$containername.tar";
$tarname = "$containername.r" . $self->revision . ".tar";
my $unversionedtar;
$unversionedtar = "$containername.tar" if (! $user);
# start the fun
my $tar = $::progs{'tar'};
my $xz;
if (!defined($tar)) {
tlwarn("$0: programs not set up, trying \"tar\".\n");
$tar = "tar";
}
if ($type eq "xz") {
$xz = $::progs{'xz'};
if (!defined($xz)) {
tlwarn("$0: programs not set up, trying \"xz\".\n");
$xz = "xz";
}
}
$containername = $tarname;
# Here we need to distinguish between making the master containers for
# tlnet (where we can assume GNU tar) and making backups on a user's
# machine (where we can assume nothing). We determine this by whether
......@@ -654,7 +659,9 @@ sub make_container {
# overflow standard tar format and result in special things being
# done. We don't want the GNU-specific special things.
#
my $is_user_container = ( $containername =~ /\.r[0-9]/ );
# We use versioned containers throughout, user mode is determined by
# argument.
my $is_user_container = $user;
my @attrs
= $is_user_container
? ()
......@@ -706,7 +713,7 @@ sub make_container {
# A complication, as always. collapse_dirs returns absolute paths.
# We want to change them back to relative so that the backup tar
# has the same structure.
# in relative mode we have to remove the texmf-dist prefix, too
# In relative mode we have to remove the texmf-dist prefix, too.
s,^$instroot/,, foreach @files_to_backup;
if ($relative) {
s,^$RelocTree/,, foreach @files_to_backup;
......@@ -717,15 +724,53 @@ sub make_container {
# Run tar. Unlink both here in case the container is also plain tar.
unlink("$destdir/$tarname");
unlink("$destdir/$unversionedtar") if (! $user);
unlink("$destdir/$containername");
xsystem(@cmdline);
# compress it.
if ($type eq "xz") {
if ($type ne 'tar') {
# compress it
my $compressor = $::progs{$type};
if (!defined($compressor)) {
# fall back to $type as compressor, but that shouldn't happen
tlwarn("$0: programs not set up, trying \"$type\".\n");
$compressor = $type;
}
my @compressorargs = @{$Compressors{$type}{'compress_args'}};
my $compressorextension = $Compressors{$type}{'extension'};
$containername = "$tarname.$compressorextension";
debug("selected compressor: $compressor with @compressorargs, "
. "on $destdir/$tarname\n");
# compress it.
if (-r "$destdir/$tarname") {
system($xz, "--force", "-z", "$destdir/$tarname");
# system return 0 on success
if (system($compressor, @compressorargs, "$destdir/$tarname")) {
tlwarn("$0: Couldn't compress $destdir/$tarname\n");
return (0,0, "");
}
# make sure we remove the original tar since old lz4 versions
# cannot automatically delete it.
# We remove the tar file only when the compressed file was
# correctly created, something that should only happen in the
# most strange cases.
unlink("$destdir/$tarname")
if ((-r "$destdir/$tarname") && (-r "$destdir/$containername"));
# in case of system containers also create the links to the
# versioned containers
if (! $user) {
my $linkname = "$destdir/$unversionedtar.$compressorextension";
unlink($linkname) if (-r $linkname);
if ($copy_instead_of_link) {
TeXLive::TLUtils::copy("-f", "$destdir/$containername", $linkname)
} else {
if (!symlink($containername, $linkname)) {
tlwarn("$0: Couldn't generate link $linkname -> $containername?\n");
}
}
}
} else {
tlwarn("$0: Couldn't find $destdir/$tarname to run $xz\n");
tlwarn("$0: Couldn't find $destdir/$tarname to run $compressor\n");
return (0, 0, "");
}
}
......@@ -751,7 +796,7 @@ sub make_container {
rmdir($InfraLocation) if $removetlpkgdir;
xchdir($cwd);
debug(" done $containername, size $size, $checksum\n");
debug(" done $containername, size $size, csum $checksum\n");
return ($size, $checksum, "$destdir/$containername");
}
......@@ -801,21 +846,10 @@ sub update_from_catalogue {
$tlcname = lc($tlcname);
if (defined($tlc->entries->{$tlcname})) {
my $entry = $tlc->entries->{$tlcname};
# Record the id of the catalogue entry if it's found due to
# quest4texlive.
# Record the id of the catalogue entry if it's found.
if ($entry->entry->{'id'} ne $tlcname) {
$self->catalogue($entry->entry->{'id'});
}
if (defined($entry->entry->{'date'})) {
my $foo = $entry->entry->{'date'};
$foo =~ s/^.Date: //;
# trying to extract the interesting part of a subversion date
# keyword expansion here, e.g.,
# $Date: 2018-02-26 19:16:54 +0100 (Mon, 26 Feb 2018) $
# ->2007-08-15 19:43:35 +0100
$foo =~ s/ \(.*\)( *\$ *)$//; # maybe nothing after parens
$self->cataloguedata->{'date'} = $foo;