Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .cli-flags.toml
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,13 @@ help = "Project-local schema-v2 environment plan path."
env = "ZED_PKG_ENV_OUTPUT"
aliases = ["output"]
type = "string"
default = ".mise.toml"
help = "Project-local manager export path."
help = "Project-local manager export path; defaults are manager-specific."

[commands.env.commands.export.flags.env_receipt]
env = "ZED_PKG_ENV_RECEIPT"
aliases = ["receipt"]
type = "string"
help = "Zed-owned deterministic receipt path for Devbox or Flox export."

[commands.env.commands.export.flags.env_check]
env = "ZED_PKG_ENV_CHECK"
Expand Down
82 changes: 77 additions & 5 deletions .github/workflows/devbox-flox-export.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,33 @@ name: Devbox and Flox environment export
on:
pull_request:
paths:
- ".cli-flags.toml"
- "README.md"
- "src/cli.rs"
- "src/main.rs"
- "src/lib.rs"
- "src/environment_export_cli.rs"
- "src/nix_environment_export.rs"
- "src/bin/zed-env-export.rs"
- "tests/nix_environment_export_cli.rs"
- "tests/nix_environment_export_main_cli.rs"
- "docs/devbox-flox-interop.md"
- ".github/workflows/devbox-flox-export.yml"
- "Cargo.toml"
- "Cargo.lock"
push:
branches: [main]
paths:
- ".cli-flags.toml"
- "README.md"
- "src/cli.rs"
- "src/main.rs"
- "src/lib.rs"
- "src/environment_export_cli.rs"
- "src/nix_environment_export.rs"
- "src/bin/zed-env-export.rs"
- "tests/nix_environment_export_cli.rs"
- "tests/nix_environment_export_main_cli.rs"
- "docs/devbox-flox-interop.md"
- ".github/workflows/devbox-flox-export.yml"
- "Cargo.toml"
Expand All @@ -37,7 +51,7 @@ jobs:
deterministic-export:
name: deterministic export / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
timeout-minutes: 35
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -65,15 +79,73 @@ jobs:
run: cargo fmt --all --check

- name: Unit tests
run: cargo test --locked --bin zed-env-export
run: |
set -euo pipefail
cargo test --locked --lib environment_export_cli::tests
cargo test --locked --lib nix_environment_export::tests

- name: Black-box CLI tests
run: cargo test --locked --test nix_environment_export_cli
- name: Staged and canonical CLI tests
run: |
set -euo pipefail
cargo test --locked --test nix_environment_export_cli
cargo test --locked --test nix_environment_export_main_cli
cargo test --locked cli::tests::environment_export_is_typed_and_rejects_ambiguous_write_modes
cargo test --locked cli::tests::cli_flags_toml_is_in_sync_with_clap
cargo test --locked cli::tests::readme_documents_every_command

- name: Clippy (strict)
run: |
set -euo pipefail
cargo clippy --locked --bin zed-env-export --test nix_environment_export_cli -- -D warnings
cargo clippy --locked --bin zed --bin zed-env-export \
--test nix_environment_export_cli \
--test nix_environment_export_main_cli -- -D warnings

- name: Repeated deterministic parity canary
run: |
set -euo pipefail
cargo build --locked --bin zed --bin zed-env-export
zed_bin="$GITHUB_WORKSPACE/target/debug/zed"
staged_bin="$GITHUB_WORKSPACE/target/debug/zed-env-export"
if [[ "$RUNNER_OS" == "Windows" ]]; then
zed_bin="${zed_bin}.exe"
staged_bin="${staged_bin}.exe"
fi
root="$RUNNER_TEMP/devbox-flox-parity"
for side in canonical staged; do
mkdir -p "$root/$side/.zed"
cat > "$root/$side/.zed/environment-plan.json" <<'JSON'
{
"schema": 1,
"tools": {
"node": {
"requirement": "^22",
"resolved": "22.11.0",
"provider": "nixpkgs",
"backend": "nodejs_22",
"checksums": [{"algorithm": "sha256", "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],
"platforms": []
}
},
"system-packages": {},
"platforms": ["x86_64-linux"],
"activation": "frozen-install",
"sources": []
}
JSON
done
(
cd "$root/canonical"
"$zed_bin" env export devbox --json > result.json
)
(
cd "$root/staged"
"$staged_bin" devbox --json > result.json
)
cmp "$root/canonical/result.json" "$root/staged/result.json"
cmp "$root/canonical/devbox.json" "$root/staged/devbox.json"
cmp \
"$root/canonical/.zed/environment-exports/devbox.json" \
"$root/staged/.zed/environment-exports/devbox.json"

- name: Prove no generated files leaked into checkout
run: |
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ registry hosts both on S3/Cloudflare R2.
| `zed env import mise [--config PATH] [--lock PATH] [--frozen] [--json]` | Import the supported project-local mise tool/lock subset as the shared normalized `EnvironmentPlan`; never loads parent/global config or executes hooks |
| `zed env verify mise [--config PATH] [--lock PATH] --frozen [--json]` | Fail closed on missing lock coverage, drift, malformed checksums, unsupported semantics, or non-portable frozen state and report the stable plan digest |
| `zed env export mise --plan PATH [--output .mise.toml] [--check|--write] [--json]` | Deterministically project a schema-v2 environment plan into conflict-safe project-local mise TOML without invoking mise or executing project code |
| `zed env export devbox [--plan PATH] [--output devbox.json] [--receipt PATH] [--json]` | Deterministically generate Devbox JSON and a Zed-owned receipt without invoking Devbox |
| `zed env export flox [--plan PATH] [--output .flox/env/manifest.toml] [--receipt PATH] [--json]` | Deterministically generate Flox manifest TOML and a Zed-owned receipt without invoking Flox |
| `zed env import asdf [--config .tool-versions] [--lock .zed/asdf.lock.toml] [--frozen] [--json]` | Import project-local asdf selections and optional immutable plugin/artifact provenance without invoking asdf or plugin code |
| `zed env verify asdf [--config .tool-versions] [--lock .zed/asdf.lock.toml] --frozen [--json]` | Verify exact asdf tool, plugin revision, artifact SHA-256, platform, and normalized plan identity without reading parent/global configuration |
| `zed task list\|info\|graph\|run ...` | Use the shared schema-v2 runtime to discover, inspect, graph, dry-run, execute, confirm, parallelize, and content-cache project tasks; `zed-task` remains a compatibility binary |
Expand Down
65 changes: 47 additions & 18 deletions docs/devbox-flox-interop.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,57 @@
# Deterministic Devbox and Flox exports

`zed-env-export` is the staged executable for the first DEN-1468 export slice.
It consumes the shared, frozen `EnvironmentPlan` and emits manager-native
configuration without invoking Devbox, Flox, Nix, package resolution, or any
activation hook.
The canonical product surface consumes the shared frozen `EnvironmentPlan` and
emits manager-native configuration without invoking Devbox, Flox, Nix, package
resolution, a shell, or an activation hook:

The module is kept separate from the moving `zed env` dispatcher so it can be
wired into `zed env export devbox|flox` after the common command branch lands
without reimplementing generation.
```console
zed env export devbox [--plan PATH] [--output PATH] [--receipt PATH] [--json]
zed env export flox [--plan PATH] [--output PATH] [--receipt PATH] [--json]
```

## Commands
The separately installed compatibility binary remains available:

```text
```console
zed-env-export devbox [--plan PATH] [--out PATH] [--receipt PATH] [--json]
zed-env-export flox [--plan PATH] [--out PATH] [--receipt PATH] [--json]
```

Defaults:
Both binaries call `zed_cli::environment_export_cli`, which delegates to one
`nix_environment_export` renderer and one persistence implementation. Output,
receipts, diagnostics, idempotence, and error behavior therefore cannot drift
between the canonical and staged routes.

## Defaults and flags-to-environment contract

| Manager | Output | Receipt |
| --- | --- | --- |
| Devbox | `devbox.json` | `.zed/environment-exports/devbox.json` |
| Flox | `.flox/env/manifest.toml` | `.zed/environment-exports/flox.json` |

The default input is `.zed/environment-plan.json`. Every path must be normalized,
project-relative, and free of symlink traversal outside the project.
The default input is `.zed/environment-plan.json`. Canonical flags use:

- `ZED_PKG_ENV_PLAN`
- `ZED_PKG_ENV_OUTPUT`
- `ZED_PKG_ENV_RECEIPT`
- `ZED_PKG_ENV_JSON`

The staged binary retains `ZED_PKG_ENV_OUT` as a compatibility spelling for its
`--out` option. The canonical command does not create a second renderer or a
second receipt format.

`--check` and `--write` remain mise-only because Devbox/Flox export is already a
fail-closed write operation: absent files are created, byte-identical files are
accepted unchanged, and any differing existing output or receipt is rejected.
`--receipt` is rejected for mise export. Import and verify retain their separate
`mise|asdf` manager enum; `devbox` and `flox` are export-only values.

Every path must be normalized, project-relative, and free of symlink traversal
outside the project.

## Mapping contract

The initial exporter supports explicit Nixpkgs-backed entries only. It never
guesses that a language name maps one-to-one to a Nix attribute.
The exporter supports explicit Nixpkgs-backed entries only. It never guesses
that a language name maps one-to-one to a Nix attribute.

A tool uses:

Expand Down Expand Up @@ -77,7 +99,7 @@ entries with `pkg-path`, `version`, and optional `systems`, an optional global
`[options].systems`, and the same fixed activation command under
`[hook].on-activate`.

The initial Flox platform subset is the common Darwin/Linux matrix:
The certified Flox platform subset is:

- `aarch64-darwin`
- `aarch64-linux`
Expand Down Expand Up @@ -115,6 +137,13 @@ writes; if receipt persistence fails after a new output was installed, the new
output is removed so the command fails closed.

Semantic merging of pre-existing human manager configuration remains a later
adapter phase. This first PR chooses explicit conflict diagnostics rather than
silently deleting scripts, services, includes, environment variables, or other
manager-owned state.
adapter phase. The current exporter chooses explicit conflict diagnostics rather
than silently deleting scripts, services, includes, environment variables, or
other manager-owned state.

## Certification

The permanent Ubuntu, macOS, and Windows workflow runs unit tests, staged and
canonical compiled-CLI tests, strict Clippy, checkout-cleanliness checks, and a
repeated parity canary that compares canonical/staged result JSON, manager
output bytes, and receipt bytes.
57 changes: 15 additions & 42 deletions src/bin/zed-env-export.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
use std::path::{Path, PathBuf};
use std::path::PathBuf;

use clap::{Args, Parser, Subcommand};

#[path = "../nix_environment_export.rs"]
#[allow(private_interfaces)]
mod nix_environment_export;

use nix_environment_export::{ExportManager, export_environment};
use zed_cli::environment_export_cli::{self, ExportOptions};
use zed_cli::nix_environment_export::ExportManager;

#[derive(Debug, Parser)]
#[command(
Expand Down Expand Up @@ -55,40 +51,17 @@ fn main() {
}

fn run(cli: Cli) -> anyhow::Result<()> {
let root = std::env::current_dir()?;
match cli.command {
Command::Devbox(args) => run_one(&root, ExportManager::Devbox, args),
Command::Flox(args) => run_one(&root, ExportManager::Flox, args),
}
}

fn run_one(root: &Path, manager: ExportManager, args: ExportArgs) -> anyhow::Result<()> {
let result = export_environment(
root,
let (manager, args) = match cli.command {
Command::Devbox(args) => (ExportManager::Devbox, args),
Command::Flox(args) => (ExportManager::Flox, args),
};
environment_export_cli::execute_current_dir(
manager,
Some(&args.plan),
args.out.as_deref(),
args.receipt.as_deref(),
)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
println!(
"{} environment {}: {}",
result.manager.as_str(),
if result.changed {
"exported"
} else {
"unchanged"
},
result.output_path
);
println!("receipt: {}", result.receipt_path);
println!(
"environment-plan-sha256: {}",
result.environment_plan_sha256
);
println!("output-sha256: {}", result.output_sha256);
}
Ok(())
ExportOptions {
plan: Some(args.plan),
output: args.out,
receipt: args.receipt,
json: args.json,
},
)
}
Loading