# Copyright © 2008-2011 Raphaël Hertzog # # 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 . package Dpkg::Source::Package::V3::quilt; use strict; use warnings; our $VERSION = "0.01"; # Based on wig&pen implementation use base 'Dpkg::Source::Package::V2'; use Dpkg; use Dpkg::Gettext; use Dpkg::ErrorHandling; use Dpkg::Source::Patch; use Dpkg::Source::Functions qw(erasedir fs_time); use Dpkg::IPC; use Dpkg::Vendor qw(get_current_vendor); use POSIX; use File::Basename; use File::Spec; use File::Path; use File::Copy; our $CURRENT_MINOR_VERSION = "0"; sub init_options { my ($self) = @_; $self->{'options'}{'single-debian-patch'} = 0 unless exists $self->{'options'}{'single-debian-patch'}; $self->{'options'}{'allow-version-of-quilt-db'} = [] unless exists $self->{'options'}{'allow-version-of-quilt-db'}; $self->SUPER::init_options(); } sub parse_cmdline_option { my ($self, $opt) = @_; return 1 if $self->SUPER::parse_cmdline_option($opt); if ($opt =~ /^--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 can_build { my ($self, $dir) = @_; my ($code, $msg) = $self->SUPER::can_build($dir); return ($code, $msg) if $code eq 0; my $pd = File::Spec->catdir($dir, "debian", "patches"); if (-e $pd and not -d _) { return (0, sprintf(_g("%s should be a directory or non-existing"), $pd)); } my $series_vendor = $self->get_series_file($dir); my $series_main = File::Spec->catfile($pd, "series"); foreach my $series ($series_vendor, $series_main) { if (defined($series) and -e $series and not -f _) { return (0, sprintf(_g("%s should be a file or non-existing"), $series)); } } return (1, ""); } sub get_autopatch_name { my ($self) = @_; if ($self->{'options'}{'single-debian-patch'}) { return "debian-changes"; } else { return "debian-changes-" . $self->{'fields'}{'Version'}; } } sub get_series_file { my ($self, $dir) = @_; my $pd = File::Spec->catdir($dir, "debian", "patches"); my $vendor = lc(get_current_vendor() || "debian"); foreach (File::Spec->catfile($pd, "$vendor.series"), File::Spec->catfile($pd, "series")) { return $_ if -e $_; } return undef; } sub read_patch_list { my ($self, $file, %opts) = @_; return () if not defined $file or not -f $file; $opts{"warn_options"} = 0 unless defined($opts{"warn_options"}); $opts{"skip_auto"} = 0 unless defined($opts{"skip_auto"}); my @patches; my $auto_patch = $self->get_autopatch_name(); open(SERIES, "<" , $file) || syserr(_g("cannot read %s"), $file); while(defined($_ = )) { chomp; s/^\s+//; s/\s+$//; # Strip leading/trailing spaces s/(^|\s+)#.*$//; # Strip comment next unless $_; if (/^(\S+)\s+(.*)$/) { $_ = $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"}; } } next if $opts{"skip_auto"} and $_ eq $auto_patch; error(_g("%s contains an insecure path: %s"), $file, $_) if m{(^|/)\.\./}; push @patches, $_; } close(SERIES); return @patches; } sub create_quilt_db { my ($self, $dir) = @_; my $db_dir = File::Spec->catdir($dir, ".pc"); if (not -d $db_dir) { mkdir $db_dir or syserr(_g("cannot mkdir %s"), $db_dir); } my $file = File::Spec->catfile($db_dir, ".version"); if (not -e $file) { open(VERSION, ">", $file) or syserr(_g("cannot write %s"), $file); print VERSION "2\n"; close(VERSION); } # 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 = File::Spec->catfile($db_dir, ".quilt_patches"); if (not -e $file) { open(QPATCH, ">", $file) or syserr(_g("cannot write %s"), $file); print QPATCH "debian/patches\n"; close(QPATCH); } $file = File::Spec->catfile($db_dir, ".quilt_series"); if (not -e $file) { open(QSERIES, ">", $file) or syserr(_g("cannot write %s"), $file); my $series = $self->get_series_file($dir) || "series"; $series = (File::Spec->splitpath($series))[2]; print QSERIES "$series\n"; close(QSERIES); } } sub apply_quilt_patch { my ($self, $dir, $patch, %opts) = @_; $opts{"verbose"} = 0 unless defined($opts{"verbose"}); $opts{"timestamp"} = fs_time($dir) unless defined($opts{"timestamp"}); my $path = File::Spec->catfile($dir, "debian", "patches", $patch); my $obj = Dpkg::Source::Patch->new(filename => $path); info(_g("applying %s"), $patch) if $opts{"verbose"}; $obj->apply($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', '-g0', '-E', '-b', '-B', ".pc/$patch/" ]); } sub get_patches { my ($self, $dir, %opts) = @_; my $series = $self->get_series_file($dir); return $self->read_patch_list($series, %opts); } 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 $patches = $opts{"patches"}; # Always create the quilt db so that if the maintainer calls quilt to # create a patch, it's stored in the right directory $self->create_quilt_db($dir); # Update debian/patches/series symlink if needed to allow quilt usage my $series = $self->get_series_file($dir); return unless $series; # No series, no patches my $basename = basename($series); if ($basename ne "series") { my $dest = File::Spec->catfile($dir, "debian", "patches", "series"); unlink($dest) if -l $dest; unless (-f _) { # Don't overwrite real files symlink($basename, $dest) || syserr(_g("can't create symlink %s"), $dest); } } unless (defined($patches)) { $patches = [ $self->get_patches($dir, %opts) ]; } return unless scalar(@$patches); if ($opts{'usage'} eq "preparation") { # We're applying the patches in --before-build, remember to unapply # them afterwards in --after-build my $pc_unapply = File::Spec->catfile($dir, ".pc", ".dpkg-source-unapply"); open(UNAPPLY, ">", $pc_unapply) || syserr(_g("cannot write %s"), $pc_unapply); close(UNAPPLY); } # Apply patches my $pc_applied = File::Spec->catfile($dir, ".pc", "applied-patches"); my @applied = $self->read_patch_list($pc_applied); my @patches = $self->read_patch_list($self->get_series_file($dir)); open(APPLIED, '>>', $pc_applied) || syserr(_g("cannot write %s"), $pc_applied); $opts{"timestamp"} = fs_time($pc_applied); foreach my $patch (@$patches) { $self->apply_quilt_patch($dir, $patch, %opts); print APPLIED "$patch\n"; } close(APPLIED); } sub unapply_patches { my ($self, $dir, %opts) = @_; $opts{'verbose'} = 1 unless defined $opts{'verbose'}; my $pc_applied = File::Spec->catfile($dir, ".pc", "applied-patches"); my @applied = $self->read_patch_list($pc_applied); $opts{"timestamp"} = fs_time($pc_applied) if @applied; foreach my $patch (reverse @applied) { my $path = File::Spec->catfile($dir, "debian", "patches", $patch); my $obj = Dpkg::Source::Patch->new(filename => $path); info(_g("unapplying %s"), $patch) if $opts{"verbose"}; $obj->apply($dir, timestamp => $opts{"timestamp"}, verbose => 0, force_timestamp => 1, remove_backup => 0, options => [ '-R', '-t', '-N', '-p1', '-u', '-V', 'never', '-g0', '-E', '--no-backup-if-mismatch' ]); erasedir(File::Spec->catdir($dir, ".pc", $patch)); } erasedir(File::Spec->catdir($dir, ".pc")); } 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 { return 1 if $_[0] =~ m{^debian/patches/series$} and -l $_[0]; return 1 if $_[0] =~ /^\.pc(\/|$)/; return 1 if $_[0] =~ /$self->{'options'}{'diff_ignore_regexp'}/; return 0; }; $self->{'diff_options'}{'diff_ignore_func'} = $func; } sub do_build { my ($self, $dir) = @_; my $pc_ver = File::Spec->catfile($dir, ".pc", ".version"); if (-f $pc_ver) { open(VER, "<", $pc_ver) || syserr(_g("cannot read %s"), $pc_ver); my $version = ; chomp $version; close(VER); if ($version != 2) { if (scalar grep { $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 $pc_unapply = File::Spec->catfile($dir, ".pc", ".dpkg-source-unapply"); if (-e $pc_unapply or $self->{'options'}{'unapply_patches'}) { unlink($pc_unapply); $self->unapply_patches($dir); } } sub check_patches_applied { my ($self, $dir) = @_; my $pc_applied = File::Spec->catfile($dir, ".pc", "applied-patches"); my @applied = $self->read_patch_list($pc_applied); my @patches = $self->read_patch_list($self->get_series_file($dir)); my @to_apply; foreach my $patch (@patches) { next if scalar grep { $_ eq $patch } @applied; push @to_apply, $patch; } if (scalar(@to_apply)) { my $first_patch = File::Spec->catfile($dir, "debian", "patches", $to_apply[0]); my $patch_obj = Dpkg::Source::Patch->new(filename => $first_patch); if ($patch_obj->check_apply($dir)) { info(_g("patches are not applied, applying them now")); $self->apply_patches($dir, usage => 'preparation', verbose => 1, patches => \@to_apply); } } } sub register_patch { my ($self, $dir, $tmpdiff, $patch_name) = @_; sub add_line { my ($file, $line) = @_; open(FILE, ">>", $file) || syserr(_g("cannot write %s"), $file); print FILE "$line\n"; close(FILE); } sub drop_line { my ($file, $re) = @_; open(FILE, "<", $file) || syserr(_g("cannot read %s"), $file); my @lines = ; close(FILE); open(FILE, ">", $file) || syserr(_g("cannot write %s"), $file); print(FILE $_) foreach grep { not /^\Q$re\E\s*$/ } @lines; close(FILE); } my @patches = $self->get_patches($dir); my $has_patch = (grep { $_ eq $patch_name } @patches) ? 1 : 0; my $series = $self->get_series_file($dir); $series ||= File::Spec->catfile($dir, "debian", "patches", "series"); my $applied = File::Spec->catfile($dir, ".pc", "applied-patches"); my $patch = File::Spec->catfile($dir, "debian", "patches", $patch_name); if (-s $tmpdiff) { copy($tmpdiff, $patch) || syserr(_g("failed to copy %s to %s"), $tmpdiff, $patch); chmod(0666 & ~ umask(), $patch) || syserr(_g("unable to change permission of `%s'"), $patch); } elsif (-e $patch) { unlink($patch) || syserr(_g("cannot remove %s"), $patch); } if (-e $patch) { $self->create_quilt_db($dir); # Add patch to series file if (not $has_patch) { add_line($series, $patch_name); add_line($applied, $patch_name); } # Ensure quilt meta-data are created and in sync with some trickery: # reverse-apply the patch, drop .pc/$patch, re-apply it # with the correct options to recreate the backup files my $patch_obj = Dpkg::Source::Patch->new(filename => $patch); $patch_obj->apply($dir, add_options => ['-R', '-E'], verbose => 0); erasedir(File::Spec->catdir($dir, ".pc", $patch_name)); $self->apply_quilt_patch($dir, $patch_name); } else { # Remove auto_patch from series if ($has_patch) { drop_line($series, $patch_name); drop_line($applied, $patch_name); erasedir(File::Spec->catdir($dir, ".pc", $patch_name)); } # Clean up empty series unlink($series) if -z $series; } return $patch; } # vim:et:sw=4:ts=8 1;