installShellFiles: rework and add installBin function (#332612)

This commit is contained in:
Philip Taron 2024-08-28 18:32:59 -07:00 committed by GitHub
commit 166ba20bf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 399 additions and 168 deletions

5
.github/CODEOWNERS vendored
View File

@ -389,3 +389,8 @@ pkgs/by-name/lx/lxc* @adamcstephens
/pkgs/os-specific/linux/checkpolicy @RossComputerGuy
/pkgs/os-specific/linux/libselinux @RossComputerGuy
/pkgs/os-specific/linux/libsepol @RossComputerGuy
# installShellFiles
/pkgs/by-name/in/installShellFiles/* @Ericson2314
/pkgs/test/install-shell-files/* @Ericson2314
/doc/hooks/installShellFiles.section.md @Ericson2314

View File

@ -1,16 +1,79 @@
# `installShellFiles` {#installshellfiles}
This hook helps with installing manpages and shell completion files. It exposes 2 shell functions `installManPage` and `installShellCompletion` that can be used from your `postInstall` hook.
This hook adds helpers that install artifacts like executable files, manpages
and shell completions.
The `installManPage` function takes one or more paths to manpages to install. The manpages must have a section suffix, and may optionally be compressed (with `.gz` suffix). This function will place them into the correct `share/man/man<section>/` directory, in [`outputMan`](#outputman).
It exposes the following functions that can be used from your `postInstall`
hook:
The `installShellCompletion` function takes one or more paths to shell completion files. By default it will autodetect the shell type from the completion file extension, but you may also specify it by passing one of `--bash`, `--fish`, or `--zsh`. These flags apply to all paths listed after them (up until another shell flag is given). Each path may also have a custom installation name provided by providing a flag `--name NAME` before the path. If this flag is not provided, zsh completions will be renamed automatically such that `foobar.zsh` becomes `_foobar`. A root name may be provided for all paths using the flag `--cmd NAME`; this synthesizes the appropriate name depending on the shell (e.g. `--cmd foo` will synthesize the name `foo.bash` for bash and `_foo` for zsh).
## `installBin` {#installshellfiles-installbin}
The `installBin` function takes one or more paths to files to install as
executable files.
This function will place them into [`outputBin`](#outputbin).
### Example Usage {#installshellfiles-installbin-exampleusage}
```nix
{
nativeBuildInputs = [ installShellFiles ];
# Sometimes the file has an undersirable name. It should be renamed before
# being installed via installBin
postInstall = ''
mv a.out delmar
installBin foobar delmar
'';
}
```
## `installManPage` {#installshellfiles-installmanpage}
The `installManPage` function takes one or more paths to manpages to install.
The manpages must have a section suffix, and may optionally be compressed (with
`.gz` suffix). This function will place them into the correct
`share/man/man<section>/` directory in [`outputMan`](#outputman).
### Example Usage {#installshellfiles-installmanpage-exampleusage}
```nix
{
nativeBuildInputs = [ installShellFiles ];
# Sometimes the manpage file has an undersirable name; e.g. it conflicts with
# another software with an equal name. It should be renamed before being
# installed via installManPage
postInstall = ''
mv fromsea.3 delmar.3
installManPage foobar.1 delmar.3
'';
}
```
## `installShellCompletion` {#installshellfiles-installshellcompletion}
The `installShellCompletion` function takes one or more paths to shell
completion files.
By default it will autodetect the shell type from the completion file extension,
but you may also specify it by passing one of `--bash`, `--fish`, or
`--zsh`. These flags apply to all paths listed after them (up until another
shell flag is given). Each path may also have a custom installation name
provided by providing a flag `--name NAME` before the path. If this flag is not
provided, zsh completions will be renamed automatically such that `foobar.zsh`
becomes `_foobar`. A root name may be provided for all paths using the flag
`--cmd NAME`; this synthesizes the appropriate name depending on the shell
(e.g. `--cmd foo` will synthesize the name `foo.bash` for bash and `_foo` for
zsh).
### Example Usage {#installshellfiles-installshellcompletion-exampleusage}
```nix
{
nativeBuildInputs = [ installShellFiles ];
postInstall = ''
installManPage doc/foobar.1 doc/barfoo.3
# explicit behavior
installShellCompletion --bash --name foobar.bash share/completions.bash
installShellCompletion --fish --name foobar.fish share/completions.fish
@ -21,9 +84,17 @@ The `installShellCompletion` function takes one or more paths to shell completio
}
```
The path may also be a fifo or named fd (such as produced by `<(cmd)`), in which case the shell and name must be provided (see below).
The path may also be a fifo or named fd (such as produced by `<(cmd)`), in which
case the shell and name must be provided (see below).
If the destination shell completion file is not actually present or consists of zero bytes after calling `installShellCompletion` this is treated as a build failure. In particular, if completion files are not vendored but are generated by running an executable, this is likely to fail in cross compilation scenarios. The result will be a zero byte completion file and hence a build failure. To prevent this, guard the completion commands against this, e.g.
If the destination shell completion file is not actually present or consists of
zero bytes after calling `installShellCompletion` this is treated as a build
failure. In particular, if completion files are not vendored but are generated
by running an executable, this is likely to fail in cross compilation
scenarios. The result will be a zero byte completion file and hence a build
failure. To prevent this, guard the completion generation commands.
### Example Usage {#installshellfiles-installshellcompletion-exampleusage-guarded}
```nix
{

View File

@ -1,12 +0,0 @@
{ makeSetupHook, tests }:
# See the header comment in ../setup-hooks/install-shell-files.sh for example usage.
let
setupHook = makeSetupHook { name = "install-shell-files"; } ../setup-hooks/install-shell-files.sh;
in
setupHook.overrideAttrs (oldAttrs: {
passthru = (oldAttrs.passthru or {}) // {
tests = tests.install-shell-files;
};
})

View File

@ -0,0 +1,16 @@
{
lib,
callPackage,
makeSetupHook,
}:
# See the header comment in ./setup-hook.sh for example usage.
makeSetupHook {
name = "install-shell-files";
passthru = {
tests = lib.packagesFromDirectoryRecursive {
inherit callPackage;
directory = ./tests;
};
};
} ./setup-hook.sh

View File

@ -24,19 +24,17 @@
installManPage() {
local path
for path in "$@"; do
if (( "${NIX_DEBUG:-0}" >= 1 )); then
echo "installManPage: installing $path"
fi
if test -z "$path"; then
echo "installManPage: error: path cannot be empty" >&2
nixErrorLog "${FUNCNAME[0]}: path cannot be empty"
return 1
fi
nixInfoLog "${FUNCNAME[0]}: installing $path"
local basename
basename=$(stripHash "$path") # use stripHash in case it's a nix store path
local trimmed=${basename%.gz} # don't get fooled by compressed manpages
local suffix=${trimmed##*.}
if test -z "$suffix" -o "$suffix" = "$trimmed"; then
echo "installManPage: error: path missing manpage section suffix: $path" >&2
nixErrorLog "${FUNCNAME[0]}: path missing manpage section suffix: $path"
return 1
fi
local outRoot
@ -45,7 +43,8 @@ installManPage() {
else
outRoot=${!outputMan:?}
fi
install -Dm644 -T "$path" "${outRoot}/share/man/man$suffix/$basename" || return
local outPath="${outRoot}/share/man/man$suffix/$basename"
install -D --mode=644 --no-target-directory "$path" "$outPath"
done
}
@ -107,7 +106,7 @@ installShellCompletion() {
--name)
name=$1
shift || {
echo 'installShellCompletion: error: --name flag expected an argument' >&2
nixErrorLog "${FUNCNAME[0]}: --name flag expected an argument"
return 1
}
continue;;
@ -118,7 +117,7 @@ installShellCompletion() {
--cmd)
cmdname=$1
shift || {
echo 'installShellCompletion: error: --cmd flag expected an argument' >&2
nixErrorLog "${FUNCNAME[0]}: --cmd flag expected an argument"
return 1
}
continue;;
@ -127,7 +126,7 @@ installShellCompletion() {
cmdname=${arg#--cmd=}
continue;;
--?*)
echo "installShellCompletion: warning: unknown flag ${arg%%=*}" >&2
nixWarnLog "${FUNCNAME[0]}: unknown flag ${arg%%=*}"
retval=2
continue;;
--)
@ -136,23 +135,21 @@ installShellCompletion() {
continue;;
esac
fi
if (( "${NIX_DEBUG:-0}" >= 1 )); then
echo "installShellCompletion: installing $arg${name:+ as $name}"
fi
nixInfoLog "${FUNCNAME[0]}: installing $arg${name:+ as $name}"
# if we get here, this is a path or named pipe
# Identify shell and output name
local curShell=$shell
local outName=''
if [[ -z "$arg" ]]; then
echo "installShellCompletion: error: empty path is not allowed" >&2
nixErrorLog "${FUNCNAME[0]}: empty path is not allowed"
return 1
elif [[ -p "$arg" ]]; then
# this is a named fd or fifo
if [[ -z "$curShell" ]]; then
echo "installShellCompletion: error: named pipe requires one of --bash, --fish, or --zsh" >&2
nixErrorLog "${FUNCNAME[0]}: named pipe requires one of --bash, --fish, or --zsh"
return 1
elif [[ -z "$name" && -z "$cmdname" ]]; then
echo "installShellCompletion: error: named pipe requires one of --cmd or --name" >&2
nixErrorLog "${FUNCNAME[0]}: named pipe requires one of --cmd or --name"
return 1
fi
else
@ -168,10 +165,10 @@ installShellCompletion() {
*)
if [[ "$argbase" = _* && "$argbase" != *.* ]]; then
# probably zsh
echo "installShellCompletion: warning: assuming path \`$arg' is zsh; please specify with --zsh" >&2
nixWarnLog "${FUNCNAME[0]}: assuming path \`$arg' is zsh; please specify with --zsh"
curShell=zsh
else
echo "installShellCompletion: warning: unknown shell for path: $arg" >&2
nixWarnLog "${FUNCNAME[0]}: unknown shell for path: $arg" >&2
retval=2
continue
fi;;
@ -188,7 +185,7 @@ installShellCompletion() {
zsh) outName=_$cmdname;;
*)
# Our list of shells is out of sync with the flags we accept or extensions we detect.
echo 'installShellCompletion: internal error' >&2
nixErrorLog "${FUNCNAME[0]}: internal: shell $curShell not recognized"
return 1;;
esac
fi
@ -206,7 +203,7 @@ installShellCompletion() {
fi;;
*)
# Our list of shells is out of sync with the flags we accept or extensions we detect.
echo 'installShellCompletion: internal error' >&2
nixErrorLog "${FUNCNAME[0]}: internal: shell $curShell not recognized"
return 1;;
esac
# Install file
@ -217,19 +214,43 @@ installShellCompletion() {
mkdir -p "$outDir" \
&& cat "$arg" > "$outPath"
else
install -Dm644 -T "$arg" "$outPath"
fi || return
install -D --mode=644 --no-target-directory "$arg" "$outPath"
fi
if [ ! -s "$outPath" ]; then
echo "installShellCompletion: error: installed shell completion file \`$outPath' does not exist or has zero size" >&2
nixErrorLog "${FUNCNAME[0]}: installed shell completion file \`$outPath' does not exist or has zero size"
return 1
fi
# Clear the per-path flags
name=
done
if [[ -n "$name" ]]; then
echo 'installShellCompletion: error: --name flag given with no path' >&2
nixErrorLog "${FUNCNAME[0]}: --name flag given with no path" >&2
return 1
fi
return $retval
}
# installBin <path> [...<path>]
#
# Install each argument to $outputBin
installBin() {
local path
for path in "$@"; do
if test -z "$path"; then
nixErrorLog "${FUNCNAME[0]}: path cannot be empty"
return 1
fi
nixInfoLog "${FUNCNAME[0]}: installing $path"
local basename
# use stripHash in case it's a nix store path
basename=$(stripHash "$path")
local outRoot
outRoot=${!outputBin:?}
local outPath="${outRoot}/bin/$basename"
install -D --mode=755 --no-target-directory "$path" "${outRoot}/bin/$basename"
done
}

View File

@ -0,0 +1,31 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-bin-output"
{
outputs = [
"out"
"bin"
];
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
mkdir -p bin
echo "echo hello za warudo" > bin/hello
echo "echo amigo me gusta mucho" > bin/amigo
installBin bin/*
# assert it didn't go into $out
[[ ! -f $out/bin/amigo ]]
[[ ! -f $out/bin/hello ]]
cmp bin/amigo ''${!outputBin}/bin/amigo
cmp bin/hello ''${!outputBin}/bin/hello
touch $out
''

View File

@ -0,0 +1,21 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-bin"
{
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
mkdir -p bin
echo "echo hello za warudo" > bin/hello
echo "echo amigo me gusta mucho" > bin/amigo
installBin bin/*
cmp bin/amigo $out/bin/amigo
cmp bin/hello $out/bin/hello
''

View File

@ -0,0 +1,24 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-completion-cmd"
{
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
echo foo > foo.bash
echo bar > bar.zsh
echo baz > baz.fish
echo qux > qux.fish
installShellCompletion --cmd foobar --bash foo.bash --zsh bar.zsh --fish baz.fish --name qux qux.fish
cmp foo.bash $out/share/bash-completion/completions/foobar.bash
cmp bar.zsh $out/share/zsh/site-functions/_foobar
cmp baz.fish $out/share/fish/vendor_completions.d/foobar.fish
cmp qux.fish $out/share/fish/vendor_completions.d/qux
''

View File

@ -0,0 +1,21 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-completion-fifo"
{
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
installShellCompletion \
--bash --name foo.bash <(echo foo) \
--zsh --name _foo <(echo bar) \
--fish --name foo.fish <(echo baz)
[[ $(<$out/share/bash-completion/completions/foo.bash) == foo ]] || { echo "foo.bash comparison failed"; exit 1; }
[[ $(<$out/share/zsh/site-functions/_foo) == bar ]] || { echo "_foo comparison failed"; exit 1; }
[[ $(<$out/share/fish/vendor_completions.d/foo.fish) == baz ]] || { echo "foo.fish comparison failed"; exit 1; }
''

View File

@ -0,0 +1,22 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-completion-inference"
{
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
echo foo > foo.bash
echo bar > bar.zsh
echo baz > baz.fish
installShellCompletion foo.bash bar.zsh baz.fish
cmp foo.bash $out/share/bash-completion/completions/foo.bash
cmp bar.zsh $out/share/zsh/site-functions/_bar
cmp baz.fish $out/share/fish/vendor_completions.d/baz.fish
''

View File

@ -0,0 +1,22 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-completion-name"
{
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
echo foo > foo
echo bar > bar
echo baz > baz
installShellCompletion --bash --name foobar.bash foo --zsh --name _foobar bar --fish baz
cmp foo $out/share/bash-completion/completions/foobar.bash
cmp bar $out/share/zsh/site-functions/_foobar
cmp baz $out/share/fish/vendor_completions.d/baz
''

View File

@ -0,0 +1,27 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-completion-output"
{
outputs = [
"out"
"bin"
];
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
echo foo > foo
installShellCompletion --bash foo
# assert it didn't go into $out
[[ ! -f $out/share/bash-completion/completions/foo ]]
cmp foo ''${!outputBin:?}/share/bash-completion/completions/foo
touch $out
''

View File

@ -0,0 +1,26 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-completion"
{
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
echo foo > foo
echo bar > bar
echo baz > baz
echo qux > qux.zsh
echo quux > quux
installShellCompletion --bash foo bar --zsh baz qux.zsh --fish quux
cmp foo $out/share/bash-completion/completions/foo
cmp bar $out/share/bash-completion/completions/bar
cmp baz $out/share/zsh/site-functions/_baz
cmp qux.zsh $out/share/zsh/site-functions/_qux
cmp quux $out/share/fish/vendor_completions.d/quux
''

View File

@ -0,0 +1,36 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-manpage-outputs"
{
outputs = [
"out"
"man"
"devman"
];
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
mkdir -p doc
echo foo > doc/foo.1
echo bar > doc/bar.3
installManPage doc/*
# assert they didn't go into $out
[[ ! -f $out/share/man/man1/foo.1 && ! -f $out/share/man/man3/bar.3 ]]
# foo.1 alone went into man
cmp doc/foo.1 ''${!outputMan:?}/share/man/man1/foo.1
[[ ! -f ''${!outputMan:?}/share/man/man3/bar.3 ]]
# bar.3 alone went into devman
cmp doc/bar.3 ''${!outputDevman:?}/share/man/man3/bar.3
[[ ! -f ''${!outputDevman:?}/share/man/man1/foo.1 ]]
touch $out
''

View File

@ -0,0 +1,23 @@
{
lib,
installShellFiles,
runCommandLocal,
}:
runCommandLocal "install-shell-files--install-manpage"
{
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
}
''
mkdir -p doc
echo foo > doc/foo.1
echo bar > doc/bar.2.gz
echo baz > doc/baz.3
installManPage doc/*
cmp doc/foo.1 $out/share/man/man1/foo.1
cmp doc/bar.2.gz $out/share/man/man2/bar.2.gz
cmp doc/baz.3 $out/share/man/man3/baz.3
''

View File

@ -34,7 +34,8 @@ stdenv.mkDerivation (finalAttrs: {
installPhase = ''
runHook preInstall
install -Dm755 a.out "$out/bin/nawk"
mv a.out nawk
installBin nawk
mv awk.1 nawk.1
installManPage nawk.1
runHook postInstall

View File

@ -1,125 +1,3 @@
{ lib, runCommandLocal, recurseIntoAttrs, installShellFiles }:
{ installShellFiles }:
let
runTest = name: env: buildCommand:
runCommandLocal "install-shell-files--${name}" ({
nativeBuildInputs = [ installShellFiles ];
meta.platforms = lib.platforms.all;
} // env) buildCommand;
in
recurseIntoAttrs {
# installManPage
install-manpage = runTest "install-manpage" {} ''
mkdir -p doc
echo foo > doc/foo.1
echo bar > doc/bar.2.gz
echo baz > doc/baz.3
installManPage doc/*
cmp doc/foo.1 $out/share/man/man1/foo.1
cmp doc/bar.2.gz $out/share/man/man2/bar.2.gz
cmp doc/baz.3 $out/share/man/man3/baz.3
'';
install-manpage-outputs = runTest "install-manpage-outputs" {
outputs = [ "out" "man" "devman" ];
} ''
mkdir -p doc
echo foo > doc/foo.1
echo bar > doc/bar.3
installManPage doc/*
# assert they didn't go into $out
[[ ! -f $out/share/man/man1/foo.1 && ! -f $out/share/man/man3/bar.3 ]]
# foo.1 alone went into man
cmp doc/foo.1 ''${!outputMan:?}/share/man/man1/foo.1
[[ ! -f ''${!outputMan:?}/share/man/man3/bar.3 ]]
# bar.3 alone went into devman
cmp doc/bar.3 ''${!outputDevman:?}/share/man/man3/bar.3
[[ ! -f ''${!outputDevman:?}/share/man/man1/foo.1 ]]
touch $out
'';
# installShellCompletion
install-completion = runTest "install-completion" {} ''
echo foo > foo
echo bar > bar
echo baz > baz
echo qux > qux.zsh
echo quux > quux
installShellCompletion --bash foo bar --zsh baz qux.zsh --fish quux
cmp foo $out/share/bash-completion/completions/foo
cmp bar $out/share/bash-completion/completions/bar
cmp baz $out/share/zsh/site-functions/_baz
cmp qux.zsh $out/share/zsh/site-functions/_qux
cmp quux $out/share/fish/vendor_completions.d/quux
'';
install-completion-output = runTest "install-completion-output" {
outputs = [ "out" "bin" ];
} ''
echo foo > foo
installShellCompletion --bash foo
# assert it didn't go into $out
[[ ! -f $out/share/bash-completion/completions/foo ]]
cmp foo ''${!outputBin:?}/share/bash-completion/completions/foo
touch $out
'';
install-completion-name = runTest "install-completion-name" {} ''
echo foo > foo
echo bar > bar
echo baz > baz
installShellCompletion --bash --name foobar.bash foo --zsh --name _foobar bar --fish baz
cmp foo $out/share/bash-completion/completions/foobar.bash
cmp bar $out/share/zsh/site-functions/_foobar
cmp baz $out/share/fish/vendor_completions.d/baz
'';
install-completion-inference = runTest "install-completion-inference" {} ''
echo foo > foo.bash
echo bar > bar.zsh
echo baz > baz.fish
installShellCompletion foo.bash bar.zsh baz.fish
cmp foo.bash $out/share/bash-completion/completions/foo.bash
cmp bar.zsh $out/share/zsh/site-functions/_bar
cmp baz.fish $out/share/fish/vendor_completions.d/baz.fish
'';
install-completion-cmd = runTest "install-completion-cmd" {} ''
echo foo > foo.bash
echo bar > bar.zsh
echo baz > baz.fish
echo qux > qux.fish
installShellCompletion --cmd foobar --bash foo.bash --zsh bar.zsh --fish baz.fish --name qux qux.fish
cmp foo.bash $out/share/bash-completion/completions/foobar.bash
cmp bar.zsh $out/share/zsh/site-functions/_foobar
cmp baz.fish $out/share/fish/vendor_completions.d/foobar.fish
cmp qux.fish $out/share/fish/vendor_completions.d/qux
'';
install-completion-fifo = runTest "install-completion-fifo" {} ''
installShellCompletion \
--bash --name foo.bash <(echo foo) \
--zsh --name _foo <(echo bar) \
--fish --name foo.fish <(echo baz)
[[ $(<$out/share/bash-completion/completions/foo.bash) == foo ]] || { echo "foo.bash comparison failed"; exit 1; }
[[ $(<$out/share/zsh/site-functions/_foo) == bar ]] || { echo "_foo comparison failed"; exit 1; }
[[ $(<$out/share/fish/vendor_completions.d/foo.fish) == baz ]] || { echo "foo.fish comparison failed"; exit 1; }
'';
}
installShellFiles.tests

View File

@ -1204,8 +1204,6 @@ with pkgs;
inherit url;
};
installShellFiles = callPackage ../build-support/install-shell-files { };
lazydocker = callPackage ../tools/misc/lazydocker { };
ld-is-cc-hook = makeSetupHook { name = "ld-is-cc-hook"; }