Skip to content

Commit

Permalink
New RFC: cargo run to accept pkgid of dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
tchernobog committed Aug 24, 2021
1 parent e0d0dfb commit c148ae6
Showing 1 changed file with 117 additions and 0 deletions.
117 changes: 117 additions & 0 deletions text/0000-cargo-run-deps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
- Feature Name: `cargo-run-deps`
- Start Date: 2021-08-24
- RFC PR: [rust-lang/rfcs#3168](https://github.com/rust-lang/rfcs/pull/3168)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

In addition to binaries from workspace members, teach `cargo run` to execute binaries from dependent packages.

# Motivation
[motivation]: #motivation

Right now, `cargo` lacks a way to run binaries that belong to packages which are not members of the current workspace.

This is desirable for multiple reasons:

- make binaries from dependencies available to external scripts, especially Continuous Integration (CI) scripts.
- ensure a specific version of the binary as specified by the manifest is run, which might differ from the version installed globally.
- avoid clashes in the global installation directory.
- make binaries callable from `build.rs` through `cargo run` as a shim.
- make the build process self-contained, instead of requiring the user a manual installation step.
- build binary dependencies lazily, only when they are needed, thus avoiding inflating build times unless necessary.

Several issues related to this request have been opened against `cargo`:

- [rust-lang/cargo/issues/2267](https://github.com/rust-lang/cargo/issues/2267): Make commands in dev-dependencies available to run
- [rust-lang/cargo/issues/872](https://github.com/rust-lang/cargo/issues/872): Cargo needs a way to run an executable out of a build-dependency

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Cargo allows you to depend on other packages as part of its manifest. Some of them might define their own runnable binary targets, but a dependency in a manifest implicitly imples building it only as a library.

For instance, you can depend on `mdbook` when building your project as follows:

```toml
[build-dependencies]
mdbook = { version = "0.4.12" }
```

This will build the `mdbook` crate's library, but not the corresponding binary.

If you wanted to invoke `mdbook` inside your `build.rs` script, you could use `cargo run --package mdbook` as a wrapper.

```rust
// build.rs
use std::{env, process::Command};

fn main() {
let cargo_path = env::var("CARGO").unwrap();
let mut mdbook = Command::new(cargo_path).args(&[
"run",
"--package",
"mdbook",
"--",
"--version",
]);

assert!(mdbook.status().expect("mdbook v0.4.12").success());
}
```

This will take care of (re)building the `mdbook` binary if needed, at the version specified in the manifest, and then invoke it with the passed parameters. As usual, in case multiple binaries are available for one package, the `--bin` option can be passed to select the right one.

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

The scope of this RFC is to alter the behavior of `cargo run` so that in addition to binaries part of the workspace, it will allow also to run binaries from dependencies which are listed inside the locked manifest for the workspace.

In order to do that, we would relax the constraints of `cargo run` to accept a package id describing any package part of `Cargo.lock` (by virtue of the `--package=<pkgid>` command line parameter). The required steps would be:

- if the specified `pkgid` is not related to a workspace member, use the resolver against the locked manifest to determine acceptable versions of dependencies. The resulting set should have a size of one, or we fail.
- from the resulting package, select either the target specified by the `--bin` option, or the default binary target if none is specified. If we get no match, we fail.
- trigger compilation of the corresponding target with the host system as the target system. This is important when cross-compiling.
- execute the resulting compiled binary, passing the trailing arguments of `cargo run` to it.


# Drawbacks
[drawbacks]: #drawbacks

[rust-lang/rfcs#3028](https://github.com/rust-lang/rfcs/pull/3028) has an overlap with this feature. They are not mutually exclusive and can be implemented mostly independently, but at some point we expect the implementation to require a rework to take in account for the first one to be merged, whichever may come first.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

Right now, users are required to manually install dependencies to either a global location, or to a dedicated location but then to also manually adjust their `PATH` variable to compensate. In case a global location is used, multiple versions of binaries cannot be parallel-installable.

This RFC keeps binary dependencies built within the workspace's `target/` directory, avoiding them spilling out of the projects they are meant for, when they are not needed globally.

It also ensures reproducibility of builds, since it avoids a different, potentially incompatible, version of a binary to be picked up by mistake. This is particularly relevant for some tools such as e.g. `mdbook-bib`, which complain if run with a different library version of `mdbook` they were built against as part of the manifest spec.

The design mimicks several of other package managers of related programming languages. See the [prior art](#prior-art) section. This allows newcomers to get a quick and intuitive way to run binaries.

# Prior art
[prior-art]: #prior-art

Many other package managers from other languages provide the same functionality:

- Node.js provides the `npx` binary,
- PHP's Composer has `php composer exec`.
- In Python, several alternatives exist, such as `pipenv run`.
- Ruby's Bundler allows to invoke binaries via `bundle exec`

[rust-lang/rfcs#3028](https://github.com/rust-lang/rfcs/pull/3028) is a more extensive, alternative proposal that also allows embeddding the resulting binary into the crate. This RFC provides a simpler alternative; additionally, it provides a way to run the built binaries from the command line, which is something not explicitly covered by RFC 3028. This is especially useful with CI integration, to bind the same version of tools as used on the developer's machine. It also lazily allows a selective build of some tools, such as `mdbook`, for those CI builds that do not require a full build.

[rust-lang/cargo/pull/9833](https://github.com/rust-lang/cargo/pull/9833) provides an initial implementation of this RFC, showing that it is likely a localized change; it might make it faster to merge than RFC 3028.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

- Decide whether binaries from transitive dependencies should also be allowed to be run, or if it should be disallowed.

# Future possibilities
[future-possibilities]: #future-possibilities

By virtue of accepting a fully qualified package id, `cargo run` can be extended to allow running any specific binary crate, regardless of whether it is part of the locked manifest. This would help to run several different versions of a binary in parallel without installing them first, which results in the previous binary to be overwritten. It might be particularly desirable e.g. for regression testing of tools such as `mdbook`, `tarpaulin`, `rusty-hook`, etc.

0 comments on commit c148ae6

Please sign in to comment.