mas: add module for Mac App Store management (#1668)
Some checks are pending
Some checks are pending
This commit is contained in:
commit
a4cf1d1085
6 changed files with 344 additions and 0 deletions
|
|
@ -112,6 +112,7 @@
|
||||||
./programs/fish.nix
|
./programs/fish.nix
|
||||||
./programs/gnupg.nix
|
./programs/gnupg.nix
|
||||||
./programs/man.nix
|
./programs/man.nix
|
||||||
|
./programs/mas.nix
|
||||||
./programs/info
|
./programs/info
|
||||||
./programs/nix-index
|
./programs/nix-index
|
||||||
./programs/ssh.nix
|
./programs/ssh.nix
|
||||||
|
|
|
||||||
209
modules/programs/mas.nix
Normal file
209
modules/programs/mas.nix
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
inherit (lib)
|
||||||
|
attrValues
|
||||||
|
concatStringsSep
|
||||||
|
escapeShellArg
|
||||||
|
getExe
|
||||||
|
literalExpression
|
||||||
|
mapAttrsToList
|
||||||
|
mkEnableOption
|
||||||
|
mkIf
|
||||||
|
mkOption
|
||||||
|
mkOptionDefault
|
||||||
|
mkPackageOption
|
||||||
|
optionalString
|
||||||
|
types
|
||||||
|
;
|
||||||
|
|
||||||
|
cfg = config.programs.mas;
|
||||||
|
|
||||||
|
apps = mapAttrsToList (name: id: { inherit name id; }) cfg.packages;
|
||||||
|
|
||||||
|
desiredIds = map (app: toString app.id) apps;
|
||||||
|
homebrewIds = map toString (attrValues config.homebrew.masApps);
|
||||||
|
|
||||||
|
hasWork = cfg.update || cfg.packages != { } || cfg.cleanup || homebrewIds != [ ];
|
||||||
|
|
||||||
|
activationScript =
|
||||||
|
if hasWork then
|
||||||
|
''
|
||||||
|
echo >&2 "setting up App Store apps (mas)..."
|
||||||
|
|
||||||
|
runAsUser() {
|
||||||
|
sudo \
|
||||||
|
--preserve-env=PATH \
|
||||||
|
--set-home \
|
||||||
|
--user=${escapeShellArg cfg.user} \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
listStatus=0
|
||||||
|
listOutput=$(
|
||||||
|
runAsUser ${getExe cfg.package} list 2>&1
|
||||||
|
) || listStatus=$?
|
||||||
|
|
||||||
|
if (( listStatus != 0 )); then
|
||||||
|
echo >&2 "warning: mas list failed (exit ''${listStatus}):"
|
||||||
|
echo >&2 "''${listOutput}"
|
||||||
|
if echo "''${listOutput}" | grep -qi "not signed in"; then
|
||||||
|
echo >&2 "login required; skipping App Store installs/updates/cleanup"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only emit cleanup-only shell variables when cleanup is enabled; otherwise shellcheck
|
||||||
|
# treats them as unused and fails the activation script build.
|
||||||
|
installedIds=()
|
||||||
|
${if cfg.cleanup then
|
||||||
|
''
|
||||||
|
# Parse mas list output: "ID AppName (version)"
|
||||||
|
declare -A installedApps
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
line="''${line#"''${line%%[![:space:]]*}"}"
|
||||||
|
id="''${line%% *}"
|
||||||
|
rest="''${line#"$id"}"
|
||||||
|
rest="''${rest#"''${rest%%[![:space:]]*}"}"
|
||||||
|
name="''${rest% (*}"
|
||||||
|
name="''${name%"''${name##*[![:space:]]}"}"
|
||||||
|
[[ -n "$id" ]] && {
|
||||||
|
installedIds+=( "$id" )
|
||||||
|
installedApps["$id"]="$name"
|
||||||
|
}
|
||||||
|
done <<<"$listOutput"
|
||||||
|
''
|
||||||
|
else
|
||||||
|
''
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
line="''${line#"''${line%%[![:space:]]*}"}"
|
||||||
|
id="''${line%% *}"
|
||||||
|
[[ -n "$id" ]] && installedIds+=( "$id" )
|
||||||
|
done <<<"$listOutput"
|
||||||
|
''}
|
||||||
|
|
||||||
|
${optionalString cfg.update ''
|
||||||
|
runAsUser ${getExe cfg.package} update || true
|
||||||
|
''}
|
||||||
|
|
||||||
|
desiredIds=(
|
||||||
|
${concatStringsSep "\n " desiredIds}
|
||||||
|
)
|
||||||
|
|
||||||
|
is_installed() {
|
||||||
|
local needle=$1
|
||||||
|
for id in "''${installedIds[@]}"; do
|
||||||
|
if [[ "$id" == "$needle" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
${optionalString (cfg.packages != { }) ''
|
||||||
|
for appId in "''${desiredIds[@]}"; do
|
||||||
|
if is_installed "$appId"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
runAsUser ${getExe cfg.package} install "$appId" || true
|
||||||
|
done
|
||||||
|
''}
|
||||||
|
|
||||||
|
${optionalString cfg.cleanup ''
|
||||||
|
homebrewIds=(
|
||||||
|
${concatStringsSep "\n " homebrewIds}
|
||||||
|
)
|
||||||
|
|
||||||
|
keepIds=( "''${desiredIds[@]}" "''${homebrewIds[@]}" )
|
||||||
|
|
||||||
|
for installedId in "''${installedIds[@]}"; do
|
||||||
|
keep=false
|
||||||
|
for keepId in "''${keepIds[@]}"; do
|
||||||
|
if [[ "$installedId" == "$keepId" ]]; then
|
||||||
|
keep=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! $keep; then
|
||||||
|
appName="''${installedApps[$installedId]:-$installedId}"
|
||||||
|
echo >&2 "removing $appName from App Store"
|
||||||
|
runAsUser ${getExe cfg.package} uninstall "$installedId" || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
''}
|
||||||
|
''
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.programs.mas = {
|
||||||
|
enable = mkEnableOption "managing Mac App Store apps with mas";
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = config.system.primaryUser;
|
||||||
|
defaultText = literalExpression "config.system.primaryUser";
|
||||||
|
description = ''
|
||||||
|
The user account that runs {command}`mas`. This user must be signed into the Mac App Store
|
||||||
|
for installs or updates to succeed.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
package = mkPackageOption pkgs "mas" { };
|
||||||
|
|
||||||
|
packages = mkOption {
|
||||||
|
type = types.attrsOf types.ints.positive;
|
||||||
|
default = { };
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
Xcode = 497799835;
|
||||||
|
"1Password for Safari" = 1569813296;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
Applications to install from the Mac App Store. Attribute names are only for readability;
|
||||||
|
values must be the numeric identifiers used by {command}`mas`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
update = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Whether to run {command}`mas update` during system activation in addition to installing the
|
||||||
|
configured apps.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
cleanup = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether to uninstall Mac App Store apps that are currently installed but not listed in
|
||||||
|
{option}`programs.mas.packages`. Apps listed in {option}`homebrew.masApps` are also preserved.
|
||||||
|
This runs before install/update; any app id not in either set will be removed.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
system.requiresPrimaryUser =
|
||||||
|
mkIf (cfg.enable && options.programs.mas.user.highestPrio == (mkOptionDefault { }).priority)
|
||||||
|
[
|
||||||
|
"programs.mas.enable"
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.systemPackages = mkIf cfg.enable [ cfg.package ];
|
||||||
|
|
||||||
|
system.activationScripts.mas.text = mkIf cfg.enable activationScript;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -134,6 +134,7 @@ in
|
||||||
${cfg.activationScripts.keyboard.text}
|
${cfg.activationScripts.keyboard.text}
|
||||||
${cfg.activationScripts.fonts.text}
|
${cfg.activationScripts.fonts.text}
|
||||||
${cfg.activationScripts.nvram.text}
|
${cfg.activationScripts.nvram.text}
|
||||||
|
${cfg.activationScripts.mas.text}
|
||||||
${cfg.activationScripts.homebrew.text}
|
${cfg.activationScripts.homebrew.text}
|
||||||
|
|
||||||
${cfg.activationScripts.postActivation.text}
|
${cfg.activationScripts.postActivation.text}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ in {
|
||||||
tests.nixpkgs-overlays = makeTest ./tests/nixpkgs-overlays.nix;
|
tests.nixpkgs-overlays = makeTest ./tests/nixpkgs-overlays.nix;
|
||||||
tests.nixpkgs-config-allow-unfree = makeTest ./tests/nixpkgs-config-allow-unfree.nix;
|
tests.nixpkgs-config-allow-unfree = makeTest ./tests/nixpkgs-config-allow-unfree.nix;
|
||||||
tests.programs-gnupg = makeTest ./tests/programs-gnupg.nix;
|
tests.programs-gnupg = makeTest ./tests/programs-gnupg.nix;
|
||||||
|
tests.programs-mas = makeTest ./tests/programs-mas.nix;
|
||||||
|
tests.programs-mas-no-cleanup = makeTest ./tests/programs-mas-no-cleanup.nix;
|
||||||
tests.programs-ssh = makeTest ./tests/programs-ssh.nix;
|
tests.programs-ssh = makeTest ./tests/programs-ssh.nix;
|
||||||
tests.programs-tmux = makeTest ./tests/programs-tmux.nix;
|
tests.programs-tmux = makeTest ./tests/programs-tmux.nix;
|
||||||
tests.programs-zsh = makeTest ./tests/programs-zsh.nix;
|
tests.programs-zsh = makeTest ./tests/programs-zsh.nix;
|
||||||
|
|
|
||||||
60
tests/programs-mas-no-cleanup.nix
Normal file
60
tests/programs-mas-no-cleanup.nix
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
mas =
|
||||||
|
pkgs.runCommand "mas-0.0.0" { } ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cat > $out/bin/mas <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x $out/bin/mas
|
||||||
|
''
|
||||||
|
// {
|
||||||
|
meta.mainProgram = "mas";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
system.primaryUser = "primary-mas-user";
|
||||||
|
|
||||||
|
programs.mas = {
|
||||||
|
enable = true;
|
||||||
|
user = "test-mas-user";
|
||||||
|
package = mas;
|
||||||
|
update = true;
|
||||||
|
cleanup = false;
|
||||||
|
packages = {
|
||||||
|
Xcode = 497799835;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
homebrew.masApps = {
|
||||||
|
"KeepFromHomebrew" = 424242;
|
||||||
|
};
|
||||||
|
|
||||||
|
test = ''
|
||||||
|
echo "checking mas present in systemPackages" >&2
|
||||||
|
test -x ${config.out}/sw/bin/mas
|
||||||
|
|
||||||
|
echo "checking mas activation script still installs and updates apps" >&2
|
||||||
|
grep 'desiredIds=(' ${config.out}/activate
|
||||||
|
grep '497799835' ${config.out}/activate
|
||||||
|
grep 'mas install \"$appId\"' ${config.out}/activate
|
||||||
|
grep 'mas update' ${config.out}/activate
|
||||||
|
|
||||||
|
echo "checking cleanup-only variables are omitted" >&2
|
||||||
|
if grep 'declare -A installedApps' ${config.out}/activate; then
|
||||||
|
echo "unexpected installedApps declaration when cleanup is disabled" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep 'keepIds=(' ${config.out}/activate; then
|
||||||
|
echo "unexpected keepIds declaration when cleanup is disabled" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
}
|
||||||
71
tests/programs-mas.nix
Normal file
71
tests/programs-mas.nix
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
mas =
|
||||||
|
pkgs.runCommand "mas-0.0.0" { } ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cat > $out/bin/mas <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x $out/bin/mas
|
||||||
|
''
|
||||||
|
// {
|
||||||
|
meta.mainProgram = "mas";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
system.primaryUser = "primary-mas-user";
|
||||||
|
|
||||||
|
programs.mas = {
|
||||||
|
enable = true;
|
||||||
|
user = "test-mas-user";
|
||||||
|
package = mas;
|
||||||
|
update = true;
|
||||||
|
cleanup = true;
|
||||||
|
packages = {
|
||||||
|
"1Password for Safari" = 1569813296;
|
||||||
|
Xcode = 497799835;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
homebrew.masApps = {
|
||||||
|
"KeepFromHomebrew" = 424242;
|
||||||
|
};
|
||||||
|
|
||||||
|
test = ''
|
||||||
|
echo "checking mas present in systemPackages" >&2
|
||||||
|
test -x ${config.out}/sw/bin/mas
|
||||||
|
|
||||||
|
echo "checking mas activation script uses requested user" >&2
|
||||||
|
grep -- '--user=test-mas-user' ${config.out}/activate
|
||||||
|
|
||||||
|
echo "checking mas desired ids are present" >&2
|
||||||
|
grep 'desiredIds=(' ${config.out}/activate
|
||||||
|
grep '1569813296' ${config.out}/activate
|
||||||
|
grep '497799835' ${config.out}/activate
|
||||||
|
|
||||||
|
echo "checking mas install loop exists" >&2
|
||||||
|
grep 'mas install \"$appId\"' ${config.out}/activate
|
||||||
|
|
||||||
|
echo "checking mas update is triggered" >&2
|
||||||
|
grep 'mas update' ${config.out}/activate
|
||||||
|
|
||||||
|
echo "checking mas parses installedApps from mas list" >&2
|
||||||
|
grep 'declare -A installedApps' ${config.out}/activate
|
||||||
|
grep 'installedApps\[' ${config.out}/activate
|
||||||
|
|
||||||
|
echo "checking mas cleanup log and uninstall" >&2
|
||||||
|
grep 'removing .* from App Store' ${config.out}/activate
|
||||||
|
grep 'runAsUser .*/mas uninstall' ${config.out}/activate
|
||||||
|
|
||||||
|
echo "checking homebrew.masApps ids are kept during cleanup" >&2
|
||||||
|
grep 'homebrewIds=(' ${config.out}/activate
|
||||||
|
grep '424242' ${config.out}/activate
|
||||||
|
'';
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue