# 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::V2; use strict; use warnings; our $VERSION = "0.01"; use base 'Dpkg::Source::Package'; use Dpkg; use Dpkg::Gettext; use Dpkg::ErrorHandling; use Dpkg::Compression; use Dpkg::Source::Archive; use Dpkg::Source::Patch; use Dpkg::Exit; use Dpkg::Source::Functions qw(erasedir is_binary fs_time); use Dpkg::Vendor qw(run_vendor_hook); use Dpkg::Control; use Dpkg::Changelog::Parse; use POSIX; use File::Basename; use File::Temp qw(tempfile tempdir); use File::Path; use File::Spec; use File::Find; use File::Copy; our $CURRENT_MINOR_VERSION = "0"; sub init_options { my ($self) = @_; $self->SUPER::init_options(); $self->{'options'}{'include_removal'} = 0 unless exists $self->{'options'}{'include_removal'}; $self->{'options'}{'include_timestamp'} = 0 unless exists $self->{'options'}{'include_timestamp'}; $self->{'options'}{'include_binaries'} = 0 unless exists $self->{'options'}{'include_binaries'}; $self->{'options'}{'preparation'} = 1 unless exists $self->{'options'}{'preparation'}; $self->{'options'}{'skip_patches'} = 0 unless exists $self->{'options'}{'skip_patches'}; $self->{'options'}{'unapply_patches'} = 0 unless exists $self->{'options'}{'unapply_patches'}; $self->{'options'}{'skip_debianization'} = 0 unless exists $self->{'options'}{'skip_debianization'}; $self->{'options'}{'create_empty_orig'} = 0 unless exists $self->{'options'}{'create_empty_orig'}; $self->{'options'}{'auto_commit'} = 0 unless exists $self->{'options'}{'auto_commit'}; } sub parse_cmdline_option { my ($self, $opt) = @_; if ($opt =~ /^--include-removal$/) { $self->{'options'}{'include_removal'} = 1; return 1; } elsif ($opt =~ /^--include-timestamp$/) { $self->{'options'}{'include_timestamp'} = 1; return 1; } elsif ($opt =~ /^--include-binaries$/) { $self->{'options'}{'include_binaries'} = 1; return 1; } elsif ($opt =~ /^--no-preparation$/) { $self->{'options'}{'preparation'} = 0; return 1; } elsif ($opt =~ /^--skip-patches$/) { $self->{'options'}{'skip_patches'} = 1; return 1; } elsif ($opt =~ /^--unapply-patches$/) { $self->{'options'}{'unapply_patches'} = 1; return 1; } elsif ($opt =~ /^--skip-debianization$/) { $self->{'options'}{'skip_debianization'} = 1; return 1; } elsif ($opt =~ /^--create-empty-orig$/) { $self->{'options'}{'create_empty_orig'} = 1; return 1; } elsif ($opt =~ /^--abort-on-upstream-changes$/) { $self->{'options'}{'auto_commit'} = 0; return 1; } elsif ($opt =~ /^--auto-commit$/) { $self->{'options'}{'auto_commit'} = 1; return 1; } return 0; } sub do_extract { my ($self, $newdirectory) = @_; my $fields = $self->{'fields'}; my $dscdir = $self->{'basedir'}; my $basename = $self->get_basename(); my $basenamerev = $self->get_basename(1); my ($tarfile, $debianfile, %origtar, %seen); my $re_ext = $compression_re_file_ext; foreach my $file ($self->get_files()) { (my $uncompressed = $file) =~ s/\.$re_ext$//; 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-([[:alnum:]-]+)\.tar\.$re_ext$/) { $origtar{$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")); } erasedir($newdirectory); # Extract main tarball info(_g("unpacking %s"), $tarfile); my $tar = Dpkg::Source::Archive->new(filename => "$dscdir$tarfile"); $tar->extract($newdirectory, no_fixperms => 1, 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 (keys %origtar) { my $file = $origtar{$subdir}; info(_g("unpacking %s"), $file); if (-e "$newdirectory/$subdir") { warning(_g("required removal of `%s' installed by original tarball"), $subdir); erasedir("$newdirectory/$subdir"); } $tar = Dpkg::Source::Archive->new(filename => "$dscdir$file"); $tar->extract("$newdirectory/$subdir", no_fixperms => 1); } # 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"); # Exclude existing symlinks from extraction of debian.tar.gz as we # don't want to overwrite something outside of $newdirectory due to a # symlink my @exclude_symlinks; my $wanted = sub { return if not -l $_; my $fn = File::Spec->abs2rel($_, $newdirectory); push @exclude_symlinks, "--exclude", $fn; }; find({ wanted => $wanted, no_chdir => 1 }, $newdirectory); $tar = Dpkg::Source::Archive->new(filename => "$dscdir$debianfile"); $tar->extract($newdirectory, in_place => 1, options => [ '--anchored', '--no-wildcards', @exclude_symlinks ]); # Apply patches (in a separate method as it might be overriden) $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 unless defined($opts{"skip_auto"}); my @patches; my $pd = "$dir/debian/patches"; my $auto_patch = $self->get_autopatch_name(); if (-d $pd) { opendir(DIR, $pd) || syserr(_g("cannot opendir %s"), $pd); foreach my $patch (sort readdir(DIR)) { # 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); } return @patches; } sub apply_patches { my ($self, $dir, %opts) = @_; $opts{"skip_auto"} = 0 unless defined($opts{"skip_auto"}); my @patches = $self->get_patches($dir, %opts); return unless scalar(@patches); my $applied = File::Spec->catfile($dir, "debian", "patches", ".dpkg-source-applied"); open(APPLIED, '>', $applied) || syserr(_g("cannot write %s"), $applied); print APPLIED "# 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 "$patch\n"; } close(APPLIED); } 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) = @_; my $ext = "{" . join(",", sort map { compression_get_property($_, "file_ext") } compression_get_list()) . "}"; return "../" . $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) { open(APPLIED, "<", $applied) || syserr(_g("cannot read %s"), $applied); $reason = ; close(APPLIED); } if ($reason =~ /^# During preparation/ or $self->{'options'}{'unapply_patches'}) { $self->unapply_patches($dir); } } sub prepare_build { my ($self, $dir) = @_; $self->{'diff_options'} = { diff_ignore_regexp => $self->{'options'}{'diff_ignore_regexp'} . '|(^|/)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); $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, %origtar); my @origtarballs; foreach (sort $self->find_original_tarballs()) { if (/\.orig\.tar\.$compression_re_file_ext$/) { if (defined($tarfile)) { error(_g("several orig.tar files found (%s and %s) but only " . "one is allowed"), $tarfile, $_); } $tarfile = $_; push @origtarballs, $_; $self->add_file($_); } elsif (/\.orig-([[:alnum:]-]+)\.tar\.$compression_re_file_ext$/) { $origtar{$1} = $_; push @origtarballs, $_; $self->add_file($_); } } error(_g("no upstream tarball found at %s"), $self->upstream_tarball_template()) unless $tarfile; if ($opts{'usage'} eq "build") { info(_g("building %s using existing %s"), $self->{'fields'}{'Source'}, "@origtarballs"); } # Unpack a second copy for comparison my $tmp = tempdir("$dirname.orig.XXXXXX", DIR => $updir); push @Dpkg::Exit::handlers, 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 %origtar) { my $file = $origtar{$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", DIR => File::Spec->tmpdir(), UNLINK => 0); push @Dpkg::Exit::handlers, sub { unlink($tmpdiff) }; my $diff = Dpkg::Source::Patch->new(filename => $tmpdiff, compression => "none"); $diff->create(); $diff->set_header($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 @Dpkg::Exit::handlers; pop @Dpkg::Exit::handlers; 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); # Prepare handling of binary files my %auth_bin_files; my $incbin_file = File::Spec->catfile($dir, "debian", "source", "include-binaries"); if (-f $incbin_file) { open(INC, "<", $incbin_file) || syserr(_g("cannot read %s"), $incbin_file); while(defined($_ = )) { chomp; s/^\s*//; s/\s*$//; next if /^#/ or /^$/; $auth_bin_files{$_} = 1; } close(INC); } my @binary_files; my $handle_binary = sub { my ($self, $old, $new) = @_; my $relfn = File::Spec->abs2rel($new, $dir); # Include binaries if they are whitelisted or if # --include-binaries has been given if ($include_binaries or $auth_bin_files{$relfn}) { push @binary_files, $relfn; } else { errormsg(_g("cannot represent change to %s: %s"), $new, _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"), $relfn); $self->register_error(); } }; # Check if the debian directory contains unwanted binary files my $unwanted_binaries = 0; my $check_binary = sub { my $fn = File::Spec->abs2rel($_, $dir); if (-f $_ and is_binary($_)) { if ($include_binaries or $auth_bin_files{$fn}) { push @binary_files, $fn; } else { errormsg(_g("unwanted binary file: %s"), $fn); $unwanted_binaries++; } } }; my $tar_ignore_glob = "{" . join(",", map { my $copy = $_; $copy =~ s/,/\\,/g; $copy; } @{$self->{'options'}{'tar_ignore'}}) . "}"; 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, $dir); my $cwd = getcwd(); # Apply the pattern both from the top dir and from the inspected dir chdir($dir) || syserr(_g("unable to chdir to `%s'"), $dir); $exclude{$_} = 1 foreach glob($tar_ignore_glob); chdir($cwd) || syserr(_g("unable to chdir to `%s'"), $cwd); chdir($File::Find::dir) || syserr(_g("unable to chdir to `%s'"), $File::Find::dir); $exclude{$_} = 1 foreach glob($tar_ignore_glob); chdir($cwd) || 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($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; # Create a patch my $autopatch = File::Spec->catfile($dir, "debian", "patches", $self->get_autopatch_name()); my $tmpdiff = $self->generate_patch($dir, order_from => $autopatch, handle_binary => $handle_binary, skip_auto => $self->{'options'}{'auto_commit'}, usage => 'build'); unless (-z $tmpdiff or $self->{'options'}{'auto_commit'}) { 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 @Dpkg::Exit::handlers, sub { unlink($tmpdiff) }; # Install the diff as the new autopatch if ($self->{'options'}{'auto_commit'}) { mkpath(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) || syserr(_g("cannot remove %s"), $tmpdiff); pop @Dpkg::Exit::handlers; # Update debian/source/include-binaries if needed if (scalar(@binary_files) and $include_binaries) { mkpath(File::Spec->catdir($dir, "debian", "source")); open(INC, ">>", $incbin_file) || syserr(_g("cannot write %s"), $incbin_file); foreach my $binary (@binary_files) { unless ($auth_bin_files{$binary}) { print INC "$binary\n"; info(_g("adding %s to %s"), $binary, "debian/source/include-binaries"); } } close(INC); } # 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); $tar->create(options => \@tar_ignore, 'chdir' => $dir); $tar->add_directory("debian"); foreach my $binary (@binary_files) { $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"); } my $text; if (-f $ph) { open(PH, "<", $ph) || syserr(_g("cannot read %s"), $ph); $text = join("", ); close(PH); return $text; } my $ch_info = changelog_parse(offset => 0, count => 1, file => File::Spec->catfile($dir, "debian", "changelog")); return '' if not defined $ch_info; my $header = Dpkg::Control->new(type => CTRL_UNKNOWN); $header->{'Description'} = "\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'}; $text = "$header"; run_vendor_hook("extend-patch-header", \$text, $ch_info); $text .= "\n--- The information above should follow the Patch Tagging Guidelines, please checkout http://dep.debian.net/deps/dep3/ to learn about the format. Here are templates for supplementary fields that you might want to add: Origin: , Bug: Bug-Debian: http://bugs.debian.org/ Bug-Ubuntu: https://launchpad.net/bugs/ Forwarded: Reviewed-By: Last-Update: \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) || syserr(_g("failed to copy %s to %s"), $patch_file, $patch); chmod(0666 & ~ umask(), $patch) || syserr(_g("unable to change permission of `%s'"), $patch); my $applied = File::Spec->catfile($dir, "debian", "patches", ".dpkg-source-applied"); open(APPLIED, '>>', $applied) || syserr(_g("cannot write %s"), $applied); print APPLIED "$patch\n"; close(APPLIED) || syserr(_g("cannot close %s"), $applied); } elsif (-e $patch) { unlink($patch) || syserr(_g("cannot remove %s"), $patch); } return $patch; } sub commit { my ($self, $dir) = @_; my ($patch_name, $tmpdiff) = @{$self->{'options'}{'ARGV'}}; sub 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; } $self->prepare_build($dir); unless ($tmpdiff && -e $tmpdiff) { $tmpdiff = $self->generate_patch($dir, usage => "commit"); } push @Dpkg::Exit::handlers, sub { unlink($tmpdiff) }; unless (-s $tmpdiff) { unlink($tmpdiff) || syserr(_g("cannot remove %s"), $tmpdiff); info(_g("there are no local changes to record")); return; } while (bad_patch_name($dir, $patch_name)) { # Ask the patch name interactively print STDOUT _g("Enter the desired patch name: "); chomp($patch_name = ); $patch_name =~ s/\s+/-/g; $patch_name =~ s/\///g; } mkpath(File::Spec->catdir($dir, "debian", "patches")); my $patch = $self->register_patch($dir, $tmpdiff, $patch_name); system("sensible-editor", $patch); unlink($tmpdiff) || syserr(_g("cannot remove %s"), $tmpdiff); pop @Dpkg::Exit::handlers; info(_g("local changes have been recorded in a new patch: %s"), $patch); } # vim:et:sw=4:ts=8 1;