bundle: install cargo packages from a git URL or path - #23451
Conversation
`cargo "name"` could only install from crates.io. Accept a `source:` option naming a git URL or a local path, mirroring the `uv` extension, and install with `cargo install --git` or `cargo install --path`. `cargo install --list` reports the origin of anything not installed from a registry, so dumped Brewfiles round-trip. The resolved commit is dropped, since a dumped entry has to compare equal to a hand-written one, while a branch, tag or revision chosen at install time is carried in the URL query and restored with `--branch`, `--tag` or `--rev`.
There was a problem hiding this comment.
Pull request overview
This PR extends Homebrew Bundle’s cargo support so Brewfiles can install Rust crates from either crates.io (default) or an explicit source: pointing at a git remote or local path, and ensures brew bundle dump round-trips those origins.
Changes:
- Adds
source:parsing/validation forcargoBrewfile entries and passes it through install/check flows. - Enhances
cargo install --listparsing to capture and dump non-registry origins, stripping resolved#<sha>while preserving?branch=?tag=?rev=selectors. - Updates bundle documentation and expands unit test coverage for source-based cargo installs/dumps.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| Library/Homebrew/bundle/extensions/cargo.rb | Implements source: handling, origin parsing/normalization, and install flag reconstruction for git/path sources. |
| Library/Homebrew/test/bundle/cargo_spec.rb | Adds specs for entry validation, checking behavior, dumping behavior, and install argument construction with source:. |
| docs/Brew-Bundle-and-Brewfile.md | Documents cargo ... source: usage and how dump/restore handles git selectors and commit stripping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two forms were accepted at parse time that `cargo install` cannot act on, so the failure surfaced later and less clearly. `--git` takes a URL and rejects an scp-style remote outright, naming the `ssh://` form to use instead, so require a scheme rather than also accepting anything ending in `.git`. Only a `branch`, `tag` or `rev` query is restored as a `cargo install` flag. Any other query was accepted and then silently dropped, which left the installed origin unequal to the `Brewfile` entry and so made `brew bundle check` report the crate missing on every run.
MikeMcQuaid
left a comment
There was a problem hiding this comment.
Thanks for PR! One high-level comment before more detailed review.
Neither a local path nor a file:// URL resolves on another machine, so both are rejected when the Brewfile is parsed. A crate cargo reports as installed from one is dumped as a registry crate rather than dropped, with a warning naming the origin that could not be expressed.
|
Heads up -- the scope of this PR has narrowed since I opened it, and the description above is now out of date in a few places. I have left it as written so the review threads still read in order. What changed, per @MikeMcQuaid’s review:
The bits of the PR that show the path example, then, are no longer valid. I chose not to rewrite the PR, but rather to call it out here, to keep the provenance clean. |
MikeMcQuaid
left a comment
There was a problem hiding this comment.
Thanks! Could you open another draft PR for the related uv local change?
A crate cargo reports as installed from a local origin is still dumped as a registry crate, but without warning about the origin that could not be expressed, which is noise for anyone who is content with the crate they have.
yep: |
MikeMcQuaid
left a comment
There was a problem hiding this comment.
Thanks! Looks good when CI is 🟢
Thanks! This was a very easy process, appreciate your patience. |
brewcommands to reproduce the bug?brew lgtm(style, typechecking and tests) locally?Disclosed: this change was drafted with Claude Code (Claude Opus 5) and reviewed by me before opening. Verification is described under "How this was verified" below; commits are not attributed to AI.
What
cargo "name"can currently only install from crates.io. This adds an optionalsource:naming either a git URL or a local path:A git source installs with
cargo install --git, a path source withcargo install --path. The option mirrors theuvextension's existingsource:, and the accepted forms are the same setuvaccepts (a URL scheme, or a path beginning/,./or../); anything else is rejected when theBrewfileis parsed rather than being passed tocargoto fail obscurely.brew bundle dumpround-trips these, becausecargo install --listreports the origin of anything not installed from a registry. Two details in the parsing are worth calling out for review:cargoappends as#<sha>is dropped, so that a dumped entry compares equal to a hand-written one andbrew bundle checkdoes not report an endless diff.cargoas a URL query (?branch=,?tag=,?rev=, never more than one) and is restored via the matchingcargo installflag.--gitdoes not accept a query — it hands the string togitverbatim, which fails — so this mapping is what makes a dumpedBrewfilefor a pinned crate actually installable.Why
Installing a crate straight from a repository is a normal thing to do — private crates that were never published, and forks or unreleased fixes of public ones — but such crates currently cannot be expressed in a
Brewfileat all. They are silently absent frombrew bundle dumpoutput, so aBrewfilethat looks complete is not, and restoring a machine from it leaves those binaries missing.How this was verified
brew lgtmpasses (typecheck,style --changed,tests --changed). Beyond the unit tests, the source parsing and flag reconstruction were checked against realcargorather than only against mocks: a throwaway crate was installed from a local git repository via--branch,--tagand--revinto isolated roots, and thecargo install --listoutput for each was fed back through the parser. The reconstructedcargo install --locked --git <url> --branch <name> <crate>was then executed, and the resultinginstall --listentry is byte-identical to the one that produced it, so dump and restore is a fixed point.Known limits, deliberately left alone to keep this consistent with
uv:~is not expanded in a path source (Bundle.systemdoes not go through a shell), and a bare relative path such assrc/foois not accepted, since it is ambiguous with a crate name.🤖 Generated with Claude Code