commit 25c31e015969ea48307092158c5aaf6be03485d0
parent d02cb3a24fa7df0304e537501ec3bb3b90602d24
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 18 Jun 2026 09:32:50 -0700
Add build env passthrough shorthand
Diffstat:
3 files changed, 109 insertions(+), 16 deletions(-)
diff --git a/doc/BUILD_COORDINATOR.md b/doc/BUILD_COORDINATOR.md
@@ -103,6 +103,11 @@ Configuration has two scopes, distinguished by visibility:
applied along the `need` path from the root. It is canonicalized to byte-stable
text and content-addressed: `config-id = BLAKE2b(canonical map)`, stored as a
CAS blob so any recorded `config-id` resolves back to the actual map.
+ Driver spelling is `--config K=V`. For recipe environment pass-through,
+ `--config env.NAME` copies the current process's `$NAME` value into config key
+ `env.NAME`; `--env NAME` and `--env NAME=VALUE` are shorthand for
+ `--config env.NAME` and `--config env.NAME=VALUE`. The pass-through forms
+ require `NAME` to be set in the current environment.
- **Local configuration** is the target's **argv**, supplied entirely by the
build request (both the top-level request and a `need` carry an optional argv);
@@ -673,8 +678,8 @@ command.
| `config-get <key>` | the propagated value (or *unset*) | config dep: key recorded as consumed; its value is the one in the effective `config-id` map (absent = unset) |
| `source <path>` | `blob-id` (or *absent*) + a path to read | source dep: `(path, blob-id)`, or `(path, absent)` |
| `glob <pattern>` | sorted list of matching paths (streamed) | glob dep: `(pattern, glob-result-hash)` |
-| `need <target> [k=v…] [argv…]` | the dep's output `tree-id` + a path (blocks) | target dep edge: `(dep, dep-config-id, dep-argv-id, output-tree-id)` |
-| `need-submit <target> [k=v…] [argv…]` | a **token** (does not block) | *(nothing yet — logged on await)* |
+| `need <target> [k=v…] [env…] [argv…]` | the dep's output `tree-id` + a path (blocks) | target dep edge: `(dep, dep-config-id, dep-argv-id, output-tree-id)` |
+| `need-submit <target> [k=v…] [env…] [argv…]` | a **token** (does not block) | *(nothing yet — logged on await)* |
| `need-await <token>` | the submitted dep's output `tree-id` + a path | target dep edge: `(dep, dep-config-id, dep-argv-id, output-tree-id)` |
- **`config-get`** reads the target's *effective propagated* config and records the
@@ -687,6 +692,9 @@ command.
`env.` prefix (e.g. `env.PATH`) become the recipe's environment variables, so a
recipe's env is tracked, hermetic, and propagates like any config — the spawned
process otherwise gets a *clean* environment (nothing ambient inherited).
+ `--env PATH` is the CLI shorthand for copying the driver's current `$PATH` into
+ config key `env.PATH`; `--env PATH=/bin` sets that key explicitly. If `$PATH`
+ is unset, `--env PATH` / `--config env.PATH` is rejected.
- **`source`** hands back a path inside the **live workspace** (not a staged
copy) plus the blob-id the read is pinned to; an absent path is reported and
recorded as an *absent* leaf (creating it later busts the trace). Returning the
@@ -697,8 +705,9 @@ command.
**streamed** across as many frames as needed, so an arbitrarily large match set
is not bounded by a single frame.
- **`need`** is the dynamic-dependency primitive. The optional `k=v` pairs
- **overlay** propagated config for that sub-build; an optional argv vector sets
- the dep's **local argv** (omitted ⇒ the empty argv; local argv never
+ **overlay** propagated config for that sub-build; `--env NAME` /
+ `--env NAME=value` are accepted as env-prefixed config overlays; an optional
+ argv vector sets the dep's **local argv** (omitted ⇒ the empty argv; local argv never
propagates). The coordinator resolves `(dep, cfg ⊕ overrides, argv)`
recursively, returns its output `tree-id` and a readable path, and records the
edge with the dep's `config-id` and `argv-id` so the shallow path can re-resolve
diff --git a/driver/cmd/build_coord.c b/driver/cmd/build_coord.c
@@ -15,6 +15,7 @@
#define BUILD_TOOL "build"
#define BUILD_DEFAULT_DEF "BUILD.kit"
#define BUILD_MAX_CONFIG 128u
+#define BUILD_MAX_CONFIG_KEY 64u
#define BUILD_MAX_ARGS 128u
typedef struct BuildCli {
@@ -23,6 +24,7 @@ typedef struct BuildCli {
const char* def;
const char* target;
KitBuildKV config[BUILD_MAX_CONFIG];
+ char config_keys[BUILD_MAX_CONFIG][BUILD_MAX_CONFIG_KEY];
size_t nconfig;
KitSlice args[BUILD_MAX_ARGS];
size_t nargs;
@@ -37,17 +39,21 @@ void driver_help_build(void) {
"\n"
"USAGE\n"
" kit build [--store DIR] [--root DIR] [--def FILE]\n"
- " [--config K=V]... [--verify] [--stats] TARGET [-- ARG...]\n"
+ " [--config K=V|env.NAME]... [--env NAME[=V]]...\n"
+ " [--verify] [--stats] TARGET [-- ARG...]\n"
" kit build test [--store DIR] [--root DIR] [--def FILE]\n"
- " [--config K=V]... [--verify] [--stats] TARGET [-- ARG...]\n"
+ " [--config K=V|env.NAME]... [--env NAME[=V]]...\n"
+ " [--verify] [--stats] TARGET [-- ARG...]\n"
" kit build config-get KEY # inside a build recipe\n"
" kit build source PATH # inside a build recipe\n"
" kit build fetch BLOB URL... # inside a build recipe\n"
" kit build depfile [--lines] FILE # inside a build recipe\n"
" kit build glob PATTERN # inside a build recipe\n"
- " kit build need [--config K=V]... TARGET [-- ARG...]\n"
+ " kit build need [--config K=V|env.NAME]... [--env NAME[=V]]...\n"
+ " TARGET [-- ARG...]\n"
" # inside a build recipe\n"
- " kit build need-submit [--config K=V]... TARGET [-- ARG...]\n"
+ " kit build need-submit [--config K=V|env.NAME]... [--env NAME[=V]]...\n"
+ " TARGET [-- ARG...]\n"
" # inside a build recipe\n"
" kit build need-await TOKEN # inside a build recipe\n"
"\n"
@@ -56,6 +62,8 @@ void driver_help_build(void) {
" --root DIR Workspace root (default: .)\n"
" --def FILE Build definition path (default: BUILD.kit)\n"
" --config K=V Seed propagated configuration\n"
+ " --config env.N Seed env.N from current $N\n"
+ " --env N[=V] Shorthand for --config env.N[=V]\n"
" --verify Verify the returned output tree in the CAS\n"
" --stats Print build-resolution counters to stderr\n");
}
@@ -91,16 +99,68 @@ static int build_join_path(char* out, size_t cap, const char* a,
return 0;
}
+static int build_add_config(BuildCli* cli, KitSlice key, KitSlice value) {
+ if (!cli || !key.s || key.len == 0u || cli->nconfig >= BUILD_MAX_CONFIG)
+ return 1;
+ cli->config[cli->nconfig].key = key;
+ cli->config[cli->nconfig].value = value;
+ ++cli->nconfig;
+ return 0;
+}
+
+static int build_env_name_valid(const char* name, size_t len) {
+ size_t i;
+ if (!name || len == 0u) return 0;
+ for (i = 0; i < len; ++i) {
+ unsigned char c = (unsigned char)name[i];
+ if (c == '\0' || c == '=' || c == '\n' || c == '\r') return 0;
+ }
+ return 1;
+}
+
+static int build_add_env_config(BuildCli* cli, const char* name, size_t name_len,
+ const char* value) {
+ char* key;
+ if (!cli || !build_env_name_valid(name, name_len) || !value ||
+ cli->nconfig >= BUILD_MAX_CONFIG ||
+ sizeof(KIT_BUILD_ENV_PREFIX) - 1u + name_len >= BUILD_MAX_CONFIG_KEY)
+ return 1;
+ key = cli->config_keys[cli->nconfig];
+ memcpy(key, KIT_BUILD_ENV_PREFIX, sizeof(KIT_BUILD_ENV_PREFIX) - 1u);
+ memcpy(key + sizeof(KIT_BUILD_ENV_PREFIX) - 1u, name, name_len);
+ key[sizeof(KIT_BUILD_ENV_PREFIX) - 1u + name_len] = '\0';
+ return build_add_config(cli, kit_slice_cstr(key), kit_slice_cstr(value));
+}
+
static int build_parse_config(BuildCli* cli, const char* text) {
const char* eq;
+ size_t env_prefix_len = sizeof(KIT_BUILD_ENV_PREFIX) - 1u;
if (!cli || !text) return 1;
eq = driver_strchr(text, '=');
- if (!eq || eq == text || cli->nconfig >= BUILD_MAX_CONFIG) return 1;
- cli->config[cli->nconfig].key.s = text;
- cli->config[cli->nconfig].key.len = (size_t)(eq - text);
- cli->config[cli->nconfig].value = kit_slice_cstr(eq + 1);
- ++cli->nconfig;
- return 0;
+ if (!eq) {
+ const char* value;
+ if (!driver_strneq(text, KIT_BUILD_ENV_PREFIX, env_prefix_len) ||
+ text[env_prefix_len] == '\0')
+ return 1;
+ value = driver_getenv(text + env_prefix_len);
+ if (!value) return 1;
+ return build_add_config(cli, kit_slice_cstr(text), kit_slice_cstr(value));
+ }
+ if (eq == text) return 1;
+ return build_add_config(cli, (KitSlice){.s = text, .len = (size_t)(eq - text)},
+ kit_slice_cstr(eq + 1));
+}
+
+static int build_parse_env(BuildCli* cli, const char* text) {
+ const char* eq;
+ if (!cli || !text) return 1;
+ eq = driver_strchr(text, '=');
+ if (!eq) {
+ const char* value = driver_getenv(text);
+ if (!value) return 1;
+ return build_add_env_config(cli, text, driver_strlen(text), value);
+ }
+ return build_add_env_config(cli, text, (size_t)(eq - text), eq + 1);
}
static int build_client_glob_print(void* user, KitSlice path) {
@@ -139,7 +199,12 @@ static int build_client_need_parse(BuildCli* cli, int argc, char** argv,
const char* a = argv[i];
if (driver_streq(a, "--config") && i + 1 < argc) {
if (build_parse_config(cli, argv[++i]) != 0) {
- driver_errf(BUILD_TOOL, "bad --config, expected K=V");
+ driver_errf(BUILD_TOOL, "bad --config, expected K=V or env.NAME");
+ return 2;
+ }
+ } else if (driver_streq(a, "--env") && i + 1 < argc) {
+ if (build_parse_env(cli, argv[++i]) != 0) {
+ driver_errf(BUILD_TOOL, "bad --env, expected NAME or NAME=VALUE");
return 2;
}
} else if (driver_streq(a, "--")) {
@@ -395,7 +460,12 @@ static int build_parse_args(BuildCli* cli, int argc, char** argv, int first_arg,
cli->def = argv[++i];
} else if (driver_streq(a, "--config") && i + 1 < argc) {
if (build_parse_config(cli, argv[++i]) != 0) {
- driver_errf(BUILD_TOOL, "bad --config, expected K=V");
+ driver_errf(BUILD_TOOL, "bad --config, expected K=V or env.NAME");
+ return 2;
+ }
+ } else if (driver_streq(a, "--env") && i + 1 < argc) {
+ if (build_parse_env(cli, argv[++i]) != 0) {
+ driver_errf(BUILD_TOOL, "bad --env, expected NAME or NAME=VALUE");
return 2;
}
} else if (driver_streq(a, "--")) {
diff --git a/test/buildcoord/run.sh b/test/buildcoord/run.sh
@@ -1123,6 +1123,20 @@ env_two_path=$(tree_path_from "$work/buildcoord-spec-env-change-invalidates.out"
contains "buildcoord-spec-env-change-output" "$env_two_path/env.txt" \
"declared:two"
+build_assert_ok buildcoord-env-shorthand-set --stats \
+ --env DECLARED=three //env:probe
+env_three_path=$(tree_path_from "$work/buildcoord-env-shorthand-set.out")
+contains "buildcoord-env-shorthand-set-output" "$env_three_path/env.txt" \
+ "declared:three"
+
+export DECLARED=from-current-env
+build_assert_ok buildcoord-env-config-passthrough --stats \
+ --config env.DECLARED //env:probe
+unset DECLARED
+env_current_path=$(tree_path_from "$work/buildcoord-env-config-passthrough.out")
+contains "buildcoord-env-config-passthrough-output" \
+ "$env_current_path/env.txt" "declared:from-current-env"
+
build_assert_ok buildcoord-spec-nondet-cold //nondet:counter
run_fail "buildcoord-spec-verify-reruns-cache-hit" \
"$KIT" build --store "$store" --root "$ws" --def BUILD.kit \