Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RUSTUP_TOOLCHAIN_DIR #3207

Closed
wants to merge 1 commit into from
Closed

Conversation

ehuss
Copy link
Contributor

@ehuss ehuss commented Feb 12, 2023

This adds an environment variable RUSTUP_TOOLCHAIN_DIR that is set when running a toolchain command. This is intended to be used by Cargo to execute rustc and rustdoc directly without going through the rustup wrappers. This should help improve performance on non-Windows platforms, and to avoid the performance loss on Windows in #3178.

Modifying PATH isn't an option because it prevents recursive commands from being able to run via the proxies (#812).
Setting RUSTC isn't an option because it breaks recursive invocations (#3029 and #3031).
This option should be safe, since cargo will only invoke the toolchain binary if no other setting is specified. Recursive invocations should continue to be able to run the rustup wrappers.

ehuss/cargo@f6d949b is a prototype of what the cargo side looks like.

I have done performance testing on a variety of machines running macOS and Linux across 10 different projects. Typical improvement for a clean cargo check is about 1.07 to 1.16 times faster (with a peak of 1.25).

Closes #3035

@rbtcollins
Copy link
Contributor

This looks appealing to me. Thank you.

So to be sure I understand:

  • we keep the proxies on the path so code such as RUSTC_WRAPPERS, and build.rs scripts can use +toolchain syntax without fear
  • but we want the cargo->rust interface to be fast, so we're going to set a variable for cargo to use to find rust directly

I think cargo will need to know to reset that variable : consider this stack

cargo(proxy) (sets var)
cargo
some-build-rs
explicit-call-to-a-different-toolchain-cargo (uses var)
wrong-rustc-invoked

I think we need to keep this var hermetic: vars are great for tunnelling but know no boundaries :/.

What do you think?

@ehuss
Copy link
Contributor Author

ehuss commented Feb 13, 2023

explicit-call-to-a-different-toolchain-cargo (uses var)
wrong-rustc-invoked

Can you say more about what this step looks like? If it is something like executing cargo +nightly build, then that should execute the proxy which should reset all of the environment variables (RUSTUP_TOOLCHAIN, RUSTUP_TOOLCHAIN_DIR, etc.). Or if the script overrides the toolchain with the RUSTUP_TOOLCHAIN environment variable, then that should also work (and reset RUSTUP_TOOLCHAIN_DIR to point to the new toolchain).

@rbtcollins
Copy link
Contributor

explicit-call-to-a-different-toolchain-cargo (uses var)
wrong-rustc-invoked

Can you say more about what this step looks like? If it is something like executing cargo +nightly build, then that should execute the proxy which should reset all of the environment variables (RUSTUP_TOOLCHAIN, RUSTUP_TOOLCHAIN_DIR, etc.).

Lets say someone has a custom toolchain they aren't managing with rustup. They do /path/to/that/toolchains/cargo do-something-here.

@ehuss
Copy link
Contributor Author

ehuss commented Feb 13, 2023

I'm still not clear exactly what would happen differently in that circumstance.

Before:
cargo (proxy) -> Sets RUSTUP_TOOLCHAIN -> cargo (real) -> build-script -> calls /other/cargo -> other cargo calls rustc (proxy) -> rustc proxy sees RUSTUP_TOOLCHAIN and executes the toolchain from the original cargo

New:
cargo (proxy) -> Sets RUSTUP_TOOLCHAIN and RUSTUP_TOOLCHAIN_DIR -> cargo (real) -> build-script -> calls /other/cargo-> other cargo callsrustc` directly from RUSTUP_TOOLCHAIN_DIR

In both cases, the rustc should be the one from the original toolchain determined from the first cargo.

A build script or extension could intentionally modify PATH to have a different toolchain earlier in PATH. I could see that causing it to use a different rustc. FWIW, I think the scenario of a build script or extension trying to intentionally circumvent rustup in that fashion seems fairly unlikely (and lurks in the evil "you shouldn't do that" category).

@rbtcollins
Copy link
Contributor

I'm still not clear exactly what would happen differently in that circumstance.

Before: cargo (proxy) -> Sets RUSTUP_TOOLCHAIN -> cargo (real) -> build-script -> calls /other/cargo -> other cargo calls rustc (proxy) -> rustc proxy sees RUSTUP_TOOLCHAIN and executes the toolchain from the original cargo

Ah, I'd forgotten we set that too; ok so thats clear.

New: cargo (proxy) -> Sets RUSTUP_TOOLCHAIN and RUSTUP_TOOLCHAIN_DIR -> cargo (real) -> build-script -> calls /other/cargo-> other cargo callsrustc` directly from RUSTUP_TOOLCHAIN_DIR

yep. What if someone sets RUSTUP_TOOLCHAIN in there somewhere?

In both cases, the rustc should be the one from the original toolchain determined from the first cargo.

A build script or extension could intentionally modify PATH to have a different toolchain earlier in PATH. I could see that causing it to use a different rustc. FWIW, I think the scenario of a build script or extension trying to intentionally circumvent rustup in that fashion seems fairly unlikely (and lurks in the evil "you shouldn't do that" category).

I have largely given up assuming good intentions from users :). I don't mean that in a negative way, but rather lets be conservative rather than surprised.

@rbtcollins
Copy link
Contributor

I have a proposal for a variation on this. Two in fact.

  1. Rust looks at the version of the toolchain and if new enough sets RUSTUP_TOOLCHAIN to {"toolchain":toolchain, "toolchain_dir": thedir}

  2. we set RUSTUP_TOOLCHAIN_DATA={"toolchain":toolchain, "toolchain_dir": thedir} and cargo checks to see if the toolchain value is different to RUSTUP_TOOLCHAIN before using any other fields.

JSON is an arbitrary choice; we could use protobuf or whatever you like.

@ehuss
Copy link
Contributor Author

ehuss commented Feb 14, 2023

yep. What if someone sets RUSTUP_TOOLCHAIN in there somewhere?

Yea, I think in that case it would also be different.


I'd be concerned about changing the format of RUSTUP_TOOLCHAIN, since there are likely things inspecting it. Code search shows a fair number of hits, which would likely be a breaking change for them.

Another option I was reluctant to suggest is that Cargo could unset RUSTUP_TOOLCHAIN_DIR when calling other processes. That would ensure it isn't "sticky", and only RUSTUP_TOOLCHAIN would drive things. That should address the concern about an inner-non-rustup cargo calling something with RUSTUP_TOOLCHAIN changed or modifying PATH. That should make sure the behavior doesn't change (at least for things driven with cargo).

@rbtcollins
Copy link
Contributor

So given these three options:

  • add ..._DIR
  • add ..._DIR and unset it in cargo (which is what I originally asked for in Add RUSTUP_TOOLCHAIN_DIR #3207 (comment))
  • add ..._DATA with a built-in check code [the value of RUST_TOOLCHAIN when it was set]

What do you prefer and why?

@ehuss
Copy link
Contributor Author

ehuss commented Feb 19, 2023

Of the latter two, I don't have a strong preference. Using an encoded form is more flexible, but a little more complicated. I'm happy to do either, do you have a preference?

@rbtcollins
Copy link
Contributor

I am terrible at predicting the future ;).

If we're going to want to pass more info to cargo over time, having an extensible format is nice, because we can treat this as a bit of an API.

If we really just need this one thing to fix things, then _DIR + resetting it is clearly less work.

My hunch is that we'll find other things in future, but the question is whether they will be coupled like this is, or distinct and thus safer to simply add a new variable for

@ehuss ehuss marked this pull request as draft March 16, 2023 15:23
@rbtcollins
Copy link
Contributor

And I found a problem with the idea of Cargo programmatically converting RUSTUP_TOOLCHAIN -> the dir. We now support toolchains in arbitrary locations on disk in rustup-toolchain.toml.

Whether that was a mistake or not (I think it is) its out there now, and we'd have to be graceful in pulling it back.

OTOH an arbitrary absolute file path is now a valid toolchain name in RUSTUP_TOOLCHAIN. So cargo could look for that - but that will run into some trouble with respect to trusted_dirs. You should trust any arbitrary file path IFF rustup trusts it, but we have no method to signal that today, since most any process can invoke cargo with an env variable set :/.

@ehuss
Copy link
Contributor Author

ehuss commented Mar 29, 2023

Thanks for the heads up. I think if someone does that, then the optimization just won't work, which I'm fine with for now.

bors added a commit to rust-lang/cargo that referenced this pull request May 4, 2023
Optimize usage under rustup.

Closes #10986

This optimizes cargo when running under rustup to circumvent the rustup proxies. The rustup proxies introduce overhead that can make a noticeable difference.

The solution here is to identify if cargo would normally run `rustc` from PATH, and the current `rustc` in PATH points to something that looks like a rustup proxy (by comparing it to the `rustup` binary which is a hard-link to the proxy). If it detects this situation, then it looks for a binary in `$RUSTUP_HOME/toolchains/$TOOLCHAIN/bin/$TOOL`. If it finds the direct toolchain executable, then it uses that instead.

## Considerations

There have been some past attempts in the past to address this, but it has been a tricky problem to solve. This change has some risk because cargo is attempting to guess what the user and rustup wants, and it may guess wrong. Here are some considerations and risks for this:

* Setting `RUSTC` (as in rust-lang/rustup#2958) isn't an option. This makes the `RUSTC` setting "sticky" through invocations of different toolchains, such as a cargo subcommand or build script which does something like `cargo +nightly build`.

* Changing `PATH` isn't an option, due to issues like rust-lang/rustup#3036 where cargo subcommands would be unable to execute proxies (so things like `+toolchain` shorthands don't work).

* Setting other environment variables in rustup (as in rust-lang/rustup#3207 which adds `RUSTUP_TOOLCHAIN_DIR` the path to the toolchain dir) comes with various complications, as there is risk that the environment variables could get out of sync with one another (like with `RUSTUP_TOOLCHAIN`), causing tools to break or become confused.

  There was some consideration in that PR for adding protections by using an encoded environment variable that could be cross-checked, but I have concerns about the complexity of the solution.

  We may want to go with this solution in the long run, but I would like to try a short term solution in this PR first to see how it turns out.

* This won't work for a `rustup-toolchain.toml` override with a [`path`](https://rust-lang.github.io/rustup/overrides.html#path) setting. Cargo will use the slow path in that case. In theory it could try to detect this situation, which may be an exercise for the future.

* Some build-scripts, proc-macros, or custom cargo subcommands may be doing unusual things that interfere with the assumptions made in this PR. For example, a custom subcommand could call a `cargo` executable that is not managed by rustup. Proc-macros may be executing cargo or rustc, assuming it will reach some particular toolchain. It can be difficult to predict what unusual ways cargo and rustc are being used. This PR (and its tests) tries to make extra sure that it is resilient even in unusual circumstances.

* The "dev" fallback in rustup can introduce some complications for some solutions to this problem. If a rustup toolchain does not have cargo, such as with a developer "toolchain link", then rustup will automatically call either the nightly, beta, or stable cargo if they are available. This PR should work correctly, since rustup sets the correct `RUSTUP_TOOLCHAIN` environment variable for the *actual* toolchain, not the one where cargo was executed from.

* Special care should be considered for dynamic linking. `LD_LIBRARY_PATH` (linux), `DYLD_LIBRARY_PATH` (macos), and `PATH` (windows) need to be carefully set so that `rustc` can find its shared libraries. Directly executing `rustc` has some risk that it will load the wrong shared libraries. There are some mitigations for this. macOS and Linux use rpath, and Windows looks in the same directory as `rustc.exe`. Also, rustup configures the dyld environment variables from the outer cargo. Finally, cargo also configures these (particularly for the deprecated compiler plugins).

* This shouldn't impact installations that don't use rustup.

* I've done a variety of testing on the big three platforms, but certainly nowhere exhaustive.
    * One of many examples is making sure Clippy's development environment works correctly, which has special requirements for dynamic linking.

* There is risk about future rustup versions changing some assumptions made here. Some assumptions:
    * It assumes that if `RUSTUP_TOOLCHAIN` is set, then the proxy *always* runs exactly that toolchain and no other. If this changes, cargo could execute the wrong version. Currently `RUSTUP_TOOLCHAIN` is the highest priority [toolchain override](https://rust-lang.github.io/rustup/overrides.html) and is fundamental to how toolchain selection becomes "sticky", so I think it is unlikely to change.
    * It assumes rustup sets `RUSTUP_TOOLCHAIN` to a value that is exactly equal to the name of the toolchain in the `toolchains` directory. This works for user shorthands like `RUSTUP_TOOLCHAIN=nightly`, which gets converted to the full toolchain name. However, it does not work for `path` overrides (see above).
    * It assumes the `toolchains` directory layout is always `$RUSTUP_HOME/toolchains/$TOOLCHAIN`. If this changes, then I think the only consequence is that cargo will go back to the slow path.
    * It assumes downloading toolchains is not needed (since cargo running from the toolchain means it should already be downloaded).
    * It assumes there is no other environment setup needed (such as the dyld paths mentioned above).

  My hope is that if assumptions are no longer valid that the worst case is that cargo falls back to the slow path of running the proxy from PATH.

## Performance

This change won't affect the performance on Windows because rustup currently alters PATH to point to the toolchain directory. However, rust-lang/rustup#3178 is attempting to remove that, so this PR will be required to avoid a performance penalty on Windows. That change is currently opt-in, and will likely take a long while to roll out since it won't be released until after the next release, and may be difficult to get sufficient testing.

I have done some rough performance testing on macOS, Windows, and Linux on a variety of different kinds of projects with different commands. The following attempts to summarize what I saw.

The timings are going to be heavily dependent on the system and the project. These are the values I get on my systems, but will likely be very different for everyone else.

The Windows tests were performed with a custom build of rustup with rust-lang/rustup#3178 applied and enabled (stock rustup shows no change in performance as explained above).

The data is summarized in this spreadsheet: https://docs.google.com/spreadsheets/d/1zSvU1fQ0uSELxv3VqWmegGBhbLR-8_KUkyIzCIk21X0/edit?usp=sharing

`hello-world` has a particularly large impact of about 1.68 to 2.7x faster. However, a large portion of this overhead is related to running `rustc` at the start to discover its version and querying it for information. This is cached after the first run, so except for first-time builds, the effect isn't as noticeable. The "check with info" row is an benchmark that removes `target/debug/deps` but keeps the `.rustc_info.json` file.

Incremental builds are a bit more difficult to construct since it requires customizing the commands for each project. I only did an incremental test for cargo itself, running `touch src/cargo/lib.rs` and then `cargo check --lib`.

These measurements excluded the initial overhead of launching the rustup proxy to launch the initial cargo process. This was done just for simplicity, but it makes the test a little less characteristic of a typical usage, which will have some constant overhead for running the proxy.

These tests were done using [`hyperfine`](https://crates.io/crates/hyperfine) version 1.16.1. The macOS system was an M2 Max (12-thread). The Windows and Linux experiments were run on a AMD Ryzen Threadripper 2950X (32-thread). Rust 1.68.2 was used for testing. I can share the commands if people want to see them.
@rbtcollins
Copy link
Contributor

I think we can probably close this at this point?

@ehuss
Copy link
Contributor Author

ehuss commented May 21, 2023

Yea, it's fine to close. We can revisit if needed in the future. Thanks for the review and discussion!

@ehuss ehuss closed this May 21, 2023
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.

un-revert "Set RUSTC and RUSTDOC env for child processes run through the proxy"
2 participants