diff options
Diffstat (limited to 'dh_assistant')
-rwxr-xr-x | dh_assistant | 576 |
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; |