diff options
Diffstat (limited to 'lib/Debian/Debhelper/SequencerUtil.pm')
-rw-r--r-- | lib/Debian/Debhelper/SequencerUtil.pm | 897 |
1 files changed, 897 insertions, 0 deletions
diff --git a/lib/Debian/Debhelper/SequencerUtil.pm b/lib/Debian/Debhelper/SequencerUtil.pm new file mode 100644 index 0000000..2fe5824 --- /dev/null +++ b/lib/Debian/Debhelper/SequencerUtil.pm @@ -0,0 +1,897 @@ +#!/usr/bin/perl +# +# Internal library functions for the dh(1) command + +package Debian::Debhelper::SequencerUtil; +use strict; +use warnings; +use constant { + 'DUMMY_TARGET' => 'debhelper-fail-me', + 'SEQUENCE_NO_SUBSEQUENCES' => 'none', + 'SEQUENCE_ARCH_INDEP_SUBSEQUENCES' => 'both', + 'SEQUENCE_TYPE_ARCH_ONLY' => 'arch', + 'SEQUENCE_TYPE_INDEP_ONLY' => 'indep', + 'SEQUENCE_TYPE_BOTH' => 'both', + 'FLAG_OPT_SOURCE_BUILDS_NO_ARCH_PACKAGES' => 0x1, + 'FLAG_OPT_SOURCE_BUILDS_NO_INDEP_PACKAGES' => 0x2, + 'UNSKIPPABLE_CLI_OPTIONS_BUILD_SYSTEM' => q(-S|--buildsystem|-D|--sourcedir|--sourcedirectory|-B|--builddir|--builddirectory), +}; + +use Exporter qw(import); + +use Debian::Debhelper::Dh_Lib qw( + %dh + basename + commit_override_log + compat error + escape_shell + get_buildoption + getpackages + load_log + package_is_arch_all + pkgfile + rm_files + tmpdir + warning + write_log +); + + +our @EXPORT = qw( + extract_rules_target_name + to_rules_target + sequence_type + unpack_sequence + rules_explicit_target + extract_skipinfo + compute_selected_addons + load_sequence_addon + run_sequence_command_and_exit_on_failure + should_skip_due_to_dpo + check_for_obsolete_commands + compute_starting_point_in_sequences + parse_dh_cmd_options + run_hook_target + run_through_command_sequence + DUMMY_TARGET + SEQUENCE_NO_SUBSEQUENCES + SEQUENCE_ARCH_INDEP_SUBSEQUENCES + SEQUENCE_TYPE_ARCH_ONLY + SEQUENCE_TYPE_INDEP_ONLY + SEQUENCE_TYPE_BOTH + FLAG_OPT_SOURCE_BUILDS_NO_ARCH_PACKAGES + FLAG_OPT_SOURCE_BUILDS_NO_INDEP_PACKAGES +); + +our (%EXPLICIT_TARGETS, $RULES_PARSED); + +sub extract_rules_target_name { + my ($command) = @_; + if ($command =~ m{^debian/rules\s++(.++)}) { + return $1 + } + return; +} + +sub to_rules_target { + return 'debian/rules '.join(' ', @_); +} + +sub sequence_type { + my ($sequence_name) = @_; + if ($sequence_name =~ m/-indep$/) { + return 'indep'; + } elsif ($sequence_name =~ m/-arch$/) { + return 'arch'; + } + return 'both'; +} + +sub _agg_subseq { + my ($current_subseq, $outer_subseq) = @_; + if ($current_subseq eq $outer_subseq) { + return $current_subseq; + } + if ($current_subseq eq 'both') { + return $outer_subseq; + } + return $current_subseq; +} + +sub unpack_sequence { + my ($sequences, $sequence_name, $always_inline, $completed_sequences, $flags) = @_; + my (@sequence, @targets, %seen, %non_inlineable_targets, @stack); + my $sequence_type = sequence_type($sequence_name); + # Walk through the sequence effectively doing a DFS of the rules targets + # (when we are allowed to inline them). + my $seq = $sequences->{$sequence_name}; + $flags //= 0; + + push(@stack, [$seq->flatten_sequence($sequence_type, $flags)]); + while (@stack) { + my $current_sequence = pop(@stack); + COMMAND: + while (@{$current_sequence}) { + my $command = shift(@{$current_sequence}); + if (ref($command) eq 'ARRAY') { + $command = $command->[0]; + } + my $rules_target=extract_rules_target_name($command); + next if (defined($rules_target) and exists($completed_sequences->{$rules_target})); + if (defined($rules_target) and $always_inline) { + my $subsequence = $sequences->{$rules_target}; + my $subseq_type = _agg_subseq(sequence_type($rules_target), $sequence_type); + push(@stack, $current_sequence); + $current_sequence = [$subsequence->flatten_sequence($subseq_type, $flags)]; + } elsif (defined($rules_target)) { + my $subsequence = $sequences->{$rules_target}; + my $subseq_type = _agg_subseq(sequence_type($rules_target), $sequence_type); + my @subseq_types = ($subseq_type); + my %subtarget_status; + my ($transparent_subseq, $opaque_subseq, $subtarget_decided_both); + if ($subseq_type eq SEQUENCE_TYPE_BOTH) { + push(@subseq_types, SEQUENCE_TYPE_ARCH_ONLY, SEQUENCE_TYPE_INDEP_ONLY); + } + for my $ss_type (@subseq_types) { + my $full_rule_target = ($ss_type eq SEQUENCE_TYPE_BOTH) ? $rules_target : "${rules_target}-${ss_type}"; + if (exists($completed_sequences->{$full_rule_target})) { + $subtarget_status{$ss_type} = 'complete'; + last if $ss_type eq $subseq_type; + } + elsif (defined(rules_explicit_target($full_rule_target))) { + $subtarget_status{$ss_type} = 'opaque'; + last if $ss_type eq $subseq_type; + } + else { + $subtarget_status{$ss_type} = 'transparent'; + } + } + # At this point, %subtarget_status has 1 or 3 kv-pairs. + # - If it has 1, then just check that and be done + # - If it has 3, then "both" must be "transparent". + + if (scalar(keys(%subtarget_status)) == 3) { + if ($subtarget_status{${\SEQUENCE_TYPE_ARCH_ONLY}} eq $subtarget_status{${\SEQUENCE_TYPE_INDEP_ONLY}}) { + # The "both" target is transparent and the subtargets agree. This is the common case + # of "everything is transparent" (or both subtargets are opaque) and we reduce that by + # reducing it to only have one key. + %subtarget_status = ( $subseq_type => $subtarget_status{${\SEQUENCE_TYPE_ARCH_ONLY}} ); + # There is one special-case for this flow if both targets are opaque. + $subtarget_decided_both = 1; + } else { + # The subtargets have different status but we know that the "both" key must be irrelevant + # then. Remove it to simplify matters below. + delete($subtarget_status{${\SEQUENCE_TYPE_BOTH}}); + } + } + + if (scalar(keys(%subtarget_status)) == 1) { + # "Simple" case where we only have to check exactly one result + if ($subtarget_status{$subseq_type} eq 'opaque') { + $opaque_subseq = $subseq_type; + } + elsif ($subtarget_status{$subseq_type} eq 'transparent') { + $transparent_subseq = $subseq_type; + } + } else { + # Either can be transparent, opaque or complete at this point. + if ($subtarget_status{${\SEQUENCE_TYPE_ARCH_ONLY}} eq 'transparent') { + $transparent_subseq = SEQUENCE_TYPE_ARCH_ONLY + } elsif ($subtarget_status{${\SEQUENCE_TYPE_INDEP_ONLY}} eq 'transparent') { + $transparent_subseq = SEQUENCE_TYPE_INDEP_ONLY + } + if ($subtarget_status{${\SEQUENCE_TYPE_ARCH_ONLY}} eq 'opaque') { + $opaque_subseq = SEQUENCE_TYPE_ARCH_ONLY + } elsif ($subtarget_status{${\SEQUENCE_TYPE_INDEP_ONLY}} eq 'opaque') { + $opaque_subseq = SEQUENCE_TYPE_INDEP_ONLY + } + } + if ($opaque_subseq) { + if ($subtarget_decided_both) { + # Final special-case - we are here because the rules file define X-arch AND X-indep but + # not X. In this case, we want two d/rules X-{arch,indep} calls rather than a single + # d/rules X call. + for my $ss_type ((SEQUENCE_TYPE_ARCH_ONLY, SEQUENCE_TYPE_INDEP_ONLY)) { + my $rules_target_cmd = $subsequence->as_rules_target_command($ss_type); + push(@targets, $rules_target_cmd) if not $seen{$rules_target_cmd}++; + } + } else { + my $rules_target_cmd = $subsequence->as_rules_target_command($opaque_subseq); + push(@targets, $rules_target_cmd) if not $seen{$rules_target_cmd}++; + } + } + if ($transparent_subseq) { + push(@stack, $current_sequence); + $current_sequence = [$subsequence->flatten_sequence($transparent_subseq, $flags)]; + } + next COMMAND; + } else { + if (defined($rules_target) and not $always_inline) { + next COMMAND if exists($non_inlineable_targets{$rules_target}); + push(@targets, $command) if not $seen{$command}++; + } elsif (! $seen{$command}) { + $seen{$command} = 1; + push(@sequence, $command); + } + } + } + } + return (\@targets, \@sequence); +} + + +sub rules_explicit_target { + # Checks if a specified target exists as an explicit target + # in debian/rules. + # undef is returned if target does not exist, 0 if target is noop + # and 1 if target has dependencies or executes commands. + my ($target) = @_; + + if (! $RULES_PARSED) { + my $processing_targets = 0; + my $not_a_target = 0; + my $current_target; + open(MAKE, "LC_ALL=C make -Rrnpsf debian/rules ${\DUMMY_TARGET} 2>/dev/null |"); + while (<MAKE>) { + if ($processing_targets) { + if (/^# Not a target:/) { + $not_a_target = 1; + } else { + if (!$not_a_target && m/^([^#:]+)::?\s*(.*)$/) { + # Target is defined. NOTE: if it is a dependency of + # .PHONY it will be defined too but that's ok. + # $2 contains target dependencies if any. + $current_target = $1; + $EXPLICIT_TARGETS{$current_target} = [($2) ? 1 : 0, 'debian/rules']; + } else { + if (defined($current_target)) { + if (m/^#/) { + # Check if target has commands to execute + if (m/^#\s*(commands|recipe) to execute/) { + my $where; + if (m{from ["'](\S+)["'], line \d+}) { + # The line is the first line of the recipe and not the target + # definition. To keep things simple, we just do not report the line + $where = [1, $1]; + } else { + $where = [1, 'debian/rules']; + } + $EXPLICIT_TARGETS{$current_target} = $where; + } + } else { + # Target parsed. + $current_target = undef; + } + } + } + # "Not a target:" is always followed by + # a target name, so resetting this one + # here is safe. + $not_a_target = 0; + } + } elsif (m/^# Files$/) { + $processing_targets = 1; + } + } + close MAKE; + $RULES_PARSED = 1; + } + + return $EXPLICIT_TARGETS{$target}[0] if exists($EXPLICIT_TARGETS{$target}); + return; +} + +sub extract_skipinfo { + my ($command) = @_; + + foreach my $dir (split(':', $ENV{PATH})) { + if (open (my $h, "<", "$dir/$command")) { + while (<$h>) { + if (m/PROMISE: DH NOOP( WITHOUT\s+(.*))?\s*$/) { + close $h; + return split(' ', $2) if defined($2); + return ('always-skip'); + } + } + close $h; + return; + } + } + return; +} + +sub _skipped_call_due_dpo { + my ($command, $dbo_flag) = @_; + my $me = Debian::Debhelper::Dh_Lib::_color(basename($0), 'bold'); + my $skipped = Debian::Debhelper::Dh_Lib::_color('command-omitted', 'yellow'); + print "${me}: ${skipped}: The call to \"${command}\" was omitted due to \"DEB_BUILD_OPTIONS=${dbo_flag}\"\n"; + return; +} + +sub should_skip_due_to_dpo { + my ($command, $to_be_invoked) = @_; + + # Indirection/reference for readability + my $commands_ref = \%Debian::Debhelper::DH::SequenceState::commands_skippable_via_deb_build_options; + + if (not $dh{'NO_ACT'} and exists($commands_ref->{$command})) { + my $flags_ref = $commands_ref->{$command}; + for my $flag (@{$flags_ref}) { + if (get_buildoption($flag)) { + _skipped_call_due_dpo($to_be_invoked, $flag) if defined($to_be_invoked); + return 1; + } + } + } + return 0; +} + +sub compute_starting_point_in_sequences { + my ($packages_ref, $full_sequence, $logged) = @_; + my %startpoint; + if (compat(9)) { + foreach my $package (@{$packages_ref}) { + my @log = load_log($package, $logged); + # Find the last logged command that is in the sequence, and + # continue with the next command after it. If no logged + # command is in the sequence, we're starting at the beginning.. + $startpoint{$package} = 0; + COMMAND: + foreach my $command (reverse(@log)) { + foreach my $i (0 .. $#{$full_sequence}) { + if ($command eq $full_sequence->[$i]) { + $startpoint{$package} = $i + 1; + last COMMAND; + } + } + } + } + } else { + foreach my $package (@{$packages_ref}) { + $startpoint{$package} = 0; + } + } + return %startpoint; +} + + +sub compute_selected_addons { + my ($sequence_name, @addon_requests_from_args) = @_; + my (@enabled_addons, %disabled_addons, %enabled); + my @addon_requests; + my $sequence_type = sequence_type($sequence_name); + + my %addon_constraints = %{ Debian::Debhelper::Dh_Lib::bd_dh_sequences() }; + my %explicitly_managed; + + # Inject elf-tools early as other addons rely on their presence and it historically + # has been considered a part of the "core" sequence. + if (exists($addon_constraints{'elf-tools'})) { + # Explicitly requested; respect that + push(@addon_requests, '+elf-tools'); + } elsif (compat(12, 1)) { + # In compat 12 and earlier, we only inject the sequence if there are arch + # packages present and the sequence requires it. + if (getpackages('arch') and $sequence_type ne SEQUENCE_TYPE_INDEP_ONLY) { + push(@addon_requests, '+elf-tools'); + } + } else { + # In compat 13, we always inject the addon if not explicitly requested and + # then flag it as arch_only + push(@addon_requests, '+elf-tools'); + $addon_constraints{'elf-tools'} = SEQUENCE_TYPE_ARCH_ONLY if not exists($addon_constraints{'elf-tools'}); + } + + # Order is important; DH_EXTRA_ADDONS must come before everything + # else; then comes built-in and finally argument provided add-ons + # requests. + push(@addon_requests, map { "+${_}" } split(",", $ENV{DH_EXTRA_ADDONS})) + if $ENV{DH_EXTRA_ADDONS}; + if (not compat(9, 1)) { + # Enable autoreconf'ing by default in compat 10 or later. + push(@addon_requests, '+autoreconf'); + + # Enable systemd support by default in compat 10 or later. + # - compat 11 injects the dh_installsystemd tool directly in the + # sequence instead of using a --with sequence. + push(@addon_requests, '+systemd') if compat(10, 1); + push(@addon_requests, '+build-stamp'); + } + for my $addon_name (sort(keys(%addon_constraints))) { + my $addon_type = $addon_constraints{$addon_name}; + + # Special-case for the "clean" target to avoid B-D-I dependencies in that for conditional add-ons + next if $sequence_name eq 'clean' and $addon_type ne SEQUENCE_TYPE_BOTH; + if ($addon_type eq 'both' or $sequence_type eq 'both' or $addon_type eq $sequence_type) { + push(@addon_requests, "+${addon_name}"); + } + } + + push(@addon_requests, @addon_requests_from_args); + + # Removing disabled add-ons are expensive (O(N) per time), so we + # attempt to make removals in bulk. Note that we have to be order + # preserving (due to #885580), so there is a limit to how "smart" + # we can be. + my $flush_disable_cache = sub { + @enabled_addons = grep { not exists($disabled_addons{$_}) } @enabled_addons; + for my $addon (keys(%disabled_addons)) { + delete($enabled{$addon}); + } + %disabled_addons = (); + }; + + for my $request (@addon_requests) { + if ($request =~ m/^[+-]root[-_]sequence$/) { + error("Invalid request to skip the sequence \"root-sequence\": It cannot be disabled") + if $request =~ m/^-/; + error("Invalid request to load the sequence \"root-sequence\": Do not reference it directly"); + } + if ($request =~ s/^[+]//) { + # Normalize "_" to "-" in the name. + $request =~ tr/_/-/; + $flush_disable_cache->() if %disabled_addons; + $explicitly_managed{$request} = 1; + push(@enabled_addons, $request) if not $enabled{$request}++; + } elsif ($request =~ s/^-//) { + # Normalize "_" to "-" in the name. + $request =~ tr/_/-/; + $explicitly_managed{$request} = 1; + $disabled_addons{$request} = 1; + } else { + error("Internal error: Invalid add-on request: $request (Missing +/- prefix)"); + } + } + + if (compat(14, 1) && getpackages() == 1 && !exists($explicitly_managed{'single-binary'})) { + if (not compat(13, 1)) { + warning("Implicitly activating single-binary dh addon for backwards compatibility. In compat 14+,"); + warning("this fallback will *not* happen automatically and dh_auto_install will instead use a"); + warning("different default for --destdir, which can cause the source to produce an empty binary package"); + warning(); + warning('To keep the existing behaviour, please activate the single-binary addon explicitly.'); + warning('This can be done by adding "dh-sequence-single-binary" to Build-Depends or passing'); + warning('--with=single-binary to dh.'); + warning(); + warning('If you have solved this issue differently (e.g., by passing --destdir explicitly to'); + warning('dh_auto_install or not using dh_auto_install at all) and want to silence this warning'); + warning('without activating the addon, you can do that by passing --without=single-binary to dh'); + warning('to explicitly acknowledge the change.'); + warning(); + warning('Please see the description of the "single-binary" in "man dh" for more details of what'); + warning('it does and why this is changing from implicit behaviour to explicitly opt-in.'); + } + push(@enabled_addons, 'single-binary'); + } + + $flush_disable_cache->() if %disabled_addons; + return map { + { + 'name' => $_, + 'addon-type' => $addon_constraints{$_} // SEQUENCE_TYPE_BOTH, + } + } @enabled_addons; +} + + +sub load_sequence_addon { + my ($addon_name, $addon_type) = @_; + require Debian::Debhelper::DH::AddonAPI; + my $mod="Debian::Debhelper::Sequence::${addon_name}"; + $mod=~s/-/_/g; + local $Debian::Debhelper::DH::AddonAPI::DH_INTERNAL_ADDON_NAME = $addon_name; + local $Debian::Debhelper::DH::AddonAPI::DH_INTERNAL_ADDON_TYPE = $addon_type; + eval "package Debian::Debhelper::DH::AddonAPI; use $mod"; + if ($@) { + error("unable to load addon ${addon_name}: $@"); + } +} + +sub check_for_obsolete_commands { + my ($full_sequence) = @_; + my ($found_obsolete_targets, $min_compat); + for my $command (@{$full_sequence}) { + if (exists($Debian::Debhelper::DH::SequenceState::obsolete_command{$command})) { + my $addon_name = $Debian::Debhelper::DH::SequenceState::obsolete_command{$command}[1]; + error("The addon ${addon_name} claimed that $command was obsolete, but it is not!?"); + } + } + for my $command (sort(keys(%Debian::Debhelper::DH::SequenceState::obsolete_command))) { + my ($addon_name, $error_compat) = @{$Debian::Debhelper::DH::SequenceState::obsolete_command{$command}}; + $addon_name = 'debhelper' if $addon_name eq 'root-sequence'; + for my $prefix (qw(execute_before_ execute_after_ override_)) { + for my $suffix ('', '-arch', '-indep') { + my $target = "${prefix}${command}${suffix}"; + if (defined(rules_explicit_target($target))) { + $found_obsolete_targets = 1; + $min_compat //= $error_compat; + $min_compat = $error_compat if $error_compat < $min_compat; + warning("The target ${target} references a now obsolete command and will not be run!" + . " (Marked by ${addon_name}, will be an error in compat $error_compat)"); + } + } + } + } + if ($found_obsolete_targets and not compat($min_compat - 1)) { + error("Aborting due to left over override/hook targets for now removed commands."); + } + return; +} + +sub run_sequence_command_and_exit_on_failure { + my ($command, @options) = @_; + + # 3 space indent lines the command being run up under the + # sequence name after "dh ". + if (!$dh{QUIET}) { + print " ".escape_shell($command, @options)."\n"; + } + + return if $dh{NO_ACT}; + + my $ret=system { $command } $command, @options; + if ($ret >> 8 != 0) { + exit $ret >> 8; + } + if ($ret) { + exit 1; + } + return; +} + + +sub run_hook_target { + my ($target_stem, $min_compat_level, $command, $packages, @opts) = @_; + my @todo = @{$packages}; + foreach my $override_type (undef, "arch", "indep") { + @todo = _run_injected_rules_target($target_stem, $override_type, $min_compat_level, $command, \@todo, @opts); + } + return @todo; +} + +# Tries to run an override / hook target for a command. Returns the list of +# packages that it was unable to run the target for. +sub _run_injected_rules_target { + my ($target_stem, $override_type, $min_compat_level, $command, $packages, @options) = @_; + + my $rules_target = $target_stem . + (defined $override_type ? "-".$override_type : ""); + + $command //= $rules_target; # Ensure it is defined + + # Check which packages are of the right architecture for the + # override_type. + my (@todo, @rest); + my $has_explicit_target = rules_explicit_target($rules_target); + + if ($has_explicit_target and defined($min_compat_level) and compat($min_compat_level - 1)) { + error("Hook target ${rules_target} is only supported in compat ${min_compat_level} or later"); + } + + if (defined $override_type) { + foreach my $package (@{$packages}) { + my $isall=package_is_arch_all($package); + if (($override_type eq 'indep' && $isall) || + ($override_type eq 'arch' && !$isall)) { + push @todo, $package; + } else { + push @rest, $package; + push @options, "-N$package"; + } + } + } else { + @todo=@{$packages}; + } + + return @{$packages} unless defined $has_explicit_target; # no such override + return @rest if ! $has_explicit_target; # has empty override + return @rest unless @todo; # has override, but no packages to act on + return @rest if should_skip_due_to_dpo($command, "debian/rules $rules_target"); + + if (defined $override_type) { + # Ensure appropriate -a or -i option is passed when running + # an arch-specific override target. + my $opt=$override_type eq "arch" ? "-a" : "-i"; + push @options, $opt unless grep { $_ eq $opt } @options; + } + + # Discard any override log files before calling the override + # target + if (not compat(9)) { + my @files = glob('debian/*.debhelper.log'); + rm_files(@files) if @files; + } + # This passes the options through to commands called + # inside the target. + $ENV{DH_INTERNAL_OPTIONS}=join("\x1e", @options); + $ENV{DH_INTERNAL_OVERRIDE}=$command; + run_sequence_command_and_exit_on_failure("debian/rules", $rules_target); + delete $ENV{DH_INTERNAL_OPTIONS}; + delete $ENV{DH_INTERNAL_OVERRIDE}; + + # Update log for overridden command now that it has + # finished successfully. + # (But avoid logging for dh_clean since it removes + # the log earlier.) + if (! $dh{NO_ACT} && $command ne 'dh_clean' && compat(9)) { + write_log($command, @todo); + commit_override_log(@todo); + } + + # Override targets may introduce new helper files. Strictly + # speaking this *shouldn't* be necessary, but lets make no + # assumptions. + Debian::Debhelper::Dh_Lib::dh_clear_unsafe_cache(); + + return @rest; +} + + +# Options parsed to dh that may need to be passed on to helpers +sub parse_dh_cmd_options { + my (@argv) = @_; + + # Ref for readability + my $options_ref = \@Debian::Debhelper::DH::SequenceState::options; + + while (@argv) { + my $opt = shift(@argv); + if ($opt =~ /^--?(after|until|before|with|without)$/) { + shift(@argv); + next; + } elsif ($opt =~ /^--?(no-act|remaining|(after|until|before|with|without)=)/) { + next; + } elsif ($opt =~ /^-/) { + if (not @{$options_ref} and $opt eq '--parallel' or $opt eq '--no-parallel') { + my $max_parallel; + # Ignore the option if it is the default for the given + # compat level. + next if compat(9) and $opt eq '--no-parallel'; + next if not compat(9) and $opt eq '--parallel'; + # Having an non-empty "@options" hurts performance quite a + # bit. At the same time, we want to promote the use of + # --(no-)parallel, so "tweak" the options a bit if there + # is no reason to include this option. + $max_parallel = get_buildoption('parallel') // 1; + next if $max_parallel == 1; + } + if ($opt =~ m/^(--[^=]++)(?:=.*)?$/ or $opt =~ m/^(-[^-])(?:=.*)?$/) { + my $optname = $1; + if (length($optname) > 2 and (compat(12, 1) or $optname =~ m/^-[^-][^=]/)) { + # We cannot optimize bundled options but we can optimize a single + # short option with an explicit parameter (-B=F is ok, -BF is not) + # In compat 12 or earlier, we also punt on long options due to + # auto-abbreviation. + $Debian::Debhelper::DH::SequenceState::unoptimizable_option_bundle = 1 + } + $Debian::Debhelper::DH::SequenceState::seen_options{$optname} = 1; + } elsif ($opt =~ m/^-[^-][^-]/) { + # We cannot optimize bundled options but we can optimize a single + # short option with an explicit parameter (-B=F is ok, -BF is not) + $Debian::Debhelper::DH::SequenceState::unoptimizable_option_bundle = 1 + } else { + # Special case that disables NOOP cli-options() as well + $Debian::Debhelper::DH::SequenceState::unoptimizable_user_option = 1; + } + push(@{$options_ref}, "-O" . $opt); + } elsif (@{$options_ref}) { + if ($options_ref->[$#{$options_ref}] =~ /^-O--/) { + $options_ref->[$#{$options_ref}] .= '=' . $opt; + } else { + # Special case that disables NOOP cli-options() as well + $Debian::Debhelper::DH::SequenceState::unoptimizable_user_option = 1; + $options_ref->[$#{$options_ref}] .= $opt; + } + } else { + error("Unknown parameter: $opt"); + } + } + return; +} + + +sub run_through_command_sequence { + my ($full_sequence, $startpoint, $logged, $options, $all_packages, $arch_packages, $indep_packages) = @_; + + my $command_opts = \%Debian::Debhelper::DH::SequenceState::command_opts; + my $stoppoint = $#{$full_sequence}; + + # Now run the commands in the sequence. + foreach my $i (0 .. $stoppoint) { + my $command = $full_sequence->[$i]; + + # Figure out which packages need to run this command. + my (@todo, @opts); + my @filtered_packages = _active_packages_for_command($command, $all_packages, $arch_packages, $indep_packages); + + foreach my $package (@filtered_packages) { + if (($startpoint->{$package}//0) > $i || + $logged->{$package}{$full_sequence->[$i]}) { + push(@opts, "-N$package"); + } + else { + push(@todo, $package); + } + } + next unless @todo; + push(@opts, @{$options}); + + my $rules_target = extract_rules_target_name($command); + error("Internal error: $command is a rules target, but it is not supported to be!?") if defined($rules_target); + + if (my $stamp_file = _stamp_target($command)) { + my %seen; + print " create-stamp " . escape_shell($stamp_file) . "\n"; + + next if $dh{NO_ACT}; + open(my $fd, '+>>', $stamp_file) or error("open($stamp_file, rw) failed: $!"); + # Seek to the beginning + seek($fd, 0, 0) or error("seek($stamp_file) failed: $!"); + while (my $line = <$fd>) { + chomp($line); + $seen{$line} = 1; + } + for my $pkg (grep {not exists($seen{$_})} @todo) { + print {$fd} "$pkg\n"; + } + close($fd) or error("close($stamp_file) failed: $!"); + next; + } + + my @full_todo = @todo; + run_hook_target("execute_before_${command}", 10, $command, \@full_todo, @opts); + + # Check for override targets in debian/rules, and run instead of + # the usual command. (The non-arch-specific override is tried first, + # for simplest semantics; mixing it with arch-specific overrides + # makes little sense.) + @todo = run_hook_target("override_${command}", undef, $command, \@full_todo, @opts); + + if (@todo and not _can_skip_command($command, @todo)) { + # No need to run the command for any packages handled by the + # override targets. + my %todo = map {$_ => 1} @todo; + foreach my $package (@full_todo) { + if (!$todo{$package}) { + push @opts, "-N$package"; + } + } + if (not should_skip_due_to_dpo($command, Debian::Debhelper::Dh_Lib::_format_cmdline($command, @opts))) { + my @cmd_options; + # Include additional command options if any + push(@cmd_options, @{$command_opts->{$command}}) + if exists($command_opts->{$command}); + push(@cmd_options, @opts); + run_sequence_command_and_exit_on_failure($command, _remove_dup_pkg_options(@cmd_options)); + } + } + + run_hook_target("execute_after_${command}", 10, $command, \@full_todo, @opts); + } +} + +sub _remove_dup_pkg_options { + my (@options) = @_; + my @filtered_options; + my $arch = 0; + my $indep = 0; + for my $option (@options) { + if ($option eq '-a' or $option eq '--arch') { + next if $arch; + $arch = 1; + } + if ($option eq '-i' or $option eq '--indep') { + next if $indep; + $indep = 1; + } + push(@filtered_options, $option); + } + return @filtered_options; +} + + +sub _stamp_target { + my ($command) = @_; + if ($command =~ s/^create-stamp\s+//) { + return $command; + } + return; +} + +{ + my %skipinfo; + sub _can_skip_command { + my ($command, @packages) = @_; + + return 0 if $dh{NO_ACT} and not $ENV{DH_INTERNAL_TEST_CAN_SKIP}; + + return 0 if $Debian::Debhelper::DH::SequenceState::unoptimizable_user_option || + (exists $ENV{DH_OPTIONS} && length $ENV{DH_OPTIONS}); + + return 0 if exists($Debian::Debhelper::DH::SequenceState::command_opts{$command}) + and @{$Debian::Debhelper::DH::SequenceState::command_opts{$command}}; + + if (! defined $skipinfo{$command}) { + $skipinfo{$command}=[extract_skipinfo($command)]; + } + my @skipinfo=@{$skipinfo{$command}}; + return 0 unless @skipinfo; + return 1 if scalar(@skipinfo) == 1 and $skipinfo[0] eq 'always-skip'; + my ($all_pkgs, $had_cli_options); + + foreach my $skipinfo (@skipinfo) { + my $type = 'pkgfile'; + my $need = $skipinfo; + if ($skipinfo=~/^([a-zA-Z0-9-_]+)\((.*)\)$/) { + ($type, $need) = ($1, $2); + } + if ($type eq 'tmp') { + foreach my $package (@packages) { + my $tmp = tmpdir($package); + return 0 if -e "$tmp/$need"; + } + } elsif ($type eq 'pkgfile' or $type eq 'pkgfile-logged') { + my $pkgs; + if ($type eq 'pkgfile') { + $pkgs = \@packages; + } else { + $all_pkgs //= [ getpackages() ]; + $pkgs = $all_pkgs; + } + # Use the secret bulk check call + return 0 if pkgfile($pkgs, $need) ne ''; + } elsif ($type eq 'cli-options') { + $had_cli_options = 1; + # If cli-options is empty, we know the helper does not + # react to any thing and can always be skipped. + next if $need =~ m/^\s*$/; + # Long options are subject to abbreviations so it is + # very difficult to implement this optimization with + # long options. + return 0 if $Debian::Debhelper::DH::SequenceState::unoptimizable_option_bundle; + $need =~ s/(?:^|\s)BUILDSYSTEM(?:\s|$)/${\UNSKIPPABLE_CLI_OPTIONS_BUILD_SYSTEM}/; + my @behavior_options = split(qr/\Q|\E/, $need); + for my $opt (@behavior_options) { + return 0 if exists($Debian::Debhelper::DH::SequenceState::seen_options{$opt}); + } + } elsif ($type eq 'buildsystem') { + require Debian::Debhelper::Dh_Buildsystems; + my $system = Debian::Debhelper::Dh_Buildsystems::load_buildsystem(undef, $need); + return 0 if defined($system); + } elsif ($type eq 'internal') { + if ($need eq 'bug#950723') { + $all_pkgs //= [ getpackages() ]; + push(@{$all_pkgs}, map { "${_}@"} getpackages()); + push(@packages, map { "${_}@"} @packages); + } elsif ($need eq 'rrr') { + my $req = Debian::Debhelper::Dh_Lib::root_requirements(); + return 0 if $req ne 'none'; + } else { + warning('Broken internal NOOP hint; should not happen unless someone is using implementation details'); + error("Unknown internal NOOP type hint in ${command}: ${need}"); + } + } else { + # Unknown hint - make no assumptions + return 0; + } + } + return 0 if not $had_cli_options and %Debian::Debhelper::DH::SequenceState::seen_options; + return 1; + } +} + +sub _active_packages_for_command { + my ($command, $all_packages, $arch_packages, $indep_packages) = @_; + my $command_opts_ref = $Debian::Debhelper::DH::SequenceState::command_opts{$command}; + my $selection = $all_packages; + if (grep { $_ eq '-i'} @{$command_opts_ref}) { + if (grep { $_ ne '-a'} @{$command_opts_ref}) { + $selection = $indep_packages; + } + } elsif (grep { $_ eq '-a'} @{$command_opts_ref}) { + $selection = $arch_packages; + } + return @{$selection}; +} + +1; |