diff options
Diffstat (limited to 'dh_assistant')
-rwxr-xr-x | dh_assistant | 1449 |
1 files changed, 1449 insertions, 0 deletions
diff --git a/dh_assistant b/dh_assistant new file mode 100755 index 0000000..3d3337f --- /dev/null +++ b/dh_assistant @@ -0,0 +1,1449 @@ +#!/usr/bin/perl + +=head1 NAME + +dh_assistant - tool for supporting debhelper tools and provide introspection + +=cut + +use strict; +use warnings; +use constant EXIT_CODE_LINT_ISSUES_FOUND => 2; +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 (AJSON) + +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 X-DH-Compat: <C> + +The compatibility was declared in the F<debian/control> via a the B<X-DH-Compat> +field. In the output, the B<C> is replaced by the actual compatibility level. +A full example value would be: + + X-DH-Compat: 15 + +=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 (= 15) + +=back + +=back + +=head2 supported-compat-levels (AJSON, 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 (AJSON) + +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 (AJSON) + +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, + "filename": "debian/rules", + "target-name": "override_dh_strip_nondeterminism" + }, + { + "command": "dh_foo", + "is-empty": false, + "package-section-param": "-a", + "filename": "debian/rules", + "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>. + +=item filename + +This attribute reports which file the target was found it. In most cases, this will always be "debian/rules" +though in case of include files, the target could appear in an include file. Note this attribute is not +super reliable as L<make(1)> only reports it for targets with a "recipe" (targets with commands inside +them). When B<make> does not provide the filename, B<dh_assistant> blindly assumes the filename is +"debian/rules" (as overrides via includes is not a commonly used feature). + +Note this accuracy of this attribute is limited about what data B<dh_assistant> can read out from the +following command: + + LC_ALL=C make -Rrnpsf debian/rules debhelper-fail-me 2>/dev/null + +=back + +=back + +This command accepts no options or arguments. + + +=head2 detect-unknown-hook-targets (AJSON, LINT) + +B<Synopsis>: B<dh_assistant> B<detect-unknown-hook-targets> [--output-format=json] [command-options] + +Detects unknown and possibly misspelled override targets and hook targets in F<debian/rules> that +will most likely not be used by L<dh(1)>. + +This command differs from B<detect-hook-targets> subtly in the scope. The B<detect-hook-targets> +will list all targets that looks like hook targets whether they are applicable or not. This +command show all hook targets, for which a command cannot be found in any sequence. Accordingly, +this command is better for linting purposes whereas B<detect-hook-targets> is better if you want +to know which hook targets are present. All the limitations listed in B<detect-hook-targets> +about scanning the rules file apply equally to this command. + +This command will attempt will attempt to load any sequence add-on listed via build-dependencies +and therefore these must be installed. Additional modules can be passed via B<--with> like with +L<dh(1)> as needed. + +This command will also need one of the following perl modules to be available: +L<Text::Levenshtein>, L<Text::LevenshteinXS>, L<Text::Levenshtein::XS>. The first one can be +installed via B<apt install libtext-levenshtein-perl>. + +The text output is intended for human consumption and should be self-explanatory. Since it is +not stable, it will not be documented. The JSON output looks something like this: + + { + "unknown-hook-targets": [ + { + "target-name": "execute_before_dh_instlal", + "filename": "debian/rules", + "candidates": [ + "execute_before_dh_install" + ] + } + ] + } + +In more details: + +=over 4 + +=item unknown-hook-targets + +List of all the unknown hook targets found along with additional information about them. + +=over 4 + +=item target-name + +The actual target name detected in the file (usually F<debian/rules>). + +=item filename + +This attribute reports which file the target was found it. In most cases, this will always be "debian/rules" +though in case of include files, the target could appear in an include file. Note this attribute is not +super reliable as L<make(1)> only reports it for targets with a "recipe" (targets with commands inside +them). When B<make> does not provide the filename, B<dh_assistant> blindly assumes the filename is +"debian/rules" (as overrides via includes is not a commonly used feature). + +Note this accuracy of this attribute is limited about what data B<dh_assistant> can read out from the +following command: + + LC_ALL=C make -Rrnpsf debian/rules debhelper-fail-me 2>/dev/null + +=item candidates + +When not null and not empty, each element in this list are names for likely candidates for the +"correct" name of this target. + +=item filename + +=back + +=item issues + +If present, then it is a list of one or more reasons why this output is definitely incomplete. Each element +in the list is an object with the following keys: + +=over 4 + +=item issue + +A key defining the issue. Currently, it is always B<load-addon>, which signals that B<dh_assistant> could +not load the add-on listed in the B<addon> key. + +Parsers should assume new issue types may appear in the future. + +=item addon + +If present, it defines the name of a B<dh> sequence add-on that is related to the failure. + +=back + +=back + +This command accepts the following options: + +=over 4 + +=item B<--output-format=>I<FORMAT> + +Request a certain type of output format. Valid values are B<text> or B<json>. + +The text format is intended for human consumption and may change between versions without any +regard for machine consumption. If you want to use this command for machine consumption, please +use the JSON format. + +=item B<--no-linter-exit-code>, B<--linter-exit-code> + +These options control whether the command should exit with the linter exit code (2) or not (0) +when an unknown target is found. By default, it uses the linter exit code when an unknown target +is found. + +=item B<--with> I<addon>, B<--without> I<addon> + +These options behave the same as the L<dh(1)> options with the same name. + +=back + +=head2 list-commands (RJSON) + +B<Synopsis>: B<dh_assistant> B<list-commands> [--output-format=json] [command-options] + +Load all B<dh> sequence add-ons and extract a full list of all commands that will be invoked across +all sequences. The command makes no attempt to filter out commands that will not be run due to +override targets or due to certain sequences not being run (by B<dh> or at all). + +As the command will attempt to load all plugins, they must be installed. + +The text output is intended for human consumption and should be self-explanatory. Since it is +not stable, it will not be documented. The JSON output looks something like this: + + { + "commands": [ + { + "command": "dh_auto_build" + }, + { + "command": "dh_auto_clean" + }, + [... more commands listed here... ] + ], + "issues": [ + { + "issue": "load-addon", + "addon": "foo" + } + ] + } + +=over 4 + +=item commands + +The top level key containing the list of all commands. Each element in the list are an object and +can have the following keys: + +=over 4 + +=item command + +The name of the command. + +While most commands are resolved via PATH, a sequence add-on could register a command via a full path +(by passing the path search). If so, the command provided in this output will also use the full path. + +=back + +=item issues + +If present, then it is a list of one or more reasons why this output is definitely incomplete. Each element +in the list is an object with the following keys: + +=over 4 + +=item issue + +A key defining the issue. Currently, it is always B<load-addon>, which signals that B<dh_assistant> could +not load the add-on listed in the B<addon> key. + +Parsers should assume new issue types may appear in the future. + +=item addon + +If present, it defines the name of a B<dh> sequence add-on that is related to the failure. + +=back + +=back + +This command accepts the following options: + +=over 4 + +=item B<--output-format=>I<FORMAT> + +Request a certain type of output format. Valid values are B<text> or B<json>. + +The text format is intended for human consumption and may change between versions without any +regard for machine consumption. If you want to use this command for machine consumption, please +use the JSON format. + +=item B<--with> I<addon>, B<--without> I<addon> + +These options behave the same as the L<dh(1)> options with the same name. + +=back + +=head2 list-guessed-dh-config-files (AJSON) + +B<Synopsis>: B<dh_assistant> B<list-guessed-dh-config-files> [command-options] + +Load all B<dh> sequence add-ons, determine the full list of commands could be used by this +source package and for each command used, then attempt to I<guess> which "config files" +these commands are interested in. + +Note this command only guesses "per command config files". Standard global config files +such as F<debian/control>, F<debian/rules>, and F<debian/compat> are not included in this +output. + +As the command name implies, the resulting output is not a full list (and will never be). +The B<dh_assistant> tool have to derive this from optional metadata that commands can +choose to provide and B<dh_assistant> has no means to validate that this metadata is up +to date. + +As the command will attempt to load all plugins, they must be installed. + +The text output is intended for human consumption and should be self-explanatory. Since it is +not stable, it will not be documented. The JSON output looks something like this: + + { + "config-files": [ + { + "commands": [ + { + "command": "dh_autoreconf_clean" + } + ], + "file-type": "pkgfile", + "pkgfile": "autoreconf.before" + }, + { + "commands": [ + { + "command": "dh_installgsettings" + } + ], + "file-type": "pkgfile", + "pkgfile": "gsettings-override" + }, + # [ ... more entries here ...] + ], + "issues": [ + { + "issue": "load-addon", + "addon": "foo" + } + ] + } + + +=over 4 + +=item config-files + +The top level key containing the list of all config-files. Each element in the list are an object and +can have the following keys: + +=over 4 + +=item file-type + +The type of config file detected. At the time of writing, this will always be B<pkgfile>. However, +other values may appear in the future. + +The B<pkgfile> key means that the config file is a B<debhelper pkgfile> (named after the B<pkgfile> sub +in L<Debian::Debhelper::Dh_Lib> that locates the file). + +=item pkgfile + +When B<file-type> is B<pkgfile>, this key defines the name stem of the B<pkgfile>. An example, this will +be B<install> for L<dh_install(1)>'s config file and B<docs> for L<dh_installdocs(1)>'s config file. + +When B<file-type> is B<not> B<pkgfile>, then this key will be absent. + +Typically names for these files are: + + debian/PKGFILE + debian/PACKAGE.PKGFILE + +However, there are more variants caused by B<--name> plus architecture specific suffixes. + +=item internal + +This key may exist and any value for it is not standardized. Use at own peril. + +It used for document certain specific implementation details such as bug compatibility and may change +as the situation changes. + +=item commands + +This key will be a list with each element in it being an object with the following keys: + +=over 4 + +=item command + +Name of the command that is interested in this config file. Multiple commands can be interested in the same +config file. An example of this would be B<dh_installinit>, B<dh_installsystemd> and B<dh_installtmpfiles>, +which all reacts to (the now) deprecated B<tmpfile> pkgfile. In the particular case, only one command reacts +to the file for a given compat level (but that information is not available to B<dh_assistant> and therefore +is not available in this output either). + +=back + +=back + +=item issues + +If present, then it is a list of one or more reasons why this output is definitely incomplete. Each element +in the list is an object with the following keys: + +=over 4 + +=item issue + +A key defining the issue. Currently, it is always B<load-addon>, which signals that B<dh_assistant> could +not load the add-on listed in the B<addon> key. + +Parsers should assume new issue types may appear in the future. + +=item addon + +If present, it defines the name of a B<dh> sequence add-on that is related to the failure. + +=back + +=back + +This command accepts the following options: + +=over 4 + +=item B<--with> I<addon>, B<--without> I<addon> + +These options behave the same as the L<dh(1)> options with the same name. + +=back + +=head2 log-installed-files (BLD) + +B<Synopsis>: B<dh_assistant> B<log-installed-files> 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). + +=head2 restore-file-on-clean (BLD) + +B<Synopsis>: B<dh_assistant> B<restore-file-on-clean> B<FILE ...> + +This command will take a backup of listed files and tell L<dh_clean(1)> to restore them when it runs. + +Note that generally you do not need to restore modified files on clean. Often you can get away with just +removing them if they are regenerated anyway (which is the most common case for files being modified during +builds). Use this command when something taints a file and the build does not cope with the file being +removed. + +The file is stored in B<debian/.debhelper>. If you remove this directory manually without calling +L<dh_clean(1)> then your B<dh_assistant> provided backup is gone permanently and the restore will never +occur. At this point, only a version control system or another backup can restore the files. + +The command has the following limitations: + +=over 4 + +=item No thread-safety - concurrency will corrupt the restore + +The command relies on updating an internal index and concurrent writes will cause it to be corrupt. + +While most B<dh_*> commands does not use the underlying function, any of them could do so. Avoid running +another B<dh_*> command while B<dh_assistant> processes this command (especially running multiple concurrent +instances of B<dh_assistant restore-file-on-clean> is asking for corruption!). + +=item Files only, not directories nor symlinks to files + +This command will only restore files; not directories or symlinks to files. It will reject any non-files. + +Additionally, if the directory containing the file is removed, the restore will fail (as B<debhelper> +does not track the directory, it cannot restore it reliably). If this happens, you can do a B<mkdir> +to restore the directory and run L<dh_clean(1)> again to get the files back. After that, consider +what went wrong and whether you are using the correct tool(s). + +=item Strict file names + +All filenames must be relative to the package root (without using the B<./> prefix). No hidden files (that +is any file starting with a period B<.>) and no version control directories (such as B<CVS>). The checks +are best effort. + +These checks are here to ensure you do not accidentally trash important data that would help you undo +mistakes. + +=item Heavy duty + +The command takes a B<full copy> of all files you pass it. This is fine for a handful of small files, +which is the intended use-case. If you find yourself passing 10+ files or very large files, you might +be applying a sledgehammer where you needed a different tool. + +=back + +=head2 supports (CFFA) + +B<Synopsis>: B<dh_assistant> B<supports> B<COMMAND> + +This command is a scripting aid to programmatically determine whether B<dh_assistant> knows about a given +subcommand. Pass the name of a subcommand and this command will exit successfully if the subcommand was known +and unsuccessfully otherwise. + +=head1 COMMAND TAGS + +Most commands have one or more of the following "tags" associated with them. Their +meaning is defined here. + +=over 4 + +=item AJSON + +The command always provides JSON output. See L</JSON OUTPUT> for details. + +=item OJSON + +The command *can* provide JSON output via B<--output-format=json>, but does not +do so by default. See L</JSON OUTPUT> for details when using B<--output-format=json>. + +=item LINT + +The command is or can be used for linting purposes. This command will exit with code 2 when an important +issue is found. + +Note that commands may have options that redefine what is considered an "important" issue. + +=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. + +=item BLD + +The command is intended to be used as a part of a package build. It may leave +artifacts behind that will need a L<dh_clean(1)> invocation to remove. + +=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(); +$Debian::Debhelper::Dh_Lib::PARSE_DH_SEQUENCE_INFO = 1; + +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, + 'detect-unknown-hook-targets' => \&detect_unknown_hook_targets, + 'list-commands' => \&list_commands, + 'list-guessed-dh-config-files' => \&list_guessed_dh_config_files, + 'log-installed-files' => \&log_installed_files_cmd, + 'restore-file-on-clean' => \&dh_assistant_restore_file_on_clean, + 'supports' => \&supports, +); + +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 (AJSON) + supported-compat-levels Output information about supported compat levels (AJSON, CRFA) + which-build-system Determine which build system will be used (AJSON) + detect-hook-targets Detect and output possible override and hook targets (AJSON) + detect-unknown-hook-targets + Detect unknown / typos of known hook targets (RJSON) + list-commands List all commands across all sequences (RJSON) + list-guessed-dh-config-files + List guessed "config files" for debhelper commands (AJSON) + log-installed-files Mark one or more paths as "installed" so dh_missing is aware (BLD) + restore-file-on-clean Mark one or more files as to be restored by dh_clean (BLD) + supports Script aid: Test whether dh_assistant knows a particular command (CRFA) + +Command tags: + + * AJSON The command always provides JSON output. + * RJSON The command *can* provide JSON output via --output-format=json. + * LINT The command is or can be used for linting purposes. This command will exit with code 2 + when an important issue is found. + * 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 or can provide 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 _load_hook_targets { + require Debian::Debhelper::SequencerUtil; + Debian::Debhelper::SequencerUtil::rules_explicit_target('does-not-matter'); + my ($explicit_targets); + { + no warnings qw(once); + $explicit_targets = \%Debian::Debhelper::SequencerUtil::EXPLICIT_TARGETS; + } + return $explicit_targets; +} + +sub _hook_target_variants { + my ($name) = @_; + my @base = ( + "override_${name}", + "execute_before_${name}", + "execute_after_${name}", + ); + return map { + ( + $_, + "${_}-arch", + "${_}-indep", + ) + } @base; +} + + +sub _load_levenshtein { + my @modules = (qw( + Text::LevenshteinXS + Text::Levenshtein::XS + Text::Levenshtein + )); + my $err; + for my $module (@modules) { + my $distance_func = eval "use $module (); \\&${module}::distance"; + $err = $@; + if (defined($distance_func)) { + return $distance_func; + } + } + my $module_names = join(', ', @modules); + warning("Could not load any of the modules ${module_names}"); + warning("Usually, `apt install libtext-levenshtein-perl` will fix this problem."); + error("This subcommand requires one of the following modules to be available: ${module_names}. Last failure was: $@"); +} + + +sub _all_sequence_commands { + my ($forgive_errors, @addon_requests) = @_; + my ($sequences, @all_commands, @unloadable); + Debian::Debhelper::SequencerUtil::load_sequence_addon('root-sequence', 'both'); + my @addons = Debian::Debhelper::SequencerUtil::compute_selected_addons('binary', @addon_requests); + # Load addons, which can modify sequences. + foreach my $addon (@addons) { + my $addon_name = $addon->{'name'}; + my $addon_type = $addon->{'addon-type'}; + eval { + Debian::Debhelper::SequencerUtil::load_sequence_addon($addon_name, $addon_type); + }; + if (my $err = $@) { + die($err) if not $forgive_errors; + push(@unloadable, $addon_name); + } + } + { + no warnings qw(once); + $sequences = \%Debian::Debhelper::DH::SequenceState::sequences; + } + my %seen; + for my $sequence(values(%{$sequences})) { + my @commands = map {$_->[0]} $sequence->flatten_sequence('both', 0); + for my $command (@commands) { + next if $command =~ m{^(?:debian/rules|create-stamp)}; + next if exists($seen{$command}); + $seen{$command} = 1; + push(@all_commands, $command); + } + } + return \@all_commands, \@unloadable; +} + +sub list_commands { + _assert_debian_control_exists(); + require Getopt::Long; + Getopt::Long::config('no_ignore_case'); + require Debian::Debhelper::SequencerUtil; + my $output_format = "text"; + my (@addon_requests); + my %options=( + "output-format=s" => \$output_format, + "with=s" => sub { + my ($option, $value) = @_; + push(@addon_requests, map { "+${_}" } split(",", $value)); + }, + "without=s" => sub { + my ($option, $value) = @_; + push(@addon_requests, map { "-${_}" } split(",", $value)); + }, + ); + Getopt::Long::GetOptionsFromArray(\@ARGV, %options) + or error("Could not parse the arguments"); + + if (@ARGV) { + my $value = $ARGV[0]; + error("$COMMAND: No non-options supported - please remove ${value}"); + } + + my ($all_commands, $unloadables) = _all_sequence_commands(1, @addon_requests); + if ($output_format eq 'json') { + my @commands_json; + for my $command (sort(@{$all_commands})) { + push(@commands_json, { + 'command' => $command, + }); + } + my %result = ( + "commands" => \@commands_json, + ); + if (@{$unloadables}) { + my @issues; + for my $addon (@{$unloadables}) { + push(@issues, { + "issue" => "load-addon", + "addon" => $addon, + }); + }; + $result{'issues'} = \@issues; + } + _output(\%result); + } elsif ($output_format eq 'text') { + print("Commands present in at least one sequence for this source package (sorted by name):\n"); + for my $command (sort(@{$all_commands})) { + print("\t${command}\n"); + } + if (@{$unloadables}) { + my $addon_names = join(" ", @{$unloadables}); + print("\n"); + warning("Incomplete result. The following sequence add-ons could not be loaded: $addon_names"); + } + } else { + error("Internal error: Missing case for ${output_format}"); + } +} + +sub _extract_annotations { + my ($command) = @_; + my @annotations; + + foreach my $dir (split(':', $ENV{PATH})) { + if (open (my $h, "<", "$dir/$command")) { + while (<$h>) { + if (m/PROMISE: DH NOOP( WITHOUT\s+(.*))?\s*$/) { + if (defined($2)) { + push(@annotations, split(' ', $2)); + } else { + push(@annotations, 'always-skip'); + } + } + if (m/INTROSPECTABLE: CONFIG-FILES\s+(.*)\s*$/) { + push(@annotations, split(' ', $1)); + } + } + close $h; + return @annotations; + } + } + return; +} + +sub list_guessed_dh_config_files { + _assert_debian_control_exists(); + require Getopt::Long; + Getopt::Long::config('no_ignore_case'); + require Debian::Debhelper::SequencerUtil; + my (@addon_requests); + my %options=( + "with=s" => sub { + my ($option, $value) = @_; + push(@addon_requests, map { "+${_}" } split(",", $value)); + }, + "without=s" => sub { + my ($option, $value) = @_; + push(@addon_requests, map { "-${_}" } split(",", $value)); + }, + ); + Getopt::Long::GetOptionsFromArray(\@ARGV, %options) + or error("Could not parse the arguments"); + + if (@ARGV) { + my $value = $ARGV[0]; + error("$COMMAND: No non-options supported - please remove ${value}"); + } + + my ($all_commands, $unloadables) = _all_sequence_commands(1, @addon_requests); + my $pkg_files = {}; + my $bug_950723; + for my $command (@{$all_commands}) { + my @annotations = _extract_annotations($command); + next if not @annotations or $annotations[0] eq 'always-skip'; + for my $annotation (@annotations) { + my $type = 'pkgfile'; + my $need = $annotation; + if ($annotation =~ m/^([a-zA-Z0-9-_]+)\((.*)\)$/) { + ($type, $need) = ($1, $2); + } + if ($type eq 'internal') { + $bug_950723 = 1 if $need eq 'bug#950723'; + } + next if $type ne 'pkgfile' and $type ne 'pkgfile-logged'; + my $key = "pkgfile/${need}"; + my $existing = $pkg_files->{$key}; + if (defined($existing)) { + push(@{$existing->{'commands'}}, { + 'command' => $command, + }); + } else { + $existing = { + 'file-type' => 'pkgfile', + 'pkgfile' => $need, + 'commands' => [{ + 'command' => $command, + }], + }; + $pkg_files->{$key} = $existing; + } + if ($bug_950723) { + $existing->{"internal"}{"bug#950723"} = JSON::PP::true; + } + } + } + + my @config_files = values(%{$pkg_files}); + my %result = ( + "config-files" => \@config_files, + ); + if (@{$unloadables}) { + my @issues; + for my $addon (@{$unloadables}) { + push(@issues, { + "issue" => "load-addon", + "addon" => $addon, + }); + }; + $result{'issues'} = \@issues; + } + _output(\%result); +} + + +sub detect_unknown_hook_targets { + _assert_debian_control_exists(); + my $distance_func = _load_levenshtein(); + require Getopt::Long; + Getopt::Long::config('no_ignore_case'); + require Debian::Debhelper::SequencerUtil; + my $output_format = "text"; + my (@addon_requests, %all_overrides, %unknown_hooks); + my $lint_exit = 1; + my %options=( + "output-format=s" => \$output_format, + "linter-exit-code!" => \$lint_exit, + "with=s" => sub { + my ($option, $value) = @_; + push(@addon_requests, map { "+${_}" } split(",", $value)); + }, + "without=s" => sub { + my ($option, $value) = @_; + push(@addon_requests, map { "-${_}" } split(",", $value)); + }, + ); + + Getopt::Long::GetOptionsFromArray(\@ARGV, %options) + or error("Could not parse the arguments"); + if ($output_format ne 'text' and $output_format ne 'json') { + error("--output-format must be either text or json\n"); + } + if (@ARGV) { + my $value = $ARGV[0]; + error("$COMMAND: No non-options supported - please remove ${value}"); + } + my $explicit_targets = _load_hook_targets(); + my %missing_targets = map { + $_ => 1 + } grep { + $_ ne 'debhelper-fail-me' and !m{^(?:debian/rules|create-stamp)} + } keys(%{$explicit_targets}); + + my ($all_commands, $unloadables) = _all_sequence_commands(1, @addon_requests); + for my $command (@{$all_commands}) { + for my $variant (_hook_target_variants($command)) { + $all_overrides{$variant} = 1; + delete($missing_targets{$variant}); + } + } + my @variants = sort(keys(%all_overrides)); + for my $target (keys(%missing_targets)) { + my @closest_variants; + my $closest_variant_distance = 9999; + for my $variant (@variants) { + next if abs(length($target) - length($variant)) > 3; + my $dist = $distance_func->($target, $variant); + next if $dist > $closest_variant_distance or $dist > 3; + if ($dist < $closest_variant_distance) { + $closest_variant_distance = $dist; + @closest_variants = (); + } + push(@closest_variants, $variant); + } + next if not @closest_variants and $target !~ m{^(?:override|execute_before|execute_after)_}; + @closest_variants = sort(@closest_variants); + $unknown_hooks{$target} = \@closest_variants; + } + + if ($output_format eq 'json') { + my @hook_target_data; + for my $target (sort(keys(%unknown_hooks))) { + my $options = $unknown_hooks{$target}; + my (undef, $filename) = @{$explicit_targets->{$target}}; + push(@hook_target_data, { + 'target-name' => $target, + 'filename' => $filename, + 'candidates' => $options, + }); + } + my %result = ( + "unknown-hook-targets" => \@hook_target_data, + ); + if (@{$unloadables}) { + my @issues; + for my $addon (@{$unloadables}) { + push(@issues, { + "issue" => "load-addon", + "addon" => $addon, + }); + }; + $result{'issues'} = \@issues; + } + _output(\%result); + } elsif ($output_format eq 'text') { + for my $target (sort(keys(%unknown_hooks))) { + my $options = $unknown_hooks{$target}; + my (undef, $filename) = @{$explicit_targets->{$target}}; + my $help = ''; + if (@{$options}) { + if (scalar(@{$options}) == 1) { + my $name = $options->[0]; + $help = " Likely a typo of ${name}"; + } else { + my $names = join(', ', @{$options}); + $help = " Likely a typo of one of ${names}"; + } + } + print("The hook target ${target} in ${filename} does not seem to match any known commands. ${help}\n"); + } + if (@{$unloadables}) { + my $addon_names = join(" ", @{$unloadables}); + print("\n"); + warning("Incomplete result. The following sequence add-ons could not be loaded: $addon_names"); + } + } else { + error("Internal error: Missing case for ${output_format}"); + } + if ($lint_exit && (%unknown_hooks or @{$unloadables})) { + exit(EXIT_CODE_LINT_ISSUES_FOUND); + } + exit(0); +} + +sub detect_hook_targets { + if (@ARGV) { + error("$COMMAND: No arguments supported (please remove everything after the command)"); + } + _assert_debian_control_exists(); + my $explicit_targets = _load_hook_targets(); + my (%result, @targets, @unverifiable_commands, %seen_cmds); + while (my ($target, $rule_details) = 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 ($non_empty, $filename) = @{$rule_details}; + + my $target_info = { + 'target-name' => $target, + 'command' => $command, + 'package-section-param' => $param, + 'is-empty' => $non_empty ? JSON::PP::false : JSON::PP::true, + 'filename' => $filename, + }; + 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 dh_assistant_restore_file_on_clean { + init(inhibit_log => 1); + if (not @ARGV) { + error("At least one file name is required"); + } + for my $file (@ARGV) { + lstat($file); + if ( ! -f _ ) { + error("The path ${file} was a symlink. It must be a file; not a symlink to a file") if -l _; + error("The path ${file} does not exist") if not -e _; + error("The path ${file} was not a file and this command only supports files"); + } + if ($file =~ m{[.][.]}) { + # Someone can provide a patch when there is a use-case for "..foo". + # Said patch will need to ensure the file is inside the package root dir. + error("Files containing \"..\" (which ${file} does) are not supported."); + } + if ($file =~ m{^/}) { + error("Files must be relative to the package root (which ${file} was not)") + } + if ($file =~ m{^\.} or $file =~ m{/CVS/} or $file =~ m{/\.}) { + error("Cowardly refusing to track hidden files / version control files (${file})."); + } + Debian::Debhelper::Dh_Lib::restore_file_on_clean($file) + } +} + +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); +} + +sub supports { + my ($command, @more) = @ARGV; + if (@more or not defined($command)) { + error("$COMMAND: Please provide exactly one argument"); + } + exit(0) if exists($COMMANDS{$command}); + exit(2); +} + +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/^-/; + my $available_cmds = join(' ', sort(grep { $_ ne '-h' and $_ ne '--help' and $_ ne 'help' } keys(%COMMANDS))); + error("Unknown command: $COMMAND. Use \"help\" or \"--help\" as first argument for usage. Available commands: ${available_cmds}"); +} + +$handler->(); + +=head1 SEE ALSO + +L<debhelper(7)> + +This program is a part of debhelper. + +=cut + +1; |