Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-06
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
## Context

The `uvm_install` crate writes `modules.json` as a side-effect of installation. During development and debugging of the installation pipeline, engineers need to inspect this file for a specific version/platform/architecture without running a full install. The `uvm_live_platform` crate already provides `FetchRelease` to query the Unity release API, and the platform and architecture types already carry a `clap` feature flag that exposes `ValueEnum`.

The `uvm` crate is the only production-facing binary. Adding a feature flag is the lowest-overhead path: no new crates, no new binaries, same build system, naturally excluded from production builds.

## Goals / Non-Goals

**Goals:**
- Add a `dev-commands` feature to the `uvm` crate that compiles in one or more debugging subcommands
- Implement `download-modules-json` as the first command under this feature
- Accept version (required), platform (optional, defaults to current), and architecture (optional, defaults to current) as CLI arguments
- Accept `--output <file>` to write the JSON to a file; default to stdout
- Produce output in the same format as `write_modules_json` in `uvm_install` (`serde_json::to_string_pretty` of a `&[Module]`)

**Non-Goals:**
- Changing any production code paths
- Supporting streaming or incremental output
- Adding caching behaviour (the existing `uvm_live_platform` cache applies transparently)
- Filtering or transforming the module list beyond what the API returns

## Decisions

### Feature flag name: `dev-commands`

Chosen over `debug` or `unstable` because it clearly signals intent — these are commands for developer use, not production distribution. The flag is off by default in `Cargo.toml` so `cargo build` / `cargo install` will never include it.

### Single module file, `#[cfg(feature = "dev-commands")]` throughout

The command lives in `uvm/src/commands/download_modules_json.rs`. The `mod` declaration in `commands/mod.rs` and the `Commands` enum variant in `main.rs` are both wrapped in `#[cfg(feature = "dev-commands")]`. This is consistent with how Rust handles optional features — no indirection or plugin system needed.

### Re-use `uvm_live_platform`'s `clap` feature for `ValueEnum`

`UnityReleaseDownloadPlatform` and `UnityReleaseDownloadArchitecture` already derive `clap::ValueEnum` behind the `clap` feature in `uvm_live_platform`. The `uvm` crate enables this feature in its dependency. Under `dev-commands`, we rely on the same types directly as clap `Args`, so no wrapper types are needed.

**Alternative considered**: Define our own string args and parse manually. Rejected — duplicates existing logic and loses the automatic `--help` value enumeration.

### `FetchRelease` builder with explicit platform/architecture filters

The command calls:
```
FetchRelease::builder(version)
.with_extended_lts()
.with_u7_alpha()
.with_platform(platform)
.with_architecture(architecture)
.fetch()
```
This mirrors the pattern in the existing `modules.rs` command and `uvm_install`.

**Alternative considered**: Fetch all platforms/architectures and filter in memory. Rejected — unnecessary network payload; the API supports server-side filtering.

### Output: stdout default, `--output <file>` override

Writing to stdout makes the command composable (`uvm download-modules-json 2023.1.0f1 | jq …`). The `--output` flag covers the case where the file needs to persist without shell redirection. The implementation writes the serialized bytes to either a `BufWriter<File>` or `io::stdout()` via a shared `Write` trait object.

## Risks / Trade-offs

- **Feature flag discoverability**: Engineers must know to build with `--features dev-commands`. This is documented in the command's help text and in this spec. → Mitigation: add a note to `CLAUDE.md` or a contributing guide if this becomes a frequent source of confusion.
- **Format divergence**: If `write_modules_json` in `uvm_install` changes its serialization format, this command won't automatically follow. → Mitigation: the spec explicitly states both must use `serde_json::to_string_pretty(&[Module])`. A future refactor could extract a shared helper.
- **`uvm_live_platform` `clap` feature always on in `uvm`**: The `uvm` crate already depends on `uvm_live_platform` with the `clap` feature enabled (used by the existing `modules` command). No change needed, no risk.

## Migration Plan

No migration needed — additive, feature-gated change. Existing production builds are unaffected.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Why

During development and debugging of the installation pipeline, engineers need to inspect the raw `modules.json` payload that would be generated for a given Unity version, platform, and architecture — without performing a full installation. No such standalone tool exists today; the only way to obtain a `modules.json` is to run a real install and extract the side-effect file.

## What Changes

- Add a `dev-commands` feature flag to the `uvm` crate that gates developer/debugging subcommands out of production builds
- Add a `download-modules-json` subcommand (behind the feature flag) that fetches release data via `uvm_live_platform` and outputs the serialized `modules.json` content for a given version, platform, and architecture — to stdout by default, or to a file via `--output <file>`

## Capabilities

### New Capabilities
- `download-modules-json-command`: A CLI command that accepts a Unity version, target platform, and target architecture, fetches the corresponding release via `FetchRelease`, and writes the `modules.json` payload to stdout (default) or a file path supplied via `--output`

### Modified Capabilities
<!-- none -->

## Impact

- **`uvm` crate**: new `dev-commands` feature flag; command registered in the CLI only when the feature is enabled
- **`uvm/src/commands/`**: new `download_modules_json.rs` module compiled only under `#[cfg(feature = "dev-commands")]`
- **`uvm_live_platform`**: consumed read-only via `FetchRelease` — no changes needed
- **`uvm_install`**: the `modules.json` serialization format (pretty-printed JSON of `Module` slice) is reused; no source changes required
- **Production builds**: feature is off by default; the subcommand does not appear in release binaries
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
## ADDED Requirements

### Requirement: Feature flag gates the command
The `uvm` crate SHALL expose `download-modules-json` as a subcommand only when compiled with the `dev-commands` feature flag. Production builds (without the flag) SHALL NOT include this subcommand or any of its dependencies.

#### Scenario: Command absent in default build
- **WHEN** `uvm` is built without `--features dev-commands`
- **THEN** `uvm --help` does not list `download-modules-json`

#### Scenario: Command present in dev build
- **WHEN** `uvm` is built with `--features dev-commands`
- **THEN** `uvm --help` lists `download-modules-json`

---

### Requirement: Version argument is required
The command SHALL accept a single positional `<version>` argument in Unity version format (e.g. `2023.1.0f1`). The command SHALL fail with a usage error if the version is omitted or unparseable.

#### Scenario: Valid version provided
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1`
- **THEN** the command proceeds to fetch release data for that version

#### Scenario: Version omitted
- **WHEN** the user runs `uvm download-modules-json` with no arguments
- **THEN** the command exits with a non-zero code and prints a usage error to stderr

---

### Requirement: Platform defaults to current host platform
The command SHALL accept an optional `--platform <platform>` argument accepting `macos`, `linux`, or `windows` (case-insensitive, via `clap::ValueEnum`). When omitted, the platform SHALL default to the host platform.

#### Scenario: Platform omitted on macOS host
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1` on a macOS machine
- **THEN** the command fetches modules for the `macos` platform

#### Scenario: Platform explicitly specified
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1 --platform linux`
- **THEN** the command fetches modules for the `linux` platform regardless of host OS

---

### Requirement: Architecture defaults to current host architecture
The command SHALL accept an optional `--architecture <arch>` argument accepting `x86_64` or `arm64` (case-insensitive, via `clap::ValueEnum`). When omitted, the architecture SHALL default to the host architecture (`x86_64` on Linux regardless of actual arch, per existing platform logic).

#### Scenario: Architecture omitted
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1` on an arm64 macOS machine
- **THEN** the command fetches modules for the `arm64` architecture

#### Scenario: Architecture explicitly overridden
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1 --architecture x86_64`
- **THEN** the command fetches modules for the `x86_64` architecture

---

### Requirement: Output defaults to stdout
When `--output` is not specified, the command SHALL write the serialized JSON to stdout.

#### Scenario: No output flag
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1`
- **THEN** the JSON content is written to stdout
- **THEN** the process exits with code 0

---

### Requirement: Output can be written to a file
The command SHALL accept an optional `--output <path>` argument. When provided, the command SHALL write the JSON to the specified file path, creating the file if it does not exist and overwriting it if it does. If the parent directory does not exist, the command SHALL create it (including all intermediate directories) before writing the file.

#### Scenario: Valid output path provided
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1 --output /tmp/modules.json`
- **THEN** the file `/tmp/modules.json` is created (or overwritten) with the JSON content
- **THEN** nothing is written to stdout
- **THEN** the process exits with code 0

#### Scenario: Output path in non-existent directory
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1 --output /some/new/dir/modules.json`
- **THEN** the directory `/some/new/dir/` is created recursively
- **THEN** the file `/some/new/dir/modules.json` is written with the JSON content
- **THEN** the process exits with code 0

---

### Requirement: JSON format matches modules.json produced during installation
The output SHALL be the pretty-printed JSON serialization (`serde_json::to_string_pretty`) of the `Vec<Module>` slice returned by `FetchRelease` for the given version, platform, and architecture — identical to the format written by `write_modules_json` in `uvm_install`.

#### Scenario: Output is valid JSON array
- **WHEN** the command succeeds
- **THEN** stdout (or the output file) contains a valid JSON array
- **THEN** each element is a serialized `Module` object matching the structure written during installation

---

### Requirement: Version not found exits with error
When the Unity release API returns no result for the requested version, the command SHALL exit with a non-zero code and print a human-readable error to stderr.

#### Scenario: Unknown version
- **WHEN** the user runs `uvm download-modules-json 9999.9.9f9`
- **THEN** the command exits with code 1
- **THEN** stderr contains a message indicating the version was not found
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## 1. Feature Flag Setup

- [x] 1.1 Add `[features]` section to `uvm/Cargo.toml` with a `dev-commands` feature (no extra dependencies required — all needed crates are already in `[dependencies]`)

## 2. Command Implementation

- [x] 2.1 Create `uvm/src/commands/download_modules_json.rs` with a `DownloadModulesJsonCommand` struct deriving `clap::Args`, containing:
- positional `version: Version`
- `--platform` (`UnityReleaseDownloadPlatform`, default = current)
- `--architecture` (`UnityReleaseDownloadArchitecture`, default = current)
- `--output` (`Option<PathBuf>`)
- [x] 2.2 Implement `execute()` on `DownloadModulesJsonCommand`:
- Call `FetchRelease::builder(version).with_extended_lts().with_u7_alpha().with_platform(platform).with_architecture(architecture).fetch()`
- Collect modules from `release.downloads` (all modules via `iter_modules()`)
- Serialize with `serde_json::to_string_pretty(&modules)`
- If `--output` is set: create parent directories with `fs::create_dir_all`, then write to file
- Otherwise: write to stdout
- Return exit code 0 on success, 1 on error (print error to stderr)

## 3. Wire Into CLI

- [x] 3.1 Add `#[cfg(feature = "dev-commands")] pub mod download_modules_json;` to `uvm/src/commands/mod.rs`
- [x] 3.2 Add `#[cfg(feature = "dev-commands")] DownloadModulesJson(DownloadModulesJsonCommand)` variant to the `Commands` enum in `uvm/src/main.rs`
- [x] 3.3 Add the matching arm to `Commands::exec()`: `#[cfg(feature = "dev-commands")] Commands::DownloadModulesJson(cmd) => cmd.execute()`

## 4. Verification

- [x] 4.1 Build without feature flag and confirm `download-modules-json` does not appear in `uvm --help`
- [x] 4.2 Build with `--features dev-commands` and confirm `download-modules-json` appears in `uvm --help`
- [x] 4.3 Run `uvm download-modules-json <real-version>` and verify valid JSON is printed to stdout
- [x] 4.4 Run with `--output /tmp/test/modules.json` (non-existent dir) and verify the directory is created and file is written
- [x] 4.5 Run with `--platform linux --architecture x86_64` on a non-Linux host and verify the output reflects Linux/x86_64 modules
104 changes: 104 additions & 0 deletions openspec/specs/download-modules-json-command/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Spec: download-modules-json-command

## Purpose

The `download-modules-json` subcommand fetches the `modules.json` data for a given Unity version from the Unity release API and writes it to stdout or a file. It is intended as a developer/diagnostic tool and is gated behind a feature flag.

## Requirements

### Requirement: Feature flag gates the command
The `uvm` crate SHALL expose `download-modules-json` as a subcommand only when compiled with the `dev-commands` feature flag. Production builds (without the flag) SHALL NOT include this subcommand or any of its dependencies.

#### Scenario: Command absent in default build
- **WHEN** `uvm` is built without `--features dev-commands`
- **THEN** `uvm --help` does not list `download-modules-json`

#### Scenario: Command present in dev build
- **WHEN** `uvm` is built with `--features dev-commands`
- **THEN** `uvm --help` lists `download-modules-json`

---

### Requirement: Version argument is required
The command SHALL accept a single positional `<version>` argument in Unity version format (e.g. `2023.1.0f1`). The command SHALL fail with a usage error if the version is omitted or unparseable.

#### Scenario: Valid version provided
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1`
- **THEN** the command proceeds to fetch release data for that version

#### Scenario: Version omitted
- **WHEN** the user runs `uvm download-modules-json` with no arguments
- **THEN** the command exits with a non-zero code and prints a usage error to stderr

---

### Requirement: Platform defaults to current host platform
The command SHALL accept an optional `--platform <platform>` argument accepting `macos`, `linux`, or `windows` (case-insensitive, via `clap::ValueEnum`). When omitted, the platform SHALL default to the host platform.

#### Scenario: Platform omitted on macOS host
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1` on a macOS machine
- **THEN** the command fetches modules for the `macos` platform

#### Scenario: Platform explicitly specified
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1 --platform linux`
- **THEN** the command fetches modules for the `linux` platform regardless of host OS

---

### Requirement: Architecture defaults to current host architecture
The command SHALL accept an optional `--architecture <arch>` argument accepting `x86_64` or `arm64` (case-insensitive, via `clap::ValueEnum`). When omitted, the architecture SHALL default to the host architecture (`x86_64` on Linux regardless of actual arch, per existing platform logic).

#### Scenario: Architecture omitted
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1` on an arm64 macOS machine
- **THEN** the command fetches modules for the `arm64` architecture

#### Scenario: Architecture explicitly overridden
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1 --architecture x86_64`
- **THEN** the command fetches modules for the `x86_64` architecture

---

### Requirement: Output defaults to stdout
When `--output` is not specified, the command SHALL write the serialized JSON to stdout.

#### Scenario: No output flag
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1`
- **THEN** the JSON content is written to stdout
- **THEN** the process exits with code 0

---

### Requirement: Output can be written to a file
The command SHALL accept an optional `--output <path>` argument. When provided, the command SHALL write the JSON to the specified file path, creating the file if it does not exist and overwriting it if it does. If the parent directory does not exist, the command SHALL create it (including all intermediate directories) before writing the file.

#### Scenario: Valid output path provided
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1 --output /tmp/modules.json`
- **THEN** the file `/tmp/modules.json` is created (or overwritten) with the JSON content
- **THEN** nothing is written to stdout
- **THEN** the process exits with code 0

#### Scenario: Output path in non-existent directory
- **WHEN** the user runs `uvm download-modules-json 2023.1.0f1 --output /some/new/dir/modules.json`
- **THEN** the directory `/some/new/dir/` is created recursively
- **THEN** the file `/some/new/dir/modules.json` is written with the JSON content
- **THEN** the process exits with code 0

---

### Requirement: JSON format matches modules.json produced during installation
The output SHALL be the pretty-printed JSON serialization (`serde_json::to_string_pretty`) of the `Vec<Module>` slice returned by `FetchRelease` for the given version, platform, and architecture — identical to the format written by `write_modules_json` in `uvm_install`.

#### Scenario: Output is valid JSON array
- **WHEN** the command succeeds
- **THEN** stdout (or the output file) contains a valid JSON array
- **THEN** each element is a serialized `Module` object matching the structure written during installation

---

### Requirement: Version not found exits with error
When the Unity release API returns no result for the requested version, the command SHALL exit with a non-zero code and print a human-readable error to stderr.

#### Scenario: Unknown version
- **WHEN** the user runs `uvm download-modules-json 9999.9.9f9`
- **THEN** the command exits with code 1
- **THEN** stderr contains a message indicating the version was not found
4 changes: 4 additions & 0 deletions uvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ unity-hub = { version = "0.6.0", path = "../unity-hub", features = ["mutate"] }
uvm_install = { version = "0.22.0", path = "../uvm_install", features = ["clap"]}
uvm_gc = { version = "0.2.0", path = "../uvm_gc" }
itertools = { workspace = true }
serde_json = { workspace = true, optional = true }
humantime = "2.3.0"
walkdir = "2.4.0"
[features]
dev-commands = ["dep:serde_json"]

[dev-dependencies]
tempfile = "3.19.1"
Loading
Loading