diff options
Diffstat (limited to 'lib/Debian/Debhelper/Dh_Lib.pm')
-rw-r--r-- | lib/Debian/Debhelper/Dh_Lib.pm | 3289 |
1 files changed, 3289 insertions, 0 deletions
diff --git a/lib/Debian/Debhelper/Dh_Lib.pm b/lib/Debian/Debhelper/Dh_Lib.pm new file mode 100644 index 0000000..720741f --- /dev/null +++ b/lib/Debian/Debhelper/Dh_Lib.pm @@ -0,0 +1,3289 @@ +#!/usr/bin/perl +# +# Library functions for debhelper programs, perl version. +# +# Joey Hess, GPL copyright 1997-2008. + +package Debian::Debhelper::Dh_Lib; + +use v5.28; +use warnings; +use utf8; + +# Disable unicode_strings for now until a better solution for +# Debian#971362 comes around. +no feature 'unicode_strings'; + +use Errno qw(ENOENT); + +use constant { + # Lowest compat level supported + 'MIN_COMPAT_LEVEL' => 7, + # Lowest compat level that does *not* cause deprecation + # warnings + 'LOWEST_NON_DEPRECATED_COMPAT_LEVEL' => 10, + # Lowest compat level to generate "debhelper-compat (= X)" + # relations for. + 'LOWEST_VIRTUAL_DEBHELPER_COMPAT_LEVEL' => 9, + # Highest compat level permitted + 'MAX_COMPAT_LEVEL' => 15, + # Magic value for xargs + 'XARGS_INSERT_PARAMS_HERE' => \'<INSERT-HERE>', #'# Hi emacs. + # Magic value for debhelper tools to request "current version" + 'DH_BUILTIN_VERSION' => \'<DH_LIB_VERSION>', #'# Hi emacs. + # Default Package-Type / extension (must be aligned with dpkg) + 'DEFAULT_PACKAGE_TYPE' => 'deb', +}; + + +# The Makefile changes this if debhelper is installed in a PREFIX. +my $prefix="/usr"; +# The Makefile changes this during install to match the actual version. +use constant HIGHEST_STABLE_COMPAT_LEVEL => undef; + +# Locations we search for data files by default +my @DATA_INC_PATH = ( + "${prefix}/share/debhelper", +); +# Enable the use of DH_DATAFILES for testing purposes. +unshift(@DATA_INC_PATH, split(':', $ENV{'DH_DATAFILES'})) if exists($ENV{'DH_DATAFILES'}); + +use constant { + # Package-Type / extension for dbgsym packages + # TODO: Find a way to determine this automatically from the vendor + # - blocked by Dpkg::Vendor having a rather high load time (for debhelper) + 'DBGSYM_PACKAGE_TYPE' => DEFAULT_PACKAGE_TYPE, + # Lowest compat level supported that is not scheduled for removal. + # - Set to MIN_COMPAT_LEVEL when there are no pending compat removals. + 'MIN_COMPAT_LEVEL_NOT_SCHEDULED_FOR_REMOVAL' => MIN_COMPAT_LEVEL, +}; + + +# Internal constants used to define limits in variable expansions. +use constant { + # How many expansions are permitted in total. + _VAR_SUBST_EXPANSION_COUNT_LIMIT => 50, + # When recursion is enabled, how many times will we expand a pattern + # on the same position in the string. + _VAR_SUBST_SAME_POSITION_RECURSION_LIMIT => 20, + # Expansions are always allowed to grow up to this length regardless + # of original input size (provided it does not trip another limit) + _VAR_SUBST_EXPANSION_MIN_SUPPORTED_SIZE_LIMIT => 4096, + # Factor input is allowed to grow before it triggers an error + # (_VAR_SUBST_EXPANSION_MIN_SUPPORTED_SIZE_LIMIT overrules this for a + # given input if the max size limit computed with this factor is less + # than _VAR_SUBST_EXPANSION_MIN_SUPPORTED_SIZE_LIMIT) + _VAR_SUBST_EXPANSION_DYNAMIC_EXPANSION_FACTOR_LIMIT => 3, +}; + + +use Errno qw(ENOENT EXDEV); +use Exporter qw(import); +use File::Glob qw(bsd_glob GLOB_CSH GLOB_NOMAGIC GLOB_TILDE); +our (@EXPORT, %dh); +@EXPORT = ( + # debhelper basis functionality +qw( + init + %dh + compat +), + # External command tooling API +qw( + doit + doit_noerror + qx_cmd + xargs + XARGS_INSERT_PARAMS_HERE + print_and_doit + print_and_doit_noerror + + complex_doit + escape_shell +), + # Logging/messaging/error handling +qw( + error + error_exitcode + warning + verbose_print + nonquiet_print +), + # Package related actions +qw( + getpackages + sourcepackage + tmpdir + dbgsym_tmpdir + default_sourcedir + pkgfile + pkgext + pkgfilename + package_is_arch_all + package_binary_arch + package_declared_arch + package_multiarch + package_is_essential + package_section + package_arch + package_type + package_field + process_pkg + compute_doc_main_package + isnative + is_udeb +), + # File/path related actions +qw( + basename + dirname + mkdirs + install_file + install_prog + install_lib + install_dir + install_dh_config_file + make_symlink + make_symlink_raw_target + rename_path + find_hardlinks + rm_files + excludefile + is_so_or_exec_elf_file + is_empty_dir + reset_perm_and_owner + log_installed_files + + filearray + filedoublearray + glob_expand + glob_expand_error_handler_reject + glob_expand_error_handler_warn_and_discard + glob_expand_error_handler_silently_ignore + glob_expand_error_handler_reject_nomagic_warn_discard +), + # Generate triggers, substvars, maintscripts, build-time temporary files +qw( + autoscript + autotrigger + addsubstvar + delsubstvar + ensure_substvars_are_present + + generated_file + restore_file_on_clean +), + # Split tasks among different cores +qw( + on_pkgs_in_parallel + on_items_in_parallel + on_selected_pkgs_in_parallel +), + # R³ framework +qw( + should_use_root + gain_root_cmd + +), + # Architecture, cross-tooling, build options and profiles +qw( + dpkg_architecture_value + hostarch + cross_command + is_cross_compiling + is_build_profile_active + get_buildoption + perl_cross_incdir +), + # Other +qw( + open_gz + get_source_date_epoch + get_non_binnmu_date_epoch + deprecated_functionality +), + # Special-case functionality (e.g. tool specific), debhelper(-core) functionality and deprecated functions +qw( + inhibit_log + load_log + write_log + commit_override_log + debhelper_script_subst + debhelper_script_per_package_subst + is_make_jobserver_unavailable + clean_jobserver_makeflags + set_buildflags + DEFAULT_PACKAGE_TYPE + DBGSYM_PACKAGE_TYPE + DH_BUILTIN_VERSION + is_known_package + assert_opt_is_known_package + restore_all_files + + buildarch +)); + +my $MAX_PROCS = get_buildoption("parallel") || 1; +my $DH_TOOL_VERSION; + +our $PKGNAME_REGEX = qr/[a-z0-9][-+\.a-z0-9]+/o; +our $PKGVERSION_REGEX = qr/ + (?: \d+ : )? # Optional epoch + [0-9][0-9A-Za-z.+:~]* # Upstream version (with no hyphens) + (?: - [0-9A-Za-z.+:~]+ )* # Optional debian revision (+ upstreams versions with hyphens) + /xoa; +our $MAINTSCRIPT_TOKEN_REGEX = qr/[A-Za-z0-9_.+]+/o; +our $TOOL_NAME = basename($0); + +# From Policy 5.1: +# +# The field name is composed of US-ASCII characters excluding control +# characters, space, and colon (i.e., characters in the ranges U+0021 +# (!) through U+0039 (9), and U+003B (;) through U+007E (~), +# inclusive). Field names must not begin with the comment character +# (U+0023 #), nor with the hyphen character (U+002D -). +our $DEB822_FIELD_REGEX = qr/ + [\x21\x22\x24-\x2C\x2F-\x39\x3B-\x7F] # First character + [\x21-\x39\x3B-\x7F]* # Subsequent characters (if any) + /xoa; + +our $PARSE_DH_SEQUENCE_INFO = 0; + +# We need logging in compat 9 or in override/hook targets (for --remaining-packages to work) +# - This option is a global toggle to disable logs for special commands (e.g. dh or dh_clean) +# It is initialized during "init". This implies that commands that never calls init are +# not dh_* commands or do not need the log +my $write_log = undef; + +sub init { + my %params=@_; + + if ($params{internal_parse_dh_sequence_info}) { + $PARSE_DH_SEQUENCE_INFO = 1; + } + + # Check if we can by-pass the expensive Getopt::Long by optimising for the + # common case of "-a" or "-i" + if (scalar(@ARGV) == 1 && ($ARGV[0] eq '-a' || $ARGV[0] eq '-i') && + ! (defined $ENV{DH_OPTIONS} && length $ENV{DH_OPTIONS}) && + ! (defined $ENV{DH_INTERNAL_OPTIONS} && length $ENV{DH_INTERNAL_OPTIONS})) { + + # Single -i or -a as dh does it. + if ($ARGV[0] eq '-i') { + push(@{$dh{DOPACKAGES}}, getpackages('indep')); + $dh{DOINDEP} = 1; + } else { + push(@{$dh{DOPACKAGES}}, getpackages('arch')); + $dh{DOARCH} = 1; + } + + if (! @{$dh{DOPACKAGES}}) { + if (! $dh{BLOCK_NOOP_WARNINGS}) { + warning("You asked that all arch in(dep) packages be built, but there are none of that type."); + } + exit(0); + } + # Clear @ARGV so we do not hit the expensive case below + @ARGV = (); + } + + # Check to see if an option line starts with a dash, + # or DH_OPTIONS is set. + # If so, we need to pass this off to the resource intensive + # Getopt::Long, which I'd prefer to avoid loading at all if possible. + if ((defined $ENV{DH_OPTIONS} && length $ENV{DH_OPTIONS}) || + (defined $ENV{DH_INTERNAL_OPTIONS} && length $ENV{DH_INTERNAL_OPTIONS}) || + grep /^-/, @ARGV) { + eval { require Debian::Debhelper::Dh_Getopt; }; + error($@) if $@; + Debian::Debhelper::Dh_Getopt::parseopts(%params); + } + + # Another way to set excludes. + if (exists $ENV{DH_ALWAYS_EXCLUDE} && length $ENV{DH_ALWAYS_EXCLUDE}) { + push @{$dh{EXCLUDE}}, split(":", $ENV{DH_ALWAYS_EXCLUDE}); + } + + # Generate EXCLUDE_FIND. + if ($dh{EXCLUDE}) { + $dh{EXCLUDE_FIND}=''; + foreach (@{$dh{EXCLUDE}}) { + my $x=$_; + $x=escape_shell($x); + $x=~s/\./\\\\./g; + $dh{EXCLUDE_FIND}.="-regex .\\*$x.\\* -or "; + } + $dh{EXCLUDE_FIND}=~s/ -or $//; + } + + # Check to see if DH_VERBOSE environment variable was set, if so, + # make sure verbose is on. Otherwise, check DH_QUIET. + if (defined $ENV{DH_VERBOSE} && $ENV{DH_VERBOSE} ne "") { + $dh{VERBOSE}=1; + } elsif (defined $ENV{DH_QUIET} && $ENV{DH_QUIET} ne "" || get_buildoption("terse")) { + $dh{QUIET}=1; + } + + # Check to see if DH_NO_ACT environment variable was set, if so, + # make sure no act mode is on. + if (defined $ENV{DH_NO_ACT} && $ENV{DH_NO_ACT} ne "") { + $dh{NO_ACT}=1; + } + + # Get the name of the main binary package (first one listed in + # debian/control). Only if the main package was not set on the + # command line. + if (! exists $dh{MAINPACKAGE} || ! defined $dh{MAINPACKAGE}) { + my @allpackages=getpackages(); + $dh{MAINPACKAGE}=$allpackages[0]; + } + + # Check if packages to build have been specified, if not, fall back to + # the default, building all relevant packages. + if (! defined $dh{DOPACKAGES} || ! @{$dh{DOPACKAGES}}) { + push @{$dh{DOPACKAGES}}, getpackages('both'); + } + + # Check to see if -P was specified. If so, we can only act on a single + # package. + if ($dh{TMPDIR} && $#{$dh{DOPACKAGES}} > 0) { + error("-P was specified, but multiple packages would be acted on (".join(",",@{$dh{DOPACKAGES}}).")."); + } + + # Figure out which package is the first one we were instructed to build. + # This package gets special treatement: files and directories specified on + # the command line may affect it. + $dh{FIRSTPACKAGE}=${$dh{DOPACKAGES}}[0]; + + # If no error handling function was specified, just propagate + # errors out. + if (! exists $dh{ERROR_HANDLER} || ! defined $dh{ERROR_HANDLER}) { + $dh{ERROR_HANDLER}='exit 1'; + } + + $dh{U_PARAMS} //= []; + + if ($params{'inhibit_log'}) { + $write_log = 0; + } else { + # Only initialize if unset (i.e. avoid overriding an early call + # to inhibit_log() + $write_log //= 1; + } +} + +# Ensure the log is written if requested but only if the command was +# successful. +sub END { + return if $? != 0 or not $write_log; + # If there is no 'debian/control', then we are not being run from + # a package directory and then the write_log will not do what we + # expect. + return if not -f 'debian/control'; + if (compat(9, 1) || $ENV{DH_INTERNAL_OVERRIDE}) { + write_log($TOOL_NAME, @{$dh{DOPACKAGES}}); + } +} + +sub logfile { + my $package=shift; + my $ext=pkgext($package); + return "debian/${ext}debhelper.log" +} + +sub load_log { + my ($package, $db)=@_; + + my @log; + open(LOG, "<", logfile($package)) || return; + while (<LOG>) { + chomp; + my $command = $_; + push @log, $command; + $db->{$package}{$command}=1 if defined $db; + } + close LOG; + return @log; +} + +sub write_log { + my $cmd=shift; + my @packages=@_; + + return if $dh{NO_ACT}; + + foreach my $package (@packages) { + my $log = logfile($package); + open(LOG, ">>", $log) || error("failed to write to ${log}: $!"); + print LOG $cmd."\n"; + close LOG; + } +} + +sub commit_override_log { + my @packages=@_; + + return if $dh{NO_ACT}; + + foreach my $package (@packages) { + my @log = load_log($package); + my $log = logfile($package); + open(LOG, ">", $log) || error("failed to write to ${log}: $!"); + print LOG $_."\n" foreach @log; + close LOG; + } +} + +sub inhibit_log { + $write_log=0; +} + +# Pass it an array containing the arguments of a shell command like would +# be run by exec(). It turns that into a line like you might enter at the +# shell, escaping metacharacters and quoting arguments that contain spaces. +sub escape_shell { + my @args=@_; + my @ret; + foreach my $word (@args) { + if ($word=~/\s/) { + # Escape only a few things since it will be quoted. + # Note we use double quotes because you cannot + # escape ' in single quotes, while " can be escaped + # in double. + # This does make -V"foo bar" turn into "-Vfoo bar", + # but that will be parsed identically by the shell + # anyway.. + $word=~s/([\n`\$"\\])/\\$1/g; + push @ret, "\"$word\""; + } + else { + # This list is from _Unix in a Nutshell_. (except '#') + $word=~s/([\s!"\$()*+#;<>?@\[\]\\`|~])/\\$1/g; + push @ret,$word; + } + } + return join(' ', @ret); +} + +# Run a command, and display the command to stdout if verbose mode is on. +# Throws error if command exits nonzero. +# +# All commands that modify files in $TMP should be run via this +# function. +# +# Note that this cannot handle complex commands, especially anything +# involving redirection. Use complex_doit instead. +sub doit { + doit_noerror(@_) || error_exitcode(_format_cmdline(@_)); +} + +sub doit_noerror { + verbose_print(_format_cmdline(@_)) if $dh{VERBOSE}; + + goto \&_doit; +} + +sub print_and_doit { + print_and_doit_noerror(@_) || error_exitcode(_format_cmdline(@_)); +} + +sub print_and_doit_noerror { + nonquiet_print(_format_cmdline(@_)); + + goto \&_doit; +} + +sub _post_fork_setup_and_exec { + my ($close_stdin, $options, @cmd) = @_; + if (defined($options)) { + if (defined(my $dir = $options->{chdir})) { + if ($dir ne '.') { + chdir($dir) or error("chdir(\"${dir}\") failed: $!"); + } + } + if ($close_stdin) { + open(STDIN, '<', '/dev/null') or error("redirect STDIN failed: $!"); + } + if (defined(my $output = $options->{stdout})) { + open(STDOUT, '>', $output) or error("redirect STDOUT failed: $!"); + } + if (defined(my $update_env = $options->{update_env})) { + while (my ($k, $v) = each(%{$update_env})) { + if (defined($v)) { + $ENV{$k} = $v; + } else { + delete($ENV{$k}); + } + } + } + } + # Force execvp call to avoid shell. Apparently, even exec can + # involve a shell if you don't do this. + exec { $cmd[0] } @cmd or error('exec (for cmd: ' . escape_shell(@cmd) . ") failed: $!"); +} + +sub _doit { + my (@cmd) = @_; + my $options = ref($cmd[0]) ? shift(@cmd) : undef; + # In compat <= 11, we warn, in compat 12 we assume people know what they are doing. + if (not defined($options) and @cmd == 1 and compat(12) and $cmd[0] =~ m/[\s<&>|;]/) { + deprecated_functionality('doit() + doit_*() calls will no longer spawn a shell in compat 12 for single string arguments (please use complex_doit instead)', + 12); + return 1 if $dh{NO_ACT}; + return system(@cmd) == 0; + } + return 1 if $dh{NO_ACT}; + my $pid = fork() // error("fork(): $!"); + if (not $pid) { + _post_fork_setup_and_exec(1, $options, @cmd) // error("Assertion error: sub should not return!"); + } + return waitpid($pid, 0) == $pid && $? == 0; +} + +sub _format_cmdline { + my (@cmd) = @_; + my $options = ref($cmd[0]) ? shift(@cmd) : {}; + my $cmd_line = escape_shell(@cmd); + if (defined(my $update_env = $options->{update_env})) { + my $need_env = 0; + my @params; + for my $key (sort(keys(%{$update_env}))) { + my $value = $update_env->{$key}; + if (defined($value)) { + my $quoted_key = escape_shell($key); + push(@params, join('=', $quoted_key, escape_shell($value))); + # shell does not like: "FU BAR"=1 cmd + # if the ENV key has weird symbols, the best bet is to use env + $need_env = 1 if $quoted_key ne $key; + } else { + $need_env = 1; + push(@params, escape_shell("--unset=${key}")); + } + } + unshift(@params, 'env', '--') if $need_env; + $cmd_line = join(' ', @params, $cmd_line); + } + if (defined(my $dir = $options->{chdir})) { + $cmd_line = join(' ', 'cd', escape_shell($dir), '&&', $cmd_line) if $dir ne '.'; + } + if (defined(my $output = $options->{stdout})) { + $cmd_line .= ' > ' . escape_shell($output); + } + return $cmd_line; +} + +sub qx_cmd { + my (@cmd) = @_; + my $options = ref($cmd[0]) ? shift(@cmd) : undef; + my ($output, @output); + my $pid = open(my $fd, '-|') // error('fork (for cmd: ' . escape_shell(@cmd) . ") failed: $!"); + if ($pid == 0) { + _post_fork_setup_and_exec(0, $options, @cmd) // error("Assertion error: sub should not return!"); + } + if (wantarray) { + @output = <$fd>; + } else { + local $/ = undef; + $output = <$fd>; + } + if (not close($fd)) { + error("close pipe failed: $!") if $!; + error_exitcode(escape_shell(@cmd)); + } + return @output if wantarray; + return $output; +} + +# Run a command and display the command to stdout if verbose mode is on. +# Use doit() if you can, instead of this function, because this function +# forks a shell. However, this function can handle more complicated stuff +# like redirection. +sub complex_doit { + verbose_print(join(" ",@_)); + + if (! $dh{NO_ACT}) { + # The join makes system get a scalar so it forks off a shell. + system(join(" ", @_)) == 0 || error_exitcode(join(" ", @_)) + } +} + + +sub error_exitcode { + my $command=shift; + if ($? == -1) { + error("$command failed to execute: $!"); + } + elsif ($? & 127) { + error("$command died with signal ".($? & 127)); + } + elsif ($?) { + error("$command returned exit code ".($? >> 8)); + } + else { + warning("This tool claimed that $command have failed, but it"); + warning("appears to have returned 0."); + error("Probably a bug in this tool is hiding the actual problem."); + } +} + +# Some shortcut functions for installing files and dirs to always +# have the same owner and mode +# install_file - installs a non-executable +# install_prog - installs an executable +# install_lib - installs a shared library (some systems may need x-bit, others don't) +# install_dir - installs a directory +{ + my $_loaded = 0; + sub install_file { + unshift(@_, 0644); + goto \&_install_file_to_path; + } + + sub install_prog { + unshift(@_, 0755); + goto \&_install_file_to_path; + } + sub install_lib { + unshift(@_, 0644); + goto \&_install_file_to_path; + } + + sub _install_file_to_path { + my ($mode, $source, $dest) = @_; + if (not $_loaded) { + $_loaded++; + require File::Copy; + } + verbose_print(sprintf('install -p -m%04o %s', $mode, escape_shell($source, $dest))) + if $dh{VERBOSE}; + return 1 if $dh{NO_ACT}; + # "install -p -mXXXX foo bar" silently discards broken + # symlinks to install the file in place. File::Copy does not, + # so emulate it manually. (#868204) + if ( -l $dest and not -e $dest and not unlink($dest) and $! != ENOENT) { + error("unlink $dest failed: $!"); + } + File::Copy::copy($source, $dest) or error("copy($source, $dest): $!"); + chmod($mode, $dest) or error("chmod($mode, $dest): $!"); + my (@stat) = stat($source); + error("stat($source): $!") if not @stat; + utime($stat[8], $stat[9], $dest) + or error(sprintf("utime(%d, %d, %s): $!", $stat[8] , $stat[9], $dest)); + return 1; + } +} + + +sub _mkdirs { + my ($log, @dirs) = @_; + return if not @dirs; + if ($log && $dh{VERBOSE}) { + verbose_print(sprintf('install -m0755 -d %s', escape_shell(@dirs))); + } + return 1 if $dh{NO_ACT}; + state $_loaded; + if (not $_loaded) { + $_loaded++; + require File::Path; + } + my %opts = ( + # install -d uses 0755 (no umask), make_path uses 0777 (& umask) by default. + # Since we claim to run install -d, then ensure the mode is correct. + 'chmod' => 0755, + ); + eval { + File::Path::make_path(@dirs, \%opts); + }; + if (my $err = "$@") { + $err =~ s/\s+at\s+\S+\s+line\s+\d+\.?\n//; + error($err); + } + return; +} + +sub mkdirs { + my @to_create = grep { not -d $_ } @_; + return _mkdirs(0, @to_create); +} + +sub install_dir { + my @dirs = @_; + return _mkdirs(1, @dirs); +} + +sub rename_path { + my ($source, $dest) = @_; + + if ($dh{VERBOSE}) { + my $files = escape_shell($source, $dest); + verbose_print("mv $files"); + } + return 1 if $dh{NO_ACT}; + if (not rename($source, $dest)) { + my $ok = 0; + if ($! == EXDEV) { + # Replay with a fork+exec to handle crossing two mount + # points (See #897569) + $ok = _doit('mv', $source, $dest); + } + if (not $ok) { + my $files = escape_shell($source, $dest); + error("mv $files: $!"); + } + } + return 1; +} + +sub reset_perm_and_owner { + my ($mode, @paths) = @_; + my $use_root = should_use_root(); + if ($dh{VERBOSE}) { + verbose_print(sprintf('chmod %#o -- %s', $mode, escape_shell(@paths))); + verbose_print(sprintf('chown 0:0 -- %s', escape_shell(@paths))) if $use_root; + } + return if $dh{NO_ACT}; + for my $path (@paths) { + chmod($mode, $path) or error(sprintf('chmod(%#o, %s): %s', $mode, $path, $!)); + if ($use_root) { + chown(0, 0, $path) or error("chown(0, 0, $path): $!"); + } + } +} + +# Run a command that may have a huge number of arguments, like xargs does. +# Pass in a reference to an array containing the arguments, and then other +# parameters that are the command and any parameters that should be passed to +# it each time. +sub xargs { + my ($args, @static_args) = @_; + + # The kernel can accept command lines up to 20k worth of characters. + my $command_max=20000; # LINUX SPECIFIC!! + # (And obsolete; it's bigger now.) + # I could use POSIX::ARG_MAX, but that would be slow. + + # Figure out length of static portion of command. + my $static_length=0; + my $subst_index = -1; + for my $i (0..$#static_args) { + my $arg = $static_args[$i]; + if ($arg eq XARGS_INSERT_PARAMS_HERE) { + error("Only one insertion place supported in xargs, got command: @static_args") if $subst_index > -1; + $subst_index = $i; + next; + } + $static_length+=length($arg)+1; + } + + my @collect=(); + my $length=$static_length; + foreach (@$args) { + if (length($_) + 1 + $static_length > $command_max) { + error("This command is greater than the maximum command size allowed by the kernel, and cannot be split up further. What on earth are you doing? \"@_ $_\""); + } + $length+=length($_) + 1; + if ($length < $command_max) { + push @collect, $_; + } + else { + if ($#collect > -1) { + if ($subst_index < 0) { + doit(@static_args, @collect); + } else { + my @cmd = @static_args; + splice(@cmd, $subst_index, 1, @collect); + doit(@cmd); + } + } + @collect=($_); + $length=$static_length + length($_) + 1; + } + } + if ($#collect > -1) { + if ($subst_index < 0) { + doit(@static_args, @collect); + } else { + my @cmd = @static_args; + splice(@cmd, $subst_index, 1, @collect); + doit(@cmd); + } + } +} + +# Print something if the verbose flag is on. +sub verbose_print { + my $message=shift; + + if ($dh{VERBOSE}) { + print "\t$message\n"; + } +} + +# Print something unless the quiet flag is on +sub nonquiet_print { + my $message=shift; + + if (!$dh{QUIET}) { + if (defined($message)) { + print "\t$message\n"; + } else { + print "\n"; + } + } +} + +sub _color { + my ($msg, $color) = @_; + state $_use_color; + if (not defined($_use_color)) { + # This part is basically Dpkg::ErrorHandling::setup_color over again + # with some tweaks. + # (but the module uses Dpkg + Dpkg::Gettext, so it is very expensive + # to load) + my $mode = $ENV{'DH_COLORS'} // $ENV{'DPKG_COLORS'}; + # Support NO_COLOR (https://no-color.org/) + $mode //= exists($ENV{'NO_COLOR'}) ? 'never' : 'auto'; + + # Initialize with no color, so we are guaranteed to only do this once. + $_use_color = 0; + if ($mode eq 'auto') { + $_use_color = 1 if -t *STDOUT or -t *STDERR; + } elsif ($mode eq 'always') { + $_use_color = 1; + } + + eval { + require Term::ANSIColor if $_use_color; + }; + if ($@) { + # In case of errors, skip colors. + $_use_color = 0; + } + } + if ($_use_color) { + local $ENV{'NO_COLOR'} = undef; + $msg = Term::ANSIColor::colored($msg, $color); + } + return $msg; +} + +# Output an error message and die (can be caught). +sub error { + my ($message) = @_; + # ensure the error code is well defined. + $! = 255; + die(_color($TOOL_NAME, 'bold') . ': ' . _color('error', 'bold red') . ": $message\n"); +} + +# Output a warning. +sub warning { + my ($message) = @_; + $message //= ''; + + print STDERR _color($TOOL_NAME, 'bold') . ': ' . _color('warning', 'bold yellow') . ": $message\n"; +} + +# Returns the basename of the argument passed to it. +sub basename { + my $fn=shift; + + $fn=~s/\/$//g; # ignore trailing slashes + $fn=~s:^.*/(.*?)$:$1:; + return $fn; +} + +# Returns the directory name of the argument passed to it. +sub dirname { + my $fn=shift; + + $fn=~s/\/$//g; # ignore trailing slashes + $fn=~s:^(.*)/.*?$:$1:; + return $fn; +} + +# Pass in a number, will return true iff the current compatibility level +# is less than or equal to that number. +my ($compat_from_bd, $compat_from_dctrl); +{ + my $check_pending_removals = get_buildoption('dherroron', '') eq 'obsolete-compat-levels' ? 1 : 0; + my $warned_compat = $ENV{DH_INTERNAL_TESTSUITE_SILENT_WARNINGS} ? 1 : 0; + my $declared_compat; + my $delared_compat_source; + my $c; + + # Used mainly for testing + sub resetcompat { + undef $c; + undef $compat_from_bd; + undef $compat_from_dctrl; + } + + sub _load_compat_info { + my ($nowarn) = @_; + + getpackages() if not defined($compat_from_bd); + + $c=1; + if (-e 'debian/compat') { + open(my $compat_in, '<', "debian/compat") || error "debian/compat: $!"; + my $l=<$compat_in>; + close($compat_in); + if (! defined $l || ! length $l) { + error("debian/compat must contain a positive number (found an empty first line)"); + } + else { + chomp $l; + my $new_compat = $l; + $new_compat =~ s/^\s*+//; + $new_compat =~ s/\s*+$//; + if ($new_compat !~ m/^\d+$/) { + error("debian/compat must contain a positive number (found: \"${new_compat}\")"); + } + if ($compat_from_bd != -1 or $compat_from_dctrl != -1) { + warning("Please specify the debhelper compat level exactly once."); + warning(" * debian/compat requests compat ${new_compat}."); + warning(" * debian/control requests compat ${compat_from_bd} via \"debhelper-compat (= ${compat_from_bd})\"") + if $compat_from_bd > -1; + warning(" * debian/control requests compat ${compat_from_dctrl} via \"X-DH-Compat: ${compat_from_dctrl}\"") + if $compat_from_dctrl > -1; + warning(); + warning("Hint: If you just added a build-dependency on debhelper-compat, then please remember to remove debian/compat") + if $compat_from_bd > -1; + warning("Hint: If you just added a X-DH-Compat field, then please remember to remove debian/compat") + if $compat_from_dctrl > -1; + warning(); + error("debhelper compat level specified both in debian/compat and in debian/control"); + } + $c = $new_compat; + } + if ($c >= 15 or (HIGHEST_STABLE_COMPAT_LEVEL//0) > 13) { + error("Sorry, debian/compat is no longer a supported source for the debhelper compat level." + . " Please add a Build-Depends on `debhelper-compat (= C)` or add `X-DH-Compat: C` to the source stanza" + . " of d/control and remove debian/compat."); + } + if ($c >= 13 and not $nowarn) { + warning("Use of debian/compat is deprecated and will be removed in debhelper (>= 14~).") + } + $delared_compat_source = 'debian/compat'; + } elsif ($compat_from_bd != -1) { + $c = $compat_from_bd; + $delared_compat_source = "Build-Depends: debhelper-compat (= $c)"; + } elsif ($compat_from_dctrl != -1) { + $c = $compat_from_dctrl; + $delared_compat_source = "X-DH-Comat: $c"; + } elsif (not $nowarn) { + # d/compat deliberately omitted since we do not want to recommend users to it. + error("Please specify the compatibility level in debian/control. Such as, via Build-Depends: debhelper-compat (= X)"); + } + + $declared_compat = int($c); + + if (defined $ENV{DH_COMPAT}) { + my $override = $ENV{DH_COMPAT}; + error("The environment variable DH_COMPAT must be a positive integer") + if $override ne q{} and $override !~ m/^\d+$/; + $c=int($ENV{DH_COMPAT}) if $override ne q{}; + } + } + + sub get_compat_info { + if (not $c) { + _load_compat_info(1); + } + return ($c, $declared_compat, $delared_compat_source); + } + + sub compat { + my ($num, $nowarn) = @_; + + if (not $c) { + _load_compat_info($nowarn); + } + + if (not $nowarn) { + if ($c < MIN_COMPAT_LEVEL) { + error("Compatibility levels before ${\MIN_COMPAT_LEVEL} are no longer supported (level $c requested)"); + } + + if ($check_pending_removals and $c < MIN_COMPAT_LEVEL_NOT_SCHEDULED_FOR_REMOVAL) { + my $v = MIN_COMPAT_LEVEL_NOT_SCHEDULED_FOR_REMOVAL; + error("Compatibility levels before ${v} are scheduled for removal and DH_COMPAT_ERROR_ON_PENDING_REMOVAL was set (level $c requested)"); + } + + if ($c < LOWEST_NON_DEPRECATED_COMPAT_LEVEL && ! $warned_compat) { + warning("Compatibility levels before ${\LOWEST_NON_DEPRECATED_COMPAT_LEVEL} are deprecated (level $c in use)"); + $warned_compat=1; + } + + if ($c > MAX_COMPAT_LEVEL) { + error("Sorry, but ${\MAX_COMPAT_LEVEL} is the highest compatibility level supported by this debhelper."); + } + } + + return ($c <= $num); + } +} + +# Pass it a name of a binary package, it returns the name of the tmp dir to +# use, for that package. +sub tmpdir { + my $package=shift; + + if ($dh{TMPDIR}) { + return $dh{TMPDIR}; + } + else { + return "debian/$package"; + } +} + +# Pass it a name of a binary package, it returns the name of the staging dir to +# use, for that package. (Usually debian/tmp) +sub default_sourcedir { + my ($package) = @_; + + return 'debian/tmp'; +} + +# Pass this the name of a binary package, and the name of the file wanted +# for the package, and it will return the actual existing filename to use. +# +# It tries several filenames: +# * debian/package.filename.hostarch +# * debian/package.filename.hostos +# * debian/package.filename +# * debian/filename (if the package is the main package and compat < 15) +# If --name was specified then the files +# must have the name after the package name: +# * debian/package.name.filename.hostarch +# * debian/package.name.filename.hostos +# * debian/package.name.filename +# * debian/name.filename (if the package is the main package and compat < 15) + +{ + my %_check_expensive; + + sub pkgfile { + # NB: $nameless_variant_handling is an implementation-detail; third-party packages + # should not rely on it. + my ($package, $filename, $nameless_variant_handling) = @_; + my (@try, $check_expensive); + + if (not exists($_check_expensive{$filename})) { + my @f = grep { + !/\.debhelper$/ + } bsd_glob("debian/*.$filename.*", GLOB_CSH & ~(GLOB_NOMAGIC|GLOB_TILDE)); + if (not @f) { + $check_expensive = 0; + } else { + $check_expensive = 1; + } + $_check_expensive{$filename} = $check_expensive; + } else { + $check_expensive = $_check_expensive{$filename}; + } + + # Rewrite $filename after the check_expensive globbing above + # as $dh{NAME} is used as a prefix (so the glob above will + # cover it). + # + # In practise, it should not matter as NAME is ether set + # globally or not. But if someone is being "clever" then the + # cache is reusable and for the general/normal case, it has no + # adverse effects. + if (defined $dh{NAME}) { + $filename="$dh{NAME}.$filename"; + } + + if (ref($package) eq 'ARRAY') { + # !!NOT A PART OF THE PUBLIC API!! + # Bulk test used by dh to speed up the can_skip check. It + # is NOT useful for finding the most precise pkgfile. + push(@try, "debian/$filename"); + for my $pkg (@{$package}) { + push(@try, "debian/${pkg}.${filename}"); + if ($check_expensive) { + my $cross_type = uc(package_cross_type($pkg)); + push(@try, + "debian/${pkg}.${filename}.".dpkg_architecture_value("DEB_${cross_type}_ARCH"), + "debian/${pkg}.${filename}.".dpkg_architecture_value("DEB_${cross_type}_ARCH_OS"), + ); + } + } + } else { + # Avoid checking for hostarch+hostos unless we have reason + # to believe that they exist. + if ($check_expensive) { + my $cross_type = uc(package_cross_type($package)); + push(@try, + "debian/${package}.${filename}.".dpkg_architecture_value("DEB_${cross_type}_ARCH"), + "debian/${package}.${filename}.".dpkg_architecture_value("DEB_${cross_type}_ARCH_OS"), + ); + } + push(@try, "debian/$package.$filename"); + if ($nameless_variant_handling or (not defined($nameless_variant_handling) and $package eq $dh{MAINPACKAGE})) { + my $nameless_variant = "debian/$filename"; + push(@try, $nameless_variant); + if (getpackages() > 1 and not $nameless_variant_handling and not compat(13) and -f $nameless_variant) { + warning('The use of prefix-less debhelper config files is deprecated.'); + warning("Please rename \"${nameless_variant}\" to \"debian/$dh{MAINPACKAGE}.${filename}\""); + error("Prefix-less debhelper config files is not supported in compat 15 and later") + if not compat(14); + warning('Prefix-less debhelper config files will trigger an error in compat 15 or later'); + } + } + } + foreach my $file (@try) { + return $file if -f $file; + } + + return ""; + } + + # Used by dh to ditch some caches that makes assumptions about + # dh_-tools can do, which does not hold for override targets. + sub dh_clear_unsafe_cache { + %_check_expensive = (); + } +} + +# Pass it a name of a binary package, it returns the name to prefix to files +# in debian/ for this package. +sub pkgext { + my ($package) = @_; + return "$package."; +} + +# Pass it the name of a binary package, it returns the name to install +# files by in eg, etc. Normally this is the same, but --name can override +# it. +sub pkgfilename { + my $package=shift; + + if (defined $dh{NAME}) { + return $dh{NAME}; + } + return $package; +} + +# Returns 1 if the package is a native debian package, null otherwise. +# As a side effect, sets $dh{VERSION} to the version of this package. +sub isnative { + my ($package) = @_; + my $cache_key = $package; + + state (%isnative_cache, %pkg_version); + + if (exists($isnative_cache{$cache_key})) { + $dh{VERSION} = $pkg_version{$cache_key}; + return $isnative_cache{$cache_key}; + } + + # Make sure we look at the correct changelog. + my $isnative_changelog = pkgfile($package, 'changelog', 0); + if (! $isnative_changelog) { + $isnative_changelog = "debian/changelog"; + $cache_key = '_source'; + # check if we looked up the default changelog + if (exists($isnative_cache{$cache_key})) { + $dh{VERSION} = $pkg_version{$cache_key}; + return $isnative_cache{$cache_key}; + } + } + + if (not %isnative_cache) { + require Dpkg::Changelog::Parse; + } + + my $res = Dpkg::Changelog::Parse::changelog_parse( + file => $isnative_changelog, + compression => 0, + ); + if (not defined($res)) { + error("No changelog entries for $package!? (changelog file: ${isnative_changelog})"); + } + my $version = $res->{'Version'}; + # Do we have a valid version? + if (not defined($version) or not $version->is_valid) { + error("changelog parse failure; invalid or missing version"); + } + # Get and cache the package version. + $dh{VERSION} = $pkg_version{$cache_key} = $version->as_string; + + # Is this a native Debian package? + if (index($dh{VERSION}, '-') > -1) { + return $isnative_cache{$cache_key} = 0; + } else { + return $isnative_cache{$cache_key} = 1; + } +} + +sub _tool_version { + return $DH_TOOL_VERSION if defined($DH_TOOL_VERSION); + if (defined($main::VERSION)) { + $DH_TOOL_VERSION = $main::VERSION; + } + if (defined($DH_TOOL_VERSION) and $DH_TOOL_VERSION eq DH_BUILTIN_VERSION) { + my $version = "UNRELEASED-${\MAX_COMPAT_LEVEL}"; + eval { + require Debian::Debhelper::Dh_Version; + $version = $Debian::Debhelper::Dh_Version::version; + }; + $DH_TOOL_VERSION = $version; + } else { + $DH_TOOL_VERSION //= 'UNDECLARED'; + } + return $DH_TOOL_VERSION; +} + +# Automatically add a shell script snippet to a debian script. +# Only works if the script has #DEBHELPER# in it. +# +# Parameters: +# 1: package +# 2: script to add to +# 3: filename of snippet +# 4: either text: shell-quoted sed to run on the snippet. Ie, 's/#PACKAGE#/$PACKAGE/' +# or a sub to run on each line of the snippet. Ie sub { s/#PACKAGE#/$PACKAGE/ } +# or a hashref with keys being variables and values being their replacement. Ie. { PACKAGE => $PACKAGE } +# 5: Internal usage only +sub autoscript { + my ($package, $script, $filename, $sed, $extra_options) = @_; + + my $tool_version = _tool_version(); + # This is the file we will modify. + my $outfile="debian/".pkgext($package)."$script.debhelper"; + if ($extra_options && exists($extra_options->{'snippet-order'})) { + my $order = $extra_options->{'snippet-order'}; + error("Internal error - snippet order set to unknown value: \"${order}\"") + if $order ne 'service'; + $outfile = generated_file($package, "${script}.${order}"); + } + + # Figure out what shell script snippet to use. + my $infile; + if (defined($ENV{DH_AUTOSCRIPTDIR}) && + -e "$ENV{DH_AUTOSCRIPTDIR}/$filename") { + $infile="$ENV{DH_AUTOSCRIPTDIR}/$filename"; + } + else { + for my $dir (@DATA_INC_PATH) { + my $path = "${dir}/autoscripts/${filename}"; + if (-e $path) { + $infile = $path; + last; + } + } + if (not defined($infile)) { + my @dirs = map { "$_/autoscripts" } @DATA_INC_PATH; + unshift(@dirs, $ENV{DH_AUTOSCRIPTDIR}) if exists($ENV{DH_AUTOSCRIPTDIR}); + error("Could not find autoscript $filename (search path: " . join(':', @dirs) . ')'); + } + } + + if (-e $outfile && ($script eq 'postrm' || $script eq 'prerm')) { + # Add fragments to top so they run in reverse order when removing. + if (not defined($sed) or ref($sed)) { + verbose_print("[META] Prepend autosnippet \"$filename\" to $script [${outfile}.new]"); + if (not $dh{NO_ACT}) { + open(my $out_fd, '>', "${outfile}.new") or error("open(${outfile}.new): $!"); + print {$out_fd} '# Automatically added by ' . $TOOL_NAME . "/${tool_version}\n"; + autoscript_sed($sed, $infile, undef, $out_fd); + print {$out_fd} "# End automatically added section\n"; + open(my $in_fd, '<', $outfile) or error("open($outfile): $!"); + while (my $line = <$in_fd>) { + print {$out_fd} $line; + } + close($in_fd); + close($out_fd) or error("close(${outfile}.new): $!"); + } + } else { + complex_doit("echo \"# Automatically added by ".$TOOL_NAME."/${tool_version}\"> $outfile.new"); + autoscript_sed($sed, $infile, "$outfile.new"); + complex_doit("echo '# End automatically added section' >> $outfile.new"); + complex_doit("cat $outfile >> $outfile.new"); + } + rename_path("${outfile}.new", $outfile); + } elsif (not defined($sed) or ref($sed)) { + verbose_print("[META] Append autosnippet \"$filename\" to $script [${outfile}]"); + if (not $dh{NO_ACT}) { + open(my $out_fd, '>>', $outfile) or error("open(${outfile}): $!"); + print {$out_fd} '# Automatically added by ' . $TOOL_NAME . "/${tool_version}\n"; + autoscript_sed($sed, $infile, undef, $out_fd); + print {$out_fd} "# End automatically added section\n"; + close($out_fd) or error("close(${outfile}): $!"); + } + } else { + complex_doit("echo \"# Automatically added by ".$TOOL_NAME."/${tool_version}\">> $outfile"); + autoscript_sed($sed, $infile, $outfile); + complex_doit("echo '# End automatically added section' >> $outfile"); + } +} + +sub autoscript_sed { + my ($sed, $infile, $outfile, $out_fd) = @_; + if (not defined($sed) or ref($sed)) { + my $out = $out_fd; + open(my $in, '<', $infile) or error("open $infile failed: $!"); + if (not defined($out_fd)) { + open($out, '>>', $outfile) or error("open($outfile): $!"); + } + if (not defined($sed) or ref($sed) eq 'CODE') { + while (<$in>) { $sed->() if $sed; print {$out} $_; } + } else { + my $rstr = sprintf('#(%s)#', join('|', reverse(sort(keys(%$sed))))); + my $regex = qr/$rstr/; + while (my $line = <$in>) { + $line =~ s/$regex/$sed->{$1}/eg; + print {$out} $line; + } + } + if (not defined($out_fd)) { + close($out) or error("close $outfile failed: $!"); + } + close($in) or error("close $infile failed: $!"); + } + else { + error("Internal error - passed open handle for legacy method") if defined($out_fd); + complex_doit("sed \"$sed\" $infile >> $outfile"); + } +} + +# Adds a trigger to the package +{ + my %VALID_TRIGGER_TYPES = map { $_ => 1 } qw( + interest interest-await interest-noawait + activate activate-await activate-noawait + ); + + sub autotrigger { + my ($package, $trigger_type, $trigger_target) = @_; + my ($triggers_file, $ifd, $tool_version); + + if (not exists($VALID_TRIGGER_TYPES{$trigger_type})) { + require Carp; + Carp::confess("Invalid/unknown trigger ${trigger_type}"); + } + return if $dh{NO_ACT}; + + $tool_version = _tool_version(); + $triggers_file = generated_file($package, 'triggers'); + if ( -f $triggers_file ) { + open($ifd, '<', $triggers_file) + or error("open $triggers_file failed $!"); + } else { + open($ifd, '<', '/dev/null') + or error("open /dev/null failed $!"); + } + open(my $ofd, '>', "${triggers_file}.new") + or error("open ${triggers_file}.new failed: $!"); + while (my $line = <$ifd>) { + next if $line =~ m{\A \Q${trigger_type}\E \s+ + \Q${trigger_target}\E (?:\s|\Z) + }x; + print {$ofd} $line; + } + print {$ofd} '# Triggers added by ' . $TOOL_NAME . "/${tool_version}\n"; + print {$ofd} "${trigger_type} ${trigger_target}\n"; + close($ofd) or error("closing ${triggers_file}.new failed: $!"); + close($ifd); + rename_path("${triggers_file}.new", $triggers_file); + } +} + +# Generated files are cleaned by dh_clean AND dh_prep +# - Package can be set to "_source" to generate a file relevant +# for the source package (the meson build does this atm.). +# Files for "_source" are only cleaned by dh_clean. +sub generated_file { + my ($package, $filename, $mkdirs) = @_; + my $dir = "debian/.debhelper/generated/${package}"; + my $path = "${dir}/${filename}"; + $mkdirs //= 1; + mkdirs($dir) if $mkdirs; + return $path; +} + +sub _update_substvar { + my ($substvar_file, $update_logic, $insert_logic) = @_; + my @lines; + my $changed = 0; + if ( -f $substvar_file) { + open(my $in, '<', $substvar_file) // error("open($substvar_file): $!"); + while (my $line = <$in>) { + chomp($line); + my $orig_value = $line; + my $updated_value = $update_logic->($line); + $changed ||= !defined($updated_value) || $orig_value ne $updated_value; + push(@lines, $updated_value) if defined($updated_value); + } + close($in); + } + my $len = scalar(@lines); + push(@lines, $insert_logic->()) if $insert_logic; + $changed ||= $len != scalar(@lines); + if ($changed && !$dh{NO_ACT}) { + open(my $out, '>', "${substvar_file}.new") // error("open(${substvar_file}.new, \"w\"): $!"); + for my $line (@lines) { + print {$out} "$line\n"; + } + close($out) // error("close(${substvar_file}.new): $!"); + rename_path("${substvar_file}.new", $substvar_file); + } + return; +} + +# Removes a whole substvar line. +sub delsubstvar { + my ($package, $substvar) = @_; + my $ext = pkgext($package); + my $substvarfile = "debian/${ext}substvars"; + + return _update_substvar($substvarfile, sub { + my ($line) = @_; + return $line if $line !~ m/^\Q${substvar}\E[?]?=/; + return; + }); +} + +# Adds a dependency on some package to the specified +# substvar in a package's substvar's file. +sub addsubstvar { + my ($package, $substvar, $deppackage, $verinfo, $remove) = @_; + my ($present); + my $ext = pkgext($package); + my $substvarfile = "debian/${ext}substvars"; + my $str = $deppackage; + $str .= " ($verinfo)" if defined $verinfo && length $verinfo; + + if (not defined($deppackage) and not $remove) { + error("Bug in helper: Must provide a value for addsubstvar (or set the remove flag, but then use delsubstvar instead)") + } + + if (defined($str) and $str =~ m/[\n]/) { + $str =~ s/\n/\\n/g; + # Per #1026014 + warning('Unescaped newlines in the value of a substvars can cause broken substvars files (see #1025714).'); + warning("Hint: If you really need a newline character, provide it as \"\${Newline}\"."); + error("Bug in helper: The substvar must not contain a raw newline character (${substvar}=${str})"); + } + + my $update_logic = sub { + my ($line) = @_; + return $line if $line !~ m/^\Q${substvar}\E([?]?=)(.*)/; + my $assignment_type = $1; + my %items = map { $_ => 1 } split(", ", $2); + $present = 1; + if ($remove) { + # Unchanged; we can avoid rewriting the file. + return $line if not exists($items{$str}); + delete($items{$str}); + my $replacement = join(", ", sort(keys(%items))); + return "${substvar}${assignment_type}${replacement}" if $replacement ne ''; + return; + } + # Unchanged; we can avoid rewriting the file. + return $line if %items and exists($items{$str}); + + $items{$str} = 1; + return "${substvar}${assignment_type}" . join(", ", sort(keys(%items))); + }; + my $insert_logic = sub { + return ("${substvar}=${str}") if not $present and not $remove; + return; + }; + return _update_substvar($substvarfile, $update_logic, $insert_logic); +} + +sub ensure_substvars_are_present { + my ($file, @substvars) = @_; + my (%vars, $fd); + return 1 if $dh{NO_ACT}; + if (open($fd, '+<', $file)) { + while (my $line = <$fd>) { + my $k; + ($k, undef) = split(m/=/, $line, 2); + $vars{$k} = 1 if $k; + } + # Fall-through and append the missing vars if any. + } else { + error("open(${file}) failed: $!") if $! != ENOENT; + open($fd, '>', $file) or error("open(${file}) failed: $!"); + } + + for my $var (@substvars) { + if (not exists($vars{$var})) { + verbose_print("echo ${var}= >> ${file}"); + print ${fd} "${var}=\n"; + $vars{$var} = 1; + } + } + close($fd) or error("close(${file}) failed: $!"); + return 1; +} + +sub _glob_expand_error_default_msg { + my ($pattern, $dir_ref) = @_; + my $dir_list = join(', ', map { escape_shell($_) } @{$dir_ref}); + return "Cannot find (any matches for) \"${pattern}\" (tried in $dir_list)"; +} + +sub glob_expand_error_handler_reject { + my $msg = _glob_expand_error_default_msg(@_); + error("$msg\n"); + return; +} + +sub glob_expand_error_handler_warn_and_discard { + my $msg = _glob_expand_error_default_msg(@_); + warning("$msg\n"); + return; +} + +# Emulates the "old" glob mechanism; not recommended for new code as +# it permits some globs expand to nothing with only a warning. +sub glob_expand_error_handler_reject_nomagic_warn_discard { + my ($pattern, $dir_ref) = @_; + for my $dir (@{$dir_ref}) { + my $full_pattern = "$dir/$pattern"; + my @matches = bsd_glob($full_pattern, GLOB_CSH & ~(GLOB_TILDE)); + if (@matches) { + goto \&glob_expand_error_handler_reject; + } + } + goto \&glob_expand_error_handler_warn_and_discard; +} + +sub glob_expand_error_handler_silently_ignore { + return; +} + +sub glob_expand { + my ($dir_ref, $error_handler, @patterns) = @_; + my @dirs = @{$dir_ref}; + my @result; + for my $pattern (@patterns) { + my @m; + for my $dir (@dirs) { + my $full_pattern = "$dir/$pattern"; + @m = bsd_glob($full_pattern, GLOB_CSH & ~(GLOB_NOMAGIC|GLOB_TILDE)); + last if @m; + # Handle "foo{bar}" pattern (#888251) + if (-l $full_pattern or -e _) { + push(@m, $full_pattern); + last; + } + } + if (not @m) { + $error_handler //= \&glob_expand_error_handler_reject; + $error_handler->($pattern, $dir_ref); + } + push(@result, @m); + } + return @result; +} + + +my %BUILT_IN_SUBST = ( + 'Space' => ' ', + 'Dollar' => '$', + 'Newline' => "\n", + 'Tab' => "\t", +); + +sub _variable_substitution { + my ($text, $loc) = @_; + return $text if index($text, '$') < 0; + my $pos = -1; + my $subst_count = 0; + my $expansion_count = 0; + my $current_size = length($text); + my $expansion_size_limit = _VAR_SUBST_EXPANSION_DYNAMIC_EXPANSION_FACTOR_LIMIT * $current_size; + $expansion_size_limit = _VAR_SUBST_EXPANSION_MIN_SUPPORTED_SIZE_LIMIT + if $expansion_size_limit < _VAR_SUBST_EXPANSION_MIN_SUPPORTED_SIZE_LIMIT; + 1 while ($text =~ s< + \$\{([A-Za-z0-9][-_:0-9A-Za-z]*)\} # Match ${something} and replace it + >[ + my $match = $1; + my $new_pos = pos()//-1; + my $value; + + if ($pos == $new_pos) { + # Safe-guard in case we ever implement recursive expansion + error("Error substituting in ${loc} (at position $pos); recursion limit while expanding \${${match}}") + if (++$subst_count >= _VAR_SUBST_SAME_POSITION_RECURSION_LIMIT); + } else { + $subst_count = 0; + $pos = $new_pos; + if (++$expansion_count >= _VAR_SUBST_EXPANSION_COUNT_LIMIT) { + error("Error substituting in ${loc}; substitution limit of ${expansion_count} reached"); + } + } + if (exists($BUILT_IN_SUBST{$match})) { + $value = $BUILT_IN_SUBST{$match}; + } elsif ($match =~ m/^DEB_(?:BUILD|HOST|TARGET)_/) { + $value = dpkg_architecture_value($match) // + error(qq{Cannot expand "\${${match}}" in ${loc} as it is not a known dpkg-architecture value}); + } elsif ($match =~ m/^env:(.+)/) { + my $env_var = $1; + $value = $ENV{$env_var} // + error(qq{Cannot expand "\${${match}}" in ${loc} as the ENV variable "${env_var}" is unset}); + } + error(qq{Cannot resolve variable "\${$match}" in ${loc}}) + if not defined($value); + # We do not support recursive expansion. + $value =~ s/\$/\$\{\}/; + $current_size += length($value) - length($match) - 3; + if ($current_size > $expansion_size_limit) { + error("Refusing to expand \${${match}} in ${loc} - the original input seems to grow beyond reasonable' + . ' limits!"); + } + $value; + ]gex); + $text =~ s/\$\{\}/\$/g; + + return $text; +} + +# Reads in the specified file, one line at a time. splits on words, +# and returns an array of arrays of the contents. +# If a value is passed in as the second parameter, then glob +# expansion is done in the directory specified by the parameter ("." is +# frequently a good choice). +# In compat 13+, it will do variable expansion (after splitting the lines +# into words) +sub filedoublearray { + my ($file, $globdir, $error_handler) = @_; + + # executable config files are a v9 thing. + my $x=! compat(8) && -x $file; + my $expand_patterns = compat(12) ? 0 : 1; + my $source; + if ($x) { + require Cwd; + my $cmd=Cwd::abs_path($file); + $ENV{"DH_CONFIG_ACT_ON_PACKAGES"} = join(",", @{$dh{"DOPACKAGES"}}); + open(DH_FARRAY_IN, '-|', $cmd) || error("cannot run $file: $!"); + delete $ENV{"DH_CONFIG_ACT_ON_PACKAGES"}; + $source = "output of ./${file}"; + } + else { + open (DH_FARRAY_IN, '<', $file) || error("cannot read $file: $!"); + $source = $file; + } + + my @ret; + while (<DH_FARRAY_IN>) { + chomp; + if ($x) { + if (m/^\s++$/) { + error("Executable config file $file produced a non-empty whitespace-only line"); + } + } else { + s/^\s++//; + next if /^#/; + s/\s++$//; + } + # We always ignore/permit empty lines + next if $_ eq ''; + my @line; + my $source_ref = "${source} (line $.)"; + + if (defined($globdir) && ! $x) { + if (ref($globdir)) { + my @patterns = split; + if ($expand_patterns) { + @patterns = map {_variable_substitution($_, $source_ref)} @patterns; + } + push(@line, glob_expand($globdir, $error_handler, @patterns)); + } else { + # Legacy call - Silently discards globs that match nothing. + # + # The tricky bit is that the glob expansion is done + # as if we were in the specified directory, so the + # filenames that come out are relative to it. + foreach (map { glob "$globdir/$_" } split) { + s#^$globdir/##; + if ($expand_patterns) { + $_ = _variable_substitution($_, $source_ref); + } + push @line, $_; + } + } + } + else { + @line = split; + if ($expand_patterns) { + @line = map {_variable_substitution($_, $source_ref)} @line; + } + } + push @ret, [@line]; + } + + if (!close(DH_FARRAY_IN)) { + if ($x) { + _executable_dh_config_file_failed($file, $!, $?); + } else { + error("problem reading $file: $!"); + } + } + + return @ret; +} + +# Reads in the specified file, one word at a time, and returns an array of +# the result. Can do globbing as does filedoublearray. +sub filearray { + return map { @$_ } filedoublearray(@_); +} + +# Passed a filename, returns true if -X says that file should be excluded. +sub excludefile { + my $filename = shift; + foreach my $f (@{$dh{EXCLUDE}}) { + return 1 if $filename =~ /\Q$f\E/; + } + return 0; +} + +sub dpkg_architecture_value { + my $var = shift; + state %dpkg_arch_output; + if (exists($ENV{$var})) { + my $value = $ENV{$var}; + return $value if $value ne q{}; + warning("ENV[$var] is set to the empty string. It has been ignored to avoid bugs like #862842"); + delete($ENV{$var}); + } + if (! exists($dpkg_arch_output{$var})) { + # Return here if we already consulted dpkg-architecture + # (saves a fork+exec on unknown variables) + return if %dpkg_arch_output; + + open(my $fd, '-|', 'dpkg-architecture') + or error("dpkg-architecture failed"); + while (my $line = <$fd>) { + chomp($line); + my ($k, $v) = split(/=/, $line, 2); + $dpkg_arch_output{$k} = $v; + } + close($fd); + } + return $dpkg_arch_output{$var}; +} + +# Confusing name for hostarch +sub buildarch { + deprecated_functionality('buildarch() is deprecated and replaced by hostarch()', 12); + goto \&hostarch; +} + +# Returns the architecture that will run binaries produced (DEB_HOST_ARCH) +sub hostarch { + dpkg_architecture_value('DEB_HOST_ARCH'); +} + +# Returns a truth value if this seems to be a cross-compile +sub is_cross_compiling { + return dpkg_architecture_value("DEB_BUILD_GNU_TYPE") + ne dpkg_architecture_value("DEB_HOST_GNU_TYPE"); +} + +# Passed an arch and a space-separated list of arches to match against, returns true if matched +sub samearch { + my $arch=shift; + my @archlist=split(/\s+/,shift); + state %knownsame; + + foreach my $a (@archlist) { + if (exists $knownsame{$arch}{$a}) { + return 1 if $knownsame{$arch}{$a}; + next; + } + + require Dpkg::Arch; + if (Dpkg::Arch::debarch_is($arch, $a)) { + return $knownsame{$arch}{$a}=1; + } + else { + $knownsame{$arch}{$a}=0; + } + } + + return 0; +} + + +# Returns a list of packages in the control file. +# Pass "arch" or "indep" to specify arch-dependent (that will be built +# for the system's arch) or independent. If nothing is specified, +# returns all packages. Also, "both" returns the union of "arch" and "indep" +# packages. +# +# As a side effect, populates %package_arches and %package_types +# with the types of all packages (not only those returned). +my (%packages_by_type, $sourcepackage, %dh_bd_sequences, %package_fields); + +# Resets the arrays; used mostly for testing +sub resetpackages { + undef $sourcepackage; + %package_fields = %packages_by_type = (); + %dh_bd_sequences = (); +} + +# Returns source package name +sub sourcepackage { + getpackages() if not defined($sourcepackage); + return $sourcepackage; +} + +sub getpackages { + my ($type) = @_; + error("getpackages: First argument must be one of \"arch\", \"indep\", or \"both\"") + if defined($type) and $type ne 'both' and $type ne 'indep' and $type ne 'arch'; + + $type //= 'all-listed-in-control-file'; + + if (not %packages_by_type) { + _parse_debian_control(); + } + return @{$packages_by_type{$type}}; +} + +sub _strip_spaces { + my ($v) = @_; + return if not defined($v); + $v =~ s/^\s++//; + $v =~ s/\s++$//; + return $v; +} + +sub _parse_debian_control { + my $valid_pkg_re = qr{^${PKGNAME_REGEX}$}o; + my (%seen, @profiles, $source_section, $cross_target_arch, %field_values, + $field_name, %bd_fields, $bd_field_value, %seen_fields, $fd); + if (exists $ENV{'DEB_BUILD_PROFILES'}) { + @profiles=split /\s+/, $ENV{'DEB_BUILD_PROFILES'}; + } + if (not open($fd, '<', 'debian/control')) { + error("\"debian/control\" not found. Are you sure you are in the correct directory?") + if $! == ENOENT; + error("cannot read debian/control: $!\n"); + }; + + $packages_by_type{$_} = [] for qw(both indep arch all-listed-in-control-file); + while (<$fd>) { + chomp; + s/\s+$//; + next if m/^\s*+\#/; + + if (/^\s/) { + if (not %seen_fields) { + error("Continuation line seen before first stanza in debian/control (line $.)"); + } + # Continuation line + s/^\s[.]?//; + push(@{$bd_field_value}, $_) if $bd_field_value; + error('X-DH-Compat should not need to span multiple lines') + if ($field_name and $field_name eq 'x-dh-compat'); + + # Ensure it is not completely empty or the code below will assume the paragraph ended + $_ = '.' if not $_; + } elsif (not $_ and not %seen_fields) { + # Ignore empty lines before first stanza + next; + } elsif ($_) { + my ($value); + + if (m/^($DEB822_FIELD_REGEX):\s*(.*)/o) { + ($field_name, $value) = (lc($1), $2); + if (exists($seen_fields{$field_name})) { + my $first_time = $seen_fields{$field_name}; + error("${field_name}-field appears twice in the same stanza of debian/control. " . + "First time on line $first_time, second time: $."); + } + $seen_fields{$field_name} = $.; + $bd_field_value = undef; + } else { + # Invalid file + error("Parse error in debian/control, line $., read: $_"); + } + if ($field_name eq 'source') { + $sourcepackage = $value; + if ($sourcepackage !~ $valid_pkg_re) { + error('Source-field must be a valid package name, ' . + "got: \"${sourcepackage}\", should match \"${valid_pkg_re}\""); + } + next; + } elsif ($field_name eq 'section') { + $source_section = $value; + next; + } elsif ($field_name =~ /^(?:build-depends(?:-arch|-indep)?)$/) { + $bd_field_value = [$value]; + $bd_fields{$field_name} = $bd_field_value; + } elsif ($field_name eq 'x-dh-compat') { + error('The X-DH-Compat field must contain a single integer') if ($value !~ m/^\d+$/); + $compat_from_dctrl = int($value); + } + } + last if not $_ or eof; + } + error("could not find Source: line in control file.") if not defined($sourcepackage); + $compat_from_dctrl //= -1; + if (%bd_fields) { + my ($dh_compat_bd, $final_level); + my %field2addon_type = ( + 'build-depends' => 'both', + 'build-depends-arch' => 'arch', + 'build-depends-indep' => 'indep', + ); + for my $field (sort(keys(%bd_fields))) { + my $value = join(' ', @{$bd_fields{$field}}); + $value =~ s/^\s*//; + $value =~ s/\s*(?:,\s*)?$//; + for my $dep (split(/\s*,\s*/, $value)) { + if ($dep =~ m/^debhelper-compat\s*[(]\s*=\s*(${PKGVERSION_REGEX})\s*[)]$/) { + my $version = $1; + if ($version =~m/^(\d+)\D.*$/) { + my $guessed_compat = $1; + warning("Please use the compat level as the exact version rather than the full version."); + warning(" Perhaps you meant: debhelper-compat (= ${guessed_compat})"); + if ($field ne 'build-depends') { + warning(" * Also, please move the declaration to Build-Depends (it was found in ${field})"); + } + error("Invalid compat level ${version}, derived from relation: ${dep}"); + } + $final_level = $version; + error("Duplicate debhelper-compat build-dependency: ${dh_compat_bd} vs. ${dep}") if $dh_compat_bd; + error("The debhelper-compat build-dependency must be in the Build-Depends field (not $field)") + if $field ne 'build-depends'; + $dh_compat_bd = $dep; + } elsif ($dep =~ m/^debhelper-compat\s*(?:\S.*)?$/) { + my $clevel = "${\MAX_COMPAT_LEVEL}"; + eval { + require Debian::Debhelper::Dh_Version; + $clevel = $Debian::Debhelper::Dh_Version::version; + }; + $clevel =~ s/^\d+\K\D.*$//; + warning("Found invalid debhelper-compat relation: ${dep}"); + warning(" * Please format the relation as (example): debhelper-compat (= ${clevel})"); + warning(" * Note that alternatives, architecture restrictions, build-profiles etc. are not supported."); + if ($field ne 'build-depends') { + warning(" * Also, please move the declaration to Build-Depends (it was found in ${field})"); + } + warning(" * If this is not possible, then please remove the debhelper-compat relation and insert the"); + warning(" compat level into the file debian/compat. (E.g. \"echo ${clevel} > debian/compat\")"); + error("Could not parse desired debhelper compat level from relation: $dep"); + } + # Build-Depends on dh-sequence-<foo> OR dh-sequence-<foo> (<op> <version>) + if ($PARSE_DH_SEQUENCE_INFO and $dep =~ m/^dh-sequence-(${PKGNAME_REGEX})\s*(?:[(]\s*(?:[<>]?=|<<|>>)\s*(?:${PKGVERSION_REGEX})\s*[)])?(\s*[^\|]+[]>]\s*)?$/) { + my $sequence = $1; + my $has_profile_or_arch_restriction = $2 ? 1 : 0; + my $addon_type = $field2addon_type{$field}; + if (not defined($field)) { + warning("Cannot map ${field} to an add-on type (like \"both\", \"indep\" or \"arch\")"); + error("Internal error: Cannot satisfy dh sequence add-on request for sequence ${sequence} via ${field}."); + } + if (defined($dh_bd_sequences{$sequence})) { + error("Saw $dep multiple times (last time in $field). However dh only support that build-" + . 'dependency at most once across all Build-Depends(-Arch|-Indep) fields'); + } + if ($has_profile_or_arch_restriction) { + require Dpkg::Deps; + my $dpkg_dep = Dpkg::Deps::deps_parse($dep, build_profiles => \@profiles, build_dep => 1, + reduce_restrictions => 1); + # If dpkg reduces it to nothing, then it was not relevant for us after all + next if not $dpkg_dep; + } + $dh_bd_sequences{$sequence} = $addon_type; + } + } + } + $compat_from_bd = $final_level // -1; + } else { + $compat_from_bd = -1; + } + + error( + 'The X-DH-Compat field cannot be used together with a Build-Dependency on debhelper-compat.' + . ' Please remove one of the two.' + ) if ($compat_from_bd > -1 and $compat_from_dctrl > -1); + + + %seen_fields = (); + $field_name = undef; + + while (<$fd>) { + chomp; + s/\s+$//; + if (m/^\#/) { + # Skip unless EOF for the special case where the last line + # is a comment line directly after the last stanza. In + # that case we need to "commit" the last stanza as well or + # we end up omitting the last package. + next if not eof; + $_ = ''; + } + + if (/^\s/) { + # Continuation line + if (not %seen_fields) { + error("Continuation line seen outside stanza in debian/control (line $.)"); + } + s/^\s[.]?//; + $field_values{$field_name} .= ' ' . $_; + # Ensure it is not completely empty or the code below will assume the paragraph ended + $_ = '.' if not $_; + } elsif (not $_ and not %seen_fields) { + # Ignore empty lines before first stanza + next; + } elsif ($_) { + my ($value); + if (m/^($DEB822_FIELD_REGEX):\s*(.*)/o) { + ($field_name, $value) = (lc($1), $2); + if (exists($seen_fields{$field_name})) { + my $first_time = $seen_fields{$field_name}; + error("${field_name}-field appears twice in the same stanza of debian/control. " . + "First time on line $first_time, second time: $."); + } + + if ($field_name =~ m/^(?:x[bc]*-)?package-type$/) { + # Normalize variants into the main "Package-Type" field + $field_name = 'package-type'; + if (exists($seen_fields{$field_name})) { + my $package = _strip_spaces($field_values{'package'} // ''); + my $help = "(issue seen prior \"Package\"-field)"; + $help = "for package ${package}" if $package; + error("Multiple definitions of (X-)Package-Type in line $. ${help}"); + } + } + $seen_fields{$field_name} = $.; + $field_values{$field_name} = $value; + $bd_field_value = undef; + } else { + # Invalid file + error("Parse error in debian/control, line $., read: $_"); + } + } + if (!$_ or eof) { # end of stanza. + if (%field_values) { + my $package = _strip_spaces($field_values{'package'} // ''); + my $build_profiles = $field_values{'build-profiles'}; + my $included_in_build_profile = 1; + my $arch = _strip_spaces($field_values{'architecture'} // ''); + my $cross_type = _strip_spaces($field_values{'x-dh-build-for-type'} // 'host'); + + # Detect duplicate package names in the same control file. + if ($package eq '') { + error("Binary paragraph ending on line $. is missing mandatory \"Package\"-field"); + } + if (! $seen{$package}) { + $seen{$package}=1; + } else { + error("debian/control has a duplicate entry for $package"); + } + if ($package !~ $valid_pkg_re) { + error('Package-field must be a valid package name, ' . + "got: \"${package}\", should match \"${valid_pkg_re}\""); + } + if ($cross_type ne 'host' and $cross_type ne 'target') { + error("Unknown value of X-DH-Build-For-Type \"$cross_type\" for package $package"); + } + + $field_values{'package-type'} = _strip_spaces($field_values{'package-type'} // 'deb'); + $field_values{'architecture'} = $arch; + $field_values{'multi-arch'} = _strip_spaces($field_values{'multi-arch'} // ''); + $field_values{'section'} = _strip_spaces($field_values{'section'} // $source_section); + $field_values{'x-dh-build-for-type'} = $cross_type; + $field_values{'x-time64-compat'} = _strip_spaces($field_values{'x-time64-compat'} // ''); + my %fields = %field_values; + $package_fields{$package} = \%fields; + push(@{$packages_by_type{'all-listed-in-control-file'}}, $package); + + if (defined($build_profiles)) { + eval { + # rely on libdpkg-perl providing the parsing functions + # because if we work on a package with a Build-Profiles + # field, then a high enough version of dpkg-dev is needed + # anyways + require Dpkg::BuildProfiles; + my @restrictions = Dpkg::BuildProfiles::parse_build_profiles($build_profiles); + if (@restrictions) { + $included_in_build_profile = Dpkg::BuildProfiles::evaluate_restriction_formula( + \@restrictions, + \@profiles); + } + }; + if ($@) { + error("The control file has a Build-Profiles field. Requires libdpkg-perl >= 1.17.14"); + } + } + + if ($included_in_build_profile) { + if ($arch eq 'all') { + push(@{$packages_by_type{'indep'}}, $package); + push(@{$packages_by_type{'both'}}, $package); + } else { + my $included = 0; + $included = 1 if $arch eq 'any'; + if (not $included) { + my $desired_arch = hostarch(); + if ($cross_type eq 'target') { + $cross_target_arch //= dpkg_architecture_value('DEB_TARGET_ARCH'); + $desired_arch = $cross_target_arch; + } + $included = 1 if samearch($desired_arch, $arch); + } + if ($included) { + push(@{$packages_by_type{'arch'}}, $package); + push(@{$packages_by_type{'both'}}, $package); + } + } + } + } + %field_values = (); + %seen_fields = (); + } + } + close($fd); +} + +# Return true if we should use root. +# - Takes an optional keyword; if passed, this will return true if the keyword is listed in R^3 (Rules-Requires-Root) +# - If the optional keyword is omitted or not present in R^3 and R^3 is not 'binary-targets', then returns false +# - Returns true otherwise (i.e. keyword is in R^3 or R^3 is 'binary-targets') +sub should_use_root { + my ($keyword) = @_; + my $rrr_env = $ENV{'DEB_RULES_REQUIRES_ROOT'} // 'binary-targets'; + $rrr_env =~ s/^\s++//; + $rrr_env =~ s/\s++$//; + return 0 if $rrr_env eq 'no'; + return 1 if $rrr_env eq 'binary-targets'; + return 0 if not defined($keyword); + + state %rrr = map { $_ => 1 } split(' ', $rrr_env); + return 1 if exists($rrr{$keyword}); + return 0; +} + +# Returns the "gain root command" as a list suitable for passing as a part of the command to "doit()" +sub gain_root_cmd { + my $raw_cmd = $ENV{DEB_GAIN_ROOT_CMD}; + return if not defined($raw_cmd) or $raw_cmd =~ m/^\s*+$/; + return split(' ', $raw_cmd); +} + +sub root_requirements { + my $rrr_env = $ENV{'DEB_RULES_REQUIRES_ROOT'} // 'binary-targets'; + $rrr_env =~ s/^\s++//; + $rrr_env =~ s/\s++$//; + return 'none' if $rrr_env eq 'no'; + return 'legacy-root' if $rrr_env eq 'binary-targets'; + return 'targeted-promotion'; +} + +# Returns the arch a package will build for. +# +# Deprecated: please switch to the more descriptive +# package_binary_arch function instead. +sub package_arch { + my $package=shift; + return package_binary_arch($package); +} + +# Returns the architecture going into the resulting .deb, i.e. the +# host architecture or "all". +sub package_binary_arch { + my $package=shift; + + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + return hostarch(); + } + return 'all' if $package_fields{$package}{'architecture'} eq 'all'; + return dpkg_architecture_value('DEB_TARGET_ARCH') if package_cross_type($package) eq 'target'; + return hostarch(); +} + +# Returns the Architecture: value which the package declared. +sub package_declared_arch { + my $package=shift; + + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + return hostarch(); + } + return $package_fields{$package}{'architecture'}; +} + +# Returns whether the package specified Architecture: all +sub package_is_arch_all { + my $package=shift; + + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + return hostarch(); + } + return $package_fields{$package}{'architecture'} eq 'all'; +} + +# Returns the multiarch value of a package. +sub package_multiarch { + my $package=shift; + + # Test the architecture field instead, as it is common for a + # package to not have a multi-arch value. + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + # The only sane default + return 'no'; + } + return $package_fields{$package}{'multi-arch'} // 'no'; +} + +sub package_is_essential { + my ($package) = @_; + + # Test the architecture field instead, as it is common for a + # package to not have a multi-arch value. + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + # The only sane default + return 0; + } + my $essential = $package_fields{$package}{'essential'} // 'no'; + return $essential eq 'yes'; +} + +sub package_field { + my ($package, $field, $default_value) = @_; + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + return $default_value; + } + return $package_fields{$package}{$field} if exists($package_fields{$package}{$field}); + return $default_value; +} + + +# Returns the (raw) section value of a package (possibly including component). +sub package_section { + my ($package) = @_; + + # Test the architecture field instead, as it is common for a + # package to not have a multi-arch value. + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + return 'unknown'; + } + return $package_fields{$package}{'section'} // 'unknown'; +} + +sub package_cross_type { + my ($package) = @_; + + # Test the architecture field instead, as it is common for a + # package to not have a multi-arch value. + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + return 'host'; + } + return $package_fields{$package}{'x-dh-build-for-type'} // 'host'; +} + +sub package_type { + my ($package) = @_; + + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + return DEFAULT_PACKAGE_TYPE; + } + return $package_fields{$package}{'package-type'}; +} + +sub t64_compat_name { + my ($package) = @_; + + if (! exists($package_fields{$package})) { + warning "package $package is not in control info"; + return ''; + } + return $package_fields{$package}{'x-time64-compat'}; +} + +# Return true if a given package is really a udeb. +sub is_udeb { + my $package=shift; + + return package_type($package) eq 'udeb'; +} + + +sub process_pkg { + my ($package) = @_; + state %packages_to_process = map { $_ => 1 } @{$dh{DOPACKAGES}}; + return $packages_to_process{$package} // 0; +} + +# Only useful for dh(1) +sub bd_dh_sequences { + # Use $sourcepackage as check because %dh_bd_sequence can be empty + # after running getpackages(). + getpackages() if not defined($sourcepackage); + return \%dh_bd_sequences; +} + +sub _concat_slurp_script_files { + my (@files) = @_; + my $res = ''; + for my $file (@files) { + open(my $fd, '<', $file) or error("open($file) failed: $!"); + my $f = join('', <$fd>); + close($fd); + $res .= $f; + } + return $res; +} + +sub _substitution_generator { + my ($input) = @_; + my $cache = {}; + return sub { + my ($orig_key) = @_; + return $cache->{$orig_key} if exists($cache->{$orig_key}); + my $value = exists($input->{$orig_key}) ? $input->{$orig_key} : undef; + if (not defined($value)) { + if ($orig_key =~ m/^DEB_(?:BUILD|HOST|TARGET)_/) { + $value = dpkg_architecture_value($orig_key); + } elsif ($orig_key =~ m{^ENV[.](\S+)$}) { + $value = $ENV{$1} // ''; + } + } elsif (ref($value) eq 'CODE') { + $value = $value->($orig_key); + } elsif ($value =~ s/^@//) { + $value = _concat_slurp_script_files($value); + } + $cache->{$orig_key} = $value; + return $value; + }; +} + +sub debhelper_script_per_package_subst { + my ($package, $provided_subst) = @_; + my %vars = %{$provided_subst}; + $vars{'PACKAGE'} = $package if not exists($vars{'PACKAGE'}); + for my $var (keys(%{$provided_subst})) { + if ($var !~ $Debian::Debhelper::Dh_Lib::MAINTSCRIPT_TOKEN_REGEX) { + warning("User defined token ${var} does not match ${Debian::Debhelper::Dh_Lib::MAINTSCRIPT_TOKEN_REGEX}"); + error("Invalid provided token ${var}: It cannot be substituted as it does not follow the token name rules"); + } + if ($var =~ m/^pkg[.]\Q${package}\E[.](.+)$/) { + my $new_key = $1; + $vars{$new_key} = $provided_subst->{$var}; + } + } + return \%vars; +} + + +# Handles #DEBHELPER# substitution in a script; also can generate a new +# script from scratch if none exists but there is a .debhelper file for it. +sub debhelper_script_subst { + my ($package, $script, $extra_vars) = @_; + + my $tmp=tmpdir($package); + my $ext=pkgext($package); + my $file=pkgfile($package,$script); + my %variables = defined($extra_vars) ? %{$extra_vars} : (); + my $service_script = generated_file($package, "${script}.service", 0); + my @generated_scripts = ("debian/$ext$script.debhelper", $service_script); + my $subst; + @generated_scripts = grep { -f } @generated_scripts; + if ($script eq 'prerm' or $script eq 'postrm') { + @generated_scripts = reverse(@generated_scripts); + } + if (not exists($variables{'DEBHELPER'})) { + $variables{'DEBHELPER'} = sub { + return _concat_slurp_script_files(@generated_scripts); + }; + } + $subst = _substitution_generator(\%variables); + + if ($file ne '') { + if ($dh{VERBOSE}) { + verbose_print('cp -f ' . escape_shell($file) . " $tmp/DEBIAN/$script"); + verbose_print("[META] Replace #TOKEN#s in \"$tmp/DEBIAN/$script\""); + } + if (not $dh{NO_ACT}) { + my $regex = qr{#(${MAINTSCRIPT_TOKEN_REGEX})#}o; + open(my $out_fd, '>', "$tmp/DEBIAN/$script") or error("open($tmp/DEBIAN/$script) failed: $!"); + open(my $in_fd, '<', $file) or error("open($file) failed: $!"); + while (my $line = <$in_fd>) { + $line =~ s{$regex}{$subst->($1) // "#${1}#"}ge; + print {$out_fd} $line; + } + close($in_fd); + close($out_fd) or error("close($tmp/DEBIAN/$script) failed: $!"); + } + reset_perm_and_owner(0755, "$tmp/DEBIAN/$script"); + } + elsif (@generated_scripts) { + if ($dh{VERBOSE}) { + verbose_print(q{printf '#!/bin/sh\nset -e\n' > } . "$tmp/DEBIAN/$script"); + verbose_print("cat @generated_scripts >> $tmp/DEBIAN/$script"); + } + if (not $dh{NO_ACT}) { + open(my $out_fd, '>', "$tmp/DEBIAN/$script") or error("open($tmp/DEBIAN/$script): $!"); + print {$out_fd} "#!/bin/sh\n"; + print {$out_fd} "set -e\n"; + for my $generated_script (@generated_scripts) { + open(my $in_fd, '<', $generated_script) + or error("open($generated_script) failed: $!"); + while (my $line = <$in_fd>) { + print {$out_fd} $line; + } + close($in_fd); + } + close($out_fd) or error("close($tmp/DEBIAN/$script) failed: $!"); + } + reset_perm_and_owner(0755, "$tmp/DEBIAN/$script"); + } +} + +sub rm_files { + my @files = @_; + verbose_print('rm -f ' . escape_shell(@files)) + if $dh{VERBOSE}; + return 1 if $dh{NO_ACT}; + for my $file (@files) { + if (not unlink($file) and $! != ENOENT) { + error("unlink $file failed: $!"); + } + } + return 1; +} + +sub make_symlink_raw_target { + my ($src, $dest) = @_; + verbose_print('ln -s ' . escape_shell($src, $dest)) + if $dh{VERBOSE}; + return 1 if $dh{NO_ACT}; + if (not symlink($src, $dest)) { + error("symlink($src, $dest) failed: $!"); + } + return 1; +} + +# make_symlink($dest, $src[, $tmp]) creates a symlink from $dest -> $src. +# if $tmp is given, $dest will be created within it. +# Usually $tmp should be the value of tmpdir($package); +sub make_symlink{ + my $dest = shift; + my $src = _expand_path(shift); + my $tmp = shift; + $tmp = '' if not defined($tmp); + + if ($dest =~ m{(?:^|/)*[.]{2}(?:/|$)}) { + error("Invalid destination/link name (contains \"..\"-segments): $dest"); + } + + $src =~ s{^(?:[.]/+)++}{}; + $dest =~ s{^(?:[.]/+)++}{}; + + $src=~s:^/++::; + $dest=~s:^/++::; + + if ($src eq $dest) { + warning("skipping link from $src to self"); + return; + } + + + + # Policy says that if the link is all within one toplevel + # directory, it should be relative. If it's between + # top level directories, leave it absolute. + my @src_dirs = grep { $_ ne '.' } split(m:/+:,$src); + my @dest_dirs = grep { $_ ne '.' } split(m:/+:,$dest); + if (@src_dirs > 0 && $src_dirs[0] eq $dest_dirs[0]) { + # Figure out how much of a path $src and $dest + # share in common. + my $x; + for ($x=0; $x < @src_dirs && $src_dirs[$x] eq $dest_dirs[$x]; $x++) {} + # Build up the new src. + $src=""; + for (1..$#dest_dirs - $x) { + $src.="../"; + } + for ($x .. $#src_dirs) { + $src.=$src_dirs[$_]."/"; + } + if ($x > $#src_dirs && ! length $src) { + $src="."; # special case + } + $src=~s:/$::; + } + else { + # Make sure it's properly absolute. + $src="/$src"; + } + + my $full_dest = "$tmp/$dest"; + if ( -l $full_dest ) { + # All ok - we can always replace a link, and target directory must exists + } elsif (-d _) { + # We cannot replace a directory though + error("link destination $full_dest is a directory"); + } else { + # Make sure the directory the link will be in exists. + my $basedir=dirname($full_dest); + install_dir($basedir); + } + rm_files($full_dest); + make_symlink_raw_target($src, $full_dest); +} + +# _expand_path expands all path "." and ".." components, but doesn't +# resolve symbolic links. +sub _expand_path { + my $start = @_ ? shift : '.'; + my @pathname = split(m:/+:,$start); + my @respath; + for my $entry (@pathname) { + if ($entry eq '.' || $entry eq '') { + # Do nothing + } + elsif ($entry eq '..') { + if ($#respath == -1) { + # Do nothing + } + else { + pop @respath; + } + } + else { + push @respath, $entry; + } + } + + my $result; + for my $entry (@respath) { + $result .= '/' . $entry; + } + if (! defined $result) { + $result="/"; # special case + } + return $result; +} + +# Checks if make's jobserver is enabled via MAKEFLAGS, but +# the FD used to communicate with it is actually not available. +sub is_make_jobserver_unavailable { + if (exists $ENV{MAKEFLAGS} && + $ENV{MAKEFLAGS} =~ /(?:^|\s)--jobserver-(?:fds|auth)=(\d+)/) { + if (!open(my $in, "<&$1")) { + return 1; # unavailable + } + else { + close $in; + return 0; # available + } + } + + return; # no jobserver specified +} + +# Cleans out jobserver options from MAKEFLAGS. +sub clean_jobserver_makeflags { + if (exists $ENV{MAKEFLAGS}) { + if ($ENV{MAKEFLAGS} =~ /(?:^|\s)--jobserver-(?:fds|auth)=\d+/) { + $ENV{MAKEFLAGS} =~ s/(?:^|\s)--jobserver-(?:fds|auth)=\S+//g; + $ENV{MAKEFLAGS} =~ s/(?:^|\s)-j\b//g; + } + delete $ENV{MAKEFLAGS} if $ENV{MAKEFLAGS} =~ /^\s*$/; + } +} + +# If cross-compiling, returns appropriate cross version of command. +sub cross_command { + my ($package, $command) = @_; + if (package_cross_type($package) eq 'target') { + if (dpkg_architecture_value("DEB_HOST_GNU_TYPE") ne dpkg_architecture_value("DEB_TARGET_GNU_TYPE")) { + return dpkg_architecture_value("DEB_TARGET_GNU_TYPE") . "-$command"; + } + } + if (is_cross_compiling()) { + return dpkg_architecture_value("DEB_HOST_GNU_TYPE")."-$command"; + } + else { + return $command; + } +} + +# Returns the SOURCE_DATE_EPOCH ENV variable if set OR computes it +# from the latest changelog entry, sets the SOURCE_DATE_EPOCH ENV +# variable and returns the computed value. +sub get_source_date_epoch { + return $ENV{SOURCE_DATE_EPOCH} if exists($ENV{SOURCE_DATE_EPOCH}); + _parse_non_binnmu_date_epoch(); + return $ENV{SOURCE_DATE_EPOCH}; +} + +{ + my $_non_binnmu_date_epoch; + + # Needed for dh_strip_nondeterminism - not exported by default because it is not likely + # to be useful beyond that one helper. + sub get_non_binnmu_date_epoch { + return $_non_binnmu_date_epoch if defined($_non_binnmu_date_epoch); + _parse_non_binnmu_date_epoch(); + return $_non_binnmu_date_epoch; + } + + sub _parse_non_binnmu_date_epoch { + eval { require Dpkg::Changelog::Debian }; + if ($@) { + warning "unable to set SOURCE_DATE_EPOCH: $@"; + return; + } + eval { require Time::Piece }; + if ($@) { + warning "unable to set SOURCE_DATE_EPOCH: $@"; + return; + } + + my $changelog = Dpkg::Changelog::Debian->new(range => {"count" => 2}); + $changelog->load("debian/changelog"); + + my $first_entry = $changelog->[0]; + my $non_binnmu_entry = $first_entry; + my $optional_fields = $first_entry->get_optional_fields(); + my $first_tt = $first_entry->get_timestamp(); + $first_tt =~ s/\s*\([^\)]+\)\s*$//; # Remove the optional timezone codename + my $first_timestamp = Time::Piece->strptime($first_tt, "%a, %d %b %Y %T %z")->epoch; + my $non_binnmu_timestamp = $first_timestamp; + if (exists($optional_fields->{'Binary-Only'}) and lc($optional_fields->{'Binary-Only'}) eq 'yes') { + $non_binnmu_entry = $changelog->[1]; + my $non_binnmu_options = $non_binnmu_entry->get_optional_fields(); + if (exists($non_binnmu_options->{'Binary-Only'}) and lc($non_binnmu_options->{'Binary-Only'}) eq 'yes') { + error("internal error: Could not locate the first non-binnmu entry in the change (assumed it would be the second entry)"); + } + my $non_binnmu_tt = $non_binnmu_entry->get_timestamp(); + $non_binnmu_tt =~ s/\s*\([^\)]+\)\s*$//; # Remove the optional timezone codename + $non_binnmu_timestamp = Time::Piece->strptime($non_binnmu_tt, "%a, %d %b %Y %T %z")->epoch(); + } + + $ENV{SOURCE_DATE_EPOCH} = $first_timestamp if not exists($ENV{SOURCE_DATE_EPOCH}); + $_non_binnmu_date_epoch = $non_binnmu_timestamp; + return; + } +} + +# Setup the build ENV by setting dpkg-buildflags (via set_buildflags()) plus +# cleaning up HOME (etc) in compat 13+ +sub setup_buildenv { + set_buildflags(); + if (not compat(12)) { + setup_home_and_xdg_dirs(); + } +} + +sub setup_home_and_xdg_dirs { + require Cwd; + my $cwd = Cwd::getcwd(); + my $home_dir = join('/', $cwd, generated_file('_source', 'home', 0)); + my @paths = ( + $home_dir, + ); + my @clear_env = qw( + XDG_CACHE_HOME + XDG_CONFIG_DIRS + XDG_CONFIG_HOME + XDG_DATA_HOME + XDG_DATA_DIRS + XDG_RUNTIME_DIR + ); + mkdirs(@paths); + for my $envname (@clear_env) { + delete($ENV{$envname}); + } + $ENV{'HOME'} = $home_dir; + return; +} + +sub reset_buildflags { + eval { require Dpkg::BuildFlags }; + if ($@) { + warning "unable to load build flags: $@"; + return; + } + delete($ENV{'DH_INTERNAL_BUILDFLAGS'}); + my $buildflags = Dpkg::BuildFlags->new(); + foreach my $flag ($buildflags->list()) { + next unless $flag =~ /^[A-Z]/; # Skip flags starting with lowercase + delete($ENV{$flag}); + } +} + +# Sets environment variables from dpkg-buildflags. Avoids changing +# any existing environment variables. +sub set_buildflags { + return if $ENV{DH_INTERNAL_BUILDFLAGS}; + $ENV{DH_INTERNAL_BUILDFLAGS}=1; + + # For the side effect of computing the SOURCE_DATE_EPOCH variable. + get_source_date_epoch(); + + return if compat(8); + + # Export PERL_USE_UNSAFE_INC as a transitional step to allow us + # to remove . from @INC by default without breaking packages which + # rely on this [CVE-2016-1238] + $ENV{PERL_USE_UNSAFE_INC} = 1 if compat(10); + + eval { require Dpkg::BuildFlags }; + if ($@) { + warning "unable to load build flags: $@"; + return; + } + + my $buildflags = Dpkg::BuildFlags->new(); + $buildflags->load_config(); + foreach my $flag ($buildflags->list()) { + next unless $flag =~ /^[A-Z]/; # Skip flags starting with lowercase + if (! exists $ENV{$flag}) { + $ENV{$flag} = $buildflags->get($flag); + } + } +} + +# Gets a DEB_BUILD_OPTIONS option, if set. +sub get_buildoption { + my ($wanted, $default) = @_; + + return $default if not exists($ENV{DEB_BUILD_OPTIONS}); + + foreach my $opt (split(/\s+/, $ENV{DEB_BUILD_OPTIONS})) { + # currently parallel= is the only one with a parameter + if ($opt =~ /^parallel=(-?\d+)$/ && $wanted eq 'parallel') { + return $1; + } elsif ($opt =~ m/^dherroron=(\S*)$/ && $wanted eq 'dherroron') { + my $value = $1; + if ($value ne 'obsolete-compat-levels') { + warning("Unknown value \"${value}\" as parameter for \"dherroron\" seen in DEB_BUILD_OPTIONS"); + } + return $value; + } elsif ($opt eq $wanted) { + return 1; + } + } + return $default; +} + +# Returns true if DEB_BUILD_PROFILES lists the given profile. +sub is_build_profile_active { + my ($wanted) = @_; + return 0 if not exists($ENV{DEB_BUILD_PROFILES}); + for my $prof (split(m/\s+/, $ENV{DEB_BUILD_PROFILES})) { + return 1 if $prof eq $wanted; + } + return 0; +} + + +# Called when an executable config file failed. It provides a more helpful error message in +# some cases (especially when the file was not intended to be executable). +sub _executable_dh_config_file_failed { + my ($source, $err, $proc_err) = @_; + error("Error closing fd/process for ${source}: $err") if $err; + # The interpreter did not like the file for some reason. + # Lets check if the maintainer intended it to be + # executable. + if (not is_so_or_exec_elf_file($source) and not _has_shebang_line($source)) { + warning("${source} is marked executable but does not appear to an executable config."); + warning(); + warning("If ${source} is intended to be an executable config file, please ensure it can"); + warning("be run as a stand-alone script/program (e.g. \"./${source}\")"); + warning("Otherwise, please remove the executable bit from the file (e.g. chmod -x \"${source}\")"); + warning(); + warning('Please see "Executable debhelper config files" in debhelper(7) for more information.'); + warning(); + } + $? = $proc_err; + error_exitcode("${source} (executable config)"); + return; +} + + +# install a dh config file (e.g. debian/<pkg>.lintian-overrides) into +# the package. Under compat 9+ it may execute the file and use its +# output instead. +# +# install_dh_config_file(SOURCE, TARGET) +sub install_dh_config_file { + my ($source, $target) = @_; + + if (!compat(8) and -x $source) { + my @sstat = stat(_) || error("cannot stat $source: $!"); + open(my $tfd, '>', $target) || error("cannot open $target: $!"); + chmod(0644, $tfd) || error("cannot chmod $target: $!"); + open(my $sfd, '-|', $source) || error("cannot run $source: $!"); + while (my $line = <$sfd>) { + print ${tfd} $line; + } + if (!close($sfd)) { + _executable_dh_config_file_failed($source, $!, $?); + } + close($tfd) || error("cannot close $target: $!"); + # Set the mtime (and atime) to ensure reproducibility. + utime($sstat[9], $sstat[9], $target); + } else { + install_file($source, $target); + } + return 1; +} + +sub restore_file_on_clean { + my ($file) = @_; + my $bucket_index = 'debian/.debhelper/bucket/index'; + my $bucket_dir = 'debian/.debhelper/bucket/files'; + my $checksum; + mkdirs($bucket_dir); + if ($file =~ m{^/}) { + error("restore_file_on_clean requires a path relative to the package dir"); + } + $file =~ s{^\./}{}g; + $file =~ s{//++}{}g; + if ($file =~ m{^\.} or $file =~ m{/CVS/} or $file =~ m{/\.}) { + # We do not want to smash a Vcs repository by accident. + warning("Attempt to store $file, which looks like a VCS file or"); + warning("a hidden package file (like quilt's \".pc\" directory)"); + error("This tool probably contains a bug."); + } + if (-l $file or not -f _) { + error("Cannot store $file: Can only store regular files (no symlinks, etc.)"); + } + require Digest::SHA; + + $checksum = Digest::SHA->new('256')->addfile($file, 'b')->hexdigest; + + if (not $dh{NO_ACT}) { + my ($in_index); + open(my $fd, '+>>', $bucket_index) + or error("open($bucket_index, a+) failed: $!"); + seek($fd, 0, 0); + while (my $line = <$fd>) { + my ($cs, $stored_file); + chomp($line); + ($cs, $stored_file) = split(m/ /, $line, 2); + next if ($stored_file ne $file); + $in_index = 1; + } + if (not $in_index) { + # Copy and then rename so we always have the full copy of + # the file in the correct place (if any at all). + doit('cp', '-an', '--reflink=auto', $file, "${bucket_dir}/${checksum}.tmp"); + rename_path("${bucket_dir}/${checksum}.tmp", "${bucket_dir}/${checksum}"); + print {$fd} "${checksum} ${file}\n"; + } + close($fd) or error("close($bucket_index) failed: $!"); + } + + return 1; +} + +sub restore_all_files { + my ($clear_index) = @_; + my $bucket_index = 'debian/.debhelper/bucket/index'; + my $bucket_dir = 'debian/.debhelper/bucket/files'; + + return if not -f $bucket_index; + open(my $fd, '<', $bucket_index) + or error("open($bucket_index) failed: $!"); + + while (my $line = <$fd>) { + my ($cs, $stored_file, $bucket_file); + chomp($line); + ($cs, $stored_file) = split(m/ /, $line, 2); + $bucket_file = "${bucket_dir}/${cs}"; + # Restore by copy and then rename. This ensures that: + # 1) If dh_clean is interrupted, we can always do a full restore again + # (otherwise, we would be missing some of the files and have to handle + # that with scary warnings) + # 2) The file is always fully restored or in its "pre-restore" state. + doit('cp', '-an', '--reflink=auto', $bucket_file, "${bucket_file}.tmp"); + rename_path("${bucket_file}.tmp", $stored_file); + } + close($fd); + rm_files($bucket_index) if $clear_index; + return; +} + +sub open_gz { + my ($file) = @_; + my $fd; + eval { + require PerlIO::gzip; + }; + if ($@) { + open($fd, '-|', 'gzip', '-dc', $file) + or error("gzip -dc $file failed: $!"); + } else { + # Pass ":unix" as well due to https://rt.cpan.org/Public/Bug/Display.html?id=114557 + # Alternatively, we could ensure we always use "POSIX::_exit". Unfortunately, + # loading POSIX is insanely slow. + open($fd, '<:unix:gzip', $file) + or error("open $file [<:unix:gzip] failed: $!"); + } + return $fd; +} + +sub deprecated_functionality { + my ($warning_msg, $compat_removal, $removal_msg) = @_; + if (defined($compat_removal) and not compat($compat_removal - 1)) { + my $msg = $removal_msg // $warning_msg; + warning($msg); + error("This feature was removed in compat ${compat_removal}."); + } else { + warning($warning_msg); + warning("This feature will be removed in compat ${compat_removal}.") + if defined($compat_removal); + } + return 1; +} + +sub log_installed_files { + my ($package, @patterns) = @_; + + return if $dh{NO_ACT}; + my $tool = $TOOL_NAME; + if (ref($package) eq 'HASH') { + my $options = $package; + $tool = $options->{'tool_name'} // error('Missing mandatory "tool_name" option for log_installed_files'); + $package = $options->{'package'} // error('Missing mandatory "package" option for log_installed_files'); + } + + my $log = generated_file($package, 'installed-by-' . $tool); + open(my $fh, '>>', $log) or error("open $log: $!"); + for my $src (@patterns) { + print $fh "$src\n"; + } + close($fh) or error("close $log: $!"); + + return 1; +} + +use constant { + # The ELF header is at least 0x32 bytes (32bit); any filer shorter than that is not an ELF file + ELF_MIN_LENGTH => 0x32, + ELF_MAGIC => "\x7FELF", + ELF_ENDIAN_LE => 0x01, + ELF_ENDIAN_BE => 0x02, + ELF_TYPE_EXECUTABLE => 0x0002, + ELF_TYPE_SHARED_OBJECT => 0x0003, +}; + +sub is_so_or_exec_elf_file { + my ($file) = @_; + open(my $fd, '<:raw', $file) or error("open $file: $!"); + my $buflen = 0; + my ($buf, $endian); + while ($buflen < ELF_MIN_LENGTH) { + my $r = read($fd, $buf, ELF_MIN_LENGTH - $buflen, $buflen) // error("read ($file): $!"); + last if $r == 0; # EOF + $buflen += $r + } + close($fd); + return 0 if $buflen < ELF_MIN_LENGTH; + + return 0 if substr($buf, 0x00, 4) ne ELF_MAGIC; + $endian = unpack('c', substr($buf, 0x05, 1)); + my ($long_format, $short_format); + + if ($endian == ELF_ENDIAN_BE) { + $long_format = 'N'; + $short_format = 'n'; + } elsif ($endian == ELF_ENDIAN_LE) { + $long_format = 'V'; + $short_format = 'v'; + } else { + return 0; + } + my $elf_version = substr($buf, 0x14, 4); + my $elf_type = substr($buf, 0x10, 2); + + + return 0 if unpack($long_format, $elf_version) != 0x00000001; + my $elf_type_unpacked = unpack($short_format, $elf_type); + return 0 if $elf_type_unpacked != ELF_TYPE_EXECUTABLE and $elf_type_unpacked != ELF_TYPE_SHARED_OBJECT; + return 1; +} + +sub _has_shebang_line { + my ($file) = @_; + open(my $fd, '<', $file) or error("open $file: $!"); + my $line = <$fd>; + close($fd); + return 1 if (defined($line) and substr($line, 0, 2) eq '#!'); + return 0; +} + +# Returns true iff the given argument is an empty directory. +# Corner-cases: +# - false if not a directory +sub is_empty_dir { + my ($dir) = @_; + return 0 if not -d $dir; + my $ret = 1; + opendir(my $dir_fd, $dir) or error("opendir($dir) failed: $!"); + while (defined(my $entry = readdir($dir_fd))) { + next if $entry eq '.' or $entry eq '..'; + $ret = 0; + last; + } + closedir($dir_fd); + return $ret; +} + +sub on_pkgs_in_parallel(&) { + unshift(@_, $dh{DOPACKAGES}); + goto \&on_items_in_parallel; +} + +# Given a list of files, find all hardlinked files and return: +# 1: a list of unique files (all files in the list are not hardlinked with any other file in that list) +# 2: a map where the keys are names of hardlinks and the value points to the name selected as the file put in the +# list of unique files. +# +# This is can be used to relink hard links after modifying one of them. +sub find_hardlinks { + my (@all_files) = @_; + my (%seen, %hardlinks, @unique_files); + for my $file (@all_files) { + my ($dev, $inode, undef, $nlink)=stat($file); + if (defined $nlink && $nlink > 1) { + if (! $seen{"$inode.$dev"}) { + $seen{"$inode.$dev"}=$file; + push(@unique_files, $file); + } else { + # This is a hardlink. + $hardlinks{$file}=$seen{"$inode.$dev"}; + } + } else { + push(@unique_files, $file); + } + } + return (\@unique_files, \%hardlinks); +} + +sub on_items_in_parallel { + my ($pkgs_ref, $code) = @_; + my @pkgs = @{$pkgs_ref}; + my %pids; + my $parallel = $MAX_PROCS; + my $count_per_proc = int( (scalar(@pkgs) + $parallel - 1)/ $parallel); + my $exit = 0; + if ($count_per_proc < 1) { + $count_per_proc = 1; + if (@pkgs > 3) { + # Forking has a considerable overhead, so bulk the number + # a bit. We do not do this unconditionally, because we + # want parallel issues (if any) to appear already with 2 + # packages and two procs (because people are lazy when + # testing). + # + # Same reason for also unconditionally forking with 1 pkg + # in 1 proc. + $count_per_proc = 2; + } + } + # Assertion, $count_per_proc * $parallel >= scalar(@pkgs) + while (@pkgs) { + my @batch = splice(@pkgs, 0, $count_per_proc); + my $pid = fork() // error("fork: $!"); + if (not $pid) { + # Child processes should not write to the log file + inhibit_log(); + eval { + $code->(@batch); + }; + if (my $err = $@) { + $err =~ s/\n$//; + print STDERR "$err\n"; + exit(2); + } + exit(0); + } + $pids{$pid} = 1; + } + while (%pids) { + my $pid = wait; + error("wait() failed: $!") if $pid == -1; + delete($pids{$pid}); + if ($? != 0) { + $exit = 1; + } + } + if ($exit) { + error("Aborting due to earlier error"); + } + return; +} + +*on_selected_pkgs_in_parallel = \&on_items_in_parallel; + +sub compute_doc_main_package { + my ($doc_package) = @_; + # if explicitly set, then choose that. + return $dh{DOC_MAIN_PACKAGE} if $dh{DOC_MAIN_PACKAGE}; + # In compat 10 (and earlier), there is no auto-detection + return $doc_package if compat(10); + my $target_package = $doc_package; + # If it is not a -doc package, then docs should be installed + # under its own package name. + return $doc_package if $target_package !~ s/-doc$//; + # FOO-doc hosts the docs for FOO; seems reasonable + return $target_package if exists($package_fields{$target_package}); + if ($doc_package =~ m/^lib./) { + # Special case, "libFOO-doc" can host docs for "libFOO-dev" + my $lib_dev = "${target_package}-dev"; + return $lib_dev if exists($package_fields{$lib_dev}); + # Technically, we could go look for a libFOO<something>-dev, + # but atm. it is presumed to be that much of a corner case + # that it warrents an override. + } + # We do not know; make that clear to the caller + return; +} + +sub dbgsym_tmpdir { + my ($package) = @_; + return "debian/.debhelper/${package}/dbgsym-root"; +} + +sub perl_cross_incdir { + return if !is_cross_compiling(); + + # native builds don't currently need this so only load it on demand + require Config; Config->import(); + + my $triplet = dpkg_architecture_value("DEB_HOST_MULTIARCH"); + my $perl_version = $Config::Config{version}; + my $incdir = "/usr/lib/$triplet/perl/cross-config-${perl_version}"; + return undef if !-e "$incdir/Config.pm"; + return $incdir; +} + +sub is_known_package { + my ($package) = @_; + state %known_packages = map { $_ => 1 } getpackages(); + return 1 if exists($known_packages{$package}); + return 0 +} + +sub assert_opt_is_known_package { + my ($package, $method) = @_; + if (not is_known_package($package)) { + error("Requested unknown package $package via $method, expected one of: " . join(' ', getpackages())); + } + return 1; +} + + +sub dh_gencontrol_automatic_substvars { + my ($package, $substvars_file, $has_dbgsym) = @_; + return if not -f $substvars_file; + + require Dpkg::Control; + require Dpkg::Control::Fields; + open(my $sfd, '+<', $substvars_file) or error("open $substvars_file: $!"); + my @dep_fields = Dpkg::Control::Fields::field_list_pkg_dep(); + my %known_dep_fields = map { lc($_) => 1 } @dep_fields; + my (%field_vars, $needs_dbgsym); + while (my $line = <$sfd>) { + next if $line =~ m{^\s*(?:[#].*)?$}; + chomp($line); + next if $line !~ m{(\w[-:0-9A-Za-z]*)([?!\$]?=)(?:.*)}; + my $key = $1; + my $assignment = $2; + # Ignore `$=` because they will work without us doing anything (which in turn means + # we might not have to rewrite the file). + if ($assignment eq '$=') { + $needs_dbgsym = 1; + next; + } + # If there is "required" substvar, then it will not be used for the dbgsym. + $needs_dbgsym = 1 if $assignment eq '!='; + next if ($key !~ m/:([-0-9A-Za-z]+)$/); + my $field_name_lc = lc($1); + next if not exists($known_dep_fields{$field_name_lc}); + my $substvar = '${' . $key . '}'; + push(@{$field_vars{$field_name_lc}}, $substvar); + } + close($sfd); + return if not %field_vars and not $needs_dbgsym; + + open(my $ocfd, '<', 'debian/control') or error("open debian/control: $!"); + my $src_stanza = Dpkg::Control->new; + my $pkg_stanza; + $src_stanza->parse($ocfd, 'debian/control') or error("No source stanza!?"); + while (1) { + $pkg_stanza = Dpkg::Control->new; + $pkg_stanza->parse($ocfd, 'debian/control') // error("EOF before the ${package} stanza appeared!?"); + last if $pkg_stanza->{'Package'} eq $package; + } + close($ocfd); + + my $rewritten_dctrl = generated_file($package, "rewritten-dctrl"); + for my $field_name (@dep_fields) { + my $field_name_lc = lc($field_name); + # No merging required + next if not exists($field_vars{$field_name_lc}); + my $field_value = $pkg_stanza->{$field_name}; + my $merge_value = join(", ", @{$field_vars{$field_name_lc}}); + if (defined($field_value) and $field_value !~ m{^\s*+$}) { + $field_value =~ s/,\s*$//; + $field_value .= ", "; + $field_value .= $merge_value; + } else { + $field_value = $merge_value; + } + $pkg_stanza->{$field_name} = $field_value; + } + open(my $wfd, '>', $rewritten_dctrl) or error("open ${rewritten_dctrl}: $!"); + $src_stanza->output($wfd); + print {$wfd} "\n"; + $pkg_stanza->output($wfd); + if ($has_dbgsym) { + my $dbgsym_stanza = Dpkg::Control->new; + # Minimal stanza to avoid substvars warnings. Most fields are still set + # via -D. + $dbgsym_stanza->{'Package'} = "${package}-dbgsym"; + $dbgsym_stanza->{'Architecture'} = $pkg_stanza->{"Architecture"}; + $dbgsym_stanza->{'Description'} = "debug symbols for ${package}"; + print {$wfd} "\n"; + $dbgsym_stanza->output($wfd); + } + close($wfd) or error("Failed to close/flush ${rewritten_dctrl}: $!"); + return ($rewritten_dctrl, $has_dbgsym); +} + + +sub _internal_optional_file_args { + state $_disable_file_seccomp; + if (not defined($_disable_file_seccomp)) { + my $consider_disabling_seccomp = 0; + if ($ENV{'FAKEROOTKEY'} or ($ENV{'LD_PRELOAD'} // '') =~ m/fakeroot/) { + $consider_disabling_seccomp = 1; + } + if ($consider_disabling_seccomp) { + my $has_no_sandbox = (qx_cmd('file', '--help') // '') =~ m/--no-sandbox/; + $consider_disabling_seccomp = 0 if not $has_no_sandbox; + } + $_disable_file_seccomp = $consider_disabling_seccomp; + } + return('--no-sandbox') if $_disable_file_seccomp; + return; +} + +1 |