#!/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;
					} else {
						if (defined($current_target)) {
							if (m/^#/) {
								# Check if target has commands to execute
								if (m/^#\s*(commands|recipe) to execute/) {
									$EXPLICIT_TARGETS{$current_target} = 1;
								}
							} 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};
}

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;
