diff options
Diffstat (limited to 'scripts/Dpkg/Source')
-rw-r--r-- | scripts/Dpkg/Source/Archive.pm | 240 | ||||
-rw-r--r-- | scripts/Dpkg/Source/BinaryFiles.pm | 161 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Format.pm | 191 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Functions.pm | 124 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Package.pm | 741 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Package/V1.pm | 512 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Package/V2.pm | 744 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Package/V3/Bzr.pm | 213 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Package/V3/Custom.pm | 74 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Package/V3/Git.pm | 283 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Package/V3/Native.pm | 121 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Package/V3/Quilt.pm | 269 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Patch.pm | 697 | ||||
-rw-r--r-- | scripts/Dpkg/Source/Quilt.pm | 383 |
14 files changed, 4753 insertions, 0 deletions
diff --git a/scripts/Dpkg/Source/Archive.pm b/scripts/Dpkg/Source/Archive.pm new file mode 100644 index 0000000..88e6700 --- /dev/null +++ b/scripts/Dpkg/Source/Archive.pm @@ -0,0 +1,240 @@ +# Copyright © 2008 Raphaël Hertzog <hertzog@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Archive; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use Carp; +use Errno qw(ENOENT); +use File::Temp qw(tempdir); +use File::Basename qw(basename); +use File::Spec; +use File::Find; +use Cwd; + +use Dpkg (); +use Dpkg::Gettext; +use Dpkg::ErrorHandling; +use Dpkg::IPC; +use Dpkg::Source::Functions qw(erasedir fixperms); + +use parent qw(Dpkg::Compression::FileHandle); + +sub create { + my ($self, %opts) = @_; + $opts{options} //= []; + my %spawn_opts; + # Possibly run tar from another directory + if ($opts{chdir}) { + $spawn_opts{chdir} = $opts{chdir}; + *$self->{chdir} = $opts{chdir}; + } + # Redirect input/output appropriately + $self->ensure_open('w'); + $spawn_opts{to_handle} = $self->get_filehandle(); + $spawn_opts{from_pipe} = \*$self->{tar_input}; + # Try to use a deterministic mtime. + my $mtime = $opts{source_date} // $ENV{SOURCE_DATE_EPOCH} || time; + # Call tar creation process + $spawn_opts{delete_env} = [ 'TAR_OPTIONS' ]; + $spawn_opts{exec} = [ + $Dpkg::PROGTAR, '-cf', '-', '--format=gnu', '--sort=name', + '--mtime', "\@$mtime", '--clamp-mtime', '--null', + '--numeric-owner', '--owner=0', '--group=0', + @{$opts{options}}, '-T', '-', + ]; + *$self->{pid} = spawn(%spawn_opts); + *$self->{cwd} = getcwd(); +} + +sub _add_entry { + my ($self, $file) = @_; + my $cwd = *$self->{cwd}; + croak 'call create() first' unless *$self->{tar_input}; + $file = $2 if ($file =~ /^\Q$cwd\E\/(.+)$/); # Relative names + print({ *$self->{tar_input} } "$file\0") + or syserr(g_('write on tar input')); +} + +sub add_file { + my ($self, $file) = @_; + my $testfile = $file; + if (*$self->{chdir}) { + $testfile = File::Spec->catfile(*$self->{chdir}, $file); + } + croak 'add_file() does not handle directories' + if not -l $testfile and -d _; + $self->_add_entry($file); +} + +sub add_directory { + my ($self, $file) = @_; + my $testfile = $file; + if (*$self->{chdir}) { + $testfile = File::Spec->catdir(*$self->{chdir}, $file); + } + croak 'add_directory() only handles directories' + if -l $testfile or not -d _; + $self->_add_entry($file); +} + +sub finish { + my $self = shift; + + close(*$self->{tar_input}) or syserr(g_('close on tar input')); + wait_child(*$self->{pid}, cmdline => 'tar -cf -'); + delete *$self->{pid}; + delete *$self->{tar_input}; + delete *$self->{cwd}; + delete *$self->{chdir}; + $self->close(); +} + +sub extract { + my ($self, $dest, %opts) = @_; + $opts{options} //= []; + $opts{in_place} //= 0; + $opts{no_fixperms} //= 0; + my %spawn_opts = (wait_child => 1); + + # Prepare destination + my $template = basename($self->get_filename()) . '.tmp-extract.XXXXX'; + unless (-e $dest) { + # Kludge so that realpath works + mkdir($dest) or syserr(g_('cannot create directory %s'), $dest); + } + my $tmp = tempdir($template, DIR => Cwd::realpath("$dest/.."), CLEANUP => 1); + $spawn_opts{chdir} = $tmp; + + # Prepare stuff that handles the input of tar + $self->ensure_open('r', delete_sig => [ 'PIPE' ]); + $spawn_opts{from_handle} = $self->get_filehandle(); + + # Call tar extraction process + $spawn_opts{delete_env} = [ 'TAR_OPTIONS' ]; + $spawn_opts{exec} = [ + $Dpkg::PROGTAR, '-xf', '-', '--no-same-permissions', + '--no-same-owner', @{$opts{options}}, + ]; + spawn(%spawn_opts); + $self->close(); + + # Fix permissions on extracted files because tar insists on applying + # our umask _to the original permissions_ rather than mostly-ignoring + # the original permissions. + # We still need --no-same-permissions because otherwise tar might + # extract directory setgid (which we want inherited, not + # extracted); we need --no-same-owner because putting the owner + # back is tedious - in particular, correct group ownership would + # have to be calculated using mount options and other madness. + fixperms($tmp) unless $opts{no_fixperms}; + + # If we are extracting "in-place" do not remove the destination directory. + if ($opts{in_place}) { + my $canon_basedir = Cwd::realpath($dest); + # On Solaris /dev/null points to /devices/pseudo/mm@0:null. + my $canon_devnull = Cwd::realpath('/dev/null'); + my $check_symlink = sub { + my $pathname = shift; + my $canon_pathname = Cwd::realpath($pathname); + if (not defined $canon_pathname) { + return if $! == ENOENT; + + syserr(g_("pathname '%s' cannot be canonicalized"), $pathname); + } + return if $canon_pathname eq $canon_devnull; + return if $canon_pathname eq $canon_basedir; + return if $canon_pathname =~ m{^\Q$canon_basedir/\E}; + warning(g_("pathname '%s' points outside source root (to '%s')"), + $pathname, $canon_pathname); + }; + + my $move_in_place = sub { + my $relpath = File::Spec->abs2rel($File::Find::name, $tmp); + my $destpath = File::Spec->catfile($dest, $relpath); + + my ($mode, $atime, $mtime); + lstat $File::Find::name + or syserr(g_('cannot get source pathname %s metadata'), $File::Find::name); + ((undef) x 2, $mode, (undef) x 5, $atime, $mtime) = lstat _; + my $src_is_dir = -d _; + + my $dest_exists = 1; + if (not lstat $destpath) { + if ($! == ENOENT) { + $dest_exists = 0; + } else { + syserr(g_('cannot get target pathname %s metadata'), $destpath); + } + } + my $dest_is_dir = -d _; + if ($dest_exists) { + if ($dest_is_dir && $src_is_dir) { + # Refresh the destination directory attributes with the + # ones from the tarball. + chmod $mode, $destpath + or syserr(g_('cannot change directory %s mode'), $File::Find::name); + utime $atime, $mtime, $destpath + or syserr(g_('cannot change directory %s times'), $File::Find::name); + + # We should do nothing, and just walk further tree. + return; + } elsif ($dest_is_dir) { + rmdir $destpath + or syserr(g_('cannot remove destination directory %s'), $destpath); + } else { + $check_symlink->($destpath); + unlink $destpath + or syserr(g_('cannot remove destination file %s'), $destpath); + } + } + # If we are moving a directory, we do not need to walk it. + if ($src_is_dir) { + $File::Find::prune = 1; + } + rename $File::Find::name, $destpath + or syserr(g_('cannot move %s to %s'), $File::Find::name, $destpath); + }; + + find({ + wanted => $move_in_place, + no_chdir => 1, + dangling_symlinks => 0, + }, $tmp); + } else { + # Rename extracted directory + opendir(my $dir_dh, $tmp) or syserr(g_('cannot opendir %s'), $tmp); + my @entries = grep { $_ ne '.' && $_ ne '..' } readdir($dir_dh); + closedir($dir_dh); + + erasedir($dest); + + if (scalar(@entries) == 1 && ! -l "$tmp/$entries[0]" && -d _) { + rename("$tmp/$entries[0]", $dest) + or syserr(g_('unable to rename %s to %s'), + "$tmp/$entries[0]", $dest); + } else { + rename($tmp, $dest) + or syserr(g_('unable to rename %s to %s'), $tmp, $dest); + } + } + erasedir($tmp); +} + +1; diff --git a/scripts/Dpkg/Source/BinaryFiles.pm b/scripts/Dpkg/Source/BinaryFiles.pm new file mode 100644 index 0000000..48c84c8 --- /dev/null +++ b/scripts/Dpkg/Source/BinaryFiles.pm @@ -0,0 +1,161 @@ +# Copyright © 2008-2011 Raphaël Hertzog <hertzog@debian.org> +# Copyright © 2008-2015 Guillem Jover <guillem@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::BinaryFiles; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use Cwd; +use File::Path qw(make_path); +use File::Spec; +use File::Find; + +use Dpkg::ErrorHandling; +use Dpkg::Gettext; +use Dpkg::Source::Functions qw(is_binary); + +sub new { + my ($this, $dir) = @_; + my $class = ref($this) || $this; + + my $self = { + dir => $dir, + allowed_binaries => {}, + seen_binaries => {}, + include_binaries_path => + File::Spec->catfile($dir, 'debian', 'source', 'include-binaries'), + }; + bless $self, $class; + $self->load_allowed_binaries(); + return $self; +} + +sub new_binary_found { + my ($self, $path) = @_; + + $self->{seen_binaries}{$path} = 1; +} + +sub load_allowed_binaries { + my $self = shift; + my $incbin_file = $self->{include_binaries_path}; + + if (-f $incbin_file) { + open my $incbin_fh, '<', $incbin_file + or syserr(g_('cannot read %s'), $incbin_file); + while (<$incbin_fh>) { + chomp; + s/^\s*//; + s/\s*$//; + next if /^#/ or length == 0; + $self->{allowed_binaries}{$_} = 1; + } + close $incbin_fh; + } +} + +sub binary_is_allowed { + my ($self, $path) = @_; + + return 1 if exists $self->{allowed_binaries}{$path}; + return 0; +} + +sub update_debian_source_include_binaries { + my $self = shift; + + my @unknown_binaries = $self->get_unknown_binaries(); + return unless scalar @unknown_binaries; + + my $incbin_file = $self->{include_binaries_path}; + make_path(File::Spec->catdir($self->{dir}, 'debian', 'source')); + open my $incbin_fh, '>>', $incbin_file + or syserr(g_('cannot write %s'), $incbin_file); + foreach my $binary (@unknown_binaries) { + print { $incbin_fh } "$binary\n"; + info(g_('adding %s to %s'), $binary, 'debian/source/include-binaries'); + $self->{allowed_binaries}{$binary} = 1; + } + close $incbin_fh; +} + +sub get_unknown_binaries { + my $self = shift; + + return grep { not $self->binary_is_allowed($_) } $self->get_seen_binaries(); +} + +sub get_seen_binaries { + my $self = shift; + my @seen = sort keys %{$self->{seen_binaries}}; + + return @seen; +} + +sub detect_binary_files { + my ($self, %opts) = @_; + + my $unwanted_binaries = 0; + my $check_binary = sub { + if (-f and is_binary($_)) { + my $fn = File::Spec->abs2rel($_, $self->{dir}); + $self->new_binary_found($fn); + unless ($opts{include_binaries} or $self->binary_is_allowed($fn)) { + errormsg(g_('unwanted binary file: %s'), $fn); + $unwanted_binaries++; + } + } + }; + my $exclude_glob = '{' . + join(',', map { s/,/\\,/rg } @{$opts{exclude_globs}}) . + '}'; + my $filter_ignore = sub { + # Filter out files that are not going to be included in the debian + # tarball due to ignores. + my %exclude; + my $reldir = File::Spec->abs2rel($File::Find::dir, $self->{dir}); + my $cwd = getcwd(); + # Apply the pattern both from the top dir and from the inspected dir + chdir $self->{dir} + or syserr(g_("unable to chdir to '%s'"), $self->{dir}); + $exclude{$_} = 1 foreach glob $exclude_glob; + chdir $cwd or syserr(g_("unable to chdir to '%s'"), $cwd); + chdir $File::Find::dir + or syserr(g_("unable to chdir to '%s'"), $File::Find::dir); + $exclude{$_} = 1 foreach glob $exclude_glob; + chdir $cwd or syserr(g_("unable to chdir to '%s'"), $cwd); + my @result; + foreach my $fn (@_) { + unless (exists $exclude{$fn} or exists $exclude{"$reldir/$fn"}) { + push @result, $fn; + } + } + return @result; + }; + find({ wanted => $check_binary, preprocess => $filter_ignore, + no_chdir => 1 }, File::Spec->catdir($self->{dir}, 'debian')); + error(P_('detected %d unwanted binary file (add it in ' . + 'debian/source/include-binaries to allow its inclusion).', + 'detected %d unwanted binary files (add them in ' . + 'debian/source/include-binaries to allow their inclusion).', + $unwanted_binaries), $unwanted_binaries) + if $unwanted_binaries; +} + +1; diff --git a/scripts/Dpkg/Source/Format.pm b/scripts/Dpkg/Source/Format.pm new file mode 100644 index 0000000..41596a2 --- /dev/null +++ b/scripts/Dpkg/Source/Format.pm @@ -0,0 +1,191 @@ +# Copyright © 2008-2011 Raphaël Hertzog <hertzog@debian.org> +# Copyright © 2008-2018 Guillem Jover <guillem@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Format; + +=encoding utf8 + +=head1 NAME + +Dpkg::Source::Format - manipulate debian/source/format files + +=head1 DESCRIPTION + +This module provides a class that can manipulate Debian source +package F<debian/source/format> files. + +=cut + +use strict; +use warnings; + +our $VERSION = '1.00'; + +use Dpkg::Gettext; +use Dpkg::ErrorHandling; + +use parent qw(Dpkg::Interface::Storable); + +=head1 METHODS + +=over 4 + +=item $f = Dpkg::Source::Format->new(%opts) + +Creates a new object corresponding to a source package's +F<debian/source/format> file. When the key B<filename> is set, it will +be used to parse and set the format. Otherwise if the B<format> key is +set it will be validated and used to set the format. + +=cut + +sub new { + my ($this, %opts) = @_; + my $class = ref($this) || $this; + my $self = { + filename => undef, + major => undef, + minor => undef, + variant => undef, + }; + bless $self, $class; + + if (exists $opts{filename}) { + $self->load($opts{filename}, compression => 0); + } elsif ($opts{format}) { + $self->set($opts{format}); + } + return $self; +} + +=item $f->set_from_parts($major[, $minor[, $variant]]) + +Sets the source format from its parts. The $major part is mandatory. +The $minor and $variant parts are optional. + +B<Notice>: This function performs no validation. + +=cut + +sub set_from_parts { + my ($self, $major, $minor, $variant) = @_; + + $self->{major} = $major; + $self->{minor} = $minor // 0; + $self->{variant} = $variant; +} + +=item ($major, $minor, $variant) = $f->set($format) + +Sets (and validates) the source $format specified. Will return the parsed +format parts as a list, the optional $minor and $variant parts might be +undef. + +=cut + +sub set { + my ($self, $format) = @_; + + if ($format =~ /^(\d+)(?:\.(\d+))?(?:\s+\(([a-z0-9]+)\))?$/) { + my ($major, $minor, $variant) = ($1, $2, $3); + + $self->set_from_parts($major, $minor, $variant); + + return ($major, $minor, $variant); + } else { + error(g_("source package format '%s' is invalid"), $format); + } +} + +=item ($major, $minor, $variant) = $f->get() + +=item $format = $f->get() + +Gets the source format, either as properly formatted scalar, or as a list +of its parts, where the optional $minor and $variant parts might be undef. + +=cut + +sub get { + my $self = shift; + + if (wantarray) { + return ($self->{major}, $self->{minor}, $self->{variant}); + } else { + my $format = "$self->{major}.$self->{minor}"; + $format .= " ($self->{variant})" if defined $self->{variant}; + + return $format; + } +} + +=item $count = $f->parse($fh, $desc) + +Parse the source format string from $fh, with filehandle description $desc. + +=cut + +sub parse { + my ($self, $fh, $desc) = @_; + + my $format = <$fh>; + chomp $format if defined $format; + error(g_('%s is empty'), $desc) + unless defined $format and length $format; + + $self->set($format); + + return 1; +} + +=item $count = $f->load($filename) + +Parse $filename contents for a source package format string. + +=item $str = $f->output([$fh]) + +=item "$f" + +Returns a string representing the source package format version. +If $fh is set, it prints the string to the filehandle. + +=cut + +sub output { + my ($self, $fh) = @_; + + my $str = $self->get(); + + print { $fh } "$str\n" if defined $fh; + + return $str; +} + +=item $f->save($filename) + +Save the source package format into the given $filename. + +=back + +=head1 CHANGES + +=head2 Version 1.00 (dpkg 1.19.3) + +Mark the module as public. + +=cut + +1; diff --git a/scripts/Dpkg/Source/Functions.pm b/scripts/Dpkg/Source/Functions.pm new file mode 100644 index 0000000..0576657 --- /dev/null +++ b/scripts/Dpkg/Source/Functions.pm @@ -0,0 +1,124 @@ +# Copyright © 2008-2010, 2012-2015 Guillem Jover <guillem@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Functions; + +use strict; +use warnings; + +our $VERSION = '0.01'; +our @EXPORT_OK = qw( + erasedir + fixperms + chmod_if_needed + fs_time + is_binary +); + +use Exporter qw(import); +use Errno qw(ENOENT); + +use Dpkg::ErrorHandling; +use Dpkg::Gettext; +use Dpkg::File; +use Dpkg::IPC; + +sub erasedir { + my $dir = shift; + if (not lstat($dir)) { + return if $! == ENOENT; + syserr(g_('cannot stat directory %s (before removal)'), $dir); + } + system 'rm', '-rf', '--', $dir; + subprocerr("rm -rf $dir") if $?; + if (not stat($dir)) { + return if $! == ENOENT; + syserr(g_("unable to check for removal of directory '%s'"), $dir); + } + error(g_("rm -rf failed to remove '%s'"), $dir); +} + +sub fixperms { + my $dir = shift; + my ($mode, $modes_set); + # Unfortunately tar insists on applying our umask _to the original + # permissions_ rather than mostly-ignoring the original + # permissions. We fix it up with chmod -R (which saves us some + # work) but we have to construct a u+/- string which is a bit + # of a palaver. (Numeric doesn't work because we need [ugo]+X + # and [ugo]=<stuff> doesn't work because that unsets sgid on dirs.) + $mode = 0777 & ~umask; + for my $i (0 .. 2) { + $modes_set .= ',' if $i; + $modes_set .= qw(u g o)[$i]; + for my $j (0 .. 2) { + $modes_set .= $mode & (0400 >> ($i * 3 + $j)) ? '+' : '-'; + $modes_set .= qw(r w X)[$j]; + } + } + system('chmod', '-R', '--', $modes_set, $dir); + subprocerr("chmod -R -- $modes_set $dir") if $?; +} + +# Only change the pathname permissions if they differ from the desired. +# +# To be able to build a source tree, a user needs write permissions on it, +# but not necessarily ownership of those files. +sub chmod_if_needed { + my ($newperms, $pathname) = @_; + my $oldperms = (stat $pathname)[2] & 07777; + + return 1 if $oldperms == $newperms; + return chmod $newperms, $pathname; +} + +# Touch the file and read the resulting mtime. +# +# If the file doesn't exist, create it, read the mtime and unlink it. +# +# Use this instead of time() when the timestamp is going to be +# used to set file timestamps. This avoids confusion when an +# NFS server and NFS client disagree about what time it is. +sub fs_time($) { + my $file = shift; + my $is_temp = 0; + if (not -e $file) { + file_touch($file); + $is_temp = 1; + } else { + utime(undef, undef, $file) or + syserr(g_('cannot change timestamp for %s'), $file); + } + stat($file) or syserr(g_('cannot read timestamp from %s'), $file); + my $mtime = (stat(_))[9]; + unlink($file) if $is_temp; + return $mtime; +} + +sub is_binary($) { + my $file = shift; + + # Perform the same check as diff(1), look for a NUL character in the first + # 4 KiB of the file. + open my $fh, '<', $file + or syserr(g_('cannot open file %s for binary detection'), $file); + read $fh, my $buf, 4096, 0; + my $res = index $buf, "\0"; + close $fh; + + return $res >= 0; +} + +1; diff --git a/scripts/Dpkg/Source/Package.pm b/scripts/Dpkg/Source/Package.pm new file mode 100644 index 0000000..412ea5d --- /dev/null +++ b/scripts/Dpkg/Source/Package.pm @@ -0,0 +1,741 @@ +# Copyright © 2008-2011 Raphaël Hertzog <hertzog@debian.org> +# Copyright © 2008-2019 Guillem Jover <guillem@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Package; + +=encoding utf8 + +=head1 NAME + +Dpkg::Source::Package - manipulate Debian source packages + +=head1 DESCRIPTION + +This module provides a class that can manipulate Debian source +packages. While it supports both the extraction and the creation +of source packages, the only API that is officially supported +is the one that supports the extraction of the source package. + +=cut + +use strict; +use warnings; + +our $VERSION = '2.02'; +our @EXPORT_OK = qw( + get_default_diff_ignore_regex + set_default_diff_ignore_regex + get_default_tar_ignore_pattern +); + +use Exporter qw(import); +use POSIX qw(:errno_h :sys_wait_h); +use Carp; +use File::Temp; +use File::Copy qw(cp); +use File::Basename; +use File::Spec; + +use Dpkg::Gettext; +use Dpkg::ErrorHandling; +use Dpkg::Control; +use Dpkg::Checksums; +use Dpkg::Version; +use Dpkg::Compression; +use Dpkg::Path qw(check_files_are_the_same check_directory_traversal); +use Dpkg::Vendor qw(run_vendor_hook); +use Dpkg::Source::Format; +use Dpkg::OpenPGP; +use Dpkg::OpenPGP::ErrorCodes; + +my $diff_ignore_default_regex = ' +# Ignore general backup files +(?:^|/).*~$| +# Ignore emacs recovery files +(?:^|/)\.#.*$| +# Ignore vi swap files +(?:^|/)\..*\.sw.$| +# Ignore baz-style junk files or directories +(?:^|/),,.*(?:$|/.*$)| +# File-names that should be ignored (never directories) +(?:^|/)(?:DEADJOE|\.arch-inventory|\.(?:bzr|cvs|hg|git|mtn-)ignore)$| +# File or directory names that should be ignored +(?:^|/)(?:CVS|RCS|\.deps|\{arch\}|\.arch-ids|\.svn| +\.hg(?:tags|sigs)?|_darcs|\.git(?:attributes|modules|review)?| +\.mailmap|\.shelf|_MTN|\.be|\.bzr(?:\.backup|tags)?)(?:$|/.*$) +'; +# Take out comments and newlines +$diff_ignore_default_regex =~ s/^#.*$//mg; +$diff_ignore_default_regex =~ s/\n//sg; + +no warnings 'qw'; ## no critic (TestingAndDebugging::ProhibitNoWarnings) +my @tar_ignore_default_pattern = qw( +*.a +*.la +*.o +*.so +.*.sw? +*/*~ +,,* +.[#~]* +.arch-ids +.arch-inventory +.be +.bzr +.bzr.backup +.bzr.tags +.bzrignore +.cvsignore +.deps +.git +.gitattributes +.gitignore +.gitmodules +.gitreview +.hg +.hgignore +.hgsigs +.hgtags +.mailmap +.mtn-ignore +.shelf +.svn +CVS +DEADJOE +RCS +_MTN +_darcs +{arch} +); +## use critic + +=head1 FUNCTIONS + +=over 4 + +=item $string = get_default_diff_ignore_regex() + +Returns the default diff ignore regex. + +=cut + +sub get_default_diff_ignore_regex { + return $diff_ignore_default_regex; +} + +=item set_default_diff_ignore_regex($string) + +Set a regex as the new default diff ignore regex. + +=cut + +sub set_default_diff_ignore_regex { + my $regex = shift; + + $diff_ignore_default_regex = $regex; +} + +=item @array = get_default_tar_ignore_pattern() + +Returns the default tar ignore pattern, as an array. + +=cut + +sub get_default_tar_ignore_pattern { + return @tar_ignore_default_pattern; +} + +=back + +=head1 METHODS + +=over 4 + +=item $p = Dpkg::Source::Package->new(%opts, options => {}) + +Creates a new object corresponding to a source package. When the key +B<filename> is set to a F<.dsc> file, it will be used to initialize the +source package with its description. Otherwise if the B<format> key is +set to a valid value, the object will be initialized for that format +(since dpkg 1.19.3). + +The B<options> key is a hash ref which supports the following options: + +=over 8 + +=item skip_debianization + +If set to 1, do not apply Debian changes on the extracted source package. + +=item skip_patches + +If set to 1, do not apply Debian-specific patches. This options is +specific for source packages using format "2.0" and "3.0 (quilt)". + +=item require_valid_signature + +If set to 1, the check_signature() method will be stricter and will error +out if the signature can't be verified. + +=item require_strong_checksums + +If set to 1, the check_checksums() method will be stricter and will error +out if there is no strong checksum. + +=item copy_orig_tarballs + +If set to 1, the extraction will copy the upstream tarballs next the +target directory. This is useful if you want to be able to rebuild the +source package after its extraction. + +=back + +=cut + +# Class methods +sub new { + my ($this, %args) = @_; + my $class = ref($this) || $this; + my $self = { + fields => Dpkg::Control->new(type => CTRL_PKG_SRC), + format => Dpkg::Source::Format->new(), + options => {}, + checksums => Dpkg::Checksums->new(), + openpgp => Dpkg::OpenPGP->new(needs => { api => 'verify' }), + }; + bless $self, $class; + if (exists $args{options}) { + $self->{options} = $args{options}; + } + if (exists $args{filename}) { + $self->initialize($args{filename}); + $self->init_options(); + } elsif ($args{format}) { + $self->{fields}{Format} = $args{format}; + $self->upgrade_object_type(0); + $self->init_options(); + } + + if ($self->{options}{require_valid_signature}) { + $self->{report_verify} = \&error; + } else { + $self->{report_verify} = \&warning; + } + + return $self; +} + +sub init_options { + my $self = shift; + # Use full ignore list by default + # note: this function is not called by V1 packages + $self->{options}{diff_ignore_regex} ||= $diff_ignore_default_regex; + $self->{options}{diff_ignore_regex} .= '|(?:^|/)debian/source/local-.*$'; + $self->{options}{diff_ignore_regex} .= '|(?:^|/)debian/files(?:\.new)?$'; + if (defined $self->{options}{tar_ignore}) { + $self->{options}{tar_ignore} = [ @tar_ignore_default_pattern ] + unless @{$self->{options}{tar_ignore}}; + } else { + $self->{options}{tar_ignore} = [ @tar_ignore_default_pattern ]; + } + push @{$self->{options}{tar_ignore}}, + 'debian/source/local-options', + 'debian/source/local-patch-header', + 'debian/files', + 'debian/files.new'; + $self->{options}{copy_orig_tarballs} //= 0; + + # Skip debianization while specific to some formats has an impact + # on code common to all formats + $self->{options}{skip_debianization} //= 0; + $self->{options}{skip_patches} //= 0; + + # Set default validation checks. + $self->{options}{require_valid_signature} //= 0; + $self->{options}{require_strong_checksums} //= 0; + + # Set default compressor for new formats. + $self->{options}{compression} //= 'xz'; + $self->{options}{comp_level} //= compression_get_level($self->{options}{compression}); + $self->{options}{comp_ext} //= compression_get_file_extension($self->{options}{compression}); +} + +sub initialize { + my ($self, $filename) = @_; + my ($fn, $dir) = fileparse($filename); + error(g_('%s is not the name of a file'), $filename) unless $fn; + $self->{basedir} = $dir || './'; + $self->{filename} = $fn; + + # Read the fields + my $fields = $self->{fields}; + $fields->load($filename); + $self->{is_signed} = $fields->get_option('is_pgp_signed'); + + foreach my $f (qw(Source Version Files)) { + unless (defined($fields->{$f})) { + error(g_('missing critical source control field %s'), $f); + } + } + + $self->{checksums}->add_from_control($fields, use_files_for_md5 => 1); + + $self->upgrade_object_type(0); +} + +sub upgrade_object_type { + my ($self, $update_format) = @_; + $update_format //= 1; + + my $format = $self->{fields}{'Format'} // '1.0'; + my ($major, $minor, $variant) = $self->{format}->set($format); + + my $module = "Dpkg::Source::Package::V$major"; + $module .= '::' . ucfirst $variant if defined $variant; + eval qq{ + pop \@INC if \$INC[-1] eq '.'; + require $module; + \$minor = \$${module}::CURRENT_MINOR_VERSION; + }; + if ($@) { + error(g_("source package format '%s' is not supported: %s"), + $format, $@); + } + if ($update_format) { + $self->{format}->set_from_parts($major, $minor, $variant); + $self->{fields}{'Format'} = $self->{format}->get(); + } + + $module->prerequisites() if $module->can('prerequisites'); + bless $self, $module; +} + +=item $p->get_filename() + +Returns the filename of the DSC file. + +=cut + +sub get_filename { + my $self = shift; + return File::Spec->catfile($self->{basedir}, $self->{filename}); +} + +=item $p->get_files() + +Returns the list of files referenced by the source package. The filenames +usually do not have any path information. + +=cut + +sub get_files { + my $self = shift; + return $self->{checksums}->get_files(); +} + +=item $p->check_checksums() + +Verify the checksums embedded in the DSC file. It requires the presence of +the other files constituting the source package. If any inconsistency is +discovered, it immediately errors out. It will make sure at least one strong +checksum is present. + +If the object has been created with the "require_strong_checksums" option, +then any problem will result in a fatal error. + +=cut + +sub check_checksums { + my $self = shift; + my $checksums = $self->{checksums}; + my $warn_on_weak = 0; + + # add_from_file verify the checksums if they are already existing + foreach my $file ($checksums->get_files()) { + if (not $checksums->has_strong_checksums($file)) { + if ($self->{options}{require_strong_checksums}) { + error(g_('source package uses only weak checksums')); + } else { + $warn_on_weak = 1; + } + } + my $pathname = File::Spec->catfile($self->{basedir}, $file); + $checksums->add_from_file($pathname, key => $file); + } + + warning(g_('source package uses only weak checksums')) if $warn_on_weak; +} + +sub get_basename { + my ($self, $with_revision) = @_; + my $f = $self->{fields}; + unless (exists $f->{'Source'} and exists $f->{'Version'}) { + error(g_('%s and %s fields are required to compute the source basename'), + 'Source', 'Version'); + } + my $v = Dpkg::Version->new($f->{'Version'}); + my $vs = $v->as_string(omit_epoch => 1, omit_revision => !$with_revision); + return $f->{'Source'} . '_' . $vs; +} + +sub find_original_tarballs { + my ($self, %opts) = @_; + $opts{extension} //= compression_get_file_extension_regex(); + $opts{include_main} //= 1; + $opts{include_supplementary} //= 1; + my $basename = $self->get_basename(); + my @tar; + foreach my $dir ('.', $self->{basedir}, $self->{options}{origtardir}) { + next unless defined($dir) and -d $dir; + opendir(my $dir_dh, $dir) or syserr(g_('cannot opendir %s'), $dir); + push @tar, map { File::Spec->catfile($dir, $_) } grep { + ($opts{include_main} and + /^\Q$basename\E\.orig\.tar\.$opts{extension}$/) or + ($opts{include_supplementary} and + /^\Q$basename\E\.orig-[[:alnum:]-]+\.tar\.$opts{extension}$/) + } readdir($dir_dh); + closedir($dir_dh); + } + return @tar; +} + +=item $p->get_upstream_signing_key($dir) + +Get the filename for the upstream key. + +=cut + +sub get_upstream_signing_key { + my ($self, $dir) = @_; + + return "$dir/debian/upstream/signing-key.asc"; +} + +=item $p->armor_original_tarball_signature($bin, $asc) + +Convert a signature from binary to ASCII armored form. If the signature file +does not exist, it is a no-op. If the signature file is already ASCII armored +then simply copy it, otherwise convert it from binary to ASCII armored form. + +=cut + +sub armor_original_tarball_signature { + my ($self, $bin, $asc) = @_; + + if (-e $bin) { + return $self->{openpgp}->armor('SIGNATURE', $bin, $asc); + } + + return; +} + +=item $p->check_original_tarball_signature($dir, @asc) + +Verify the original upstream tarball signatures @asc using the upstream +public keys. It requires the origin upstream tarballs, their signatures +and the upstream signing key, as found in an unpacked source tree $dir. +If any inconsistency is discovered, it immediately errors out. + +=cut + +sub check_original_tarball_signature { + my ($self, $dir, @asc) = @_; + + my $upstream_key = $self->get_upstream_signing_key($dir); + if (not -e $upstream_key) { + warning(g_('upstream tarball signatures but no upstream signing key')); + return; + } + + foreach my $asc (@asc) { + my $datafile = $asc =~ s/\.asc$//r; + + info(g_('verifying %s'), $asc); + my $rc = $self->{openpgp}->verify($datafile, $asc, $upstream_key); + if ($rc) { + $self->{report_verify}->(g_('cannot verify upstream tarball signature for %s: %s'), + $datafile, openpgp_errorcode_to_string($rc)); + } + } +} + +=item $bool = $p->is_signed() + +Returns 1 if the DSC files contains an embedded OpenPGP signature. +Otherwise returns 0. + +=cut + +sub is_signed { + my $self = shift; + return $self->{is_signed}; +} + +=item $p->check_signature() + +Implement the same OpenPGP signature check that dpkg-source does. +In case of problems, it prints a warning or errors out. + +If the object has been created with the "require_valid_signature" option, +then any problem will result in a fatal error. + +=cut + +sub check_signature { + my $self = shift; + my $dsc = $self->get_filename(); + my @certs; + + push @certs, $self->{openpgp}->get_trusted_keyrings(); + + foreach my $vendor_keyring (run_vendor_hook('package-keyrings')) { + if (-r $vendor_keyring) { + push @certs, $vendor_keyring; + } + } + + my $rc = $self->{openpgp}->inline_verify($dsc, undef, @certs); + if ($rc) { + $self->{report_verify}->(g_('cannot verify inline signature for %s: %s'), + $dsc, openpgp_errorcode_to_string($rc)); + } +} + +sub describe_cmdline_options { + return; +} + +sub parse_cmdline_options { + my ($self, @opts) = @_; + foreach my $option (@opts) { + if (not $self->parse_cmdline_option($option)) { + warning(g_('%s is not a valid option for %s'), $option, ref $self); + } + } +} + +sub parse_cmdline_option { + return 0; +} + +=item $p->extract($targetdir) + +Extracts the source package in the target directory $targetdir. Beware +that if $targetdir already exists, it will be erased (as long as the +no_overwrite_dir option is set). + +=cut + +sub extract { + my ($self, $newdirectory) = @_; + + my ($ok, $error) = version_check($self->{fields}{'Version'}); + if (not $ok) { + if ($self->{options}{ignore_bad_version}) { + warning($error); + } else { + error($error); + } + } + + # Copy orig tarballs + if ($self->{options}{copy_orig_tarballs}) { + my $basename = $self->get_basename(); + my ($dirname, $destdir) = fileparse($newdirectory); + $destdir ||= './'; + my $ext = compression_get_file_extension_regex(); + foreach my $orig (grep { /^\Q$basename\E\.orig(-[[:alnum:]-]+)?\.tar\.$ext$/ } + $self->get_files()) + { + my $src = File::Spec->catfile($self->{basedir}, $orig); + my $dst = File::Spec->catfile($destdir, $orig); + if (not check_files_are_the_same($src, $dst, 1)) { + cp($src, $dst) + or syserr(g_('cannot copy %s to %s'), $src, $dst); + } + } + } + + # Try extract + $self->do_extract($newdirectory); + + # Check for directory traversals. + if (not $self->{options}{skip_debianization} and not $self->{no_check}) { + # We need to add a trailing slash to handle the debian directory + # possibly being a symlink. + check_directory_traversal($newdirectory, "$newdirectory/debian/"); + } + + # Store format if non-standard so that next build keeps the same format + if ($self->{fields}{'Format'} and + $self->{fields}{'Format'} ne '1.0' and + not $self->{options}{skip_debianization}) + { + my $srcdir = File::Spec->catdir($newdirectory, 'debian', 'source'); + my $format_file = File::Spec->catfile($srcdir, 'format'); + unless (-e $format_file) { + mkdir($srcdir) unless -e $srcdir; + $self->{format}->save($format_file); + } + } + + # Make sure debian/rules is executable + my $rules = File::Spec->catfile($newdirectory, 'debian', 'rules'); + my @s = lstat($rules); + if (not scalar(@s)) { + unless ($! == ENOENT) { + syserr(g_('cannot stat %s'), $rules); + } + warning(g_('%s does not exist'), $rules) + unless $self->{options}{skip_debianization}; + } elsif (-f _) { + chmod($s[2] | 0111, $rules) + or syserr(g_('cannot make %s executable'), $rules); + } else { + warning(g_('%s is not a plain file'), $rules); + } +} + +sub do_extract { + croak 'Dpkg::Source::Package does not know how to unpack a ' . + 'source package; use one of the subclasses'; +} + +# Function used specifically during creation of a source package + +sub before_build { + my ($self, $dir) = @_; +} + +sub build { + my $self = shift; + + $self->do_build(@_); +} + +sub after_build { + my ($self, $dir) = @_; +} + +sub do_build { + croak 'Dpkg::Source::Package does not know how to build a ' . + 'source package; use one of the subclasses'; +} + +sub can_build { + my ($self, $dir) = @_; + return (0, 'can_build() has not been overridden'); +} + +sub add_file { + my ($self, $filename) = @_; + my ($fn, $dir) = fileparse($filename); + if ($self->{checksums}->has_file($fn)) { + croak "tried to add file '$fn' twice"; + } + $self->{checksums}->add_from_file($filename, key => $fn); + $self->{checksums}->export_to_control($self->{fields}, + use_files_for_md5 => 1); +} + +sub commit { + my $self = shift; + + $self->do_commit(@_); +} + +sub do_commit { + my ($self, $dir) = @_; + info(g_("'%s' is not supported by the source format '%s'"), + 'dpkg-source --commit', $self->{fields}{'Format'}); +} + +sub write_dsc { + my ($self, %opts) = @_; + my $fields = $self->{fields}; + + foreach my $f (keys %{$opts{override}}) { + $fields->{$f} = $opts{override}{$f}; + } + + unless ($opts{nocheck}) { + foreach my $f (qw(Source Version Architecture)) { + unless (defined($fields->{$f})) { + error(g_('missing information for critical output field %s'), $f); + } + } + foreach my $f (qw(Maintainer Standards-Version)) { + unless (defined($fields->{$f})) { + warning(g_('missing information for output field %s'), $f); + } + } + } + + foreach my $f (keys %{$opts{remove}}) { + delete $fields->{$f}; + } + + my $filename = $opts{filename}; + $filename //= $self->get_basename(1) . '.dsc'; + open(my $dsc_fh, '>', $filename) + or syserr(g_('cannot write %s'), $filename); + $fields->apply_substvars($opts{substvars}); + $fields->output($dsc_fh); + close($dsc_fh); +} + +=back + +=head1 CHANGES + +=head2 Version 2.02 (dpkg 1.21.10) + +New method: armor_original_tarball_signature(). + +=head2 Version 2.01 (dpkg 1.20.1) + +New method: get_upstream_signing_key(). + +=head2 Version 2.00 (dpkg 1.20.0) + +New method: check_original_tarball_signature(). + +Remove variable: $diff_ignore_default_regexp. + +Hide variable: @tar_ignore_default_pattern. + +=head2 Version 1.03 (dpkg 1.19.3) + +New option: format in new(). + +=head2 Version 1.02 (dpkg 1.18.7) + +New option: require_strong_checksums in check_checksums(). + +=head2 Version 1.01 (dpkg 1.17.2) + +New functions: get_default_diff_ignore_regex(), set_default_diff_ignore_regex(), +get_default_tar_ignore_pattern() + +Deprecated variables: $diff_ignore_default_regexp, @tar_ignore_default_pattern + +=head2 Version 1.00 (dpkg 1.16.1) + +Mark the module as public. + +=cut + +1; diff --git a/scripts/Dpkg/Source/Package/V1.pm b/scripts/Dpkg/Source/Package/V1.pm new file mode 100644 index 0000000..96e2932 --- /dev/null +++ b/scripts/Dpkg/Source/Package/V1.pm @@ -0,0 +1,512 @@ +# Copyright © 2008-2009 Raphaël Hertzog <hertzog@debian.org> +# Copyright © 2008, 2012-2015 Guillem Jover <guillem@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Package::V1; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use Errno qw(ENOENT); +use Cwd; +use File::Basename; +use File::Temp qw(tempfile); +use File::Spec; + +use Dpkg (); +use Dpkg::Gettext; +use Dpkg::ErrorHandling; +use Dpkg::Compression; +use Dpkg::Source::Archive; +use Dpkg::Source::Patch; +use Dpkg::Exit qw(push_exit_handler pop_exit_handler); +use Dpkg::Source::Functions qw(erasedir); +use Dpkg::Source::Package::V3::Native; + +use parent qw(Dpkg::Source::Package); + +our $CURRENT_MINOR_VERSION = '0'; + +sub init_options { + my $self = shift; + + # Don't call $self->SUPER::init_options() on purpose, V1.0 has no + # ignore by default + if ($self->{options}{diff_ignore_regex}) { + $self->{options}{diff_ignore_regex} .= '|(?:^|/)debian/source/local-.*$'; + } else { + $self->{options}{diff_ignore_regex} = '(?:^|/)debian/source/local-.*$'; + } + $self->{options}{diff_ignore_regex} .= '|(?:^|/)debian/files(?:\.new)?$'; + push @{$self->{options}{tar_ignore}}, + 'debian/source/local-options', + 'debian/source/local-patch-header', + 'debian/files', + 'debian/files.new'; + $self->{options}{sourcestyle} //= 'X'; + $self->{options}{skip_debianization} //= 0; + $self->{options}{ignore_bad_version} //= 0; + $self->{options}{abort_on_upstream_changes} //= 0; + + # Set default validation checks. + $self->{options}{require_valid_signature} //= 0; + $self->{options}{require_strong_checksums} //= 0; + + # V1.0 only supports gzip compression. + $self->{options}{compression} //= 'gzip'; + $self->{options}{comp_level} //= compression_get_level('gzip'); + $self->{options}{comp_ext} //= compression_get_file_extension('gzip'); +} + +my @module_cmdline = ( + { + name => '-sa', + help => N_('auto select original source'), + when => 'build', + }, { + name => '-sk', + help => N_('use packed original source (unpack and keep)'), + when => 'build', + }, { + name => '-sp', + help => N_('use packed original source (unpack and remove)'), + when => 'build', + }, { + name => '-su', + help => N_('use unpacked original source (pack and keep)'), + when => 'build', + }, { + name => '-sr', + help => N_('use unpacked original source (pack and remove)'), + when => 'build', + }, { + name => '-ss', + help => N_('trust packed and unpacked original sources are same'), + when => 'build', + }, { + name => '-sn', + help => N_('there is no diff, do main tarfile only'), + when => 'build', + }, { + name => '-sA, -sK, -sP, -sU, -sR', + help => N_('like -sa, -sk, -sp, -su, -sr but may overwrite'), + when => 'build', + }, { + name => '--abort-on-upstream-changes', + help => N_('abort if generated diff has upstream files changes'), + when => 'build', + }, { + name => '-sp', + help => N_('leave original source packed in current directory'), + when => 'extract', + }, { + name => '-su', + help => N_('do not copy original source to current directory'), + when => 'extract', + }, { + name => '-sn', + help => N_('unpack original source tree too'), + when => 'extract', + }, { + name => '--skip-debianization', + help => N_('do not apply debian diff to upstream sources'), + when => 'extract', + }, +); + +sub describe_cmdline_options { + return @module_cmdline; +} + +sub parse_cmdline_option { + my ($self, $opt) = @_; + my $o = $self->{options}; + if ($opt =~ m/^-s([akpursnAKPUR])$/) { + warning(g_('-s%s option overrides earlier -s%s option'), $1, + $o->{sourcestyle}) if $o->{sourcestyle} ne 'X'; + $o->{sourcestyle} = $1; + $o->{copy_orig_tarballs} = 0 if $1 eq 'n'; # Extract option -sn + return 1; + } elsif ($opt eq '--skip-debianization') { + $o->{skip_debianization} = 1; + return 1; + } elsif ($opt eq '--ignore-bad-version') { + $o->{ignore_bad_version} = 1; + return 1; + } elsif ($opt eq '--abort-on-upstream-changes') { + $o->{abort_on_upstream_changes} = 1; + return 1; + } + return 0; +} + +sub do_extract { + my ($self, $newdirectory) = @_; + my $sourcestyle = $self->{options}{sourcestyle}; + my $fields = $self->{fields}; + + $sourcestyle =~ y/X/p/; + unless ($sourcestyle =~ m/[pun]/) { + usageerr(g_('source handling style -s%s not allowed with -x'), + $sourcestyle); + } + + my $basename = $self->get_basename(); + my $basenamerev = $self->get_basename(1); + + # V1.0 only supports gzip compression + my ($tarfile, $difffile); + my $tarsign; + foreach my $file ($self->get_files()) { + if ($file =~ /^(?:\Q$basename\E\.orig|\Q$basenamerev\E)\.tar\.gz$/) { + error(g_('multiple tarfiles in v1.0 source package')) if $tarfile; + $tarfile = $file; + } elsif ($file =~ /^\Q$basename\E\.orig\.tar\.gz\.asc$/) { + $tarsign = $file; + } elsif ($file =~ /^\Q$basenamerev\E\.diff\.gz$/) { + $difffile = $file; + } else { + error(g_('unrecognized file for a %s source package: %s'), + 'v1.0', $file); + } + } + + error(g_('no tarfile in Files field')) unless $tarfile; + my $native = $difffile ? 0 : 1; + if ($native and ($tarfile =~ /\.orig\.tar\.gz$/)) { + warning(g_('native package with .orig.tar')); + $native = 0; # V3::Native doesn't handle orig.tar + } + + if ($native) { + Dpkg::Source::Package::V3::Native::do_extract($self, $newdirectory); + } else { + my $expectprefix = $newdirectory; + $expectprefix .= '.orig'; + + if ($self->{options}{no_overwrite_dir} and -e $newdirectory) { + error(g_('unpack target exists: %s'), $newdirectory); + } else { + erasedir($newdirectory); + } + if (-e $expectprefix) { + rename($expectprefix, "$newdirectory.tmp-keep") + or syserr(g_("unable to rename '%s' to '%s'"), $expectprefix, + "$newdirectory.tmp-keep"); + } + + info(g_('unpacking %s'), $tarfile); + my $tar = Dpkg::Source::Archive->new( + filename => File::Spec->catfile($self->{basedir}, $tarfile), + ); + $tar->extract($expectprefix); + + if ($sourcestyle =~ /u/) { + # -su: keep .orig directory unpacked + if (-e "$newdirectory.tmp-keep") { + error(g_('unable to keep orig directory (already exists)')); + } + system('cp', '-ar', '--', $expectprefix, "$newdirectory.tmp-keep"); + subprocerr("cp $expectprefix to $newdirectory.tmp-keep") if $?; + } + + rename($expectprefix, $newdirectory) + or syserr(g_('failed to rename newly-extracted %s to %s'), + $expectprefix, $newdirectory); + + # rename the copied .orig directory + if (-e "$newdirectory.tmp-keep") { + rename("$newdirectory.tmp-keep", $expectprefix) + or syserr(g_('failed to rename saved %s to %s'), + "$newdirectory.tmp-keep", $expectprefix); + } + } + + if ($difffile and not $self->{options}{skip_debianization}) { + my $patch = File::Spec->catfile($self->{basedir}, $difffile); + info(g_('applying %s'), $difffile); + my $patch_obj = Dpkg::Source::Patch->new(filename => $patch); + my $analysis = $patch_obj->apply($newdirectory, force_timestamp => 1); + my @files = grep { ! m{^\Q$newdirectory\E/debian/} } + sort keys %{$analysis->{filepatched}}; + info(g_('upstream files that have been modified: %s'), + "\n " . join("\n ", @files)) if scalar @files; + } +} + +sub can_build { + my ($self, $dir) = @_; + + # As long as we can use gzip, we can do it as we have + # native packages as fallback + return (0, g_('only supports gzip compression')) + unless $self->{options}{compression} eq 'gzip'; + return 1; +} + +sub do_build { + my ($self, $dir) = @_; + my $sourcestyle = $self->{options}{sourcestyle}; + my @argv = @{$self->{options}{ARGV}}; + my @tar_ignore = map { "--exclude=$_" } @{$self->{options}{tar_ignore}}; + my $diff_ignore_regex = $self->{options}{diff_ignore_regex}; + + if (scalar(@argv) > 1) { + usageerr(g_('-b takes at most a directory and an orig source ' . + 'argument (with v1.0 source package)')); + } + + $sourcestyle =~ y/X/a/; + unless ($sourcestyle =~ m/[akpursnAKPUR]/) { + usageerr(g_('source handling style -s%s not allowed with -b'), + $sourcestyle); + } + + my $sourcepackage = $self->{fields}{'Source'}; + my $basenamerev = $self->get_basename(1); + my $basename = $self->get_basename(); + my $basedirname = $basename; + $basedirname =~ s/_/-/; + + # Try to find a .orig tarball for the package + my $origdir = "$dir.orig"; + my $origtargz = $self->get_basename() . '.orig.tar.gz'; + if (-e $origtargz) { + unless (-f $origtargz) { + error(g_("packed orig '%s' exists but is not a plain file"), $origtargz); + } + } else { + $origtargz = undef; + } + + if (@argv) { + # We have a second-argument <orig-dir> or <orig-targz>, check what it + # is to decide the mode to use + my $origarg = shift(@argv); + if (length($origarg)) { + stat($origarg) + or syserr(g_('cannot stat orig argument %s'), $origarg); + if (-d _) { + $origdir = File::Spec->catdir($origarg); + + $sourcestyle =~ y/aA/rR/; + unless ($sourcestyle =~ m/[ursURS]/) { + error(g_('orig argument is unpacked but source handling ' . + 'style -s%s calls for packed (.orig.tar.<ext>)'), + $sourcestyle); + } + } elsif (-f _) { + $origtargz = $origarg; + $sourcestyle =~ y/aA/pP/; + unless ($sourcestyle =~ m/[kpsKPS]/) { + error(g_('orig argument is packed but source handling ' . + 'style -s%s calls for unpacked (.orig/)'), + $sourcestyle); + } + } else { + error(g_('orig argument %s is not a plain file or directory'), + $origarg); + } + } else { + $sourcestyle =~ y/aA/nn/; + unless ($sourcestyle =~ m/n/) { + error(g_('orig argument is empty (means no orig, no diff) ' . + 'but source handling style -s%s wants something'), + $sourcestyle); + } + } + } elsif ($sourcestyle =~ m/[aA]/) { + # We have no explicit <orig-dir> or <orig-targz>, try to use + # a .orig tarball first, then a .orig directory and fall back to + # creating a native .tar.gz + if ($origtargz) { + $sourcestyle =~ y/aA/pP/; # .orig.tar.<ext> + } else { + if (stat($origdir)) { + unless (-d _) { + error(g_("unpacked orig '%s' exists but is not a directory"), + $origdir); + } + $sourcestyle =~ y/aA/rR/; # .orig directory + } elsif ($! != ENOENT) { + syserr(g_("unable to stat putative unpacked orig '%s'"), $origdir); + } else { + $sourcestyle =~ y/aA/nn/; # Native tar.gz + } + } + } + + my $v = Dpkg::Version->new($self->{fields}->{'Version'}); + if ($sourcestyle =~ m/[kpursKPUR]/) { + error(g_('non-native package version does not contain a revision')) + if $v->is_native(); + } else { + # TODO: This will become fatal in the near future. + warning(g_('native package version may not have a revision')) + unless $v->is_native(); + } + + my ($dirname, $dirbase) = fileparse($dir); + if ($dirname ne $basedirname) { + warning(g_("source directory '%s' is not <sourcepackage>" . + "-<upstreamversion> '%s'"), $dir, $basedirname); + } + + my ($tarname, $tardirname, $tardirbase); + my $tarsign; + if ($sourcestyle ne 'n') { + my ($origdirname, $origdirbase) = fileparse($origdir); + + if ($origdirname ne "$basedirname.orig") { + warning(g_('.orig directory name %s is not <package>' . + '-<upstreamversion> (wanted %s)'), + $origdirname, "$basedirname.orig"); + } + $tardirbase = $origdirbase; + $tardirname = $origdirname; + + $tarname = $origtargz || "$basename.orig.tar.gz"; + $tarsign = "$tarname.asc"; + unless ($tarname =~ /\Q$basename\E\.orig\.tar\.gz/) { + warning(g_('.orig.tar name %s is not <package>_<upstreamversion>' . + '.orig.tar (wanted %s)'), + $tarname, "$basename.orig.tar.gz"); + } + } + + if ($sourcestyle eq 'n') { + $self->{options}{ARGV} = []; # ensure we have no error + Dpkg::Source::Package::V3::Native::do_build($self, $dir); + } elsif ($sourcestyle =~ m/[urUR]/) { + if (stat($tarname)) { + unless ($sourcestyle =~ m/[UR]/) { + error(g_("tarfile '%s' already exists, not overwriting, " . + 'giving up; use -sU or -sR to override'), $tarname); + } + } elsif ($! != ENOENT) { + syserr(g_("unable to check for existence of '%s'"), $tarname); + } + + info(g_('building %s in %s'), + $sourcepackage, $tarname); + + my ($ntfh, $newtar) = tempfile("$tarname.new.XXXXXX", + DIR => getcwd(), UNLINK => 0); + my $tar = Dpkg::Source::Archive->new(filename => $newtar, + compression => compression_guess_from_filename($tarname), + compression_level => $self->{options}{comp_level}); + $tar->create(options => \@tar_ignore, chdir => $tardirbase); + $tar->add_directory($tardirname); + $tar->finish(); + rename($newtar, $tarname) + or syserr(g_("unable to rename '%s' (newly created) to '%s'"), + $newtar, $tarname); + chmod(0666 &~ umask(), $tarname) + or syserr(g_("unable to change permission of '%s'"), $tarname); + } else { + info(g_('building %s using existing %s'), + $sourcepackage, $tarname); + } + + if ($tarname) { + $self->add_file($tarname); + if (-e "$tarname.sig" and not -e "$tarname.asc") { + $self->armor_original_tarball_signature("$tarname.sig", "$tarname.asc"); + } + } + if ($tarsign and -e $tarsign) { + $self->check_original_tarball_signature($dir, $tarsign); + + info(g_('building %s using existing %s'), $sourcepackage, $tarsign); + $self->add_file($tarsign); + } else { + my $key = $self->get_upstream_signing_key($dir); + if (-e $key) { + warning(g_('upstream signing key but no upstream tarball signature')); + } + } + + if ($sourcestyle =~ m/[kpKP]/) { + if (stat($origdir)) { + unless ($sourcestyle =~ m/[KP]/) { + error(g_("orig directory '%s' already exists, not overwriting, ". + 'giving up; use -sA, -sK or -sP to override'), + $origdir); + } + erasedir($origdir); + } elsif ($! != ENOENT) { + syserr(g_("unable to check for existence of orig directory '%s'"), + $origdir); + } + + my $tar = Dpkg::Source::Archive->new(filename => $origtargz); + $tar->extract($origdir); + } + + my $ur; # Unrepresentable changes + if ($sourcestyle =~ m/[kpursKPUR]/) { + my $diffname = "$basenamerev.diff.gz"; + info(g_('building %s in %s'), + $sourcepackage, $diffname); + my ($ndfh, $newdiffgz) = tempfile("$diffname.new.XXXXXX", + DIR => getcwd(), UNLINK => 0); + push_exit_handler(sub { unlink($newdiffgz) }); + my $diff = Dpkg::Source::Patch->new(filename => $newdiffgz, + compression => 'gzip', + compression_level => $self->{options}{comp_level}); + $diff->create(); + $diff->add_diff_directory($origdir, $dir, + basedirname => $basedirname, + diff_ignore_regex => $diff_ignore_regex, + options => []); # Force empty set of options to drop the + # default -p option + $diff->finish() || $ur++; + pop_exit_handler(); + + my $analysis = $diff->analyze($origdir); + my @files = grep { ! m{^debian/} } + map { s{^[^/]+/+}{}r } + sort keys %{$analysis->{filepatched}}; + if (scalar @files) { + warning(g_('the diff modifies the following upstream files: %s'), + "\n " . join("\n ", @files)); + info(g_("use the '3.0 (quilt)' format to have separate and " . + 'documented changes to upstream files, see dpkg-source(1)')); + error(g_('aborting due to --abort-on-upstream-changes')) + if $self->{options}{abort_on_upstream_changes}; + } + + rename($newdiffgz, $diffname) + or syserr(g_("unable to rename '%s' (newly created) to '%s'"), + $newdiffgz, $diffname); + chmod(0666 &~ umask(), $diffname) + or syserr(g_("unable to change permission of '%s'"), $diffname); + + $self->add_file($diffname); + } + + if ($sourcestyle =~ m/[prPR]/) { + erasedir($origdir); + } + + if ($ur) { + errormsg(g_('unrepresentable changes to source')); + exit(1); + } +} + +1; diff --git a/scripts/Dpkg/Source/Package/V2.pm b/scripts/Dpkg/Source/Package/V2.pm new file mode 100644 index 0000000..b3c21e5 --- /dev/null +++ b/scripts/Dpkg/Source/Package/V2.pm @@ -0,0 +1,744 @@ +# Copyright © 2008-2011 Raphaël Hertzog <hertzog@debian.org> +# Copyright © 2008-2015 Guillem Jover <guillem@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Package::V2; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use List::Util qw(first); +use Cwd; +use File::Basename; +use File::Temp qw(tempfile tempdir); +use File::Path qw(make_path); +use File::Spec; +use File::Find; +use File::Copy; + +use Dpkg::Gettext; +use Dpkg::ErrorHandling; +use Dpkg::File; +use Dpkg::Path qw(find_command); +use Dpkg::Compression; +use Dpkg::Source::Archive; +use Dpkg::Source::Patch; +use Dpkg::Source::BinaryFiles; +use Dpkg::Exit qw(push_exit_handler pop_exit_handler); +use Dpkg::Source::Functions qw(erasedir chmod_if_needed fs_time); +use Dpkg::Vendor qw(run_vendor_hook); +use Dpkg::Control; +use Dpkg::Changelog::Parse; + +use parent qw(Dpkg::Source::Package); + +our $CURRENT_MINOR_VERSION = '0'; + +sub init_options { + my $self = shift; + $self->SUPER::init_options(); + $self->{options}{include_removal} //= 0; + $self->{options}{include_timestamp} //= 0; + $self->{options}{include_binaries} //= 0; + $self->{options}{preparation} //= 1; + $self->{options}{skip_patches} //= 0; + $self->{options}{unapply_patches} //= 'auto'; + $self->{options}{skip_debianization} //= 0; + $self->{options}{create_empty_orig} //= 0; + $self->{options}{auto_commit} //= 0; + $self->{options}{ignore_bad_version} //= 0; +} + +my @module_cmdline = ( + { + name => '--include-removal', + help => N_('include removed files in the patch'), + when => 'build', + }, { + name => '--include-timestamp', + help => N_('include timestamp in the patch'), + when => 'build', + }, { + name => '--include-binaries', + help => N_('include binary files in the tarball'), + when => 'build', + }, { + name => '--no-preparation', + help => N_('do not prepare build tree by applying patches'), + when => 'build', + }, { + name => '--no-unapply-patches', + help => N_('do not unapply patches if previously applied'), + when => 'build', + }, { + name => '--unapply-patches', + help => N_('unapply patches if previously applied (default)'), + when => 'build', + }, { + name => '--create-empty-orig', + help => N_('create an empty original tarball if missing'), + when => 'build', + }, { + name => '--abort-on-upstream-changes', + help => N_('abort if generated diff has upstream files changes'), + when => 'build', + }, { + name => '--auto-commit', + help => N_('record generated patches, instead of aborting'), + when => 'build', + }, { + name => '--skip-debianization', + help => N_('do not extract debian tarball into upstream sources'), + when => 'extract', + }, { + name => '--skip-patches', + help => N_('do not apply patches at the end of the extraction'), + when => 'extract', + } +); + +sub describe_cmdline_options { + return @module_cmdline; +} + +sub parse_cmdline_option { + my ($self, $opt) = @_; + if ($opt eq '--include-removal') { + $self->{options}{include_removal} = 1; + return 1; + } elsif ($opt eq '--include-timestamp') { + $self->{options}{include_timestamp} = 1; + return 1; + } elsif ($opt eq '--include-binaries') { + $self->{options}{include_binaries} = 1; + return 1; + } elsif ($opt eq '--no-preparation') { + $self->{options}{preparation} = 0; + return 1; + } elsif ($opt eq '--skip-patches') { + $self->{options}{skip_patches} = 1; + return 1; + } elsif ($opt eq '--unapply-patches') { + $self->{options}{unapply_patches} = 'yes'; + return 1; + } elsif ($opt eq '--no-unapply-patches') { + $self->{options}{unapply_patches} = 'no'; + return 1; + } elsif ($opt eq '--skip-debianization') { + $self->{options}{skip_debianization} = 1; + return 1; + } elsif ($opt eq '--create-empty-orig') { + $self->{options}{create_empty_orig} = 1; + return 1; + } elsif ($opt eq '--abort-on-upstream-changes') { + $self->{options}{auto_commit} = 0; + return 1; + } elsif ($opt eq '--auto-commit') { + $self->{options}{auto_commit} = 1; + return 1; + } elsif ($opt eq '--ignore-bad-version') { + $self->{options}{ignore_bad_version} = 1; + return 1; + } + return 0; +} + +sub do_extract { + my ($self, $newdirectory) = @_; + my $fields = $self->{fields}; + + my $basename = $self->get_basename(); + my $basenamerev = $self->get_basename(1); + + my ($tarfile, $debianfile, %addonfile, %seen); + my ($tarsign, %addonsign); + my $re_ext = compression_get_file_extension_regex(); + foreach my $file ($self->get_files()) { + my $uncompressed = $file; + $uncompressed =~ s/\.$re_ext$/.*/; + $uncompressed =~ s/\.$re_ext\.asc$/.*.asc/; + error(g_('duplicate files in %s source package: %s'), 'v2.0', + $uncompressed) if $seen{$uncompressed}; + $seen{$uncompressed} = 1; + if ($file =~ /^\Q$basename\E\.orig\.tar\.$re_ext$/) { + $tarfile = $file; + } elsif ($file =~ /^\Q$basename\E\.orig\.tar\.$re_ext\.asc$/) { + $tarsign = $file; + } elsif ($file =~ /^\Q$basename\E\.orig-([[:alnum:]-]+)\.tar\.$re_ext$/) { + $addonfile{$1} = $file; + } elsif ($file =~ /^\Q$basename\E\.orig-([[:alnum:]-]+)\.tar\.$re_ext\.asc$/) { + $addonsign{$1} = $file; + } elsif ($file =~ /^\Q$basenamerev\E\.debian\.tar\.$re_ext$/) { + $debianfile = $file; + } else { + error(g_('unrecognized file for a %s source package: %s'), + 'v2.0', $file); + } + } + + unless ($tarfile and $debianfile) { + error(g_('missing orig.tar or debian.tar file in v2.0 source package')); + } + if ($tarsign and $tarfile ne substr $tarsign, 0, -4) { + error(g_('mismatched orig.tar %s for signature %s in source package'), + $tarfile, $tarsign); + } + foreach my $name (keys %addonsign) { + error(g_('missing addon orig.tar for signature %s in source package'), + $addonsign{$name}) + if not exists $addonfile{$name}; + error(g_('mismatched addon orig.tar %s for signature %s in source package'), + $addonfile{$name}, $addonsign{$name}) + if $addonfile{$name} ne substr $addonsign{$name}, 0, -4; + } + + if ($self->{options}{no_overwrite_dir} and -e $newdirectory) { + error(g_('unpack target exists: %s'), $newdirectory); + } else { + erasedir($newdirectory); + } + + # Extract main tarball + info(g_('unpacking %s'), $tarfile); + my $tar = Dpkg::Source::Archive->new( + filename => File::Spec->catfile($self->{basedir}, $tarfile), + ); + $tar->extract($newdirectory, + options => [ '--anchored', '--no-wildcards-match-slash', + '--exclude', '*/.pc', '--exclude', '.pc' ]); + # The .pc exclusion is only needed for 3.0 (quilt) and to avoid + # having an upstream tarball provide a directory with symlinks + # that would be blindly followed when applying the patches + + # Extract additional orig tarballs + foreach my $subdir (sort keys %addonfile) { + my $file = $addonfile{$subdir}; + info(g_('unpacking %s'), $file); + + # If the pathname is an empty directory, just silently remove it, as + # it might be part of a git repository, as a submodule for example. + rmdir "$newdirectory/$subdir"; + if (-e "$newdirectory/$subdir") { + warning(g_("required removal of '%s' installed by original tarball"), + $subdir); + erasedir("$newdirectory/$subdir"); + } + $tar = Dpkg::Source::Archive->new( + filename => File::Spec->catfile($self->{basedir}, $file), + ); + $tar->extract("$newdirectory/$subdir"); + } + + # Stop here if debianization is not wanted + return if $self->{options}{skip_debianization}; + + # Extract debian tarball after removing the debian directory + info(g_('unpacking %s'), $debianfile); + erasedir("$newdirectory/debian"); + $tar = Dpkg::Source::Archive->new( + filename => File::Spec->catfile($self->{basedir}, $debianfile), + ); + $tar->extract($newdirectory, in_place => 1); + + # Apply patches (in a separate method as it might be overridden) + $self->apply_patches($newdirectory, usage => 'unpack') + unless $self->{options}{skip_patches}; +} + +sub get_autopatch_name { + return 'zz_debian-diff-auto'; +} + +sub _get_patches { + my ($self, $dir, %opts) = @_; + $opts{skip_auto} //= 0; + my @patches; + my $pd = "$dir/debian/patches"; + my $auto_patch = $self->get_autopatch_name(); + if (-d $pd) { + opendir(my $dir_dh, $pd) or syserr(g_('cannot opendir %s'), $pd); + foreach my $patch (sort readdir($dir_dh)) { + # patches match same rules as run-parts + next unless $patch =~ /^[\w-]+$/ and -f "$pd/$patch"; + next if $opts{skip_auto} and $patch eq $auto_patch; + push @patches, $patch; + } + closedir($dir_dh); + } + return @patches; +} + +sub apply_patches { + my ($self, $dir, %opts) = @_; + $opts{skip_auto} //= 0; + my @patches = $self->_get_patches($dir, %opts); + return unless scalar(@patches); + my $applied = File::Spec->catfile($dir, 'debian', 'patches', '.dpkg-source-applied'); + open(my $applied_fh, '>', $applied) + or syserr(g_('cannot write %s'), $applied); + print { $applied_fh } "# During $opts{usage}\n"; + my $timestamp = fs_time($applied); + foreach my $patch ($self->_get_patches($dir, %opts)) { + my $path = File::Spec->catfile($dir, 'debian', 'patches', $patch); + info(g_('applying %s'), $patch) unless $opts{skip_auto}; + my $patch_obj = Dpkg::Source::Patch->new(filename => $path); + $patch_obj->apply($dir, force_timestamp => 1, + timestamp => $timestamp, + add_options => [ '-E' ]); + print { $applied_fh } "$patch\n"; + } + close($applied_fh); +} + +sub unapply_patches { + my ($self, $dir, %opts) = @_; + my @patches = reverse($self->_get_patches($dir, %opts)); + return unless scalar(@patches); + my $applied = File::Spec->catfile($dir, 'debian', 'patches', '.dpkg-source-applied'); + my $timestamp = fs_time($applied); + foreach my $patch (@patches) { + my $path = File::Spec->catfile($dir, 'debian', 'patches', $patch); + info(g_('unapplying %s'), $patch) unless $opts{quiet}; + my $patch_obj = Dpkg::Source::Patch->new(filename => $path); + $patch_obj->apply($dir, force_timestamp => 1, verbose => 0, + timestamp => $timestamp, + add_options => [ '-E', '-R' ]); + } + unlink($applied); +} + +sub _upstream_tarball_template { + my $self = shift; + my $ext = '{' . join(',', + sort map { + compression_get_file_extension($_) + } compression_get_list()) . '}'; + return File::Spec->catfile('..', $self->get_basename() . ".orig.tar.$ext"); +} + +sub can_build { + my ($self, $dir) = @_; + return 1 if $self->find_original_tarballs(include_supplementary => 0); + return 1 if $self->{options}{create_empty_orig} and + $self->find_original_tarballs(include_main => 0); + return (0, sprintf(g_('no upstream tarball found at %s'), + $self->_upstream_tarball_template())); +} + +sub before_build { + my ($self, $dir) = @_; + $self->check_patches_applied($dir) if $self->{options}{preparation}; +} + +sub after_build { + my ($self, $dir) = @_; + my $applied = File::Spec->catfile($dir, 'debian', 'patches', '.dpkg-source-applied'); + my $reason = ''; + if (-e $applied) { + $reason = file_slurp($applied); + } + my $opt_unapply = $self->{options}{unapply_patches}; + if (($opt_unapply eq 'auto' and $reason =~ /^# During preparation/) or + $opt_unapply eq 'yes') { + $self->unapply_patches($dir); + } +} + +sub prepare_build { + my ($self, $dir) = @_; + $self->{diff_options} = { + diff_ignore_regex => $self->{options}{diff_ignore_regex} . + '|(^|/)debian/patches/.dpkg-source-applied$', + include_removal => $self->{options}{include_removal}, + include_timestamp => $self->{options}{include_timestamp}, + use_dev_null => 1, + }; + push @{$self->{options}{tar_ignore}}, 'debian/patches/.dpkg-source-applied'; + $self->check_patches_applied($dir) if $self->{options}{preparation}; + if ($self->{options}{create_empty_orig} and + not $self->find_original_tarballs(include_supplementary => 0)) + { + # No main orig.tar, create a dummy one + my $filename = $self->get_basename() . '.orig.tar.' . + $self->{options}{comp_ext}; + my $tar = Dpkg::Source::Archive->new(filename => $filename, + compression_level => $self->{options}{comp_level}); + $tar->create(); + $tar->finish(); + } +} + +sub check_patches_applied { + my ($self, $dir) = @_; + my $applied = File::Spec->catfile($dir, 'debian', 'patches', '.dpkg-source-applied'); + unless (-e $applied) { + info(g_('patches are not applied, applying them now')); + $self->apply_patches($dir, usage => 'preparation'); + } +} + +sub _generate_patch { + my ($self, $dir, %opts) = @_; + my ($dirname, $updir) = fileparse($dir); + my $basedirname = $self->get_basename(); + $basedirname =~ s/_/-/; + + # Identify original tarballs + my ($tarfile, %addonfile); + my $comp_ext_regex = compression_get_file_extension_regex(); + my @origtarfiles; + my @origtarsigns; + foreach my $file (sort $self->find_original_tarballs()) { + if ($file =~ /\.orig\.tar\.$comp_ext_regex$/) { + if (defined($tarfile)) { + error(g_('several orig.tar files found (%s and %s) but only ' . + 'one is allowed'), $tarfile, $file); + } + $tarfile = $file; + } elsif ($file =~ /\.orig-([[:alnum:]-]+)\.tar\.$comp_ext_regex$/) { + $addonfile{$1} = $file; + } else { + next; + } + + push @origtarfiles, $file; + $self->add_file($file); + + # Check for an upstream signature. + if (-e "$file.sig" and not -e "$file.asc") { + $self->armor_original_tarball_signature("$file.sig", "$file.asc"); + } + if (-e "$file.asc") { + push @origtarfiles, "$file.asc"; + push @origtarsigns, "$file.asc"; + $self->add_file("$file.asc") + } + } + + error(g_('no upstream tarball found at %s'), + $self->_upstream_tarball_template()) unless $tarfile; + + if ($opts{usage} eq 'build') { + if (@origtarsigns) { + $self->check_original_tarball_signature($dir, @origtarsigns); + } else { + my $key = $self->get_upstream_signing_key($dir); + if (-e $key) { + warning(g_('upstream signing key but no upstream tarball signature')); + } + } + + foreach my $origtarfile (@origtarfiles) { + info(g_('building %s using existing %s'), + $self->{fields}{'Source'}, $origtarfile); + } + } + + # Unpack a second copy for comparison + my $tmp = tempdir("$dirname.orig.XXXXXX", DIR => $updir); + push_exit_handler(sub { erasedir($tmp) }); + + # Extract main tarball + my $tar = Dpkg::Source::Archive->new(filename => $tarfile); + $tar->extract($tmp); + + # Extract additional orig tarballs + foreach my $subdir (keys %addonfile) { + my $file = $addonfile{$subdir}; + $tar = Dpkg::Source::Archive->new(filename => $file); + $tar->extract("$tmp/$subdir"); + } + + # Copy over the debian directory + erasedir("$tmp/debian"); + system('cp', '-a', '--', "$dir/debian", "$tmp/"); + subprocerr(g_('copy of the debian directory')) if $?; + + # Apply all patches except the last automatic one + $opts{skip_auto} //= 0; + $self->apply_patches($tmp, skip_auto => $opts{skip_auto}, usage => 'build'); + + # Create a patch + my ($difffh, $tmpdiff) = tempfile($self->get_basename(1) . '.diff.XXXXXX', + TMPDIR => 1, UNLINK => 0); + push_exit_handler(sub { unlink($tmpdiff) }); + my $diff = Dpkg::Source::Patch->new(filename => $tmpdiff, + compression => 'none'); + $diff->create(); + $diff->set_header(sub { + if ($opts{header_from} and -e $opts{header_from}) { + my $header_from = Dpkg::Source::Patch->new( + filename => $opts{header_from}); + my $analysis = $header_from->analyze($dir, verbose => 0); + return $analysis->{patchheader}; + } else { + return $self->_get_patch_header($dir); + } + }); + $diff->add_diff_directory($tmp, $dir, basedirname => $basedirname, + %{$self->{diff_options}}, + handle_binary_func => $opts{handle_binary}, + order_from => $opts{order_from}); + error(g_('unrepresentable changes to source')) if not $diff->finish(); + + if (-s $tmpdiff) { + info(g_('local changes detected, the modified files are:')); + my $analysis = $diff->analyze($dir, verbose => 0); + foreach my $fn (sort keys %{$analysis->{filepatched}}) { + print " $fn\n"; + } + } + + # Remove the temporary directory + erasedir($tmp); + pop_exit_handler(); + pop_exit_handler(); + + return $tmpdiff; +} + +sub do_build { + my ($self, $dir) = @_; + my @argv = @{$self->{options}{ARGV}}; + if (scalar(@argv)) { + usageerr(g_("-b takes only one parameter with format '%s'"), + $self->{fields}{'Format'}); + } + $self->prepare_build($dir); + + my $include_binaries = $self->{options}{include_binaries}; + my @tar_ignore = map { "--exclude=$_" } @{$self->{options}{tar_ignore}}; + + my $sourcepackage = $self->{fields}{'Source'}; + my $basenamerev = $self->get_basename(1); + + # Check if the debian directory contains unwanted binary files + my $binaryfiles = Dpkg::Source::BinaryFiles->new($dir); + + $binaryfiles->detect_binary_files( + exclude_globs => $self->{options}{tar_ignore}, + include_binaries => $include_binaries, + ); + + # Handle modified binary files detected by the auto-patch generation + my $handle_binary = sub { + my ($self, $old, $new, %opts) = @_; + + my $file = $opts{filename}; + $binaryfiles->new_binary_found($file); + unless ($include_binaries or $binaryfiles->binary_is_allowed($file)) { + errormsg(g_('cannot represent change to %s: %s'), $file, + g_('binary file contents changed')); + errormsg(g_('add %s in debian/source/include-binaries if you want ' . + 'to store the modified binary in the debian tarball'), + $file); + $self->register_error(); + } + }; + + # Create a patch + my $autopatch = File::Spec->catfile($dir, 'debian', 'patches', + $self->get_autopatch_name()); + my $tmpdiff = $self->_generate_patch($dir, order_from => $autopatch, + header_from => $autopatch, + handle_binary => $handle_binary, + skip_auto => $self->{options}{auto_commit}, + usage => 'build'); + unless (-z $tmpdiff or $self->{options}{auto_commit}) { + info(g_('Hint: make sure the version in debian/changelog matches ' . + 'the unpacked source tree')); + info(g_('you can integrate the local changes with %s'), + 'dpkg-source --commit'); + error(g_('aborting due to unexpected upstream changes, see %s'), + $tmpdiff); + } + push_exit_handler(sub { unlink($tmpdiff) }); + $binaryfiles->update_debian_source_include_binaries() if $include_binaries; + + # Install the diff as the new autopatch + if ($self->{options}{auto_commit}) { + make_path(File::Spec->catdir($dir, 'debian', 'patches')); + $autopatch = $self->register_patch($dir, $tmpdiff, + $self->get_autopatch_name()); + info(g_('local changes have been recorded in a new patch: %s'), + $autopatch) if -e $autopatch; + rmdir(File::Spec->catdir($dir, 'debian', 'patches')); # No check on purpose + } + unlink($tmpdiff) or syserr(g_('cannot remove %s'), $tmpdiff); + pop_exit_handler(); + + # Create the debian.tar + my $debianfile = "$basenamerev.debian.tar." . $self->{options}{comp_ext}; + info(g_('building %s in %s'), $sourcepackage, $debianfile); + my $tar = Dpkg::Source::Archive->new(filename => $debianfile, + compression_level => $self->{options}{comp_level}); + $tar->create(options => \@tar_ignore, chdir => $dir); + $tar->add_directory('debian'); + foreach my $binary ($binaryfiles->get_seen_binaries()) { + $tar->add_file($binary) unless $binary =~ m{^debian/}; + } + $tar->finish(); + + $self->add_file($debianfile); +} + +sub _get_patch_header { + my ($self, $dir) = @_; + + my $ph = File::Spec->catfile($dir, 'debian', 'source', 'local-patch-header'); + unless (-f $ph) { + $ph = File::Spec->catfile($dir, 'debian', 'source', 'patch-header'); + } + if (-f $ph) { + return file_slurp($ph); + } + + if ($self->{options}->{single_debian_patch}) { + return <<'AUTOGEN_HEADER'; +This is an autogenerated patch header for a single-debian-patch file. The +delta against upstream is either kept as a single patch, or maintained +in some VCS, and exported as a single patch instead of more manageable +atomic patches. + +AUTOGEN_HEADER + } + + my $ch_info = changelog_parse(offset => 0, count => 1, + file => $self->{options}{changelog_file}); + return '' if not defined $ch_info; + my $header = Dpkg::Control->new(type => CTRL_UNKNOWN); + $header->{'Description'} = "<short summary of the patch>\n"; + $header->{'Description'} .= +"TODO: Put a short summary on the line above and replace this paragraph +with a longer explanation of this change. Complete the meta-information +with other relevant fields (see below for details). To make it easier, the +information below has been extracted from the changelog. Adjust it or drop +it.\n"; + $header->{'Description'} .= $ch_info->{'Changes'} . "\n"; + $header->{'Author'} = $ch_info->{'Maintainer'}; + my $yyyy_mm_dd = POSIX::strftime('%Y-%m-%d', gmtime); + + my $text; + $text = "$header"; + run_vendor_hook('extend-patch-header', \$text, $ch_info); + $text .= "\n--- +The information above should follow the Patch Tagging Guidelines, please +checkout https://dep.debian.net/deps/dep3/ to learn about the format. Here +are templates for supplementary fields that you might want to add: + +Origin: (upstream|backport|vendor|other), (<patch-url>|commit:<commit-id>) +Bug: <upstream-bugtracker-url> +Bug-Debian: https://bugs.debian.org/<bugnumber> +Bug-Ubuntu: https://launchpad.net/bugs/<bugnumber> +Forwarded: (no|not-needed|<patch-forwarded-url>) +Applied-Upstream: <version>, (<commit-url>|commit:<commid-id>) +Reviewed-By: <name and email of someone who approved/reviewed the patch> +Last-Update: $yyyy_mm_dd\n\n"; + return $text; +} + +sub register_patch { + my ($self, $dir, $patch_file, $patch_name) = @_; + my $patch = File::Spec->catfile($dir, 'debian', 'patches', $patch_name); + if (-s $patch_file) { + copy($patch_file, $patch) + or syserr(g_('failed to copy %s to %s'), $patch_file, $patch); + chmod_if_needed(0666 & ~ umask(), $patch) + or syserr(g_("unable to change permission of '%s'"), $patch); + my $applied = File::Spec->catfile($dir, 'debian', 'patches', '.dpkg-source-applied'); + open(my $applied_fh, '>>', $applied) + or syserr(g_('cannot write %s'), $applied); + print { $applied_fh } "$patch\n"; + close($applied_fh) or syserr(g_('cannot close %s'), $applied); + } elsif (-e $patch) { + unlink($patch) or syserr(g_('cannot remove %s'), $patch); + } + return $patch; +} + +sub _is_bad_patch_name { + my ($dir, $patch_name) = @_; + + return 1 if not defined($patch_name); + return 1 if not length($patch_name); + + my $patch = File::Spec->catfile($dir, 'debian', 'patches', $patch_name); + if (-e $patch) { + warning(g_('cannot register changes in %s, this patch already exists'), + $patch); + return 1; + } + return 0; +} + +sub do_commit { + my ($self, $dir) = @_; + my ($patch_name, $tmpdiff) = @{$self->{options}{ARGV}}; + + $self->prepare_build($dir); + + # Try to fix up a broken relative filename for the patch + if ($tmpdiff and not -e $tmpdiff) { + $tmpdiff = File::Spec->catfile($dir, $tmpdiff) + unless File::Spec->file_name_is_absolute($tmpdiff); + error(g_("patch file '%s' doesn't exist"), $tmpdiff) if not -e $tmpdiff; + } + + my $binaryfiles = Dpkg::Source::BinaryFiles->new($dir); + my $handle_binary = sub { + my ($self, $old, $new, %opts) = @_; + my $fn = File::Spec->abs2rel($new, $dir); + $binaryfiles->new_binary_found($fn); + }; + + unless ($tmpdiff) { + $tmpdiff = $self->_generate_patch($dir, handle_binary => $handle_binary, + usage => 'commit'); + $binaryfiles->update_debian_source_include_binaries(); + } + push_exit_handler(sub { unlink($tmpdiff) }); + unless (-s $tmpdiff) { + unlink($tmpdiff) or syserr(g_('cannot remove %s'), $tmpdiff); + info(g_('there are no local changes to record')); + return; + } + while (_is_bad_patch_name($dir, $patch_name)) { + # Ask the patch name interactively + print g_('Enter the desired patch name: '); + $patch_name = <STDIN>; + if (not defined $patch_name) { + error(g_('no patch name given; cannot proceed')); + } + chomp $patch_name; + $patch_name =~ s/\s+/-/g; + $patch_name =~ s/\///g; + } + make_path(File::Spec->catdir($dir, 'debian', 'patches')); + my $patch = $self->register_patch($dir, $tmpdiff, $patch_name); + my @editors = ('sensible-editor', $ENV{VISUAL}, $ENV{EDITOR}, 'vi'); + my $editor = first { find_command($_) } @editors; + if (not $editor) { + error(g_('cannot find an editor')); + } + system($editor, $patch); + subprocerr($editor) if $?; + unlink($tmpdiff) or syserr(g_('cannot remove %s'), $tmpdiff); + pop_exit_handler(); + info(g_('local changes have been recorded in a new patch: %s'), $patch); +} + +1; diff --git a/scripts/Dpkg/Source/Package/V3/Bzr.pm b/scripts/Dpkg/Source/Package/V3/Bzr.pm new file mode 100644 index 0000000..2f18fee --- /dev/null +++ b/scripts/Dpkg/Source/Package/V3/Bzr.pm @@ -0,0 +1,213 @@ +# +# bzr support for dpkg-source +# +# Copyright © 2007 Colin Watson <cjwatson@debian.org>. +# Based on Dpkg::Source::Package::V3_0::git, which is: +# Copyright © 2007 Joey Hess <joeyh@debian.org>. +# Copyright © 2008 Frank Lichtenheld <djpig@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Package::V3::Bzr; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use Cwd; +use File::Basename; +use File::Spec; +use File::Find; +use File::Temp qw(tempdir); + +use Dpkg::Gettext; +use Dpkg::Compression; +use Dpkg::ErrorHandling; +use Dpkg::Source::Archive; +use Dpkg::Exit qw(push_exit_handler pop_exit_handler); +use Dpkg::Path qw(find_command); +use Dpkg::Source::Functions qw(erasedir); + +use parent qw(Dpkg::Source::Package); + +our $CURRENT_MINOR_VERSION = '0'; + +sub prerequisites { + return 1 if find_command('bzr'); + error(g_('cannot unpack bzr-format source package because ' . + 'bzr is not in the PATH')); +} + +sub _check_workdir { + my $srcdir = shift; + + if (! -d "$srcdir/.bzr") { + error(g_('source directory is not the top directory of a bzr repository (%s/.bzr not present), but Format bzr was specified'), + $srcdir); + } + + # Symlinks from .bzr to outside could cause unpack failures, or + # point to files they shouldn't, so check for and don't allow. + if (-l "$srcdir/.bzr") { + error(g_('%s is a symlink'), "$srcdir/.bzr"); + } + my $abs_srcdir = Cwd::abs_path($srcdir); + find(sub { + if (-l) { + if (Cwd::abs_path(readlink) !~ /^\Q$abs_srcdir\E(?:\/|$)/) { + error(g_('%s is a symlink to outside %s'), + $File::Find::name, $srcdir); + } + } + }, "$srcdir/.bzr"); + + return 1; +} + +sub can_build { + my ($self, $dir) = @_; + + return (0, g_("doesn't contain a bzr repository")) unless -d "$dir/.bzr"; + return 1; +} + +sub do_build { + my ($self, $dir) = @_; + my @argv = @{$self->{options}{ARGV}}; + # TODO: warn here? + #my @tar_ignore = map { "--exclude=$_" } @{$self->{options}{tar_ignore}}; + my $diff_ignore_regex = $self->{options}{diff_ignore_regex}; + + $dir =~ s{/+$}{}; # Strip trailing / + my ($dirname, $updir) = fileparse($dir); + + if (scalar(@argv)) { + usageerr(g_("-b takes only one parameter with format '%s'"), + $self->{fields}{'Format'}); + } + + my $sourcepackage = $self->{fields}{'Source'}; + my $basenamerev = $self->get_basename(1); + my $basename = $self->get_basename(); + my $basedirname = $basename; + $basedirname =~ s/_/-/; + + _check_workdir($dir); + + my $old_cwd = getcwd(); + chdir $dir or syserr(g_("unable to chdir to '%s'"), $dir); + + local $_; + + # Check for uncommitted files. + # To support dpkg-source -i, remove any ignored files from the + # output of bzr status. + open(my $bzr_status_fh, '-|', 'bzr', 'status') + or subprocerr('bzr status'); + my @files; + while (<$bzr_status_fh>) { + chomp; + next unless s/^ +//; + if (! length $diff_ignore_regex || + ! m/$diff_ignore_regex/o) { + push @files, $_; + } + } + close($bzr_status_fh) or syserr(g_('bzr status exited nonzero')); + if (@files) { + error(g_('uncommitted, not-ignored changes in working directory: %s'), + join(' ', @files)); + } + + chdir $old_cwd or syserr(g_("unable to chdir to '%s'"), $old_cwd); + + my $tmp = tempdir("$dirname.bzr.XXXXXX", DIR => $updir); + push_exit_handler(sub { erasedir($tmp) }); + my $tardir = "$tmp/$dirname"; + + system('bzr', 'branch', $dir, $tardir); + subprocerr("bzr branch $dir $tardir") if $?; + + # Remove the working tree. + system('bzr', 'remove-tree', $tardir); + subprocerr("bzr remove-tree $tardir") if $?; + + # Some branch metadata files are unhelpful. + unlink("$tardir/.bzr/branch/branch-name", + "$tardir/.bzr/branch/parent"); + + # Create the tar file + my $debianfile = "$basenamerev.bzr.tar." . $self->{options}{comp_ext}; + info(g_('building %s in %s'), + $sourcepackage, $debianfile); + my $tar = Dpkg::Source::Archive->new(filename => $debianfile, + compression => $self->{options}{compression}, + compression_level => $self->{options}{comp_level}); + $tar->create(chdir => $tmp); + $tar->add_directory($dirname); + $tar->finish(); + + erasedir($tmp); + pop_exit_handler(); + + $self->add_file($debianfile); +} + +# Called after a tarball is unpacked, to check out the working copy. +sub do_extract { + my ($self, $newdirectory) = @_; + my $fields = $self->{fields}; + + my $basename = $self->get_basename(); + my $basenamerev = $self->get_basename(1); + + my @files = $self->get_files(); + if (@files > 1) { + error(g_('format v3.0 (bzr) uses only one source file')); + } + my $tarfile = $files[0]; + my $comp_ext_regex = compression_get_file_extension_regex(); + if ($tarfile !~ /^\Q$basenamerev\E\.bzr\.tar\.$comp_ext_regex$/) { + error(g_('expected %s, got %s'), + "$basenamerev.bzr.tar.$comp_ext_regex", $tarfile); + } + + if ($self->{options}{no_overwrite_dir} and -e $newdirectory) { + error(g_('unpack target exists: %s'), $newdirectory); + } else { + erasedir($newdirectory); + } + + # Extract main tarball + info(g_('unpacking %s'), $tarfile); + my $tar = Dpkg::Source::Archive->new( + filename => File::Spec->catfile($self->{basedir}, $tarfile), + ); + $tar->extract($newdirectory); + + _check_workdir($newdirectory); + + my $old_cwd = getcwd(); + chdir($newdirectory) + or syserr(g_("unable to chdir to '%s'"), $newdirectory); + + # Reconstitute the working tree. + system('bzr', 'checkout'); + subprocerr('bzr checkout') if $?; + + chdir $old_cwd or syserr(g_("unable to chdir to '%s'"), $old_cwd); +} + +1; diff --git a/scripts/Dpkg/Source/Package/V3/Custom.pm b/scripts/Dpkg/Source/Package/V3/Custom.pm new file mode 100644 index 0000000..63f1769 --- /dev/null +++ b/scripts/Dpkg/Source/Package/V3/Custom.pm @@ -0,0 +1,74 @@ +# Copyright © 2008 Raphaël Hertzog <hertzog@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Package::V3::Custom; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use Dpkg::Gettext; +use Dpkg::ErrorHandling; + +use parent qw(Dpkg::Source::Package); + +our $CURRENT_MINOR_VERSION = '0'; + +my @module_cmdline = ( + { + name => '--target-format=<value>', + help => N_('define the format of the generated source package'), + when => 'build', + } +); + +sub describe_cmdline_options { + return @module_cmdline; +} + +sub parse_cmdline_option { + my ($self, $opt) = @_; + if ($opt =~ /^--target-format=(.*)$/) { + $self->{options}{target_format} = $1; + return 1; + } + return 0; +} +sub do_extract { + error(g_("Format '3.0 (custom)' is only used to create source packages")); +} + +sub can_build { + my ($self, $dir) = @_; + + return (0, g_('no files indicated on command line')) + unless scalar(@{$self->{options}{ARGV}}); + return 1; +} + +sub do_build { + my ($self, $dir) = @_; + # Update real target format + my $format = $self->{options}{target_format}; + error(g_('--target-format option is missing')) unless $format; + $self->{fields}{'Format'} = $format; + # Add all files + foreach my $file (@{$self->{options}{ARGV}}) { + $self->add_file($file); + } +} + +1; diff --git a/scripts/Dpkg/Source/Package/V3/Git.pm b/scripts/Dpkg/Source/Package/V3/Git.pm new file mode 100644 index 0000000..721036a --- /dev/null +++ b/scripts/Dpkg/Source/Package/V3/Git.pm @@ -0,0 +1,283 @@ +# +# git support for dpkg-source +# +# Copyright © 2007,2010 Joey Hess <joeyh@debian.org>. +# Copyright © 2008 Frank Lichtenheld <djpig@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Package::V3::Git; + +use strict; +use warnings; + +our $VERSION = '0.02'; + +use Cwd qw(abs_path getcwd); +use File::Basename; +use File::Spec; +use File::Temp qw(tempdir); + +use Dpkg::Gettext; +use Dpkg::ErrorHandling; +use Dpkg::Exit qw(push_exit_handler pop_exit_handler); +use Dpkg::Path qw(find_command); +use Dpkg::Source::Functions qw(erasedir); + +use parent qw(Dpkg::Source::Package); + +our $CURRENT_MINOR_VERSION = '0'; + +# Remove variables from the environment that might cause git to do +# something unexpected. +delete $ENV{GIT_DIR}; +delete $ENV{GIT_INDEX_FILE}; +delete $ENV{GIT_OBJECT_DIRECTORY}; +delete $ENV{GIT_ALTERNATE_OBJECT_DIRECTORIES}; +delete $ENV{GIT_WORK_TREE}; + +sub prerequisites { + return 1 if find_command('git'); + error(g_('cannot unpack git-format source package because ' . + 'git is not in the PATH')); +} + +sub _check_workdir { + my $srcdir = shift; + + if (! -d "$srcdir/.git") { + error(g_('source directory is not the top directory of a git ' . + 'repository (%s/.git not present), but Format git was ' . + 'specified'), $srcdir); + } + if (-s "$srcdir/.gitmodules") { + error(g_('git repository %s uses submodules; this is not yet supported'), + $srcdir); + } + + return 1; +} + +sub _parse_vcs_git { + my $vcs_git = shift; + my ($url, $opt, $branch) = split ' ', $vcs_git; + + if (defined $opt && $opt eq '-b' && defined $branch) { + return ($url, $branch); + } else { + return ($url); + } +} + +my @module_cmdline = ( + { + name => '--git-ref=<ref>', + help => N_('specify a git <ref> to include in the git bundle'), + when => 'build', + }, { + name => '--git-depth=<number>', + help => N_('create a shallow clone with <number> depth'), + when => 'build', + } +); + +sub describe_cmdline_options { + my $self = shift; + + my @cmdline = ( $self->SUPER::describe_cmdline_options(), @module_cmdline ); + + return @cmdline; +} + +sub parse_cmdline_option { + my ($self, $opt) = @_; + return 1 if $self->SUPER::parse_cmdline_option($opt); + if ($opt =~ /^--git-ref=(.*)$/) { + push @{$self->{options}{git_ref}}, $1; + return 1; + } elsif ($opt =~ /^--git-depth=(\d+)$/) { + $self->{options}{git_depth} = $1; + return 1; + } + return 0; +} + +sub can_build { + my ($self, $dir) = @_; + + return (0, g_("doesn't contain a git repository")) unless -d "$dir/.git"; + return 1; +} + +sub do_build { + my ($self, $dir) = @_; + my $diff_ignore_regex = $self->{options}{diff_ignore_regex}; + + $dir =~ s{/+$}{}; # Strip trailing / + my ($dirname, $updir) = fileparse($dir); + my $basenamerev = $self->get_basename(1); + + _check_workdir($dir); + + my $old_cwd = getcwd(); + chdir $dir or syserr(g_("unable to chdir to '%s'"), $dir); + + # Check for uncommitted files. + # To support dpkg-source -i, get a list of files + # equivalent to the ones git status finds, and remove any + # ignored files from it. + my @ignores = '--exclude-per-directory=.gitignore'; + my $core_excludesfile = qx(git config --get core.excludesfile); + chomp $core_excludesfile; + if (length $core_excludesfile && -e $core_excludesfile) { + push @ignores, "--exclude-from=$core_excludesfile"; + } + if (-e '.git/info/exclude') { + push @ignores, '--exclude-from=.git/info/exclude'; + } + open(my $git_ls_files_fh, '-|', 'git', 'ls-files', '--modified', '--deleted', + '-z', '--others', @ignores) or subprocerr('git ls-files'); + my @files; + { + local $_; + local $/ = "\0"; + while (<$git_ls_files_fh>) { + chomp; + if (! length $diff_ignore_regex || + ! m/$diff_ignore_regex/o) { + push @files, $_; + } + } + } + close($git_ls_files_fh) or syserr(g_('git ls-files exited nonzero')); + if (@files) { + error(g_('uncommitted, not-ignored changes in working directory: %s'), + join(' ', @files)); + } + + # If a depth was specified, need to create a shallow clone and + # bundle that. + my $tmp; + my $shallowfile; + if ($self->{options}{git_depth}) { + chdir $old_cwd or syserr(g_("unable to chdir to '%s'"), $old_cwd); + $tmp = tempdir("$dirname.git.XXXXXX", DIR => $updir); + push_exit_handler(sub { erasedir($tmp) }); + my $clone_dir = "$tmp/repo.git"; + # file:// is needed to avoid local cloning, which does not + # create a shallow clone. + info(g_('creating shallow clone with depth %s'), + $self->{options}{git_depth}); + system('git', 'clone', '--depth=' . $self->{options}{git_depth}, + '--quiet', '--bare', 'file://' . abs_path($dir), $clone_dir); + subprocerr('git clone') if $?; + chdir($clone_dir) + or syserr(g_("unable to chdir to '%s'"), $clone_dir); + $shallowfile = "$basenamerev.gitshallow"; + system('cp', '-f', 'shallow', "$old_cwd/$shallowfile"); + subprocerr('cp shallow') if $?; + } + + # Create the git bundle. + my $bundlefile = "$basenamerev.git"; + my @bundle_arg = $self->{options}{git_ref} ? + (@{$self->{options}{git_ref}}) : '--all'; + info(g_('bundling: %s'), join(' ', @bundle_arg)); + system('git', 'bundle', 'create', "$old_cwd/$bundlefile", + @bundle_arg, + 'HEAD', # ensure HEAD is included no matter what + '--', # avoids ambiguity error when referring to eg, a debian branch + ); + subprocerr('git bundle') if $?; + + chdir $old_cwd or syserr(g_("unable to chdir to '%s'"), $old_cwd); + + if (defined $tmp) { + erasedir($tmp); + pop_exit_handler(); + } + + $self->add_file($bundlefile); + if (defined $shallowfile) { + $self->add_file($shallowfile); + } +} + +sub do_extract { + my ($self, $newdirectory) = @_; + my $fields = $self->{fields}; + + my $basenamerev = $self->get_basename(1); + + my @files = $self->get_files(); + my ($bundle, $shallow); + foreach my $file (@files) { + if ($file =~ /^\Q$basenamerev\E\.git$/) { + if (! defined $bundle) { + $bundle = $file; + } else { + error(g_('format v3.0 (git) uses only one .git file')); + } + } elsif ($file =~ /^\Q$basenamerev\E\.gitshallow$/) { + if (! defined $shallow) { + $shallow = $file; + } else { + error(g_('format v3.0 (git) uses only one .gitshallow file')); + } + } else { + error(g_('format v3.0 (git) unknown file: %s'), $file); + } + } + if (! defined $bundle) { + error(g_('format v3.0 (git) expected %s'), "$basenamerev.git"); + } + + if ($self->{options}{no_overwrite_dir} and -e $newdirectory) { + error(g_('unpack target exists: %s'), $newdirectory); + } else { + erasedir($newdirectory); + } + + # Extract git bundle. + info(g_('cloning %s'), $bundle); + my $bundle_path = File::Spec->catfile($self->{basedir}, $bundle); + system('git', 'clone', '--quiet', '--origin=bundle', $bundle_path, $newdirectory); + subprocerr('git bundle') if $?; + + if (defined $shallow) { + # Move shallow info file into place, so git does not + # try to follow parents of shallow refs. + info(g_('setting up shallow clone')); + my $shallow_orig = File::Spec->catfile($self->{basedir}, $shallow); + my $shallow_dest = File::Spec->catfile($newdirectory, '.git', 'shallow'); + system('cp', '-f', $shallow_orig, $shallow_dest); + subprocerr('cp') if $?; + } + + _check_workdir($newdirectory); + + if (defined $fields->{'Vcs-Git'}) { + my $remote = 'origin'; + my ($url, $head) = _parse_vcs_git($fields->{'Vcs-Git'}); + + my @git_remote_add = (qw(git -C), $newdirectory, qw(remote add)); + push @git_remote_add, '-m', $head if defined $head; + + info(g_('setting remote %s to %s'), $remote, $url); + system(@git_remote_add, $remote, $url); + subprocerr('git remote add') if $?; + } +} + +1; diff --git a/scripts/Dpkg/Source/Package/V3/Native.pm b/scripts/Dpkg/Source/Package/V3/Native.pm new file mode 100644 index 0000000..933315a --- /dev/null +++ b/scripts/Dpkg/Source/Package/V3/Native.pm @@ -0,0 +1,121 @@ +# Copyright © 2008 Raphaël Hertzog <hertzog@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Package::V3::Native; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use Cwd; +use File::Basename; +use File::Spec; +use File::Temp qw(tempfile); + +use Dpkg::Gettext; +use Dpkg::ErrorHandling; +use Dpkg::Compression; +use Dpkg::Exit qw(push_exit_handler pop_exit_handler); +use Dpkg::Version; +use Dpkg::Source::Archive; +use Dpkg::Source::Functions qw(erasedir); + +use parent qw(Dpkg::Source::Package); + +our $CURRENT_MINOR_VERSION = '0'; + +sub do_extract { + my ($self, $newdirectory) = @_; + my $sourcestyle = $self->{options}{sourcestyle}; + my $fields = $self->{fields}; + + my $basename = $self->get_basename(); + my $basenamerev = $self->get_basename(1); + + my $tarfile; + my $comp_ext_regex = compression_get_file_extension_regex(); + foreach my $file ($self->get_files()) { + if ($file =~ /^\Q$basenamerev\E\.tar\.$comp_ext_regex$/) { + error(g_('multiple tarfiles in native source package')) if $tarfile; + $tarfile = $file; + } else { + error(g_('unrecognized file for a native source package: %s'), $file); + } + } + + error(g_('no tarfile in Files field')) unless $tarfile; + + if ($self->{options}{no_overwrite_dir} and -e $newdirectory) { + error(g_('unpack target exists: %s'), $newdirectory); + } else { + erasedir($newdirectory); + } + + info(g_('unpacking %s'), $tarfile); + my $tar = Dpkg::Source::Archive->new( + filename => File::Spec->catfile($self->{basedir}, $tarfile), + ); + $tar->extract($newdirectory); +} + +sub can_build { + my ($self, $dir) = @_; + + my $v = Dpkg::Version->new($self->{fields}->{'Version'}); + return (0, g_('native package version may not have a revision')) + unless $v->is_native(); + + return 1; +} + +sub do_build { + my ($self, $dir) = @_; + my @tar_ignore = map { "--exclude=$_" } @{$self->{options}{tar_ignore}}; + my @argv = @{$self->{options}{ARGV}}; + + if (scalar(@argv)) { + usageerr(g_("-b takes only one parameter with format '%s'"), + $self->{fields}{'Format'}); + } + + my $sourcepackage = $self->{fields}{'Source'}; + my $basenamerev = $self->get_basename(1); + my $tarname = "$basenamerev.tar." . $self->{options}{comp_ext}; + + info(g_('building %s in %s'), $sourcepackage, $tarname); + + my ($ntfh, $newtar) = tempfile("$tarname.new.XXXXXX", + DIR => getcwd(), UNLINK => 0); + push_exit_handler(sub { unlink($newtar) }); + + my ($dirname, $dirbase) = fileparse($dir); + my $tar = Dpkg::Source::Archive->new(filename => $newtar, + compression => compression_guess_from_filename($tarname), + compression_level => $self->{options}{comp_level}); + $tar->create(options => \@tar_ignore, chdir => $dirbase); + $tar->add_directory($dirname); + $tar->finish(); + rename($newtar, $tarname) + or syserr(g_("unable to rename '%s' (newly created) to '%s'"), + $newtar, $tarname); + pop_exit_handler(); + chmod(0666 &~ umask(), $tarname) + or syserr(g_("unable to change permission of '%s'"), $tarname); + + $self->add_file($tarname); +} + +1; diff --git a/scripts/Dpkg/Source/Package/V3/Quilt.pm b/scripts/Dpkg/Source/Package/V3/Quilt.pm new file mode 100644 index 0000000..1359168 --- /dev/null +++ b/scripts/Dpkg/Source/Package/V3/Quilt.pm @@ -0,0 +1,269 @@ +# Copyright © 2008-2012 Raphaël Hertzog <hertzog@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Package::V3::Quilt; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use List::Util qw(any); +use File::Spec; +use File::Copy; + +use Dpkg::Gettext; +use Dpkg::ErrorHandling; +use Dpkg::File; +use Dpkg::Version; +use Dpkg::Source::Patch; +use Dpkg::Source::Functions qw(erasedir chmod_if_needed fs_time); +use Dpkg::Source::Quilt; +use Dpkg::Exit; + +# Based on wig&pen implementation +use parent qw(Dpkg::Source::Package::V2); + +our $CURRENT_MINOR_VERSION = '0'; + +sub init_options { + my $self = shift; + $self->{options}{single_debian_patch} //= 0; + $self->{options}{allow_version_of_quilt_db} //= []; + + $self->SUPER::init_options(); +} + +my @module_cmdline = ( + { + name => '--single-debian-patch', + help => N_('use a single debianization patch'), + when => 'build', + }, { + name => '--allow-version-of-quilt-db=<version>', + help => N_('accept quilt metadata <version> even if unknown'), + when => 'build', + } +); + +sub describe_cmdline_options { + my $self = shift; + + my @cmdline = ( $self->SUPER::describe_cmdline_options(), @module_cmdline ); + + return @cmdline; +} + +sub parse_cmdline_option { + my ($self, $opt) = @_; + return 1 if $self->SUPER::parse_cmdline_option($opt); + if ($opt eq '--single-debian-patch') { + $self->{options}{single_debian_patch} = 1; + # For backwards compatibility. + $self->{options}{auto_commit} = 1; + return 1; + } elsif ($opt =~ /^--allow-version-of-quilt-db=(.*)$/) { + push @{$self->{options}{allow_version_of_quilt_db}}, $1; + return 1; + } + return 0; +} + +sub _build_quilt_object { + my ($self, $dir) = @_; + return $self->{quilt}{$dir} if exists $self->{quilt}{$dir}; + $self->{quilt}{$dir} = Dpkg::Source::Quilt->new($dir); + return $self->{quilt}{$dir}; +} + +sub can_build { + my ($self, $dir) = @_; + my ($code, $msg) = $self->SUPER::can_build($dir); + return ($code, $msg) if $code == 0; + + my $v = Dpkg::Version->new($self->{fields}->{'Version'}); + return (0, g_('non-native package version does not contain a revision')) + if $v->is_native(); + + my $quilt = $self->_build_quilt_object($dir); + $msg = $quilt->find_problems(); + return (0, $msg) if $msg; + return 1; +} + +sub get_autopatch_name { + my $self = shift; + if ($self->{options}{single_debian_patch}) { + return 'debian-changes'; + } else { + return 'debian-changes-' . $self->{fields}{'Version'}; + } +} + +sub apply_patches { + my ($self, $dir, %opts) = @_; + + if ($opts{usage} eq 'unpack') { + $opts{verbose} = 1; + } elsif ($opts{usage} eq 'build') { + $opts{warn_options} = 1; + $opts{verbose} = 0; + } + + my $quilt = $self->_build_quilt_object($dir); + $quilt->load_series(%opts) if $opts{warn_options}; # Trigger warnings + + # Always create the quilt db so that if the maintainer calls quilt to + # create a patch, it's stored in the right directory + $quilt->save_db(); + + # Update debian/patches/series symlink if needed to allow quilt usage + my $series = $quilt->get_series_file(); + my $basename = (File::Spec->splitpath($series))[2]; + if ($basename ne 'series') { + my $dest = $quilt->get_patch_file('series'); + unlink($dest) if -l $dest; + unless (-f _) { # Don't overwrite real files + symlink($basename, $dest) + or syserr(g_("can't create symlink %s"), $dest); + } + } + + return unless scalar($quilt->series()); + + info(g_('using patch list from %s'), "debian/patches/$basename"); + + if ($opts{usage} eq 'preparation' and + $self->{options}{unapply_patches} eq 'auto') { + # We're applying the patches in --before-build, remember to unapply + # them afterwards in --after-build + my $pc_unapply = $quilt->get_db_file('.dpkg-source-unapply'); + file_touch($pc_unapply); + } + + # Apply patches + my $pc_applied = $quilt->get_db_file('applied-patches'); + $opts{timestamp} = fs_time($pc_applied); + if ($opts{skip_auto}) { + my $auto_patch = $self->get_autopatch_name(); + $quilt->push(%opts) while ($quilt->next() and $quilt->next() ne $auto_patch); + } else { + $quilt->push(%opts) while $quilt->next(); + } +} + +sub unapply_patches { + my ($self, $dir, %opts) = @_; + + my $quilt = $self->_build_quilt_object($dir); + + $opts{verbose} //= 1; + + my $pc_applied = $quilt->get_db_file('applied-patches'); + my @applied = $quilt->applied(); + $opts{timestamp} = fs_time($pc_applied) if @applied; + + $quilt->pop(%opts) while $quilt->top(); + + erasedir($quilt->get_db_dir()); +} + +sub prepare_build { + my ($self, $dir) = @_; + $self->SUPER::prepare_build($dir); + # Skip .pc directories of quilt by default and ignore difference + # on debian/patches/series symlinks and d/p/.dpkg-source-applied + # stamp file created by ourselves + my $func = sub { + my $pathname = shift; + + return 1 if $pathname eq 'debian/patches/series' and -l $pathname; + return 1 if $pathname =~ /^\.pc(\/|$)/; + return 1 if $pathname =~ /$self->{options}{diff_ignore_regex}/; + return 0; + }; + $self->{diff_options}{diff_ignore_func} = $func; +} + +sub do_build { + my ($self, $dir) = @_; + + my $quilt = $self->_build_quilt_object($dir); + my $version = $quilt->get_db_version(); + + if (defined($version) and $version != 2) { + if (any { $version eq $_ } + @{$self->{options}{allow_version_of_quilt_db}}) + { + warning(g_('unsupported version of the quilt metadata: %s'), $version); + } else { + error(g_('unsupported version of the quilt metadata: %s'), $version); + } + } + + $self->SUPER::do_build($dir); +} + +sub after_build { + my ($self, $dir) = @_; + my $quilt = $self->_build_quilt_object($dir); + my $pc_unapply = $quilt->get_db_file('.dpkg-source-unapply'); + my $opt_unapply = $self->{options}{unapply_patches}; + if (($opt_unapply eq 'auto' and -e $pc_unapply) or $opt_unapply eq 'yes') { + unlink($pc_unapply); + $self->unapply_patches($dir); + } +} + +sub check_patches_applied { + my ($self, $dir) = @_; + + my $quilt = $self->_build_quilt_object($dir); + my $next = $quilt->next(); + return if not defined $next; + + my $first_patch = File::Spec->catfile($dir, 'debian', 'patches', $next); + my $patch_obj = Dpkg::Source::Patch->new(filename => $first_patch); + return unless $patch_obj->check_apply($dir, fatal_dupes => 1); + + $self->apply_patches($dir, usage => 'preparation', verbose => 1); +} + +sub register_patch { + my ($self, $dir, $tmpdiff, $patch_name) = @_; + + my $quilt = $self->_build_quilt_object($dir); + my $patch = $quilt->get_patch_file($patch_name); + + if (-s $tmpdiff) { + copy($tmpdiff, $patch) + or syserr(g_('failed to copy %s to %s'), $tmpdiff, $patch); + chmod_if_needed(0666 & ~ umask(), $patch) + or syserr(g_("unable to change permission of '%s'"), $patch); + } elsif (-e $patch) { + unlink($patch) or syserr(g_('cannot remove %s'), $patch); + } + + if (-e $patch) { + # Add patch to series file + $quilt->register($patch_name); + } else { + # Remove auto_patch from series + $quilt->unregister($patch_name); + } + return $patch; +} + +1; diff --git a/scripts/Dpkg/Source/Patch.pm b/scripts/Dpkg/Source/Patch.pm new file mode 100644 index 0000000..e670898 --- /dev/null +++ b/scripts/Dpkg/Source/Patch.pm @@ -0,0 +1,697 @@ +# Copyright © 2008 Raphaël Hertzog <hertzog@debian.org> +# Copyright © 2008-2010, 2012-2015 Guillem Jover <guillem@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Patch; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use POSIX qw(:errno_h :sys_wait_h); +use File::Find; +use File::Basename; +use File::Spec; +use File::Path qw(make_path); +use File::Compare; +use Fcntl ':mode'; +use Time::HiRes qw(stat); + +use Dpkg; +use Dpkg::Gettext; +use Dpkg::ErrorHandling; +use Dpkg::IPC; +use Dpkg::Source::Functions qw(fs_time); + +use parent qw(Dpkg::Compression::FileHandle); + +sub create { + my ($self, %opts) = @_; + $self->ensure_open('w'); # Creates the file + *$self->{errors} = 0; + *$self->{empty} = 1; + if ($opts{old} and $opts{new} and $opts{filename}) { + $opts{old} = '/dev/null' unless -e $opts{old}; + $opts{new} = '/dev/null' unless -e $opts{new}; + if (-d $opts{old} and -d $opts{new}) { + $self->add_diff_directory($opts{old}, $opts{new}, %opts); + } elsif (-f $opts{old} and -f $opts{new}) { + $self->add_diff_file($opts{old}, $opts{new}, %opts); + } else { + $self->_fail_not_same_type($opts{old}, $opts{new}, $opts{filename}); + } + $self->finish() unless $opts{nofinish}; + } +} + +sub set_header { + my ($self, $header) = @_; + *$self->{header} = $header; +} + +sub get_header { + my $self = shift; + + if (ref *$self->{header} eq 'CODE') { + return *$self->{header}->(); + } else { + return *$self->{header}; + } +} + +sub add_diff_file { + my ($self, $old, $new, %opts) = @_; + $opts{include_timestamp} //= 0; + my $handle_binary = $opts{handle_binary_func} // sub { + my ($self, $old, $new, %opts) = @_; + my $file = $opts{filename}; + $self->_fail_with_msg($file, g_('binary file contents changed')); + }; + # Optimization to avoid forking diff if unnecessary + return 1 if compare($old, $new, 4096) == 0; + # Default diff options + my @options; + if ($opts{options}) { + push @options, @{$opts{options}}; + } else { + push @options, '-p'; + } + # Add labels + if ($opts{label_old} and $opts{label_new}) { + if ($opts{include_timestamp}) { + my $ts = (stat($old))[9]; + my $t = POSIX::strftime('%Y-%m-%d %H:%M:%S', gmtime($ts)); + $opts{label_old} .= sprintf("\t%s.%09d +0000", $t, + ($ts - int($ts)) * 1_000_000_000); + $ts = (stat($new))[9]; + $t = POSIX::strftime('%Y-%m-%d %H:%M:%S', gmtime($ts)); + $opts{label_new} .= sprintf("\t%s.%09d +0000", $t, + ($ts - int($ts)) * 1_000_000_000); + } else { + # Space in filenames need special treatment + $opts{label_old} .= "\t" if $opts{label_old} =~ / /; + $opts{label_new} .= "\t" if $opts{label_new} =~ / /; + } + push @options, '-L', $opts{label_old}, + '-L', $opts{label_new}; + } + # Generate diff + my $diffgen; + my $diff_pid = spawn( + exec => [ 'diff', '-u', @options, '--', $old, $new ], + env => { LC_ALL => 'C', LANG => 'C', TZ => 'UTC0' }, + to_pipe => \$diffgen, + ); + # Check diff and write it in patch file + my $difflinefound = 0; + my $binary = 0; + local $_; + + while (<$diffgen>) { + if (m/^(?:binary|[^-+\@ ].*\bdiffer\b)/i) { + $binary = 1; + $handle_binary->($self, $old, $new, %opts); + last; + } elsif (m/^[-+\@ ]/) { + $difflinefound++; + } elsif (m/^\\ /) { + warning(g_('file %s has no final newline (either ' . + 'original or modified version)'), $new); + } else { + chomp; + error(g_("unknown line from diff -u on %s: '%s'"), $new, $_); + } + if (*$self->{empty} and defined(*$self->{header})) { + $self->print($self->get_header()) or syserr(g_('failed to write')); + *$self->{empty} = 0; + } + print { $self } $_ or syserr(g_('failed to write')); + } + close($diffgen) or syserr('close on diff pipe'); + wait_child($diff_pid, nocheck => 1, + cmdline => "diff -u @options -- $old $new"); + # Verify diff process ended successfully + # Exit code of diff: 0 => no difference, 1 => diff ok, 2 => error + # Ignore error if binary content detected + my $exit = WEXITSTATUS($?); + unless (WIFEXITED($?) && ($exit == 0 || $exit == 1 || $binary)) { + subprocerr(g_('diff on %s'), $new); + } + return ($exit == 0 || $exit == 1); +} + +sub add_diff_directory { + my ($self, $old, $new, %opts) = @_; + # TODO: make this function more configurable + # - offer to disable some checks + my $basedir = $opts{basedirname} || basename($new); + my $diff_ignore; + if ($opts{diff_ignore_func}) { + $diff_ignore = $opts{diff_ignore_func}; + } elsif ($opts{diff_ignore_regex}) { + $diff_ignore = sub { return $_[0] =~ /$opts{diff_ignore_regex}/o }; + } else { + $diff_ignore = sub { return 0 }; + } + + my @diff_files; + my %files_in_new; + my $scan_new = sub { + my $fn = (length > length($new)) ? substr($_, length($new) + 1) : '.'; + return if $diff_ignore->($fn); + $files_in_new{$fn} = 1; + lstat("$new/$fn") or syserr(g_('cannot stat file %s'), "$new/$fn"); + my $mode = S_IMODE((lstat(_))[2]); + my $size = (lstat(_))[7]; + if (-l _) { + unless (-l "$old/$fn") { + $self->_fail_not_same_type("$old/$fn", "$new/$fn", $fn); + return; + } + my $n = readlink("$new/$fn"); + unless (defined $n) { + syserr(g_('cannot read link %s'), "$new/$fn"); + } + my $n2 = readlink("$old/$fn"); + unless (defined $n2) { + syserr(g_('cannot read link %s'), "$old/$fn"); + } + unless ($n eq $n2) { + $self->_fail_not_same_type("$old/$fn", "$new/$fn", $fn); + } + } elsif (-f _) { + my $old_file = "$old/$fn"; + if (not lstat("$old/$fn")) { + if ($! != ENOENT) { + syserr(g_('cannot stat file %s'), "$old/$fn"); + } + $old_file = '/dev/null'; + } elsif (not -f _) { + $self->_fail_not_same_type("$old/$fn", "$new/$fn", $fn); + return; + } + + my $label_old = "$basedir.orig/$fn"; + if ($opts{use_dev_null}) { + $label_old = $old_file if $old_file eq '/dev/null'; + } + push @diff_files, [$fn, $mode, $size, $old_file, "$new/$fn", + $label_old, "$basedir/$fn"]; + } elsif (-p _) { + unless (-p "$old/$fn") { + $self->_fail_not_same_type("$old/$fn", "$new/$fn", $fn); + } + } elsif (-b _ || -c _ || -S _) { + $self->_fail_with_msg("$new/$fn", + g_('device or socket is not allowed')); + } elsif (-d _) { + if (not lstat("$old/$fn")) { + if ($! != ENOENT) { + syserr(g_('cannot stat file %s'), "$old/$fn"); + } + } elsif (not -d _) { + $self->_fail_not_same_type("$old/$fn", "$new/$fn", $fn); + } + } else { + $self->_fail_with_msg("$new/$fn", g_('unknown file type')); + } + }; + my $scan_old = sub { + my $fn = (length > length($old)) ? substr($_, length($old) + 1) : '.'; + return if $diff_ignore->($fn); + return if $files_in_new{$fn}; + lstat("$old/$fn") or syserr(g_('cannot stat file %s'), "$old/$fn"); + if (-f _) { + if (not defined $opts{include_removal}) { + warning(g_('ignoring deletion of file %s'), $fn); + } elsif (not $opts{include_removal}) { + warning(g_('ignoring deletion of file %s, use --include-removal to override'), $fn); + } else { + push @diff_files, [$fn, 0, 0, "$old/$fn", '/dev/null', + "$basedir.orig/$fn", '/dev/null']; + } + } elsif (-d _) { + warning(g_('ignoring deletion of directory %s'), $fn); + } elsif (-l _) { + warning(g_('ignoring deletion of symlink %s'), $fn); + } else { + $self->_fail_not_same_type("$old/$fn", "$new/$fn", $fn); + } + }; + + find({ wanted => $scan_new, no_chdir => 1 }, $new); + find({ wanted => $scan_old, no_chdir => 1 }, $old); + + if ($opts{order_from} and -e $opts{order_from}) { + my $order_from = Dpkg::Source::Patch->new( + filename => $opts{order_from}); + my $analysis = $order_from->analyze($basedir, verbose => 0); + my %patchorder; + my $i = 0; + foreach my $fn (@{$analysis->{patchorder}}) { + $fn =~ s{^[^/]+/}{}; + $patchorder{$fn} = $i++; + } + # 'quilt refresh' sorts files as follows: + # - Any files in the existing patch come first, in the order in + # which they appear in the existing patch. + # - New files follow, sorted lexicographically. + # This seems a reasonable policy to follow, and avoids autopatches + # being shuffled when they are regenerated. + foreach my $diff_file (sort { $a->[0] cmp $b->[0] } @diff_files) { + my $fn = $diff_file->[0]; + $patchorder{$fn} //= $i++; + } + @diff_files = sort { $patchorder{$a->[0]} <=> $patchorder{$b->[0]} } + @diff_files; + } else { + @diff_files = sort { $a->[0] cmp $b->[0] } @diff_files; + } + + foreach my $diff_file (@diff_files) { + my ($fn, $mode, $size, + $old_file, $new_file, $label_old, $label_new) = @$diff_file; + my $success = $self->add_diff_file($old_file, $new_file, + filename => $fn, + label_old => $label_old, + label_new => $label_new, %opts); + if ($success and + $old_file eq '/dev/null' and $new_file ne '/dev/null') { + if (not $size) { + warning(g_("newly created empty file '%s' will not " . + 'be represented in diff'), $fn); + } else { + if ($mode & (S_IXUSR | S_IXGRP | S_IXOTH)) { + warning(g_("executable mode %04o of '%s' will " . + 'not be represented in diff'), $mode, $fn) + unless $fn eq 'debian/rules'; + } + if ($mode & (S_ISUID | S_ISGID | S_ISVTX)) { + warning(g_("special mode %04o of '%s' will not " . + 'be represented in diff'), $mode, $fn); + } + } + } + } +} + +sub finish { + my $self = shift; + close($self) or syserr(g_('cannot close %s'), $self->get_filename()); + return not *$self->{errors}; +} + +sub register_error { + my $self = shift; + *$self->{errors}++; +} +sub _fail_with_msg { + my ($self, $file, $msg) = @_; + errormsg(g_('cannot represent change to %s: %s'), $file, $msg); + $self->register_error(); +} +sub _fail_not_same_type { + my ($self, $old, $new, $file) = @_; + my $old_type = get_type($old); + my $new_type = get_type($new); + errormsg(g_('cannot represent change to %s:'), $file); + errormsg(g_(' new version is %s'), $new_type); + errormsg(g_(' old version is %s'), $old_type); + $self->register_error(); +} + +sub _getline { + my $handle = shift; + + my $line = <$handle>; + if (defined $line) { + # Strip end-of-line chars + chomp($line); + $line =~ s/\r$//; + } + return $line; +} + +# Fetch the header filename ignoring the optional timestamp +sub _fetch_filename { + my ($diff, $header) = @_; + + # Strip any leading spaces. + $header =~ s/^\s+//; + + # Is it a C-style string? + if ($header =~ m/^"/) { + error(g_('diff %s patches file with C-style encoded filename'), $diff); + } else { + # Tab is the official separator, it's always used when + # filename contain spaces. Try it first, otherwise strip on space + # if there's no tab + $header =~ s/\s.*// unless $header =~ s/\t.*//; + } + + return $header; +} + +sub _intuit_file_patched { + my ($old, $new) = @_; + + return $new unless defined $old; + return $old unless defined $new; + return $new if -e $new and not -e $old; + return $old if -e $old and not -e $new; + + # We don't consider the case where both files are non-existent and + # where patch picks the one with the fewest directories to create + # since dpkg-source will pre-create the required directories + + # Precalculate metrics used by patch + my ($tmp_o, $tmp_n) = ($old, $new); + my ($len_o, $len_n) = (length($old), length($new)); + $tmp_o =~ s{[/\\]+}{/}g; + $tmp_n =~ s{[/\\]+}{/}g; + my $nb_comp_o = ($tmp_o =~ tr{/}{/}); + my $nb_comp_n = ($tmp_n =~ tr{/}{/}); + $tmp_o =~ s{^.*/}{}; + $tmp_n =~ s{^.*/}{}; + my ($blen_o, $blen_n) = (length($tmp_o), length($tmp_n)); + + # Decide like patch would + if ($nb_comp_o != $nb_comp_n) { + return ($nb_comp_o < $nb_comp_n) ? $old : $new; + } elsif ($blen_o != $blen_n) { + return ($blen_o < $blen_n) ? $old : $new; + } elsif ($len_o != $len_n) { + return ($len_o < $len_n) ? $old : $new; + } + return $old; +} + +# check diff for sanity, find directories to create as a side effect +sub analyze { + my ($self, $destdir, %opts) = @_; + + $opts{verbose} //= 1; + my $diff = $self->get_filename(); + my %filepatched; + my %dirtocreate; + my @patchorder; + my $patch_header = ''; + my $diff_count = 0; + + my $line = _getline($self); + + HUNK: + while (defined $line or not eof $self) { + my (%path, %fn); + + # Skip comments leading up to the patch (if any). Although we do not + # look for an Index: pseudo-header in the comments, because we would + # not use it anyway, as we require both ---/+++ filename headers. + while (1) { + if ($line =~ /^(?:--- |\+\+\+ |@@ -)/) { + last; + } else { + $patch_header .= "$line\n"; + } + $line = _getline($self); + last HUNK if not defined $line; + } + $diff_count++; + # read file header (---/+++ pair) + unless ($line =~ s/^--- //) { + error(g_("expected ^--- in line %d of diff '%s'"), $., $diff); + } + $path{old} = $line = _fetch_filename($diff, $line); + if ($line ne '/dev/null' and $line =~ s{^[^/]*/+}{$destdir/}) { + $fn{old} = $line; + } + if ($line =~ /\.dpkg-orig$/) { + error(g_("diff '%s' patches file with name ending in .dpkg-orig"), + $diff); + } + + $line = _getline($self); + unless (defined $line) { + error(g_("diff '%s' finishes in middle of ---/+++ (line %d)"), + $diff, $.); + } + unless ($line =~ s/^\+\+\+ //) { + error(g_("line after --- isn't as expected in diff '%s' (line %d)"), + $diff, $.); + } + $path{new} = $line = _fetch_filename($diff, $line); + if ($line ne '/dev/null' and $line =~ s{^[^/]*/+}{$destdir/}) { + $fn{new} = $line; + } + + unless (defined $fn{old} or defined $fn{new}) { + error(g_("none of the filenames in ---/+++ are valid in diff '%s' (line %d)"), + $diff, $.); + } + + # Safety checks on both filenames that patch could use + foreach my $key ('old', 'new') { + next unless defined $fn{$key}; + if ($path{$key} =~ m{/\.\./}) { + error(g_('%s contains an insecure path: %s'), $diff, $path{$key}); + } + my $path = $fn{$key}; + while (1) { + if (-l $path) { + error(g_('diff %s modifies file %s through a symlink: %s'), + $diff, $fn{$key}, $path); + } + last unless $path =~ s{/+[^/]*$}{}; + last if length($path) <= length($destdir); # $destdir is assumed safe + } + } + + if ($path{old} eq '/dev/null' and $path{new} eq '/dev/null') { + error(g_("original and modified files are /dev/null in diff '%s' (line %d)"), + $diff, $.); + } elsif ($path{new} eq '/dev/null') { + error(g_("file removal without proper filename in diff '%s' (line %d)"), + $diff, $. - 1) unless defined $fn{old}; + if ($opts{verbose}) { + warning(g_('diff %s removes a non-existing file %s (line %d)'), + $diff, $fn{old}, $.) unless -e $fn{old}; + } + } + my $fn = _intuit_file_patched($fn{old}, $fn{new}); + + my $dirname = $fn; + if ($dirname =~ s{/[^/]+$}{} and not -d $dirname) { + $dirtocreate{$dirname} = 1; + } + + if (-e $fn and not -f _) { + error(g_("diff '%s' patches something which is not a plain file"), + $diff); + } + + if ($filepatched{$fn}) { + $filepatched{$fn}++; + + if ($opts{fatal_dupes}) { + error(g_("diff '%s' patches files multiple times; split the " . + 'diff in multiple files or merge the hunks into a ' . + 'single one'), $diff); + } elsif ($opts{verbose} and $filepatched{$fn} == 2) { + warning(g_("diff '%s' patches file %s more than once"), $diff, $fn) + } + } else { + $filepatched{$fn} = 1; + push @patchorder, $fn; + } + + # read hunks + my $hunk = 0; + while (defined($line = _getline($self))) { + # read hunk header (@@) + next if $line =~ /^\\ /; + last unless $line =~ /^@@ -\d+(,(\d+))? \+\d+(,(\d+))? @\@(?: .*)?$/; + my ($olines, $nlines) = ($1 ? $2 : 1, $3 ? $4 : 1); + # read hunk + while ($olines || $nlines) { + unless (defined($line = _getline($self))) { + if (($olines == $nlines) and ($olines < 3)) { + warning(g_("unexpected end of diff '%s'"), $diff) + if $opts{verbose}; + last; + } else { + error(g_("unexpected end of diff '%s'"), $diff); + } + } + next if $line =~ /^\\ /; + # Check stats + if ($line =~ /^ / or length $line == 0) { + --$olines; + --$nlines; + } elsif ($line =~ /^-/) { + --$olines; + } elsif ($line =~ /^\+/) { + --$nlines; + } else { + error(g_("expected [ +-] at start of line %d of diff '%s'"), + $., $diff); + } + } + $hunk++; + } + unless ($hunk) { + error(g_("expected ^\@\@ at line %d of diff '%s'"), $., $diff); + } + } + close($self); + unless ($diff_count) { + warning(g_("diff '%s' doesn't contain any patch"), $diff) + if $opts{verbose}; + } + *$self->{analysis}{$destdir}{dirtocreate} = \%dirtocreate; + *$self->{analysis}{$destdir}{filepatched} = \%filepatched; + *$self->{analysis}{$destdir}{patchorder} = \@patchorder; + *$self->{analysis}{$destdir}{patchheader} = $patch_header; + return *$self->{analysis}{$destdir}; +} + +sub prepare_apply { + my ($self, $analysis, %opts) = @_; + if ($opts{create_dirs}) { + foreach my $dir (keys %{$analysis->{dirtocreate}}) { + eval { make_path($dir, { mode => 0777 }) }; + syserr(g_('cannot create directory %s'), $dir) if $@; + } + } +} + +sub apply { + my ($self, $destdir, %opts) = @_; + # Set default values to options + $opts{force_timestamp} //= 1; + $opts{remove_backup} //= 1; + $opts{create_dirs} //= 1; + $opts{options} ||= [ + '-t', + '-F', '0', + '-N', + '-p1', + '-u', + '-V', 'never', + '-b', + '-z', '.dpkg-orig', + ]; + $opts{add_options} //= []; + push @{$opts{options}}, @{$opts{add_options}}; + # Check the diff and create missing directories + my $analysis = $self->analyze($destdir, %opts); + $self->prepare_apply($analysis, %opts); + # Apply the patch + $self->ensure_open('r'); + my ($stdout, $stderr) = ('', ''); + spawn( + exec => [ $Dpkg::PROGPATCH, @{$opts{options}} ], + chdir => $destdir, + env => { LC_ALL => 'C', LANG => 'C', PATCH_GET => '0' }, + delete_env => [ 'POSIXLY_CORRECT' ], # ensure expected patch behaviour + wait_child => 1, + nocheck => 1, + from_handle => $self->get_filehandle(), + to_string => \$stdout, + error_to_string => \$stderr, + ); + if ($?) { + print { *STDOUT } $stdout; + print { *STDERR } $stderr; + subprocerr("LC_ALL=C $Dpkg::PROGPATCH " . join(' ', @{$opts{options}}) . + ' < ' . $self->get_filename()); + } + $self->close(); + # Reset the timestamp of all the patched files + # and remove .dpkg-orig files + my @files = keys %{$analysis->{filepatched}}; + my $now = $opts{timestamp}; + $now //= fs_time($files[0]) if $opts{force_timestamp} && scalar @files; + foreach my $fn (@files) { + if ($opts{force_timestamp}) { + utime($now, $now, $fn) or $! == ENOENT + or syserr(g_('cannot change timestamp for %s'), $fn); + } + if ($opts{remove_backup}) { + $fn .= '.dpkg-orig'; + unlink($fn) or syserr(g_('remove patch backup file %s'), $fn); + } + } + return $analysis; +} + +# Verify if check will work... +sub check_apply { + my ($self, $destdir, %opts) = @_; + # Set default values to options + $opts{create_dirs} //= 1; + $opts{options} ||= [ + '--dry-run', + '-s', + '-t', + '-F', '0', + '-N', + '-p1', + '-u', + '-V', 'never', + '-b', + '-z', '.dpkg-orig', + ]; + $opts{add_options} //= []; + push @{$opts{options}}, @{$opts{add_options}}; + # Check the diff and create missing directories + my $analysis = $self->analyze($destdir, %opts); + $self->prepare_apply($analysis, %opts); + # Apply the patch + $self->ensure_open('r'); + my $patch_pid = spawn( + exec => [ $Dpkg::PROGPATCH, @{$opts{options}} ], + chdir => $destdir, + env => { LC_ALL => 'C', LANG => 'C', PATCH_GET => '0' }, + delete_env => [ 'POSIXLY_CORRECT' ], # ensure expected patch behaviour + from_handle => $self->get_filehandle(), + to_file => '/dev/null', + error_to_file => '/dev/null', + ); + wait_child($patch_pid, nocheck => 1); + my $exit = WEXITSTATUS($?); + subprocerr("$Dpkg::PROGPATCH --dry-run") unless WIFEXITED($?); + $self->close(); + return ($exit == 0); +} + +# Helper functions +sub get_type { + my $file = shift; + if (not lstat($file)) { + return g_('nonexistent') if $! == ENOENT; + syserr(g_('cannot stat %s'), $file); + } else { + -f _ && return g_('plain file'); + -d _ && return g_('directory'); + -l _ && return sprintf(g_('symlink to %s'), readlink($file)); + -b _ && return g_('block device'); + -c _ && return g_('character device'); + -p _ && return g_('named pipe'); + -S _ && return g_('named socket'); + } +} + +1; diff --git a/scripts/Dpkg/Source/Quilt.pm b/scripts/Dpkg/Source/Quilt.pm new file mode 100644 index 0000000..3e655fa --- /dev/null +++ b/scripts/Dpkg/Source/Quilt.pm @@ -0,0 +1,383 @@ +# Copyright © 2008-2012 Raphaël Hertzog <hertzog@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::Source::Quilt; + +use strict; +use warnings; + +our $VERSION = '0.02'; + +use List::Util qw(any none); +use File::Spec; +use File::Copy; +use File::Find; +use File::Path qw(make_path); +use File::Basename; + +use Dpkg::Gettext; +use Dpkg::ErrorHandling; +use Dpkg::File; +use Dpkg::Source::Patch; +use Dpkg::Source::Functions qw(erasedir chmod_if_needed fs_time); +use Dpkg::Vendor qw(get_current_vendor); + +sub new { + my ($this, $dir, %opts) = @_; + my $class = ref($this) || $this; + + my $self = { + dir => $dir, + }; + bless $self, $class; + + $self->load_series(); + $self->load_db(); + + return $self; +} + +sub setup_db { + my $self = shift; + my $db_dir = $self->get_db_file(); + if (not -d $db_dir) { + mkdir $db_dir or syserr(g_('cannot mkdir %s'), $db_dir); + } + my $file = $self->get_db_file('.version'); + if (not -e $file) { + file_dump($file, "2\n"); + } + # The files below are used by quilt to know where patches are stored + # and what file contains the patch list (supported by quilt >= 0.48-5 + # in Debian). + $file = $self->get_db_file('.quilt_patches'); + if (not -e $file) { + file_dump($file, "debian/patches\n"); + } + $file = $self->get_db_file('.quilt_series'); + if (not -e $file) { + my $series = $self->get_series_file(); + $series = (File::Spec->splitpath($series))[2]; + file_dump($file, "$series\n"); + } +} + +sub load_db { + my $self = shift; + + my $pc_applied = $self->get_db_file('applied-patches'); + $self->{applied_patches} = [ $self->read_patch_list($pc_applied) ]; +} + +sub save_db { + my $self = shift; + + $self->setup_db(); + my $pc_applied = $self->get_db_file('applied-patches'); + $self->write_patch_list($pc_applied, $self->{applied_patches}); +} + +sub load_series { + my ($self, %opts) = @_; + + my $series = $self->get_series_file(); + $self->{series} = [ $self->read_patch_list($series, %opts) ]; +} + +sub series { + my $self = shift; + return @{$self->{series}}; +} + +sub applied { + my $self = shift; + return @{$self->{applied_patches}}; +} + +sub top { + my $self = shift; + my $count = scalar @{$self->{applied_patches}}; + return $self->{applied_patches}[$count - 1] if $count; + return; +} + +sub register { + my ($self, $patch_name) = @_; + + return if any { $_ eq $patch_name } @{$self->{series}}; + + # Add patch to series files. + $self->setup_db(); + $self->_file_add_line($self->get_series_file(), $patch_name); + $self->_file_add_line($self->get_db_file('applied-patches'), $patch_name); + $self->load_db(); + $self->load_series(); + + # Ensure quilt meta-data is created and in sync with some trickery: + # Reverse-apply the patch, drop .pc/$patch, and re-apply it with the + # correct options to recreate the backup files. + $self->pop(reverse_apply => 1); + $self->push(); +} + +sub unregister { + my ($self, $patch_name) = @_; + + return if none { $_ eq $patch_name } @{$self->{series}}; + + my $series = $self->get_series_file(); + + $self->_file_drop_line($series, $patch_name); + $self->_file_drop_line($self->get_db_file('applied-patches'), $patch_name); + erasedir($self->get_db_file($patch_name)); + $self->load_db(); + $self->load_series(); + + # Clean up empty series. + unlink $series if -z $series; +} + +sub next { + my $self = shift; + my $count_applied = scalar @{$self->{applied_patches}}; + my $count_series = scalar @{$self->{series}}; + return $self->{series}[$count_applied] if ($count_series > $count_applied); + return; +} + +sub push { + my ($self, %opts) = @_; + $opts{verbose} //= 0; + $opts{timestamp} //= fs_time($self->{dir}); + + my $patch = $self->next(); + return unless defined $patch; + + my $path = $self->get_patch_file($patch); + my $obj = Dpkg::Source::Patch->new(filename => $path); + + info(g_('applying %s'), $patch) if $opts{verbose}; + eval { + $obj->apply($self->{dir}, timestamp => $opts{timestamp}, + verbose => $opts{verbose}, + force_timestamp => 1, create_dirs => 1, remove_backup => 0, + options => [ '-t', '-F', '0', '-N', '-p1', '-u', + '-V', 'never', '-E', '-b', + '-B', ".pc/$patch/", '--reject-file=-' ]); + }; + if ($@) { + info(g_('the patch has fuzz which is not allowed, or is malformed')); + info(g_("if patch '%s' is correctly applied by quilt, use '%s' to update it"), + $patch, 'quilt refresh'); + info(g_('if the file is present in the unpacked source, make sure it ' . + 'is also present in the orig tarball')); + $self->restore_quilt_backup_files($patch, %opts); + erasedir($self->get_db_file($patch)); + die $@; + } + CORE::push @{$self->{applied_patches}}, $patch; + $self->save_db(); +} + +sub pop { + my ($self, %opts) = @_; + $opts{verbose} //= 0; + $opts{timestamp} //= fs_time($self->{dir}); + $opts{reverse_apply} //= 0; + + my $patch = $self->top(); + return unless defined $patch; + + info(g_('unapplying %s'), $patch) if $opts{verbose}; + my $backup_dir = $self->get_db_file($patch); + if (-d $backup_dir and not $opts{reverse_apply}) { + # Use the backup copies to restore + $self->restore_quilt_backup_files($patch); + } else { + # Otherwise reverse-apply the patch + my $path = $self->get_patch_file($patch); + my $obj = Dpkg::Source::Patch->new(filename => $path); + + $obj->apply($self->{dir}, timestamp => $opts{timestamp}, + verbose => 0, force_timestamp => 1, remove_backup => 0, + options => [ '-R', '-t', '-N', '-p1', + '-u', '-V', 'never', '-E', + '--no-backup-if-mismatch' ]); + } + + erasedir($backup_dir); + pop @{$self->{applied_patches}}; + $self->save_db(); +} + +sub get_db_version { + my $self = shift; + my $pc_ver = $self->get_db_file('.version'); + if (-f $pc_ver) { + my $version = file_slurp($pc_ver); + chomp $version; + return $version; + } + return; +} + +sub find_problems { + my $self = shift; + my $patch_dir = $self->get_patch_file(); + if (-e $patch_dir and not -d _) { + return sprintf(g_('%s should be a directory or non-existing'), $patch_dir); + } + my $series = $self->get_series_file(); + if (-e $series and not -f _) { + return sprintf(g_('%s should be a file or non-existing'), $series); + } + return; +} + +sub get_series_file { + my $self = shift; + my $vendor = lc(get_current_vendor() || 'debian'); + # Series files are stored alongside patches + my $default_series = $self->get_patch_file('series'); + my $vendor_series = $self->get_patch_file("$vendor.series"); + return $vendor_series if -e $vendor_series; + return $default_series; +} + +sub get_db_file { + my $self = shift; + return File::Spec->catfile($self->{dir}, '.pc', @_); +} + +sub get_db_dir { + my $self = shift; + return $self->get_db_file(); +} + +sub get_patch_file { + my $self = shift; + return File::Spec->catfile($self->{dir}, 'debian', 'patches', @_); +} + +sub get_patch_dir { + my $self = shift; + return $self->get_patch_file(); +} + +## METHODS BELOW ARE INTERNAL ## + +sub _file_load { + my ($self, $file) = @_; + + open my $file_fh, '<', $file or syserr(g_('cannot read %s'), $file); + my @lines = <$file_fh>; + close $file_fh; + + return @lines; +} + +sub _file_add_line { + my ($self, $file, $line) = @_; + + my @lines; + @lines = $self->_file_load($file) if -f $file; + CORE::push @lines, $line; + chomp @lines; + + open my $file_fh, '>', $file or syserr(g_('cannot write %s'), $file); + print { $file_fh } "$_\n" foreach @lines; + close $file_fh; +} + +sub _file_drop_line { + my ($self, $file, $re) = @_; + + my @lines = $self->_file_load($file); + open my $file_fh, '>', $file or syserr(g_('cannot write %s'), $file); + print { $file_fh } $_ foreach grep { not /^\Q$re\E\s*$/ } @lines; + close $file_fh; +} + +sub read_patch_list { + my ($self, $file, %opts) = @_; + return () if not defined $file or not -f $file; + $opts{warn_options} //= 0; + my @patches; + open(my $series_fh, '<' , $file) or syserr(g_('cannot read %s'), $file); + while (defined(my $line = <$series_fh>)) { + chomp $line; + # Strip leading/trailing spaces + $line =~ s/^\s+//; + $line =~ s/\s+$//; + # Strip comment + $line =~ s/(?:^|\s+)#.*$//; + next unless $line; + if ($line =~ /^(\S+)\s+(.*)$/) { + $line = $1; + if ($2 ne '-p1') { + warning(g_('the series file (%s) contains unsupported ' . + "options ('%s', line %s); dpkg-source might " . + 'fail when applying patches'), + $file, $2, $.) if $opts{warn_options}; + } + } + if ($line =~ m{(^|/)\.\./}) { + error(g_('%s contains an insecure path: %s'), $file, $line); + } + CORE::push @patches, $line; + } + close($series_fh); + return @patches; +} + +sub write_patch_list { + my ($self, $series, $patches) = @_; + + open my $series_fh, '>', $series or syserr(g_('cannot write %s'), $series); + foreach my $patch (@{$patches}) { + print { $series_fh } "$patch\n"; + } + close $series_fh; +} + +sub restore_quilt_backup_files { + my ($self, $patch, %opts) = @_; + my $patch_dir = $self->get_db_file($patch); + return unless -d $patch_dir; + info(g_('restoring quilt backup files for %s'), $patch) if $opts{verbose}; + find({ + no_chdir => 1, + wanted => sub { + return if -d; + my $relpath_in_srcpkg = File::Spec->abs2rel($_, $patch_dir); + my $target = File::Spec->catfile($self->{dir}, $relpath_in_srcpkg); + if (-s) { + unlink($target); + make_path(dirname($target)); + unless (link($_, $target)) { + copy($_, $target) + or syserr(g_('failed to copy %s to %s'), $_, $target); + chmod_if_needed((stat _)[2], $target) + or syserr(g_("unable to change permission of '%s'"), $target); + } + } else { + # empty files are "backups" for new files that patch created + unlink($target); + } + } + }, $patch_dir); +} + +1; |