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:
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;