Skip to content

Commit

Permalink
installShellFiles: rework and add installBin function (#332612)
Browse files Browse the repository at this point in the history
  • Loading branch information
philiptaron authored Aug 29, 2024
2 parents 3a4a3e9 + 618cffa commit 166ba20
Show file tree
Hide file tree
Showing 18 changed files with 399 additions and 168 deletions.
5 changes: 5 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
83 changes: 77 additions & 6 deletions doc/hooks/installShellFiles.section.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 generation commands.

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.
### Example Usage {#installshellfiles-installshellcompletion-exampleusage-guarded}

```nix
{
Expand Down
12 changes: 0 additions & 12 deletions pkgs/build-support/install-shell-files/default.nix

This file was deleted.

16 changes: 16 additions & 0 deletions pkgs/by-name/in/installShellFiles/package.nix
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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;;
Expand All @@ -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;;
Expand All @@ -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;;
--)
Expand All @@ -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
Expand All @@ -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;;
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
31 changes: 31 additions & 0 deletions pkgs/by-name/in/installShellFiles/tests/install-bin-output.nix
Original file line number Diff line number Diff line change
@@ -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
''
21 changes: 21 additions & 0 deletions pkgs/by-name/in/installShellFiles/tests/install-bin.nix
Original file line number Diff line number Diff line change
@@ -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
''
24 changes: 24 additions & 0 deletions pkgs/by-name/in/installShellFiles/tests/install-completion-cmd.nix
Original file line number Diff line number Diff line change
@@ -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
''
Loading

0 comments on commit 166ba20

Please sign in to comment.