lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 1f9e77a01e38c59e0b3bd47180a74f52e8e5625e
parent dd548294ec25f66a61165f1d7b916958b997f745
Author: triesap <tyson@radroots.org>
Date:   Thu, 20 Aug 2026 19:27:26 +0000

build: add service apps and shell

- expose the built service binary as the default flake application
- add a declared-input release-acceptance application for service-owned checks
- provide a pinned toolchain development shell with validated native inputs
- qualify app execution, shell commands, and fail-closed helper inputs in the fixture

Diffstat:
Abuild/nix/service/apps.nix | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbuild/nix/service/default.nix | 2++
Abuild/nix/service/devshell.nix | 46++++++++++++++++++++++++++++++++++++++++++++++
Mbuild/nix/service/fixture.nix | 157++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mflake.nix | 30++++++++++++++++++------------
5 files changed, 289 insertions(+), 26 deletions(-)

diff --git a/build/nix/service/apps.nix b/build/nix/service/apps.nix @@ -0,0 +1,80 @@ +{ + lib, + pkgs, +}: +{ + serviceName, + package, + binaryName, + toolchain, + nativeInputs, + releaseAcceptanceCommand, +}: +assert lib.assertMsg ( + builtins.isString serviceName && builtins.match "^[a-z][a-z0-9_]*$" serviceName != null +) "serviceName must be a lowercase snake-case identifier"; +assert lib.assertMsg (lib.isDerivation package) "package must be a derivation"; +assert lib.assertMsg ( + builtins.isString binaryName && builtins.match "^[a-z][a-z0-9_-]*$" binaryName != null +) "binaryName must be a lowercase Cargo binary identifier"; +assert lib.assertMsg (lib.isDerivation toolchain) "toolchain must be a derivation"; +assert lib.assertMsg ( + builtins.isAttrs nativeInputs + && builtins.isList (nativeInputs.nativeBuildInputs or null) + && builtins.isList (nativeInputs.buildInputs or null) + && builtins.isAttrs (nativeInputs.environment or null) +) "nativeInputs must come from mkNativeInputs"; +assert lib.assertMsg (lib.all lib.isDerivation ( + nativeInputs.nativeBuildInputs ++ nativeInputs.buildInputs +)) "nativeInputs must contain only derivations"; +assert lib.assertMsg (lib.all builtins.isString ( + builtins.attrValues nativeInputs.environment +)) "nativeInputs.environment values must be strings"; +assert lib.assertMsg (lib.all (name: builtins.match "^[A-Za-z_][A-Za-z0-9_]*$" name != null) ( + builtins.attrNames nativeInputs.environment +)) "nativeInputs.environment names must be shell identifiers"; +assert lib.assertMsg (lib.all (name: !(builtins.hasAttr name nativeInputs.environment)) [ + "CARGO" + "PATH" + "RUSTC" + "RUSTC_WRAPPER" + "RUSTC_WORKSPACE_WRAPPER" + "RUSTUP_TOOLCHAIN" +]) "nativeInputs.environment must not replace the selected toolchain"; +assert lib.assertMsg ( + builtins.isString releaseAcceptanceCommand && releaseAcceptanceCommand != "" +) "releaseAcceptanceCommand must be a non-empty string"; +let + environmentExports = lib.concatStringsSep "\n" ( + lib.mapAttrsToList ( + name: value: "export ${name}=${lib.escapeShellArg value}" + ) nativeInputs.environment + ); + releaseAcceptance = pkgs.writeShellApplication { + name = "${serviceName}-release-acceptance"; + runtimeInputs = lib.unique ( + [ + toolchain + package + ] + ++ nativeInputs.nativeBuildInputs + ++ nativeInputs.buildInputs + ); + text = '' + ${environmentExports} + ${releaseAcceptanceCommand} + ''; + }; +in +{ + default = { + type = "app"; + program = "${package}/bin/${binaryName}"; + meta.description = "Run the built ${serviceName} service"; + }; + release-acceptance = { + type = "app"; + program = "${releaseAcceptance}/bin/${serviceName}-release-acceptance"; + meta.description = "Run the ${serviceName} release-acceptance command"; + }; +} diff --git a/build/nix/service/default.nix b/build/nix/service/default.nix @@ -9,5 +9,7 @@ mkNativeInputs = import ./native-inputs.nix { inherit lib; }; mkServicePackage = import ./package.nix { inherit crane lib pkgs; }; mkServiceChecks = import ./checks.nix { inherit crane lib pkgs; }; + mkServiceApps = import ./apps.nix { inherit lib pkgs; }; + mkServiceDevShell = import ./devshell.nix { inherit lib pkgs; }; mkServiceOutputs = import ./compose.nix { inherit lib; }; } diff --git a/build/nix/service/devshell.nix b/build/nix/service/devshell.nix @@ -0,0 +1,46 @@ +{ + lib, + pkgs, +}: +{ + serviceName, + toolchain, + nativeInputs, +}: +assert lib.assertMsg ( + builtins.isString serviceName && builtins.match "^[a-z][a-z0-9_]*$" serviceName != null +) "serviceName must be a lowercase snake-case identifier"; +assert lib.assertMsg (lib.isDerivation toolchain) "toolchain must be a derivation"; +assert lib.assertMsg ( + builtins.isAttrs nativeInputs + && builtins.isList (nativeInputs.nativeBuildInputs or null) + && builtins.isList (nativeInputs.buildInputs or null) + && builtins.isAttrs (nativeInputs.environment or null) +) "nativeInputs must come from mkNativeInputs"; +assert lib.assertMsg (lib.all lib.isDerivation ( + nativeInputs.nativeBuildInputs ++ nativeInputs.buildInputs +)) "nativeInputs must contain only derivations"; +assert lib.assertMsg (lib.all builtins.isString ( + builtins.attrValues nativeInputs.environment +)) "nativeInputs.environment values must be strings"; +assert lib.assertMsg (lib.all (name: builtins.match "^[A-Za-z_][A-Za-z0-9_]*$" name != null) ( + builtins.attrNames nativeInputs.environment +)) "nativeInputs.environment names must be shell identifiers"; +assert lib.assertMsg (lib.all (name: !(builtins.hasAttr name nativeInputs.environment)) [ + "CARGO" + "PATH" + "RUSTC" + "RUSTC_WRAPPER" + "RUSTC_WORKSPACE_WRAPPER" + "RUSTUP_TOOLCHAIN" +]) "nativeInputs.environment must not replace the selected toolchain"; +pkgs.mkShell { + name = "${serviceName}-dev-shell"; + packages = lib.unique ([ toolchain ] ++ nativeInputs.nativeBuildInputs); + buildInputs = nativeInputs.buildInputs; + shellHook = lib.concatStringsSep "\n" ( + lib.mapAttrsToList ( + name: value: "export ${name}=${lib.escapeShellArg value}" + ) nativeInputs.environment + ); +} diff --git a/build/nix/service/fixture.nix b/build/nix/service/fixture.nix @@ -20,6 +20,28 @@ let binaryName = "fixture-service"; releaseProfile = "release"; }; + apps = service.mkServiceApps { + serviceName = "fixture_service"; + inherit nativeInputs package toolchain; + binaryName = "fixture-service"; + releaseAcceptanceCommand = '' + output="$(fixture-service --help)" + if [ "$output" != "fixture-service" ]; then + echo "fixture release acceptance observed unexpected output" >&2 + exit 1 + fi + printf '%s\n' "fixture-service release acceptance" + ''; + }; + devShell = service.mkServiceDevShell { + serviceName = "fixture_service"; + inherit nativeInputs toolchain; + }; + appSmoke = pkgs.runCommand "fixture-service-app-smoke" { } '' + test "$(${apps.default.program} --help)" = "fixture-service" + test "$(${apps.release-acceptance.program})" = "fixture-service release acceptance" + touch "$out" + ''; hooks = { sqlx = pkgs.runCommand "fixture-service-sqlx" { } '' if grep -F "sqlx" ${fixtureSource}/Cargo.toml; then @@ -77,22 +99,16 @@ let ; source = fixtureSource; cargoLock = fixtureSource + "/Cargo.lock"; - extraChecks.smoke = smoke; + extraChecks = { + app-smoke = appSmoke; + inherit smoke; + }; }; outputs = service.mkServiceOutputs { serviceName = "fixture_service"; inherit nativeInputs package; - inherit checks; - apps.default = { - type = "app"; - program = "${package}/bin/fixture-service"; - }; - devShells.default = pkgs.mkShell { - packages = nativeInputs.nativeBuildInputs; - shellHook = '' - export RADROOTS_SERVICE_FIXTURE=${lib.escapeShellArg nativeInputs.environment.RADROOTS_SERVICE_FIXTURE} - ''; - }; + inherit apps checks; + devShells.default = devShell; }; invalidName = builtins.tryEval ( (service.mkServiceOutputs { @@ -236,6 +252,107 @@ let } )).check.outPath ); + appArgs = { + serviceName = "fixture_service"; + inherit nativeInputs package toolchain; + binaryName = "fixture-service"; + releaseAcceptanceCommand = "fixture-service --help"; + }; + invalidAppResults = [ + (builtins.tryEval ( + (service.mkServiceApps (appArgs // { serviceName = "../fixture"; })).default.program + )) + (builtins.tryEval ( + (service.mkServiceApps (appArgs // { package = "not-a-derivation"; })).default.program + )) + (builtins.tryEval ( + (service.mkServiceApps (appArgs // { binaryName = "fixture service"; })).default.program + )) + (builtins.tryEval ( + (service.mkServiceApps (appArgs // { toolchain = "not-a-derivation"; })).default.program + )) + (builtins.tryEval ((service.mkServiceApps (appArgs // { nativeInputs = { }; })).default.program)) + (builtins.tryEval ( + (service.mkServiceApps ( + appArgs + // { + nativeInputs = service.mkNativeInputs { nativeBuildInputs = [ "not-a-derivation" ]; }; + } + )).default.program + )) + (builtins.tryEval ( + (service.mkServiceApps ( + appArgs + // { + nativeInputs = service.mkNativeInputs { environment.VALUE = 1; }; + } + )).default.program + )) + (builtins.tryEval ( + (service.mkServiceApps ( + appArgs + // { + nativeInputs = service.mkNativeInputs { environment."INVALID-NAME" = "value"; }; + } + )).default.program + )) + (builtins.tryEval ( + (service.mkServiceApps ( + appArgs + // { + nativeInputs = service.mkNativeInputs { environment.PATH = "/tmp"; }; + } + )).default.program + )) + (builtins.tryEval ( + (service.mkServiceApps (appArgs // { releaseAcceptanceCommand = ""; })).default.program + )) + ]; + devShellArgs = { + serviceName = "fixture_service"; + inherit nativeInputs toolchain; + }; + invalidDevShellResults = [ + (builtins.tryEval ( + (service.mkServiceDevShell (devShellArgs // { serviceName = "../fixture"; })).drvPath + )) + (builtins.tryEval ( + (service.mkServiceDevShell (devShellArgs // { toolchain = "not-a-derivation"; })).drvPath + )) + (builtins.tryEval ((service.mkServiceDevShell (devShellArgs // { nativeInputs = { }; })).drvPath)) + (builtins.tryEval ( + (service.mkServiceDevShell ( + devShellArgs + // { + nativeInputs = service.mkNativeInputs { buildInputs = [ "not-a-derivation" ]; }; + } + )).drvPath + )) + (builtins.tryEval ( + (service.mkServiceDevShell ( + devShellArgs + // { + nativeInputs = service.mkNativeInputs { environment.VALUE = 1; }; + } + )).drvPath + )) + (builtins.tryEval ( + (service.mkServiceDevShell ( + devShellArgs + // { + nativeInputs = service.mkNativeInputs { environment."INVALID-NAME" = "value"; }; + } + )).drvPath + )) + (builtins.tryEval ( + (service.mkServiceDevShell ( + devShellArgs + // { + nativeInputs = service.mkNativeInputs { environment.RUSTC = "/tmp/rustc"; }; + } + )).drvPath + )) + ]; in assert service.supportedSystems == [ @@ -252,6 +369,7 @@ assert outputs.packages.default == package; assert outputs.checks == checks; assert builtins.attrNames checks == [ + "app-smoke" "check" "clippy" "config" @@ -269,9 +387,18 @@ assert checks.sqlx == hooks.sqlx; assert checks.config == hooks.config; assert checks.source-lock == hooks.source-lock; assert checks.integration == hooks.integration; +assert checks.app-smoke == appSmoke; assert checks.smoke == smoke; -assert outputs.apps.default.program == "${package}/bin/fixture-service"; -assert outputs.devShells.default != null; +assert outputs.apps == apps; +assert + builtins.attrNames apps == [ + "default" + "release-acceptance" + ]; +assert apps.default.program == "${package}/bin/fixture-service"; +assert lib.hasSuffix "/bin/fixture_service-release-acceptance" apps.release-acceptance.program; +assert outputs.devShells.default == devShell; +assert devShell.name == "fixture_service-dev-shell"; assert invalidName.success == false; assert defaultOverride.success == false; assert invalidPackage.success == false; @@ -286,6 +413,8 @@ assert unexpectedHook.success == false; assert lib.all (result: result.success == false) weakenedPolicyResults; assert invalidExtraCheck.success == false; assert standardOverride.success == false; +assert lib.all (result: result.success == false) invalidAppResults; +assert lib.all (result: result.success == false) invalidDevShellResults; { inherit outputs; } diff --git a/flake.nix b/flake.nix @@ -57,15 +57,17 @@ { treefmt = import ./treefmt.nix; - apps = import ./build/nix/apps.nix { - inherit - common - config - lib - pkgs - toolchains - ; - }; + apps = + (import ./build/nix/apps.nix { + inherit + common + config + lib + pkgs + toolchains + ; + }) + // serviceFixture.outputs.apps; checks = lib.filterAttrs (_: value: value != null) ( (import ./build/nix/checks.nix { @@ -74,9 +76,13 @@ // serviceFixture.outputs.checks ); - devShells = import ./build/nix/devshells.nix { - inherit common pkgs toolchains; - }; + devShells = + (import ./build/nix/devshells.nix { + inherit common pkgs toolchains; + }) + // { + service-fixture = serviceFixture.outputs.devShells.default; + }; packages = serviceFixture.outputs.packages // { xtask = common.xtaskPackage;