1
0
mirror of https://git.dpkg.org/git/dpkg/dpkg-repack.git synced 2025-04-06 18:04:35 +00:00
Files
Guillem Jover ef405ae490 Fix Conffiles field regex
There was a spurious leading slash from the // to m{} conversion.

Fixes: commit 5bc7ff670807c3df54722fcf7d6eaf64a508d8fa
2024-09-25 03:57:40 +02:00

379 lines
12 KiB
Perl
Executable File

#!/usr/bin/perl
#
# dpkg-repack puts humpty-dumpty back together again.
#
# Copyright © 1996-2006 Joey Hess <joeyh@debian.org>
# Copyright © 2012-2024 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/>.
use strict;
use warnings;
use File::stat;
use File::Temp;
use List::Util qw(any none);
use Dpkg::ErrorHandling;
use Dpkg::Path qw(find_command);
use Dpkg::IPC;
use Dpkg::Control;
use Dpkg::Control::Fields;
use Getopt::Long qw(:config posix_default bundling_values no_ignore_case);
my $VERSION = 'x.y';
my $rootdir;
my $arch;
my @deb_options;
my $generate;
my $tags = q{};
my %tag = (
description => 1,
version => 0,
);
sub usage {
print { *STDERR } <<'USAGE';
Usage: dpkg-repack [<option>...] <package-name>...
Options:
--root=<dir> Take package from filesystem rooted on <dir>.
--arch=<arch> Force the package to be built for architecture <arch>.
--generate Generate build directory but do not build deb.
To build use: "dpkg-deb --build dpkg-repack.../ .".
--tag=<type> Tag the package as being repackaged.
Types: none, description, version, all.
-d, --deb-option=<option>
Pass build <option> to dpkg-deb.
-?, --help Show this usage information.
--version Show the version.
<package-name> is the name of the package(s) to attempt to repack.
USAGE
}
sub version {
print 'dpkg-repack ' . $VERSION . "\n";
}
# Run a system command, and print an error message if it fails.
sub safe_system {
my (@command) = @_;
spawn(exec => [ @command ], wait_child => 1);
}
sub safe_chmod {
my ($dir, $perms) = @_;
chmod $perms, $dir or syserr("cannot change permissions on '$dir'");
}
sub safe_chown {
my ($uid, $gid, @pathnames) = @_;
my $nr = chown $uid, $gid, @pathnames;
if ($nr != scalar @pathnames) {
syserr("cannot change ownership on '@pathnames'");
}
}
# Make the passed directory, print an error message if it fails.
sub safe_mkdir {
my ($dir, $perms) = @_;
mkdir $dir, $perms or syserr("cannot make directory '$dir'");
# mkdir doesn't do sticky bits and suidness.
safe_chmod($dir, $perms);
}
# This makes the directories we will rebuild the package in.
sub make_deb_dirs {
my $pkgname = shift;
my %opts = (
TEMPLATE => "dpkg-repack.$pkgname.XXXXXX",
CLEANUP => !$generate,
);
my $dir = File::Temp->newdir(%opts);
safe_chmod($dir, 0755);
safe_mkdir("$dir/DEBIAN", 0755);
return $dir;
}
# Get package control file via dpkg -s.
sub extract_status {
my $pkgname = shift;
my $inst = Dpkg::Control->new(type => CTRL_FILE_STATUS);
my $fh;
my @cmd = ('dpkg-query', "--root=$rootdir/", '-s', $pkgname);
my $pid = spawn(exec => \@cmd, to_pipe => \$fh);
$inst->parse($fh, "dpkg status for $pkgname");
wait_child($pid, cmdline => "@cmd");
if ($inst->{Status} !~ m{^\S+\s+\S+\s+installed$}) {
error("package $pkgname is not fully installed: $inst->{Status}");
}
return $inst;
}
# Install the control file from the installed package control information.
sub make_control_file {
my ($build_dir, $inst) = @_;
my $ctrl = Dpkg::Control->new(type => CTRL_PKG_DEB);
field_transfer_all($inst, $ctrl);
# Add something to the Description to mention dpkg-repack.
if ($tag{description}) {
my $date = qx'date -R';
chomp $date;
$ctrl->{Description} .= "\n";
$ctrl->{Description} .= "\n";
$ctrl->{Description} .= "(Repackaged on $date by dpkg-repack.)";
}
if ($tag{version}) {
$ctrl->{Version} .= '+repack';
}
if ($arch) {
$ctrl->{Architecture} = $arch;
}
$ctrl->save("$build_dir/DEBIAN/control");
safe_chown(0, 0, "$build_dir/DEBIAN/control");
}
# Install all the files in the DEBIAN directory. (Except control file and
# file list file.)
sub populate_deb_ctrl {
my ($pkgname, $build_dir, $inst, @conffiles) = @_;
my $fh;
my @cmd = ('dpkg-query', "--root=$rootdir/",
'--control-path', $pkgname);
my $pid = spawn(exec => \@cmd, to_pipe => \$fh);
my @control_files;
while (my $fn = <$fh>) {
chomp $fn;
push @control_files, $fn;
}
wait_child($pid, cmdline => "@cmd");
foreach my $fn (@control_files) {
my ($basename) = $fn =~ m{^.*[.](.*?)$};
safe_system('cp', '-p', $fn, "$build_dir/DEBIAN/$basename");
}
# Conffiles have to be handled specially, because dpkg-query --control-path
# does not list the conffiles file. Also, we need to generate one that only
# contains conffiles that are still present on the filesystem.
if (@conffiles) {
open my $out_fh, '>', "$build_dir/DEBIAN/conffiles"
or error("write conffiles: $!");
foreach (@conffiles) {
print { $out_fh } "$_\n";
}
close $out_fh
or error("cannot write conffiles: $!");
safe_chown(0, 0, "$build_dir/DEBIAN/conffiles");
}
make_control_file($build_dir, $inst);
}
# This looks at the list of files in this package, and places them
# all on the directory tree.
sub populate_deb_fsys {
my ($pkgname, $build_dir, $inst) = @_;
# There are two types of conffiles. Obsolete conffiles should be
# skipped, while other conffiles should be included if present.
my @conffiles = ();
my @obsolete_conffiles;
my @removing_conffiles;
foreach my $line (split m{\n}, $inst->{Conffiles} // q{}) {
if ($line =~ m{^(.*)\s+(\S+)\s+obsolete$}) {
push @obsolete_conffiles, $1;
} elsif ($line =~ m{^(.*)\s+(\S+)\s+remove-on-upgrade$}) {
push @removing_conffiles, $1;
} elsif ($line =~ m{^(.*)\s+(\S+)$}) {
push @conffiles, $1;
}
}
# We need a list of all the files, for later lookups when we test to
# see where symlinks point to. Note that because we parse the output
# of the command (for diversions, below) it's important to make sure
# it runs with English language output.
my $filelist;
spawn(exec => [ 'dpkg-query', "--root=$rootdir/", '-L', $pkgname ],
env => { LC_ALL => 'C' }, to_string => \$filelist, wait_child => 1);
my @filelist = split m{\n}, $filelist;
# Set up a hash for easy lookups.
my %filelist = map { $_ => 1 } @filelist;
my $fn;
foreach my $x (0 .. $#filelist) {
my $origfn = $filelist[$x];
# dpkg -L spits out extra lines to report diversions. We have to
# parse those (ugly), to find out where the file was diverted to,
# and use the diverted file.
if (defined $filelist[$x + 1] &&
($filelist[$x + 1] =~ m{locally diverted to: (.*)} ||
$filelist[$x + 1] =~ m{diverted by .*? to: (.*)})) {
$fn = "$rootdir/$1";
# Skip over that line.
$x++;
} elsif ($origfn =~ m{package diverts others to: (.*)}) {
# Not a file at all, skip over it.
next;
} else {
$fn = $rootdir . $origfn;
}
if (any { $_ eq $fn } @obsolete_conffiles) {
warning("skipping obsolete conffile $fn");
next;
}
if (any { $_ eq $fn } @removing_conffiles) {
warning("skipping remove-on-upgrade conffile $fn");
next;
}
if (!-e $fn && !-l $fn) {
warning("cannot find file '$fn'") if none { $_ eq $fn } @conffiles;
} elsif ((-d $fn and not -l $fn) or
(-d $fn and -l $fn and not $filelist{readlink $fn} and
($x + 1 <= $#filelist and $filelist[$x + 1] =~ m{^\Q$origfn\E/}))) {
# If the package contains a file, that locally looks like a symlink
# pointing to a directory that is not in the package, then change
# it to a real directory in the repacked package. This assumes
# that in this case, the symlink was a local change (e.g., /usr
# is a symlink).
#
# However, if the directory in question contains no files in the
# filelist for this package, don't do that, just preserve the
# symlink in the repacked package. This handles the case where a
# package contains a symlink to a directory elsewhere.
#
# We rely on the order of the filelist listing parent directories
# first, and then their contents. There has to be a better way to
# do this!
my $f = q{};
foreach my $dir (split m{/+}, $origfn) {
$f .= "/$dir";
next if -d "$build_dir/$f";
my $st = stat "$rootdir/$f";
safe_mkdir("$build_dir/$f", $st->mode);
chown $st->uid, $st->gid, "$build_dir/$f";
}
} elsif (-p $fn) {
# Copy a named pipe with cp -a.
safe_system('cp', '-a', $fn, "$build_dir/$origfn");
} else {
safe_system('cp', '-pd', $fn, "$build_dir/$origfn");
}
}
return @conffiles;
}
sub archive_package {
my $pkgname = shift;
my $inst = extract_status($pkgname);
# If the umask is set wrong, the directories will end up with the wrong
# perms. (Is this still needed?)
umask 022;
# Generate the directory tree.
my $build_dir = make_deb_dirs($pkgname);
my @conffiles = populate_deb_fsys($pkgname, $build_dir, $inst);
populate_deb_ctrl($pkgname, $build_dir, $inst, @conffiles);
# Do we need to create the binary packages?
my @cmd = ('dpkg-deb', @deb_options, '--build', $build_dir, q{.});
if ($generate) {
info("created $build_dir for $pkgname");
info("to build use: \"@cmd\"");
} else {
# Let dpkg-deb do its magic.
safe_system(@cmd);
}
}
# Some sanity checks.
if ($> != 0) {
# Try to exec self with fakeroot if we are not running as root.
if (find_command('fakeroot')) {
exec 'fakeroot', '-u', $0, @ARGV;
}
error('this program should be run as root (or you could use fakeroot -u); aborting');
}
if (exists $ENV{FAKED_MODE} && $ENV{FAKED_MODE} ne 'unknown-is-real') {
warning('fakeroot run without its -u flag may corrupt some file permissions');
}
# Parse parameters.
$rootdir = q{};
my $ret = GetOptions(
'root|r=s', \$rootdir,
'arch|a=s', \$arch,
'deb-option|d=s@', \@deb_options,
'generate|g' , \$generate,
'tag=s', \$tags,
'help|?', sub { usage(); exit 0; },
'version', sub { version(); exit 0; },
);
# Handle metadata tagging.
foreach my $type (split m{,}, $tags) {
if ($type eq 'none') {
$tag{$_} = 0 foreach (keys %tag);
} elsif ($type eq 'all') {
$tag{$_} = 1 foreach (keys %tag);
} elsif (exists $tag{$type}) {
$tag{$type} = 1;
} else {
error("unknown --tag type '$type'");
}
}
if (not @ARGV or not $ret) {
usage();
exit 1;
}
foreach my $pkgname (@ARGV) {
eval {
archive_package($pkgname);
1;
} or do {
print { \*STDERR } qq{$@};
warning("problems found processing $pkgname, the package may be broken");
};
}