summaryrefslogtreecommitdiffstats
path: root/dh_assistant
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xdh_assistant576
1 files changed, 576 insertions, 0 deletions
diff --git a/dh_assistant b/dh_assistant
new file mode 100755
index 0000000..27b8703
--- /dev/null
+++ b/dh_assistant
@@ -0,0 +1,576 @@
+#!/usr/bin/perl
+
+=head1 NAME
+
+dh_assistant - tool for supporting debhelper tools and provide introspection
+
+=cut
+
+use strict;
+use warnings;
+use Debian::Debhelper::Dh_Lib;
+use JSON::PP ();
+
+=head1 SYNOPSIS
+
+B<dh_assistant> B<I<command>> [S<I<additional options>>]
+
+=head1 DESCRIPTION
+
+B<dh_assistant> is a debhelper program that provides introspection into the
+debhelper stack to assist third-party tools (e.g. linters) or third-party
+debhelper implementations not using the debhelper script API (e.g., because
+they are not written in Perl).
+
+=head1 COMMANDS
+
+The B<dh_assistant> supports the following commands:
+
+=head2 active-compat-level (JSON)
+
+B<Synopsis>: B<dh_assistant> B<active-compat-level>
+
+Outputs information about which compat level the package is using.
+
+For packages without valid debhelper compatibility information (whether missing, ambiguous,
+not supported or simply invalid), this command operates on a "best effort" basis and may abort
+when error instead of providing data.
+
+The returned JSON dictionary contains the following key-value pairs:
+
+=over 4
+
+=item active-compat-level
+
+The compat level that debhelper will be using. This is the same as B<DH_COMPAT> when present
+or else B<declared-compat-level>. This can be B<null> when no compat level can be detected.
+
+=item declared-compat-level
+
+The compat level that the package declared as its default compat level. This can be B<null>
+if the package does not declare any compat level at all.
+
+=item declared-compat-level-source
+
+Defines how the compat level was declared. This is null (for the same reason as
+B<declared-compat-level>) or one of:
+
+=over 4
+
+=item debian/compat
+
+The compatibility level was declared in the first line F<debian/compat> file.
+
+=item Build-Depends: debhelper-compat (= <C>)
+
+The compatibility was declared in the F<debian/control> via a build dependency on the
+B<< debhelper-compat (= <C>) >> package in the B<Build-Depends> field. In the output,
+the B<C> is replaced by the actual compatibility level. A full example value would be:
+
+ Build-Depends: debhelper-compat (= 13)
+
+=back
+
+=back
+
+=head2 supported-compat-levels (JSON, CRFA)
+
+B<Synopsis>: B<dh_assistant> B<supported-compat-levels>
+
+Outputs information about which compat levels, this build of debhelper knows
+about.
+
+This command accepts no options or arguments.
+
+=head2 which-build-system (JSON)
+
+B<Synopsis>: B<dh_assistant> B<which-build-system> [S<I<build step>>] [S<I<build system options>>]
+
+Output information about which build system would be used for a particular build step. The build step
+must be one of B<configure>, B<build>, B<test>, B<install> or B<clean> and must be the first argument
+after B<which-build-system> when provided. If omitted, it defaults to B<configure> as it is the
+most reliable step to use auto-detection on in a clean source directory. Note that build steps do not
+always agree when using auto-detection - particularly if the B<configure> step has not been run.
+
+Additionally, the B<clean> step can also provide "surprising" results for builds that rely on
+a separate build directory. In such cases, debhelper will return the first build system that
+uses a separate build directory rather than the one build system that B<configure> would detect.
+This is generally a cosmetic issue as both build systems are all basically a glorified
+B<rm -fr builddir> and more precise detection is functionally irrelevant as far as debhelper is
+concerned.
+
+The option accepts all debhelper build system arguments - i.e., options you can pass to all of
+the B<dh_auto_*> commands plus (for the B<install> step) the B<--destdir> option. These options
+affect the output and auto-detection in various ways. Passing B<-S> or B<--buildsystem>
+overrides the auto-detection (as it does for B<dh_auto_*>) but it still provides introspection
+into the chosen build system.
+
+Things that are useful to know about the output:
+
+=over 4
+
+=item *
+
+The key B<build-system> is the build system that would be used by debhelper for the given
+step (with the given options, debhelper compat level, environment variables and the given
+working directory). When B<-S> and B<--buildsystem> are omitted, this is the result of
+debhelper's auto-detection logic.
+
+The value is valid as a parameter for the B<--buildsystem> option.
+
+The special value B<none> is used to denote that no build system would be used. This value
+is not present in B<--list> parameter for the B<dh_auto_*> commands, but since debhelper/12.9
+the value is accepted for the B<--buildsystem> option.
+
+Note that auto-detection is subject to limitations in regards to third-party build systems.
+While debhelper I<does> support auto-detecting some third-party build systems, they must be
+installed for the detection to work. If they are not installed, the detection logic silently
+skips that build system (often resulting in B<build-system> being B<none> in the output).
+
+=item *
+
+The B<build-directory> and B<buildpath> values serve different but related purposes. The
+B<build-directory> generally mirrors the B<--builddirectory> option where as B<buildpath>
+is the output directory that debhelper will use. Therefore the former will often be null
+when B<--builddirectory> has not been passed while the latter will generally not be null
+(except when B<build-system> is B<none>).
+
+=item *
+
+The B<dest-directory> (B<--destdir>) is undefined for all build steps except the B<install> build
+step (will be output as null or absent). For the same reason, B<--destdir> should only be
+passed for B<install> build step.
+
+Note that if not specified, this value is currently null by default.
+
+=item *
+
+The B<parallel> value is subject to B<DEB_BUILD_OPTIONS>. Notably, if that does not include
+the B<parallel> keyword, then B<parallel> field in the output will always be 1.
+
+=item *
+
+Most fields in the output I<can> be null. Particular if there is no build system is detected
+(or when B<--buildsystem=none>). Additionally, many of the fields can be null even if there
+is a build system if the build system does not use/set/define that variable.
+
+=back
+
+=head2 detect-hook-targets (JSON)
+
+B<Synopsis>: B<dh_assistant> B<detect-hook-targets>
+
+Detects possible override targets and hook targets that L<dh(1)> might use (provided that the
+relevant command is in the sequence).
+
+The detection is based on scanning the rules file for any target that I<might look> like a hook
+target and can therefore list targets that are in fact not hook targets (or are but will never
+be triggered for other reasons).
+
+The detection uses a similar logic for scanning the rules file and is therefore subject to
+makefile conditionals (i.e., the truth value of makefile conditionals can change whether a hook
+target is visible in the output of this command). In theory, you would have to setup up the
+environment to look like it would during a build for getting the most accurate output. Though,
+a lot of packages will not have conditional hook targets, so the "out of the box" behaviour
+will work well in most cases.
+
+The output looks something like this:
+
+ {
+ "commands-not-in-path": [
+ "dh_foo"
+ ],
+ "hook-targets": [
+ {
+ "command": "dh_strip_nondeterminism",
+ "is-empty": true,
+ "package-section-param": null,
+ "target-name": "override_dh_strip_nondeterminism"
+ },
+ {
+ "command": "dh_foo",
+ "is-empty": false,
+ "package-section-param": "-a",
+ "target-name": "override_dh_foo-arch"
+ }
+ ]
+ }
+
+In more details:
+
+=over 4
+
+=item commands-not-in-path
+
+This attribute lists all the commands related to hook targets, which B<dh_assistant> could B<not>
+find in PATH. These are usually caused by either the command not being installed on the system
+where B<dh_assistant> is run or by the command not existing at all.
+
+If you are using this command to verify an hook target is present, please double check that the
+command is spelled correctly.
+
+=item hook-targets
+
+List over hook targets found along with additional information about them.
+
+=over 4
+
+=item command
+
+Attribute that lists which command this hook target is related too.
+
+=item target-name
+
+The actual target name detected in the F<debian/rules> file.
+
+=item is-empty
+
+A boolean that determines whether L<dh(1)> will optimize the hook out at runtime (see "Completely empty targets" in
+L<dh(1)>). Note that empty override targets will still cause L<dh(1)> to skip the original command.
+
+=item package-section-param
+
+This attribute defines what package selection parameter should be passed to B<dh_*> commands used
+in the hook target. It can either be B<-a>, B<-i> or (if no parameter should be used) C<null>.
+
+=back
+
+=back
+
+This command accepts no options or arguments.
+
+=head2 log-installed-files
+
+B<Synopsis>: B<dh_assistant> B<< -pI<pkg> >> I<[--on-behalf-of-cmd=dh_foo]> B<path ...>
+
+Mark one or more paths as installed for a given package. This is useful for telling L<dh_missing(1)> that the
+paths have been installed manually.
+
+The B<--on-behalf-of-cmd> option can be used by third-party tools to have B<dh_assistant> list them as the
+installer of the provided paths. The convention is to use the basename of the tool itself as its name
+(e.g. B<dh_install>).
+
+Please keep in mind that:
+
+=over 4
+
+=item *
+
+B<No> glob or substitution expansion is done by B<dh_assistant> on the provided paths. If you want to use globs,
+have the shell perform the expansion first.
+
+=item *
+
+Paths must be given as relative to the source root directory (e.g., F<debian/tmp/...>)
+
+=item *
+
+You I<can> provide a directory. If you do, the directory and anything recursively below it will be considered
+as installed. Note that it is fine to provide the directory even if paths inside of it has been excluded as long
+as the directory is fully "covered".
+
+=item *
+
+Do not worry about providing the same filename twice in different invocations to B<dh_assistant> due to B<-arch> /
+B<-indep> overrides. While it will be recorded multiple internally, L<dh_missing(1)> will deduplicate when it
+parses the records.
+
+=back
+
+Note this command only I<marks> paths as installed. It does not actually install them - the caller should ensure
+that the paths are in fact handled (or installed).
+
+=head1 COMMAND TAGS
+
+Most commands have one or more of the following "tags" associated with them. Their
+meaning is defined here.
+
+=over 4
+
+=item JSON
+
+The command provides JSON output. See L</JSON OUTPUT> for details.
+
+=item CRFA
+
+I<Mnemonic "Can be Run From Anywhere">
+
+Most commands must be run inside a source package root directory (a directory
+containing F<debian/control>) because debhelper will need the package metadata
+to lookup the information. Any command with this tag are exempt from this
+requirement and is expected to work regardless of where they are run.
+
+=back
+
+=head1 JSON OUTPUT
+
+Most commands uses JSON format as output. Consumers need to be aware that:
+
+=over 4
+
+=item *
+
+Additional keys may be added at any time. For backwards compatibility, the absence
+of a key should in general be interpreted as null unless another default is documented
+or would be "obvious" for that case.
+
+=item *
+
+Many keys can be null/undefined in special cases. As an example, some information may
+be unavailable when this command is run directly from the debhelper source (git repository).
+
+=back
+
+The output will be prettified when stdout is detected as a terminal. If
+you need to pipe the output to a pager/file (etc.) and still want it
+prettified, please use an external JSON formatter. An example of this:
+
+ dh_assistant supported-compat-levels | json_pp | less
+
+=cut
+
+my $JSON_ENCODER = JSON::PP->new->utf8;
+
+# Prettify if we think the user is reading this.
+$JSON_ENCODER = $JSON_ENCODER->pretty->space_before(0)->canonical if -t STDOUT;
+
+# We never use the log file for this tool
+inhibit_log();
+
+my %COMMANDS = (
+ 'help' => \&_do_help,
+ '-h' => \&_do_help,
+ '--help' => \&_do_help,
+ 'active-compat-level' => \&active_compat_level,
+ 'supported-compat-levels' => \&supported_compat_levels,
+ 'which-build-system' => \&which_build_system,
+ 'detect-hook-targets' => \&detect_hook_targets,
+ 'log-installed-files' => \&log_installed_files_cmd,
+);
+
+my ($COMMAND) = shift(@ARGV);
+for my $arg (@ARGV) {
+ if ($arg eq '--help' or $arg eq '-h') {
+ $COMMAND = 'help';
+ last;
+ }
+}
+
+
+sub _do_help {
+ my $me = basename($0);
+ print <<"EOF";
+${me}: Tool for supporting debhelper tools and provide introspection
+
+Usage: ${me} <command> [... addition arguments or options ...]
+
+The following commands are available:
+ help Show this help
+ active-compat-level Output information about which compat level is declared/active (JSON)
+ supported-compat-levels Output information about supported compat levels (JSON, CRFA)
+ which-build-system Determine which build system will be used (JSON)
+ detect-hook-targets Detect and output possible override and hook targets (JSON)
+ log-installed-files Mark one or more paths as "installed" so dh_missing is aware (BLD)
+
+Command tags:
+
+ * JSON The command provides JSON output.
+ * CRFA Command does not need to be run from a package source directory
+ (Mnemonic "Can be Run From Anywhere")
+ * BLD The command is intended to be used as a part of a package build.
+ It may leave artifacts behind that will need a dh_clean invocation to remove.
+
+
+Its primary purpose is to provide support for third-party debhelper implementations
+not using the debhelper script API or provide introspection for third-party tools
+(e.g., linters). Unless stated otherwise, commands must be run inside a source
+package root directory - that is, the directory containing "debian/control".
+
+Most commands use JSON output. When stdout is a TTY, the JSON will be prettified.
+See the manpage if you want formatting in other cases.
+EOF
+ return;
+}
+
+sub _assert_debian_control_exists {
+ return if -f 'debian/control';
+ require Cwd;
+ my $cwd = Cwd::getcwd();
+ warning("$cwd does not look like a package source directory (expected $cwd/debian/control to exist and be a file)");
+ error("$COMMAND must be run inside a package source directory");
+ return;
+}
+
+sub _output {
+ my ($kvpairs) = @_;
+ print $JSON_ENCODER->encode($kvpairs);
+ return;
+}
+
+sub active_compat_level {
+ if (@ARGV) {
+ error("$COMMAND: No arguments supported (please remove everything after the command)");
+ }
+ _assert_debian_control_exists();
+ my ($active_compat, $declared_compat, $declared_compat_source) = Debian::Debhelper::Dh_Lib::get_compat_info();
+ if (not defined($declared_compat_source)) {
+ $declared_compat = undef;
+ $active_compat = undef if not exists($ENV{DH_COMPAT});
+ }
+ my %compat_info = (
+ 'active-compat-level' => $active_compat,
+ 'declared-compat-level' => $declared_compat,
+ 'declared-compat-level-source' => $declared_compat_source,
+ );
+ _output(\%compat_info);
+ return;
+}
+
+sub supported_compat_levels {
+ if (@ARGV) {
+ error("$COMMAND: No arguments supported (please remove everything after the command)");
+ }
+ my %compat_levels = (
+ 'MIN_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::MIN_COMPAT_LEVEL,
+ 'LOWEST_NON_DEPRECATED_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::LOWEST_NON_DEPRECATED_COMPAT_LEVEL,
+ 'LOWEST_VIRTUAL_DEBHELPER_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::LOWEST_VIRTUAL_DEBHELPER_COMPAT_LEVEL,
+ 'MAX_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::MAX_COMPAT_LEVEL,
+ 'HIGHEST_STABLE_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::HIGHEST_STABLE_COMPAT_LEVEL,
+ 'MIN_COMPAT_LEVEL_NOT_SCHEDULED_FOR_REMOVAL' => Debian::Debhelper::Dh_Lib::MIN_COMPAT_LEVEL_NOT_SCHEDULED_FOR_REMOVAL,
+ );
+ _output(\%compat_levels);
+ return;
+}
+
+sub which_build_system {
+ my ($opt_buildsys, $destdir);
+ my $first_argv = @ARGV ? $ARGV[0] : '';
+ my %options = (
+ # Emulate dh_auto_install's --destdir
+ "destdir=s" => \$destdir,
+ );
+ _assert_debian_control_exists();
+ # We never want the build system initialization to modify anything (e.g. create "HOME")
+ $dh{NO_ACT} = 1;
+ require Debian::Debhelper::Dh_Buildsystems;
+ Debian::Debhelper::Dh_Buildsystems::buildsystems_init(options => \%options);
+ my @non_options = grep { !m/^-/ } @ARGV;
+ my $step = @non_options ? $non_options[0] : 'configure';
+ if (@non_options && $first_argv =~ m/^-/) {
+ error("$COMMAND: If the build step is provided, it must be before any options");
+ }
+ if (@non_options > 1) {
+ error("$COMMAND: At most one positional argument is supported");
+ }
+ if (defined($destdir) and $step ne 'install') {
+ warning("$COMMAND: --destdir is not defined for build step \"$step\". Ignoring option")
+ }
+ {
+ no warnings qw(once);
+ $opt_buildsys = $Debian::Debhelper::Dh_Buildsystems::opt_buildsys;
+ }
+ my $build_system = Debian::Debhelper::Dh_Buildsystems::load_buildsystem($opt_buildsys, $step);
+ my %result = (
+ 'build-system' => defined($build_system) ? $build_system->NAME : 'none',
+ 'for-build-step' => $step,
+ 'source-directory' => defined($build_system) ? $build_system->get_sourcedir : undef,
+ 'build-directory' => defined($build_system) ? $build_system->get_builddir : undef,
+ 'dest-directory' => defined($build_system) ? $destdir : undef,
+ 'buildpath' => defined($build_system) ? $build_system->get_buildpath : undef,
+ 'parallel' => defined($build_system) ? $build_system->get_parallel : undef,
+ 'upstream-arguments' => $dh{U_PARAMS},
+ );
+ _output(\%result);
+ return;
+}
+
+sub _in_path {
+ my ($cmd) = @_;
+ for my $dir (split(':', $ENV{PATH})) {
+ return 1 if -x "${dir}/${cmd}";
+ }
+ return 0;
+}
+
+sub detect_hook_targets {
+ if (@ARGV) {
+ error("$COMMAND: No arguments supported (please remove everything after the command)");
+ }
+ _assert_debian_control_exists();
+ require Debian::Debhelper::SequencerUtil;
+ Debian::Debhelper::SequencerUtil::rules_explicit_target('does-not-matter');
+ my ($explicit_targets, %result, @targets, @unverifiable_commands, %seen_cmds);
+ {
+ no warnings qw(once);
+ $explicit_targets = \%Debian::Debhelper::SequencerUtil::EXPLICIT_TARGETS;
+ }
+ while (my ($target, $non_empty) = each(%{$explicit_targets})) {
+ next if $target !~ m{^(?:execute_before_|execute_after_|override_)(\S+?)(-indep|-arch)?$};
+ my ($command, $archness) = ($1, $2);
+ my $param;
+ if ($archness) {
+ $param = ($archness eq '-arch') ? '-a' : '-i' ;
+ }
+ my $target_info = {
+ 'target-name' => $target,
+ 'command' => $command,
+ 'package-section-param' => $param,
+ 'is-empty' => $non_empty ? JSON::PP::false : JSON::PP::true,
+ };
+ push(@targets, $target_info);
+ push(@unverifiable_commands, $command) if not exists($seen_cmds{$command}) and not _in_path($command);
+ $seen_cmds{$command} = 1;
+ }
+ $result{'hook-targets'} = \@targets;
+ $result{'commands-not-in-path'} = \@unverifiable_commands;
+ _output(\%result);
+}
+
+sub log_installed_files_cmd {
+ my $on_behalf_of = 'manually-via-dh_assistant';
+ init(
+ options => {
+ 'on-behalf-of-cmd=s' => \$on_behalf_of,
+ },
+ inhibit_log => 1,
+ );
+ if (index($on_behalf_of, '/') >= 0) {
+ error('The value for --on-behalf-of-cmd must not contain slashes');
+ }
+ if (@{$dh{DOPACKAGES}} != 1) {
+ error('The log-installed-files command must act on exactly one package (use -p<pkg> to define which)');
+ }
+ my $package = $dh{DOPACKAGES}[0];
+ for my $arg (@ARGV) {
+ $arg =~ tr:/:/:s;
+ if (! -e $arg) {
+ warning("The path ${arg} does not exist - double check it is correct. Note: it will recorded anyway.");
+ }
+ }
+ log_installed_files({
+ 'package' => $package,
+ 'tool_name' => $on_behalf_of,
+ }, @ARGV);
+}
+
+if (not defined($COMMAND)) {
+ error('Usage: ' . basename($0) . ' <command>');
+}
+my $handler = $COMMANDS{$COMMAND};
+if (not defined($handler)) {
+ warning("Arguments/options must not be the first argument (except for --help)")
+ if $COMMAND =~ m/^-/;
+ error("Unknown command: $COMMAND");
+}
+
+$handler->();
+
+=head1 SEE ALSO
+
+L<debhelper(7)>
+
+This program is a part of debhelper.
+
+=cut
+
+1;