Skip to content

feat(toolchain): select a local baml-cli by path - #4264

Merged
hellovai merged 3 commits into
canaryfrom
hellovai/baml-toolchain
Jul 29, 2026
Merged

feat(toolchain): select a local baml-cli by path#4264
hellovai merged 3 commits into
canaryfrom
hellovai/baml-toolchain

Conversation

@hellovai

@hellovai hellovai commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Lets you point the baml wrapper at a locally built baml-cli.

Why

Today the only ways to run a local build are repointing BAML_HOME and hand-seeding toolchains/<v>/bin/baml-cli + VERSION (what freshness_e2e.rs does), or bypassing the wrapper entirely with BAML_CLI_ALLOW_DIRECT. Neither survives a project that pins [toolchain].

Design

A path is recognized by shape, not by a prefix. A selector containing a path separator (or starting with ~ or .) is a binary path. Channels and versions never contain one, so no path: prefix, subcommand, or flag is needed to disambiguate.

The one input this does not catch is a bare filename with no separator: baml toolchain use baml-cli reads as a version. ./baml-cli is the answer, same convention as running any local executable.

Two places to set it, both typed by the developer, with precedence unchanged (BAML_VERSION > baml.toml > ~/.baml/config.toml > canary):

# point at a local build
export BAML_VERSION=~/repos/baml/target/debug/baml-cli
baml check

# one-off
BAML_VERSION=./target/debug/baml-cli baml check

# persist to ~/.baml/config.toml
baml toolchain use ~/repos/baml/target/debug/baml-cli
baml toolchain use canary          # back to normal

Deliberately not settable from baml.toml. A committed path plus a mode-755 file (git preserves the exec bit) would execute on any baml invocation in that tree, with no prompt and nothing on stderr. rustup avoids this by requiring path to be an absolute path, which also makes the field useless to a teammate, so there is little left to want. The field is refused with a message pointing at the two supported ways:

/work/demo/baml.toml sets [toolchain] path, which is not supported.
A checked-out repository must not be able to choose which binary runs on your machine.
To use a local build, run: baml toolchain use <path>
Or set $BAML_VERSION to it for a single command.

Refuses to exec the wrapper itself. baml and baml-cli build into the same directory, so pointing at baml is a one-character slip. Without a guard it re-resolves the same selector and execs itself forever: silent on unix, an unbounded process tree on Windows where the non-unix branch spawns a child per iteration.

Paths are tidied lexically, not canonicalized. Canonicalizing resolves symlinks, which would freeze a current -> release-N indirection to whatever it pointed at the day it was set. It also yields \\?\C:\... verbatim paths on Windows, which would land in config.toml and in --version output. Lexical .. removal keeps the symlink and reads the same on every platform. If lexical tidying would produce a path that does not exist while the untidied one does (a symlinked directory followed by ..), the untidied form is kept, so tidying can never break a working path.

Path toolchains skip the version bookkeeping: no VERSION check, no channel-outdated warning, no background manifest refresh. That keeps the exec() fast path unconditional and keeps stderr clean for parsed output.

Subcommands: install and uninstall reject a path, update no-ops, status and list report the path and its origin without touching the network. list surfaces resolution errors rather than swallowing them, since it is what you run to find out why nothing works.

baml ide install needs a managed toolchain and now says so. A local build has no assets/ beside it, so it previously reported a missing file the developer never expected to exist. baml playground and baml pack also degrade under a local toolchain: both look for siblings of the executable, which only exist in the installed layout.

Errors name the source, so a forgotten override is traceable:

toolchain binary not found: /Users/vbv/build/nope
  set by default.selector in /Users/vbv/.baml/config.toml

toolchain path points at the baml wrapper itself: /repos/baml/target/debug/baml
  set by $BAML_VERSION
Point at the baml-cli binary instead, which is built alongside it.

Failures that cannot be caught up front (wrong architecture, a noexec mount, a missing interpreter) carry the same attribution.

Windows

The separator rule is split into is_path_selector_on(selector, windows) so the Windows behavior is exercised by tests on every platform rather than only compiling under cfg(windows). A path with no extension falls back to .exe, so baml toolchain use .\target\debug\baml-cli works. A local Windows compile-check was not possible here (the target needs mingw for a TLS dependency's build script); the cargo test (windows) CI job covers it, since it runs baml as part of --workspace.

Testing

34 unit tests and the 2 existing e2e tests pass; cargo fmt and cargo clippy are clean.

Manually exercised end to end against both a stub and the real baml-cli: env var with relative and ~ paths, toolchain use <path> followed by a plain invocation, a symlinked path surviving a repoint, the self-exec guard (direct and through a symlink), the baml.toml refusal, a relative default.selector being refused, list surfacing that error, baml ide install refusing a local toolchain, and the directory / missing / non-executable errors. Channels and pinned versions still resolve exactly as before.

Review note

The security, self-exec, symlink, Windows .exe, list, and global-config issues above were found by an adversarial review pass over the first commit and fixed in the second.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y9aTrkwDQgGr9HSvdEcq61

Summary by CodeRabbit

  • New Features
    • Added support for using a local baml-cli binary as an active toolchain.
    • Expanded path-based selection via baml toolchain use <path> and $BAML_VERSION, including local version detection.
    • Toolchain status, listing, and --version now identify active local binaries and usability.
  • Bug Fixes
    • Improved selector normalization and validation (including platform-specific path handling) and more consistent error messaging.
    • Added safeguards for local toolchain execution and verification (with attribution), plus a timeout for local version checks.
    • Updated IDE VS Code extension checks to explicitly guide use of managed toolchains when assets are missing.

Running a locally built baml-cli meant either repointing BAML_HOME and
hand-seeding a toolchain directory or bypassing the wrapper entirely with
BAML_CLI_ALLOW_DIRECT. Neither survives a project that pins [toolchain].

A selector containing a path separator now names a baml-cli binary the
wrapper does not manage, accepted anywhere a selector is: BAML_VERSION,
[toolchain] path in baml.toml, and baml toolchain use. Channels and
versions never contain a separator, so no prefix or flag is needed to
tell them apart.

Paths in baml.toml resolve relative to that file, so a relative path is
committable. Resolved paths are absolutized once and canonicalized when
the target exists, keeping ../ out of --version, status, and the config
file; a path that does not exist yet keeps its joined form so the error
names what was asked for.

Path toolchains skip the VERSION check, the channel-outdated warning,
and the background manifest refresh, so they always take the exec fast
path. install and uninstall reject them, update no-ops, and status and
list report them without touching the network.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9aTrkwDQgGr9HSvdEcq61
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jul 29, 2026 5:23am
promptfiddle Ready Ready Preview, Comment Jul 29, 2026 5:23am
promptfiddle2 Ready Ready Preview, Comment Jul 29, 2026 5:23am

Request Review

@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@vercel
vercel Bot temporarily deployed to Preview – beps July 28, 2026 23:18 Inactive
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4277b91e-fc34-49c2-8ecf-5afd88854631

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf6995 and b9a5b42.

📒 Files selected for processing (1)
  • baml_language/crates/baml/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/crates/baml/src/main.rs

📝 Walkthrough

Walkthrough

The wrapper now supports local path selectors alongside managed channels and versions. It normalizes and verifies local baml-cli binaries, executes them directly, skips remote management, and reports path-specific status and IDE errors.

Changes

Path toolchain support

Layer / File(s) Summary
Selector contracts and resolution
baml_language/crates/baml/src/main.rs
Configuration and CLI selectors support local paths with platform-aware detection, normalization, source attribution, and explicit rejection of version-only resolution.
Local binary verification and execution
baml_language/crates/baml/src/main.rs
Path selectors verify local executable binaries, prevent wrapper self-execution, set the local-toolchain environment marker, and execute directly.
Command behavior and validation
baml_language/crates/baml/src/main.rs, baml_language/crates/baml_cli/src/ide_command.rs
Use, status, update, list, install, uninstall, version reporting, IDE asset errors, help text, and path-selector tests cover local toolchain behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant ToolchainCLI
  participant SelectorResolver
  participant LocalBamlCLI
  participant IDECommand
  ToolchainCLI->>SelectorResolver: resolve path selector
  SelectorResolver->>LocalBamlCLI: verify local binary
  LocalBamlCLI-->>ToolchainCLI: return usability result
  ToolchainCLI->>LocalBamlCLI: execute with local-toolchain marker
  IDECommand->>LocalBamlCLI: check VSIX asset
  LocalBamlCLI-->>IDECommand: report local binary context
Loading

Poem

A bunny chose a toolchain path,
And skipped the download bath.
It checked the file, then hopped away,
To run local code throughout the day.
“No version needed!” the rabbit sings.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: selecting a local baml-cli by path.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hellovai/baml-toolchain

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 28, 2026 23:25 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/baml/src/main.rs`:
- Around line 207-220: Update the path-selector branch in print_version so
verify_path_toolchain receives path_selector_origin(&selector.source) instead of
an empty origin. Preserve the existing success annotation and failure output
while ensuring verification errors identify whether the selected path came from
the environment, project configuration, or default configuration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f439737d-cf5f-4deb-afb7-9df80ba48a0c

📥 Commits

Reviewing files that changed from the base of the PR and between 6fc97e5 and 7626f53.

📒 Files selected for processing (1)
  • baml_language/crates/baml/src/main.rs

Comment thread baml_language/crates/baml/src/main.rs
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 27.4 MB 11.7 MB file 27.3 MB +75.1 KB (+0.3%) OK
packed-program Linux 🔒 17.8 MB 7.3 MB file 17.7 MB +30.6 KB (+0.2%) OK
baml-cli macOS 🔒 21.3 MB 10.2 MB file 21.2 MB +49.7 KB (+0.2%) OK
packed-program macOS 🔒 13.9 MB 6.4 MB file 13.9 MB +32 B (+0.0%) OK
baml-cli Windows 🔒 22.9 MB 10.4 MB file 22.9 MB +56.3 KB (+0.2%) OK
packed-program Windows 🔒 14.8 MB 6.5 MB file 14.8 MB +14.7 KB (+0.1%) OK
bridge_wasm WASM 16.6 MB 🔒 4.5 MB gzip 4.5 MB +10.8 KB (+0.2%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 28, 2026 23:39 Inactive
Follow-up to the path-selector change, from an adversarial review.

Drop [toolchain] path from baml.toml. A checked-out repository could
otherwise choose which binary runs: a committed path plus a mode-755
file (git preserves the exec bit) executed on any baml invocation, with
no prompt and deliberately nothing on stderr. rustup avoids this by
requiring an absolute path, which would make the field useless to a
teammate anyway, so the field is refused outright and the refusal
explains where to set a local toolchain instead. $BAML_VERSION and
baml toolchain use are unaffected; both are typed by the developer.

Refuse to exec the wrapper itself. baml and baml-cli build into the
same directory, so pointing at the wrapper is a one-character slip, and
it re-resolved the same selector and exec'd itself forever: silent and
unkillable-looking on unix, an unbounded process tree elsewhere.

Tidy paths lexically instead of canonicalizing. Canonicalizing resolved
symlinks away, so a current -> release-N indirection was frozen to the
target it had the day it was set and stopped following repoints. It
also produced \\?\C:\... verbatim paths on Windows, which would have
been written into config.toml and printed by --version and status.

Also: fall back to .exe on Windows when the path has no extension;
refuse a relative default.selector in the global config, which would
otherwise resolve against cwd and run a different binary per directory;
surface resolution errors from baml toolchain list instead of swallowing
them and reporting a selector that cannot run; attribute exec failures
that verify_path_toolchain cannot catch, such as a wrong architecture or
a noexec mount; and make baml ide install say that a local toolchain has
no bundled extension rather than reporting a missing asset file.

The wrapper now passes BAML_WRAPPER_LOCAL_TOOLCHAIN for a path
toolchain, leaving BAML_WRAPPER_RESOLVED_TOOLCHAIN version-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9aTrkwDQgGr9HSvdEcq61
@vercel
vercel Bot temporarily deployed to Preview – beps July 29, 2026 00:02 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 29, 2026 00:10 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/crates/baml/src/main.rs (1)

494-532: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

~-prefixed default selectors can silently regain cwd-dependence when HOME is unset.

The relative-path guard exempts any selector starting with ~ on the assumption tilde always resolves to an absolute HOME path. But per tilde_without_home_is_left_relative (line ~1729), when HOME is unset, resolve_selector_path_with_home joins the literal ~/... string onto base (cwd here) instead of erroring — so a manually-edited global config with default.selector = "~/builds/baml-cli" and no HOME set would silently produce a different binary per directory, exactly the failure mode this check exists to prevent.

🐛 Proposed fix
         if is_path_selector(selector)
-            && !selector.starts_with('~')
+            && (!selector.starts_with('~') || env::var_os("HOME").is_none())
             && !Path::new(selector).is_absolute()
         {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml/src/main.rs` around lines 494 - 532, Update the
default-selector validation in active_selector so a ~-prefixed path is rejected
when HOME is unset and would remain relative to the current directory. Preserve
the existing acceptance of ~ paths when they resolve independently of cwd, while
retaining the relative-path error for other non-absolute selectors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@baml_language/crates/baml/src/main.rs`:
- Around line 494-532: Update the default-selector validation in active_selector
so a ~-prefixed path is rejected when HOME is unset and would remain relative to
the current directory. Preserve the existing acceptance of ~ paths when they
resolve independently of cwd, while retaining the relative-path error for other
non-absolute selectors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 47b83e34-0fe2-48e9-b186-65114166af81

📥 Commits

Reviewing files that changed from the base of the PR and between 7626f53 and 4bf6995.

📒 Files selected for processing (2)
  • baml_language/crates/baml/src/main.rs
  • baml_language/crates/baml_cli/src/ide_command.rs

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 29, 2026 00:23 Inactive
baml --version and baml toolchain status now ask a local toolchain what
version it is, instead of printing only a path and leaving anything that
reads them with no version at all. The call is capped at three seconds
and the child is killed on timeout, so a binary that will not answer
degrades to "version unknown" rather than hanging the command.

Both now always name the setting that selected the binary, including the
global config, which selector_origin stays quiet about and which is the
one most likely to have been forgotten. Verification failures in
--version carry that attribution too, so a broken local toolchain can be
traced to whatever chose it.

baml toolchain uninstall on a path now says how to get back to a managed
toolchain rather than only refusing.

Guard the global-config check on whether a selector can actually stand on
its own instead of on a bare `~` prefix. With no home directory `~/x`
falls back to being joined onto the current directory, and `~user` is not
a form we expand, so both slipped through a prefix test and reintroduced
the per-directory behaviour the check exists to prevent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9aTrkwDQgGr9HSvdEcq61
@vercel
vercel Bot temporarily deployed to Preview – beps July 29, 2026 05:04 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 29, 2026 05:11 Inactive
@hellovai
hellovai added this pull request to the merge queue Jul 29, 2026
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 29, 2026 05:23 Inactive
Merged via the queue into canary with commit effeb21 Jul 29, 2026
83 checks passed
@hellovai
hellovai deleted the hellovai/baml-toolchain branch July 29, 2026 06:15
meefs pushed a commit to meefs/baml that referenced this pull request Aug 4, 2026
## Summary

- bump the BAML wrapper from 0.2.3 to 0.2.4
- publish the local baml-cli path selection support merged in BoundaryML#4264

## Root cause

The wrapper 0.2.3 release predates BoundaryML#4264. Because that PR changed
wrapper behavior without advancing the independently versioned wrapper
package, users on the latest wrapper still interpreted an absolute
baml-cli path as a version and attempted to fetch a manifest for it.

## Impact

The next wrapper release will support local toolchain selection with
commands such as:

    baml toolchain use ./target/release/baml-cli

## Validation

- scripts/baml-wrapper-version check
- python3 -m unittest scripts.tests.test_release_pipeline_contract
- cargo check --manifest-path baml_language/Cargo.toml -p baml --release
- git diff --check
- pre-commit hooks


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
  * Version updated to 0.2.4

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant