diff --git a/.gitignore b/.gitignore index 391b5b6c..7fa3aaa4 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ external/ !CHANGELOG.md *.vsix editors/vscode/package-lock.json +editors/vscode/node_modules/ # mdBook render output /book/book/ diff --git a/CHANGELOG.md b/CHANGELOG.md index fc242176..7c1608a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -627,6 +627,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### infs CLI +- Add opt-in post-build WASM optimization via Binaryen `wasm-opt` + - After a successful project-mode `infs build`/`infs run`, when the manifest declares `[build.wasm-opt]`, the external `wasm-opt` binary optimizes `out/main.wasm` in place; absent the table, the pipeline is unchanged + - Runs only for executable artifacts: proof-mode builds and any `-v` build are always skipped silently, since their WASM can carry non-deterministic opcodes (`forall`/`exists`/`assume`/`unique`/`@` uzumaki) that `wasm-opt` cannot parse + - A compile-mode artifact that still contains a non-deterministic opcode is a hard error naming the construct and pointing at the fix (move it into a `spec` block, or disable optimization), rather than an opaque `wasm-opt` parse failure + - `infs run` applies the same optimization as `infs build`, so it always executes exactly what a build would ship; single-file mode is unaffected + - New `--no-wasm-opt` flag on `infs build` and `infs run` skips optimization for a single invocation regardless of the manifest + - `wasm-opt` is resolved via the `WASM_OPT_PATH` environment variable, falling back to PATH, then an infs-managed Binaryen install (see below); if none resolves, the build fails with install hints led by `infs component add wasm-opt` + - The resolved binary must be Binaryen 116 or newer; an older version is a hard error, while an unparseable `--version` output only warns and proceeds + - `wasm-opt` strips the WASM names custom section, so stack traces from an optimized artifact lose function names +- Add infs-managed Binaryen provisioning for `wasm-opt` (`infs component`) + - New `infs component add|list|remove ` command family (rustup-style) manages optional toolchain components, a tier distinct from the `infc` toolchain install; `wasm-opt` (Binaryen) is the only component today + - `infs component add wasm-opt` downloads a pinned, sha256-verified Binaryen release (`version_130`) into `~/.inference/tools/binaryen//`; the checksum is verified before anything reaches the install directory, the install is idempotent (no network access when already installed) and atomic (staged under a per-process temp directory, published with a single rename), and a broken prior install is repaired rather than left stale + - `infs component list` reports each component's install state and location; `infs component remove wasm-opt` deletes the managed install; `add` prints a note when `WASM_OPT_PATH` or a PATH `wasm-opt` would shadow the newly installed managed copy at build time + - `wasm-opt` resolution gains a third precedence tier — `WASM_OPT_PATH` env → PATH → the managed install — completing a chain that previously hard-errored whenever the first two missed; set `INFS_VERBOSE=1` to trace which tier resolved the binary + - New `[build.wasm-opt] auto-install` manifest key (default `false`): when `true` and `wasm-opt` resolves in no tier, `infs` downloads the pinned Binaryen at build time instead of erroring + - The missing-`wasm-opt` install-hint error now leads with `infs component add wasm-opt`, ahead of the brew/apt/npm/releases hints, and mentions `auto-install = true` as the hands-off alternative + - `infs doctor` gains an appended `wasm-opt` check: OK naming the resolved path, precedence tier, and Binaryen version (noting when a managed copy is shadowed by PATH); an unused `wasm-opt` reports OK as "not installed (optional)" rather than alarming projects that don't use `[build.wasm-opt]`; a broken managed install, a failing `--version` probe, or an invalid `WASM_OPT_PATH` each WARN with remediation - Make `infs build` and `infs run` project-aware ([#223]) - Invoked with no path, both commands discover the project's `Inference.toml` by walking up from the current directory (nearest ancestor wins; the start directory is canonicalized once for symlink stability), then compile `/src/main.inf` with the compiler's working directory set to the project root so `out/` always lands at the root regardless of where the command was invoked - The existing single-file forms (`infs build path/to/file.inf`, `infs run path/to/file.inf`) are preserved unchanged @@ -714,6 +731,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Project Manifest +- Add optional `[build.wasm-opt]` sub-table to `Inference.toml` + - `enabled` (bool, default `true`): table presence alone enables optimization; set `enabled = false` to keep the table while disabling the step + - `level` (string, default `"3"`): forwarded to `wasm-opt` as `-O`; one of `"0"`–`"4"`, `"s"`, `"z"`, validated on load with a clear error naming the offending value + - `auto-install` (bool, default `false`): downloads a missing `wasm-opt` automatically at build time — the same pinned, checksum-verified Binaryen `infs component add wasm-opt` installs — instead of hard-erroring; recorded in the versioned manifest since `infs` has no interactive prompts + - `infs new`/`infs init` scaffold a commented-out `[build.wasm-opt]` block after `[build]`, including an `# auto-install = true` line - Consume `[build]` and `[verification]` configuration in project-mode builds ([#223]) - New `[build] mode = "compile" | "proof"` field (default `"compile"`), validated on load; an invalid value is a clear error naming the field and allowed values - `[verification] output-dir` is honored only in effective-proof builds, where it redirects artifacts via `infc --out-dir`; in compile mode it is ignored so the default `proofs/` never relocates `out/main.wasm` @@ -757,6 +779,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Simplify `buildFakeInfcArchive()` to emit only `infc` binary - Update doctor check expectations from 6 to 5 checks (single `infc` check replaces `inf-llc`, `rust-lld`, `libLLVM`) - Change "missing lib directory triggers doctor warning" to "missing infc triggers doctor failure" +- Add "Install Component (wasm-opt)" command to VS Code extension (`inference.installComponent`) + - Runs `infs component add ` with a progress notification; refreshes `infs doctor` on success; offers Show Output / Retry actions on failure + - `infs doctor` notifications (error and warning toasts alike) gain an "Install wasm-opt" action button whenever a `wasm-opt` check reports a warning or failure, invoking the install command directly --- diff --git a/apps/infs/Cargo.toml b/apps/infs/Cargo.toml index a297483a..83ff2ad6 100644 --- a/apps/infs/Cargo.toml +++ b/apps/infs/Cargo.toml @@ -35,6 +35,7 @@ crossterm = "0.29.0" anyhow.workspace = true thiserror.workspace = true inference-compiler-interface.workspace = true +inf-wasmparser.workspace = true [target.'cfg(windows)'.dependencies] winreg = "0.56" @@ -45,6 +46,10 @@ predicates = "3.1.3" assert_fs = "1.1.1" serial_test = "3.2" regex = "1" +wasm-encoder = "0.249.0" +tar = "0.4" +flate2 = "1" +sha2 = "0.11" [profile.release] opt-level = "z" # Optimize for size (not speed) diff --git a/apps/infs/README.md b/apps/infs/README.md index 2b4643b3..5db8603e 100644 --- a/apps/infs/README.md +++ b/apps/infs/README.md @@ -51,6 +51,9 @@ cargo build -p infs --release | `infs default ` | Set the default toolchain | | `infs doctor` | Check installation health with intelligent recommendations | | `infs self update` | Update infs itself | +| `infs component add ` | Install a managed component (currently `wasm-opt`, the Binaryen optimizer behind `[build.wasm-opt]`) | +| `infs component list` | List managed components and their install state | +| `infs component remove ` | Remove an installed managed component | ### Other @@ -106,20 +109,22 @@ infs build example.inf --analyze | `-v` | Generate Rocq (.v) translation file | | `--mode proof` | Proof mode: preserve non-det specs; implies `-v` inside `infc` | | `--mode compile` | Compile mode: strip specs for executable WASM | +| `--no-wasm-opt` | Skip `[build.wasm-opt]` post-build optimization (project mode only) | When no phase flag is given, `infs build` defaults to full compilation and writes the WASM binary to disk — equivalent to `--codegen -o`. ### Project-mode Manifest Semantics -When `infs build` runs in project mode, it reads two fields from `Inference.toml` to resolve the build configuration: +When `infs build` runs in project mode, it reads fields from `Inference.toml` to resolve the build configuration: | Manifest field | Effect | |----------------|--------| | `[build] mode = "proof"` | Forwards `--mode proof` to `infc`; activates `output-dir` | | `[build] mode = "compile"` (default) | Forwards nothing; `infc` defaults to compile mode | | `[verification] output-dir` | Honored only in effective-proof mode; relocates both `.wasm` and `.v` | +| `[build.wasm-opt]` | Opt-in post-build optimization of `out/main.wasm` via Binaryen `wasm-opt`, resolved from `WASM_OPT_PATH` → PATH → an infs-managed install; absent table is a no-op | -CLI flags always override manifest settings. `infs run` ignores `[build] mode` entirely and always builds an executable in `out/` (proof-mode WASM contains non-deterministic opcodes that `wasmtime` cannot execute). +CLI flags always override manifest settings. `infs run` ignores `[build] mode` entirely and always builds an executable in `out/` (proof-mode WASM contains non-deterministic opcodes that `wasmtime` cannot execute) — but it does honor `[build.wasm-opt]`, since `run` optimizes exactly what it then executes. `[build.wasm-opt]` applies only to compile-mode artifacts (proof-mode and `-v` builds always skip it) and can be skipped for a single invocation with `--no-wasm-opt`. `wasm-opt` itself does not need to be preinstalled: run `infs component add wasm-opt` to provision the pinned, checksum-verified Binaryen up front, or set `auto-install = true` under `[build.wasm-opt]` to have `infs` download it automatically the first time a build needs it. See [`docs/inference-toml.md`](docs/inference-toml.md) for the full field reference, including the non-deterministic-construct hard error and the complete `wasm-opt` resolution order. ### Run Command @@ -139,9 +144,12 @@ infs run example.inf --entry-point helper # Pass arguments to the program (single-file only) infs run example.inf -- arg1 arg2 + +# Project mode: build, but skip [build.wasm-opt] for this run +infs run --no-wasm-opt ``` -Requires `wasmtime` to be installed. +Requires `wasmtime` to be installed. In project mode, `infs run` applies the same `[build.wasm-opt]` post-build optimization as `infs build` before executing the result. ### Project Commands diff --git a/apps/infs/docs/inference-toml.md b/apps/infs/docs/inference-toml.md index 02f40367..89a40871 100644 --- a/apps/infs/docs/inference-toml.md +++ b/apps/infs/docs/inference-toml.md @@ -34,6 +34,11 @@ target = "wasm32" optimize = "debug" mode = "compile" # "compile" (executable WASM) or "proof" (Rocq translation) +[build.wasm-opt] # optional: post-build optimization of the executable +enabled = true # table presence enables; set false to keep it off +level = "3" # forwarded as -O: "0".."4", "s", "z" +auto-install = false # download wasm-opt automatically if it is missing + [verification] output-dir = "proofs/" # honored only in proof mode ``` @@ -130,6 +135,66 @@ optimize = "release" mode = "proof" ``` +### [build.wasm-opt] + +The `[build.wasm-opt]` table is an optional sub-table of `[build]` that enables post-build optimization of the compiled WASM executable via the external [Binaryen](https://github.com/WebAssembly/binaryen) `wasm-opt` binary. `infs` can provision `wasm-opt` for you — `infs component add wasm-opt` installs a pinned, checksum-verified Binaryen release, and `auto-install = true` does the same automatically at build time — or resolve one already on your system; resolution order is covered below. + +An `Inference.toml` with no `[build.wasm-opt]` table at all leaves the build pipeline unchanged — this feature is off by default. + +#### Fields + +- **`enabled`** (boolean, default: `true`): Whether the optimizer runs. + - Table *presence* is what turns the feature on: an empty `[build.wasm-opt]` table enables optimization at the default level. Set `enabled = false` to keep the table (and a configured `level`) in the manifest while disabling the step. + +- **`level`** (string, default: `"3"`): The optimization level, forwarded to `wasm-opt` as `-O`. + - One of `"0"`, `"1"`, `"2"`, `"3"`, `"4"`, `"s"`, `"z"` — the same levels `wasm-opt` itself accepts (`s` and `z` bias toward size over speed). Any other value is a load error naming the offending value and the allowed set. + +- **`auto-install`** (boolean, default: `false`): Whether a missing `wasm-opt` is downloaded automatically at build time. + - When `true` and `wasm-opt` does not resolve in any of the three tiers below, `infs` downloads the pinned, sha256-verified Binaryen release into `~/.inference/tools/binaryen//` — the same install `infs component add wasm-opt` performs — before optimizing. The default `false` keeps a build network-free: a missing binary is a hard error with install remediation instead. The opt-in is recorded in the versioned manifest; `infs` has no interactive prompts. + - An invalid `WASM_OPT_PATH` override is always an error, even with `auto-install = true` — a typo in that variable is surfaced rather than silently downloaded over. + +#### Example + +```toml +[build.wasm-opt] +enabled = true +level = "z" +``` + +#### When optimization runs + +- **Project mode only, for executable artifacts.** Both `infs build` and `infs run` apply `[build.wasm-opt]` to `out/main.wasm` after a successful compile — `run` optimizes exactly the artifact it then executes, so what you run is what `build` would have shipped. Single-file mode (`infs build file.inf`) never consults the manifest and is unaffected. +- **Proof-mode and `-v` builds are always skipped, silently.** A build counts as proof mode when the effective `[build] mode` is `"proof"`, `--mode proof` is passed, or `-v` is passed at all (even without `--mode`). Their WASM can carry the non-deterministic opcodes (`forall`, `exists`, `assume`, `unique`, `@` uzumaki) that `wasm-opt` cannot parse, and they are a different artifact class from an executable. +- **A compile-mode artifact that still contains a non-deterministic opcode is a hard error**, not a silent skip. Compile-mode builds strip `spec` blocks, so a well-formed executable should never carry one of these opcodes — if it does, `infs` scans for it before invoking `wasm-opt` (which would otherwise fail with an opaque parse error) and reports the offending construct by name, with remediation: move it into a `spec` block, or turn optimization off. + +#### Disabling optimization for one invocation + +Pass `--no-wasm-opt` to skip `[build.wasm-opt]` for a single `infs build` or `infs run` without editing the manifest: + +```bash +infs build --no-wasm-opt +infs run --no-wasm-opt +``` + +#### Resolving the `wasm-opt` binary + +`infs` resolves `wasm-opt` through three precedence tiers, in order: + +1. **`WASM_OPT_PATH`** environment variable, if set. It must point at an existing file, or the build errors naming the variable and the invalid path — a set override is never silently discarded in favor of a lower tier, even when `auto-install = true`. +2. **PATH** — a standard lookup for `wasm-opt`. This tier wins over the managed tier below, so a system-installed `wasm-opt` on PATH always takes precedence over an infs-managed one. +3. **Managed tools** — the pinned Binaryen installed by `infs component add wasm-opt` (or by `auto-install`, see below) at `~/.inference/tools/binaryen//`. + +Set `INFS_VERBOSE=1` to trace which tier resolved `wasm-opt` to stderr (`infs: resolved wasm-opt via : `). + +If no tier resolves, the build fails with install hints led by `infs component add wasm-opt`, followed by the system package managers (`brew install binaryen`, `apt install binaryen`, `npm install -g binaryen`) and a link to the [Binaryen releases page](https://github.com/WebAssembly/binaryen/releases). Set `auto-install = true` to have `infs` provision Binaryen automatically at build time instead of erroring. + +The resolved binary must report **Binaryen 116 or newer** (`wasm-opt --version`); an older version is a hard error naming both the found and required versions. If `--version` cannot be run or its output cannot be parsed, `infs` warns and proceeds rather than blocking the build over an unrecognized binary. This check runs against whichever binary was resolved, managed installs included. + +#### Caveats + +- **Function names are dropped.** `wasm-opt` strips the WASM names custom section, so stack traces and any tooling that resolves function names from an optimized `out/main.wasm` will not see them. There is currently no flag to preserve it. +- **Deterministic per Binaryen version, not across versions.** The same source, flags, and Binaryen version always produce identical optimized bytes, but upgrading Binaryen can change the output even for unchanged input. Do not treat an optimized `.wasm` as a stable byte-for-byte reference across toolchain upgrades. + ### [verification] The `[verification]` section configures Rocq (Coq) proof generation. diff --git a/apps/infs/src/commands/build.rs b/apps/infs/src/commands/build.rs index 36071d2f..b8d535ca 100644 --- a/apps/infs/src/commands/build.rs +++ b/apps/infs/src/commands/build.rs @@ -67,7 +67,7 @@ use std::process::Command; use crate::commands::project_build::{check_compiler_compatibility, mode_flag, run_project_build}; use crate::errors::InfsError; -use crate::project::manifest::{find_manifest_dir, InferenceToml, MANIFEST_FILE_NAME}; +use crate::project::manifest::{InferenceToml, MANIFEST_FILE_NAME, find_manifest_dir}; use crate::project::{self, ProjectContext}; use crate::toolchain::find_infc; @@ -118,6 +118,14 @@ pub struct BuildArgs { /// `use { … } from ;`. Repeatable; forwarded verbatim to `infc`. #[clap(short = 'L', long = "wasm-lib-dir", value_name = "DIR")] pub wasm_lib_dirs: Vec, + + /// Skip the `[build.wasm-opt]` post-build optimization for this build. + /// + /// Project mode only: when the manifest declares `[build.wasm-opt]`, this + /// leaves `out/main.wasm` exactly as `infc` emitted it. No effect in + /// single-file mode or when no `[build.wasm-opt]` table is present. + #[clap(long = "no-wasm-opt")] + pub no_wasm_opt: bool, } /// Executes the build command with the given arguments. @@ -187,7 +195,8 @@ fn execute_single_file(path: &Path, args: &BuildArgs) -> Result<()> { } for (name, path) in manifest_wasm_dependencies(path)? { - cmd.arg("--wasm-dep").arg(format_wasm_dep_arg(&name, &path)?); + cmd.arg("--wasm-dep") + .arg(format_wasm_dep_arg(&name, &path)?); } let status = cmd @@ -275,7 +284,13 @@ fn execute_project(ctx: &ProjectContext, args: &BuildArgs) -> Result<()> { let effective_mode = resolve_effective_mode(args.mode, &ctx.manifest.build.mode); let out_dir = resolve_out_dir(effective_mode, &ctx.manifest.verification)?; - run_project_build(ctx, args.generate_v_output, effective_mode, out_dir.as_deref()) + run_project_build( + ctx, + args.generate_v_output, + effective_mode, + out_dir.as_deref(), + args.no_wasm_opt, + ) } /// Resolves the effective `--mode` to forward to `infc`, or `None` to forward @@ -383,7 +398,9 @@ mod manifest_dep_tests { let temp = assert_fs::TempDir::new().unwrap(); let manifest = temp.child("Inference.toml"); manifest - .write_str("[package]\nname = \"demo\"\nversion = \"0.1.0\"\ninfc_version = \"0.1.0\"\n") + .write_str( + "[package]\nname = \"demo\"\nversion = \"0.1.0\"\ninfc_version = \"0.1.0\"\n", + ) .unwrap(); let source = temp.child("main.inf"); source.write_str("").unwrap(); @@ -449,7 +466,10 @@ mod tests { #[test] fn manifest_proof_forwards_proof() { - assert_eq!(resolve_effective_mode(None, "proof"), Some(BuildMode::Proof)); + assert_eq!( + resolve_effective_mode(None, "proof"), + Some(BuildMode::Proof) + ); } #[test] diff --git a/apps/infs/src/commands/component.rs b/apps/infs/src/commands/component.rs new file mode 100644 index 00000000..9bd286e3 --- /dev/null +++ b/apps/infs/src/commands/component.rs @@ -0,0 +1,199 @@ +//! `infs component` — manage optional managed toolchain components. +//! +//! The sole component today is `wasm-opt` (Binaryen), the optimizer the +//! `[build.wasm-opt]` manifest table drives. `infs component add wasm-opt` +//! downloads a pinned, checksum-verified Binaryen into +//! `~/.inference/tools/binaryen//`; `list` reports install state; and +//! `remove` deletes it. Managed components are a distinct install tier from +//! toolchains — they live under `tools/`, never `toolchains/`, and are resolved +//! independently of `infc` (see [`crate::commands::wasm_opt`]). + +use anyhow::{Context, Result, bail}; +use clap::{Args, Subcommand}; + +use crate::toolchain::binaryen; +use crate::toolchain::{Platform, ToolchainPaths}; + +/// The components `infs component` understands. The seam for future components; +/// unknown names are rejected against this list. Every entry must have a +/// matching dispatch arm in [`add`] and [`remove`] — those arms fail loudly on +/// a listed-but-unhandled name rather than operating on the wrong component. +const KNOWN_COMPONENTS: &[&str] = &[binaryen::COMPONENT_NAME]; + +/// Arguments for the `component` command. +#[derive(Args)] +pub struct ComponentArgs { + /// The component operation to perform. + #[command(subcommand)] + pub command: ComponentCommand, +} + +/// The `component` subcommands. +#[derive(Subcommand)] +pub enum ComponentCommand { + /// Download and install a managed component. + Add { + /// Component name (currently only `wasm-opt`). + name: String, + }, + /// List managed components and their install state. + List, + /// Remove an installed managed component. + Remove { + /// Component name (currently only `wasm-opt`). + name: String, + }, +} + +/// Executes the `component` command. +/// +/// # Errors +/// +/// Returns an error if the component name is unknown, or if the underlying +/// install / remove operation fails. +pub async fn execute(args: &ComponentArgs) -> Result<()> { + match &args.command { + ComponentCommand::Add { name } => add(name).await, + ComponentCommand::List => list(), + ComponentCommand::Remove { name } => remove(name), + } +} + +/// Installs a component, dispatching on the validated name so a component +/// listed in [`KNOWN_COMPONENTS`] without a handler here fails loudly instead +/// of silently installing the wrong one. +async fn add(name: &str) -> Result<()> { + ensure_known_component(name)?; + match name { + binaryen::COMPONENT_NAME => add_wasm_opt().await, + other => bail!("component '{other}' has no install handler; this is a bug in infs"), + } +} + +/// Installs the managed Binaryen, then surfaces a precedence note if a +/// non-managed `wasm-opt` would shadow the managed copy at build time. +async fn add_wasm_opt() -> Result<()> { + let platform = Platform::detect()?; + let paths = ToolchainPaths::new()?; + + println!( + "Installing component '{}' (Binaryen {}) for {platform}...", + binaryen::COMPONENT_NAME, + binaryen::BINARYEN_PIN + ); + binaryen::install(&paths, platform).await.with_context(|| { + format!( + "Failed to install component '{}'. Install Binaryen manually and put \ + `wasm-opt` on PATH, or set WASM_OPT_PATH to its full path.", + binaryen::COMPONENT_NAME + ) + })?; + + print_precedence_note(); + Ok(()) +} + +/// Lists the managed components and whether each is installed. +fn list() -> Result<()> { + let paths = ToolchainPaths::new()?; + let status = binaryen::status(&paths); + if status.installed { + let location = binaryen::installed_wasm_opt(&paths) + .map_or_else(|| "unknown".to_string(), |p| p.display().to_string()); + println!( + "* {:<12}(installed: Binaryen {} at {location})", + status.name, status.version + ); + } else { + println!( + " {:<12}(not installed; run 'infs component add {}')", + status.name, status.name + ); + } + Ok(()) +} + +/// Removes a component's managed install, dispatching on the validated name +/// with the same listed-but-unhandled guard as [`add`]. +fn remove(name: &str) -> Result<()> { + ensure_known_component(name)?; + match name { + binaryen::COMPONENT_NAME => remove_wasm_opt(), + other => bail!("component '{other}' has no remove handler; this is a bug in infs"), + } +} + +/// Removes the managed Binaryen install. +fn remove_wasm_opt() -> Result<()> { + let paths = ToolchainPaths::new()?; + binaryen::remove(&paths)?; + println!( + "Removed component '{}' (Binaryen {}).", + binaryen::COMPONENT_NAME, + binaryen::BINARYEN_PIN + ); + Ok(()) +} + +/// Prints a note when `WASM_OPT_PATH` or a PATH `wasm-opt` would take precedence +/// over the copy just installed — both resolve ahead of the managed tier at +/// build time, so without this the managed copy could silently not take effect. +fn print_precedence_note() { + if std::env::var_os("WASM_OPT_PATH").is_some() { + println!( + "Note: WASM_OPT_PATH is set; it takes precedence over the managed \ + copy at build time." + ); + } else if which::which("wasm-opt").is_ok() { + println!( + "Note: a `wasm-opt` on your PATH takes precedence over the managed \ + copy at build time." + ); + } +} + +/// Validates `name` against [`KNOWN_COMPONENTS`]. +/// +/// # Errors +/// +/// Bails when `name` is not a known component, listing the known ones. +fn ensure_known_component(name: &str) -> Result<()> { + if KNOWN_COMPONENTS.contains(&name) { + return Ok(()); + } + bail!( + "Unknown component '{name}'. Known components: {}", + KNOWN_COMPONENTS.join(", ") + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ensure_known_component_accepts_wasm_opt() { + assert!(ensure_known_component("wasm-opt").is_ok()); + } + + #[test] + fn ensure_known_component_rejects_unknown() { + let err = ensure_known_component("wasm-optimizer").unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("Unknown component 'wasm-optimizer'") + && msg.contains("Known components: wasm-opt"), + "an unknown component must name it and list the known ones, got: {msg}" + ); + } + + #[test] + fn known_components_all_have_dispatch_handlers() { + // The add/remove dispatch handles exactly the Binaryen component today. + // A new KNOWN_COMPONENTS entry must come with matching dispatch arms; + // this pins the current one-to-one state so growing the list without + // touching the dispatch is caught here, not by a wrong-component + // install. + assert_eq!(KNOWN_COMPONENTS, &[binaryen::COMPONENT_NAME]); + } +} diff --git a/apps/infs/src/commands/mod.rs b/apps/infs/src/commands/mod.rs index dad2e0dc..5da47ab5 100644 --- a/apps/infs/src/commands/mod.rs +++ b/apps/infs/src/commands/mod.rs @@ -7,6 +7,7 @@ //! - [`build`] - Compile Inference source files //! - [`run`] - Build and execute WASM with wasmtime //! - [`project_build`] - Shared project-build helper used by `build` and `run` +//! - [`wasm_opt`] - Post-build WASM optimization via the external `wasm-opt` binary //! - [`version`] - Display version information //! //! ## Project Management Commands @@ -21,10 +22,12 @@ //! - [`list`] - List installed toolchains //! - [`versions`] - List available remote versions //! - [`default`] - Set default toolchain version +//! - [`component`] - Manage optional components (e.g. `wasm-opt`) //! - [`doctor`] - Check installation health //! - [`self_cmd`] - Manage infs itself pub mod build; +pub mod component; pub mod default; pub mod doctor; pub mod init; @@ -37,3 +40,4 @@ pub mod self_cmd; pub mod uninstall; pub mod version; pub mod versions; +pub(crate) mod wasm_opt; diff --git a/apps/infs/src/commands/project_build.rs b/apps/infs/src/commands/project_build.rs index a76fa709..2990885b 100644 --- a/apps/infs/src/commands/project_build.rs +++ b/apps/infs/src/commands/project_build.rs @@ -2,10 +2,12 @@ //! //! Both `infs build` (project mode) and `infs run` (project mode) need to //! perform the *same* project compilation: resolve the conventional -//! `src/main.inf` entry point, run the `infc` compatibility handshake, and spawn +//! `src/main.inf` entry point, run the `infc` compatibility handshake, spawn //! `infc` with its working directory set to the project root so `out/` lands at -//! the root. This module owns that shared logic so the two command modules do -//! not duplicate it (and so `run` inherits the handshake "for free"). +//! the root, and apply the optional `[build.wasm-opt]` post-build optimization +//! to the resulting executable. This module owns that shared logic so the two +//! command modules do not duplicate it (and so `run` inherits both the +//! handshake and the optimizer "for free", running exactly what it ships). //! //! `infc` compiles the whole import-reachable closure starting at //! `src/main.inf` and is the sole authority on which files are part of the @@ -63,6 +65,12 @@ use inference_compiler_interface::{COMPILER_ABI_MAJOR, COMPILER_ABI_MINOR}; /// `infs run` always passes `out_dir = None` (and `mode = None`), so project /// `run` always builds an executable in `out/`. /// +/// After a successful `infc` exit, the optional `[build.wasm-opt]` post-build +/// optimization is applied to `/out/main.wasm` (see +/// [`crate::commands::wasm_opt::post_build_optimize`]). `no_wasm_opt` (the +/// `--no-wasm-opt` flag) suppresses it, as do proof/`-v` builds; when no +/// `[build.wasm-opt]` table is present it is a no-op. +/// /// ## Errors /// /// Returns an error if: @@ -71,11 +79,14 @@ use inference_compiler_interface::{COMPILER_ABI_MAJOR, COMPILER_ABI_MINOR}; /// - infc reports a *major* ABI version mismatch (hard error with remediation) /// - `out_dir` is requested but the resolved `infc` does not support `--out-dir` /// - infc exits with non-zero code (as `InfsError::ProcessExitCode`) +/// - post-build optimization is active and fails (missing/invalid artifact, +/// `wasm-opt` resolution, or the optimization itself) pub(crate) fn run_project_build( ctx: &ProjectContext, generate_v_output: bool, mode: Option, out_dir: Option<&Path>, + no_wasm_opt: bool, ) -> Result<()> { if !ctx.entry_point.is_file() { bail!( @@ -123,6 +134,7 @@ pub(crate) fn run_project_build( .with_context(|| format!("Failed to execute infc at {}", infc_path.display()))?; if status.success() { + crate::commands::wasm_opt::post_build_optimize(ctx, generate_v_output, mode, no_wasm_opt)?; Ok(()) } else { let code = status.code().unwrap_or(1); @@ -341,7 +353,7 @@ mod project_tests { entry_point: root.join("src").join("main.inf"), }; - let err = run_project_build(&ctx, false, None, None).unwrap_err(); + let err = run_project_build(&ctx, false, None, None, false).unwrap_err(); let msg = format!("{err}"); assert!( msg.contains("Missing entry point") && msg.contains("main.inf"), @@ -365,7 +377,7 @@ mod project_tests { entry_point: entry, }; - let err = run_project_build(&ctx, false, None, None).unwrap_err(); + let err = run_project_build(&ctx, false, None, None, false).unwrap_err(); let msg = format!("{err}"); assert!( msg.contains("Missing entry point") && msg.contains("main.inf"), @@ -437,7 +449,10 @@ mod project_tests { fn entry_point_resolves_to_src_main_inf() { let dir = assert_fs::TempDir::new().unwrap(); InferenceToml::new("demo") - .write_to_file(&dir.path().join(crate::project::manifest::MANIFEST_FILE_NAME)) + .write_to_file( + &dir.path() + .join(crate::project::manifest::MANIFEST_FILE_NAME), + ) .unwrap(); let src = dir.path().join("src"); std::fs::create_dir_all(&src).unwrap(); @@ -590,8 +605,14 @@ mod tests { // ABI "9.9" would major-mismatch if probed; commit match must short-circuit. let stub = write_stub(&dir, env!("INFS_GIT_COMMIT"), "9.9", false); let compat = probe_compiler_compatibility(&stub).unwrap(); - assert!(compat.commit_matched, "matching commit must set commit_matched"); - assert_eq!(compat.abi, None, "commit match short-circuits the ABI probe"); + assert!( + compat.commit_matched, + "matching commit must set commit_matched" + ); + assert_eq!( + compat.abi, None, + "commit match short-circuits the ABI probe" + ); assert!(compat.supports_out_dir()); } diff --git a/apps/infs/src/commands/run.rs b/apps/infs/src/commands/run.rs index 2e2a521f..fc9e0862 100644 --- a/apps/infs/src/commands/run.rs +++ b/apps/infs/src/commands/run.rs @@ -39,6 +39,10 @@ //! execute. So project `run` ignores `[build] mode` and //! `[verification] output-dir` entirely: the artifact is always an executable //! under `/out/`. Use `infs build` to produce proof artifacts. +//! - **Applies `[build.wasm-opt]`** when the manifest declares it: `run` builds +//! an executable in compile mode, so the same post-build optimization `build` +//! performs runs here too (`run` executes exactly what it ships). Pass +//! `--no-wasm-opt` to skip it. //! - **Missing-WASM guard:** if the build reports success but //! `/out/main.wasm` is absent, `run` errors before invoking wasmtime, //! mirroring the single-file `compile_to_wasm` guard. @@ -86,6 +90,14 @@ pub struct RunArgs { #[clap(long, default_value = DEFAULT_ENTRY_POINT)] pub entry_point: String, + /// Skip the `[build.wasm-opt]` post-build optimization for this build. + /// + /// Project mode only: `run` executes the artifact it builds, so this makes + /// it run exactly what `infc` emitted. No effect in single-file mode or when + /// no `[build.wasm-opt]` table is present. + #[clap(long = "no-wasm-opt")] + pub no_wasm_opt: bool, + /// Arguments to pass to the invoked function. /// /// For functions other than `main`, these are passed directly as function arguments. @@ -201,8 +213,10 @@ fn execute_project(args: &RunArgs) -> Result<()> { // regardless of `[build] mode` in the manifest: proof-mode WASM embeds the // custom non-deterministic opcodes (0xfc family) that wasmtime cannot // execute. Hence `mode = None` and `out_dir = None` here — manifest - // mode/output-dir resolution lives only in `build`'s project path. - run_project_build(&ctx, false, None, None)?; + // mode/output-dir resolution lives only in `build`'s project path. The + // `[build.wasm-opt]` optimization still applies (unless `--no-wasm-opt`) so + // `run` executes exactly what `build` would ship. + run_project_build(&ctx, false, None, None, args.no_wasm_opt)?; let wasm_path = project_wasm_path(&ctx); if !wasm_path.exists() { @@ -361,6 +375,7 @@ mod tests { let args = RunArgs { path: None, entry_point: "helper".to_string(), + no_wasm_opt: false, args: Vec::new(), }; @@ -384,6 +399,7 @@ mod tests { let args = RunArgs { path: None, entry_point: DEFAULT_ENTRY_POINT.to_string(), + no_wasm_opt: false, args: Vec::new(), }; diff --git a/apps/infs/src/commands/wasm_opt.rs b/apps/infs/src/commands/wasm_opt.rs new file mode 100644 index 00000000..64f3ca24 --- /dev/null +++ b/apps/infs/src/commands/wasm_opt.rs @@ -0,0 +1,1082 @@ +//! Post-build optimization of the compile-mode artifact via Binaryen `wasm-opt`. +//! +//! When a project's `Inference.toml` declares `[build.wasm-opt]`, `infs` runs +//! the external `wasm-opt` binary over `/out/main.wasm` after `infc` exits +//! successfully, replacing the artifact in place with a smaller/faster +//! equivalent. This is an opt-in, project-level step: absent the table the +//! default pipeline is byte-identical, and `infc`/core are never touched. It +//! mirrors the rustc-vs-cargo split, where the wrapping tool (wasm-pack, trunk) +//! runs `wasm-opt`, not the compiler. +//! +//! Only executable artifacts are optimized. Proof-mode builds — and any `-v` +//! build, which `infs` treats conservatively as a verification workflow — are +//! skipped silently: their WASM carries verification-only opcodes (the `0xfc` +//! non-det/uzumaki family) that `wasm-opt` cannot process, and they are a +//! different artifact class. As a backstop, a compile-mode artifact that still +//! carries such an opcode is a hard error with remediation rather than a +//! confusing `wasm-opt` parse failure. +//! +//! It lives under `commands/` for the same reason as +//! [`crate::commands::project_build`]: spawning an external tool and propagating +//! its outcome is command-execution logic, not manifest/filesystem logic. + +use std::ffi::{OsStr, OsString}; +use std::fmt::Write as _; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{Context, Result, bail}; +use inf_wasmparser::{Operator, Parser, Payload, WasmFeatures}; + +use crate::commands::build::BuildMode; +use crate::project::ProjectContext; +use crate::toolchain::binaryen; +use crate::toolchain::doctor::DoctorCheck; +use crate::toolchain::{Platform, ToolchainPaths}; + +/// Environment variable that overrides `wasm-opt` resolution, taking priority +/// over a PATH lookup. +const WASM_OPT_PATH_ENV: &str = "WASM_OPT_PATH"; + +/// Minimum supported Binaryen major version. The forwarded flags +/// (`--mvp-features` plus the mutable-globals / bulk-memory enables) and the +/// `-Os`/`-Oz` levels are stable from Binaryen 116 onward. +const MIN_WASM_OPT_VERSION: u32 = 116; + +/// Identifies which precedence tier resolved `wasm-opt`. +/// +/// [`WasmOptSource::label`] emits the exact strings used in both the +/// `INFS_VERBOSE` trace line and the `infs doctor` line, so those two surfaces +/// stay in sync. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum WasmOptSource { + /// Resolved via the `WASM_OPT_PATH` environment variable (highest priority). + EnvOverride, + /// Resolved via `which::which("wasm-opt")` against the system `PATH`. + SystemPath, + /// Resolved via the infs-managed Binaryen under `/tools/binaryen/`. + ManagedTools, +} + +impl WasmOptSource { + /// The human-readable label for this resolution tier. + fn label(self) -> &'static str { + match self { + Self::EnvOverride => "WASM_OPT_PATH env", + Self::SystemPath => "PATH", + Self::ManagedTools => "managed tools", + } + } +} + +/// Whether `INFS_VERBOSE` is set to a non-empty, non-"0" value. Mirrors the +/// toolchain resolver's predicate so build traces and `infs doctor` agree. +fn verbose() -> bool { + std::env::var_os("INFS_VERBOSE").is_some_and(|v| !v.is_empty() && v != "0") +} + +/// Emits a resolution trace line to stderr under `INFS_VERBOSE`. +fn trace_resolved(source: WasmOptSource, path: &Path) { + if verbose() { + eprintln!( + "infs: resolved wasm-opt via {}: {}", + source.label(), + path.display() + ); + } +} + +/// Optimizes `/out/main.wasm` in place when `[build.wasm-opt]` is enabled. +/// +/// Called from [`crate::commands::project_build::run_project_build`] after a +/// successful `infc` exit. Returns `Ok(())` — a no-op — whenever optimization is +/// not requested: no `[build.wasm-opt]` table, `enabled = false`, or +/// `cli_disabled` (the `--no-wasm-opt` flag). +/// +/// Proof-mode builds and any `-v` build are skipped silently. `infs` does not +/// own the `-v` ⇄ proof implication (that lives in `infc::normalize_args`), so +/// it treats `--mode proof` *or* a `-v` build as a verification workflow and +/// leaves the artifact alone. Because a verification build is the only thing +/// that forwards `--out-dir`, reaching the optimization path guarantees the +/// artifact is at the conventional `/out/main.wasm`. +/// +/// # Errors +/// +/// When optimization is active, errors if the artifact is missing, still +/// carries a verification-only opcode, `wasm-opt` cannot be resolved or is too +/// old, or the optimization/re-validation fails. Every failure leaves the +/// original `out/main.wasm` untouched. +pub(crate) fn post_build_optimize( + ctx: &ProjectContext, + generate_v_output: bool, + mode: Option, + cli_disabled: bool, +) -> Result<()> { + let Some(config) = &ctx.manifest.build.wasm_opt else { + return Ok(()); + }; + if !config.enabled || cli_disabled { + return Ok(()); + } + + if mode == Some(BuildMode::Proof) || generate_v_output { + if verbose() { + eprintln!( + "wasm-opt: skipping a verification build (proof mode or -v); \ + only executable artifacts are optimized." + ); + } + return Ok(()); + } + + let wasm_path = ctx.root.join("out").join("main.wasm"); + if !wasm_path.is_file() { + bail!( + "Compilation succeeded but WASM file not found at: {}", + wasm_path.display() + ); + } + + let wasm_bytes = std::fs::read(&wasm_path) + .with_context(|| format!("Failed to read {} for optimization", wasm_path.display()))?; + + if let Some(construct) = find_verification_construct(&wasm_bytes)? { + bail!( + "`[build.wasm-opt]` is enabled but `out/main.wasm` contains the \ + verification-only construct `{construct}`, which wasm-opt cannot \ + process. Verification constructs (forall/exists/assume/unique and \ + `@`/uzumaki) belong in `spec` blocks, which compile-mode builds \ + strip. Move the construct into a `spec` block, or disable \ + optimization (`enabled = false` under `[build.wasm-opt]`, or pass \ + `--no-wasm-opt`)." + ); + } + + let wasm_opt = match resolve_wasm_opt_with_source()? { + Some((path, _)) => path, + None if config.auto_install => auto_install_wasm_opt()?, + None => return Err(missing_wasm_opt_error()), + }; + check_wasm_opt_version(&wasm_opt)?; + + let before = wasm_bytes.len() as u64; + optimize_in_place(&wasm_opt, &config.level, &wasm_path)?; + let after = std::fs::metadata(&wasm_path) + .with_context(|| format!("Failed to stat optimized {}", wasm_path.display()))? + .len(); + + println!( + "wasm-opt -O{}: main.wasm {before} -> {after} bytes", + config.level + ); + Ok(()) +} + +/// Resolves the `wasm-opt` binary and reports which precedence tier fired: the +/// `WASM_OPT_PATH` override, then `PATH`, then the infs-managed Binaryen under +/// `/tools/binaryen/`. +/// +/// `Ok(None)` means `wasm-opt` was found nowhere — the caller decides whether +/// that is a hard error or triggers `auto-install`. An invalid `WASM_OPT_PATH` +/// override is an `Err`, never a silent fallthrough: a user who set the variable +/// gets their mistake surfaced rather than papered over (in particular, an +/// `auto-install` build must not download over a typo'd override). +/// +/// Emits an `INFS_VERBOSE` trace naming the winning tier and path. +/// +/// # Errors +/// +/// Errors when `WASM_OPT_PATH` is set but does not name a file. +fn resolve_wasm_opt_with_source() -> Result> { + let managed = ToolchainPaths::new() + .ok() + .and_then(|paths| binaryen::installed_wasm_opt(&paths)); + let resolved = resolve_wasm_opt_from(std::env::var_os(WASM_OPT_PATH_ENV).as_deref(), managed)?; + if let Some((path, source)) = &resolved { + trace_resolved(*source, path); + } + Ok(resolved) +} + +/// The testable core of [`resolve_wasm_opt_with_source`], with the environment +/// override and the managed-install lookup lifted into parameters so tests need +/// not mutate process-global state. +/// +/// Precedence: a `WASM_OPT_PATH` override (`override_path`) wins, then a `PATH` +/// lookup, then `managed`. A `Some` override must name an existing file or the +/// call errors (naming the env var and the path) — an explicit override is +/// never discarded in favor of a lower tier. `Ok(None)` means nothing resolved +/// in any tier. +/// +/// # Errors +/// +/// Errors when `override_path` is `Some` but does not name a file. +fn resolve_wasm_opt_from( + override_path: Option<&OsStr>, + managed: Option, +) -> Result> { + if let Some(raw) = override_path { + let path = PathBuf::from(raw); + if path.is_file() { + return Ok(Some((path, WasmOptSource::EnvOverride))); + } + bail!( + "`{WASM_OPT_PATH_ENV}` is set to `{}`, which is not a file. Point it \ + at a `wasm-opt` executable, or unset it to search PATH.", + path.display() + ); + } + + if let Ok(path) = which::which("wasm-opt") { + return Ok(Some((path, WasmOptSource::SystemPath))); + } + + Ok(managed.map(|path| (path, WasmOptSource::ManagedTools))) +} + +/// Downloads the pinned Binaryen `wasm-opt` at build time for a +/// `[build.wasm-opt] auto-install = true` project whose `wasm-opt` resolved +/// nowhere, returning the path to the freshly installed binary. +/// +/// # Errors +/// +/// Errors if the toolchain directory cannot be prepared, the platform cannot be +/// detected, or the download / verification / install fails — with remediation +/// naming a retry, the manual `infs component add wasm-opt`, and disabling the +/// optimizer. +fn auto_install_wasm_opt() -> Result { + println!("wasm-opt not found; [build.wasm-opt] auto-install is enabled."); + let paths = ToolchainPaths::new()?; + paths.ensure_directories()?; + let platform = Platform::detect()?; + binaryen::install_blocking(&paths, platform).with_context(|| { + format!( + "Failed to auto-install the Binaryen `wasm-opt` optimizer ({}). \ + Retry the build, install it manually with `infs component add \ + wasm-opt`, or disable optimization (`enabled = false` under \ + `[build.wasm-opt]`, or pass `--no-wasm-opt`).", + binaryen::BINARYEN_PIN + ) + }) +} + +/// The install-hint error for a `wasm-opt` that resolved in no tier. Leads with +/// the infs-managed option, then the system package managers, and points at +/// `auto-install` for a hands-off setup. +fn missing_wasm_opt_error() -> anyhow::Error { + anyhow::anyhow!( + "wasm-opt not found.\n\n\ + `[build.wasm-opt]` is enabled but the Binaryen `wasm-opt` optimizer \ + could not be located. To install Binaryen:\n \ + - Managed by infs: infs component add wasm-opt\n \ + - macOS: brew install binaryen\n \ + - Linux: apt install binaryen (or your distribution's package)\n \ + - npm: npm install -g binaryen\n \ + - Or download a release: https://github.com/WebAssembly/binaryen/releases\n\n\ + Set `auto-install = true` under `[build.wasm-opt]` to download it \ + automatically at build time. Then ensure `wasm-opt` is in PATH, or set \ + {WASM_OPT_PATH_ENV} to its full path. To build without optimization, \ + set `enabled = false` under `[build.wasm-opt]`, or pass `--no-wasm-opt`." + ) +} + +/// Verifies the resolved `wasm-opt` is new enough ([`MIN_WASM_OPT_VERSION`]). +/// +/// A parsed version below the minimum is a hard error. If `--version` cannot be +/// run, exits unsuccessfully, or emits output that does not parse, this warns +/// (quoting the raw output) and proceeds: a best-effort check must never block a +/// build over an unrecognized — but possibly perfectly good — binary. +/// +/// # Errors +/// +/// Errors only when a version is successfully parsed and is below the minimum. +fn check_wasm_opt_version(wasm_opt: &Path) -> Result<()> { + let output = match Command::new(wasm_opt).arg("--version").output() { + Ok(output) => output, + Err(err) => { + eprintln!( + "warning: could not run `{} --version` ({err}); proceeding \ + without a wasm-opt version check.", + wasm_opt.display() + ); + return Ok(()); + } + }; + + let stdout = String::from_utf8_lossy(&output.stdout); + if !output.status.success() { + eprintln!( + "warning: `{} --version` exited unsuccessfully (output: {:?}); \ + proceeding without a wasm-opt version check.", + wasm_opt.display(), + stdout.trim() + ); + return Ok(()); + } + + let Some(version) = parse_wasm_opt_version(&stdout) else { + eprintln!( + "warning: could not parse a wasm-opt version from {:?}; proceeding \ + without a version check.", + stdout.trim() + ); + return Ok(()); + }; + + if version < MIN_WASM_OPT_VERSION { + bail!( + "wasm-opt version {version} is too old: `[build.wasm-opt]` requires \ + Binaryen {MIN_WASM_OPT_VERSION} or newer. Update Binaryen, or set \ + {WASM_OPT_PATH_ENV} to a newer `wasm-opt`." + ); + } + Ok(()) +} + +/// Parses the major version from `wasm-opt --version` output, whose first line +/// reads like `wasm-opt version 116 (version_116-...)`. Returns the first +/// whitespace-delimited token that parses as a `u32`, or `None` if there is +/// none. +fn parse_wasm_opt_version(stdout: &str) -> Option { + stdout + .split_whitespace() + .find_map(|token| token.parse::().ok()) +} + +/// Runs `wasm-opt --version` and returns the parsed major version, or `None` if +/// the probe cannot be run, exits unsuccessfully, or emits an unparseable +/// banner. Unlike [`check_wasm_opt_version`] this makes no judgement and emits +/// no warnings — it is the classifier [`doctor_check`] uses to pick OK vs WARN. +fn probe_wasm_opt_version(wasm_opt: &Path) -> Option { + let output = Command::new(wasm_opt).arg("--version").output().ok()?; + if !output.status.success() { + return None; + } + parse_wasm_opt_version(&String::from_utf8_lossy(&output.stdout)) +} + +/// The `infs doctor` health check for `wasm-opt`. +/// +/// Reports the resolved binary and its precedence tier when one is found and its +/// version parses; warns when a resolved binary's `--version` fails (a managed +/// copy gets a repair hint — this catches the macOS missing-dylib case); and +/// reports the optional never-installed state as OK so a project that does not +/// use `[build.wasm-opt]` is never alarmed. A `tools/binaryen` directory without +/// the pinned binary, and an invalid `WASM_OPT_PATH`, both warn with remediation. +/// +/// The message is always a single line, per the `[OK|WARN|FAIL] name: message` +/// contract the VS Code extension parses. +#[must_use] +pub(crate) fn doctor_check() -> DoctorCheck { + const NAME: &str = "wasm-opt"; + let paths = ToolchainPaths::new().ok(); + let managed = paths.as_ref().and_then(binaryen::installed_wasm_opt); + + match resolve_wasm_opt_from( + std::env::var_os(WASM_OPT_PATH_ENV).as_deref(), + managed.clone(), + ) { + Ok(Some((path, source))) => wasm_opt_doctor_found(NAME, &path, source, managed.as_deref()), + Ok(None) => wasm_opt_doctor_absent(NAME, paths.as_ref()), + Err(err) => DoctorCheck::warning(NAME, err.to_string()), + } +} + +/// Builds the doctor line for a resolved `wasm-opt`: OK with the source tier and +/// Binaryen version when `--version` parses (noting a managed copy shadowed by a +/// `PATH` hit), or WARN when the version probe fails. +fn wasm_opt_doctor_found( + name: &str, + path: &Path, + source: WasmOptSource, + managed: Option<&Path>, +) -> DoctorCheck { + let Some(version) = probe_wasm_opt_version(path) else { + let hint = if source == WasmOptSource::ManagedTools { + "run 'infs component add wasm-opt' to repair" + } else { + "check the installation" + }; + return DoctorCheck::warning( + name, + format!( + "Found at {} (source: {}) but `wasm-opt --version` failed; {hint}.", + path.display(), + source.label() + ), + ); + }; + + let mut message = format!( + "Found at {} (source: {}, Binaryen {version})", + path.display(), + source.label() + ); + if source == WasmOptSource::SystemPath + && let Some(managed) = managed + { + let _ = write!( + message, + "; managed copy at {} is shadowed by PATH", + managed.display() + ); + } + DoctorCheck::ok(name, message) +} + +/// Builds the doctor line when no `wasm-opt` resolved: WARN when a managed +/// Binaryen directory is present but missing its binary (a broken install to +/// repair), otherwise the optional never-installed OK line that leaves +/// non-users unalarmed. +fn wasm_opt_doctor_absent(name: &str, paths: Option<&ToolchainPaths>) -> DoctorCheck { + if let Some(paths) = paths { + let dir = paths.binaryen_dir(binaryen::BINARYEN_PIN); + if dir.exists() { + return DoctorCheck::warning( + name, + format!( + "A managed Binaryen install at {} is missing its wasm-opt \ + binary; run 'infs component add wasm-opt' to repair.", + dir.display() + ), + ); + } + } + DoctorCheck::ok( + name, + "Not installed (optional — needed only for [build.wasm-opt]; install \ + with 'infs component add wasm-opt')", + ) +} + +/// Scans `wasm_bytes` for a verification-only opcode and returns its source +/// spelling (e.g. `"forall"`, `"i32.uzumaki"`) if one is present. +/// +/// Compile-mode builds strip `spec` blocks, so a well-formed executable artifact +/// carries none of these. Finding one means a verification construct leaked into +/// an ordinary function — `wasm-opt` would reject the unknown `0xfc` opcode with +/// an opaque error, so this pre-scan surfaces it with remediation instead. +/// +/// # Errors +/// +/// Errors if the artifact cannot be parsed as WebAssembly. +fn find_verification_construct(wasm_bytes: &[u8]) -> Result> { + for payload in Parser::new(0).parse_all(wasm_bytes) { + let payload = payload.map_err(|err| { + anyhow::anyhow!("failed to scan out/main.wasm for verification constructs: {err}") + })?; + let Payload::CodeSectionEntry(body) = payload else { + continue; + }; + let operators = body.get_operators_reader().map_err(|err| { + anyhow::anyhow!("failed to read a function body while scanning out/main.wasm: {err}") + })?; + for op in operators { + let op = op.map_err(|err| { + anyhow::anyhow!("failed to decode an operator while scanning out/main.wasm: {err}") + })?; + if let Some(name) = verification_construct_name(&op) { + return Ok(Some(name)); + } + } + } + Ok(None) +} + +/// The source spelling of a verification-only operator, or `None` for an +/// ordinary executable one. +/// +/// This is the local mirror of `is_verification_only` in +/// `core/wasm-linker/src/safety.rs` — the linker's fail-closed predicate over +/// the same six opcodes. Both consume the same `inf-wasmparser` fork, so a new +/// verification opcode requires touching that fork (where the mirrored-predicate +/// note lives); a wasm-linker dependency for six match arms is not worth the +/// coupling. +fn verification_construct_name(op: &Operator) -> Option<&'static str> { + use Operator::{Assume, Exists, Forall, I32Uzumaki, I64Uzumaki, Unique}; + match op { + Forall { .. } => Some("forall"), + Exists { .. } => Some("exists"), + Assume { .. } => Some("assume"), + Unique { .. } => Some("unique"), + I32Uzumaki { .. } => Some("i32.uzumaki"), + I64Uzumaki { .. } => Some("i64.uzumaki"), + _ => None, + } +} + +/// Builds the `wasm-opt` argument vector. +/// +/// `--mvp-features` pins the baseline feature set so the result is stable across +/// Binaryen versions; the two `--enable-*` flags re-admit exactly the proposals +/// Inference codegen relies on — a mutable exported `__stack_pointer` global +/// (mutable-globals) and `memory.copy`/`memory.fill` (bulk-memory) — matching +/// the linker's supported-feature envelope. `-O` works uniformly for +/// every value `WasmOptConfig` validates, so there is no second mapping table. +fn wasm_opt_args(level: &str, input: &Path, output: &Path) -> Vec { + vec![ + OsString::from(format!("-O{level}")), + OsString::from("--mvp-features"), + OsString::from("--enable-mutable-globals"), + OsString::from("--enable-bulk-memory"), + input.as_os_str().to_os_string(), + OsString::from("-o"), + output.as_os_str().to_os_string(), + ] +} + +/// Runs `wasm-opt` over `wasm_path`, replacing it in place only if the optimized +/// bytes re-validate. +/// +/// The optimizer writes to a sibling temp file (`main.wasm.opt`, same directory +/// and filesystem) and the original is swapped in via an atomic +/// [`std::fs::rename`] only after the result validates. Every failure path — a +/// nonzero `wasm-opt` exit, unreadable output, or failed re-validation — leaves +/// the original artifact untouched and makes a best-effort attempt to remove the +/// temp file. +/// +/// # Errors +/// +/// Errors if `wasm-opt` cannot be spawned, exits nonzero, produces output that +/// cannot be read or fails re-validation, or if the final rename fails. +fn optimize_in_place(wasm_opt: &Path, level: &str, wasm_path: &Path) -> Result<()> { + let tmp_path = optimized_tmp_path(wasm_path); + let args = wasm_opt_args(level, wasm_path, &tmp_path); + + let output = Command::new(wasm_opt) + .args(&args) + .output() + .with_context(|| format!("Failed to execute wasm-opt at {}", wasm_opt.display()))?; + + if !output.status.success() { + let _ = std::fs::remove_file(&tmp_path); + let code = output.status.code().unwrap_or(1); + bail!( + "wasm-opt failed (exit code {code}): {}\nThe unoptimized artifact at \ + {} is unchanged.", + String::from_utf8_lossy(&output.stderr).trim(), + wasm_path.display() + ); + } + + let optimized = match std::fs::read(&tmp_path) { + Ok(bytes) => bytes, + Err(err) => { + let _ = std::fs::remove_file(&tmp_path); + return Err(anyhow::Error::from(err).context(format!( + "wasm-opt reported success but its output at {} could not be read", + tmp_path.display() + ))); + } + }; + + if let Err(err) = validate_optimized(&optimized) { + let _ = std::fs::remove_file(&tmp_path); + bail!( + "wasm-opt produced an artifact that failed re-validation: {err}. The \ + original {} is unchanged; try `--no-wasm-opt`, or a different \ + Binaryen version.", + wasm_path.display() + ); + } + + std::fs::rename(&tmp_path, wasm_path).map_err(|err| { + let _ = std::fs::remove_file(&tmp_path); + anyhow::Error::from(err).context(format!( + "Failed to replace {} with the optimized artifact", + wasm_path.display() + )) + })?; + + Ok(()) +} + +/// The sibling temp path `wasm-opt` writes to: the artifact path with `.opt` +/// appended (`out/main.wasm` → `out/main.wasm.opt`). Kept in the same directory +/// so the final [`std::fs::rename`] stays on one filesystem and is atomic. +fn optimized_tmp_path(wasm_path: &Path) -> PathBuf { + let mut tmp = wasm_path.as_os_str().to_os_string(); + tmp.push(".opt"); + PathBuf::from(tmp) +} + +/// Re-validates `bytes` against the same feature envelope the linker enforces +/// (`GC_TYPES | MUTABLE_GLOBAL | BULK_MEMORY`), guarding against a `wasm-opt` +/// that emits something outside the executable subset the pipeline supports. +/// +/// # Errors +/// +/// Errors with the validator's message when `bytes` is not valid WebAssembly +/// within that feature set. +fn validate_optimized(bytes: &[u8]) -> Result<()> { + let features = WasmFeatures::GC_TYPES + .union(WasmFeatures::MUTABLE_GLOBAL) + .union(WasmFeatures::BULK_MEMORY); + inf_wasmparser::Validator::new_with_features(features) + .validate_all(bytes) + .map_err(|err| anyhow::anyhow!("{err}"))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_fs::prelude::*; + + /// Wraps a raw code-section body (an operator stream) into a one-function + /// module and returns the finished bytes. `wat` cannot assemble the custom + /// `0xfc`-prefixed Inference opcodes, so bodies exercising them are built + /// byte-by-byte (recipe mirrored from `core/wasm-linker/src/safety.rs`). + /// `Function::new([])` emits the empty-locals byte, so `body` is the + /// instruction stream that follows it. + fn module_with_raw_body(body: &[u8]) -> Vec { + use wasm_encoder::{CodeSection, Function, FunctionSection, Module, TypeSection}; + let mut module = Module::new(); + let mut types = TypeSection::new(); + types.ty().function([], []); + module.section(&types); + let mut funcs = FunctionSection::new(); + funcs.function(0); + module.section(&funcs); + let mut code = CodeSection::new(); + let mut f = Function::new([]); + f.raw(body.iter().copied()); + code.function(&f); + module.section(&code); + module.finish() + } + + #[test] + fn parse_wasm_opt_version_reads_release_output() { + assert_eq!( + parse_wasm_opt_version("wasm-opt version 116 (version_116)"), + Some(116) + ); + } + + #[test] + fn parse_wasm_opt_version_reads_git_suffix_output() { + assert_eq!( + parse_wasm_opt_version("wasm-opt version 123 (version_123-4-gdeadbee)"), + Some(123) + ); + } + + #[test] + fn parse_wasm_opt_version_rejects_garbage() { + assert_eq!(parse_wasm_opt_version("banana"), None); + assert_eq!(parse_wasm_opt_version("wasm-opt version vNext"), None); + } + + #[test] + fn parse_wasm_opt_version_rejects_empty() { + assert_eq!(parse_wasm_opt_version(""), None); + } + + #[test] + fn wasm_opt_args_are_exact_for_level_z() { + let args = wasm_opt_args("z", Path::new("in.wasm"), Path::new("out.wasm.opt")); + let expected: Vec = [ + "-Oz", + "--mvp-features", + "--enable-mutable-globals", + "--enable-bulk-memory", + "in.wasm", + "-o", + "out.wasm.opt", + ] + .into_iter() + .map(OsString::from) + .collect(); + assert_eq!(args, expected); + } + + #[test] + fn wasm_opt_args_are_exact_for_level_3() { + let args = wasm_opt_args("3", Path::new("a.wasm"), Path::new("b.wasm.opt")); + let expected: Vec = [ + "-O3", + "--mvp-features", + "--enable-mutable-globals", + "--enable-bulk-memory", + "a.wasm", + "-o", + "b.wasm.opt", + ] + .into_iter() + .map(OsString::from) + .collect(); + assert_eq!(args, expected); + } + + #[test] + fn resolve_from_override_wins_over_managed() { + // A valid WASM_OPT_PATH override short-circuits before the PATH lookup + // and the managed tier, and reports the EnvOverride source. + let dir = assert_fs::TempDir::new().unwrap(); + let fake = dir.child("wasm-opt"); + fake.write_str("#!/bin/sh\n").unwrap(); + let managed = dir.path().join("managed-wasm-opt"); + + let (path, source) = resolve_wasm_opt_from(Some(fake.path().as_os_str()), Some(managed)) + .unwrap() + .expect("a valid override must resolve"); + assert_eq!(path, fake.path()); + assert_eq!(source, WasmOptSource::EnvOverride); + } + + #[test] + fn resolve_from_rejects_nonexistent_override_even_with_managed() { + // An invalid override is an error even when a managed copy is available: + // a user's mistake is surfaced, never silently papered over by a lower + // tier (an auto-install build must not download over a typo). + let dir = assert_fs::TempDir::new().unwrap(); + let missing = dir.path().join("nope-wasm-opt"); + let managed = dir.path().join("managed-wasm-opt"); + let err = resolve_wasm_opt_from(Some(missing.as_os_str()), Some(managed)).unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains(WASM_OPT_PATH_ENV) && msg.contains("not a file"), + "an override that is not a file must name the env var and the failure, got: {msg}" + ); + } + + #[test] + fn resolve_from_reports_nothing_when_no_tier_resolves() { + // No override, no managed, and a PATH lookup that (in the vanishing + // chance a real wasm-opt is present) would be the only hit — the + // contract is that a genuine miss is Ok(None). Guarded by clearing the + // managed tier; the PATH-dependent cases are covered by the serial and + // integration tests. + let resolved = resolve_wasm_opt_from(None, None); + assert!( + matches!(resolved, Ok(None | Some((_, WasmOptSource::SystemPath)))), + "with no override and no managed tier, resolution is either a PATH \ + hit or Ok(None), got: {resolved:?}" + ); + } + + #[test] + #[serial_test::serial] + fn resolve_from_uses_managed_when_path_misses() { + // With no override and an empty PATH, the managed tier is the fallback + // and is reported as ManagedTools. + let dir = assert_fs::TempDir::new().unwrap(); + let managed = dir.path().join("managed-wasm-opt"); + std::fs::write(&managed, b"managed").unwrap(); + + let original = std::env::var_os("PATH"); + // SAFETY: serialized test; PATH restored immediately below. + unsafe { + std::env::set_var("PATH", ""); + } + let resolved = resolve_wasm_opt_from(None, Some(managed.clone())); + // SAFETY: restore regardless of the assertion outcome. + unsafe { + match original { + Some(path) => std::env::set_var("PATH", path), + None => std::env::remove_var("PATH"), + } + } + + let (path, source) = resolved + .unwrap() + .expect("managed must resolve when PATH misses"); + assert_eq!(path, managed); + assert_eq!(source, WasmOptSource::ManagedTools); + } + + // Requires an executable stub on PATH; `which` only accepts an executable + // file, so this is gated to unix where a chmod is portable. + #[cfg(unix)] + #[test] + #[serial_test::serial] + fn resolve_from_path_wins_over_managed() { + use std::os::unix::fs::PermissionsExt; + + let path_dir = assert_fs::TempDir::new().unwrap(); + let on_path = path_dir.path().join("wasm-opt"); + std::fs::write(&on_path, b"#!/bin/sh\nexit 0\n").unwrap(); + std::fs::set_permissions(&on_path, std::fs::Permissions::from_mode(0o755)).unwrap(); + + let managed_dir = assert_fs::TempDir::new().unwrap(); + let managed = managed_dir.path().join("managed-wasm-opt"); + std::fs::write(&managed, b"managed").unwrap(); + + let original = std::env::var_os("PATH"); + // SAFETY: serialized test; PATH restored immediately below. + unsafe { + std::env::set_var("PATH", path_dir.path()); + } + let resolved = resolve_wasm_opt_from(None, Some(managed.clone())); + // SAFETY: restore regardless of the assertion outcome. + unsafe { + match original { + Some(path) => std::env::set_var("PATH", path), + None => std::env::remove_var("PATH"), + } + } + + // If `which` located our stub, PATH must win over managed. In a + // restricted sandbox where `which` cannot see it, fall through to the + // managed tier — both are acceptable; what must never happen is PATH + // losing to managed when PATH did resolve. + let (path, source) = resolved.unwrap().expect("a tier must resolve"); + if source == WasmOptSource::SystemPath { + assert_eq!( + path.canonicalize().unwrap(), + on_path.canonicalize().unwrap(), + "the PATH hit must be the stub, not the managed copy" + ); + } else { + assert_eq!(source, WasmOptSource::ManagedTools); + } + } + + #[test] + fn wasm_opt_source_labels_are_stable() { + // The labels are a contract shared by the INFS_VERBOSE trace and the + // doctor line; they must not drift. + assert_eq!(WasmOptSource::EnvOverride.label(), "WASM_OPT_PATH env"); + assert_eq!(WasmOptSource::SystemPath.label(), "PATH"); + assert_eq!(WasmOptSource::ManagedTools.label(), "managed tools"); + } + + #[test] + fn doctor_absent_reports_optional_ok_without_managed_residue() { + // No managed Binaryen directory: the never-installed state is optional + // OK so a project that does not use `[build.wasm-opt]` is never alarmed. + let root = assert_fs::TempDir::new().unwrap(); + let paths = ToolchainPaths::with_root(root.path().to_path_buf()); + let check = wasm_opt_doctor_absent("wasm-opt", Some(&paths)); + assert_eq!(check.prefix(), "[OK]"); + assert!(check.message.contains("Not installed (optional")); + assert!(check.message.contains("infs component add wasm-opt")); + } + + #[test] + fn doctor_absent_warns_on_broken_managed_dir() { + // A managed directory without the binary is a broken install: WARN with + // the repair hint. + let root = assert_fs::TempDir::new().unwrap(); + let paths = ToolchainPaths::with_root(root.path().to_path_buf()); + std::fs::create_dir_all(paths.binaryen_dir(binaryen::BINARYEN_PIN)).unwrap(); + let check = wasm_opt_doctor_absent("wasm-opt", Some(&paths)); + assert_eq!(check.prefix(), "[WARN]"); + assert!(check.message.contains("repair")); + assert!(check.message.contains("infs component add wasm-opt")); + } + + /// Writes an executable stub at `/wasm-opt` printing `version_output` + /// (empty means print nothing) and exiting `exit_code`. + #[cfg(unix)] + fn write_version_stub( + dir: &assert_fs::TempDir, + version_output: &str, + exit_code: i32, + ) -> PathBuf { + use assert_fs::prelude::*; + use std::os::unix::fs::PermissionsExt; + let stub = dir.child("wasm-opt"); + stub.write_str(&format!( + "#!/bin/sh\necho '{version_output}'\nexit {exit_code}\n" + )) + .unwrap(); + std::fs::set_permissions(stub.path(), std::fs::Permissions::from_mode(0o755)).unwrap(); + stub.path().to_path_buf() + } + + #[cfg(unix)] + #[test] + fn doctor_found_warns_when_version_probe_fails() { + // A resolved binary whose `--version` exits nonzero warns; a managed + // copy gets the repair hint (this is the macOS missing-dylib case), + // everything else the generic "check the installation". + let dir = assert_fs::TempDir::new().unwrap(); + let stub = write_version_stub(&dir, "", 1); + + let managed = wasm_opt_doctor_found("wasm-opt", &stub, WasmOptSource::ManagedTools, None); + assert_eq!(managed.prefix(), "[WARN]"); + assert!( + managed + .message + .contains("run 'infs component add wasm-opt' to repair") + ); + + let system = wasm_opt_doctor_found("wasm-opt", &stub, WasmOptSource::SystemPath, None); + assert_eq!(system.prefix(), "[WARN]"); + assert!(system.message.contains("check the installation")); + } + + #[cfg(unix)] + #[test] + fn doctor_found_ok_notes_managed_shadowed_by_path() { + // A PATH hit that parses a version, with a managed copy also present, + // is OK and notes the managed copy is shadowed by PATH. + let dir = assert_fs::TempDir::new().unwrap(); + let stub = write_version_stub(&dir, "wasm-opt version 118 (test)", 0); + let managed = dir.path().join("managed-wasm-opt"); + + let check = + wasm_opt_doctor_found("wasm-opt", &stub, WasmOptSource::SystemPath, Some(&managed)); + assert_eq!(check.prefix(), "[OK]"); + assert!( + check.message.contains("source: PATH, Binaryen 118"), + "the OK line must name the tier and version, got: {}", + check.message + ); + assert!( + check.message.contains("shadowed by PATH"), + "a shadowed managed copy must be noted, got: {}", + check.message + ); + } + + #[test] + fn find_verification_construct_detects_each_nondet_block() { + for (sub_opcode, name) in [ + (0x3a_u8, "forall"), + (0x3b, "exists"), + (0x3c, "assume"), + (0x3d, "unique"), + ] { + // ` (empty blocktype) end; end`. + let body = [0x00, 0xfc, sub_opcode, 0x40, 0x0b, 0x0b]; + let module = module_with_raw_body(&body); + assert_eq!( + find_verification_construct(&module).unwrap(), + Some(name), + "sub-opcode {sub_opcode:#x} must be reported as `{name}`" + ); + } + } + + #[test] + fn find_verification_construct_detects_uzumaki() { + // ` drop; end`, for both the i32 and i64 forms. + let i32_body = [0x00, 0xfc, 0x31, 0x1a, 0x0b]; + assert_eq!( + find_verification_construct(&module_with_raw_body(&i32_body)).unwrap(), + Some("i32.uzumaki") + ); + let i64_body = [0x00, 0xfc, 0x32, 0x1a, 0x0b]; + assert_eq!( + find_verification_construct(&module_with_raw_body(&i64_body)).unwrap(), + Some("i64.uzumaki") + ); + } + + #[test] + fn find_verification_construct_ignores_plain_body() { + // An ordinary executable body (just `end`) carries no verification-only + // opcode. + let module = module_with_raw_body(&[0x0b]); + assert_eq!(find_verification_construct(&module).unwrap(), None); + } + + // Spawns a real executable stub, so it is gated to unix (mirroring the + // stub-based tests in `project_build.rs`); executing a script through + // `Command` is not portable to Windows. + #[cfg(unix)] + #[test] + fn optimize_in_place_leaves_original_on_wasm_opt_failure() { + // A `wasm-opt` that exits nonzero must not touch the original artifact, + // and must not leave the temp file behind. + let dir = assert_fs::TempDir::new().unwrap(); + let wasm_path = dir.path().join("main.wasm"); + let original = b"original artifact bytes"; + std::fs::write(&wasm_path, original).unwrap(); + + let fake = write_failing_wasm_opt(&dir); + let err = optimize_in_place(&fake, "z", &wasm_path).unwrap_err(); + assert!( + err.to_string().contains("wasm-opt failed"), + "a nonzero exit must surface as a wasm-opt failure, got: {err}" + ); + assert_eq!( + std::fs::read(&wasm_path).unwrap(), + original, + "the original artifact must be unchanged after a wasm-opt failure" + ); + assert!( + !optimized_tmp_path(&wasm_path).exists(), + "the temp file must be cleaned up after a failure" + ); + } + + #[test] + fn post_build_optimize_is_noop_without_table() { + // No [build.wasm-opt] table: a no-op even when out/main.wasm is absent. + let dir = assert_fs::TempDir::new().unwrap(); + let ctx = ctx_with_wasm_opt(dir.path(), None); + assert!(post_build_optimize(&ctx, false, None, false).is_ok()); + } + + #[test] + fn post_build_optimize_is_noop_when_disabled_by_config() { + let dir = assert_fs::TempDir::new().unwrap(); + let ctx = ctx_with_wasm_opt(dir.path(), Some((false, "3"))); + assert!(post_build_optimize(&ctx, false, None, false).is_ok()); + } + + #[test] + fn post_build_optimize_is_noop_when_disabled_by_cli() { + let dir = assert_fs::TempDir::new().unwrap(); + let ctx = ctx_with_wasm_opt(dir.path(), Some((true, "3"))); + // cli_disabled short-circuits before the missing-artifact check. + assert!(post_build_optimize(&ctx, false, None, true).is_ok()); + } + + #[test] + fn post_build_optimize_skips_proof_and_v_builds() { + let dir = assert_fs::TempDir::new().unwrap(); + let ctx = ctx_with_wasm_opt(dir.path(), Some((true, "3"))); + // Proof mode and any -v build skip before touching the (absent) artifact. + assert!(post_build_optimize(&ctx, false, Some(BuildMode::Proof), false).is_ok()); + assert!(post_build_optimize(&ctx, true, None, false).is_ok()); + } + + #[test] + fn post_build_optimize_errors_on_missing_artifact_when_active() { + // Active config, compile mode: the missing artifact is a hard error + // (the skip short-circuits above do not apply). + let dir = assert_fs::TempDir::new().unwrap(); + let ctx = ctx_with_wasm_opt(dir.path(), Some((true, "3"))); + let err = post_build_optimize(&ctx, false, None, false).unwrap_err(); + assert!( + err.to_string().contains("WASM file not found"), + "an active config with no artifact must error, got: {err}" + ); + } + + /// Builds a [`ProjectContext`] rooted at `root` whose manifest carries the + /// given `[build.wasm-opt]` setting (`Some((enabled, level))`) or none. + fn ctx_with_wasm_opt(root: &Path, wasm_opt: Option<(bool, &str)>) -> ProjectContext { + use crate::project::manifest::{InferenceToml, WasmOptConfig}; + let mut manifest = InferenceToml::new("demo"); + manifest.build.wasm_opt = wasm_opt.map(|(enabled, level)| WasmOptConfig { + enabled, + level: level.to_string(), + auto_install: false, + }); + ProjectContext { + root: root.to_path_buf(), + manifest, + entry_point: root.join("src").join("main.inf"), + } + } + + /// Writes an executable stub at `/wasm-opt` that exits nonzero for any + /// non-`--version` invocation, used to exercise the failure path. + #[cfg(unix)] + fn write_failing_wasm_opt(dir: &assert_fs::TempDir) -> PathBuf { + use std::os::unix::fs::PermissionsExt; + let stub = dir.child("wasm-opt"); + stub.write_str("#!/bin/sh\necho 'fake failure' 1>&2\nexit 1\n") + .unwrap(); + let mut perms = std::fs::metadata(stub.path()).unwrap().permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(stub.path(), perms).unwrap(); + stub.path().to_path_buf() + } +} diff --git a/apps/infs/src/main.rs b/apps/infs/src/main.rs index b830ee28..62b2863d 100644 --- a/apps/infs/src/main.rs +++ b/apps/infs/src/main.rs @@ -63,7 +63,8 @@ mod tui; use anyhow::Result; use clap::{Parser, Subcommand}; use commands::{ - build, default, doctor, init, install, list, new, run, self_cmd, uninstall, version, versions, + build, component, default, doctor, init, install, list, new, run, self_cmd, uninstall, version, + versions, }; use errors::InfsError; @@ -178,6 +179,12 @@ pub enum Commands { /// correctly. Reports any issues with suggested remediation steps. Doctor, + /// Manage optional toolchain components. + /// + /// Installs, lists, or removes managed components such as `wasm-opt` + /// (Binaryen), the optimizer used by the `[build.wasm-opt]` manifest table. + Component(component::ComponentArgs), + /// Manage the infs binary itself. /// /// Provides subcommands for updating or managing the infs CLI tool. @@ -221,6 +228,7 @@ async fn run() -> Result<()> { Some(Commands::Versions(args)) => versions::execute(&args).await, Some(Commands::Default(args)) => default::execute(&args).await, Some(Commands::Doctor) => doctor::execute().await, + Some(Commands::Component(args)) => component::execute(&args).await, Some(Commands::SelfCmd(args)) => self_cmd::execute(&args).await, None => { if cli.headless || !tui::should_use_tui() { diff --git a/apps/infs/src/project/manifest.rs b/apps/infs/src/project/manifest.rs index fed50a4b..5d36fdbd 100644 --- a/apps/infs/src/project/manifest.rs +++ b/apps/infs/src/project/manifest.rs @@ -26,6 +26,11 @@ //! optimize = "release" //! mode = "compile" # "compile" (executable) or "proof" (Rocq specs) //! +//! [build.wasm-opt] # optional: post-build optimization of the executable +//! enabled = true # table presence enables; set false to keep it off +//! level = "3" # forwarded as -O: "0".."4", "s", "z" +//! auto-install = false # download wasm-opt automatically if it is missing +//! //! [verification] //! output-dir = "proofs/" # honored only in proof mode //! ``` @@ -200,9 +205,7 @@ pub fn validate_wasm_dependency_key(key: &str) -> Result<()> { bail!("invalid [wasm-dependencies] key: the module name is empty"); } if key.contains('=') { - bail!( - "invalid [wasm-dependencies] key `{key}`: a module name cannot contain `=`" - ); + bail!("invalid [wasm-dependencies] key `{key}`: a module name cannot contain `=`"); } let segments: Vec<&str> = key.split("::").collect(); @@ -264,6 +267,15 @@ pub struct BuildConfig { /// Validated case-sensitively on load (see [`InferenceToml::from_toml`]). #[serde(default = "default_mode")] pub mode: String, + + /// Optional `[build.wasm-opt]` sub-table. Absent means post-build + /// optimization is off; present means on unless `enabled = false`. + /// + /// Declared last because a TOML sub-table must serialize after the scalar + /// keys of its parent table, and `toml` emits struct fields in declaration + /// order. + #[serde(rename = "wasm-opt", default, skip_serializing_if = "Option::is_none")] + pub wasm_opt: Option, } impl Default for BuildConfig { @@ -272,17 +284,24 @@ impl Default for BuildConfig { target: default_target(), optimize: default_optimize(), mode: default_mode(), + wasm_opt: None, } } } impl BuildConfig { /// Returns true if this is the default configuration. + /// + /// A present `[build.wasm-opt]` table makes the config non-default even + /// when the other fields are defaults: without this, a manifest whose only + /// `[build]` content is `[build.wasm-opt]` would round-trip to nothing + /// (the whole `[build]` table is skipped when `is_default()` holds). #[must_use] pub fn is_default(&self) -> bool { self.target == default_target() && self.optimize == default_optimize() && self.mode == default_mode() + && self.wasm_opt.is_none() } /// Validates the `mode` field, accepting only `"compile"` or `"proof"` @@ -301,6 +320,70 @@ impl BuildConfig { self.mode ); } + if let Some(wasm_opt) = &self.wasm_opt { + wasm_opt.validate()?; + } + Ok(()) + } +} + +/// The `[build.wasm-opt]` table: post-build optimization of the compile-mode +/// artifact via the external Binaryen `wasm-opt` binary. Table presence means +/// enabled unless `enabled = false`. Proof-mode artifacts are never optimized. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct WasmOptConfig { + /// Whether the optimizer runs. Defaults to `true` so that merely declaring + /// `[build.wasm-opt]` turns the feature on. + #[serde(default = "default_wasm_opt_enabled")] + pub enabled: bool, + + /// Optimization level, forwarded as `-O` to `wasm-opt`. One of + /// [`WASM_OPT_LEVELS`]: `"0"`..`"4"`, `"s"`, `"z"`. + #[serde(default = "default_wasm_opt_level")] + pub level: String, + + /// Whether a missing `wasm-opt` is downloaded automatically at build time. + /// + /// Defaults to `false`: an absent binary is a hard error with install + /// remediation, so a build never reaches out to the network unless the + /// project opts in. Set `true` to have `infs` provision the pinned, + /// checksum-verified Binaryen (the same install `infs component add + /// wasm-opt` performs) on first use. The opt-in is recorded in the versioned + /// manifest — there are no interactive prompts. + #[serde(rename = "auto-install", default)] + pub auto_install: bool, +} + +/// The `-O` values accepted by `[build.wasm-opt] level`, matching the +/// levels `wasm-opt` itself understands: numeric `0`–`4`, plus the size-biased +/// `s` and `z`. +pub const WASM_OPT_LEVELS: &[&str] = &["0", "1", "2", "3", "4", "s", "z"]; + +impl Default for WasmOptConfig { + fn default() -> Self { + Self { + enabled: default_wasm_opt_enabled(), + level: default_wasm_opt_level(), + auto_install: false, + } + } +} + +impl WasmOptConfig { + /// Validates the `level` field against [`WASM_OPT_LEVELS`]. + /// + /// # Errors + /// + /// Returns an error naming the offending value and the allowed set when + /// `level` is not one of the accepted `-O` values. + fn validate(&self) -> Result<()> { + if !WASM_OPT_LEVELS.contains(&self.level.as_str()) { + bail!( + "Invalid `[build.wasm-opt] level` value `{}`: expected one of \ + `0`, `1`, `2`, `3`, `4`, `s`, `z`.", + self.level + ); + } Ok(()) } } @@ -467,6 +550,14 @@ fn default_mode() -> String { String::from("compile") } +fn default_wasm_opt_enabled() -> bool { + true +} + +fn default_wasm_opt_level() -> String { + String::from("3") +} + fn default_output_dir() -> String { String::from("proofs/") } @@ -585,8 +676,7 @@ impl InferenceToml { pub fn load(path: &Path) -> Result { let content = std::fs::read_to_string(path) .with_context(|| format!("Failed to read manifest: {}", path.display()))?; - Self::from_toml(&content) - .with_context(|| format!("Invalid manifest: {}", path.display())) + Self::from_toml(&content).with_context(|| format!("Invalid manifest: {}", path.display())) } } @@ -740,6 +830,7 @@ mod tests { target: String::from("wasm64"), optimize: String::from("debug"), mode: default_mode(), + wasm_opt: None, }; assert!(!config.is_default()); } @@ -884,6 +975,209 @@ mod tests { ); } + #[test] + fn wasm_opt_absent_table_parses_as_none() { + // Backcompat: a manifest without [build.wasm-opt] leaves the field + // None, and the [build] config still counts as default. + let src = r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build] +mode = "compile" +"#; + let manifest = InferenceToml::from_toml(src).unwrap(); + assert!(manifest.build.wasm_opt.is_none()); + assert!(manifest.build.is_default()); + } + + #[test] + fn wasm_opt_bare_table_enables_with_default_level() { + // Presence alone enables the optimizer; the level defaults to "3". + let src = r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build.wasm-opt] +"#; + let manifest = InferenceToml::from_toml(src).unwrap(); + let wasm_opt = manifest.build.wasm_opt.expect("table present"); + assert!(wasm_opt.enabled); + assert_eq!(wasm_opt.level, "3"); + } + + #[test] + fn wasm_opt_enabled_false_is_honored() { + let src = r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build.wasm-opt] +enabled = false +"#; + let manifest = InferenceToml::from_toml(src).unwrap(); + let wasm_opt = manifest.build.wasm_opt.expect("table present"); + assert!(!wasm_opt.enabled); + assert_eq!(wasm_opt.level, "3", "level still defaults when disabled"); + } + + #[test] + fn wasm_opt_auto_install_defaults_to_false() { + // Absent `auto-install`, a build never reaches out to the network: a + // missing binary is a hard error, not a silent download. + let src = r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build.wasm-opt] +"#; + let manifest = InferenceToml::from_toml(src).unwrap(); + let wasm_opt = manifest.build.wasm_opt.expect("table present"); + assert!( + !wasm_opt.auto_install, + "auto-install must default to false (opt-in provisioning)" + ); + assert!( + !WasmOptConfig::default().auto_install, + "the Default impl must also be false" + ); + } + + #[test] + fn wasm_opt_auto_install_true_is_parsed() { + let src = r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build.wasm-opt] +auto-install = true +"#; + let manifest = InferenceToml::from_toml(src).unwrap(); + let wasm_opt = manifest.build.wasm_opt.expect("table present"); + assert!( + wasm_opt.auto_install, + "`auto-install = true` must be honored" + ); + } + + #[test] + fn wasm_opt_auto_install_round_trips_through_toml() { + let src = r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build.wasm-opt] +enabled = true +level = "z" +auto-install = true +"#; + let manifest = InferenceToml::from_toml(src).expect("parses"); + let serialized = manifest.to_toml().expect("serializes"); + let reparsed = InferenceToml::from_toml(&serialized).expect("reparses"); + assert_eq!(manifest, reparsed); + assert!( + serialized.contains("auto-install = true"), + "the auto-install flag must survive serialization under its \ + hyphenated key, got:\n{serialized}" + ); + } + + #[test] + fn wasm_opt_accepts_every_documented_level() { + for level in WASM_OPT_LEVELS { + let src = format!( + r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build.wasm-opt] +level = "{level}" +"# + ); + let manifest = InferenceToml::from_toml(&src) + .unwrap_or_else(|e| panic!("level `{level}` must be accepted: {e}")); + assert_eq!(manifest.build.wasm_opt.unwrap().level, *level); + } + } + + #[test] + fn wasm_opt_rejects_unknown_level() { + let src = r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build.wasm-opt] +level = "9" +"#; + let err = InferenceToml::from_toml(src).unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("wasm-opt") && msg.contains("level") && msg.contains('9'), + "error must name the table, the field, and the offending value, got: {msg}" + ); + } + + #[test] + fn wasm_opt_table_round_trips_through_toml() { + let src = r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build.wasm-opt] +enabled = true +level = "z" +"#; + let manifest = InferenceToml::from_toml(src).expect("parses"); + let serialized = manifest.to_toml().expect("serializes"); + let reparsed = InferenceToml::from_toml(&serialized).expect("reparses"); + assert_eq!(manifest, reparsed); + assert!( + serialized.contains("wasm-opt"), + "the [build.wasm-opt] table must survive serialization, got:\n{serialized}" + ); + } + + #[test] + fn wasm_opt_table_makes_build_config_non_default() { + // A [build.wasm-opt]-only manifest must not round-trip to nothing: the + // sub-table's presence makes BuildConfig non-default, so the whole + // [build] table is serialized rather than skipped. + let src = r#" +[package] +name = "demo" +version = "0.1.0" +infc_version = "0.1.0" + +[build.wasm-opt] +level = "z" +"#; + let manifest = InferenceToml::from_toml(src).unwrap(); + assert!( + !manifest.build.is_default(), + "a present [build.wasm-opt] table must make BuildConfig non-default" + ); + let serialized = manifest.to_toml().unwrap(); + assert!(serialized.contains("wasm-opt")); + } + #[test] fn test_new_manifest_has_no_wasm_dependencies() { let manifest = InferenceToml::new("myproject"); @@ -1032,7 +1326,10 @@ mode = "Proof" let config = VerificationConfig { output_dir: String::from("proofs/"), }; - assert_eq!(config.normalized_output_dir().unwrap(), PathBuf::from("proofs")); + assert_eq!( + config.normalized_output_dir().unwrap(), + PathBuf::from("proofs") + ); } #[test] @@ -1178,7 +1475,9 @@ mode = "Proof" // separately in scaffold.rs.) let dir = assert_fs::TempDir::new().unwrap(); let path = dir.path().join(MANIFEST_FILE_NAME); - InferenceToml::new("scaffolded").write_to_file(&path).unwrap(); + InferenceToml::new("scaffolded") + .write_to_file(&path) + .unwrap(); let loaded = InferenceToml::load(&path).unwrap(); assert_eq!(loaded.build.mode, "compile"); @@ -1452,7 +1751,14 @@ target = "wasm32" #[test] fn validate_wasm_dependency_key_accepts_logical_names() { - for key in ["arith", "crypto", "_priv", "a1", "crypto::sha256", "a::b::c"] { + for key in [ + "arith", + "crypto", + "_priv", + "a1", + "crypto::sha256", + "a::b::c", + ] { assert!( validate_wasm_dependency_key(key).is_ok(), "`{key}` should be a valid logical name" @@ -1509,7 +1815,9 @@ target = "wasm32" fn test_find_manifest_dir_in_same_directory() { let temp = assert_fs::TempDir::new().unwrap(); let manifest = temp.child(MANIFEST_FILE_NAME); - manifest.write_str("[package]\nname = \"x\"\nversion = \"0.1.0\"\n").unwrap(); + manifest + .write_str("[package]\nname = \"x\"\nversion = \"0.1.0\"\n") + .unwrap(); let source = temp.child("main.inf"); source.write_str("").unwrap(); @@ -1521,7 +1829,9 @@ target = "wasm32" fn test_find_manifest_dir_walks_up_from_nested_source() { let temp = assert_fs::TempDir::new().unwrap(); let manifest = temp.child(MANIFEST_FILE_NAME); - manifest.write_str("[package]\nname = \"x\"\nversion = \"0.1.0\"\n").unwrap(); + manifest + .write_str("[package]\nname = \"x\"\nversion = \"0.1.0\"\n") + .unwrap(); let nested = temp.child("src").child("deep"); nested.create_dir_all().unwrap(); let source = nested.child("main.inf"); diff --git a/apps/infs/src/project/scaffold.rs b/apps/infs/src/project/scaffold.rs index 4beb82a9..d1682688 100644 --- a/apps/infs/src/project/scaffold.rs +++ b/apps/infs/src/project/scaffold.rs @@ -208,9 +208,11 @@ fn write_git_files(project_path: &Path) -> Result<()> { /// in project mode (`infs build`/`run` consume it), so the scaffolded file must /// produce a value the loader reads back consistently — the round-trip is /// covered by a test. `target`/`optimize` stay commented because they are not -/// yet consumed (writing them would imply they work). `[verification] -/// output-dir` stays commented because its default (`proofs/`) lives in code -/// and is honored only in proof mode. +/// yet consumed (writing them would imply they work). `[build.wasm-opt]` stays +/// commented because it is opt-in and requires an external `wasm-opt` binary +/// the project may not have installed. `[verification] output-dir` stays +/// commented because its default (`proofs/`) lives in code and is honored only +/// in proof mode. fn manifest_content(project_name: &str) -> String { let infc_version = detect_infc_version(); format!( @@ -235,6 +237,14 @@ mode = "compile" # target = "wasm32" # optimize = "release" +# [build.wasm-opt] +# Post-build optimization of the executable via Binaryen wasm-opt. Uncomment to +# enable; `auto-install` downloads the `wasm-opt` binary on first use when it is +# not already present (equivalent to `infs component add wasm-opt`). +# enabled = true +# level = "3" +# auto-install = true + # [verification] # Output directory for proof artifacts (honored only in proof mode). # Defaults to "proofs/". diff --git a/apps/infs/src/toolchain/archive.rs b/apps/infs/src/toolchain/archive.rs index b1bcc5f2..cf65ab1d 100644 --- a/apps/infs/src/toolchain/archive.rs +++ b/apps/infs/src/toolchain/archive.rs @@ -405,6 +405,36 @@ pub fn set_executable_permissions(_dir: &Path) -> Result<()> { Ok(()) } +/// Marks a single file as executable (`0o755`) on Unix; a no-op on Windows, +/// where executability is determined by extension rather than a permission bit. +/// +/// Unlike [`set_executable_permissions`], which targets the `infc` binary at a +/// toolchain root by name, this operates on an arbitrary path — used when +/// staging individual managed-tool files whose names are not known in advance. +/// +/// # Errors +/// +/// Returns an error if file metadata cannot be read or permissions cannot be set. +#[cfg(unix)] +pub fn set_executable_file(path: &Path) -> Result<()> { + use std::os::unix::fs::PermissionsExt; + + let mut perms = std::fs::metadata(path) + .with_context(|| format!("Failed to get metadata: {}", path.display()))? + .permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(path, perms) + .with_context(|| format!("Failed to set permissions: {}", path.display()))?; + Ok(()) +} + +/// Marks a file as executable (no-op on Windows). +#[cfg(windows)] +#[allow(clippy::unnecessary_wraps)] +pub fn set_executable_file(_path: &Path) -> Result<()> { + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -986,6 +1016,39 @@ mod tests { let _ = std::fs::remove_dir_all(&temp_dir); } + + #[test] + fn set_executable_file_sets_755_on_named_file() { + let temp_dir = temp_test_dir("exec_file_755"); + let file = temp_dir.join("wasm-opt"); + std::fs::write(&file, b"binary").expect("Should write file"); + + std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o644)) + .expect("Should set initial perms"); + + set_executable_file(&file).expect("Should set permissions"); + + let mode = std::fs::metadata(&file) + .expect("Should get metadata") + .permissions() + .mode(); + assert_eq!(mode & 0o777, 0o755, "file should have 0o755 mode"); + + let _ = std::fs::remove_dir_all(&temp_dir); + } + + #[test] + fn set_executable_file_errors_on_missing_path() { + let temp_dir = temp_test_dir("exec_file_missing"); + let missing = temp_dir.join("nope"); + + assert!( + set_executable_file(&missing).is_err(), + "a missing path must surface as an error" + ); + + let _ = std::fs::remove_dir_all(&temp_dir); + } } #[test] diff --git a/apps/infs/src/toolchain/binaryen.rs b/apps/infs/src/toolchain/binaryen.rs new file mode 100644 index 00000000..538bb1f5 --- /dev/null +++ b/apps/infs/src/toolchain/binaryen.rs @@ -0,0 +1,561 @@ +//! Provisioning for Binaryen's `wasm-opt`, the optimizer the `[build.wasm-opt]` +//! manifest table drives. +//! +//! `infs` installs a **pinned, sha256-verified** Binaryen release into +//! `~/.inference/tools/binaryen//`, so projects that opt into +//! optimization work out of the box and teams get version-consistent artifacts +//! (`wasm-opt` output is deterministic only per Binaryen version). This is a +//! distinct install tier from `toolchains/`: the infc-specific +//! `MANAGED_BINARY`/symlink machinery is untouched, and resolution precedence +//! lives in [`crate::commands::wasm_opt`]. +//! +//! ## Platform layout +//! +//! Binaryen ships one `.tar.gz` per platform with a versioned root directory +//! (`binaryen-/`). On Linux and Windows `wasm-opt` is statically +//! linked, so only `bin/wasm-opt[.exe]` is installed. On macOS it is dynamically +//! linked against `@rpath/libbinaryen.dylib` with an rpath of +//! `@loader_path/../lib`, so `lib/libbinaryen.dylib` must be installed as a +//! sibling of `bin/` or the binary will not launch. [`required_files`] encodes +//! this per platform. +//! +//! ## Bumping the pin +//! +//! 1. Change [`BINARYEN_PIN`] to the new release tag. +//! 2. Refresh the three [`pinned_sha256`] constants. Download each asset and run +//! `shasum -a 256`, cross-checking against the release's `.sha256` sidecars. +//! (The sidecars are a pin-time cross-check only; they are never trusted at +//! install time.) +//! 3. Confirm `MIN_WASM_OPT_VERSION` in [`crate::commands::wasm_opt`] is still +//! less than or equal to the new pin. +//! 4. Add a CHANGELOG entry. +//! +//! Quarantine note: `reqwest` downloads do not set the macOS +//! `com.apple.quarantine` attribute, so the extracted `wasm-opt` runs without a +//! Gatekeeper prompt. + +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result, bail}; + +use crate::toolchain::archive::set_executable_file; +use crate::toolchain::{Platform, ToolchainPaths, download_file, extract_archive, verify_checksum}; + +/// The pinned Binaryen release tag. +pub const BINARYEN_PIN: &str = "version_130"; + +/// The user-facing name of the component this module provisions. +pub const COMPONENT_NAME: &str = "wasm-opt"; + +/// Base URL for release downloads. Overridable via [`BINARYEN_BASE_URL_ENV`]. +const DEFAULT_BASE_URL: &str = "https://github.com/WebAssembly/binaryen/releases/download"; + +/// Environment variable overriding the download base URL, for mirrors and as the +/// hermetic test seam. +pub const BINARYEN_BASE_URL_ENV: &str = "INFS_BINARYEN_BASE_URL"; + +/// Environment variable overriding the expected checksum. Honored **only** when +/// [`BINARYEN_BASE_URL_ENV`] is also set — see [`sha_seam`]. +const BINARYEN_SHA256_OVERRIDE_ENV: &str = "INFS_BINARYEN_SHA256"; + +/// Pinned checksum of `binaryen-version_130-x86_64-linux.tar.gz`. +const SHA256_LINUX_X64: &str = "0a18362361ad05465118cd8eeb72edaeec89de6894bc283576ef4e07aa3babcc"; + +/// Pinned checksum of `binaryen-version_130-arm64-macos.tar.gz`. +const SHA256_MACOS_ARM64: &str = "79d3ab9f417d9e215f15f598f523d001a7d9ac1e59367e5c869fbdabd1cba72e"; + +/// Pinned checksum of `binaryen-version_130-x86_64-windows.tar.gz`. +const SHA256_WINDOWS_X64: &str = "cc09c874f4332d00aa32ab72745a9b98c9a172f795762f21d03e70638a3f7f4c"; + +/// The install status of a managed component. +#[derive(Debug, Clone)] +pub struct ComponentStatus { + /// The component's user-facing name (e.g. `wasm-opt`). + pub name: &'static str, + /// Whether the component is installed and usable. + pub installed: bool, + /// The pinned version the component would install/report. + pub version: &'static str, +} + +/// The Binaryen release-asset target triple for `platform`. +fn release_target(platform: Platform) -> &'static str { + match platform { + Platform::LinuxX64 => "x86_64-linux", + Platform::MacosArm64 => "arm64-macos", + Platform::WindowsX64 => "x86_64-windows", + } +} + +/// The release asset file name for `platform` at the pinned version. +#[must_use = "returns the asset name without side effects"] +pub fn asset_name(platform: Platform) -> String { + format!( + "binaryen-{BINARYEN_PIN}-{}.tar.gz", + release_target(platform) + ) +} + +/// The full download URL for `platform`, joining `base`, `version`, and the +/// asset name. A trailing slash on `base` is trimmed so the join never doubles. +#[must_use = "returns the URL without side effects"] +pub fn download_url(base: &str, version: &str, platform: Platform) -> String { + format!( + "{}/{version}/{}", + base.trim_end_matches('/'), + asset_name(platform) + ) +} + +/// The pinned sha256 of `platform`'s release asset. +#[must_use = "returns the checksum without side effects"] +pub fn pinned_sha256(platform: Platform) -> &'static str { + match platform { + Platform::LinuxX64 => SHA256_LINUX_X64, + Platform::MacosArm64 => SHA256_MACOS_ARM64, + Platform::WindowsX64 => SHA256_WINDOWS_X64, + } +} + +/// The archive-relative files that must be installed for `platform`, in +/// POSIX-separated form (the archive's own layout). macOS additionally requires +/// the `libbinaryen.dylib` sibling `wasm-opt` links against at runtime. +fn required_files(platform: Platform) -> &'static [&'static str] { + match platform { + Platform::LinuxX64 => &["bin/wasm-opt"], + Platform::MacosArm64 => &["bin/wasm-opt", "lib/libbinaryen.dylib"], + Platform::WindowsX64 => &["bin/wasm-opt.exe"], + } +} + +/// The download base URL: the [`BINARYEN_BASE_URL_ENV`] override if set, +/// otherwise [`DEFAULT_BASE_URL`]. +fn base_url() -> String { + std::env::var(BINARYEN_BASE_URL_ENV).unwrap_or_else(|_| DEFAULT_BASE_URL.to_string()) +} + +/// The checksum to verify a download against, applying the test/mirror seam. +fn expected_sha256(platform: Platform) -> String { + sha_seam( + pinned_sha256(platform), + std::env::var_os(BINARYEN_BASE_URL_ENV).is_some(), + std::env::var(BINARYEN_SHA256_OVERRIDE_ENV).ok(), + ) +} + +/// Pure core of the checksum seam: the override replaces the pinned value only +/// when the base URL was *also* overridden. With the production base URL +/// (`base_overridden == false`) the pin is returned unconditionally — no +/// environment variable can weaken the checksum of an official download. This +/// is a security property, so the two conditions are deliberately coupled. +fn sha_seam(pinned: &str, base_overridden: bool, override_sha: Option) -> String { + match override_sha { + Some(sha) if base_overridden => sha, + _ => pinned.to_string(), + } +} + +/// The `wasm-opt` executable path relative to a Binaryen install root. Uses the +/// host executable suffix because a managed install always matches the host. +fn wasm_opt_relative_path() -> PathBuf { + Path::new("bin").join(format!("wasm-opt{}", std::env::consts::EXE_SUFFIX)) +} + +/// Whether `dir` (a Binaryen install root) contains a `wasm-opt` executable. +fn dir_has_wasm_opt(dir: &Path) -> bool { + dir.join(wasm_opt_relative_path()).is_file() +} + +/// Converts an archive-relative POSIX path from [`required_files`] into a +/// platform path by joining its `/`-separated segments. +fn rel_to_path(rel: &str) -> PathBuf { + rel.split('/').collect() +} + +/// The path to the managed `wasm-opt`, or `None` if it is not installed. +#[must_use = "returns the path without side effects"] +pub fn installed_wasm_opt(paths: &ToolchainPaths) -> Option { + let candidate = paths + .binaryen_dir(BINARYEN_PIN) + .join(wasm_opt_relative_path()); + candidate.is_file().then_some(candidate) +} + +/// Reports whether the managed `wasm-opt` is installed. +#[must_use = "returns status without side effects"] +pub fn status(paths: &ToolchainPaths) -> ComponentStatus { + ComponentStatus { + name: COMPONENT_NAME, + installed: installed_wasm_opt(paths).is_some(), + version: BINARYEN_PIN, + } +} + +/// Downloads, verifies, and installs the pinned Binaryen `wasm-opt` into +/// `binaryen_dir(BINARYEN_PIN)`, returning the path to the installed binary. +/// +/// The operation is idempotent and atomic. An existing install short-circuits +/// with no network access. Otherwise the download is verified before anything +/// reaches `tools/`, staged under a per-process temp directory, and published +/// with a single [`std::fs::rename`] — so a failure at any step leaves no +/// partial install at the pinned path, and a pre-existing *broken* install +/// (a directory without the binary) is repaired. +/// +/// # Errors +/// +/// Returns an error if the download fails, the checksum does not match, the +/// archive is missing an expected file, or the install cannot be published. +pub async fn install(paths: &ToolchainPaths, platform: Platform) -> Result { + // Idempotent: an existing install means no network access at all. + if let Some(existing) = installed_wasm_opt(paths) { + println!( + "Component '{COMPONENT_NAME}' (Binaryen {BINARYEN_PIN}) is already installed at {}.", + existing.display() + ); + return Ok(existing); + } + + let dest_dir = paths.binaryen_dir(BINARYEN_PIN); + let binaryen_root = dest_dir + .parent() + .expect("binaryen_dir always has a parent") + .to_path_buf(); + std::fs::create_dir_all(&binaryen_root) + .with_context(|| format!("Failed to create {}", binaryen_root.display()))?; + + // Sweep temp dirs left behind by interrupted prior installs. + sweep_stale_temp_dirs(&binaryen_root); + + let asset = asset_name(platform); + let url = download_url(&base_url(), BINARYEN_PIN, platform); + let archive_path = paths.download_path(&asset); + + println!("Downloading {url}..."); + download_file(&url, &archive_path).await?; + + // Verify before anything reaches tools/. A mismatch deletes the archive so + // nothing is left to be mistaken for a good download. + println!("Verifying checksum..."); + if let Err(err) = verify_checksum(&archive_path, &expected_sha256(platform)) { + std::fs::remove_file(&archive_path).ok(); + return Err(err); + } + + // Everything below writes only under the per-process temp dir until the + // final atomic rename. + let temp_dir = binaryen_root.join(format!(".tmp-{}", std::process::id())); + let result = stage_and_publish(&archive_path, &temp_dir, &dest_dir, platform); + + // Always clean the temp dir and the downloaded archive. + std::fs::remove_dir_all(&temp_dir).ok(); + std::fs::remove_file(&archive_path).ok(); + result?; + + let installed = installed_wasm_opt(paths).with_context(|| { + format!( + "wasm-opt still not present at {} after install", + dest_dir.display() + ) + })?; + println!( + "Installed {COMPONENT_NAME} (Binaryen {BINARYEN_PIN}) at {}.", + installed.display() + ); + Ok(installed) +} + +/// Extracts the archive, copies the platform's [`required_files`] into a staging +/// directory, and atomically publishes it at `dest_dir`. +fn stage_and_publish( + archive_path: &Path, + temp_dir: &Path, + dest_dir: &Path, + platform: Platform, +) -> Result<()> { + // Start from a clean temp dir (a same-pid crash could have left one). + std::fs::remove_dir_all(temp_dir).ok(); + let extract_dir = temp_dir.join("extract"); + let install_dir = temp_dir.join("install"); + std::fs::create_dir_all(&install_dir) + .with_context(|| format!("Failed to create {}", install_dir.display()))?; + + println!("Extracting..."); + extract_archive(archive_path, &extract_dir)?; + + for rel in required_files(platform) { + let src = extract_dir.join(rel_to_path(rel)); + if !src.is_file() { + bail!( + "Binaryen archive is missing the expected file `{rel}`. The \ + release layout for {platform} may have changed; the pin \ + `{BINARYEN_PIN}` may need updating." + ); + } + let dst = install_dir.join(rel_to_path(rel)); + if let Some(parent) = dst.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("Failed to create {}", parent.display()))?; + } + std::fs::copy(&src, &dst) + .with_context(|| format!("Failed to copy {} to {}", src.display(), dst.display()))?; + // Harmless on the dylib (dylibs are conventionally 0o755) and required + // for the binary. + set_executable_file(&dst)?; + } + + publish(&install_dir, dest_dir) +} + +/// Atomically moves the staged `install_dir` to `dest_dir`. +/// +/// A `dest_dir` that already holds a valid binary is a concurrent installer that +/// won the race (or an install that appeared after our idempotent check) — it is +/// adopted rather than clobbered. A `dest_dir` *without* the binary is a broken +/// leftover, removed so the fresh install repairs it. If the rename still races +/// a concurrent winner, the loser re-checks and succeeds. +fn publish(install_dir: &Path, dest_dir: &Path) -> Result<()> { + if dir_has_wasm_opt(dest_dir) { + return Ok(()); + } + if dest_dir.exists() { + std::fs::remove_dir_all(dest_dir).with_context(|| { + format!("Failed to remove broken install at {}", dest_dir.display()) + })?; + } + if let Err(err) = std::fs::rename(install_dir, dest_dir) { + if dir_has_wasm_opt(dest_dir) { + return Ok(()); + } + return Err(anyhow::Error::from(err).context(format!( + "Failed to publish {COMPONENT_NAME} install to {}", + dest_dir.display() + ))); + } + Ok(()) +} + +/// Removes temp directories left behind by interrupted installs. +fn sweep_stale_temp_dirs(binaryen_root: &Path) { + let Ok(entries) = std::fs::read_dir(binaryen_root) else { + return; + }; + for entry in entries.flatten() { + if entry.file_name().to_string_lossy().starts_with(".tmp-") { + std::fs::remove_dir_all(entry.path()).ok(); + } + } +} + +/// Synchronous bridge to [`install`] for the build-time auto-install path, which +/// runs in a synchronous context. Reuses the ambient multi-threaded Tokio +/// runtime via [`tokio::task::block_in_place`] when one is present, and falls +/// back to a fresh runtime otherwise. +/// +/// # Errors +/// +/// Propagates every error from [`install`], plus a runtime-creation failure on +/// the fallback path. +pub fn install_blocking(paths: &ToolchainPaths, platform: Platform) -> Result { + if let Ok(handle) = tokio::runtime::Handle::try_current() { + tokio::task::block_in_place(|| handle.block_on(install(paths, platform))) + } else { + let runtime = tokio::runtime::Runtime::new() + .context("Failed to create a Tokio runtime for the wasm-opt download")?; + runtime.block_on(install(paths, platform)) + } +} + +/// Removes the managed Binaryen install. +/// +/// # Errors +/// +/// Bails when nothing is installed (mirroring `infs uninstall`), or if the +/// install directory cannot be removed. +pub fn remove(paths: &ToolchainPaths) -> Result<()> { + let dir = paths.binaryen_dir(BINARYEN_PIN); + if !dir.exists() { + bail!("Component '{COMPONENT_NAME}' (Binaryen {BINARYEN_PIN}) is not installed."); + } + std::fs::remove_dir_all(&dir).with_context(|| format!("Failed to remove {}", dir.display()))?; + // Best-effort: drop the now-empty binaryen parent directory. + if let Some(parent) = dir.parent() { + std::fs::remove_dir(parent).ok(); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn unique_root(tag: &str) -> PathBuf { + std::env::temp_dir().join(format!("infs_binaryen_{tag}_{}", fastrand::u64(..))) + } + + #[test] + fn asset_name_matches_release_layout() { + assert_eq!( + asset_name(Platform::LinuxX64), + "binaryen-version_130-x86_64-linux.tar.gz" + ); + assert_eq!( + asset_name(Platform::MacosArm64), + "binaryen-version_130-arm64-macos.tar.gz" + ); + assert_eq!( + asset_name(Platform::WindowsX64), + "binaryen-version_130-x86_64-windows.tar.gz" + ); + } + + #[test] + fn download_url_joins_base_version_and_asset() { + assert_eq!( + download_url("https://example.com/dl", BINARYEN_PIN, Platform::LinuxX64), + "https://example.com/dl/version_130/binaryen-version_130-x86_64-linux.tar.gz" + ); + } + + #[test] + fn download_url_trims_trailing_slash_on_base() { + assert_eq!( + download_url( + "https://example.com/dl/", + BINARYEN_PIN, + Platform::MacosArm64 + ), + "https://example.com/dl/version_130/binaryen-version_130-arm64-macos.tar.gz" + ); + } + + #[test] + fn pinned_sha256_values_are_lowercase_64_hex() { + for platform in [ + Platform::LinuxX64, + Platform::MacosArm64, + Platform::WindowsX64, + ] { + let sha = pinned_sha256(platform); + assert_eq!(sha.len(), 64, "sha for {platform} must be 64 chars"); + assert!( + sha.chars() + .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()), + "sha for {platform} must be lowercase hex" + ); + } + } + + #[test] + fn required_files_shapes_per_platform() { + assert_eq!(required_files(Platform::LinuxX64), &["bin/wasm-opt"]); + assert_eq!(required_files(Platform::WindowsX64), &["bin/wasm-opt.exe"]); + let macos = required_files(Platform::MacosArm64); + assert!(macos.contains(&"bin/wasm-opt")); + assert!( + macos.contains(&"lib/libbinaryen.dylib"), + "macOS layout must include the dylib sibling" + ); + } + + #[test] + fn rel_to_path_joins_posix_segments() { + assert_eq!( + rel_to_path("bin/wasm-opt"), + Path::new("bin").join("wasm-opt") + ); + } + + #[test] + fn sha_seam_ignores_override_without_base_override() { + // Security property: the pin cannot be weakened by the sha override alone. + let pinned = pinned_sha256(Platform::LinuxX64); + assert_eq!( + sha_seam(pinned, false, Some("deadbeef".to_string())), + pinned + ); + } + + #[test] + fn sha_seam_honors_override_only_with_base_override() { + assert_eq!( + sha_seam("pin", true, Some("deadbeef".to_string())), + "deadbeef" + ); + } + + #[test] + fn sha_seam_uses_pin_when_no_override_present() { + assert_eq!(sha_seam("pin", true, None), "pin"); + } + + #[test] + fn installed_wasm_opt_finds_binary_under_root() { + let root = unique_root("installed"); + let paths = ToolchainPaths::with_root(root.clone()); + assert!( + installed_wasm_opt(&paths).is_none(), + "absent binary must yield None" + ); + + let bin_dir = paths.binaryen_dir(BINARYEN_PIN).join("bin"); + std::fs::create_dir_all(&bin_dir).unwrap(); + let binary = bin_dir.join(format!("wasm-opt{}", std::env::consts::EXE_SUFFIX)); + std::fs::write(&binary, b"fake").unwrap(); + + assert_eq!(installed_wasm_opt(&paths), Some(binary)); + + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn status_reflects_install_state() { + let root = unique_root("status"); + let paths = ToolchainPaths::with_root(root.clone()); + + let absent = status(&paths); + assert_eq!(absent.name, "wasm-opt"); + assert_eq!(absent.version, BINARYEN_PIN); + assert!(!absent.installed); + + let bin_dir = paths.binaryen_dir(BINARYEN_PIN).join("bin"); + std::fs::create_dir_all(&bin_dir).unwrap(); + std::fs::write( + bin_dir.join(format!("wasm-opt{}", std::env::consts::EXE_SUFFIX)), + b"fake", + ) + .unwrap(); + assert!(status(&paths).installed); + + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn remove_bails_when_absent() { + let root = unique_root("remove_absent"); + let paths = ToolchainPaths::with_root(root.clone()); + let err = remove(&paths).unwrap_err(); + assert!( + err.to_string().contains("not installed"), + "removing an absent component must bail, got: {err}" + ); + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn remove_deletes_installed_component() { + let root = unique_root("remove_present"); + let paths = ToolchainPaths::with_root(root.clone()); + let bin_dir = paths.binaryen_dir(BINARYEN_PIN).join("bin"); + std::fs::create_dir_all(&bin_dir).unwrap(); + std::fs::write( + bin_dir.join(format!("wasm-opt{}", std::env::consts::EXE_SUFFIX)), + b"fake", + ) + .unwrap(); + + remove(&paths).unwrap(); + assert!(!paths.binaryen_dir(BINARYEN_PIN).exists()); + + std::fs::remove_dir_all(&root).ok(); + } +} diff --git a/apps/infs/src/toolchain/doctor.rs b/apps/infs/src/toolchain/doctor.rs index a844e4cd..ef35de7c 100644 --- a/apps/infs/src/toolchain/doctor.rs +++ b/apps/infs/src/toolchain/doctor.rs @@ -116,6 +116,7 @@ pub fn run_all_checks() -> Vec { if let Some(ambiguity) = check_resolution_ambiguity() { checks.push(ambiguity); } + checks.push(crate::commands::wasm_opt::doctor_check()); checks } @@ -340,9 +341,9 @@ mod tests { fn run_all_checks_returns_expected_count() { let checks = run_all_checks(); // Base checks: infs, platform, toolchain dir, default toolchain, - // infc, resolved infc. Ambiguity check is conditional (0 or 1). + // infc, resolved infc, wasm-opt. Ambiguity check is conditional (0 or 1). assert!( - checks.len() == 6 || checks.len() == 7, + checks.len() == 7 || checks.len() == 8, "unexpected check count: {}", checks.len() ); diff --git a/apps/infs/src/toolchain/mod.rs b/apps/infs/src/toolchain/mod.rs index 6bed9f91..dec322fb 100644 --- a/apps/infs/src/toolchain/mod.rs +++ b/apps/infs/src/toolchain/mod.rs @@ -11,10 +11,12 @@ //! - [`download`] - HTTP download with progress tracking //! - [`verify`] - SHA256 checksum verification //! - [`archive`] - ZIP and tar.gz archive extraction utilities +//! - [`binaryen`] - Pinned Binaryen (`wasm-opt`) provisioning //! - [`doctor`] - Toolchain health checks //! - [`conflict`] - PATH conflict detection pub mod archive; +pub mod binaryen; pub mod conflict; pub mod doctor; pub mod download; diff --git a/apps/infs/src/toolchain/paths.rs b/apps/infs/src/toolchain/paths.rs index 4cf15912..dd37e51f 100644 --- a/apps/infs/src/toolchain/paths.rs +++ b/apps/infs/src/toolchain/paths.rs @@ -327,6 +327,23 @@ impl ToolchainPaths { self.toolchains.join(version) } + /// Returns the directory holding managed developer tools (`/tools`). + /// + /// Managed components such as Binaryen's `wasm-opt` live here, a tier + /// distinct from `toolchains/`: they are not versioned compilers and are + /// resolved independently of the `infc` toolchain. + #[must_use = "returns the path without side effects"] + pub fn tools_dir(&self) -> PathBuf { + self.root.join("tools") + } + + /// Returns the install directory for a specific Binaryen version + /// (`/tools/binaryen/`). + #[must_use = "returns the path without side effects"] + pub fn binaryen_dir(&self, version: &str) -> PathBuf { + self.tools_dir().join("binaryen").join(version) + } + /// Returns the path to the file storing the default toolchain version. #[must_use = "returns the path without side effects"] pub fn default_file(&self) -> PathBuf { @@ -741,6 +758,25 @@ mod tests { assert_eq!(paths.default_file(), temp_dir.join("default")); } + #[test] + fn tools_dir_constructs_correct_path() { + let temp_dir = env::temp_dir().join("infs_test_tools_dir"); + let paths = ToolchainPaths::with_root(temp_dir.clone()); + + assert_eq!(paths.tools_dir(), temp_dir.join("tools")); + } + + #[test] + fn binaryen_dir_constructs_correct_path() { + let temp_dir = env::temp_dir().join("infs_test_binaryen_dir"); + let paths = ToolchainPaths::with_root(temp_dir.clone()); + + assert_eq!( + paths.binaryen_dir("version_130"), + temp_dir.join("tools").join("binaryen").join("version_130") + ); + } + #[test] fn download_path_constructs_correctly() { let temp_dir = env::temp_dir().join("infs_test_download"); diff --git a/apps/infs/tests/cli_integration.rs b/apps/infs/tests/cli_integration.rs index d933659f..5e4a1daf 100644 --- a/apps/infs/tests/cli_integration.rs +++ b/apps/infs/tests/cli_integration.rs @@ -438,11 +438,13 @@ const PROJECT_MAIN_SRC: &str = "pub fn main() -> i32 {\n return 0;\n}\n"; /// `src/main.inf` with the given source. Returns nothing; `dir` is mutated in /// place. Paths are built with `join` so they are platform-correct. fn scaffold_project(dir: &assert_fs::TempDir, name: &str, main_src: &str) { - let manifest = format!( - "[package]\nname = \"{name}\"\nversion = \"0.1.0\"\ninfc_version = \"0.1.0\"\n" - ); + let manifest = + format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\ninfc_version = \"0.1.0\"\n"); dir.child("Inference.toml").write_str(&manifest).unwrap(); - dir.child("src").child("main.inf").write_str(main_src).unwrap(); + dir.child("src") + .child("main.inf") + .write_str(main_src) + .unwrap(); } /// Project mode: `infs build` (no path) invoked from the project root discovers @@ -535,9 +537,9 @@ fn project_build_missing_entry_point_errors() { let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); cmd.current_dir(temp.path()).arg("build"); - cmd.assert().failure().stderr( - predicate::str::contains("entry point").and(predicate::str::contains("main.inf")), - ); + cmd.assert() + .failure() + .stderr(predicate::str::contains("entry point").and(predicate::str::contains("main.inf"))); } // Project-mode Multi-file Build Tests (#63) @@ -631,7 +633,9 @@ fn project_build_unreachable_file_warns_without_stale_text() { cmd.assert() .success() .stderr(predicate::str::contains("orphan.inf")) - .stderr(predicate::str::contains("not imported by any reachable file")) + .stderr(predicate::str::contains( + "not imported by any reachable file", + )) .stderr(predicate::str::contains(STALE_PENDING_WARNING_FRAGMENT).not()); let wasm = temp.child("out").child("main.wasm"); @@ -785,7 +789,10 @@ fn project_build_wasm_byte_identical_to_infc() { let project_wasm = temp_project.child("out").child("main.wasm"); let ref_wasm = temp_ref.child("out").child("main.wasm"); - assert!(project_wasm.path().exists(), "project build produced no WASM"); + assert!( + project_wasm.path().exists(), + "project build produced no WASM" + ); assert!(ref_wasm.path().exists(), "reference infc produced no WASM"); let project_bytes = std::fs::read(project_wasm.path()).unwrap(); @@ -932,9 +939,9 @@ fn project_run_missing_entry_point_errors() { let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); cmd.current_dir(temp.path()).arg("run"); - cmd.assert().failure().stderr( - predicate::str::contains("entry point").and(predicate::str::contains("main.inf")), - ); + cmd.assert() + .failure() + .stderr(predicate::str::contains("entry point").and(predicate::str::contains("main.inf"))); } /// `--entry-point` with a non-`main` value in project mode is rejected with @@ -1043,7 +1050,10 @@ fn scaffold_project_with_manifest( "[package]\nname = \"{name}\"\nversion = \"0.1.0\"\ninfc_version = \"0.1.0\"\n\n{manifest_extra}" ); dir.child("Inference.toml").write_str(&manifest).unwrap(); - dir.child("src").child("main.inf").write_str(main_src).unwrap(); + dir.child("src") + .child("main.inf") + .write_str(main_src) + .unwrap(); } /// Default-manifest (compile) build writes `/out/main.wasm` and creates @@ -1088,7 +1098,12 @@ fn project_build_manifest_proof_writes_under_proofs() { }; let temp = assert_fs::TempDir::new().unwrap(); - scaffold_project_with_manifest(&temp, "demo", PROJECT_MAIN_SRC, "[build]\nmode = \"proof\"\n"); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build]\nmode = \"proof\"\n", + ); let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); cmd.env("INFC_PATH", &infc_path) @@ -1252,7 +1267,12 @@ fn project_run_forces_compile_ignoring_manifest_proof() { }; let temp = assert_fs::TempDir::new().unwrap(); - scaffold_project_with_manifest(&temp, "demo", PROJECT_MAIN_SRC, "[build]\nmode = \"proof\"\n"); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build]\nmode = \"proof\"\n", + ); let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); cmd.env("INFC_PATH", &infc_path) @@ -1293,7 +1313,10 @@ fn scaffolded_project_manifest_has_compile_mode() { let project = temp.child("demo"); let manifest_path = project.child("Inference.toml"); - assert!(manifest_path.path().exists(), "new must scaffold a manifest"); + assert!( + manifest_path.path().exists(), + "new must scaffold a manifest" + ); let content = std::fs::read_to_string(manifest_path.path()).unwrap(); assert!( @@ -1324,7 +1347,12 @@ fn project_build_old_infc_with_output_dir_hard_errors() { use std::os::unix::fs::PermissionsExt; let temp = assert_fs::TempDir::new().unwrap(); - scaffold_project_with_manifest(&temp, "demo", PROJECT_MAIN_SRC, "[build]\nmode = \"proof\"\n"); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build]\nmode = \"proof\"\n", + ); // Stub infc: reports a non-matching commit and ABI "1.0" (minor 0 → no // --out-dir support), exits 0 for the probes. @@ -1656,8 +1684,7 @@ fn doctor_shows_all_checks() { /// `INFC_PATH` removed so resolver priorities behave deterministically. #[test] fn doctor_output_respects_vscode_check_line_contract() { - let check_pattern = - regex::Regex::new(r"^\s+\[(OK|WARN|FAIL)]\s+(.+?):\s+(.*)").unwrap(); + let check_pattern = regex::Regex::new(r"^\s+\[(OK|WARN|FAIL)]\s+(.+?):\s+(.*)").unwrap(); let temp = assert_fs::TempDir::new().unwrap(); let output = Command::new(assert_cmd::cargo::cargo_bin!("infs")) @@ -1705,8 +1732,7 @@ fn doctor_output_respects_vscode_check_line_contract() { fn doctor_output_respects_vscode_contract_on_path_conflict() { use std::os::unix::fs::PermissionsExt; - let check_pattern = - regex::Regex::new(r"^\s+\[(OK|WARN|FAIL)]\s+(.+?):\s+(.*)").unwrap(); + let check_pattern = regex::Regex::new(r"^\s+\[(OK|WARN|FAIL)]\s+(.+?):\s+(.*)").unwrap(); // Build a managed toolchain layout: INFERENCE_HOME/bin/infc must exist // so `detect_path_conflicts` considers the expected location "real". @@ -1744,9 +1770,7 @@ fn doctor_output_respects_vscode_contract_on_path_conflict() { .collect(); assert!( - check_lines - .iter() - .any(|l| l.contains("PATH conflict:")), + check_lines.iter().any(|l| l.contains("PATH conflict:")), "expected a `[WARN] PATH conflict: …` line. Output was:\n{stdout}" ); @@ -2828,3 +2852,1550 @@ fn build_handles_nested_paths() { cmd.assert().success(); } + +// Project-mode wasm-opt Post-Build Optimization Tests +// +// These exercise the optional `[build.wasm-opt]` post-build step end-to-end +// through the `infs` binary. Almost all are hermetic: a dependency-free fake +// `wasm-opt` (tests/fixtures/fake_wasm_opt.rs) is compiled once per test process +// and injected per-child via `WASM_OPT_PATH`, so they run on machines without +// Binaryen installed and never mutate global process state (no `serial_test` +// needed). The handful of real-binary end-to-end tests skip-gate on +// `require_wasm_opt`. + +/// The invocation marker the fake `wasm-opt` writes before each logged call. +/// Kept in sync with `tests/fixtures/fake_wasm_opt.rs`. +const WASM_OPT_INVOCATION_MARKER: &str = "--- wasm-opt invocation ---"; + +/// `src/main.inf` that leaks a verification-only construct (a `forall` block +/// with an uzumaki `@`) into an ordinary function. Compile-mode codegen +/// preserves the `0xfc` opcode, so the post-build scan must reject it before +/// wasm-opt ever sees the artifact. +const PROJECT_MAIN_NONDET_SRC: &str = + "pub fn main() -> i32 {\n forall {\n let x: i32 = @;\n }\n return 0;\n}\n"; + +/// Compiles the dependency-free fake `wasm-opt` fixture once per test process +/// and returns the path to the built binary. +/// +/// The binary stands in for a real Binaryen `wasm-opt`, letting the hermetic +/// optimization tests run anywhere `rustc` is available (which is everywhere +/// `cargo test` runs). The temp directory holding it is kept alive for the life +/// of the process via `OnceLock`, so the returned path stays valid across every +/// test. The `.exe` suffix is applied on Windows so the produced binary is +/// spawnable. +fn fake_wasm_opt_binary() -> std::path::PathBuf { + static FAKE: std::sync::OnceLock<(assert_fs::TempDir, std::path::PathBuf)> = + std::sync::OnceLock::new(); + FAKE.get_or_init(|| { + let dir = assert_fs::TempDir::new().unwrap(); + let source = fixture_file("fake_wasm_opt.rs"); + let binary = dir + .path() + .join(format!("wasm-opt{}", std::env::consts::EXE_SUFFIX)); + let status = Command::new("rustc") + .arg("--edition") + .arg("2024") + .arg(&source) + .arg("-o") + .arg(&binary) + .status() + .expect("failed to spawn rustc to build the fake wasm-opt fixture"); + assert!( + status.success(), + "rustc failed to compile the fake wasm-opt fixture" + ); + (dir, binary) + }) + .1 + .clone() +} + +/// Skip-gate for the real-binary end-to-end tests: returns `true` only when a +/// genuine Binaryen `wasm-opt` is on PATH, mirroring `require_infc`'s +/// skip-with-notice pattern so the suite still passes without Binaryen. +fn require_wasm_opt() -> bool { + if which::which("wasm-opt").is_ok() { + true + } else { + eprintln!("Skipping test: wasm-opt (Binaryen) not found in PATH"); + false + } +} + +/// Parses the fake `wasm-opt` log into one argument vector per recorded +/// invocation. A missing log file — the optimizer was never spawned — yields an +/// empty list. The version probe is not logged, so a single optimization run +/// produces exactly one entry. +fn optimizer_invocations(log_path: &std::path::Path) -> Vec> { + let Ok(contents) = std::fs::read_to_string(log_path) else { + return Vec::new(); + }; + contents + .split(WASM_OPT_INVOCATION_MARKER) + .map(str::trim) + .filter(|chunk| !chunk.is_empty()) + .map(|chunk| chunk.lines().map(str::to_string).collect()) + .collect() +} + +/// Validates `bytes` against the same feature envelope the linker and the +/// optimizer's re-validation enforce (`GC_TYPES | MUTABLE_GLOBAL | +/// BULK_MEMORY`), using the workspace `inf-wasmparser` fork. +fn wasm_is_valid(bytes: &[u8]) -> bool { + let features = inf_wasmparser::WasmFeatures::GC_TYPES + .union(inf_wasmparser::WasmFeatures::MUTABLE_GLOBAL) + .union(inf_wasmparser::WasmFeatures::BULK_MEMORY); + inf_wasmparser::Validator::new_with_features(features) + .validate_all(bytes) + .is_ok() +} + +/// The file name of `raw`, as a `&str`, for asserting an argument path points at +/// the expected artifact regardless of the (absolute, platform-specific) parent. +fn file_name_of(raw: &str) -> Option<&str> { + std::path::Path::new(raw) + .file_name() + .and_then(|s| s.to_str()) +} + +/// With no `[build.wasm-opt]` table the optimizer is never spawned even when +/// `WASM_OPT_PATH` is set — the default pipeline is untouched — and the ordinary +/// `out/main.wasm` is still produced. Pins the default-off neutrality. +#[test] +fn wasm_opt_absent_table_never_invokes_optimizer() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project(&temp, "demo", PROJECT_MAIN_SRC); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("build"); + + cmd.assert().success(); + + assert!( + optimizer_invocations(log.path()).is_empty(), + "absent [build.wasm-opt] must never spawn wasm-opt" + ); + assert!( + temp.child("out").child("main.wasm").path().exists(), + "the ordinary artifact must still be produced" + ); +} + +/// An enabled `[build.wasm-opt] level = "z"` forwards exactly the expected +/// argument vector — the level flag, the three feature flags, the input, then +/// `-o` and the sibling temp target, in order — leaves the artifact in place, +/// and prints the one-line size summary. +#[test] +fn wasm_opt_enabled_forwards_exact_args_and_prints_summary() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("build"); + + cmd.assert() + .success() + .stdout(predicate::str::contains("wasm-opt -Oz: main.wasm")) + .stdout(predicate::str::contains(" -> ")) + .stdout(predicate::str::contains("bytes")); + + let invocations = optimizer_invocations(log.path()); + assert_eq!( + invocations.len(), + 1, + "wasm-opt must be spawned exactly once for optimization, got: {invocations:?}" + ); + let args = &invocations[0]; + assert_eq!(args.len(), 7, "unexpected argument count: {args:?}"); + assert_eq!(args[0], "-Oz"); + assert_eq!(args[1], "--mvp-features"); + assert_eq!(args[2], "--enable-mutable-globals"); + assert_eq!(args[3], "--enable-bulk-memory"); + assert_eq!( + file_name_of(&args[4]), + Some("main.wasm"), + "input must be out/main.wasm, got: {}", + args[4] + ); + assert_eq!(args[5], "-o"); + assert_eq!( + file_name_of(&args[6]), + Some("main.wasm.opt"), + "output must be the sibling temp file, got: {}", + args[6] + ); + + assert!( + temp.child("out").child("main.wasm").path().exists(), + "the optimized artifact must remain at out/main.wasm" + ); +} + +/// `[build.wasm-opt] enabled = false` keeps the optimizer off even though the +/// table is present. +#[test] +fn wasm_opt_enabled_false_skips_optimizer() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nenabled = false\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("build"); + + cmd.assert().success(); + + assert!( + optimizer_invocations(log.path()).is_empty(), + "`enabled = false` must not spawn wasm-opt" + ); + assert!(temp.child("out").child("main.wasm").path().exists()); +} + +/// `infs build --no-wasm-opt` suppresses the step even when `[build.wasm-opt]` +/// is enabled, leaving the artifact exactly as infc emitted it. +#[test] +fn wasm_opt_no_wasm_opt_flag_skips_optimizer_on_build() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("build") + .arg("--no-wasm-opt"); + + cmd.assert().success(); + + assert!( + optimizer_invocations(log.path()).is_empty(), + "--no-wasm-opt must not spawn wasm-opt" + ); + assert!(temp.child("out").child("main.wasm").path().exists()); +} + +/// A proof-mode manifest skips optimization silently: proof artifacts are a +/// different class (they carry the non-det opcodes wasm-opt cannot process), so +/// the optimizer is never spawned and the build still succeeds. +#[test] +fn wasm_opt_skipped_for_manifest_proof_mode() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build]\nmode = \"proof\"\n\n[build.wasm-opt]\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("build"); + + cmd.assert().success(); + + assert!( + optimizer_invocations(log.path()).is_empty(), + "a proof-mode manifest must skip wasm-opt" + ); +} + +/// `infs build --mode proof` on a compile-mode manifest that enables the +/// optimizer skips optimization: the explicitly-owned proof signal wins and the +/// artifact is left unoptimized. +#[test] +fn wasm_opt_skipped_for_cli_proof_mode() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("build") + .arg("--mode") + .arg("proof"); + + cmd.assert().success(); + + assert!( + optimizer_invocations(log.path()).is_empty(), + "`--mode proof` must skip wasm-opt" + ); +} + +/// A plain `-v` build is treated conservatively as a verification workflow and +/// skips optimization, even on a compile-mode manifest that enables it. Both +/// `out/main.wasm` and `out/main.v` are still produced. +#[test] +fn wasm_opt_skipped_for_v_flag() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("build") + .arg("-v"); + + cmd.assert().success(); + + assert!( + optimizer_invocations(log.path()).is_empty(), + "a -v build must skip wasm-opt" + ); + assert!( + temp.child("out").child("main.wasm").path().exists() + && temp.child("out").child("main.v").path().exists(), + "-v must still write both out/main.wasm and out/main.v" + ); +} + +/// A verification construct that leaks into an ordinary function is a hard error +/// when optimization is enabled: the pre-scan names the construct (`forall`) and +/// points at `spec` blocks, the optimizer is never spawned, and the unoptimized +/// `out/main.wasm` infc wrote is left in place. +#[test] +fn wasm_opt_rejects_leaked_verification_construct() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_NONDET_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("build"); + + cmd.assert().failure().stderr( + predicate::str::contains("verification-only construct `forall`") + .and(predicate::str::contains("spec")), + ); + + assert!( + optimizer_invocations(log.path()).is_empty(), + "the pre-scan must reject before wasm-opt is spawned" + ); + assert!( + temp.child("out").child("main.wasm").path().exists(), + "the unoptimized artifact must be left in place after the scan error" + ); +} + +/// A `WASM_OPT_PATH` that does not name a file is a hard error naming the +/// environment variable — an explicit override is never silently discarded in +/// favor of a PATH search. +#[test] +fn wasm_opt_missing_binary_via_env_names_env_var() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let missing = temp.child("no-such-wasm-opt"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", missing.path()) + .current_dir(temp.path()) + .arg("build"); + + cmd.assert() + .failure() + .stderr(predicate::str::contains("WASM_OPT_PATH")); +} + +/// With no `WASM_OPT_PATH` override, a PATH containing no `wasm-opt`, and no +/// managed install, the missing-binary error leads with the infs-managed option +/// (`infs component add wasm-opt`) and still carries the Binaryen package hints. +/// `INFERENCE_HOME` is isolated to an empty directory so a managed `wasm-opt` on +/// the developer's real machine cannot resolve and mask the error. Unix-only: an +/// empty PATH is the portable way to guarantee the lookup fails, and infc is +/// still reached through `INFC_PATH`. +#[cfg(unix)] +#[test] +fn wasm_opt_missing_binary_via_path_hints_install() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let home = assert_fs::TempDir::new().unwrap(); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("INFERENCE_HOME", home.path()) + .env("PATH", "") + .env_remove("WASM_OPT_PATH") + .current_dir(temp.path()) + .arg("build"); + + cmd.assert().failure().stderr( + predicate::str::contains("infs component add wasm-opt") + .and(predicate::str::contains("brew install binaryen")), + ); +} + +/// A nonzero `wasm-opt` exit is a hard error carrying its stderr, and the +/// original artifact is left intact — the optimizer writes to a sibling temp +/// file that is cleaned up on failure, so the in-place artifact never sees a +/// partial result. +#[test] +fn wasm_opt_nonzero_exit_fails_and_preserves_artifact() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_EXIT", "1") + .current_dir(temp.path()) + .arg("build"); + + cmd.assert() + .failure() + .stderr(predicate::str::contains("fake failure")); + + let artifact = temp.child("out").child("main.wasm"); + let bytes = std::fs::read(artifact.path()).expect("original artifact must remain readable"); + assert!( + !bytes.is_empty() && wasm_is_valid(&bytes), + "the unoptimized artifact must be left valid after a wasm-opt failure" + ); + assert!( + !temp.child("out").child("main.wasm.opt").path().exists(), + "the temp file must be cleaned up after a failure" + ); +} + +/// A `wasm-opt` older than the supported minimum is a hard error naming both the +/// found version and the required minimum. +#[test] +fn wasm_opt_version_too_old_errors() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_VERSION", "wasm-opt version 90 (version_90)") + .current_dir(temp.path()) + .arg("build"); + + cmd.assert() + .failure() + .stderr(predicate::str::contains("90").and(predicate::str::contains("116"))); +} + +/// An unparseable `--version` banner is a warning, not a blocker: the build +/// proceeds to optimization and succeeds. A possibly-fine binary must never be +/// rejected over an unrecognized version string. +#[test] +fn wasm_opt_unparseable_version_warns_and_succeeds() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_VERSION", "banana") + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("build"); + + cmd.assert().success().stderr(predicate::str::contains( + "could not parse a wasm-opt version", + )); + + assert_eq!( + optimizer_invocations(log.path()).len(), + 1, + "the build must proceed to optimization despite the unparseable version" + ); +} + +/// Optimized bytes that fail re-validation are a hard error; the original +/// `out/main.wasm` is left valid and the temp file is cleaned up. This guards +/// against a `wasm-opt` that emits something outside the executable subset. +#[test] +fn wasm_opt_garbage_output_fails_revalidation_and_preserves_artifact() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_GARBAGE", "1") + .current_dir(temp.path()) + .arg("build"); + + cmd.assert() + .failure() + .stderr(predicate::str::contains("failed re-validation")); + + let artifact = temp.child("out").child("main.wasm"); + let bytes = std::fs::read(artifact.path()).expect("original artifact must remain readable"); + assert!( + wasm_is_valid(&bytes), + "the original artifact must stay valid when the optimized output is rejected" + ); + assert!( + !temp.child("out").child("main.wasm.opt").path().exists(), + "the temp file must be cleaned up after a re-validation failure" + ); +} + +/// Project `run` applies the same optimization `build` does — it runs exactly +/// what it ships — so an enabled table spawns the optimizer, and `main`'s return +/// value (42) still surfaces. Gated on wasmtime. +#[test] +fn wasm_opt_run_enabled_invokes_optimizer_and_runs() { + let Some(infc_path) = require_infc_and_wasmtime() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_NONZERO_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("run"); + + cmd.assert() + .success() + .stdout(predicate::str::contains("42")); + + assert_eq!( + optimizer_invocations(log.path()).len(), + 1, + "project run must apply the optimization it ships" + ); +} + +/// `infs run --no-wasm-opt` executes the artifact exactly as infc emitted it: +/// the optimizer is skipped and `main` still runs. Gated on wasmtime. +#[test] +fn wasm_opt_run_no_wasm_opt_flag_skips_optimizer() { + let Some(infc_path) = require_infc_and_wasmtime() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_NONZERO_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("WASM_OPT_PATH", fake_wasm_opt_binary()) + .env("FAKE_WASM_OPT_LOG", log.path()) + .current_dir(temp.path()) + .arg("run") + .arg("--no-wasm-opt"); + + cmd.assert() + .success() + .stdout(predicate::str::contains("42")); + + assert!( + optimizer_invocations(log.path()).is_empty(), + "--no-wasm-opt must skip optimization on run too" + ); +} + +/// Real-binary end-to-end: an optimized artifact validates under the workspace +/// feature envelope and is no larger than the unoptimized one built from the +/// same source (`wasm-opt -Oz` at minimum drops the names section). Gated on a +/// real Binaryen `wasm-opt`. +#[test] +fn wasm_opt_real_binary_produces_valid_no_larger_artifact() { + let Some(infc_path) = require_infc() else { + return; + }; + if !require_wasm_opt() { + return; + } + + let optimized = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &optimized, + "demo", + PROJECT_MAIN_NONZERO_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let mut opt_cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + opt_cmd + .env("INFC_PATH", &infc_path) + .current_dir(optimized.path()) + .arg("build"); + opt_cmd.assert().success(); + + let unoptimized = assert_fs::TempDir::new().unwrap(); + scaffold_project(&unoptimized, "demo", PROJECT_MAIN_NONZERO_SRC); + let mut plain_cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + plain_cmd + .env("INFC_PATH", &infc_path) + .current_dir(unoptimized.path()) + .arg("build"); + plain_cmd.assert().success(); + + let optimized_bytes = std::fs::read(optimized.child("out").child("main.wasm").path()).unwrap(); + let unoptimized_bytes = + std::fs::read(unoptimized.child("out").child("main.wasm").path()).unwrap(); + + assert!( + wasm_is_valid(&optimized_bytes), + "the optimized artifact must validate under the workspace feature set" + ); + assert!( + optimized_bytes.len() <= unoptimized_bytes.len(), + "optimized ({}) must be no larger than unoptimized ({})", + optimized_bytes.len(), + unoptimized_bytes.len() + ); +} + +/// Real-binary end-to-end: running an optimized project yields the same +/// observable result (`main` returns 42) as running the unoptimized one — +/// optimization preserves behavior. Gated on both a real `wasm-opt` and +/// wasmtime. +#[test] +fn wasm_opt_real_binary_run_matches_unoptimized() { + let Some(infc_path) = require_infc_and_wasmtime() else { + return; + }; + if !require_wasm_opt() { + return; + } + + let optimized = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &optimized, + "demo", + PROJECT_MAIN_NONZERO_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let mut opt_run = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + opt_run + .env("INFC_PATH", &infc_path) + .current_dir(optimized.path()) + .arg("run"); + opt_run + .assert() + .success() + .stdout(predicate::str::contains("42")); + + let unoptimized = assert_fs::TempDir::new().unwrap(); + scaffold_project(&unoptimized, "demo", PROJECT_MAIN_NONZERO_SRC); + let mut plain_run = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + plain_run + .env("INFC_PATH", &infc_path) + .current_dir(unoptimized.path()) + .arg("run"); + plain_run + .assert() + .success() + .stdout(predicate::str::contains("42")); +} + +// `infs component` Tests +// +// These exercise the managed-component install path end to end without touching +// the network: a `TcpListener` stub serves an in-test Binaryen tarball, and each +// test isolates `INFERENCE_HOME` so nothing leaks into the real toolchain. The +// tarball's `bin/wasm-opt` payload reuses the compiled `fake_wasm_opt_binary()` +// fixture (a real, spawnable binary), so an install is byte-for-byte realistic. + +/// The pinned Binaryen version, mirrored from `toolchain::binaryen::BINARYEN_PIN` +/// (an integration test cannot import the `infs` binary crate). +const BINARYEN_PIN: &str = "version_130"; + +/// Spawns a throwaway HTTP/1.1 server on `127.0.0.1:0` that answers every request +/// with `body`, and returns its base URL (`http://127.0.0.1:`). +/// +/// The download client (`reqwest`) issues a single `GET`; the stub drains the +/// request headers before responding so the client never sees a reset, then +/// closes the connection (`Connection: close`). The accept loop lives in a +/// detached thread for the life of the test process. +fn spawn_binaryen_stub(body: Vec) -> String { + use std::io::{Read, Write}; + use std::net::TcpListener; + + let listener = TcpListener::bind("127.0.0.1:0").expect("bind stub listener"); + let base = format!("http://{}", listener.local_addr().expect("stub local addr")); + std::thread::spawn(move || { + for stream in listener.incoming() { + let Ok(mut stream) = stream else { + continue; + }; + let mut request = Vec::new(); + let mut byte = [0u8; 1]; + while let Ok(1) = stream.read(&mut byte) { + request.push(byte[0]); + if request.ends_with(b"\r\n\r\n") || request.len() > 8192 { + break; + } + } + let header = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + if stream.write_all(header.as_bytes()).is_err() { + continue; + } + let _ = stream.write_all(&body); + let _ = stream.flush(); + } + }); + base +} + +/// Builds a gzip-compressed tar from `(archive_path, bytes)` entries, each with +/// mode `0o755`. Archive paths use `/` separators (the tar convention). +fn build_tarball(entries: &[(&str, &[u8])]) -> Vec { + use flate2::Compression; + use flate2::write::GzEncoder; + use tar::{Builder, Header}; + + let mut buf = Vec::new(); + { + let mut builder = Builder::new(GzEncoder::new(&mut buf, Compression::default())); + for &(path, bytes) in entries { + let mut header = Header::new_gnu(); + header.set_size(u64::try_from(bytes.len()).expect("entry size fits in u64")); + header.set_mode(0o755); + header.set_cksum(); + builder + .append_data(&mut header, path, bytes) + .expect("append tar entry"); + } + builder + .into_inner() + .expect("finish tar") + .finish() + .expect("finish gzip"); + } + buf +} + +/// Builds a Binaryen release tarball matching the host platform's required +/// layout: `binaryen-/bin/wasm-opt[.exe]`, plus the `lib/libbinaryen.dylib` +/// sibling the installer requires on macOS. +fn build_host_binaryen_tarball() -> Vec { + let wasm_opt = std::fs::read(fake_wasm_opt_binary()).expect("read compiled fake wasm-opt"); + let bin_path = format!( + "binaryen-{BINARYEN_PIN}/bin/wasm-opt{}", + std::env::consts::EXE_SUFFIX + ); + + #[cfg(target_os = "macos")] + let dylib_path = format!("binaryen-{BINARYEN_PIN}/lib/libbinaryen.dylib"); + #[cfg(target_os = "macos")] + let entries: Vec<(&str, &[u8])> = vec![ + (bin_path.as_str(), wasm_opt.as_slice()), + (dylib_path.as_str(), b"fake libbinaryen dylib"), + ]; + #[cfg(not(target_os = "macos"))] + let entries: Vec<(&str, &[u8])> = vec![(bin_path.as_str(), wasm_opt.as_slice())]; + + build_tarball(&entries) +} + +/// The lowercase-hex SHA256 of `bytes`. +fn sha256_hex(bytes: &[u8]) -> String { + use sha2::{Digest, Sha256}; + use std::fmt::Write; + let mut hasher = Sha256::new(); + hasher.update(bytes); + let mut out = String::with_capacity(64); + for b in hasher.finalize() { + let _ = write!(out, "{b:02x}"); + } + out +} + +/// Whether `dir` contains an entry whose name starts with `prefix`. Returns +/// `false` when `dir` does not exist. +fn dir_has_prefixed_entry(dir: &std::path::Path, prefix: &str) -> bool { + let Ok(entries) = std::fs::read_dir(dir) else { + return false; + }; + entries + .filter_map(Result::ok) + .any(|e| e.file_name().to_string_lossy().starts_with(prefix)) +} + +/// The install path of the managed `wasm-opt` under `home`. +fn managed_wasm_opt(home: &assert_fs::TempDir) -> assert_fs::fixture::ChildPath { + home.child("tools") + .child("binaryen") + .child(BINARYEN_PIN) + .child("bin") + .child(format!("wasm-opt{}", std::env::consts::EXE_SUFFIX)) +} + +/// Builds an `infs component add wasm-opt` command wired to `home` and the stub. +fn component_add_cmd(home: &assert_fs::TempDir, base: &str, sha: &str) -> Command { + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFERENCE_HOME", home.path()) + .env("INFS_BINARYEN_BASE_URL", base) + .env("INFS_BINARYEN_SHA256", sha) + .args(["component", "add", "wasm-opt"]); + cmd +} + +/// Installs the managed component into `home` via a fresh stub, asserting success. +fn install_managed_component(home: &assert_fs::TempDir) { + let tarball = build_host_binaryen_tarball(); + let sha = sha256_hex(&tarball); + let base = spawn_binaryen_stub(tarball); + component_add_cmd(home, &base, &sha).assert().success(); +} + +/// `component add wasm-opt` downloads from the stub, verifies the checksum, +/// installs an executable `wasm-opt`, and cleans up the archive and temp dir. +#[test] +fn component_add_downloads_verifies_and_installs() { + let home = assert_fs::TempDir::new().unwrap(); + let tarball = build_host_binaryen_tarball(); + let sha = sha256_hex(&tarball); + let base = spawn_binaryen_stub(tarball); + + component_add_cmd(&home, &base, &sha) + .assert() + .success() + .stdout(predicate::str::contains("Installing component 'wasm-opt'")); + + let installed = managed_wasm_opt(&home); + assert!(installed.path().exists(), "wasm-opt must be installed"); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = std::fs::metadata(installed.path()) + .unwrap() + .permissions() + .mode(); + assert_eq!(mode & 0o111, 0o111, "installed wasm-opt must be executable"); + } + + assert!( + !dir_has_prefixed_entry(home.child("downloads").path(), "binaryen-"), + "the downloaded archive must be cleaned up" + ); + assert!( + !dir_has_prefixed_entry(home.child("tools").child("binaryen").path(), ".tmp-"), + "no per-process temp dir should remain" + ); +} + +/// A second `add` with the base URL pointed at a dead port still succeeds — an +/// already-installed component short-circuits before any network access. +#[test] +fn component_add_is_idempotent_without_network() { + let home = assert_fs::TempDir::new().unwrap(); + let tarball = build_host_binaryen_tarball(); + let sha = sha256_hex(&tarball); + let base = spawn_binaryen_stub(tarball); + + component_add_cmd(&home, &base, &sha).assert().success(); + + // Nothing listens on port 1; reaching the network here would fail. + let mut second = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + second + .env("INFERENCE_HOME", home.path()) + .env("INFS_BINARYEN_BASE_URL", "http://localhost:1") + .env("INFS_BINARYEN_SHA256", &sha) + .args(["component", "add", "wasm-opt"]); + second + .assert() + .success() + .stdout(predicate::str::contains("already installed")); +} + +/// A checksum mismatch rejects the download, installs nothing, and deletes the +/// downloaded archive. +#[test] +fn component_add_rejects_checksum_mismatch_and_cleans_up() { + let home = assert_fs::TempDir::new().unwrap(); + let tarball = build_host_binaryen_tarball(); + let base = spawn_binaryen_stub(tarball); + let wrong_sha = "0".repeat(64); + + component_add_cmd(&home, &base, &wrong_sha) + .assert() + .failure() + .stderr(predicate::str::contains("Checksum verification failed")); + + assert!( + !home + .child("tools") + .child("binaryen") + .child(BINARYEN_PIN) + .path() + .exists(), + "a checksum mismatch must install nothing" + ); + assert!( + !dir_has_prefixed_entry(home.child("downloads").path(), "binaryen-"), + "the rejected archive must be deleted" + ); +} + +/// A directory present at the pinned path but missing the binary is a broken +/// install; `add` repairs it. +#[test] +fn component_add_repairs_broken_install() { + let home = assert_fs::TempDir::new().unwrap(); + // Pre-seed a broken install: the version dir exists, but has no binary. + home.child("tools") + .child("binaryen") + .child(BINARYEN_PIN) + .child("bin") + .create_dir_all() + .unwrap(); + + install_managed_component(&home); + + assert!( + managed_wasm_opt(&home).path().exists(), + "a broken install must be repaired" + ); +} + +/// An unknown component name is rejected, naming the known components. No network +/// or platform work happens first. +#[test] +fn component_add_unknown_component_errors() { + let home = assert_fs::TempDir::new().unwrap(); + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFERENCE_HOME", home.path()) + .args(["component", "add", "wasm-optimizer"]); + cmd.assert().failure().stderr( + predicate::str::contains("Unknown component 'wasm-optimizer'") + .and(predicate::str::contains("Known components: wasm-opt")), + ); +} + +/// `component remove wasm-opt` deletes an installed component. +#[test] +fn component_remove_present_deletes_install() { + let home = assert_fs::TempDir::new().unwrap(); + install_managed_component(&home); + let dir = home.child("tools").child("binaryen").child(BINARYEN_PIN); + assert!(dir.path().exists(), "precondition: component installed"); + + let mut rm = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + rm.env("INFERENCE_HOME", home.path()) + .args(["component", "remove", "wasm-opt"]); + rm.assert() + .success() + .stdout(predicate::str::contains("Removed component 'wasm-opt'")); + + assert!(!dir.path().exists(), "remove must delete the install dir"); +} + +/// Removing an absent component bails. +#[test] +fn component_remove_absent_bails() { + let home = assert_fs::TempDir::new().unwrap(); + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFERENCE_HOME", home.path()) + .args(["component", "remove", "wasm-opt"]); + cmd.assert() + .failure() + .stderr(predicate::str::contains("not installed")); +} + +/// Removing an unknown component name is rejected before touching the filesystem. +#[test] +fn component_remove_unknown_component_errors() { + let home = assert_fs::TempDir::new().unwrap(); + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFERENCE_HOME", home.path()) + .args(["component", "remove", "binaryen"]); + cmd.assert() + .failure() + .stderr(predicate::str::contains("Unknown component 'binaryen'")); +} + +/// `component list` reports the not-installed state with the add hint. +#[test] +fn component_list_reports_not_installed() { + let home = assert_fs::TempDir::new().unwrap(); + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFERENCE_HOME", home.path()) + .args(["component", "list"]); + cmd.assert().success().stdout( + predicate::str::contains("wasm-opt") + .and(predicate::str::contains("not installed")) + .and(predicate::str::contains("infs component add wasm-opt")), + ); +} + +/// `component list` reports the installed state with the version and path. +#[test] +fn component_list_reports_installed() { + let home = assert_fs::TempDir::new().unwrap(); + install_managed_component(&home); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFERENCE_HOME", home.path()) + .args(["component", "list"]); + cmd.assert().success().stdout(predicate::str::contains( + "installed: Binaryen version_130 at", + )); +} + +/// macOS installs the `libbinaryen.dylib` sibling alongside the binary — the +/// dynamic `wasm-opt` there links against it at runtime. +#[cfg(target_os = "macos")] +#[test] +fn component_add_installs_dylib_sibling_on_macos() { + let home = assert_fs::TempDir::new().unwrap(); + install_managed_component(&home); + + let install_root = home.child("tools").child("binaryen").child(BINARYEN_PIN); + assert!( + install_root.child("bin").child("wasm-opt").path().exists(), + "the wasm-opt binary must be installed" + ); + assert!( + install_root + .child("lib") + .child("libbinaryen.dylib") + .path() + .exists(), + "the libbinaryen.dylib sibling must be installed on macOS" + ); +} + +/// An archive missing a required file surfaces a layout-drift error and installs +/// nothing. +#[test] +fn component_add_reports_layout_drift_on_missing_file() { + let home = assert_fs::TempDir::new().unwrap(); + // The binary is under an unexpected name, so the required `bin/wasm-opt` + // (required on every platform) is absent. + let entry = format!("binaryen-{BINARYEN_PIN}/bin/not-wasm-opt"); + let tarball = build_tarball(&[(entry.as_str(), b"not the binary")]); + let sha = sha256_hex(&tarball); + let base = spawn_binaryen_stub(tarball); + + component_add_cmd(&home, &base, &sha) + .assert() + .failure() + .stderr(predicate::str::contains("missing the expected file")); + + assert!( + !home + .child("tools") + .child("binaryen") + .child(BINARYEN_PIN) + .path() + .exists(), + "a layout-drift failure must install nothing" + ); +} + +// Managed-tier resolution, auto-install, and doctor Tests +// +// These exercise the three-tier `wasm-opt` resolver (WASM_OPT_PATH -> PATH -> +// managed), the manifest `auto-install` build-time provisioning, and the +// `infs doctor` check. They are unix-gated: an empty PATH is the portable way to +// force the PATH lookup to miss, and infc is still reached via `INFC_PATH`. Each +// isolates `INFERENCE_HOME` so nothing leaks into (or resolves from) the +// developer's real toolchain. + +/// Seeds a managed `wasm-opt` under `home` by copying the compiled fake into the +/// pinned Binaryen layout (`tools/binaryen//bin/wasm-opt`) and making it +/// executable, so the resolver's managed tier finds a runnable binary without a +/// network install. Returns the installed path. +#[cfg(unix)] +fn seed_managed_wasm_opt(home: &assert_fs::TempDir) -> std::path::PathBuf { + use std::os::unix::fs::PermissionsExt; + + let bin_dir = home + .child("tools") + .child("binaryen") + .child(BINARYEN_PIN) + .child("bin"); + bin_dir.create_dir_all().unwrap(); + + let managed = bin_dir.child(format!("wasm-opt{}", std::env::consts::EXE_SUFFIX)); + std::fs::copy(fake_wasm_opt_binary(), managed.path()).expect("copy fake wasm-opt into managed"); + let mut perms = std::fs::metadata(managed.path()).unwrap().permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(managed.path(), perms).unwrap(); + managed.path().to_path_buf() +} + +/// A `wasm-opt` on PATH takes precedence over a managed copy: with both present, +/// resolution reports the PATH tier (the `INFS_VERBOSE` trace names it), matching +/// `find_infc`'s PATH-over-managed precedence. +#[cfg(unix)] +#[test] +fn wasm_opt_path_beats_managed() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let home = assert_fs::TempDir::new().unwrap(); + let managed = seed_managed_wasm_opt(&home); + assert!(managed.exists(), "precondition: managed copy seeded"); + + // The compiled fake's own directory becomes the sole PATH entry, so + // `which::which("wasm-opt")` resolves to it. + let path_dir = fake_wasm_opt_binary() + .parent() + .expect("fake wasm-opt has a parent dir") + .to_path_buf(); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("INFERENCE_HOME", home.path()) + .env("PATH", &path_dir) + .env("INFS_VERBOSE", "1") + .env_remove("WASM_OPT_PATH") + .current_dir(temp.path()) + .arg("build"); + + cmd.assert() + .success() + .stderr(predicate::str::contains("resolved wasm-opt via PATH:")); +} + +/// The managed tier is the fallback when PATH has no `wasm-opt`: with an empty +/// PATH and no override, resolution uses the managed copy (the `INFS_VERBOSE` +/// trace names the tier) and the optimizer actually runs. +#[cfg(unix)] +#[test] +fn wasm_opt_managed_tier_used_when_path_empty() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nlevel = \"z\"\n", + ); + let home = assert_fs::TempDir::new().unwrap(); + seed_managed_wasm_opt(&home); + let log = temp.child("wasm-opt.log"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("INFERENCE_HOME", home.path()) + .env("PATH", "") + .env("INFS_VERBOSE", "1") + .env("FAKE_WASM_OPT_LOG", log.path()) + .env_remove("WASM_OPT_PATH") + .current_dir(temp.path()) + .arg("build"); + + cmd.assert().success().stderr(predicate::str::contains( + "resolved wasm-opt via managed tools:", + )); + + assert_eq!( + optimizer_invocations(log.path()).len(), + 1, + "the managed wasm-opt must run the optimization" + ); +} + +/// `auto-install = true` downloads the pinned Binaryen from the stub when no +/// `wasm-opt` resolves, then optimizes with it: the console announces the +/// auto-install, the managed binary lands under `INFERENCE_HOME`, and the size +/// summary confirms the optimizer ran. +#[cfg(unix)] +#[test] +fn wasm_opt_auto_install_downloads_then_optimizes() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nauto-install = true\nlevel = \"z\"\n", + ); + let home = assert_fs::TempDir::new().unwrap(); + let log = temp.child("wasm-opt.log"); + + let tarball = build_host_binaryen_tarball(); + let sha = sha256_hex(&tarball); + let base = spawn_binaryen_stub(tarball); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("INFERENCE_HOME", home.path()) + .env("INFS_BINARYEN_BASE_URL", &base) + .env("INFS_BINARYEN_SHA256", &sha) + .env("FAKE_WASM_OPT_LOG", log.path()) + .env("PATH", "") + .env_remove("WASM_OPT_PATH") + .current_dir(temp.path()) + .arg("build"); + + cmd.assert() + .success() + .stdout(predicate::str::contains( + "[build.wasm-opt] auto-install is enabled.", + )) + .stdout(predicate::str::contains("wasm-opt -Oz: main.wasm")); + + assert!( + managed_wasm_opt(&home).path().exists(), + "auto-install must leave the managed wasm-opt installed" + ); + assert_eq!( + optimizer_invocations(log.path()).len(), + 1, + "the freshly installed wasm-opt must run the optimization" + ); +} + +/// `auto-install = false` (the default) with no resolvable `wasm-opt` is a hard +/// error whose remediation leads with the managed component command *before* the +/// `brew` package hint — the managed path is the recommended first option. +#[cfg(unix)] +#[test] +fn wasm_opt_auto_install_false_errors_with_component_hint_before_brew() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nauto-install = false\nlevel = \"z\"\n", + ); + let home = assert_fs::TempDir::new().unwrap(); + + let output = Command::new(assert_cmd::cargo::cargo_bin!("infs")) + .env("INFC_PATH", &infc_path) + .env("INFERENCE_HOME", home.path()) + .env("PATH", "") + .env_remove("WASM_OPT_PATH") + .current_dir(temp.path()) + .arg("build") + .output() + .expect("infs build should run"); + + assert!( + !output.status.success(), + "a missing wasm-opt with auto-install off must fail" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + let component_at = stderr + .find("infs component add wasm-opt") + .expect("the managed component hint must be present"); + let brew_at = stderr + .find("brew install binaryen") + .expect("the brew hint must be present"); + assert!( + component_at < brew_at, + "the managed component hint must precede the brew hint; stderr:\n{stderr}" + ); +} + +/// An offline `auto-install = true` build surfaces the auto-install failure with +/// remediation context rather than a bare network error: the download against a +/// dead port fails, and the error names the retry / manual-install path. +#[cfg(unix)] +#[test] +fn wasm_opt_auto_install_offline_reports_context() { + let Some(infc_path) = require_infc() else { + return; + }; + + let temp = assert_fs::TempDir::new().unwrap(); + scaffold_project_with_manifest( + &temp, + "demo", + PROJECT_MAIN_SRC, + "[build.wasm-opt]\nauto-install = true\nlevel = \"z\"\n", + ); + let home = assert_fs::TempDir::new().unwrap(); + let sha = "0".repeat(64); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); + cmd.env("INFC_PATH", &infc_path) + .env("INFERENCE_HOME", home.path()) + .env("INFS_BINARYEN_BASE_URL", "http://localhost:1") + .env("INFS_BINARYEN_SHA256", &sha) + .env("PATH", "") + .env_remove("WASM_OPT_PATH") + .current_dir(temp.path()) + .arg("build"); + + cmd.assert() + .failure() + .stderr(predicate::str::contains("Failed to auto-install")); +} + +/// `infs doctor` reports a `wasm-opt` that resolves nowhere as an OK optional +/// line — a project that does not use `[build.wasm-opt]` is never alarmed, and +/// the line still names the component command for users who do want it. +#[cfg(unix)] +#[test] +fn doctor_reports_missing_wasm_opt_as_optional_ok() { + let home = assert_fs::TempDir::new().unwrap(); + + let output = Command::new(assert_cmd::cargo::cargo_bin!("infs")) + .arg("doctor") + .env("INFERENCE_HOME", home.path()) + .env("PATH", "") + .env_remove("WASM_OPT_PATH") + .output() + .expect("doctor should run"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("[OK] wasm-opt: Not installed (optional"), + "doctor must report a never-installed wasm-opt as optional OK; stdout:\n{stdout}" + ); + assert!( + stdout.contains("infs component add wasm-opt"), + "the optional OK line must name the component command; stdout:\n{stdout}" + ); +} + +/// `infs doctor` warns when a managed Binaryen directory exists but its binary +/// is missing (a broken/interrupted install), pointing at the component command +/// to repair it. +#[cfg(unix)] +#[test] +fn doctor_warns_on_broken_managed_wasm_opt() { + let home = assert_fs::TempDir::new().unwrap(); + // A managed version dir without the binary — a broken/interrupted install. + home.child("tools") + .child("binaryen") + .child(BINARYEN_PIN) + .child("bin") + .create_dir_all() + .unwrap(); + + let output = Command::new(assert_cmd::cargo::cargo_bin!("infs")) + .arg("doctor") + .env("INFERENCE_HOME", home.path()) + .env("PATH", "") + .env_remove("WASM_OPT_PATH") + .output() + .expect("doctor should run"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("[WARN] wasm-opt:") && stdout.contains("infs component add wasm-opt"), + "a broken managed install must warn with the repair hint; stdout:\n{stdout}" + ); +} + +/// `infs doctor` reports an invalid `WASM_OPT_PATH` as a WARN naming the +/// environment variable, rather than silently ignoring the user's override. +#[cfg(unix)] +#[test] +fn doctor_warns_on_invalid_wasm_opt_path() { + let home = assert_fs::TempDir::new().unwrap(); + let bogus = home.child("not-a-wasm-opt"); + + let output = Command::new(assert_cmd::cargo::cargo_bin!("infs")) + .arg("doctor") + .env("INFERENCE_HOME", home.path()) + .env("WASM_OPT_PATH", bogus.path()) + .output() + .expect("doctor should run"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("[WARN] wasm-opt:") && stdout.contains("WASM_OPT_PATH"), + "an invalid WASM_OPT_PATH must warn naming the env var; stdout:\n{stdout}" + ); +} + +/// `infs doctor` reports a healthy managed `wasm-opt` as OK, naming the managed +/// tier and the Binaryen version its `--version` reports. +#[cfg(unix)] +#[test] +fn doctor_reports_managed_wasm_opt_as_ok() { + let home = assert_fs::TempDir::new().unwrap(); + seed_managed_wasm_opt(&home); + + let output = Command::new(assert_cmd::cargo::cargo_bin!("infs")) + .arg("doctor") + .env("INFERENCE_HOME", home.path()) + .env("PATH", "") + .env_remove("WASM_OPT_PATH") + .output() + .expect("doctor should run"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("[OK] wasm-opt: Found at") && stdout.contains("source: managed tools"), + "a healthy managed wasm-opt must be OK naming the managed tier; stdout:\n{stdout}" + ); +} diff --git a/apps/infs/tests/fixtures/fake_wasm_opt.rs b/apps/infs/tests/fixtures/fake_wasm_opt.rs new file mode 100644 index 00000000..e111b84c --- /dev/null +++ b/apps/infs/tests/fixtures/fake_wasm_opt.rs @@ -0,0 +1,106 @@ +//! Dependency-free fake `wasm-opt` used by the `infs` CLI integration tests. +//! +//! It stands in for the real Binaryen `wasm-opt` so the hermetic post-build +//! optimization tests run on machines without Binaryen installed. The harness +//! compiles this file once per test process with `rustc` and points `infs` at +//! the result through the `WASM_OPT_PATH` environment variable. Every role the +//! tests need is selected by environment variable so a single binary suffices: +//! +//! - `--version` among the arguments: print `FAKE_WASM_OPT_VERSION` (default +//! `wasm-opt version 118 (fake)`) to stdout and exit 0. This branch runs +//! before any logging, so the version probe `infs` performs is never recorded +//! in the invocation log. +//! - `FAKE_WASM_OPT_LOG`: append this invocation's arguments (one per line, +//! after a marker line) to the named file, letting a test assert the exact +//! argument vector `infs` forwarded. +//! - `FAKE_WASM_OPT_EXIT`: when set to a nonzero integer, print `fake failure` +//! to stderr and exit with that code (the optimizer-failed path). +//! - `FAKE_WASM_OPT_GARBAGE=1`: write non-wasm bytes to the `-o` target instead +//! of a valid module (the re-validation-failed path). +//! - otherwise: copy the positional input file to the `-o` target byte-for-byte +//! and exit 0 (the success path). +//! +//! It lives under `tests/fixtures/` (not `tests/`) so Cargo does not compile it +//! as an integration-test target; the harness builds it explicitly. + +use std::io::Write; + +/// Marker written before each logged invocation so a test can count invocations +/// and isolate a single invocation's argument lines. +const INVOCATION_MARKER: &str = "--- wasm-opt invocation ---"; + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + + if args.iter().any(|arg| arg == "--version") { + let version = std::env::var("FAKE_WASM_OPT_VERSION") + .unwrap_or_else(|_| String::from("wasm-opt version 118 (fake)")); + println!("{version}"); + return; + } + + if let Ok(log_path) = std::env::var("FAKE_WASM_OPT_LOG") { + log_invocation(&log_path, &args); + } + + if let Ok(raw) = std::env::var("FAKE_WASM_OPT_EXIT") { + if let Ok(code) = raw.parse::() { + if code != 0 { + eprintln!("fake failure"); + std::process::exit(code); + } + } + } + + let (input, output) = parse_io(&args); + let output = output.expect("fake wasm-opt: no `-o ` argument was provided"); + + if std::env::var("FAKE_WASM_OPT_GARBAGE").as_deref() == Ok("1") { + std::fs::write(&output, b"not a valid wasm module") + .expect("fake wasm-opt: failed to write garbage output"); + return; + } + + let input = input.expect("fake wasm-opt: no positional input file was provided"); + let bytes = std::fs::read(&input).expect("fake wasm-opt: failed to read input"); + std::fs::write(&output, &bytes).expect("fake wasm-opt: failed to write output"); +} + +/// Appends one invocation (a marker line followed by each argument on its own +/// line) to the log file named by `FAKE_WASM_OPT_LOG`. +fn log_invocation(log_path: &str, args: &[String]) { + let mut entry = String::from(INVOCATION_MARKER); + entry.push('\n'); + for arg in args { + entry.push_str(arg); + entry.push('\n'); + } + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(log_path) + .expect("fake wasm-opt: failed to open FAKE_WASM_OPT_LOG"); + file.write_all(entry.as_bytes()) + .expect("fake wasm-opt: failed to append to FAKE_WASM_OPT_LOG"); +} + +/// Extracts the positional input (first non-flag argument) and the `-o` target +/// from a `wasm-opt`-style argument vector. +fn parse_io(args: &[String]) -> (Option, Option) { + let mut input = None; + let mut output = None; + let mut i = 0; + while i < args.len() { + let arg = &args[i]; + if arg == "-o" { + output = args.get(i + 1).cloned(); + i += 2; + continue; + } + if !arg.starts_with('-') && input.is_none() { + input = Some(arg.clone()); + } + i += 1; + } + (input, output) +} diff --git a/editors/vscode/QA_GUIDE.md b/editors/vscode/QA_GUIDE.md index 21d43c73..2575121e 100644 --- a/editors/vscode/QA_GUIDE.md +++ b/editors/vscode/QA_GUIDE.md @@ -36,6 +36,7 @@ Many QA cases below are covered by automated tests (`npm test`). Cases marked wi | 9. Error Handling | **[A]** Most paths automated (`install-failures.test.ts`, `version-parsing.test.ts`, `e2e-installation.test.ts`) | | 10. Cross-Platform | Manual (requires physical platforms); detection and extraction logic tested | | 11. Privacy & Security | **[A]** HTTPS redirect + SHA-256 automated (`https-redirect.test.ts`, `download.test.ts`) | +| 12. Component Management | Partial -- component args + doctor-attention logic automated (`components.test.ts`, `doctor.test.ts`); UI flows manual | --- @@ -46,7 +47,7 @@ Many QA cases below are covered by automated tests (`npm test`). Cases marked wi | 0.1 | `npm install` in `editors/vscode/` | Installs without errors | | 0.2 | `npm run build` | Builds `dist/extension.js` without errors | | 0.3 | `npm run build:prod` | Production build succeeds | -| 0.4 | `npm test` | All 216 tests pass, 0 failures | +| 0.4 | `npm test` | All 224 tests pass, 0 failures | | 0.5 | `npm run package` | Produces `inference-0.0.3.vsix` without errors | --- @@ -301,3 +302,26 @@ Many QA cases below are covered by automated tests (`npm test`). Cases marked wi | 11.4 | HTTPS-to-HTTP redirect is blocked | If manifest/download redirects to HTTP, fails with "Refusing HTTPS-to-HTTP redirect: {url} -> {target}" **[A]** | | | 11.5 | JSON response size limit | Responses larger than 10 MB are rejected | | | 11.6 | Redirect chain limit | More than 5 redirects are rejected: "Too many redirects fetching {url}" | | + +--- + +## 12. Component Management (wasm-opt) + +The `inference.installComponent` command shells out to `infs component add wasm-opt` +to provision the managed Binaryen `wasm-opt` binary. These cases require the extension +host (F5) and, unless noted, a working toolchain (`infs` on PATH or in `INFERENCE_HOME`). + +| # | Step | Expected | Pass? | +|---|------|----------|-------| +| 12.1 | In `editors/vscode/`: `npm install`, `npm run build`, `npm test` | Installs and builds clean; all 224 tests pass, 0 failures | | +| 12.2 | Ctrl+Shift+P > "Inference: Install Component (wasm-opt)" | Progress notification "Inference Component" ("Installing wasm-opt..."). On success: info "Inference: component 'wasm-opt' installed." Doctor re-runs and the status bar refreshes. | | +| 12.3 | After install, run `~/.inference/tools/binaryen/version_130/bin/wasm-opt --version` in a terminal | Prints a Binaryen version (>= 116) | | +| 12.4 | Run the install command **again** (component already installed) | Completes quickly with a skip message in the Output channel; success info notification; no error | | +| 12.5 | Invoke the install command while one is already running | Shows: "Inference component installation is already in progress." | | +| 12.6 | Run the command with **no** toolchain detected (rename `~/.inference/bin/infs`, clear `inference.path`, remove from PATH) | Warning: "Inference toolchain not found. Install it first." with an "Install" button that routes to `inference.installToolchain` | | +| 12.7 | Break the managed install (delete `~/.inference/tools/binaryen/version_130/bin/wasm-opt`), then Ctrl+Shift+P > "Inference: Run Doctor" | Doctor reports a WARN for `wasm-opt`. The warning notification shows an "Install wasm-opt" action alongside "Show Output". | | +| 12.8 | Click "Install wasm-opt" on the doctor warning | Triggers `inference.installComponent`, repairs the managed install; a follow-up doctor run reports `wasm-opt` as OK | | +| 12.9 | **Offline failure:** disconnect the network, delete the managed install, run the install command | Error: "Inference: failed to install component 'wasm-opt'. See output for details." with "Show Output" and "Retry" buttons | | +| 12.10 | Click "Show Output" on the failure notification | Reveals the Inference output channel with the failure details | | +| 12.11 | Click "Retry" on the failure notification | Re-invokes the install command | | +| 12.12 | **PATH shadowing:** place a `wasm-opt` on PATH while the managed copy exists, then Run Doctor | Doctor's `wasm-opt` OK line notes the managed copy is shadowed by PATH | | diff --git a/editors/vscode/dist/extension.js b/editors/vscode/dist/extension.js index 2ff66390..7a8e2518 100644 --- a/editors/vscode/dist/extension.js +++ b/editors/vscode/dist/extension.js @@ -148,7 +148,7 @@ __export(extension_exports, { deactivate: () => deactivate }); module.exports = __toCommonJS(extension_exports); -var vscode9 = __toESM(require("vscode")); +var vscode10 = __toESM(require("vscode")); var path6 = __toESM(require("path")); // src/toolchain/platform.ts @@ -900,8 +900,125 @@ function notifyInstallError(errorMessage) { }); } -// src/commands/doctor.ts +// src/commands/installComponent.ts var vscode4 = __toESM(require("vscode")); +init_exec(); + +// src/toolchain/components.ts +var KNOWN_COMPONENTS = ["wasm-opt"]; +function componentAddArgs(component) { + return ["component", "add", component]; +} +function wasmOptNeedsAttention(result) { + return result.checks.some( + (check) => check.name === "wasm-opt" && (check.status === "warn" || check.status === "fail") + ); +} + +// src/commands/installComponent.ts +var installing2 = false; +var INSTALL_TIMEOUT_MS = 6e5; +function registerInstallComponentCommand(outputChannel2) { + return vscode4.commands.registerCommand( + "inference.installComponent", + async (component = "wasm-opt") => { + if (installing2) { + vscode4.window.showInformationMessage( + "Inference component installation is already in progress." + ); + return; + } + if (!isKnownComponent(component)) { + vscode4.window.showErrorMessage( + `Inference: unknown component '${component}'.` + ); + return; + } + const detection = detectInfs(); + if (!detection) { + vscode4.window.showWarningMessage( + "Inference toolchain not found. Install it first.", + "Install" + ).then((action) => { + if (action === "Install") { + vscode4.commands.executeCommand( + "inference.installToolchain" + ); + } + }); + return; + } + installing2 = true; + try { + const result = await installWithProgress2( + detection.path, + component, + outputChannel2 + ); + if (result.stdout) { + outputChannel2.appendLine(result.stdout); + } + if (result.stderr) { + outputChannel2.appendLine(result.stderr); + } + if (result.exitCode === 0) { + vscode4.window.showInformationMessage( + `Inference: component '${component}' installed.` + ); + vscode4.commands.executeCommand("inference.runDoctor"); + } else { + notifyInstallError2(component); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + outputChannel2.appendLine( + `Component installation failed: ${message}` + ); + notifyInstallError2(component); + } finally { + installing2 = false; + } + } + ); +} +function isKnownComponent(name) { + return KNOWN_COMPONENTS.includes(name); +} +function installWithProgress2(infsPath, component, outputChannel2) { + return vscode4.window.withProgress( + { + location: vscode4.ProgressLocation.Notification, + title: "Inference Component", + cancellable: false + }, + async (progress) => { + progress.report({ message: `Installing ${component}...` }); + outputChannel2.appendLine(`Installing component '${component}'...`); + return exec(infsPath, componentAddArgs(component), { + timeoutMs: INSTALL_TIMEOUT_MS + }); + } + ); +} +function notifyInstallError2(component) { + vscode4.window.showErrorMessage( + `Inference: failed to install component '${component}'. See output for details.`, + "Show Output", + "Retry" + ).then((action) => { + if (action === "Show Output") { + vscode4.commands.executeCommand("inference.showOutput"); + } else if (action === "Retry") { + vscode4.commands.executeCommand( + "inference.installComponent", + component + ); + } + }); +} + +// src/commands/doctor.ts +var vscode5 = __toESM(require("vscode")); init_doctor(); // src/toolchain/doctorFormat.ts @@ -928,7 +1045,7 @@ function formatDoctorChecks(result) { // src/commands/doctor.ts var running = false; function registerDoctorCommand(outputChannel2, statusBarItem) { - return vscode4.commands.registerCommand( + return vscode5.commands.registerCommand( "inference.runDoctor", async () => { if (running) { @@ -938,12 +1055,12 @@ function registerDoctorCommand(outputChannel2, statusBarItem) { if (!detection) { outputChannel2.appendLine("Doctor: infs binary not found."); updateStatusBar(statusBarItem, null); - vscode4.window.showWarningMessage( + vscode5.window.showWarningMessage( "Inference toolchain not found. Install it first.", "Install" ).then((action) => { if (action === "Install") { - vscode4.commands.executeCommand( + vscode5.commands.executeCommand( "inference.installToolchain" ); } @@ -961,7 +1078,7 @@ function registerDoctorCommand(outputChannel2, statusBarItem) { "Doctor: failed to execute infs doctor." ); updateStatusBar(statusBarItem, null); - vscode4.window.showErrorMessage( + vscode5.window.showErrorMessage( "Inference: Failed to run doctor. See output for details." ); return; @@ -970,27 +1087,45 @@ function registerDoctorCommand(outputChannel2, statusBarItem) { outputChannel2.appendLine(line); } updateStatusBar(statusBarItem, result); - vscode4.commands.executeCommand("inference.refreshConfigView"); + vscode5.commands.executeCommand("inference.refreshConfigView"); if (result.hasErrors) { - vscode4.window.showErrorMessage( + const actions = ["Show Output"]; + if (wasmOptNeedsAttention(result)) { + actions.push("Install wasm-opt"); + } + vscode5.window.showErrorMessage( `Inference doctor: ${result.summary}`, - "Show Output" + ...actions ).then((action) => { if (action === "Show Output") { outputChannel2.show(); + } else if (action === "Install wasm-opt") { + vscode5.commands.executeCommand( + "inference.installComponent", + "wasm-opt" + ); } }); } else if (result.hasWarnings) { - vscode4.window.showWarningMessage( + const actions = ["Show Output"]; + if (wasmOptNeedsAttention(result)) { + actions.push("Install wasm-opt"); + } + vscode5.window.showWarningMessage( `Inference doctor: ${result.summary}`, - "Show Output" + ...actions ).then((action) => { if (action === "Show Output") { outputChannel2.show(); + } else if (action === "Install wasm-opt") { + vscode5.commands.executeCommand( + "inference.installComponent", + "wasm-opt" + ); } }); } else { - vscode4.window.showInformationMessage( + vscode5.window.showInformationMessage( "Inference: Toolchain is healthy." ); } @@ -1002,7 +1137,7 @@ function registerDoctorCommand(outputChannel2, statusBarItem) { } // src/commands/selectVersion.ts -var vscode6 = __toESM(require("vscode")); +var vscode7 = __toESM(require("vscode")); // src/toolchain/versions.ts init_exec(); @@ -1092,11 +1227,11 @@ function buildVersionPickItems(versions, currentVersion) { } // src/commands/versionChange.ts -var vscode5 = __toESM(require("vscode")); +var vscode6 = __toESM(require("vscode")); async function performVersionChange(infsPath, version, outputChannel2, actionVerb) { - await vscode5.window.withProgress( + await vscode6.window.withProgress( { - location: vscode5.ProgressLocation.Notification, + location: vscode6.ProgressLocation.Notification, title: "Inference Toolchain", cancellable: false }, @@ -1108,14 +1243,14 @@ async function performVersionChange(infsPath, version, outputChannel2, actionVer outputChannel2.appendLine( `${actionVerb} toolchain v${version} complete.` ); - vscode5.commands.executeCommand( + vscode6.commands.executeCommand( "setContext", "inference.toolchainInstalled", true ); - vscode5.commands.executeCommand("inference.applyTerminalPath"); - vscode5.commands.executeCommand("inference.runDoctor"); - vscode5.window.showInformationMessage( + vscode6.commands.executeCommand("inference.applyTerminalPath"); + vscode6.commands.executeCommand("inference.runDoctor"); + vscode6.window.showInformationMessage( `Inference toolchain ${actionVerb.toLowerCase()} to v${version}.`, "Show Output" ).then((action) => { @@ -1129,7 +1264,7 @@ async function performVersionChange(infsPath, version, outputChannel2, actionVer `${actionVerb} failed: ${result.error}` ); if (result.installedButNotDefault) { - vscode5.window.showWarningMessage( + vscode6.window.showWarningMessage( `Inference: v${version} was installed but could not be set as default. Run \`infs default ${version}\` manually.`, "Show Output" ).then((action) => { @@ -1138,7 +1273,7 @@ async function performVersionChange(infsPath, version, outputChannel2, actionVer } }); } else { - vscode5.window.showErrorMessage( + vscode6.window.showErrorMessage( `Inference: Failed to install v${version}: ${result.error}` ); } @@ -1149,23 +1284,23 @@ async function performVersionChange(infsPath, version, outputChannel2, actionVer // src/commands/selectVersion.ts var selecting = false; function registerSelectVersionCommand(outputChannel2) { - return vscode6.commands.registerCommand( + return vscode7.commands.registerCommand( "inference.selectVersion", async () => { if (selecting) { - vscode6.window.showInformationMessage( + vscode7.window.showInformationMessage( "Version selection is already in progress." ); return; } const detection = detectInfs(); if (!detection) { - vscode6.window.showWarningMessage( + vscode7.window.showWarningMessage( "Inference toolchain not found. Install it first.", "Install" ).then((action) => { if (action === "Install") { - vscode6.commands.executeCommand( + vscode7.commands.executeCommand( "inference.installToolchain" ); } @@ -1176,7 +1311,7 @@ function registerSelectVersionCommand(outputChannel2) { try { const versions = await fetchVersions(detection.path); if (!versions) { - vscode6.window.showErrorMessage( + vscode7.window.showErrorMessage( "Inference: Failed to fetch available versions." ); return; @@ -1184,12 +1319,12 @@ function registerSelectVersionCommand(outputChannel2) { const currentVersion = await getCurrentVersion(detection.path); const items = buildVersionPickItems(versions, currentVersion); if (items.length === 0) { - vscode6.window.showInformationMessage( + vscode7.window.showInformationMessage( "No toolchain versions available for this platform." ); return; } - const picked = await vscode6.window.showQuickPick(items, { + const picked = await vscode7.window.showQuickPick(items, { placeHolder: "Select toolchain version", matchOnDescription: true }); @@ -1198,7 +1333,7 @@ function registerSelectVersionCommand(outputChannel2) { } const selectedVersion = picked.label; if (selectedVersion === currentVersion) { - vscode6.window.showInformationMessage( + vscode7.window.showInformationMessage( `Already using toolchain v${selectedVersion}.` ); return; @@ -1212,7 +1347,7 @@ function registerSelectVersionCommand(outputChannel2) { } // src/commands/update.ts -var vscode7 = __toESM(require("vscode")); +var vscode8 = __toESM(require("vscode")); // src/toolchain/updateCheck.ts function checkUpdateAvailable(currentVersion, versions) { @@ -1243,23 +1378,23 @@ function checkUpdateAvailable(currentVersion, versions) { // src/commands/update.ts var updating = false; function registerUpdateCommand(outputChannel2) { - return vscode7.commands.registerCommand( + return vscode8.commands.registerCommand( "inference.updateToolchain", async () => { if (updating) { - vscode7.window.showInformationMessage( + vscode8.window.showInformationMessage( "Update check is already in progress." ); return; } const detection = detectInfs(); if (!detection) { - vscode7.window.showWarningMessage( + vscode8.window.showWarningMessage( "Inference toolchain not found. Install it first.", "Install" ).then((action) => { if (action === "Install") { - vscode7.commands.executeCommand( + vscode8.commands.executeCommand( "inference.installToolchain" ); } @@ -1295,7 +1430,7 @@ async function checkForUpdatesImpl(infsPath, outputChannel2, userInitiated) { if (!currentVersion) { outputChannel2.appendLine("Update check: could not determine current version."); if (userInitiated) { - vscode7.window.showErrorMessage( + vscode8.window.showErrorMessage( "Inference: Could not determine the current toolchain version." ); } @@ -1306,7 +1441,7 @@ async function checkForUpdatesImpl(infsPath, outputChannel2, userInitiated) { if (!versions) { outputChannel2.appendLine("Update check: failed to fetch available versions."); if (userInitiated) { - vscode7.window.showErrorMessage( + vscode8.window.showErrorMessage( "Inference: Failed to check for updates." ); } @@ -1317,7 +1452,7 @@ async function checkForUpdatesImpl(infsPath, outputChannel2, userInitiated) { case "no-current-version": outputChannel2.appendLine("Update check: could not determine current version."); if (userInitiated) { - vscode7.window.showErrorMessage( + vscode8.window.showErrorMessage( "Inference: Could not determine the current toolchain version." ); } @@ -1325,7 +1460,7 @@ async function checkForUpdatesImpl(infsPath, outputChannel2, userInitiated) { case "no-versions": outputChannel2.appendLine("Update check: no versions available for this platform."); if (userInitiated) { - vscode7.window.showInformationMessage( + vscode8.window.showInformationMessage( "Inference: No toolchain versions available for this platform." ); } @@ -1335,7 +1470,7 @@ async function checkForUpdatesImpl(infsPath, outputChannel2, userInitiated) { `Update check: toolchain is up to date (v${result.version}).` ); if (userInitiated) { - vscode7.window.showInformationMessage( + vscode8.window.showInformationMessage( `Inference toolchain is up to date (v${result.version}).` ); } @@ -1344,7 +1479,7 @@ async function checkForUpdatesImpl(infsPath, outputChannel2, userInitiated) { outputChannel2.appendLine( `Update check: v${result.latest} available (current: v${result.current}).` ); - const action = await vscode7.window.showInformationMessage( + const action = await vscode8.window.showInformationMessage( `Inference toolchain update available: v${result.latest} (current: v${result.current})`, "Update", "Release Notes" @@ -1352,8 +1487,8 @@ async function checkForUpdatesImpl(infsPath, outputChannel2, userInitiated) { if (action === "Update") { await performVersionChange(infsPath, result.latest, outputChannel2, "Updating to"); } else if (action === "Release Notes") { - vscode7.env.openExternal( - vscode7.Uri.parse( + vscode8.env.openExternal( + vscode8.Uri.parse( `https://github.com/Inferara/inference/releases/tag/v${result.latest}` ) ); @@ -1364,10 +1499,10 @@ async function checkForUpdatesImpl(infsPath, outputChannel2, userInitiated) { } // src/ui/configTree.ts -var vscode8 = __toESM(require("vscode")); +var vscode9 = __toESM(require("vscode")); init_home(); init_exec(); -var ConfigItem = class extends vscode8.TreeItem { +var ConfigItem = class extends vscode9.TreeItem { constructor(label, kind, collapsible, groupId, settingKey, copyValue) { super(label, collapsible); this.kind = kind; @@ -1375,7 +1510,7 @@ var ConfigItem = class extends vscode8.TreeItem { this.settingKey = settingKey; this.copyValue = copyValue; if (kind === "group") { - this.iconPath = new vscode8.ThemeIcon( + this.iconPath = new vscode9.ThemeIcon( groupId === "toolchain" ? "tools" : "gear" ); } @@ -1390,9 +1525,13 @@ var ConfigItem = class extends vscode8.TreeItem { this.contextValue = "inference.configPath"; } } + kind; + groupId; + settingKey; + copyValue; }; var InferenceConfigProvider = class { - _onDidChangeTreeData = new vscode8.EventEmitter(); + _onDidChangeTreeData = new vscode9.EventEmitter(); onDidChangeTreeData = this._onDidChangeTreeData.event; detection = null; version = null; @@ -1415,13 +1554,13 @@ var InferenceConfigProvider = class { new ConfigItem( "Toolchain", "group", - vscode8.TreeItemCollapsibleState.Expanded, + vscode9.TreeItemCollapsibleState.Expanded, "toolchain" ), new ConfigItem( "Settings", "group", - vscode8.TreeItemCollapsibleState.Expanded, + vscode9.TreeItemCollapsibleState.Expanded, "settings" ) ]; @@ -1441,9 +1580,9 @@ var InferenceConfigProvider = class { const item = new ConfigItem( "infs: not found", "property", - vscode8.TreeItemCollapsibleState.None + vscode9.TreeItemCollapsibleState.None ); - item.iconPath = new vscode8.ThemeIcon("error"); + item.iconPath = new vscode9.ThemeIcon("error"); item.command = { title: "Install Toolchain", command: "inference.installToolchain", @@ -1455,49 +1594,49 @@ var InferenceConfigProvider = class { const infsItem = new ConfigItem( `infs: ${detection.path} (${detection.source})`, "property", - vscode8.TreeItemCollapsibleState.None, + vscode9.TreeItemCollapsibleState.None, void 0, void 0, detection.path ); - infsItem.iconPath = new vscode8.ThemeIcon("file-binary"); + infsItem.iconPath = new vscode9.ThemeIcon("file-binary"); items.push(infsItem); const version = await this.resolveVersion(detection.path); const versionItem = new ConfigItem( `Version: ${version ?? "unknown"}`, "property", - vscode8.TreeItemCollapsibleState.None + vscode9.TreeItemCollapsibleState.None ); - versionItem.iconPath = new vscode8.ThemeIcon("tag"); + versionItem.iconPath = new vscode9.ThemeIcon("tag"); items.push(versionItem); const home = inferenceHome(); const homeIsDefault = !process.env["INFERENCE_HOME"]; const homeItem = new ConfigItem( `Home: ${home} (${homeIsDefault ? "default" : "env"})`, "property", - vscode8.TreeItemCollapsibleState.None, + vscode9.TreeItemCollapsibleState.None, void 0, void 0, home ); - homeItem.iconPath = new vscode8.ThemeIcon("home"); + homeItem.iconPath = new vscode9.ThemeIcon("home"); items.push(homeItem); const platform2 = detectPlatform(); const platformItem = new ConfigItem( `Platform: ${platform2?.id ?? "unknown"}`, "property", - vscode8.TreeItemCollapsibleState.None + vscode9.TreeItemCollapsibleState.None ); - platformItem.iconPath = new vscode8.ThemeIcon("device-desktop"); + platformItem.iconPath = new vscode9.ThemeIcon("device-desktop"); items.push(platformItem); const status = this.doctorResult ? this.doctorResult.hasErrors ? "errors" : this.doctorResult.hasWarnings ? "warnings" : "healthy" : "unknown"; const statusIcon = this.doctorResult ? this.doctorResult.hasErrors ? "error" : this.doctorResult.hasWarnings ? "warning" : "pass" : "question"; const statusItem = new ConfigItem( `Status: ${status}`, "property", - vscode8.TreeItemCollapsibleState.None + vscode9.TreeItemCollapsibleState.None ); - statusItem.iconPath = new vscode8.ThemeIcon(statusIcon); + statusItem.iconPath = new vscode9.ThemeIcon(statusIcon); statusItem.command = { title: "Run Doctor", command: "inference.runDoctor", @@ -1511,27 +1650,27 @@ var InferenceConfigProvider = class { const pathItem = new ConfigItem( `Path: ${settings.path || "(auto-detect)"}`, "property", - vscode8.TreeItemCollapsibleState.None, + vscode9.TreeItemCollapsibleState.None, void 0, "inference.path" ); - pathItem.iconPath = new vscode8.ThemeIcon("file-symlink-directory"); + pathItem.iconPath = new vscode9.ThemeIcon("file-symlink-directory"); const autoInstallItem = new ConfigItem( `Auto Install: ${settings.autoInstall ? "enabled" : "disabled"}`, "property", - vscode8.TreeItemCollapsibleState.None, + vscode9.TreeItemCollapsibleState.None, void 0, "inference.autoInstall" ); - autoInstallItem.iconPath = new vscode8.ThemeIcon("cloud-download"); + autoInstallItem.iconPath = new vscode9.ThemeIcon("cloud-download"); const updateItem = new ConfigItem( `Check for Updates: ${settings.checkForUpdates ? "enabled" : "disabled"}`, "property", - vscode8.TreeItemCollapsibleState.None, + vscode9.TreeItemCollapsibleState.None, void 0, "inference.checkForUpdates" ); - updateItem.iconPath = new vscode8.ThemeIcon("sync"); + updateItem.iconPath = new vscode9.ThemeIcon("sync"); return [pathItem, autoInstallItem, updateItem]; } async resolveVersion(infsPath) { @@ -1561,13 +1700,13 @@ var InferenceConfigProvider = class { // src/extension.ts init_doctor(); var MIN_INFS_VERSION = "0.0.1-beta.1"; -var outputChannel = vscode9.window.createOutputChannel("Inference", { log: true }); +var outputChannel = vscode10.window.createOutputChannel("Inference", { log: true }); function activate(context) { context.subscriptions.push(outputChannel); const statusBarItem = createStatusBar(); context.subscriptions.push(statusBarItem); context.subscriptions.push( - vscode9.commands.registerCommand("inference.showOutput", () => { + vscode10.commands.registerCommand("inference.showOutput", () => { outputChannel.show(); }) ); @@ -1575,31 +1714,32 @@ function activate(context) { context.subscriptions.push( registerDoctorCommand(outputChannel, statusBarItem) ); + context.subscriptions.push(registerInstallComponentCommand(outputChannel)); context.subscriptions.push(registerUpdateCommand(outputChannel)); context.subscriptions.push(registerSelectVersionCommand(outputChannel)); const configProvider = new InferenceConfigProvider(); - const configView = vscode9.window.createTreeView("inference.configView", { + const configView = vscode10.window.createTreeView("inference.configView", { treeDataProvider: configProvider }); context.subscriptions.push(configView); context.subscriptions.push(configProvider); context.subscriptions.push( - vscode9.commands.registerCommand("inference.refreshConfigView", () => { + vscode10.commands.registerCommand("inference.refreshConfigView", () => { configProvider.refresh(); }) ); context.subscriptions.push( - vscode9.commands.registerCommand("inference.applyTerminalPath", () => { + vscode10.commands.registerCommand("inference.applyTerminalPath", () => { applyTerminalPath(context); }) ); context.subscriptions.push( - vscode9.commands.registerCommand( + vscode10.commands.registerCommand( "inference.copyConfigValue", (item) => { if (item.copyValue) { - vscode9.env.clipboard.writeText(item.copyValue); - vscode9.window.showInformationMessage( + vscode10.env.clipboard.writeText(item.copyValue); + vscode10.window.showInformationMessage( `Copied: ${item.copyValue}` ); } @@ -1607,31 +1747,31 @@ function activate(context) { ) ); context.subscriptions.push( - vscode9.commands.registerCommand( + vscode10.commands.registerCommand( "inference.revealConfigPath", (item) => { if (item.copyValue) { - vscode9.commands.executeCommand( + vscode10.commands.executeCommand( "revealFileInOS", - vscode9.Uri.file(item.copyValue) + vscode10.Uri.file(item.copyValue) ); } } ) ); context.subscriptions.push( - vscode9.workspace.onDidChangeConfiguration((e) => { + vscode10.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("inference")) { configProvider.refresh(); } }) ); context.subscriptions.push( - vscode9.commands.registerCommand("inference.resetPathAcceptance", () => { + vscode10.commands.registerCommand("inference.resetPathAcceptance", () => { const home = inferenceHome(); const stateKey = `${PATH_FALLBACK_KEY}:${home}`; context.globalState.update(stateKey, void 0); - vscode9.window.showInformationMessage( + vscode10.window.showInformationMessage( "Inference: PATH fallback preference has been reset." ); }) @@ -1668,14 +1808,14 @@ async function checkToolchain(context, statusBarItem, configProvider) { `INFS_DIST_SERVER: ${distServer ?? "(not set, using production)"}` ); updateStatusBar(statusBarItem, null); - vscode9.commands.executeCommand("setContext", "inference.toolchainInstalled", false); - vscode9.window.showWarningMessage( + vscode10.commands.executeCommand("setContext", "inference.toolchainInstalled", false); + vscode10.window.showWarningMessage( `Inference: unsupported platform (${process.platform}-${process.arch}).`, "Download Page" ).then((action) => { if (action === "Download Page") { - vscode9.env.openExternal( - vscode9.Uri.parse( + vscode10.env.openExternal( + vscode10.Uri.parse( "https://github.com/Inferara/inference/releases" ) ); @@ -1695,7 +1835,7 @@ async function checkToolchain(context, statusBarItem, configProvider) { outputChannel.error("infs binary: not found"); outputChannel.error("Toolchain status: errors"); updateStatusBar(statusBarItem, null); - vscode9.commands.executeCommand("setContext", "inference.toolchainInstalled", false); + vscode10.commands.executeCommand("setContext", "inference.toolchainInstalled", false); notifyMissing(); return; } @@ -1717,13 +1857,13 @@ async function checkToolchain(context, statusBarItem, configProvider) { outputChannel.warn( `INFERENCE_HOME is set to ${home} but infs was not found there. Using binary from PATH instead.` ); - vscode9.window.showWarningMessage( + vscode10.window.showWarningMessage( `Inference: infs binary not found in INFERENCE_HOME (${home}). Found via PATH instead.`, "Install", "Dismiss" ).then((action) => { if (action === "Install") { - vscode9.commands.executeCommand("inference.installToolchain"); + vscode10.commands.executeCommand("inference.installToolchain"); } else { context.globalState.update(stateKey, "*"); } @@ -1734,10 +1874,10 @@ async function checkToolchain(context, statusBarItem, configProvider) { if (!versionOk) { outputChannel.error("Toolchain status: errors"); updateStatusBar(statusBarItem, null); - vscode9.commands.executeCommand("setContext", "inference.toolchainInstalled", false); + vscode10.commands.executeCommand("setContext", "inference.toolchainInstalled", false); return; } - vscode9.commands.executeCommand("setContext", "inference.toolchainInstalled", true); + vscode10.commands.executeCommand("setContext", "inference.toolchainInstalled", true); const doctorResult = await runDoctor(detection.path); updateStatusBar(statusBarItem, doctorResult); configProvider.refresh(detection, doctorResult); @@ -1775,12 +1915,12 @@ async function checkInfsVersion(infsPath) { outputChannel.warn( `infs version ${version} is below minimum ${MIN_INFS_VERSION}.` ); - vscode9.window.showWarningMessage( + vscode10.window.showWarningMessage( `Inference: infs version ${version} is outdated (minimum: ${MIN_INFS_VERSION}). Please update.`, "Update" ).then((action) => { if (action === "Update") { - vscode9.commands.executeCommand("inference.updateToolchain"); + vscode10.commands.executeCommand("inference.updateToolchain"); } }); return false; @@ -1792,22 +1932,22 @@ async function checkInfsVersion(infsPath) { } } function notifyMissing() { - vscode9.window.showInformationMessage( + vscode10.window.showInformationMessage( "Inference toolchain not found. Would you like to install it?", "Install", "Download Manually", "Configure Path" ).then((action) => { if (action === "Install") { - vscode9.commands.executeCommand("inference.installToolchain"); + vscode10.commands.executeCommand("inference.installToolchain"); } else if (action === "Download Manually") { - vscode9.env.openExternal( - vscode9.Uri.parse( + vscode10.env.openExternal( + vscode10.Uri.parse( "https://github.com/Inferara/inference/releases" ) ); } else if (action === "Configure Path") { - vscode9.commands.executeCommand( + vscode10.commands.executeCommand( "workbench.action.openSettings", "inference.path" ); diff --git a/editors/vscode/dist/extension.js.map b/editors/vscode/dist/extension.js.map index 2ebe3101..230e8f0e 100644 --- a/editors/vscode/dist/extension.js.map +++ b/editors/vscode/dist/extension.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../src/toolchain/home.ts", "../src/utils/exec.ts", "../src/toolchain/doctor.ts", "../src/extension.ts", "../src/toolchain/platform.ts", "../src/toolchain/detection.ts", "../src/config/settings.ts", "../src/utils/semver.ts", "../src/commands/install.ts", "../src/toolchain/installation.ts", "../src/utils/download.ts", "../src/utils/extract.ts", "../src/toolchain/manifest.ts", "../src/ui/statusBar.ts", "../src/ui/statusBarState.ts", "../src/commands/doctor.ts", "../src/toolchain/doctorFormat.ts", "../src/commands/selectVersion.ts", "../src/toolchain/versions.ts", "../src/toolchain/versionPicker.ts", "../src/commands/versionChange.ts", "../src/commands/update.ts", "../src/toolchain/updateCheck.ts", "../src/ui/configTree.ts"], - "sourcesContent": ["import * as os from 'os';\nimport * as path from 'path';\n\n/** Resolve the INFERENCE_HOME directory (default: ~/.inference). */\nexport function inferenceHome(): string {\n return process.env['INFERENCE_HOME'] || path.join(os.homedir(), '.inference');\n}\n", "import * as cp from 'child_process';\n\nexport interface ExecResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/** Default timeout for child processes (30 seconds). */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Run a command and capture its output.\n *\n * Resolves with ExecResult on completion (including non-zero exit).\n * Rejects only on spawn failure or timeout.\n */\nexport function exec(\n command: string,\n args: string[],\n options?: { timeoutMs?: number; cwd?: string; env?: Record },\n): Promise {\n const timeout = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n return new Promise((resolve, reject) => {\n const child = cp.spawn(command, args, {\n cwd: options?.cwd,\n env: options?.env ? { ...process.env, ...options.env } : undefined,\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout,\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n\n child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));\n child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));\n\n child.on('error', (err) => reject(err));\n\n child.on('close', (code) => {\n resolve({\n exitCode: code ?? 1,\n stdout: Buffer.concat(stdoutChunks).toString('utf-8'),\n stderr: Buffer.concat(stderrChunks).toString('utf-8'),\n });\n });\n });\n}\n", "import * as path from 'path';\nimport { exec } from '../utils/exec';\nimport { inferenceHome } from './home';\n\nexport type DoctorCheckStatus = 'ok' | 'warn' | 'fail';\n\nexport interface DoctorCheck {\n name: string;\n status: DoctorCheckStatus;\n message: string;\n}\n\nexport interface DoctorResult {\n checks: DoctorCheck[];\n hasErrors: boolean;\n hasWarnings: boolean;\n summary: string;\n}\n\nconst STATUS_MAP: Record = {\n OK: 'ok',\n WARN: 'warn',\n FAIL: 'fail',\n};\n\n/**\n * Check line format: ` [OK|WARN|FAIL] : `\n *\n * PATH conflict continuation lines (indented without a status prefix)\n * are intentionally not captured as individual checks.\n */\nconst CHECK_PATTERN = /^\\s+\\[(OK|WARN|FAIL)]\\s+(.+?):\\s+(.*)/;\n\n/**\n * Parse the stdout of `infs doctor` into a structured result.\n *\n * The output format is a public contract defined in\n * `apps/infs/src/commands/doctor.rs`.\n */\nexport function parseDoctorOutput(stdout: string): DoctorResult {\n const checks: DoctorCheck[] = [];\n const lines = stdout.split(/\\r?\\n/);\n\n for (const line of lines) {\n const match = line.match(CHECK_PATTERN);\n if (match) {\n checks.push({\n status: STATUS_MAP[match[1]],\n name: match[2].trim(),\n message: match[3].trim(),\n });\n }\n }\n\n let summary = '';\n for (let i = lines.length - 1; i >= 0; i--) {\n const trimmed = lines[i].trim();\n if (trimmed.length > 0 && !CHECK_PATTERN.test(lines[i])) {\n summary = trimmed;\n break;\n }\n }\n\n return {\n checks,\n hasErrors: checks.some((c) => c.status === 'fail'),\n hasWarnings: checks.some((c) => c.status === 'warn'),\n summary,\n };\n}\n\n/**\n * Execute `infs doctor` and return the parsed result.\n * Returns null if execution fails (e.g., binary not found or crash).\n */\nexport async function runDoctor(\n infsPath: string,\n): Promise {\n try {\n const binDir = path.join(inferenceHome(), 'bin');\n const sep = process.platform === 'win32' ? ';' : ':';\n const augmentedPath = `${binDir}${sep}${process.env['PATH'] ?? ''}`;\n\n const result = await exec(infsPath, ['doctor'], {\n env: { PATH: augmentedPath },\n });\n return parseDoctorOutput(result.stdout);\n } catch (err) {\n console.error('infs doctor failed:', err);\n return null;\n }\n}\n", "import * as vscode from 'vscode';\nimport * as path from 'path';\nimport { detectPlatform } from './toolchain/platform';\nimport { detectInfs, inferenceHome } from './toolchain/detection';\nimport { exec } from './utils/exec';\nimport { compareSemver } from './utils/semver';\nimport { registerInstallCommand } from './commands/install';\nimport { registerDoctorCommand } from './commands/doctor';\nimport { registerSelectVersionCommand } from './commands/selectVersion';\nimport { registerUpdateCommand, checkForUpdates } from './commands/update';\nimport { createStatusBar, updateStatusBar } from './ui/statusBar';\nimport { InferenceConfigProvider, ConfigItem } from './ui/configTree';\nimport { runDoctor } from './toolchain/doctor';\n\n/** Minimum infs CLI version the extension can work with. */\nconst MIN_INFS_VERSION = '0.0.1-beta.1';\n\nconst outputChannel = vscode.window.createOutputChannel('Inference', { log: true });\n\nexport function activate(context: vscode.ExtensionContext) {\n context.subscriptions.push(outputChannel);\n\n const statusBarItem = createStatusBar();\n context.subscriptions.push(statusBarItem);\n\n context.subscriptions.push(\n vscode.commands.registerCommand('inference.showOutput', () => {\n outputChannel.show();\n }),\n );\n\n context.subscriptions.push(registerInstallCommand(outputChannel, statusBarItem));\n context.subscriptions.push(\n registerDoctorCommand(outputChannel, statusBarItem),\n );\n\n context.subscriptions.push(registerUpdateCommand(outputChannel));\n context.subscriptions.push(registerSelectVersionCommand(outputChannel));\n\n const configProvider = new InferenceConfigProvider();\n const configView = vscode.window.createTreeView('inference.configView', {\n treeDataProvider: configProvider,\n });\n context.subscriptions.push(configView);\n context.subscriptions.push(configProvider);\n\n context.subscriptions.push(\n vscode.commands.registerCommand('inference.refreshConfigView', () => {\n configProvider.refresh();\n }),\n );\n\n context.subscriptions.push(\n vscode.commands.registerCommand('inference.applyTerminalPath', () => {\n applyTerminalPath(context);\n }),\n );\n\n context.subscriptions.push(\n vscode.commands.registerCommand(\n 'inference.copyConfigValue',\n (item: ConfigItem) => {\n if (item.copyValue) {\n vscode.env.clipboard.writeText(item.copyValue);\n vscode.window.showInformationMessage(\n `Copied: ${item.copyValue}`,\n );\n }\n },\n ),\n );\n\n context.subscriptions.push(\n vscode.commands.registerCommand(\n 'inference.revealConfigPath',\n (item: ConfigItem) => {\n if (item.copyValue) {\n vscode.commands.executeCommand(\n 'revealFileInOS',\n vscode.Uri.file(item.copyValue),\n );\n }\n },\n ),\n );\n\n context.subscriptions.push(\n vscode.workspace.onDidChangeConfiguration((e) => {\n if (e.affectsConfiguration('inference')) {\n configProvider.refresh();\n }\n }),\n );\n\n context.subscriptions.push(\n vscode.commands.registerCommand('inference.resetPathAcceptance', () => {\n const home = inferenceHome();\n const stateKey = `${PATH_FALLBACK_KEY}:${home}`;\n context.globalState.update(stateKey, undefined);\n vscode.window.showInformationMessage(\n 'Inference: PATH fallback preference has been reset.',\n );\n }),\n );\n\n applyTerminalPath(context);\n\n checkToolchain(context, statusBarItem, configProvider).catch((err) =>\n outputChannel.error(`Toolchain check failed: ${err}`),\n );\n}\n\nexport function deactivate() {\n // Nothing to clean up\n}\n\n/**\n * Prepend `INFERENCE_HOME/bin` to PATH for all integrated terminals.\n *\n * Uses VS Code's `EnvironmentVariableCollection` so that:\n * - New terminals automatically have the toolchain on PATH.\n * - Existing terminals show a relaunch indicator when the collection changes.\n *\n * Exported so that install/update commands can re-apply the mutation to\n * trigger the relaunch indicator on already-open terminals.\n */\nexport function applyTerminalPath(context: vscode.ExtensionContext): void {\n const binDir = path.join(inferenceHome(), 'bin');\n const sep = process.platform === 'win32' ? ';' : ':';\n const env = context.environmentVariableCollection;\n env.prepend('PATH', binDir + sep);\n env.description = 'Adds the Inference toolchain to PATH';\n}\n\n/** globalState key prefix for remembering PATH fallback acceptance. */\nconst PATH_FALLBACK_KEY = 'inference.acceptedPathFallback';\n\nasync function checkToolchain(\n context: vscode.ExtensionContext,\n statusBarItem: vscode.StatusBarItem,\n configProvider: InferenceConfigProvider,\n): Promise {\n const platform = detectPlatform();\n const home = inferenceHome();\n const homeIsDefault = !process.env['INFERENCE_HOME'];\n const distServer = process.env['INFS_DIST_SERVER'];\n\n outputChannel.info('Inference Activation');\n\n if (!platform) {\n outputChannel.warn(\n `Platform: ${process.platform}-${process.arch} (unsupported)`,\n );\n outputChannel.info(\n `INFERENCE_HOME: ${home} ${homeIsDefault ? '(default)' : '(env)'}`,\n );\n outputChannel.info(\n `INFS_DIST_SERVER: ${distServer ?? '(not set, using production)'}`,\n );\n updateStatusBar(statusBarItem, null);\n vscode.commands.executeCommand('setContext', 'inference.toolchainInstalled', false);\n vscode.window\n .showWarningMessage(\n `Inference: unsupported platform (${process.platform}-${process.arch}).`,\n 'Download Page',\n )\n .then((action) => {\n if (action === 'Download Page') {\n vscode.env.openExternal(\n vscode.Uri.parse(\n 'https://github.com/Inferara/inference/releases',\n ),\n );\n }\n });\n return;\n }\n\n outputChannel.info(`Platform: ${platform.id}`);\n outputChannel.info(\n `INFERENCE_HOME: ${home} ${homeIsDefault ? '(default)' : '(env)'}`,\n );\n outputChannel.info(\n `INFS_DIST_SERVER: ${distServer ?? '(not set, using production)'}`,\n );\n\n const detection = detectInfs();\n if (!detection) {\n outputChannel.error('infs binary: not found');\n outputChannel.error('Toolchain status: errors');\n updateStatusBar(statusBarItem, null);\n vscode.commands.executeCommand('setContext', 'inference.toolchainInstalled', false);\n notifyMissing();\n return;\n }\n outputChannel.info(\n `infs binary: ${detection.path} (${detection.source})`,\n );\n\n if (!homeIsDefault && detection.source === 'path') {\n const stateKey = `${PATH_FALLBACK_KEY}:${home}`;\n const accepted = context.globalState.get(stateKey);\n\n if (accepted === '*') {\n outputChannel.info(\n `Note: Using PATH binary (notification permanently suppressed for this INFERENCE_HOME).`,\n );\n } else if (accepted === detection.path) {\n outputChannel.info(\n `Note: Using PATH binary (previously accepted for this INFERENCE_HOME).`,\n );\n } else {\n outputChannel.warn(\n `INFERENCE_HOME is set to ${home} but infs was not found there. Using binary from PATH instead.`,\n );\n vscode.window.showWarningMessage(\n `Inference: infs binary not found in INFERENCE_HOME (${home}). Found via PATH instead.`,\n 'Install',\n 'Dismiss',\n ).then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand('inference.installToolchain');\n } else {\n context.globalState.update(stateKey, '*');\n }\n });\n }\n }\n\n const versionOk = await checkInfsVersion(detection.path);\n if (!versionOk) {\n outputChannel.error('Toolchain status: errors');\n updateStatusBar(statusBarItem, null);\n vscode.commands.executeCommand('setContext', 'inference.toolchainInstalled', false);\n return;\n }\n\n vscode.commands.executeCommand('setContext', 'inference.toolchainInstalled', true);\n\n const doctorResult = await runDoctor(detection.path);\n updateStatusBar(statusBarItem, doctorResult);\n configProvider.refresh(detection, doctorResult);\n\n const status = doctorResult?.hasErrors\n ? 'errors'\n : doctorResult?.hasWarnings\n ? 'warnings'\n : 'healthy';\n if (doctorResult?.hasErrors) {\n outputChannel.error(`Toolchain status: ${status}`);\n } else if (doctorResult?.hasWarnings) {\n outputChannel.warn(`Toolchain status: ${status}`);\n } else {\n outputChannel.info(`Toolchain status: ${status}`);\n }\n\n checkForUpdates(detection.path, outputChannel).catch((err) =>\n outputChannel.error(`Update check failed: ${err}`),\n );\n}\n\n/**\n * Run `infs version` and check the output against MIN_INFS_VERSION.\n * Returns true if version is acceptable.\n */\nasync function checkInfsVersion(infsPath: string): Promise {\n try {\n const result = await exec(infsPath, ['version']);\n if (result.exitCode !== 0) {\n outputChannel.error(\n `infs version failed (exit ${result.exitCode}): ${result.stderr}`,\n );\n return false;\n }\n // Output format: \"infs 0.1.0\"\n const match = result.stdout.match(/^infs\\s+(\\S+)/);\n if (!match) {\n outputChannel.error(\n `Could not parse infs version from: ${result.stdout.trim()}`,\n );\n return false;\n }\n const version = match[1];\n outputChannel.info(`infs version: ${version}`);\n\n if (compareSemver(version, MIN_INFS_VERSION) < 0) {\n outputChannel.warn(\n `infs version ${version} is below minimum ${MIN_INFS_VERSION}.`,\n );\n vscode.window\n .showWarningMessage(\n `Inference: infs version ${version} is outdated (minimum: ${MIN_INFS_VERSION}). Please update.`,\n 'Update',\n )\n .then((action) => {\n if (action === 'Update') {\n vscode.commands.executeCommand('inference.updateToolchain');\n }\n });\n return false;\n }\n return true;\n } catch (err) {\n outputChannel.error(`Failed to run infs version: ${err}`);\n return false;\n }\n}\n\nfunction notifyMissing(): void {\n vscode.window\n .showInformationMessage(\n 'Inference toolchain not found. Would you like to install it?',\n 'Install',\n 'Download Manually',\n 'Configure Path',\n )\n .then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand('inference.installToolchain');\n } else if (action === 'Download Manually') {\n vscode.env.openExternal(\n vscode.Uri.parse(\n 'https://github.com/Inferara/inference/releases',\n ),\n );\n } else if (action === 'Configure Path') {\n vscode.commands.executeCommand(\n 'workbench.action.openSettings',\n 'inference.path',\n );\n }\n });\n}\n", "import * as os from 'os';\n\nexport type PlatformId = 'linux-x64' | 'macos-arm64' | 'windows-x64';\n\nexport interface PlatformInfo {\n id: PlatformId;\n archiveExtension: string;\n binaryName: string;\n}\n\nexport const SUPPORTED_PLATFORMS: Record = {\n 'linux-x64': 'linux-x64',\n 'darwin-arm64': 'macos-arm64',\n 'win32-x64': 'windows-x64',\n};\n\n/**\n * Detect the platform and return its info, or null if unsupported.\n * When osPlatform/osArch are omitted, uses the current runtime values.\n */\nexport function detectPlatform(\n osPlatform?: string,\n osArch?: string,\n): PlatformInfo | null {\n const key = `${osPlatform ?? os.platform()}-${osArch ?? os.arch()}`;\n const id = SUPPORTED_PLATFORMS[key];\n if (!id) {\n return null;\n }\n return {\n id,\n archiveExtension: id === 'windows-x64' ? '.zip' : '.tar.gz',\n binaryName: id === 'windows-x64' ? 'infs.exe' : 'infs',\n };\n}\n", "import * as fs from 'fs';\nimport * as path from 'path';\nimport { getSettings } from '../config/settings';\nimport { detectPlatform } from './platform';\nimport { inferenceHome } from './home';\n\nexport { inferenceHome };\n\n/** Check whether a file exists and is executable (or just exists on Windows). */\nexport function isExecutable(filePath: string): boolean {\n try {\n const mode = process.platform === 'win32'\n ? fs.constants.F_OK\n : fs.constants.X_OK;\n fs.accessSync(filePath, mode);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Search PATH for the given binary name.\n * Returns the first match or null.\n */\nexport function findInPath(binaryName: string): string | null {\n const envPath = process.env['PATH'] || '';\n const sep = process.platform === 'win32' ? ';' : ':';\n const dirs = envPath.split(sep).filter(Boolean);\n for (const dir of dirs) {\n const candidate = path.join(dir, binaryName);\n if (isExecutable(candidate)) {\n return candidate;\n }\n }\n return null;\n}\n\n/** Source where the infs binary was found. */\nexport type InfsSource = 'settings' | 'managed' | 'path';\n\n/** Result of infs binary detection. */\nexport interface InfsDetection {\n path: string;\n source: InfsSource;\n}\n\n/**\n * Detect infs binary location.\n *\n * Search order:\n * 1. Custom path from settings (inference.path)\n * 2. Managed location (INFERENCE_HOME/bin/infs)\n * 3. System PATH\n *\n * Returns the resolved absolute path and source, or null if not found.\n */\nexport function detectInfs(): InfsDetection | null {\n const platform = detectPlatform();\n const binaryName = platform?.binaryName ?? 'infs';\n\n const settings = getSettings();\n if (settings.path) {\n if (isExecutable(settings.path)) {\n return { path: settings.path, source: 'settings' };\n }\n return null;\n }\n\n const managedPath = path.join(inferenceHome(), 'bin', binaryName);\n if (isExecutable(managedPath)) {\n return { path: managedPath, source: 'managed' };\n }\n\n const pathResult = findInPath(binaryName);\n if (pathResult) {\n return { path: pathResult, source: 'path' };\n }\n\n return null;\n}\n", "import * as vscode from 'vscode';\n\nexport interface InferenceSettings {\n /** Custom path to infs binary. Empty string means auto-detect. */\n path: string;\n /** Prompt to install toolchain if not found. */\n autoInstall: boolean;\n /** Check for toolchain updates on activation. */\n checkForUpdates: boolean;\n}\n\n/** Read current inference.* configuration values. */\nexport function getSettings(): InferenceSettings {\n const config = vscode.workspace.getConfiguration('inference');\n return {\n path: config.get('path', ''),\n autoInstall: config.get('autoInstall', true),\n checkForUpdates: config.get('checkForUpdates', true),\n };\n}\n", "/**\n * Compare two semver strings. Returns negative if a < b, 0 if equal, positive if a > b.\n * Handles numeric major.minor.patch and pre-release tags per the SemVer 2.0.0 spec.\n *\n * Pre-release versions have lower precedence than the associated normal version:\n * 1.0.0-alpha < 1.0.0\n *\n * Pre-release identifiers are compared left-to-right:\n * - Numeric identifiers are compared as integers.\n * - Alphanumeric identifiers are compared lexically (ASCII order).\n * - Numeric identifiers always have lower precedence than alphanumeric.\n * - A shorter set of identifiers has lower precedence if all preceding are equal.\n */\nexport function compareSemver(a: string, b: string): number {\n const clean = (v: string) => v.replace(/^v/i, '');\n const [coreA, preA] = clean(a).split('-', 2);\n const [coreB, preB] = clean(b).split('-', 2);\n\n const pa = coreA.split('.').map(Number);\n const pb = coreB.split('.').map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (pa[i] || 0) - (pb[i] || 0);\n if (diff !== 0) {\n return diff;\n }\n }\n\n if (!preA && !preB) {\n return 0;\n }\n if (preA && !preB) {\n return -1;\n }\n if (!preA && preB) {\n return 1;\n }\n\n const partsA = preA!.split('.');\n const partsB = preB!.split('.');\n const len = Math.max(partsA.length, partsB.length);\n for (let i = 0; i < len; i++) {\n if (i >= partsA.length) {\n return -1;\n }\n if (i >= partsB.length) {\n return 1;\n }\n const numA = Number(partsA[i]);\n const numB = Number(partsB[i]);\n const aIsNum = !Number.isNaN(numA);\n const bIsNum = !Number.isNaN(numB);\n if (aIsNum && bIsNum) {\n if (numA !== numB) {\n return numA - numB;\n }\n } else if (aIsNum) {\n return -1;\n } else if (bIsNum) {\n return 1;\n } else {\n if (partsA[i] < partsB[i]) {\n return -1;\n }\n if (partsA[i] > partsB[i]) {\n return 1;\n }\n }\n }\n return 0;\n}\n", "import * as vscode from 'vscode';\nimport { detectPlatform, PlatformInfo } from '../toolchain/platform';\nimport {\n installToolchain,\n InstallProgress,\n InstallProgressCallback,\n InstallResult,\n} from '../toolchain/installation';\nimport { runDoctor } from '../toolchain/doctor';\nimport { updateStatusBar } from '../ui/statusBar';\n\n/** Guard against concurrent install attempts. */\nlet installing = false;\n\n/**\n * Register the inference.installToolchain command.\n * Returns the Disposable to add to context.subscriptions.\n */\nexport function registerInstallCommand(\n outputChannel: vscode.OutputChannel,\n statusBarItem: vscode.StatusBarItem,\n): vscode.Disposable {\n return vscode.commands.registerCommand(\n 'inference.installToolchain',\n async () => {\n if (installing) {\n vscode.window.showInformationMessage(\n 'Inference toolchain installation is already in progress.',\n );\n return;\n }\n\n const platform = detectPlatform();\n if (!platform) {\n vscode.window.showErrorMessage(\n `Inference: unsupported platform (${process.platform}-${process.arch}).`,\n );\n return;\n }\n\n installing = true;\n try {\n const result = await installWithProgress(\n platform,\n outputChannel,\n );\n outputChannel.appendLine(\n `Toolchain v${result.version} installed at ${result.infsPath}`,\n );\n vscode.commands.executeCommand(\n 'setContext', 'inference.toolchainInstalled', true,\n );\n\n const doctorResult = await runDoctor(result.infsPath);\n updateStatusBar(statusBarItem, doctorResult);\n vscode.commands.executeCommand('inference.refreshConfigView');\n vscode.commands.executeCommand('inference.applyTerminalPath');\n\n notifyInstallSuccess(result.version, result.doctorWarnings);\n } catch (err) {\n const message =\n err instanceof Error ? err.message : String(err);\n outputChannel.appendLine(`Installation failed: ${message}`);\n notifyInstallError(message);\n } finally {\n installing = false;\n }\n },\n );\n}\n\n/** Run the installation with a VS Code progress notification. */\nfunction installWithProgress(\n platform: PlatformInfo,\n outputChannel: vscode.OutputChannel,\n): Promise {\n return vscode.window.withProgress(\n {\n location: vscode.ProgressLocation.Notification,\n title: 'Inference Toolchain',\n cancellable: false,\n },\n async (progress) => {\n const onProgress: InstallProgressCallback = (\n p: InstallProgress,\n ) => {\n outputChannel.appendLine(p.message);\n if (p.stage === 'downloading' && p.bytesTotal) {\n const pct = Math.round(\n ((p.bytesReceived ?? 0) / p.bytesTotal) * 100,\n );\n progress.report({ message: `${p.message} (${pct}%)` });\n } else {\n progress.report({ message: p.message });\n }\n };\n return installToolchain(platform, onProgress);\n },\n );\n}\n\n/** Show a notification that the toolchain was installed successfully. */\nfunction notifyInstallSuccess(\n version: string,\n doctorWarnings: boolean,\n): void {\n if (doctorWarnings) {\n vscode.window\n .showWarningMessage(\n `Inference toolchain v${version} installed, but doctor reported issues. See output for details.`,\n 'Show Output',\n )\n .then((action) => {\n if (action === 'Show Output') {\n vscode.commands.executeCommand('inference.showOutput');\n }\n });\n } else {\n vscode.window.showInformationMessage(\n `Inference toolchain v${version} installed successfully.`,\n );\n }\n}\n\n/** Show an error notification for installation failure. */\nfunction notifyInstallError(errorMessage: string): void {\n vscode.window\n .showErrorMessage(\n `Inference toolchain installation failed: ${errorMessage}`,\n 'Retry',\n 'Download Manually',\n 'Settings',\n )\n .then((action) => {\n if (action === 'Retry') {\n vscode.commands.executeCommand('inference.installToolchain');\n } else if (action === 'Download Manually') {\n vscode.env.openExternal(\n vscode.Uri.parse(\n 'https://github.com/Inferara/inference/releases',\n ),\n );\n } else if (action === 'Settings') {\n vscode.commands.executeCommand(\n 'workbench.action.openSettings',\n 'inference.path',\n );\n }\n });\n}\n", "import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { PlatformInfo } from './platform';\nimport { inferenceHome } from './home';\nimport { fetchJson, downloadFile, sha256File } from '../utils/download';\nimport { extractArchive } from '../utils/extract';\nimport { exec } from '../utils/exec';\nimport {\n ReleaseEntry,\n findLatestRelease,\n} from './manifest';\n\nexport type { FileEntry, ReleaseEntry } from './manifest';\nexport { findLatestRelease } from './manifest';\n\n/** Progress updates emitted during installation. */\nexport interface InstallProgress {\n stage:\n | 'fetching-manifest'\n | 'downloading'\n | 'extracting'\n | 'installing'\n | 'verifying';\n message: string;\n bytesReceived?: number;\n bytesTotal?: number;\n}\n\nexport type InstallProgressCallback = (progress: InstallProgress) => void;\n\n/** Result of a successful installation. */\nexport interface InstallResult {\n infsPath: string;\n version: string;\n doctorWarnings: boolean;\n}\n\nconst DEFAULT_DIST_SERVER = 'https://inference-lang.org';\nconst RELEASES_PATH = '/releases.json';\n\nfunction manifestUrl(): string {\n const server = process.env['INFS_DIST_SERVER']?.trim();\n const base = server && server.length > 0\n ? server.replace(/\\/+$/, '')\n : DEFAULT_DIST_SERVER;\n return `${base}${RELEASES_PATH}`;\n}\n\n/**\n * Run the full installation flow.\n * Fetches manifest, downloads infs, extracts, runs `infs install`, verifies with `infs doctor`.\n * Throws on failure with a descriptive error message.\n */\nexport async function installToolchain(\n platform: PlatformInfo,\n onProgress?: InstallProgressCallback,\n): Promise {\n onProgress?.({\n stage: 'fetching-manifest',\n message: 'Fetching release manifest...',\n });\n\n const manifest = await fetchJson(manifestUrl());\n\n if (!Array.isArray(manifest)) {\n throw new Error('Invalid release manifest: expected an array.');\n }\n for (const entry of manifest) {\n if (\n typeof entry?.version !== 'string' ||\n typeof entry?.stable !== 'boolean' ||\n !Array.isArray(entry?.files)\n ) {\n throw new Error(\n `Invalid release manifest entry: ${JSON.stringify(entry)?.slice(0, 200)}`,\n );\n }\n }\n\n const match = findLatestRelease(manifest, platform);\n if (!match) {\n throw new Error(\n `No compatible infs release found for ${platform.id}.`,\n );\n }\n\n const { release, fileUrl, sha256 } = match;\n const version = release.version;\n\n onProgress?.({\n stage: 'downloading',\n message: `Downloading infs v${version}...`,\n });\n\n const destDir = path.join(inferenceHome(), 'bin');\n fs.mkdirSync(destDir, { recursive: true });\n\n const archiveName = `infs-${platform.id}${platform.archiveExtension}`;\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'infs-'));\n const archivePath = path.join(tmpDir, archiveName);\n\n try {\n await downloadFile(fileUrl, {\n destPath: archivePath,\n onProgress: (received, total) => {\n onProgress?.({\n stage: 'downloading',\n message: `Downloading infs v${version}...`,\n bytesReceived: received,\n bytesTotal: total,\n });\n },\n });\n\n const actualHash = await sha256File(archivePath);\n if (actualHash !== sha256) {\n throw new Error(\n `SHA-256 verification failed for infs v${version}. Expected ${sha256}, got ${actualHash}.`,\n );\n }\n\n onProgress?.({\n stage: 'extracting',\n message: 'Extracting archive...',\n });\n\n await extractArchive({ archivePath, destDir });\n } finally {\n try {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n } catch {\n // best-effort cleanup\n }\n }\n\n const infsPath = path.join(destDir, platform.binaryName);\n if (!fs.existsSync(infsPath)) {\n throw new Error(\n `infs binary not found at ${infsPath} after extraction.`,\n );\n }\n\n onProgress?.({\n stage: 'installing',\n message: 'Running infs install...',\n });\n\n const binDir = path.join(inferenceHome(), 'bin');\n const sep = process.platform === 'win32' ? ';' : ':';\n const augmentedPath = `${binDir}${sep}${process.env['PATH'] ?? ''}`;\n\n const installResult = await exec(infsPath, ['install'], {\n timeoutMs: 120_000,\n env: { PATH: augmentedPath },\n });\n if (installResult.exitCode !== 0) {\n throw new Error(\n `infs install failed (exit ${installResult.exitCode}): ${installResult.stderr || installResult.stdout}`,\n );\n }\n\n onProgress?.({\n stage: 'verifying',\n message: 'Verifying installation...',\n });\n\n let doctorWarnings = false;\n try {\n const doctorResult = await exec(infsPath, ['doctor'], {\n timeoutMs: 30_000,\n env: { PATH: augmentedPath },\n });\n if (doctorResult.exitCode !== 0) {\n doctorWarnings = true;\n } else {\n const { parseDoctorOutput } = await import('./doctor');\n const parsed = parseDoctorOutput(doctorResult.stdout);\n if (parsed.hasErrors || parsed.hasWarnings) {\n doctorWarnings = true;\n }\n }\n } catch {\n doctorWarnings = true;\n }\n\n return { infsPath, version, doctorWarnings };\n}\n", "import * as https from 'https';\nimport * as http from 'http';\nimport * as fs from 'fs';\nimport * as crypto from 'crypto';\n\n/** Callback invoked during download with bytes received and total (if known). */\nexport type ProgressCallback = (\n received: number,\n total: number | undefined,\n) => void;\n\nexport interface DownloadOptions {\n /** Absolute path where the downloaded file will be saved. */\n destPath: string;\n /** Optional progress callback. */\n onProgress?: ProgressCallback;\n /** Connection timeout in milliseconds (default: 15000). */\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 15_000;\nconst MAX_REDIRECTS = 5;\n\nconst SOCKET_TIMEOUT_MS = 15_000;\nconst MAX_JSON_RESPONSE_BYTES = 10 * 1024 * 1024;\n\n/**\n * Perform an HTTPS GET request following redirects.\n * Rejects HTTPS-to-HTTP downgrades.\n */\nfunction followRedirects(\n url: string,\n remaining: number,\n): Promise {\n return new Promise((resolve, reject) => {\n const parsed = new URL(url);\n const requester = parsed.protocol === 'https:' ? https : http;\n\n const req = requester.get(url, (res) => {\n const status = res.statusCode ?? 0;\n\n if (status >= 300 && status < 400 && res.headers.location) {\n if (remaining <= 0) {\n res.resume();\n reject(new Error(`Too many redirects fetching ${url}`));\n return;\n }\n const target = new URL(res.headers.location, url).href;\n const targetProtocol = new URL(target).protocol;\n if (parsed.protocol === 'https:' && targetProtocol === 'http:') {\n res.resume();\n reject(\n new Error(\n `Refusing HTTPS-to-HTTP redirect: ${url} -> ${target}`,\n ),\n );\n return;\n }\n res.resume();\n followRedirects(target, remaining - 1).then(resolve, reject);\n return;\n }\n\n if (status < 200 || status >= 300) {\n res.resume();\n reject(new Error(`HTTP ${status} fetching ${url}`));\n return;\n }\n\n resolve(res);\n });\n\n req.setTimeout(SOCKET_TIMEOUT_MS, () => {\n req.destroy(new Error(`Connection timed out for ${url}`));\n });\n\n req.on('error', (err) =>\n reject(new Error(`Network error fetching ${url}: ${err.message}`)),\n );\n });\n}\n\n/**\n * Fetch a JSON document from a URL via HTTPS GET.\n * Follows up to 5 redirects. Rejects HTTPS-to-HTTP downgrades.\n */\nexport function fetchJson(url: string): Promise {\n return new Promise((resolve, reject) => {\n followRedirects(url, MAX_REDIRECTS).then(\n (res) => {\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n res.on('data', (chunk: Buffer) => {\n totalBytes += chunk.length;\n if (totalBytes > MAX_JSON_RESPONSE_BYTES) {\n res.destroy();\n reject(new Error(`Response too large (>${MAX_JSON_RESPONSE_BYTES} bytes) from ${url}`));\n return;\n }\n chunks.push(chunk);\n });\n res.on('end', () => {\n try {\n const text = Buffer.concat(chunks).toString('utf-8');\n resolve(JSON.parse(text) as T);\n } catch (err) {\n reject(\n new Error(\n `Failed to parse JSON from ${url}: ${err instanceof Error ? err.message : err}`,\n ),\n );\n }\n });\n res.on('error', (err) =>\n reject(\n new Error(\n `Error reading response from ${url}: ${err.message}`,\n ),\n ),\n );\n },\n (err) => reject(err),\n );\n });\n}\n\n/**\n * Download a file from a URL to destPath using streaming.\n * Uses a temp file (.partial suffix) and renames on completion.\n * Follows redirects (GitHub releases redirect to CDN).\n */\nexport function downloadFile(\n url: string,\n options: DownloadOptions,\n): Promise {\n const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const partialPath = options.destPath + '.partial';\n\n return new Promise((resolve, reject) => {\n let settled = false;\n const settle = (fn: (...args: T) => void, ...args: T) => {\n if (!settled) {\n settled = true;\n fn(...args);\n }\n };\n\n followRedirects(url, MAX_REDIRECTS).then(\n (res) => {\n const totalStr = res.headers['content-length'];\n const total = totalStr ? parseInt(totalStr, 10) : undefined;\n let received = 0;\n\n const ws = fs.createWriteStream(partialPath);\n\n res.on('data', (chunk: Buffer) => {\n received += chunk.length;\n options.onProgress?.(received, total);\n });\n\n res.pipe(ws);\n\n const cleanup = () => {\n try {\n fs.unlinkSync(partialPath);\n } catch {\n // ignore\n }\n };\n\n // No-data timeout: if no bytes arrive for `timeout` ms, abort\n let dataTimer: ReturnType | undefined;\n const clearDataTimer = () => {\n if (dataTimer) {\n clearTimeout(dataTimer);\n dataTimer = undefined;\n }\n };\n const resetTimer = () => {\n clearDataTimer();\n dataTimer = setTimeout(() => {\n res.destroy();\n ws.destroy();\n cleanup();\n settle(reject, new Error(`Download timed out for ${url}`));\n }, timeout);\n };\n resetTimer();\n res.on('data', resetTimer);\n res.on('end', clearDataTimer);\n\n ws.on('finish', () => {\n clearDataTimer();\n try {\n fs.renameSync(partialPath, options.destPath);\n settle(resolve);\n } catch (err) {\n cleanup();\n settle(\n reject,\n new Error(\n `Failed to save download to ${options.destPath}: ${err instanceof Error ? err.message : err}`,\n ),\n );\n }\n });\n\n ws.on('error', (err) => {\n clearDataTimer();\n res.destroy();\n cleanup();\n settle(\n reject,\n new Error(\n `Failed to write download: ${err.message}`,\n ),\n );\n });\n\n res.on('error', (err) => {\n clearDataTimer();\n ws.destroy();\n cleanup();\n settle(\n reject,\n new Error(\n `Download stream error from ${url}: ${err.message}`,\n ),\n );\n });\n },\n (err) => settle(reject, err),\n );\n });\n}\n\n/**\n * Compute SHA-256 hash of a file.\n * Returns lowercase hex string.\n */\nexport function sha256File(filePath: string): Promise {\n return new Promise((resolve, reject) => {\n const hash = crypto.createHash('sha256');\n const stream = fs.createReadStream(filePath);\n stream.on('data', (chunk) => hash.update(chunk));\n stream.on('end', () => resolve(hash.digest('hex')));\n stream.on('error', (err) =>\n reject(\n new Error(\n `Failed to compute SHA-256 for ${filePath}: ${err.message}`,\n ),\n ),\n );\n });\n}\n", "import * as fs from 'fs';\nimport * as path from 'path';\nimport { exec } from './exec';\n\nexport interface ExtractOptions {\n /** Path to the archive file. */\n archivePath: string;\n /** Directory to extract into (created if it doesn't exist). */\n destDir: string;\n}\n\n/**\n * Extract an archive to the destination directory.\n * Detects format from file extension (.tar.gz or .zip).\n * On Unix, sets executable permission on extracted binaries.\n */\nexport async function extractArchive(options: ExtractOptions): Promise {\n fs.mkdirSync(options.destDir, { recursive: true });\n\n if (\n options.archivePath.endsWith('.tar.gz') ||\n options.archivePath.endsWith('.tgz')\n ) {\n await extractTarGz(options.archivePath, options.destDir);\n } else if (options.archivePath.endsWith('.zip')) {\n await extractZip(options.archivePath, options.destDir);\n } else {\n throw new Error(\n `Unsupported archive format: ${path.basename(options.archivePath)}`,\n );\n }\n\n if (process.platform !== 'win32') {\n setExecutablePermissions(options.destDir);\n }\n}\n\nasync function extractTarGz(\n archivePath: string,\n destDir: string,\n): Promise {\n const result = await exec('tar', ['-xzf', archivePath, '-C', destDir]);\n if (result.exitCode !== 0) {\n throw new Error(\n `tar extraction failed (exit ${result.exitCode}): ${result.stderr}`,\n );\n }\n}\n\n/** Escape a string for use inside a PowerShell single-quoted literal. */\nfunction escapePowerShellSingleQuote(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n\nasync function extractZip(\n archivePath: string,\n destDir: string,\n): Promise {\n const safePath = escapePowerShellSingleQuote(archivePath);\n const safeDest = escapePowerShellSingleQuote(destDir);\n const result = await exec('powershell', [\n '-NoProfile',\n '-Command',\n `Expand-Archive -LiteralPath '${safePath}' -DestinationPath '${safeDest}' -Force`,\n ]);\n if (result.exitCode !== 0) {\n throw new Error(\n `zip extraction failed (exit ${result.exitCode}): ${result.stderr}`,\n );\n }\n}\n\n/** Set executable permissions on files in the directory (non-recursive, top level only). */\nfunction setExecutablePermissions(dir: string): void {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (entry.isFile()) {\n try {\n fs.chmodSync(path.join(dir, entry.name), 0o755);\n } catch {\n // best-effort per file\n }\n }\n }\n}\n", "import { compareSemver } from '../utils/semver';\n\n/** A platform-specific file entry from the manifest. */\nexport interface FileEntry {\n url: string;\n sha256: string;\n}\n\n/** A single release entry from the manifest. */\nexport interface ReleaseEntry {\n version: string;\n stable: boolean;\n files: FileEntry[];\n}\n\n/** Minimal platform info needed for manifest matching. */\nexport interface ManifestPlatform {\n id: string;\n}\n\n/**\n * Extract tool name from a manifest file URL.\n * URL format: `https://.../tool-os-arch.ext` (e.g., `infs-linux-x64.tar.gz`).\n */\nexport function toolFromUrl(url: string): string {\n const filename = url.split('/').pop() ?? '';\n return filename.split('-')[0] ?? '';\n}\n\n/**\n * Extract OS name from a manifest file URL.\n * URL format: `https://.../tool-os-arch.ext` (e.g., `infs-linux-x64.tar.gz`).\n */\nexport function osFromUrl(url: string): string {\n const filename = url.split('/').pop() ?? '';\n const parts = filename.split('-');\n return parts.length > 1 ? parts[1] : '';\n}\n\n/** Map platform ID to the OS string used in manifest URLs. */\nexport function platformOs(platform: ManifestPlatform): string {\n if (platform.id === 'linux-x64') {\n return 'linux';\n }\n if (platform.id === 'macos-arm64') {\n return 'macos';\n }\n if (platform.id === 'windows-x64') {\n return 'windows';\n }\n return '';\n}\n\n/**\n * Find the latest release from the manifest for the given platform.\n * Matches the `infs` tool artifact for the platform's OS.\n * Returns the release entry, file URL, and sha256, or null if not found.\n */\nexport function findLatestRelease(\n manifest: ReleaseEntry[],\n platform: ManifestPlatform,\n): { release: ReleaseEntry; fileUrl: string; sha256: string } | null {\n if (manifest.length === 0) {\n return null;\n }\n\n const sorted = [...manifest].sort((a, b) =>\n compareSemver(b.version, a.version),\n );\n\n const os = platformOs(platform);\n\n for (const release of sorted) {\n const file = release.files.find(\n (f) => toolFromUrl(f.url) === 'infs' && osFromUrl(f.url) === os,\n );\n if (file) {\n return { release, fileUrl: file.url, sha256: file.sha256 };\n }\n }\n\n return null;\n}\n", "import * as vscode from 'vscode';\nimport { DoctorResult } from '../toolchain/doctor';\nimport { determineStatusBarState, StatusBarIcon } from './statusBarState';\n\nconst ICON_MAP: Record = {\n loading: '$(loading~spin)',\n dash: '$(dash)',\n check: '$(check)',\n warning: '$(warning)',\n error: '$(error)',\n};\n\nconst BACKGROUND_MAP: Record = {\n none: undefined,\n warning: new vscode.ThemeColor('statusBarItem.warningBackground'),\n error: new vscode.ThemeColor('statusBarItem.errorBackground'),\n};\n\n/**\n * Create the Inference status bar item.\n * Positioned on the left side with low priority.\n * Clicking triggers the inference.runDoctor command.\n */\nexport function createStatusBar(): vscode.StatusBarItem {\n const item = vscode.window.createStatusBarItem(\n vscode.StatusBarAlignment.Left,\n 0,\n );\n item.command = 'inference.runDoctor';\n item.text = '$(loading~spin) Inference';\n item.tooltip = 'Inference: Checking toolchain...';\n item.show();\n return item;\n}\n\n/**\n * Update the status bar to reflect doctor results.\n *\n * - null: toolchain not found (grey dash icon)\n * - hasErrors: red error icon\n * - hasWarnings: yellow warning icon\n * - all OK: green check icon\n */\nexport function updateStatusBar(\n item: vscode.StatusBarItem,\n result: DoctorResult | null,\n): void {\n const state = determineStatusBarState(result);\n item.text = `${ICON_MAP[state.icon]} ${state.label}`;\n item.tooltip = state.tooltip;\n item.backgroundColor = BACKGROUND_MAP[state.background];\n}\n", "import { DoctorResult } from '../toolchain/doctor';\n\nexport type StatusBarIcon = 'loading' | 'dash' | 'check' | 'warning' | 'error';\nexport type StatusBarBackground = 'none' | 'warning' | 'error';\n\nexport interface StatusBarState {\n icon: StatusBarIcon;\n label: string;\n tooltip: string;\n background: StatusBarBackground;\n}\n\n/**\n * Determine the status bar display state from a doctor result.\n *\n * - null: toolchain not found (dash icon)\n * - hasErrors: red error icon\n * - hasWarnings: yellow warning icon\n * - all OK: green check icon\n */\nexport function determineStatusBarState(result: DoctorResult | null): StatusBarState {\n if (result === null) {\n return {\n icon: 'dash',\n label: 'Inference',\n tooltip: 'Inference: Toolchain not found. Click to run doctor.',\n background: 'none',\n };\n }\n\n if (result.hasErrors) {\n return {\n icon: 'error',\n label: 'Inference',\n tooltip: `Inference: ${result.summary || 'Toolchain errors detected'}`,\n background: 'error',\n };\n }\n\n if (result.hasWarnings) {\n return {\n icon: 'warning',\n label: 'Inference',\n tooltip: `Inference: ${result.summary || 'Toolchain warnings detected'}`,\n background: 'warning',\n };\n }\n\n return {\n icon: 'check',\n label: 'Inference',\n tooltip: 'Inference: Toolchain healthy',\n background: 'none',\n };\n}\n", "import * as vscode from 'vscode';\nimport { detectInfs } from '../toolchain/detection';\nimport { runDoctor, DoctorResult } from '../toolchain/doctor';\nimport { formatDoctorChecks } from '../toolchain/doctorFormat';\nimport { updateStatusBar } from '../ui/statusBar';\n\n/** Guard against concurrent doctor runs. */\nlet running = false;\n\n/**\n * Register the inference.runDoctor command.\n *\n * When invoked: detect infs \u2192 run doctor \u2192 display results in output\n * channel \u2192 update status bar \u2192 show notification summary.\n */\nexport function registerDoctorCommand(\n outputChannel: vscode.OutputChannel,\n statusBarItem: vscode.StatusBarItem,\n): vscode.Disposable {\n return vscode.commands.registerCommand(\n 'inference.runDoctor',\n async () => {\n if (running) {\n return;\n }\n\n const detection = detectInfs();\n if (!detection) {\n outputChannel.appendLine('Doctor: infs binary not found.');\n updateStatusBar(statusBarItem, null);\n vscode.window\n .showWarningMessage(\n 'Inference toolchain not found. Install it first.',\n 'Install',\n )\n .then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand(\n 'inference.installToolchain',\n );\n }\n });\n return;\n }\n\n running = true;\n try {\n outputChannel.appendLine(\n `Running infs doctor (${detection.path})...`,\n );\n const result = await runDoctor(detection.path);\n\n if (!result) {\n outputChannel.appendLine(\n 'Doctor: failed to execute infs doctor.',\n );\n updateStatusBar(statusBarItem, null);\n vscode.window.showErrorMessage(\n 'Inference: Failed to run doctor. See output for details.',\n );\n return;\n }\n\n for (const line of formatDoctorChecks(result)) {\n outputChannel.appendLine(line);\n }\n updateStatusBar(statusBarItem, result);\n vscode.commands.executeCommand('inference.refreshConfigView');\n\n if (result.hasErrors) {\n vscode.window\n .showErrorMessage(\n `Inference doctor: ${result.summary}`,\n 'Show Output',\n )\n .then((action) => {\n if (action === 'Show Output') {\n outputChannel.show();\n }\n });\n } else if (result.hasWarnings) {\n vscode.window\n .showWarningMessage(\n `Inference doctor: ${result.summary}`,\n 'Show Output',\n )\n .then((action) => {\n if (action === 'Show Output') {\n outputChannel.show();\n }\n });\n } else {\n vscode.window.showInformationMessage(\n 'Inference: Toolchain is healthy.',\n );\n }\n } finally {\n running = false;\n }\n },\n );\n}\n\n", "import { DoctorResult } from './doctor';\n\nconst STATUS_TAGS: Record = {\n ok: '[OK] ',\n warn: '[WARN]',\n fail: '[FAIL]',\n};\n\n/**\n * Format doctor check results into display lines.\n *\n * Each check is formatted as: ` [TAG] name: message`\n * If a summary is present, it is appended after a blank line.\n * Separator lines wrap the output.\n */\nexport function formatDoctorChecks(result: DoctorResult): string[] {\n const lines: string[] = [];\n lines.push('--- Doctor Report ---');\n for (const check of result.checks) {\n const tag = STATUS_TAGS[check.status] ?? `[${check.status.toUpperCase()}]`;\n lines.push(` ${tag} ${check.name}: ${check.message}`);\n }\n if (result.summary) {\n lines.push('');\n lines.push(result.summary);\n }\n lines.push('---------------------');\n return lines;\n}\n", "import * as vscode from 'vscode';\nimport { detectInfs } from '../toolchain/detection';\nimport { fetchVersions, getCurrentVersion } from '../toolchain/versions';\nimport { buildVersionPickItems } from '../toolchain/versionPicker';\nimport { performVersionChange } from './versionChange';\n\n/** Guard against concurrent select operations. */\nlet selecting = false;\n\n/**\n * Register the inference.selectVersion command.\n * Shows a QuickPick with available toolchain versions and switches to the selected one.\n */\nexport function registerSelectVersionCommand(\n outputChannel: vscode.OutputChannel,\n): vscode.Disposable {\n return vscode.commands.registerCommand(\n 'inference.selectVersion',\n async () => {\n if (selecting) {\n vscode.window.showInformationMessage(\n 'Version selection is already in progress.',\n );\n return;\n }\n\n const detection = detectInfs();\n if (!detection) {\n vscode.window\n .showWarningMessage(\n 'Inference toolchain not found. Install it first.',\n 'Install',\n )\n .then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand(\n 'inference.installToolchain',\n );\n }\n });\n return;\n }\n\n selecting = true;\n try {\n const versions = await fetchVersions(detection.path);\n if (!versions) {\n vscode.window.showErrorMessage(\n 'Inference: Failed to fetch available versions.',\n );\n return;\n }\n\n const currentVersion = await getCurrentVersion(detection.path);\n\n const items = buildVersionPickItems(versions, currentVersion);\n\n if (items.length === 0) {\n vscode.window.showInformationMessage(\n 'No toolchain versions available for this platform.',\n );\n return;\n }\n\n const picked = await vscode.window.showQuickPick(items, {\n placeHolder: 'Select toolchain version',\n matchOnDescription: true,\n });\n\n if (!picked) {\n return;\n }\n\n const selectedVersion = picked.label;\n if (selectedVersion === currentVersion) {\n vscode.window.showInformationMessage(\n `Already using toolchain v${selectedVersion}.`,\n );\n return;\n }\n\n await performVersionChange(detection.path, selectedVersion, outputChannel, 'Switching to');\n } finally {\n selecting = false;\n }\n },\n );\n}\n", "import { exec } from '../utils/exec';\n\n/** Version info returned by `infs versions --json`. */\nexport interface VersionInfo {\n version: string;\n stable: boolean;\n platforms: string[];\n available_for_current: boolean;\n}\n\n/**\n * Parse the JSON output of `infs versions --json`.\n * Returns an empty array if the output is invalid.\n */\nexport function parseVersionsOutput(stdout: string): VersionInfo[] {\n try {\n const parsed = JSON.parse(stdout);\n if (!Array.isArray(parsed)) {\n return [];\n }\n return parsed;\n } catch {\n return [];\n }\n}\n\n/**\n * Parse the version string from `infs version` output.\n * Expected format: \"infs X.Y.Z\"\n * Returns the version string or null on parse failure.\n */\nexport function parseCurrentVersion(stdout: string): string | null {\n const match = stdout.match(/^infs\\s+(\\S+)/);\n return match ? match[1] : null;\n}\n\n/**\n * Run `infs versions --json` and parse the output.\n * Returns null if the command fails.\n */\nexport async function fetchVersions(\n infsPath: string,\n): Promise {\n try {\n const result = await exec(infsPath, ['versions', '--json'], {\n timeoutMs: 30_000,\n });\n if (result.exitCode !== 0) {\n return null;\n }\n return parseVersionsOutput(result.stdout);\n } catch {\n return null;\n }\n}\n\n/**\n * Run `infs version` and parse the current version.\n * Returns null if the command fails or the output is unexpected.\n */\nexport async function getCurrentVersion(\n infsPath: string,\n): Promise {\n try {\n const result = await exec(infsPath, ['version'], {\n timeoutMs: 10_000,\n });\n if (result.exitCode !== 0) {\n return null;\n }\n return parseCurrentVersion(result.stdout);\n } catch {\n return null;\n }\n}\n\n/** Result of an install-and-set-default operation. */\nexport interface SwitchResult {\n success: boolean;\n installedButNotDefault: boolean;\n error?: string;\n}\n\n/**\n * Install a toolchain version and set it as default.\n *\n * Runs `infs install VERSION` followed by `infs default VERSION`.\n * Handles the partial-success case where install succeeds but setting default fails.\n */\nexport async function installAndSetDefault(\n infsPath: string,\n version: string,\n): Promise {\n const installResult = await exec(infsPath, ['install', version], {\n timeoutMs: 120_000,\n });\n if (installResult.exitCode !== 0) {\n const detail = installResult.stderr || installResult.stdout;\n return { success: false, installedButNotDefault: false, error: detail };\n }\n\n const defaultResult = await exec(infsPath, ['default', version], {\n timeoutMs: 30_000,\n });\n if (defaultResult.exitCode !== 0) {\n const detail = defaultResult.stderr || defaultResult.stdout;\n return { success: false, installedButNotDefault: true, error: detail };\n }\n\n return { success: true, installedButNotDefault: false };\n}\n", "import { VersionInfo } from './versions';\nimport { compareSemver } from '../utils/semver';\n\nexport interface PickItem {\n label: string;\n description?: string;\n}\n\n/**\n * Build QuickPick items from available versions.\n *\n * - Filters to `available_for_current` versions only\n * - Sorts descending by semver\n * - Tags the current version with \"(current)\" and stable versions with \"(stable)\"\n * - Moves the current version to the top of the list\n */\nexport function buildVersionPickItems(\n versions: VersionInfo[],\n currentVersion: string | null,\n): PickItem[] {\n const available = versions\n .filter((v) => v.available_for_current)\n .sort((a, b) => compareSemver(b.version, a.version));\n\n const items: PickItem[] = available.map((v) => {\n const tags: string[] = [];\n if (v.version === currentVersion) {\n tags.push('current');\n }\n if (v.stable) {\n tags.push('stable');\n }\n return {\n label: v.version,\n description: tags.length > 0 ? `(${tags.join(', ')})` : undefined,\n };\n });\n\n if (currentVersion) {\n const idx = items.findIndex((i) => i.label === currentVersion);\n if (idx > 0) {\n const [item] = items.splice(idx, 1);\n items.unshift(item);\n }\n }\n\n return items;\n}\n", "import * as vscode from 'vscode';\nimport { installAndSetDefault } from '../toolchain/versions';\n\n/**\n * Perform a version change (install + set default) with progress UI.\n *\n * Shared by both the \"Select Version\" and \"Update Toolchain\" commands.\n */\nexport async function performVersionChange(\n infsPath: string,\n version: string,\n outputChannel: vscode.OutputChannel,\n actionVerb: string,\n): Promise {\n await vscode.window.withProgress(\n {\n location: vscode.ProgressLocation.Notification,\n title: 'Inference Toolchain',\n cancellable: false,\n },\n async (progress) => {\n progress.report({ message: `${actionVerb} v${version}...` });\n outputChannel.appendLine(`${actionVerb} toolchain v${version}...`);\n\n const result = await installAndSetDefault(infsPath, version);\n\n if (result.success) {\n outputChannel.appendLine(\n `${actionVerb} toolchain v${version} complete.`,\n );\n vscode.commands.executeCommand(\n 'setContext', 'inference.toolchainInstalled', true,\n );\n vscode.commands.executeCommand('inference.applyTerminalPath');\n vscode.commands.executeCommand('inference.runDoctor');\n vscode.window\n .showInformationMessage(\n `Inference toolchain ${actionVerb.toLowerCase()} to v${version}.`,\n 'Show Output',\n )\n .then((action) => {\n if (action === 'Show Output') {\n outputChannel.show();\n }\n });\n return;\n }\n\n outputChannel.appendLine(\n `${actionVerb} failed: ${result.error}`,\n );\n\n if (result.installedButNotDefault) {\n vscode.window\n .showWarningMessage(\n `Inference: v${version} was installed but could not be set as default. Run \\`infs default ${version}\\` manually.`,\n 'Show Output',\n )\n .then((action) => {\n if (action === 'Show Output') {\n outputChannel.show();\n }\n });\n } else {\n vscode.window.showErrorMessage(\n `Inference: Failed to install v${version}: ${result.error}`,\n );\n }\n },\n );\n}\n", "import * as vscode from 'vscode';\nimport { detectInfs } from '../toolchain/detection';\nimport { fetchVersions, getCurrentVersion } from '../toolchain/versions';\nimport { checkUpdateAvailable } from '../toolchain/updateCheck';\nimport { getSettings } from '../config/settings';\nimport { performVersionChange } from './versionChange';\n\n/** Guard against concurrent update operations. */\nlet updating = false;\n\n/**\n * Register the inference.updateToolchain command.\n * Checks for updates and prompts the user to install if available.\n */\nexport function registerUpdateCommand(\n outputChannel: vscode.OutputChannel,\n): vscode.Disposable {\n return vscode.commands.registerCommand(\n 'inference.updateToolchain',\n async () => {\n if (updating) {\n vscode.window.showInformationMessage(\n 'Update check is already in progress.',\n );\n return;\n }\n\n const detection = detectInfs();\n if (!detection) {\n vscode.window\n .showWarningMessage(\n 'Inference toolchain not found. Install it first.',\n 'Install',\n )\n .then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand(\n 'inference.installToolchain',\n );\n }\n });\n return;\n }\n\n updating = true;\n try {\n await checkForUpdatesImpl(detection.path, outputChannel, true);\n } finally {\n updating = false;\n }\n },\n );\n}\n\n/**\n * Check for toolchain updates on activation.\n * Respects the `inference.checkForUpdates` setting.\n * This is a no-op if checks are disabled.\n */\nexport async function checkForUpdates(\n infsPath: string,\n outputChannel: vscode.OutputChannel,\n): Promise {\n if (updating) {\n return;\n }\n const settings = getSettings();\n if (!settings.checkForUpdates) {\n return;\n }\n updating = true;\n try {\n await checkForUpdatesImpl(infsPath, outputChannel, false);\n } finally {\n updating = false;\n }\n}\n\nasync function checkForUpdatesImpl(\n infsPath: string,\n outputChannel: vscode.OutputChannel,\n userInitiated: boolean,\n): Promise {\n const currentVersion = await getCurrentVersion(infsPath);\n if (!currentVersion) {\n outputChannel.appendLine('Update check: could not determine current version.');\n if (userInitiated) {\n vscode.window.showErrorMessage(\n 'Inference: Could not determine the current toolchain version.',\n );\n }\n return;\n }\n\n outputChannel.appendLine(`Update check: current version is ${currentVersion}.`);\n\n const versions = await fetchVersions(infsPath);\n if (!versions) {\n outputChannel.appendLine('Update check: failed to fetch available versions.');\n if (userInitiated) {\n vscode.window.showErrorMessage(\n 'Inference: Failed to check for updates.',\n );\n }\n return;\n }\n\n const result = checkUpdateAvailable(currentVersion, versions);\n\n switch (result.status) {\n case 'no-current-version':\n outputChannel.appendLine('Update check: could not determine current version.');\n if (userInitiated) {\n vscode.window.showErrorMessage(\n 'Inference: Could not determine the current toolchain version.',\n );\n }\n return;\n\n case 'no-versions':\n outputChannel.appendLine('Update check: no versions available for this platform.');\n if (userInitiated) {\n vscode.window.showInformationMessage(\n 'Inference: No toolchain versions available for this platform.',\n );\n }\n return;\n\n case 'up-to-date':\n outputChannel.appendLine(\n `Update check: toolchain is up to date (v${result.version}).`,\n );\n if (userInitiated) {\n vscode.window.showInformationMessage(\n `Inference toolchain is up to date (v${result.version}).`,\n );\n }\n return;\n\n case 'update-available': {\n outputChannel.appendLine(\n `Update check: v${result.latest} available (current: v${result.current}).`,\n );\n\n const action = await vscode.window.showInformationMessage(\n `Inference toolchain update available: v${result.latest} (current: v${result.current})`,\n 'Update',\n 'Release Notes',\n );\n\n if (action === 'Update') {\n await performVersionChange(infsPath, result.latest, outputChannel, 'Updating to');\n } else if (action === 'Release Notes') {\n vscode.env.openExternal(\n vscode.Uri.parse(\n `https://github.com/Inferara/inference/releases/tag/v${result.latest}`,\n ),\n );\n }\n return;\n }\n }\n}\n", "import { VersionInfo } from './versions';\nimport { compareSemver } from '../utils/semver';\n\nexport type UpdateCheckResult =\n | { status: 'up-to-date'; version: string }\n | { status: 'update-available'; current: string; latest: string }\n | { status: 'no-versions' }\n | { status: 'no-current-version' };\n\n/**\n * Determine whether an update is available based on current version and available versions.\n *\n * Filters to `available_for_current` versions and compares the highest against current.\n */\nexport function checkUpdateAvailable(\n currentVersion: string | null,\n versions: VersionInfo[] | null,\n): UpdateCheckResult {\n if (!currentVersion) {\n return { status: 'no-current-version' };\n }\n\n if (!versions) {\n return { status: 'no-versions' };\n }\n\n const candidates = versions.filter((v) => v.available_for_current);\n\n if (candidates.length === 0) {\n return { status: 'no-versions' };\n }\n\n const sorted = [...candidates].sort((a, b) =>\n compareSemver(b.version, a.version),\n );\n const latest = sorted[0];\n\n if (compareSemver(currentVersion, latest.version) >= 0) {\n return { status: 'up-to-date', version: currentVersion };\n }\n\n return {\n status: 'update-available',\n current: currentVersion,\n latest: latest.version,\n };\n}\n", "import * as vscode from 'vscode';\nimport { detectInfs, InfsDetection } from '../toolchain/detection';\nimport { inferenceHome } from '../toolchain/home';\nimport { detectPlatform } from '../toolchain/platform';\nimport { getSettings } from '../config/settings';\nimport { exec } from '../utils/exec';\nimport { DoctorResult } from '../toolchain/doctor';\n\ntype GroupId = 'toolchain' | 'settings';\n\nexport class ConfigItem extends vscode.TreeItem {\n constructor(\n label: string,\n public readonly kind: 'group' | 'property',\n collapsible: vscode.TreeItemCollapsibleState,\n public readonly groupId?: GroupId,\n public readonly settingKey?: string,\n public readonly copyValue?: string,\n ) {\n super(label, collapsible);\n\n if (kind === 'group') {\n this.iconPath = new vscode.ThemeIcon(\n groupId === 'toolchain' ? 'tools' : 'gear',\n );\n }\n\n if (settingKey) {\n this.command = {\n title: 'Open Setting',\n command: 'workbench.action.openSettings',\n arguments: [settingKey],\n };\n }\n\n if (copyValue) {\n this.contextValue = 'inference.configPath';\n }\n }\n}\n\nexport class InferenceConfigProvider\n implements vscode.TreeDataProvider\n{\n private _onDidChangeTreeData = new vscode.EventEmitter<\n ConfigItem | undefined | null\n >();\n readonly onDidChangeTreeData = this._onDidChangeTreeData.event;\n\n private detection: InfsDetection | null = null;\n private version: string | null = null;\n private doctorResult: DoctorResult | null = null;\n\n refresh(detection?: InfsDetection | null, doctorResult?: DoctorResult | null): void {\n if (detection !== undefined) {\n this.detection = detection;\n }\n if (doctorResult !== undefined) {\n this.doctorResult = doctorResult;\n }\n this._onDidChangeTreeData.fire(undefined);\n }\n\n getTreeItem(element: ConfigItem): vscode.TreeItem {\n return element;\n }\n\n async getChildren(element?: ConfigItem): Promise {\n if (!element) {\n return [\n new ConfigItem(\n 'Toolchain',\n 'group',\n vscode.TreeItemCollapsibleState.Expanded,\n 'toolchain',\n ),\n new ConfigItem(\n 'Settings',\n 'group',\n vscode.TreeItemCollapsibleState.Expanded,\n 'settings',\n ),\n ];\n }\n\n if (element.groupId === 'toolchain') {\n return this.getToolchainChildren();\n }\n\n if (element.groupId === 'settings') {\n return this.getSettingsChildren();\n }\n\n return [];\n }\n\n private async getToolchainChildren(): Promise {\n const detection = this.detection ?? detectInfs();\n const items: ConfigItem[] = [];\n\n if (!detection) {\n const item = new ConfigItem(\n 'infs: not found',\n 'property',\n vscode.TreeItemCollapsibleState.None,\n );\n item.iconPath = new vscode.ThemeIcon('error');\n item.command = {\n title: 'Install Toolchain',\n command: 'inference.installToolchain',\n arguments: [],\n };\n items.push(item);\n return items;\n }\n\n const infsItem = new ConfigItem(\n `infs: ${detection.path} (${detection.source})`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n undefined,\n detection.path,\n );\n infsItem.iconPath = new vscode.ThemeIcon('file-binary');\n items.push(infsItem);\n\n const version = await this.resolveVersion(detection.path);\n const versionItem = new ConfigItem(\n `Version: ${version ?? 'unknown'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n );\n versionItem.iconPath = new vscode.ThemeIcon('tag');\n items.push(versionItem);\n\n const home = inferenceHome();\n const homeIsDefault = !process.env['INFERENCE_HOME'];\n const homeItem = new ConfigItem(\n `Home: ${home} (${homeIsDefault ? 'default' : 'env'})`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n undefined,\n home,\n );\n homeItem.iconPath = new vscode.ThemeIcon('home');\n items.push(homeItem);\n\n const platform = detectPlatform();\n const platformItem = new ConfigItem(\n `Platform: ${platform?.id ?? 'unknown'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n );\n platformItem.iconPath = new vscode.ThemeIcon('device-desktop');\n items.push(platformItem);\n\n const status = this.doctorResult\n ? this.doctorResult.hasErrors\n ? 'errors'\n : this.doctorResult.hasWarnings\n ? 'warnings'\n : 'healthy'\n : 'unknown';\n const statusIcon = this.doctorResult\n ? this.doctorResult.hasErrors\n ? 'error'\n : this.doctorResult.hasWarnings\n ? 'warning'\n : 'pass'\n : 'question';\n const statusItem = new ConfigItem(\n `Status: ${status}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n );\n statusItem.iconPath = new vscode.ThemeIcon(statusIcon);\n statusItem.command = {\n title: 'Run Doctor',\n command: 'inference.runDoctor',\n arguments: [],\n };\n items.push(statusItem);\n\n return items;\n }\n\n private getSettingsChildren(): ConfigItem[] {\n const settings = getSettings();\n\n const pathItem = new ConfigItem(\n `Path: ${settings.path || '(auto-detect)'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n 'inference.path',\n );\n pathItem.iconPath = new vscode.ThemeIcon('file-symlink-directory');\n\n const autoInstallItem = new ConfigItem(\n `Auto Install: ${settings.autoInstall ? 'enabled' : 'disabled'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n 'inference.autoInstall',\n );\n autoInstallItem.iconPath = new vscode.ThemeIcon('cloud-download');\n\n const updateItem = new ConfigItem(\n `Check for Updates: ${settings.checkForUpdates ? 'enabled' : 'disabled'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n 'inference.checkForUpdates',\n );\n updateItem.iconPath = new vscode.ThemeIcon('sync');\n\n return [pathItem, autoInstallItem, updateItem];\n }\n\n private async resolveVersion(infsPath: string): Promise {\n if (this.version) {\n return this.version;\n }\n try {\n const result = await exec(infsPath, ['version']);\n if (result.exitCode !== 0) {\n return null;\n }\n const match = result.stdout.match(/^infs\\s+(\\S+)/);\n if (match) {\n this.version = match[1];\n return this.version;\n }\n return null;\n } catch {\n return null;\n }\n }\n\n dispose(): void {\n this._onDidChangeTreeData.dispose();\n }\n}\n"], - "mappings": "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", - "names": ["os", "path", "vscode", "path", "path", "platform", "vscode", "fs", "os", "path", "fs", "DEFAULT_TIMEOUT_MS", "fs", "path", "platform", "os", "platform", "parseDoctorOutput", "vscode", "outputChannel", "platform", "vscode", "outputChannel", "vscode", "vscode", "outputChannel", "outputChannel", "vscode", "outputChannel", "vscode", "platform", "env", "platform"] + "sources": ["../src/toolchain/home.ts", "../src/utils/exec.ts", "../src/toolchain/doctor.ts", "../src/extension.ts", "../src/toolchain/platform.ts", "../src/toolchain/detection.ts", "../src/config/settings.ts", "../src/utils/semver.ts", "../src/commands/install.ts", "../src/toolchain/installation.ts", "../src/utils/download.ts", "../src/utils/extract.ts", "../src/toolchain/manifest.ts", "../src/ui/statusBar.ts", "../src/ui/statusBarState.ts", "../src/commands/installComponent.ts", "../src/toolchain/components.ts", "../src/commands/doctor.ts", "../src/toolchain/doctorFormat.ts", "../src/commands/selectVersion.ts", "../src/toolchain/versions.ts", "../src/toolchain/versionPicker.ts", "../src/commands/versionChange.ts", "../src/commands/update.ts", "../src/toolchain/updateCheck.ts", "../src/ui/configTree.ts"], + "sourcesContent": ["import * as os from 'os';\nimport * as path from 'path';\n\n/** Resolve the INFERENCE_HOME directory (default: ~/.inference). */\nexport function inferenceHome(): string {\n return process.env['INFERENCE_HOME'] || path.join(os.homedir(), '.inference');\n}\n", "import * as cp from 'child_process';\n\nexport interface ExecResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/** Default timeout for child processes (30 seconds). */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Run a command and capture its output.\n *\n * Resolves with ExecResult on completion (including non-zero exit).\n * Rejects only on spawn failure or timeout.\n */\nexport function exec(\n command: string,\n args: string[],\n options?: { timeoutMs?: number; cwd?: string; env?: Record },\n): Promise {\n const timeout = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n return new Promise((resolve, reject) => {\n const child = cp.spawn(command, args, {\n cwd: options?.cwd,\n env: options?.env ? { ...process.env, ...options.env } : undefined,\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout,\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n\n child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));\n child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));\n\n child.on('error', (err) => reject(err));\n\n child.on('close', (code) => {\n resolve({\n exitCode: code ?? 1,\n stdout: Buffer.concat(stdoutChunks).toString('utf-8'),\n stderr: Buffer.concat(stderrChunks).toString('utf-8'),\n });\n });\n });\n}\n", "import * as path from 'path';\nimport { exec } from '../utils/exec';\nimport { inferenceHome } from './home';\n\nexport type DoctorCheckStatus = 'ok' | 'warn' | 'fail';\n\nexport interface DoctorCheck {\n name: string;\n status: DoctorCheckStatus;\n message: string;\n}\n\nexport interface DoctorResult {\n checks: DoctorCheck[];\n hasErrors: boolean;\n hasWarnings: boolean;\n summary: string;\n}\n\nconst STATUS_MAP: Record = {\n OK: 'ok',\n WARN: 'warn',\n FAIL: 'fail',\n};\n\n/**\n * Check line format: ` [OK|WARN|FAIL] : `\n *\n * PATH conflict continuation lines (indented without a status prefix)\n * are intentionally not captured as individual checks.\n */\nconst CHECK_PATTERN = /^\\s+\\[(OK|WARN|FAIL)]\\s+(.+?):\\s+(.*)/;\n\n/**\n * Parse the stdout of `infs doctor` into a structured result.\n *\n * The output format is a public contract defined in\n * `apps/infs/src/commands/doctor.rs`.\n */\nexport function parseDoctorOutput(stdout: string): DoctorResult {\n const checks: DoctorCheck[] = [];\n const lines = stdout.split(/\\r?\\n/);\n\n for (const line of lines) {\n const match = line.match(CHECK_PATTERN);\n if (match) {\n checks.push({\n status: STATUS_MAP[match[1]],\n name: match[2].trim(),\n message: match[3].trim(),\n });\n }\n }\n\n let summary = '';\n for (let i = lines.length - 1; i >= 0; i--) {\n const trimmed = lines[i].trim();\n if (trimmed.length > 0 && !CHECK_PATTERN.test(lines[i])) {\n summary = trimmed;\n break;\n }\n }\n\n return {\n checks,\n hasErrors: checks.some((c) => c.status === 'fail'),\n hasWarnings: checks.some((c) => c.status === 'warn'),\n summary,\n };\n}\n\n/**\n * Execute `infs doctor` and return the parsed result.\n * Returns null if execution fails (e.g., binary not found or crash).\n */\nexport async function runDoctor(\n infsPath: string,\n): Promise {\n try {\n const binDir = path.join(inferenceHome(), 'bin');\n const sep = process.platform === 'win32' ? ';' : ':';\n const augmentedPath = `${binDir}${sep}${process.env['PATH'] ?? ''}`;\n\n const result = await exec(infsPath, ['doctor'], {\n env: { PATH: augmentedPath },\n });\n return parseDoctorOutput(result.stdout);\n } catch (err) {\n console.error('infs doctor failed:', err);\n return null;\n }\n}\n", "import * as vscode from 'vscode';\nimport * as path from 'path';\nimport { detectPlatform } from './toolchain/platform';\nimport { detectInfs, inferenceHome } from './toolchain/detection';\nimport { exec } from './utils/exec';\nimport { compareSemver } from './utils/semver';\nimport { registerInstallCommand } from './commands/install';\nimport { registerInstallComponentCommand } from './commands/installComponent';\nimport { registerDoctorCommand } from './commands/doctor';\nimport { registerSelectVersionCommand } from './commands/selectVersion';\nimport { registerUpdateCommand, checkForUpdates } from './commands/update';\nimport { createStatusBar, updateStatusBar } from './ui/statusBar';\nimport { InferenceConfigProvider, ConfigItem } from './ui/configTree';\nimport { runDoctor } from './toolchain/doctor';\n\n/** Minimum infs CLI version the extension can work with. */\nconst MIN_INFS_VERSION = '0.0.1-beta.1';\n\nconst outputChannel = vscode.window.createOutputChannel('Inference', { log: true });\n\nexport function activate(context: vscode.ExtensionContext) {\n context.subscriptions.push(outputChannel);\n\n const statusBarItem = createStatusBar();\n context.subscriptions.push(statusBarItem);\n\n context.subscriptions.push(\n vscode.commands.registerCommand('inference.showOutput', () => {\n outputChannel.show();\n }),\n );\n\n context.subscriptions.push(registerInstallCommand(outputChannel, statusBarItem));\n context.subscriptions.push(\n registerDoctorCommand(outputChannel, statusBarItem),\n );\n context.subscriptions.push(registerInstallComponentCommand(outputChannel));\n\n context.subscriptions.push(registerUpdateCommand(outputChannel));\n context.subscriptions.push(registerSelectVersionCommand(outputChannel));\n\n const configProvider = new InferenceConfigProvider();\n const configView = vscode.window.createTreeView('inference.configView', {\n treeDataProvider: configProvider,\n });\n context.subscriptions.push(configView);\n context.subscriptions.push(configProvider);\n\n context.subscriptions.push(\n vscode.commands.registerCommand('inference.refreshConfigView', () => {\n configProvider.refresh();\n }),\n );\n\n context.subscriptions.push(\n vscode.commands.registerCommand('inference.applyTerminalPath', () => {\n applyTerminalPath(context);\n }),\n );\n\n context.subscriptions.push(\n vscode.commands.registerCommand(\n 'inference.copyConfigValue',\n (item: ConfigItem) => {\n if (item.copyValue) {\n vscode.env.clipboard.writeText(item.copyValue);\n vscode.window.showInformationMessage(\n `Copied: ${item.copyValue}`,\n );\n }\n },\n ),\n );\n\n context.subscriptions.push(\n vscode.commands.registerCommand(\n 'inference.revealConfigPath',\n (item: ConfigItem) => {\n if (item.copyValue) {\n vscode.commands.executeCommand(\n 'revealFileInOS',\n vscode.Uri.file(item.copyValue),\n );\n }\n },\n ),\n );\n\n context.subscriptions.push(\n vscode.workspace.onDidChangeConfiguration((e) => {\n if (e.affectsConfiguration('inference')) {\n configProvider.refresh();\n }\n }),\n );\n\n context.subscriptions.push(\n vscode.commands.registerCommand('inference.resetPathAcceptance', () => {\n const home = inferenceHome();\n const stateKey = `${PATH_FALLBACK_KEY}:${home}`;\n context.globalState.update(stateKey, undefined);\n vscode.window.showInformationMessage(\n 'Inference: PATH fallback preference has been reset.',\n );\n }),\n );\n\n applyTerminalPath(context);\n\n checkToolchain(context, statusBarItem, configProvider).catch((err) =>\n outputChannel.error(`Toolchain check failed: ${err}`),\n );\n}\n\nexport function deactivate() {\n // Nothing to clean up\n}\n\n/**\n * Prepend `INFERENCE_HOME/bin` to PATH for all integrated terminals.\n *\n * Uses VS Code's `EnvironmentVariableCollection` so that:\n * - New terminals automatically have the toolchain on PATH.\n * - Existing terminals show a relaunch indicator when the collection changes.\n *\n * Exported so that install/update commands can re-apply the mutation to\n * trigger the relaunch indicator on already-open terminals.\n */\nexport function applyTerminalPath(context: vscode.ExtensionContext): void {\n const binDir = path.join(inferenceHome(), 'bin');\n const sep = process.platform === 'win32' ? ';' : ':';\n const env = context.environmentVariableCollection;\n env.prepend('PATH', binDir + sep);\n env.description = 'Adds the Inference toolchain to PATH';\n}\n\n/** globalState key prefix for remembering PATH fallback acceptance. */\nconst PATH_FALLBACK_KEY = 'inference.acceptedPathFallback';\n\nasync function checkToolchain(\n context: vscode.ExtensionContext,\n statusBarItem: vscode.StatusBarItem,\n configProvider: InferenceConfigProvider,\n): Promise {\n const platform = detectPlatform();\n const home = inferenceHome();\n const homeIsDefault = !process.env['INFERENCE_HOME'];\n const distServer = process.env['INFS_DIST_SERVER'];\n\n outputChannel.info('Inference Activation');\n\n if (!platform) {\n outputChannel.warn(\n `Platform: ${process.platform}-${process.arch} (unsupported)`,\n );\n outputChannel.info(\n `INFERENCE_HOME: ${home} ${homeIsDefault ? '(default)' : '(env)'}`,\n );\n outputChannel.info(\n `INFS_DIST_SERVER: ${distServer ?? '(not set, using production)'}`,\n );\n updateStatusBar(statusBarItem, null);\n vscode.commands.executeCommand('setContext', 'inference.toolchainInstalled', false);\n vscode.window\n .showWarningMessage(\n `Inference: unsupported platform (${process.platform}-${process.arch}).`,\n 'Download Page',\n )\n .then((action) => {\n if (action === 'Download Page') {\n vscode.env.openExternal(\n vscode.Uri.parse(\n 'https://github.com/Inferara/inference/releases',\n ),\n );\n }\n });\n return;\n }\n\n outputChannel.info(`Platform: ${platform.id}`);\n outputChannel.info(\n `INFERENCE_HOME: ${home} ${homeIsDefault ? '(default)' : '(env)'}`,\n );\n outputChannel.info(\n `INFS_DIST_SERVER: ${distServer ?? '(not set, using production)'}`,\n );\n\n const detection = detectInfs();\n if (!detection) {\n outputChannel.error('infs binary: not found');\n outputChannel.error('Toolchain status: errors');\n updateStatusBar(statusBarItem, null);\n vscode.commands.executeCommand('setContext', 'inference.toolchainInstalled', false);\n notifyMissing();\n return;\n }\n outputChannel.info(\n `infs binary: ${detection.path} (${detection.source})`,\n );\n\n if (!homeIsDefault && detection.source === 'path') {\n const stateKey = `${PATH_FALLBACK_KEY}:${home}`;\n const accepted = context.globalState.get(stateKey);\n\n if (accepted === '*') {\n outputChannel.info(\n `Note: Using PATH binary (notification permanently suppressed for this INFERENCE_HOME).`,\n );\n } else if (accepted === detection.path) {\n outputChannel.info(\n `Note: Using PATH binary (previously accepted for this INFERENCE_HOME).`,\n );\n } else {\n outputChannel.warn(\n `INFERENCE_HOME is set to ${home} but infs was not found there. Using binary from PATH instead.`,\n );\n vscode.window.showWarningMessage(\n `Inference: infs binary not found in INFERENCE_HOME (${home}). Found via PATH instead.`,\n 'Install',\n 'Dismiss',\n ).then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand('inference.installToolchain');\n } else {\n context.globalState.update(stateKey, '*');\n }\n });\n }\n }\n\n const versionOk = await checkInfsVersion(detection.path);\n if (!versionOk) {\n outputChannel.error('Toolchain status: errors');\n updateStatusBar(statusBarItem, null);\n vscode.commands.executeCommand('setContext', 'inference.toolchainInstalled', false);\n return;\n }\n\n vscode.commands.executeCommand('setContext', 'inference.toolchainInstalled', true);\n\n const doctorResult = await runDoctor(detection.path);\n updateStatusBar(statusBarItem, doctorResult);\n configProvider.refresh(detection, doctorResult);\n\n const status = doctorResult?.hasErrors\n ? 'errors'\n : doctorResult?.hasWarnings\n ? 'warnings'\n : 'healthy';\n if (doctorResult?.hasErrors) {\n outputChannel.error(`Toolchain status: ${status}`);\n } else if (doctorResult?.hasWarnings) {\n outputChannel.warn(`Toolchain status: ${status}`);\n } else {\n outputChannel.info(`Toolchain status: ${status}`);\n }\n\n checkForUpdates(detection.path, outputChannel).catch((err) =>\n outputChannel.error(`Update check failed: ${err}`),\n );\n}\n\n/**\n * Run `infs version` and check the output against MIN_INFS_VERSION.\n * Returns true if version is acceptable.\n */\nasync function checkInfsVersion(infsPath: string): Promise {\n try {\n const result = await exec(infsPath, ['version']);\n if (result.exitCode !== 0) {\n outputChannel.error(\n `infs version failed (exit ${result.exitCode}): ${result.stderr}`,\n );\n return false;\n }\n // Output format: \"infs 0.1.0\"\n const match = result.stdout.match(/^infs\\s+(\\S+)/);\n if (!match) {\n outputChannel.error(\n `Could not parse infs version from: ${result.stdout.trim()}`,\n );\n return false;\n }\n const version = match[1];\n outputChannel.info(`infs version: ${version}`);\n\n if (compareSemver(version, MIN_INFS_VERSION) < 0) {\n outputChannel.warn(\n `infs version ${version} is below minimum ${MIN_INFS_VERSION}.`,\n );\n vscode.window\n .showWarningMessage(\n `Inference: infs version ${version} is outdated (minimum: ${MIN_INFS_VERSION}). Please update.`,\n 'Update',\n )\n .then((action) => {\n if (action === 'Update') {\n vscode.commands.executeCommand('inference.updateToolchain');\n }\n });\n return false;\n }\n return true;\n } catch (err) {\n outputChannel.error(`Failed to run infs version: ${err}`);\n return false;\n }\n}\n\nfunction notifyMissing(): void {\n vscode.window\n .showInformationMessage(\n 'Inference toolchain not found. Would you like to install it?',\n 'Install',\n 'Download Manually',\n 'Configure Path',\n )\n .then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand('inference.installToolchain');\n } else if (action === 'Download Manually') {\n vscode.env.openExternal(\n vscode.Uri.parse(\n 'https://github.com/Inferara/inference/releases',\n ),\n );\n } else if (action === 'Configure Path') {\n vscode.commands.executeCommand(\n 'workbench.action.openSettings',\n 'inference.path',\n );\n }\n });\n}\n", "import * as os from 'os';\n\nexport type PlatformId = 'linux-x64' | 'macos-arm64' | 'windows-x64';\n\nexport interface PlatformInfo {\n id: PlatformId;\n archiveExtension: string;\n binaryName: string;\n}\n\nexport const SUPPORTED_PLATFORMS: Record = {\n 'linux-x64': 'linux-x64',\n 'darwin-arm64': 'macos-arm64',\n 'win32-x64': 'windows-x64',\n};\n\n/**\n * Detect the platform and return its info, or null if unsupported.\n * When osPlatform/osArch are omitted, uses the current runtime values.\n */\nexport function detectPlatform(\n osPlatform?: string,\n osArch?: string,\n): PlatformInfo | null {\n const key = `${osPlatform ?? os.platform()}-${osArch ?? os.arch()}`;\n const id = SUPPORTED_PLATFORMS[key];\n if (!id) {\n return null;\n }\n return {\n id,\n archiveExtension: id === 'windows-x64' ? '.zip' : '.tar.gz',\n binaryName: id === 'windows-x64' ? 'infs.exe' : 'infs',\n };\n}\n", "import * as fs from 'fs';\nimport * as path from 'path';\nimport { getSettings } from '../config/settings';\nimport { detectPlatform } from './platform';\nimport { inferenceHome } from './home';\n\nexport { inferenceHome };\n\n/** Check whether a file exists and is executable (or just exists on Windows). */\nexport function isExecutable(filePath: string): boolean {\n try {\n const mode = process.platform === 'win32'\n ? fs.constants.F_OK\n : fs.constants.X_OK;\n fs.accessSync(filePath, mode);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Search PATH for the given binary name.\n * Returns the first match or null.\n */\nexport function findInPath(binaryName: string): string | null {\n const envPath = process.env['PATH'] || '';\n const sep = process.platform === 'win32' ? ';' : ':';\n const dirs = envPath.split(sep).filter(Boolean);\n for (const dir of dirs) {\n const candidate = path.join(dir, binaryName);\n if (isExecutable(candidate)) {\n return candidate;\n }\n }\n return null;\n}\n\n/** Source where the infs binary was found. */\nexport type InfsSource = 'settings' | 'managed' | 'path';\n\n/** Result of infs binary detection. */\nexport interface InfsDetection {\n path: string;\n source: InfsSource;\n}\n\n/**\n * Detect infs binary location.\n *\n * Search order:\n * 1. Custom path from settings (inference.path)\n * 2. Managed location (INFERENCE_HOME/bin/infs)\n * 3. System PATH\n *\n * Returns the resolved absolute path and source, or null if not found.\n */\nexport function detectInfs(): InfsDetection | null {\n const platform = detectPlatform();\n const binaryName = platform?.binaryName ?? 'infs';\n\n const settings = getSettings();\n if (settings.path) {\n if (isExecutable(settings.path)) {\n return { path: settings.path, source: 'settings' };\n }\n return null;\n }\n\n const managedPath = path.join(inferenceHome(), 'bin', binaryName);\n if (isExecutable(managedPath)) {\n return { path: managedPath, source: 'managed' };\n }\n\n const pathResult = findInPath(binaryName);\n if (pathResult) {\n return { path: pathResult, source: 'path' };\n }\n\n return null;\n}\n", "import * as vscode from 'vscode';\n\nexport interface InferenceSettings {\n /** Custom path to infs binary. Empty string means auto-detect. */\n path: string;\n /** Prompt to install toolchain if not found. */\n autoInstall: boolean;\n /** Check for toolchain updates on activation. */\n checkForUpdates: boolean;\n}\n\n/** Read current inference.* configuration values. */\nexport function getSettings(): InferenceSettings {\n const config = vscode.workspace.getConfiguration('inference');\n return {\n path: config.get('path', ''),\n autoInstall: config.get('autoInstall', true),\n checkForUpdates: config.get('checkForUpdates', true),\n };\n}\n", "/**\n * Compare two semver strings. Returns negative if a < b, 0 if equal, positive if a > b.\n * Handles numeric major.minor.patch and pre-release tags per the SemVer 2.0.0 spec.\n *\n * Pre-release versions have lower precedence than the associated normal version:\n * 1.0.0-alpha < 1.0.0\n *\n * Pre-release identifiers are compared left-to-right:\n * - Numeric identifiers are compared as integers.\n * - Alphanumeric identifiers are compared lexically (ASCII order).\n * - Numeric identifiers always have lower precedence than alphanumeric.\n * - A shorter set of identifiers has lower precedence if all preceding are equal.\n */\nexport function compareSemver(a: string, b: string): number {\n const clean = (v: string) => v.replace(/^v/i, '');\n const [coreA, preA] = clean(a).split('-', 2);\n const [coreB, preB] = clean(b).split('-', 2);\n\n const pa = coreA.split('.').map(Number);\n const pb = coreB.split('.').map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (pa[i] || 0) - (pb[i] || 0);\n if (diff !== 0) {\n return diff;\n }\n }\n\n if (!preA && !preB) {\n return 0;\n }\n if (preA && !preB) {\n return -1;\n }\n if (!preA && preB) {\n return 1;\n }\n\n const partsA = preA!.split('.');\n const partsB = preB!.split('.');\n const len = Math.max(partsA.length, partsB.length);\n for (let i = 0; i < len; i++) {\n if (i >= partsA.length) {\n return -1;\n }\n if (i >= partsB.length) {\n return 1;\n }\n const numA = Number(partsA[i]);\n const numB = Number(partsB[i]);\n const aIsNum = !Number.isNaN(numA);\n const bIsNum = !Number.isNaN(numB);\n if (aIsNum && bIsNum) {\n if (numA !== numB) {\n return numA - numB;\n }\n } else if (aIsNum) {\n return -1;\n } else if (bIsNum) {\n return 1;\n } else {\n if (partsA[i] < partsB[i]) {\n return -1;\n }\n if (partsA[i] > partsB[i]) {\n return 1;\n }\n }\n }\n return 0;\n}\n", "import * as vscode from 'vscode';\nimport { detectPlatform, PlatformInfo } from '../toolchain/platform';\nimport {\n installToolchain,\n InstallProgress,\n InstallProgressCallback,\n InstallResult,\n} from '../toolchain/installation';\nimport { runDoctor } from '../toolchain/doctor';\nimport { updateStatusBar } from '../ui/statusBar';\n\n/** Guard against concurrent install attempts. */\nlet installing = false;\n\n/**\n * Register the inference.installToolchain command.\n * Returns the Disposable to add to context.subscriptions.\n */\nexport function registerInstallCommand(\n outputChannel: vscode.OutputChannel,\n statusBarItem: vscode.StatusBarItem,\n): vscode.Disposable {\n return vscode.commands.registerCommand(\n 'inference.installToolchain',\n async () => {\n if (installing) {\n vscode.window.showInformationMessage(\n 'Inference toolchain installation is already in progress.',\n );\n return;\n }\n\n const platform = detectPlatform();\n if (!platform) {\n vscode.window.showErrorMessage(\n `Inference: unsupported platform (${process.platform}-${process.arch}).`,\n );\n return;\n }\n\n installing = true;\n try {\n const result = await installWithProgress(\n platform,\n outputChannel,\n );\n outputChannel.appendLine(\n `Toolchain v${result.version} installed at ${result.infsPath}`,\n );\n vscode.commands.executeCommand(\n 'setContext', 'inference.toolchainInstalled', true,\n );\n\n const doctorResult = await runDoctor(result.infsPath);\n updateStatusBar(statusBarItem, doctorResult);\n vscode.commands.executeCommand('inference.refreshConfigView');\n vscode.commands.executeCommand('inference.applyTerminalPath');\n\n notifyInstallSuccess(result.version, result.doctorWarnings);\n } catch (err) {\n const message =\n err instanceof Error ? err.message : String(err);\n outputChannel.appendLine(`Installation failed: ${message}`);\n notifyInstallError(message);\n } finally {\n installing = false;\n }\n },\n );\n}\n\n/** Run the installation with a VS Code progress notification. */\nfunction installWithProgress(\n platform: PlatformInfo,\n outputChannel: vscode.OutputChannel,\n): Promise {\n return vscode.window.withProgress(\n {\n location: vscode.ProgressLocation.Notification,\n title: 'Inference Toolchain',\n cancellable: false,\n },\n async (progress) => {\n const onProgress: InstallProgressCallback = (\n p: InstallProgress,\n ) => {\n outputChannel.appendLine(p.message);\n if (p.stage === 'downloading' && p.bytesTotal) {\n const pct = Math.round(\n ((p.bytesReceived ?? 0) / p.bytesTotal) * 100,\n );\n progress.report({ message: `${p.message} (${pct}%)` });\n } else {\n progress.report({ message: p.message });\n }\n };\n return installToolchain(platform, onProgress);\n },\n );\n}\n\n/** Show a notification that the toolchain was installed successfully. */\nfunction notifyInstallSuccess(\n version: string,\n doctorWarnings: boolean,\n): void {\n if (doctorWarnings) {\n vscode.window\n .showWarningMessage(\n `Inference toolchain v${version} installed, but doctor reported issues. See output for details.`,\n 'Show Output',\n )\n .then((action) => {\n if (action === 'Show Output') {\n vscode.commands.executeCommand('inference.showOutput');\n }\n });\n } else {\n vscode.window.showInformationMessage(\n `Inference toolchain v${version} installed successfully.`,\n );\n }\n}\n\n/** Show an error notification for installation failure. */\nfunction notifyInstallError(errorMessage: string): void {\n vscode.window\n .showErrorMessage(\n `Inference toolchain installation failed: ${errorMessage}`,\n 'Retry',\n 'Download Manually',\n 'Settings',\n )\n .then((action) => {\n if (action === 'Retry') {\n vscode.commands.executeCommand('inference.installToolchain');\n } else if (action === 'Download Manually') {\n vscode.env.openExternal(\n vscode.Uri.parse(\n 'https://github.com/Inferara/inference/releases',\n ),\n );\n } else if (action === 'Settings') {\n vscode.commands.executeCommand(\n 'workbench.action.openSettings',\n 'inference.path',\n );\n }\n });\n}\n", "import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { PlatformInfo } from './platform';\nimport { inferenceHome } from './home';\nimport { fetchJson, downloadFile, sha256File } from '../utils/download';\nimport { extractArchive } from '../utils/extract';\nimport { exec } from '../utils/exec';\nimport {\n ReleaseEntry,\n findLatestRelease,\n} from './manifest';\n\nexport type { FileEntry, ReleaseEntry } from './manifest';\nexport { findLatestRelease } from './manifest';\n\n/** Progress updates emitted during installation. */\nexport interface InstallProgress {\n stage:\n | 'fetching-manifest'\n | 'downloading'\n | 'extracting'\n | 'installing'\n | 'verifying';\n message: string;\n bytesReceived?: number;\n bytesTotal?: number;\n}\n\nexport type InstallProgressCallback = (progress: InstallProgress) => void;\n\n/** Result of a successful installation. */\nexport interface InstallResult {\n infsPath: string;\n version: string;\n doctorWarnings: boolean;\n}\n\nconst DEFAULT_DIST_SERVER = 'https://inference-lang.org';\nconst RELEASES_PATH = '/releases.json';\n\nfunction manifestUrl(): string {\n const server = process.env['INFS_DIST_SERVER']?.trim();\n const base = server && server.length > 0\n ? server.replace(/\\/+$/, '')\n : DEFAULT_DIST_SERVER;\n return `${base}${RELEASES_PATH}`;\n}\n\n/**\n * Run the full installation flow.\n * Fetches manifest, downloads infs, extracts, runs `infs install`, verifies with `infs doctor`.\n * Throws on failure with a descriptive error message.\n */\nexport async function installToolchain(\n platform: PlatformInfo,\n onProgress?: InstallProgressCallback,\n): Promise {\n onProgress?.({\n stage: 'fetching-manifest',\n message: 'Fetching release manifest...',\n });\n\n const manifest = await fetchJson(manifestUrl());\n\n if (!Array.isArray(manifest)) {\n throw new Error('Invalid release manifest: expected an array.');\n }\n for (const entry of manifest) {\n if (\n typeof entry?.version !== 'string' ||\n typeof entry?.stable !== 'boolean' ||\n !Array.isArray(entry?.files)\n ) {\n throw new Error(\n `Invalid release manifest entry: ${JSON.stringify(entry)?.slice(0, 200)}`,\n );\n }\n }\n\n const match = findLatestRelease(manifest, platform);\n if (!match) {\n throw new Error(\n `No compatible infs release found for ${platform.id}.`,\n );\n }\n\n const { release, fileUrl, sha256 } = match;\n const version = release.version;\n\n onProgress?.({\n stage: 'downloading',\n message: `Downloading infs v${version}...`,\n });\n\n const destDir = path.join(inferenceHome(), 'bin');\n fs.mkdirSync(destDir, { recursive: true });\n\n const archiveName = `infs-${platform.id}${platform.archiveExtension}`;\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'infs-'));\n const archivePath = path.join(tmpDir, archiveName);\n\n try {\n await downloadFile(fileUrl, {\n destPath: archivePath,\n onProgress: (received, total) => {\n onProgress?.({\n stage: 'downloading',\n message: `Downloading infs v${version}...`,\n bytesReceived: received,\n bytesTotal: total,\n });\n },\n });\n\n const actualHash = await sha256File(archivePath);\n if (actualHash !== sha256) {\n throw new Error(\n `SHA-256 verification failed for infs v${version}. Expected ${sha256}, got ${actualHash}.`,\n );\n }\n\n onProgress?.({\n stage: 'extracting',\n message: 'Extracting archive...',\n });\n\n await extractArchive({ archivePath, destDir });\n } finally {\n try {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n } catch {\n // best-effort cleanup\n }\n }\n\n const infsPath = path.join(destDir, platform.binaryName);\n if (!fs.existsSync(infsPath)) {\n throw new Error(\n `infs binary not found at ${infsPath} after extraction.`,\n );\n }\n\n onProgress?.({\n stage: 'installing',\n message: 'Running infs install...',\n });\n\n const binDir = path.join(inferenceHome(), 'bin');\n const sep = process.platform === 'win32' ? ';' : ':';\n const augmentedPath = `${binDir}${sep}${process.env['PATH'] ?? ''}`;\n\n const installResult = await exec(infsPath, ['install'], {\n timeoutMs: 120_000,\n env: { PATH: augmentedPath },\n });\n if (installResult.exitCode !== 0) {\n throw new Error(\n `infs install failed (exit ${installResult.exitCode}): ${installResult.stderr || installResult.stdout}`,\n );\n }\n\n onProgress?.({\n stage: 'verifying',\n message: 'Verifying installation...',\n });\n\n let doctorWarnings = false;\n try {\n const doctorResult = await exec(infsPath, ['doctor'], {\n timeoutMs: 30_000,\n env: { PATH: augmentedPath },\n });\n if (doctorResult.exitCode !== 0) {\n doctorWarnings = true;\n } else {\n const { parseDoctorOutput } = await import('./doctor');\n const parsed = parseDoctorOutput(doctorResult.stdout);\n if (parsed.hasErrors || parsed.hasWarnings) {\n doctorWarnings = true;\n }\n }\n } catch {\n doctorWarnings = true;\n }\n\n return { infsPath, version, doctorWarnings };\n}\n", "import * as https from 'https';\nimport * as http from 'http';\nimport * as fs from 'fs';\nimport * as crypto from 'crypto';\n\n/** Callback invoked during download with bytes received and total (if known). */\nexport type ProgressCallback = (\n received: number,\n total: number | undefined,\n) => void;\n\nexport interface DownloadOptions {\n /** Absolute path where the downloaded file will be saved. */\n destPath: string;\n /** Optional progress callback. */\n onProgress?: ProgressCallback;\n /** Connection timeout in milliseconds (default: 15000). */\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 15_000;\nconst MAX_REDIRECTS = 5;\n\nconst SOCKET_TIMEOUT_MS = 15_000;\nconst MAX_JSON_RESPONSE_BYTES = 10 * 1024 * 1024;\n\n/**\n * Perform an HTTPS GET request following redirects.\n * Rejects HTTPS-to-HTTP downgrades.\n */\nfunction followRedirects(\n url: string,\n remaining: number,\n): Promise {\n return new Promise((resolve, reject) => {\n const parsed = new URL(url);\n const requester = parsed.protocol === 'https:' ? https : http;\n\n const req = requester.get(url, (res) => {\n const status = res.statusCode ?? 0;\n\n if (status >= 300 && status < 400 && res.headers.location) {\n if (remaining <= 0) {\n res.resume();\n reject(new Error(`Too many redirects fetching ${url}`));\n return;\n }\n const target = new URL(res.headers.location, url).href;\n const targetProtocol = new URL(target).protocol;\n if (parsed.protocol === 'https:' && targetProtocol === 'http:') {\n res.resume();\n reject(\n new Error(\n `Refusing HTTPS-to-HTTP redirect: ${url} -> ${target}`,\n ),\n );\n return;\n }\n res.resume();\n followRedirects(target, remaining - 1).then(resolve, reject);\n return;\n }\n\n if (status < 200 || status >= 300) {\n res.resume();\n reject(new Error(`HTTP ${status} fetching ${url}`));\n return;\n }\n\n resolve(res);\n });\n\n req.setTimeout(SOCKET_TIMEOUT_MS, () => {\n req.destroy(new Error(`Connection timed out for ${url}`));\n });\n\n req.on('error', (err) =>\n reject(new Error(`Network error fetching ${url}: ${err.message}`)),\n );\n });\n}\n\n/**\n * Fetch a JSON document from a URL via HTTPS GET.\n * Follows up to 5 redirects. Rejects HTTPS-to-HTTP downgrades.\n */\nexport function fetchJson(url: string): Promise {\n return new Promise((resolve, reject) => {\n followRedirects(url, MAX_REDIRECTS).then(\n (res) => {\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n res.on('data', (chunk: Buffer) => {\n totalBytes += chunk.length;\n if (totalBytes > MAX_JSON_RESPONSE_BYTES) {\n res.destroy();\n reject(new Error(`Response too large (>${MAX_JSON_RESPONSE_BYTES} bytes) from ${url}`));\n return;\n }\n chunks.push(chunk);\n });\n res.on('end', () => {\n try {\n const text = Buffer.concat(chunks).toString('utf-8');\n resolve(JSON.parse(text) as T);\n } catch (err) {\n reject(\n new Error(\n `Failed to parse JSON from ${url}: ${err instanceof Error ? err.message : err}`,\n ),\n );\n }\n });\n res.on('error', (err) =>\n reject(\n new Error(\n `Error reading response from ${url}: ${err.message}`,\n ),\n ),\n );\n },\n (err) => reject(err),\n );\n });\n}\n\n/**\n * Download a file from a URL to destPath using streaming.\n * Uses a temp file (.partial suffix) and renames on completion.\n * Follows redirects (GitHub releases redirect to CDN).\n */\nexport function downloadFile(\n url: string,\n options: DownloadOptions,\n): Promise {\n const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const partialPath = options.destPath + '.partial';\n\n return new Promise((resolve, reject) => {\n let settled = false;\n const settle = (fn: (...args: T) => void, ...args: T) => {\n if (!settled) {\n settled = true;\n fn(...args);\n }\n };\n\n followRedirects(url, MAX_REDIRECTS).then(\n (res) => {\n const totalStr = res.headers['content-length'];\n const total = totalStr ? parseInt(totalStr, 10) : undefined;\n let received = 0;\n\n const ws = fs.createWriteStream(partialPath);\n\n res.on('data', (chunk: Buffer) => {\n received += chunk.length;\n options.onProgress?.(received, total);\n });\n\n res.pipe(ws);\n\n const cleanup = () => {\n try {\n fs.unlinkSync(partialPath);\n } catch {\n // ignore\n }\n };\n\n // No-data timeout: if no bytes arrive for `timeout` ms, abort\n let dataTimer: ReturnType | undefined;\n const clearDataTimer = () => {\n if (dataTimer) {\n clearTimeout(dataTimer);\n dataTimer = undefined;\n }\n };\n const resetTimer = () => {\n clearDataTimer();\n dataTimer = setTimeout(() => {\n res.destroy();\n ws.destroy();\n cleanup();\n settle(reject, new Error(`Download timed out for ${url}`));\n }, timeout);\n };\n resetTimer();\n res.on('data', resetTimer);\n res.on('end', clearDataTimer);\n\n ws.on('finish', () => {\n clearDataTimer();\n try {\n fs.renameSync(partialPath, options.destPath);\n settle(resolve);\n } catch (err) {\n cleanup();\n settle(\n reject,\n new Error(\n `Failed to save download to ${options.destPath}: ${err instanceof Error ? err.message : err}`,\n ),\n );\n }\n });\n\n ws.on('error', (err) => {\n clearDataTimer();\n res.destroy();\n cleanup();\n settle(\n reject,\n new Error(\n `Failed to write download: ${err.message}`,\n ),\n );\n });\n\n res.on('error', (err) => {\n clearDataTimer();\n ws.destroy();\n cleanup();\n settle(\n reject,\n new Error(\n `Download stream error from ${url}: ${err.message}`,\n ),\n );\n });\n },\n (err) => settle(reject, err),\n );\n });\n}\n\n/**\n * Compute SHA-256 hash of a file.\n * Returns lowercase hex string.\n */\nexport function sha256File(filePath: string): Promise {\n return new Promise((resolve, reject) => {\n const hash = crypto.createHash('sha256');\n const stream = fs.createReadStream(filePath);\n stream.on('data', (chunk) => hash.update(chunk));\n stream.on('end', () => resolve(hash.digest('hex')));\n stream.on('error', (err) =>\n reject(\n new Error(\n `Failed to compute SHA-256 for ${filePath}: ${err.message}`,\n ),\n ),\n );\n });\n}\n", "import * as fs from 'fs';\nimport * as path from 'path';\nimport { exec } from './exec';\n\nexport interface ExtractOptions {\n /** Path to the archive file. */\n archivePath: string;\n /** Directory to extract into (created if it doesn't exist). */\n destDir: string;\n}\n\n/**\n * Extract an archive to the destination directory.\n * Detects format from file extension (.tar.gz or .zip).\n * On Unix, sets executable permission on extracted binaries.\n */\nexport async function extractArchive(options: ExtractOptions): Promise {\n fs.mkdirSync(options.destDir, { recursive: true });\n\n if (\n options.archivePath.endsWith('.tar.gz') ||\n options.archivePath.endsWith('.tgz')\n ) {\n await extractTarGz(options.archivePath, options.destDir);\n } else if (options.archivePath.endsWith('.zip')) {\n await extractZip(options.archivePath, options.destDir);\n } else {\n throw new Error(\n `Unsupported archive format: ${path.basename(options.archivePath)}`,\n );\n }\n\n if (process.platform !== 'win32') {\n setExecutablePermissions(options.destDir);\n }\n}\n\nasync function extractTarGz(\n archivePath: string,\n destDir: string,\n): Promise {\n const result = await exec('tar', ['-xzf', archivePath, '-C', destDir]);\n if (result.exitCode !== 0) {\n throw new Error(\n `tar extraction failed (exit ${result.exitCode}): ${result.stderr}`,\n );\n }\n}\n\n/** Escape a string for use inside a PowerShell single-quoted literal. */\nfunction escapePowerShellSingleQuote(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n\nasync function extractZip(\n archivePath: string,\n destDir: string,\n): Promise {\n const safePath = escapePowerShellSingleQuote(archivePath);\n const safeDest = escapePowerShellSingleQuote(destDir);\n const result = await exec('powershell', [\n '-NoProfile',\n '-Command',\n `Expand-Archive -LiteralPath '${safePath}' -DestinationPath '${safeDest}' -Force`,\n ]);\n if (result.exitCode !== 0) {\n throw new Error(\n `zip extraction failed (exit ${result.exitCode}): ${result.stderr}`,\n );\n }\n}\n\n/** Set executable permissions on files in the directory (non-recursive, top level only). */\nfunction setExecutablePermissions(dir: string): void {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (entry.isFile()) {\n try {\n fs.chmodSync(path.join(dir, entry.name), 0o755);\n } catch {\n // best-effort per file\n }\n }\n }\n}\n", "import { compareSemver } from '../utils/semver';\n\n/** A platform-specific file entry from the manifest. */\nexport interface FileEntry {\n url: string;\n sha256: string;\n}\n\n/** A single release entry from the manifest. */\nexport interface ReleaseEntry {\n version: string;\n stable: boolean;\n files: FileEntry[];\n}\n\n/** Minimal platform info needed for manifest matching. */\nexport interface ManifestPlatform {\n id: string;\n}\n\n/**\n * Extract tool name from a manifest file URL.\n * URL format: `https://.../tool-os-arch.ext` (e.g., `infs-linux-x64.tar.gz`).\n */\nexport function toolFromUrl(url: string): string {\n const filename = url.split('/').pop() ?? '';\n return filename.split('-')[0] ?? '';\n}\n\n/**\n * Extract OS name from a manifest file URL.\n * URL format: `https://.../tool-os-arch.ext` (e.g., `infs-linux-x64.tar.gz`).\n */\nexport function osFromUrl(url: string): string {\n const filename = url.split('/').pop() ?? '';\n const parts = filename.split('-');\n return parts.length > 1 ? parts[1] : '';\n}\n\n/** Map platform ID to the OS string used in manifest URLs. */\nexport function platformOs(platform: ManifestPlatform): string {\n if (platform.id === 'linux-x64') {\n return 'linux';\n }\n if (platform.id === 'macos-arm64') {\n return 'macos';\n }\n if (platform.id === 'windows-x64') {\n return 'windows';\n }\n return '';\n}\n\n/**\n * Find the latest release from the manifest for the given platform.\n * Matches the `infs` tool artifact for the platform's OS.\n * Returns the release entry, file URL, and sha256, or null if not found.\n */\nexport function findLatestRelease(\n manifest: ReleaseEntry[],\n platform: ManifestPlatform,\n): { release: ReleaseEntry; fileUrl: string; sha256: string } | null {\n if (manifest.length === 0) {\n return null;\n }\n\n const sorted = [...manifest].sort((a, b) =>\n compareSemver(b.version, a.version),\n );\n\n const os = platformOs(platform);\n\n for (const release of sorted) {\n const file = release.files.find(\n (f) => toolFromUrl(f.url) === 'infs' && osFromUrl(f.url) === os,\n );\n if (file) {\n return { release, fileUrl: file.url, sha256: file.sha256 };\n }\n }\n\n return null;\n}\n", "import * as vscode from 'vscode';\nimport { DoctorResult } from '../toolchain/doctor';\nimport { determineStatusBarState, StatusBarIcon } from './statusBarState';\n\nconst ICON_MAP: Record = {\n loading: '$(loading~spin)',\n dash: '$(dash)',\n check: '$(check)',\n warning: '$(warning)',\n error: '$(error)',\n};\n\nconst BACKGROUND_MAP: Record = {\n none: undefined,\n warning: new vscode.ThemeColor('statusBarItem.warningBackground'),\n error: new vscode.ThemeColor('statusBarItem.errorBackground'),\n};\n\n/**\n * Create the Inference status bar item.\n * Positioned on the left side with low priority.\n * Clicking triggers the inference.runDoctor command.\n */\nexport function createStatusBar(): vscode.StatusBarItem {\n const item = vscode.window.createStatusBarItem(\n vscode.StatusBarAlignment.Left,\n 0,\n );\n item.command = 'inference.runDoctor';\n item.text = '$(loading~spin) Inference';\n item.tooltip = 'Inference: Checking toolchain...';\n item.show();\n return item;\n}\n\n/**\n * Update the status bar to reflect doctor results.\n *\n * - null: toolchain not found (grey dash icon)\n * - hasErrors: red error icon\n * - hasWarnings: yellow warning icon\n * - all OK: green check icon\n */\nexport function updateStatusBar(\n item: vscode.StatusBarItem,\n result: DoctorResult | null,\n): void {\n const state = determineStatusBarState(result);\n item.text = `${ICON_MAP[state.icon]} ${state.label}`;\n item.tooltip = state.tooltip;\n item.backgroundColor = BACKGROUND_MAP[state.background];\n}\n", "import { DoctorResult } from '../toolchain/doctor';\n\nexport type StatusBarIcon = 'loading' | 'dash' | 'check' | 'warning' | 'error';\nexport type StatusBarBackground = 'none' | 'warning' | 'error';\n\nexport interface StatusBarState {\n icon: StatusBarIcon;\n label: string;\n tooltip: string;\n background: StatusBarBackground;\n}\n\n/**\n * Determine the status bar display state from a doctor result.\n *\n * - null: toolchain not found (dash icon)\n * - hasErrors: red error icon\n * - hasWarnings: yellow warning icon\n * - all OK: green check icon\n */\nexport function determineStatusBarState(result: DoctorResult | null): StatusBarState {\n if (result === null) {\n return {\n icon: 'dash',\n label: 'Inference',\n tooltip: 'Inference: Toolchain not found. Click to run doctor.',\n background: 'none',\n };\n }\n\n if (result.hasErrors) {\n return {\n icon: 'error',\n label: 'Inference',\n tooltip: `Inference: ${result.summary || 'Toolchain errors detected'}`,\n background: 'error',\n };\n }\n\n if (result.hasWarnings) {\n return {\n icon: 'warning',\n label: 'Inference',\n tooltip: `Inference: ${result.summary || 'Toolchain warnings detected'}`,\n background: 'warning',\n };\n }\n\n return {\n icon: 'check',\n label: 'Inference',\n tooltip: 'Inference: Toolchain healthy',\n background: 'none',\n };\n}\n", "import * as vscode from 'vscode';\nimport { detectInfs } from '../toolchain/detection';\nimport { exec, ExecResult } from '../utils/exec';\nimport {\n ComponentName,\n KNOWN_COMPONENTS,\n componentAddArgs,\n} from '../toolchain/components';\n\n/** Guard against concurrent component install attempts. */\nlet installing = false;\n\n/**\n * Timeout for a component install. Managed downloads can be ~100 MB, far\n * beyond the default exec timeout, so allow up to ten minutes.\n */\nconst INSTALL_TIMEOUT_MS = 600_000;\n\n/**\n * Register the inference.installComponent command.\n * Returns the Disposable to add to context.subscriptions.\n */\nexport function registerInstallComponentCommand(\n outputChannel: vscode.OutputChannel,\n): vscode.Disposable {\n return vscode.commands.registerCommand(\n 'inference.installComponent',\n async (component: string = 'wasm-opt') => {\n if (installing) {\n vscode.window.showInformationMessage(\n 'Inference component installation is already in progress.',\n );\n return;\n }\n\n if (!isKnownComponent(component)) {\n vscode.window.showErrorMessage(\n `Inference: unknown component '${component}'.`,\n );\n return;\n }\n\n const detection = detectInfs();\n if (!detection) {\n vscode.window\n .showWarningMessage(\n 'Inference toolchain not found. Install it first.',\n 'Install',\n )\n .then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand(\n 'inference.installToolchain',\n );\n }\n });\n return;\n }\n\n installing = true;\n try {\n const result = await installWithProgress(\n detection.path,\n component,\n outputChannel,\n );\n if (result.stdout) {\n outputChannel.appendLine(result.stdout);\n }\n if (result.stderr) {\n outputChannel.appendLine(result.stderr);\n }\n\n if (result.exitCode === 0) {\n vscode.window.showInformationMessage(\n `Inference: component '${component}' installed.`,\n );\n vscode.commands.executeCommand('inference.runDoctor');\n } else {\n notifyInstallError(component);\n }\n } catch (err) {\n const message =\n err instanceof Error ? err.message : String(err);\n outputChannel.appendLine(\n `Component installation failed: ${message}`,\n );\n notifyInstallError(component);\n } finally {\n installing = false;\n }\n },\n );\n}\n\n/** Type guard: whether a string is a known component name. */\nfunction isKnownComponent(name: string): name is ComponentName {\n return (KNOWN_COMPONENTS as readonly string[]).includes(name);\n}\n\n/** Run `infs component add ` with a VS Code progress notification. */\nfunction installWithProgress(\n infsPath: string,\n component: ComponentName,\n outputChannel: vscode.OutputChannel,\n): Promise {\n return vscode.window.withProgress(\n {\n location: vscode.ProgressLocation.Notification,\n title: 'Inference Component',\n cancellable: false,\n },\n async (progress) => {\n progress.report({ message: `Installing ${component}...` });\n outputChannel.appendLine(`Installing component '${component}'...`);\n return exec(infsPath, componentAddArgs(component), {\n timeoutMs: INSTALL_TIMEOUT_MS,\n });\n },\n );\n}\n\n/** Show an error notification for component installation failure. */\nfunction notifyInstallError(component: ComponentName): void {\n vscode.window\n .showErrorMessage(\n `Inference: failed to install component '${component}'. See output for details.`,\n 'Show Output',\n 'Retry',\n )\n .then((action) => {\n if (action === 'Show Output') {\n vscode.commands.executeCommand('inference.showOutput');\n } else if (action === 'Retry') {\n vscode.commands.executeCommand(\n 'inference.installComponent',\n component,\n );\n }\n });\n}\n", "import { DoctorResult } from './doctor';\n\n/**\n * Toolchain components the CLI can provision via `infs component add`.\n *\n * Mirrors the `KNOWN_COMPONENTS` list in the `infs component` command\n * (`apps/infs/src/commands/component.rs`); keep the two in sync.\n */\nexport const KNOWN_COMPONENTS = ['wasm-opt'] as const;\n\n/** A component name understood by `infs component`. */\nexport type ComponentName = (typeof KNOWN_COMPONENTS)[number];\n\n/** Build the argv for `infs component add `. */\nexport function componentAddArgs(component: ComponentName): string[] {\n return ['component', 'add', component];\n}\n\n/**\n * Whether a doctor result indicates the wasm-opt component needs attention,\n * i.e. any check named `wasm-opt` reported a warning or failure.\n */\nexport function wasmOptNeedsAttention(result: DoctorResult): boolean {\n return result.checks.some(\n (check) =>\n check.name === 'wasm-opt' &&\n (check.status === 'warn' || check.status === 'fail'),\n );\n}\n", "import * as vscode from 'vscode';\nimport { detectInfs } from '../toolchain/detection';\nimport { runDoctor, DoctorResult } from '../toolchain/doctor';\nimport { formatDoctorChecks } from '../toolchain/doctorFormat';\nimport { wasmOptNeedsAttention } from '../toolchain/components';\nimport { updateStatusBar } from '../ui/statusBar';\n\n/** Guard against concurrent doctor runs. */\nlet running = false;\n\n/**\n * Register the inference.runDoctor command.\n *\n * When invoked: detect infs \u2192 run doctor \u2192 display results in output\n * channel \u2192 update status bar \u2192 show notification summary.\n */\nexport function registerDoctorCommand(\n outputChannel: vscode.OutputChannel,\n statusBarItem: vscode.StatusBarItem,\n): vscode.Disposable {\n return vscode.commands.registerCommand(\n 'inference.runDoctor',\n async () => {\n if (running) {\n return;\n }\n\n const detection = detectInfs();\n if (!detection) {\n outputChannel.appendLine('Doctor: infs binary not found.');\n updateStatusBar(statusBarItem, null);\n vscode.window\n .showWarningMessage(\n 'Inference toolchain not found. Install it first.',\n 'Install',\n )\n .then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand(\n 'inference.installToolchain',\n );\n }\n });\n return;\n }\n\n running = true;\n try {\n outputChannel.appendLine(\n `Running infs doctor (${detection.path})...`,\n );\n const result = await runDoctor(detection.path);\n\n if (!result) {\n outputChannel.appendLine(\n 'Doctor: failed to execute infs doctor.',\n );\n updateStatusBar(statusBarItem, null);\n vscode.window.showErrorMessage(\n 'Inference: Failed to run doctor. See output for details.',\n );\n return;\n }\n\n for (const line of formatDoctorChecks(result)) {\n outputChannel.appendLine(line);\n }\n updateStatusBar(statusBarItem, result);\n vscode.commands.executeCommand('inference.refreshConfigView');\n\n if (result.hasErrors) {\n const actions = ['Show Output'];\n if (wasmOptNeedsAttention(result)) {\n actions.push('Install wasm-opt');\n }\n vscode.window\n .showErrorMessage(\n `Inference doctor: ${result.summary}`,\n ...actions,\n )\n .then((action) => {\n if (action === 'Show Output') {\n outputChannel.show();\n } else if (action === 'Install wasm-opt') {\n vscode.commands.executeCommand(\n 'inference.installComponent',\n 'wasm-opt',\n );\n }\n });\n } else if (result.hasWarnings) {\n const actions = ['Show Output'];\n if (wasmOptNeedsAttention(result)) {\n actions.push('Install wasm-opt');\n }\n vscode.window\n .showWarningMessage(\n `Inference doctor: ${result.summary}`,\n ...actions,\n )\n .then((action) => {\n if (action === 'Show Output') {\n outputChannel.show();\n } else if (action === 'Install wasm-opt') {\n vscode.commands.executeCommand(\n 'inference.installComponent',\n 'wasm-opt',\n );\n }\n });\n } else {\n vscode.window.showInformationMessage(\n 'Inference: Toolchain is healthy.',\n );\n }\n } finally {\n running = false;\n }\n },\n );\n}\n\n", "import { DoctorResult } from './doctor';\n\nconst STATUS_TAGS: Record = {\n ok: '[OK] ',\n warn: '[WARN]',\n fail: '[FAIL]',\n};\n\n/**\n * Format doctor check results into display lines.\n *\n * Each check is formatted as: ` [TAG] name: message`\n * If a summary is present, it is appended after a blank line.\n * Separator lines wrap the output.\n */\nexport function formatDoctorChecks(result: DoctorResult): string[] {\n const lines: string[] = [];\n lines.push('--- Doctor Report ---');\n for (const check of result.checks) {\n const tag = STATUS_TAGS[check.status] ?? `[${check.status.toUpperCase()}]`;\n lines.push(` ${tag} ${check.name}: ${check.message}`);\n }\n if (result.summary) {\n lines.push('');\n lines.push(result.summary);\n }\n lines.push('---------------------');\n return lines;\n}\n", "import * as vscode from 'vscode';\nimport { detectInfs } from '../toolchain/detection';\nimport { fetchVersions, getCurrentVersion } from '../toolchain/versions';\nimport { buildVersionPickItems } from '../toolchain/versionPicker';\nimport { performVersionChange } from './versionChange';\n\n/** Guard against concurrent select operations. */\nlet selecting = false;\n\n/**\n * Register the inference.selectVersion command.\n * Shows a QuickPick with available toolchain versions and switches to the selected one.\n */\nexport function registerSelectVersionCommand(\n outputChannel: vscode.OutputChannel,\n): vscode.Disposable {\n return vscode.commands.registerCommand(\n 'inference.selectVersion',\n async () => {\n if (selecting) {\n vscode.window.showInformationMessage(\n 'Version selection is already in progress.',\n );\n return;\n }\n\n const detection = detectInfs();\n if (!detection) {\n vscode.window\n .showWarningMessage(\n 'Inference toolchain not found. Install it first.',\n 'Install',\n )\n .then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand(\n 'inference.installToolchain',\n );\n }\n });\n return;\n }\n\n selecting = true;\n try {\n const versions = await fetchVersions(detection.path);\n if (!versions) {\n vscode.window.showErrorMessage(\n 'Inference: Failed to fetch available versions.',\n );\n return;\n }\n\n const currentVersion = await getCurrentVersion(detection.path);\n\n const items = buildVersionPickItems(versions, currentVersion);\n\n if (items.length === 0) {\n vscode.window.showInformationMessage(\n 'No toolchain versions available for this platform.',\n );\n return;\n }\n\n const picked = await vscode.window.showQuickPick(items, {\n placeHolder: 'Select toolchain version',\n matchOnDescription: true,\n });\n\n if (!picked) {\n return;\n }\n\n const selectedVersion = picked.label;\n if (selectedVersion === currentVersion) {\n vscode.window.showInformationMessage(\n `Already using toolchain v${selectedVersion}.`,\n );\n return;\n }\n\n await performVersionChange(detection.path, selectedVersion, outputChannel, 'Switching to');\n } finally {\n selecting = false;\n }\n },\n );\n}\n", "import { exec } from '../utils/exec';\n\n/** Version info returned by `infs versions --json`. */\nexport interface VersionInfo {\n version: string;\n stable: boolean;\n platforms: string[];\n available_for_current: boolean;\n}\n\n/**\n * Parse the JSON output of `infs versions --json`.\n * Returns an empty array if the output is invalid.\n */\nexport function parseVersionsOutput(stdout: string): VersionInfo[] {\n try {\n const parsed = JSON.parse(stdout);\n if (!Array.isArray(parsed)) {\n return [];\n }\n return parsed;\n } catch {\n return [];\n }\n}\n\n/**\n * Parse the version string from `infs version` output.\n * Expected format: \"infs X.Y.Z\"\n * Returns the version string or null on parse failure.\n */\nexport function parseCurrentVersion(stdout: string): string | null {\n const match = stdout.match(/^infs\\s+(\\S+)/);\n return match ? match[1] : null;\n}\n\n/**\n * Run `infs versions --json` and parse the output.\n * Returns null if the command fails.\n */\nexport async function fetchVersions(\n infsPath: string,\n): Promise {\n try {\n const result = await exec(infsPath, ['versions', '--json'], {\n timeoutMs: 30_000,\n });\n if (result.exitCode !== 0) {\n return null;\n }\n return parseVersionsOutput(result.stdout);\n } catch {\n return null;\n }\n}\n\n/**\n * Run `infs version` and parse the current version.\n * Returns null if the command fails or the output is unexpected.\n */\nexport async function getCurrentVersion(\n infsPath: string,\n): Promise {\n try {\n const result = await exec(infsPath, ['version'], {\n timeoutMs: 10_000,\n });\n if (result.exitCode !== 0) {\n return null;\n }\n return parseCurrentVersion(result.stdout);\n } catch {\n return null;\n }\n}\n\n/** Result of an install-and-set-default operation. */\nexport interface SwitchResult {\n success: boolean;\n installedButNotDefault: boolean;\n error?: string;\n}\n\n/**\n * Install a toolchain version and set it as default.\n *\n * Runs `infs install VERSION` followed by `infs default VERSION`.\n * Handles the partial-success case where install succeeds but setting default fails.\n */\nexport async function installAndSetDefault(\n infsPath: string,\n version: string,\n): Promise {\n const installResult = await exec(infsPath, ['install', version], {\n timeoutMs: 120_000,\n });\n if (installResult.exitCode !== 0) {\n const detail = installResult.stderr || installResult.stdout;\n return { success: false, installedButNotDefault: false, error: detail };\n }\n\n const defaultResult = await exec(infsPath, ['default', version], {\n timeoutMs: 30_000,\n });\n if (defaultResult.exitCode !== 0) {\n const detail = defaultResult.stderr || defaultResult.stdout;\n return { success: false, installedButNotDefault: true, error: detail };\n }\n\n return { success: true, installedButNotDefault: false };\n}\n", "import { VersionInfo } from './versions';\nimport { compareSemver } from '../utils/semver';\n\nexport interface PickItem {\n label: string;\n description?: string;\n}\n\n/**\n * Build QuickPick items from available versions.\n *\n * - Filters to `available_for_current` versions only\n * - Sorts descending by semver\n * - Tags the current version with \"(current)\" and stable versions with \"(stable)\"\n * - Moves the current version to the top of the list\n */\nexport function buildVersionPickItems(\n versions: VersionInfo[],\n currentVersion: string | null,\n): PickItem[] {\n const available = versions\n .filter((v) => v.available_for_current)\n .sort((a, b) => compareSemver(b.version, a.version));\n\n const items: PickItem[] = available.map((v) => {\n const tags: string[] = [];\n if (v.version === currentVersion) {\n tags.push('current');\n }\n if (v.stable) {\n tags.push('stable');\n }\n return {\n label: v.version,\n description: tags.length > 0 ? `(${tags.join(', ')})` : undefined,\n };\n });\n\n if (currentVersion) {\n const idx = items.findIndex((i) => i.label === currentVersion);\n if (idx > 0) {\n const [item] = items.splice(idx, 1);\n items.unshift(item);\n }\n }\n\n return items;\n}\n", "import * as vscode from 'vscode';\nimport { installAndSetDefault } from '../toolchain/versions';\n\n/**\n * Perform a version change (install + set default) with progress UI.\n *\n * Shared by both the \"Select Version\" and \"Update Toolchain\" commands.\n */\nexport async function performVersionChange(\n infsPath: string,\n version: string,\n outputChannel: vscode.OutputChannel,\n actionVerb: string,\n): Promise {\n await vscode.window.withProgress(\n {\n location: vscode.ProgressLocation.Notification,\n title: 'Inference Toolchain',\n cancellable: false,\n },\n async (progress) => {\n progress.report({ message: `${actionVerb} v${version}...` });\n outputChannel.appendLine(`${actionVerb} toolchain v${version}...`);\n\n const result = await installAndSetDefault(infsPath, version);\n\n if (result.success) {\n outputChannel.appendLine(\n `${actionVerb} toolchain v${version} complete.`,\n );\n vscode.commands.executeCommand(\n 'setContext', 'inference.toolchainInstalled', true,\n );\n vscode.commands.executeCommand('inference.applyTerminalPath');\n vscode.commands.executeCommand('inference.runDoctor');\n vscode.window\n .showInformationMessage(\n `Inference toolchain ${actionVerb.toLowerCase()} to v${version}.`,\n 'Show Output',\n )\n .then((action) => {\n if (action === 'Show Output') {\n outputChannel.show();\n }\n });\n return;\n }\n\n outputChannel.appendLine(\n `${actionVerb} failed: ${result.error}`,\n );\n\n if (result.installedButNotDefault) {\n vscode.window\n .showWarningMessage(\n `Inference: v${version} was installed but could not be set as default. Run \\`infs default ${version}\\` manually.`,\n 'Show Output',\n )\n .then((action) => {\n if (action === 'Show Output') {\n outputChannel.show();\n }\n });\n } else {\n vscode.window.showErrorMessage(\n `Inference: Failed to install v${version}: ${result.error}`,\n );\n }\n },\n );\n}\n", "import * as vscode from 'vscode';\nimport { detectInfs } from '../toolchain/detection';\nimport { fetchVersions, getCurrentVersion } from '../toolchain/versions';\nimport { checkUpdateAvailable } from '../toolchain/updateCheck';\nimport { getSettings } from '../config/settings';\nimport { performVersionChange } from './versionChange';\n\n/** Guard against concurrent update operations. */\nlet updating = false;\n\n/**\n * Register the inference.updateToolchain command.\n * Checks for updates and prompts the user to install if available.\n */\nexport function registerUpdateCommand(\n outputChannel: vscode.OutputChannel,\n): vscode.Disposable {\n return vscode.commands.registerCommand(\n 'inference.updateToolchain',\n async () => {\n if (updating) {\n vscode.window.showInformationMessage(\n 'Update check is already in progress.',\n );\n return;\n }\n\n const detection = detectInfs();\n if (!detection) {\n vscode.window\n .showWarningMessage(\n 'Inference toolchain not found. Install it first.',\n 'Install',\n )\n .then((action) => {\n if (action === 'Install') {\n vscode.commands.executeCommand(\n 'inference.installToolchain',\n );\n }\n });\n return;\n }\n\n updating = true;\n try {\n await checkForUpdatesImpl(detection.path, outputChannel, true);\n } finally {\n updating = false;\n }\n },\n );\n}\n\n/**\n * Check for toolchain updates on activation.\n * Respects the `inference.checkForUpdates` setting.\n * This is a no-op if checks are disabled.\n */\nexport async function checkForUpdates(\n infsPath: string,\n outputChannel: vscode.OutputChannel,\n): Promise {\n if (updating) {\n return;\n }\n const settings = getSettings();\n if (!settings.checkForUpdates) {\n return;\n }\n updating = true;\n try {\n await checkForUpdatesImpl(infsPath, outputChannel, false);\n } finally {\n updating = false;\n }\n}\n\nasync function checkForUpdatesImpl(\n infsPath: string,\n outputChannel: vscode.OutputChannel,\n userInitiated: boolean,\n): Promise {\n const currentVersion = await getCurrentVersion(infsPath);\n if (!currentVersion) {\n outputChannel.appendLine('Update check: could not determine current version.');\n if (userInitiated) {\n vscode.window.showErrorMessage(\n 'Inference: Could not determine the current toolchain version.',\n );\n }\n return;\n }\n\n outputChannel.appendLine(`Update check: current version is ${currentVersion}.`);\n\n const versions = await fetchVersions(infsPath);\n if (!versions) {\n outputChannel.appendLine('Update check: failed to fetch available versions.');\n if (userInitiated) {\n vscode.window.showErrorMessage(\n 'Inference: Failed to check for updates.',\n );\n }\n return;\n }\n\n const result = checkUpdateAvailable(currentVersion, versions);\n\n switch (result.status) {\n case 'no-current-version':\n outputChannel.appendLine('Update check: could not determine current version.');\n if (userInitiated) {\n vscode.window.showErrorMessage(\n 'Inference: Could not determine the current toolchain version.',\n );\n }\n return;\n\n case 'no-versions':\n outputChannel.appendLine('Update check: no versions available for this platform.');\n if (userInitiated) {\n vscode.window.showInformationMessage(\n 'Inference: No toolchain versions available for this platform.',\n );\n }\n return;\n\n case 'up-to-date':\n outputChannel.appendLine(\n `Update check: toolchain is up to date (v${result.version}).`,\n );\n if (userInitiated) {\n vscode.window.showInformationMessage(\n `Inference toolchain is up to date (v${result.version}).`,\n );\n }\n return;\n\n case 'update-available': {\n outputChannel.appendLine(\n `Update check: v${result.latest} available (current: v${result.current}).`,\n );\n\n const action = await vscode.window.showInformationMessage(\n `Inference toolchain update available: v${result.latest} (current: v${result.current})`,\n 'Update',\n 'Release Notes',\n );\n\n if (action === 'Update') {\n await performVersionChange(infsPath, result.latest, outputChannel, 'Updating to');\n } else if (action === 'Release Notes') {\n vscode.env.openExternal(\n vscode.Uri.parse(\n `https://github.com/Inferara/inference/releases/tag/v${result.latest}`,\n ),\n );\n }\n return;\n }\n }\n}\n", "import { VersionInfo } from './versions';\nimport { compareSemver } from '../utils/semver';\n\nexport type UpdateCheckResult =\n | { status: 'up-to-date'; version: string }\n | { status: 'update-available'; current: string; latest: string }\n | { status: 'no-versions' }\n | { status: 'no-current-version' };\n\n/**\n * Determine whether an update is available based on current version and available versions.\n *\n * Filters to `available_for_current` versions and compares the highest against current.\n */\nexport function checkUpdateAvailable(\n currentVersion: string | null,\n versions: VersionInfo[] | null,\n): UpdateCheckResult {\n if (!currentVersion) {\n return { status: 'no-current-version' };\n }\n\n if (!versions) {\n return { status: 'no-versions' };\n }\n\n const candidates = versions.filter((v) => v.available_for_current);\n\n if (candidates.length === 0) {\n return { status: 'no-versions' };\n }\n\n const sorted = [...candidates].sort((a, b) =>\n compareSemver(b.version, a.version),\n );\n const latest = sorted[0];\n\n if (compareSemver(currentVersion, latest.version) >= 0) {\n return { status: 'up-to-date', version: currentVersion };\n }\n\n return {\n status: 'update-available',\n current: currentVersion,\n latest: latest.version,\n };\n}\n", "import * as vscode from 'vscode';\nimport { detectInfs, InfsDetection } from '../toolchain/detection';\nimport { inferenceHome } from '../toolchain/home';\nimport { detectPlatform } from '../toolchain/platform';\nimport { getSettings } from '../config/settings';\nimport { exec } from '../utils/exec';\nimport { DoctorResult } from '../toolchain/doctor';\n\ntype GroupId = 'toolchain' | 'settings';\n\nexport class ConfigItem extends vscode.TreeItem {\n constructor(\n label: string,\n public readonly kind: 'group' | 'property',\n collapsible: vscode.TreeItemCollapsibleState,\n public readonly groupId?: GroupId,\n public readonly settingKey?: string,\n public readonly copyValue?: string,\n ) {\n super(label, collapsible);\n\n if (kind === 'group') {\n this.iconPath = new vscode.ThemeIcon(\n groupId === 'toolchain' ? 'tools' : 'gear',\n );\n }\n\n if (settingKey) {\n this.command = {\n title: 'Open Setting',\n command: 'workbench.action.openSettings',\n arguments: [settingKey],\n };\n }\n\n if (copyValue) {\n this.contextValue = 'inference.configPath';\n }\n }\n}\n\nexport class InferenceConfigProvider\n implements vscode.TreeDataProvider\n{\n private _onDidChangeTreeData = new vscode.EventEmitter<\n ConfigItem | undefined | null\n >();\n readonly onDidChangeTreeData = this._onDidChangeTreeData.event;\n\n private detection: InfsDetection | null = null;\n private version: string | null = null;\n private doctorResult: DoctorResult | null = null;\n\n refresh(detection?: InfsDetection | null, doctorResult?: DoctorResult | null): void {\n if (detection !== undefined) {\n this.detection = detection;\n }\n if (doctorResult !== undefined) {\n this.doctorResult = doctorResult;\n }\n this._onDidChangeTreeData.fire(undefined);\n }\n\n getTreeItem(element: ConfigItem): vscode.TreeItem {\n return element;\n }\n\n async getChildren(element?: ConfigItem): Promise {\n if (!element) {\n return [\n new ConfigItem(\n 'Toolchain',\n 'group',\n vscode.TreeItemCollapsibleState.Expanded,\n 'toolchain',\n ),\n new ConfigItem(\n 'Settings',\n 'group',\n vscode.TreeItemCollapsibleState.Expanded,\n 'settings',\n ),\n ];\n }\n\n if (element.groupId === 'toolchain') {\n return this.getToolchainChildren();\n }\n\n if (element.groupId === 'settings') {\n return this.getSettingsChildren();\n }\n\n return [];\n }\n\n private async getToolchainChildren(): Promise {\n const detection = this.detection ?? detectInfs();\n const items: ConfigItem[] = [];\n\n if (!detection) {\n const item = new ConfigItem(\n 'infs: not found',\n 'property',\n vscode.TreeItemCollapsibleState.None,\n );\n item.iconPath = new vscode.ThemeIcon('error');\n item.command = {\n title: 'Install Toolchain',\n command: 'inference.installToolchain',\n arguments: [],\n };\n items.push(item);\n return items;\n }\n\n const infsItem = new ConfigItem(\n `infs: ${detection.path} (${detection.source})`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n undefined,\n detection.path,\n );\n infsItem.iconPath = new vscode.ThemeIcon('file-binary');\n items.push(infsItem);\n\n const version = await this.resolveVersion(detection.path);\n const versionItem = new ConfigItem(\n `Version: ${version ?? 'unknown'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n );\n versionItem.iconPath = new vscode.ThemeIcon('tag');\n items.push(versionItem);\n\n const home = inferenceHome();\n const homeIsDefault = !process.env['INFERENCE_HOME'];\n const homeItem = new ConfigItem(\n `Home: ${home} (${homeIsDefault ? 'default' : 'env'})`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n undefined,\n home,\n );\n homeItem.iconPath = new vscode.ThemeIcon('home');\n items.push(homeItem);\n\n const platform = detectPlatform();\n const platformItem = new ConfigItem(\n `Platform: ${platform?.id ?? 'unknown'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n );\n platformItem.iconPath = new vscode.ThemeIcon('device-desktop');\n items.push(platformItem);\n\n const status = this.doctorResult\n ? this.doctorResult.hasErrors\n ? 'errors'\n : this.doctorResult.hasWarnings\n ? 'warnings'\n : 'healthy'\n : 'unknown';\n const statusIcon = this.doctorResult\n ? this.doctorResult.hasErrors\n ? 'error'\n : this.doctorResult.hasWarnings\n ? 'warning'\n : 'pass'\n : 'question';\n const statusItem = new ConfigItem(\n `Status: ${status}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n );\n statusItem.iconPath = new vscode.ThemeIcon(statusIcon);\n statusItem.command = {\n title: 'Run Doctor',\n command: 'inference.runDoctor',\n arguments: [],\n };\n items.push(statusItem);\n\n return items;\n }\n\n private getSettingsChildren(): ConfigItem[] {\n const settings = getSettings();\n\n const pathItem = new ConfigItem(\n `Path: ${settings.path || '(auto-detect)'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n 'inference.path',\n );\n pathItem.iconPath = new vscode.ThemeIcon('file-symlink-directory');\n\n const autoInstallItem = new ConfigItem(\n `Auto Install: ${settings.autoInstall ? 'enabled' : 'disabled'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n 'inference.autoInstall',\n );\n autoInstallItem.iconPath = new vscode.ThemeIcon('cloud-download');\n\n const updateItem = new ConfigItem(\n `Check for Updates: ${settings.checkForUpdates ? 'enabled' : 'disabled'}`,\n 'property',\n vscode.TreeItemCollapsibleState.None,\n undefined,\n 'inference.checkForUpdates',\n );\n updateItem.iconPath = new vscode.ThemeIcon('sync');\n\n return [pathItem, autoInstallItem, updateItem];\n }\n\n private async resolveVersion(infsPath: string): Promise {\n if (this.version) {\n return this.version;\n }\n try {\n const result = await exec(infsPath, ['version']);\n if (result.exitCode !== 0) {\n return null;\n }\n const match = result.stdout.match(/^infs\\s+(\\S+)/);\n if (match) {\n this.version = match[1];\n return this.version;\n }\n return null;\n } catch {\n return null;\n }\n }\n\n dispose(): void {\n this._onDidChangeTreeData.dispose();\n }\n}\n"], + "mappings": "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", + "names": ["os", "path", "vscode", "path", "path", "platform", "vscode", "fs", "os", "path", "fs", "DEFAULT_TIMEOUT_MS", "fs", "path", "platform", "os", "platform", "parseDoctorOutput", "vscode", "outputChannel", "platform", "vscode", "installing", "outputChannel", "installWithProgress", "notifyInstallError", "vscode", "outputChannel", "vscode", "vscode", "outputChannel", "outputChannel", "vscode", "outputChannel", "vscode", "platform", "env", "platform"] } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index ac73623a..ecbec303 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -85,6 +85,10 @@ "command": "inference.installToolchain", "title": "Inference: Install Toolchain" }, + { + "command": "inference.installComponent", + "title": "Inference: Install Component (wasm-opt)" + }, { "command": "inference.updateToolchain", "title": "Inference: Update Toolchain" diff --git a/editors/vscode/src/commands/doctor.ts b/editors/vscode/src/commands/doctor.ts index 7b98849c..172c4048 100644 --- a/editors/vscode/src/commands/doctor.ts +++ b/editors/vscode/src/commands/doctor.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { detectInfs } from '../toolchain/detection'; import { runDoctor, DoctorResult } from '../toolchain/doctor'; import { formatDoctorChecks } from '../toolchain/doctorFormat'; +import { wasmOptNeedsAttention } from '../toolchain/components'; import { updateStatusBar } from '../ui/statusBar'; /** Guard against concurrent doctor runs. */ @@ -68,25 +69,43 @@ export function registerDoctorCommand( vscode.commands.executeCommand('inference.refreshConfigView'); if (result.hasErrors) { + const actions = ['Show Output']; + if (wasmOptNeedsAttention(result)) { + actions.push('Install wasm-opt'); + } vscode.window .showErrorMessage( `Inference doctor: ${result.summary}`, - 'Show Output', + ...actions, ) .then((action) => { if (action === 'Show Output') { outputChannel.show(); + } else if (action === 'Install wasm-opt') { + vscode.commands.executeCommand( + 'inference.installComponent', + 'wasm-opt', + ); } }); } else if (result.hasWarnings) { + const actions = ['Show Output']; + if (wasmOptNeedsAttention(result)) { + actions.push('Install wasm-opt'); + } vscode.window .showWarningMessage( `Inference doctor: ${result.summary}`, - 'Show Output', + ...actions, ) .then((action) => { if (action === 'Show Output') { outputChannel.show(); + } else if (action === 'Install wasm-opt') { + vscode.commands.executeCommand( + 'inference.installComponent', + 'wasm-opt', + ); } }); } else { diff --git a/editors/vscode/src/commands/installComponent.ts b/editors/vscode/src/commands/installComponent.ts new file mode 100644 index 00000000..8e0eb9a9 --- /dev/null +++ b/editors/vscode/src/commands/installComponent.ts @@ -0,0 +1,141 @@ +import * as vscode from 'vscode'; +import { detectInfs } from '../toolchain/detection'; +import { exec, ExecResult } from '../utils/exec'; +import { + ComponentName, + KNOWN_COMPONENTS, + componentAddArgs, +} from '../toolchain/components'; + +/** Guard against concurrent component install attempts. */ +let installing = false; + +/** + * Timeout for a component install. Managed downloads can be ~100 MB, far + * beyond the default exec timeout, so allow up to ten minutes. + */ +const INSTALL_TIMEOUT_MS = 600_000; + +/** + * Register the inference.installComponent command. + * Returns the Disposable to add to context.subscriptions. + */ +export function registerInstallComponentCommand( + outputChannel: vscode.OutputChannel, +): vscode.Disposable { + return vscode.commands.registerCommand( + 'inference.installComponent', + async (component: string = 'wasm-opt') => { + if (installing) { + vscode.window.showInformationMessage( + 'Inference component installation is already in progress.', + ); + return; + } + + if (!isKnownComponent(component)) { + vscode.window.showErrorMessage( + `Inference: unknown component '${component}'.`, + ); + return; + } + + const detection = detectInfs(); + if (!detection) { + vscode.window + .showWarningMessage( + 'Inference toolchain not found. Install it first.', + 'Install', + ) + .then((action) => { + if (action === 'Install') { + vscode.commands.executeCommand( + 'inference.installToolchain', + ); + } + }); + return; + } + + installing = true; + try { + const result = await installWithProgress( + detection.path, + component, + outputChannel, + ); + if (result.stdout) { + outputChannel.appendLine(result.stdout); + } + if (result.stderr) { + outputChannel.appendLine(result.stderr); + } + + if (result.exitCode === 0) { + vscode.window.showInformationMessage( + `Inference: component '${component}' installed.`, + ); + vscode.commands.executeCommand('inference.runDoctor'); + } else { + notifyInstallError(component); + } + } catch (err) { + const message = + err instanceof Error ? err.message : String(err); + outputChannel.appendLine( + `Component installation failed: ${message}`, + ); + notifyInstallError(component); + } finally { + installing = false; + } + }, + ); +} + +/** Type guard: whether a string is a known component name. */ +function isKnownComponent(name: string): name is ComponentName { + return (KNOWN_COMPONENTS as readonly string[]).includes(name); +} + +/** Run `infs component add ` with a VS Code progress notification. */ +function installWithProgress( + infsPath: string, + component: ComponentName, + outputChannel: vscode.OutputChannel, +): Promise { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Inference Component', + cancellable: false, + }, + async (progress) => { + progress.report({ message: `Installing ${component}...` }); + outputChannel.appendLine(`Installing component '${component}'...`); + return exec(infsPath, componentAddArgs(component), { + timeoutMs: INSTALL_TIMEOUT_MS, + }); + }, + ); +} + +/** Show an error notification for component installation failure. */ +function notifyInstallError(component: ComponentName): void { + vscode.window + .showErrorMessage( + `Inference: failed to install component '${component}'. See output for details.`, + 'Show Output', + 'Retry', + ) + .then((action) => { + if (action === 'Show Output') { + vscode.commands.executeCommand('inference.showOutput'); + } else if (action === 'Retry') { + vscode.commands.executeCommand( + 'inference.installComponent', + component, + ); + } + }); +} diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 9234ae1d..c5626796 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -5,6 +5,7 @@ import { detectInfs, inferenceHome } from './toolchain/detection'; import { exec } from './utils/exec'; import { compareSemver } from './utils/semver'; import { registerInstallCommand } from './commands/install'; +import { registerInstallComponentCommand } from './commands/installComponent'; import { registerDoctorCommand } from './commands/doctor'; import { registerSelectVersionCommand } from './commands/selectVersion'; import { registerUpdateCommand, checkForUpdates } from './commands/update'; @@ -33,6 +34,7 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( registerDoctorCommand(outputChannel, statusBarItem), ); + context.subscriptions.push(registerInstallComponentCommand(outputChannel)); context.subscriptions.push(registerUpdateCommand(outputChannel)); context.subscriptions.push(registerSelectVersionCommand(outputChannel)); diff --git a/editors/vscode/src/test/components.test.ts b/editors/vscode/src/test/components.test.ts new file mode 100644 index 00000000..fa62fe93 --- /dev/null +++ b/editors/vscode/src/test/components.test.ts @@ -0,0 +1,69 @@ +import * as assert from 'node:assert'; +import { describe, it } from 'node:test'; + +import { + KNOWN_COMPONENTS, + componentAddArgs, + wasmOptNeedsAttention, +} from '../toolchain/components'; +import { DoctorCheckStatus, DoctorResult } from '../toolchain/doctor'; + +/** Build a minimal DoctorResult from a list of (name, status) checks. */ +function doctorResult( + checks: Array<{ name: string; status: DoctorCheckStatus }>, +): DoctorResult { + return { + checks: checks.map((c) => ({ + name: c.name, + status: c.status, + message: '', + })), + hasErrors: checks.some((c) => c.status === 'fail'), + hasWarnings: checks.some((c) => c.status === 'warn'), + summary: '', + }; +} + +describe('componentAddArgs', () => { + it('builds the argv for `infs component add`', () => { + assert.deepStrictEqual(componentAddArgs('wasm-opt'), [ + 'component', + 'add', + 'wasm-opt', + ]); + }); + + it('lists wasm-opt as a known component', () => { + assert.ok(KNOWN_COMPONENTS.includes('wasm-opt')); + }); +}); + +describe('wasmOptNeedsAttention', () => { + it('returns false when the wasm-opt check is OK', () => { + const result = doctorResult([{ name: 'wasm-opt', status: 'ok' }]); + assert.strictEqual(wasmOptNeedsAttention(result), false); + }); + + it('returns true when the wasm-opt check warns', () => { + const result = doctorResult([{ name: 'wasm-opt', status: 'warn' }]); + assert.strictEqual(wasmOptNeedsAttention(result), true); + }); + + it('returns true when the wasm-opt check fails', () => { + const result = doctorResult([{ name: 'wasm-opt', status: 'fail' }]); + assert.strictEqual(wasmOptNeedsAttention(result), true); + }); + + it('returns false when there is no wasm-opt check', () => { + const result = doctorResult([{ name: 'infs binary', status: 'ok' }]); + assert.strictEqual(wasmOptNeedsAttention(result), false); + }); + + it('returns false when a different check warns', () => { + const result = doctorResult([ + { name: 'wasm-opt', status: 'ok' }, + { name: 'Default toolchain', status: 'warn' }, + ]); + assert.strictEqual(wasmOptNeedsAttention(result), false); + }); +}); diff --git a/editors/vscode/src/test/doctor.test.ts b/editors/vscode/src/test/doctor.test.ts index 06ad07d4..6b98d8c2 100644 --- a/editors/vscode/src/test/doctor.test.ts +++ b/editors/vscode/src/test/doctor.test.ts @@ -190,4 +190,36 @@ describe('parseDoctorOutput', () => { assert.strictEqual(result.hasErrors, false); assert.strictEqual(result.hasWarnings, false); }); + + it('parses wasm-opt component checks', () => { + const stdout = [ + 'Checking Inference toolchain installation...', + '', + " [OK] wasm-opt: Not installed (optional — needed only for [build.wasm-opt]; install with 'infs component add wasm-opt')", + " [WARN] wasm-opt: Managed install at /x/tools/binaryen is missing version_130/bin/wasm-opt; run 'infs component add wasm-opt' to repair", + '', + 'Some warnings were found. The toolchain may work but could have issues.', + ].join('\n'); + + const result = parseDoctorOutput(stdout); + + assert.strictEqual(result.checks.length, 2); + + assert.strictEqual(result.checks[0].name, 'wasm-opt'); + assert.strictEqual(result.checks[0].status, 'ok'); + assert.strictEqual( + result.checks[0].message, + "Not installed (optional — needed only for [build.wasm-opt]; install with 'infs component add wasm-opt')", + ); + + assert.strictEqual(result.checks[1].name, 'wasm-opt'); + assert.strictEqual(result.checks[1].status, 'warn'); + assert.strictEqual( + result.checks[1].message, + "Managed install at /x/tools/binaryen is missing version_130/bin/wasm-opt; run 'infs component add wasm-opt' to repair", + ); + + assert.strictEqual(result.hasErrors, false); + assert.strictEqual(result.hasWarnings, true); + }); }); diff --git a/editors/vscode/src/test/settings-schema.test.ts b/editors/vscode/src/test/settings-schema.test.ts index b82f295e..92366348 100644 --- a/editors/vscode/src/test/settings-schema.test.ts +++ b/editors/vscode/src/test/settings-schema.test.ts @@ -48,13 +48,14 @@ describe('settings schema (QA Section 8)', () => { describe('commands schema (QA Section 8)', () => { const commands: Array<{ command: string; title: string }> = contributes.commands; - it('has exactly 9 commands registered', () => { - assert.strictEqual(commands.length, 9); + it('has exactly 10 commands registered', () => { + assert.strictEqual(commands.length, 10); }); it('contains expected command IDs', () => { const ids = commands.map((c) => c.command); assert.ok(ids.includes('inference.installToolchain')); + assert.ok(ids.includes('inference.installComponent')); assert.ok(ids.includes('inference.updateToolchain')); assert.ok(ids.includes('inference.selectVersion')); assert.ok(ids.includes('inference.runDoctor')); diff --git a/editors/vscode/src/toolchain/components.ts b/editors/vscode/src/toolchain/components.ts new file mode 100644 index 00000000..dac3706a --- /dev/null +++ b/editors/vscode/src/toolchain/components.ts @@ -0,0 +1,29 @@ +import { DoctorResult } from './doctor'; + +/** + * Toolchain components the CLI can provision via `infs component add`. + * + * Mirrors the `KNOWN_COMPONENTS` list in the `infs component` command + * (`apps/infs/src/commands/component.rs`); keep the two in sync. + */ +export const KNOWN_COMPONENTS = ['wasm-opt'] as const; + +/** A component name understood by `infs component`. */ +export type ComponentName = (typeof KNOWN_COMPONENTS)[number]; + +/** Build the argv for `infs component add `. */ +export function componentAddArgs(component: ComponentName): string[] { + return ['component', 'add', component]; +} + +/** + * Whether a doctor result indicates the wasm-opt component needs attention, + * i.e. any check named `wasm-opt` reported a warning or failure. + */ +export function wasmOptNeedsAttention(result: DoctorResult): boolean { + return result.checks.some( + (check) => + check.name === 'wasm-opt' && + (check.status === 'warn' || check.status === 'fail'), + ); +}