diff --git a/.github/workflows/all-features.yaml b/.github/workflows/all-features.yaml new file mode 100644 index 00000000000..beb62cece6a --- /dev/null +++ b/.github/workflows/all-features.yaml @@ -0,0 +1,46 @@ +name: AllFeatures (PR) + +# This is an additional workflow to test building with all feature combinations. +# Unlike our main workflows, this doesn't self-test the rustup install scripts, +# nor run on the rust docker images. This permits a smaller workflow without the +# templating and so on. + +on: + pull_request: + branches: + - "*" + - renovate/* + +jobs: + build: + name: Build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Might add more targets in future. + target: + - x86_64-unknown-linux-gnu + steps: + - name: Clone repo + uses: actions/checkout@v3 + - name: Install rustup stable + run: rustup toolchain install stable --profile minimal + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + version: "3.x" + - name: Set environment variables appropriately for the build + run: | + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + echo "TARGET=${{ matrix.target }}" >> $GITHUB_ENV + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + - name: Install cargo-all-features + run: cargo install cargo-all-features --git https://github.com/rbtcollins/cargo-all-features.git + - name: Ensure we have our goal target installed + run: | + rustup target install "$TARGET" + - name: Build every combination + run: | + cargo check-all-features --root-only diff --git a/.github/workflows/centos-fmt-clippy-on-all.yaml b/.github/workflows/centos-fmt-clippy-on-all.yaml index d073a488a21..47f1ca8d068 100644 --- a/.github/workflows/centos-fmt-clippy-on-all.yaml +++ b/.github/workflows/centos-fmt-clippy-on-all.yaml @@ -22,7 +22,8 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v3 + - name: Clone repo + uses: actions/checkout@v3 with: # v2 defaults to a shallow checkout, but we need at least to the previous tag fetch-depth: 0 @@ -71,6 +72,10 @@ jobs: run: | rustup component add rustfmt rustup component add clippy + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + version: '3.x' - name: Run the centos check within the docker image run: | docker run \ @@ -91,6 +96,6 @@ jobs: cargo fmt --all --check - name: Run cargo check and clippy run: | - cargo check --all --all-targets + cargo check --all --all-targets --features test git ls-files -- '*.rs' | xargs touch - cargo clippy --all --all-targets + cargo clippy --all --all-targets --features test diff --git a/.github/workflows/linux-builds-on-master.yaml b/.github/workflows/linux-builds-on-master.yaml index c037f90fcc8..1eab933359d 100644 --- a/.github/workflows/linux-builds-on-master.yaml +++ b/.github/workflows/linux-builds-on-master.yaml @@ -98,6 +98,10 @@ jobs: echo "DOCKER=$DOCKER" >> $GITHUB_ENV - name: Fetch the docker run: bash ci/fetch-rust-docker.bash "${TARGET}" + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + version: '3.x' - name: Maybe build a docker from there run: | if [ -f "ci/docker/$DOCKER/Dockerfile" ]; then diff --git a/.github/workflows/linux-builds-on-pr.yaml b/.github/workflows/linux-builds-on-pr.yaml index d5ee9e8cd28..da3da8c6301 100644 --- a/.github/workflows/linux-builds-on-pr.yaml +++ b/.github/workflows/linux-builds-on-pr.yaml @@ -92,6 +92,10 @@ jobs: echo "DOCKER=$DOCKER" >> $GITHUB_ENV - name: Fetch the docker run: bash ci/fetch-rust-docker.bash "${TARGET}" + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + version: '3.x' - name: Maybe build a docker from there run: | if [ -f "ci/docker/$DOCKER/Dockerfile" ]; then diff --git a/.github/workflows/linux-builds-on-stable.yaml b/.github/workflows/linux-builds-on-stable.yaml index 35eed326229..df8b435e1c5 100644 --- a/.github/workflows/linux-builds-on-stable.yaml +++ b/.github/workflows/linux-builds-on-stable.yaml @@ -121,6 +121,10 @@ jobs: echo "DOCKER=$DOCKER" >> $GITHUB_ENV - name: Fetch the docker run: bash ci/fetch-rust-docker.bash "${TARGET}" + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + version: '3.x' - name: Maybe build a docker from there run: | if [ -f "ci/docker/$DOCKER/Dockerfile" ]; then diff --git a/.github/workflows/windows-builds-on-master.yaml b/.github/workflows/windows-builds-on-master.yaml index 5f17c53169c..186bfc210db 100644 --- a/.github/workflows/windows-builds-on-master.yaml +++ b/.github/workflows/windows-builds-on-master.yaml @@ -113,9 +113,9 @@ jobs: TARGET: ${{ matrix.target }} # os-specific code leads to lints escaping if we only run this in one target run: | - cargo check --all --all-targets + cargo check --all --all-targets --features test git ls-files -- '*.rs' | xargs touch - cargo clippy --workspace --all-targets + cargo clippy --workspace --all-targets --features test - name: Upload the built artifact if: matrix.mode == 'release' uses: actions/upload-artifact@v3 diff --git a/.github/workflows/windows-builds-on-pr.yaml b/.github/workflows/windows-builds-on-pr.yaml index 7842f91d26e..206151b8b8f 100644 --- a/.github/workflows/windows-builds-on-pr.yaml +++ b/.github/workflows/windows-builds-on-pr.yaml @@ -107,9 +107,9 @@ jobs: TARGET: ${{ matrix.target }} # os-specific code leads to lints escaping if we only run this in one target run: | - cargo check --all --all-targets + cargo check --all --all-targets --features test git ls-files -- '*.rs' | xargs touch - cargo clippy --workspace --all-targets + cargo clippy --workspace --all-targets --features test - name: Upload the built artifact if: matrix.mode == 'release' uses: actions/upload-artifact@v3 diff --git a/.github/workflows/windows-builds-on-stable.yaml b/.github/workflows/windows-builds-on-stable.yaml index bcdb3e43e33..d8cc8308530 100644 --- a/.github/workflows/windows-builds-on-stable.yaml +++ b/.github/workflows/windows-builds-on-stable.yaml @@ -116,9 +116,9 @@ jobs: TARGET: ${{ matrix.target }} # os-specific code leads to lints escaping if we only run this in one target run: | - cargo check --all --all-targets + cargo check --all --all-targets --features test git ls-files -- '*.rs' | xargs touch - cargo clippy --workspace --all-targets + cargo clippy --workspace --all-targets --features test - name: Upload the built artifact if: matrix.mode == 'release' uses: actions/upload-artifact@v3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 366deafce04..6e9f70f8867 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ 1. Fork it! 2. Create your feature branch: `git checkout -b my-new-feature` -3. Test it: `cargo test` +3. Test it: `cargo test --features=test` 4. Lint it: `cargo +beta clippy --all --all-targets -- -D warnings` > We use `cargo clippy` to ensure high-quality code and to enforce a set of best practices for Rust programming. However, not all lints provided by `cargo clippy` are relevant or applicable to our project. > We may choose to ignore some lints if they are unstable, experimental, or specific to our project. @@ -312,7 +312,7 @@ And [look in Jaeger for a trace](http://localhost:16686/search?service=rustup). The custom macro `rustup_macros::test` adds a prelude and suffix to each test to ensure that there is a tracing context setup, that the test function is a span, -and that the spans from the test are flushed. Build with features=otel to +and that the spans from the test are flushed. Build with features=otel,test to use this feature. ### Adding instrumentation diff --git a/Cargo.lock b/Cargo.lock index 8b3aaf77510..2755ca33e24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,9 +37,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6342bd4f5a1205d7f41e94a41a901f5647c938cdfa96036338e8533c9d6c2450" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", @@ -164,9 +164,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b70caf9f1b0c045f7da350636435b775a9733adf2df56e8aa2a29210fbc335d4" +checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" dependencies = [ "async-trait", "axum-core", @@ -219,6 +219,21 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -542,6 +557,17 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.6" @@ -1176,6 +1202,12 @@ version = "0.2.142" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + [[package]] name = "libz-sys" version = "1.1.9" @@ -1374,6 +1406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1622,9 +1655,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "ppv-lite86" @@ -1651,6 +1684,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error 2.0.1", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.6.29", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "prost" version = "0.11.9" @@ -1716,6 +1770,18 @@ dependencies = [ "unicase", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.26" @@ -1755,6 +1821,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.7.0" @@ -1980,10 +2055,12 @@ dependencies = [ "chrono", "clap", "clap_complete", + "derivative", "download", "effective-limits", "enum-map", "flate2", + "fs_at", "git-testament", "home", "lazy_static", @@ -1994,6 +2071,7 @@ dependencies = [ "openssl", "opentelemetry", "opentelemetry-otlp", + "proptest", "pulldown-cmark", "rand", "regex", @@ -2023,6 +2101,7 @@ dependencies = [ "wait-timeout", "walkdir", "winapi", + "windows-sys 0.45.0", "winreg 0.50.0", "xz2", "zstd", @@ -2045,6 +2124,18 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.13" @@ -2808,6 +2899,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" version = "2.6.0" @@ -3212,9 +3309,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5617da7e1f97bf363947d767b91aaf3c2bbc19db7fda9c65af1278713d58e0a2" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index c19a19dd2f0..6012ded1d24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,9 @@ otel = [ "dep:tokio", ] +# Exports code dependent on private interfaces for the integration test suite +test = ["dep:once_cell", "dep:walkdir"] + # Sorted by alphabetic order [dependencies] anyhow.workspace = true @@ -48,21 +51,19 @@ cfg-if = "1.0" chrono = "0.4" clap = { version = "3", features = ["wrap_help"] } clap_complete = "3" +derivative.workspace = true download = { path = "download", default-features = false } effective-limits = "0.5.5" enum-map = "2.5.0" flate2 = "1" +fs_at.workspace = true git-testament = "0.2" home = "0.5.4" lazy_static.workspace = true libc = "0.2" num_cpus = "1.15" -once_cell.workspace = true +once_cell = { workspace = true, optional = true } opener = "0.6.0" -# Used by `curl` or `reqwest` backend although it isn't imported by our rustup : -# this allows controlling the vendoring status without exposing the presence of -# the download crate. -openssl = { version = "0.10", optional = true } opentelemetry = { workspace = true, optional = true } opentelemetry-otlp = { workspace = true, optional = true } pulldown-cmark = { version = "0.9", default-features = false } @@ -78,8 +79,7 @@ sharded-slab = "0.1.1" strsim = "0.10" tar = "0.4.26" tempfile.workspace = true -# FIXME(issue #1818, #1826, and friends) -term = "=0.5.1" +term = "=0.5.1" # FIXME(issue #1818, #1826, and friends) thiserror.workspace = true threadpool = "1" tokio = { workspace = true, optional = true } @@ -91,9 +91,17 @@ tracing-subscriber = { workspace = true, optional = true, features = [ tracing.workspace = true url.workspace = true wait-timeout = "0.2" +walkdir = { workspace = true, optional = true } xz2 = "0.1.3" zstd = "0.12" +[dependencies.openssl] +# Used by `curl` or `reqwest` backend although it isn't imported by our rustup : +# this allows controlling the vendoring status without exposing the presence of +# the download crate. +optional = true +version = "0.10" + [dependencies.retry] default-features = false features = ["random"] @@ -133,13 +141,28 @@ features = [ ] version = "0.3" +[target."cfg(windows)".dependencies.windows-sys] +features = [ + "Win32_Foundation", + "Win32_Storage_FileSystem", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "Win32_Security", + "Win32_System_Kernel", + "Win32_System_IO", + "Win32_System_Ioctl", +] +version = "0.45.0" + + [dev-dependencies] enum-map = "2.5.0" once_cell.workspace = true +proptest.workspace = true rustup-macros.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } trycmd = "0.14.13" -walkdir = "2" [build-dependencies] lazy_static = "1" @@ -150,18 +173,24 @@ members = ["download", "rustup-macros"] [workspace.dependencies] anyhow = "1.0.69" +derivative = "2.2.0" +fs_at = "0.1.6" lazy_static = "1" once_cell = "1.17.1" opentelemetry = { version = "0.18.0", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.11.0" } +proptest = "1.1.0" rustup-macros = { path = "rustup-macros" } tempfile = "3.5" thiserror = "1.0" -tokio = { version = "1.26.0", default-features = false } +tokio = { version = "1.26.0", default-features = false, features = [ + "rt-multi-thread", +] } tracing = "0.1" tracing-opentelemetry = { version = "0.18.0" } tracing-subscriber = "0.3.16" url = "2.3" +walkdir = "2" [lib] name = "rustup" @@ -174,3 +203,7 @@ lto = true # Reduce build time by setting proc-macro crates non optimized. [profile.release.build-override] opt-level = 0 + +[package.metadata.cargo-all-features] +# Building with no web backend will error. +always_include_features = ["reqwest-backend", "reqwest-rustls-tls"] diff --git a/ci/actions-templates/centos-fmt-clippy-template.yaml b/ci/actions-templates/centos-fmt-clippy-template.yaml index d073a488a21..47f1ca8d068 100644 --- a/ci/actions-templates/centos-fmt-clippy-template.yaml +++ b/ci/actions-templates/centos-fmt-clippy-template.yaml @@ -22,7 +22,8 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v3 + - name: Clone repo + uses: actions/checkout@v3 with: # v2 defaults to a shallow checkout, but we need at least to the previous tag fetch-depth: 0 @@ -71,6 +72,10 @@ jobs: run: | rustup component add rustfmt rustup component add clippy + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + version: '3.x' - name: Run the centos check within the docker image run: | docker run \ @@ -91,6 +96,6 @@ jobs: cargo fmt --all --check - name: Run cargo check and clippy run: | - cargo check --all --all-targets + cargo check --all --all-targets --features test git ls-files -- '*.rs' | xargs touch - cargo clippy --all --all-targets + cargo clippy --all --all-targets --features test diff --git a/ci/actions-templates/linux-builds-template.yaml b/ci/actions-templates/linux-builds-template.yaml index 0e10e868fd9..8799eb36558 100644 --- a/ci/actions-templates/linux-builds-template.yaml +++ b/ci/actions-templates/linux-builds-template.yaml @@ -130,6 +130,10 @@ jobs: echo "DOCKER=$DOCKER" >> $GITHUB_ENV - name: Fetch the docker run: bash ci/fetch-rust-docker.bash "${TARGET}" + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + version: '3.x' - name: Maybe build a docker from there run: | if [ -f "ci/docker/$DOCKER/Dockerfile" ]; then diff --git a/ci/actions-templates/windows-builds-template.yaml b/ci/actions-templates/windows-builds-template.yaml index 94c673ca3be..3e102b7c145 100644 --- a/ci/actions-templates/windows-builds-template.yaml +++ b/ci/actions-templates/windows-builds-template.yaml @@ -125,9 +125,9 @@ jobs: TARGET: ${{ matrix.target }} # os-specific code leads to lints escaping if we only run this in one target run: | - cargo check --all --all-targets + cargo check --all --all-targets --features test git ls-files -- '*.rs' | xargs touch - cargo clippy --workspace --all-targets + cargo clippy --workspace --all-targets --features test - name: Upload the built artifact if: matrix.mode == 'release' uses: actions/upload-artifact@v3 diff --git a/ci/run.bash b/ci/run.bash index eae7d8f26af..41eda29eda2 100644 --- a/ci/run.bash +++ b/ci/run.bash @@ -56,26 +56,28 @@ download_pkg_test() { # Machines have 7GB of RAM, and our target/ contents is large enough that # thrashing will occur if we build-run-build-run rather than -# build-build-build-run-run-run. +# build-build-build-run-run-run. Since this is used soley for non-release +# artifacts, we try to keep features consistent across the builds, whether for +# docs/test/runs etc. build_test() { cmd="$1" shift download_pkg_test "${cmd}" - if [ "build" = "${cmd}" ]; then - target_cargo "${cmd}" --workspace --all-targets + if [ "build" = "${cmd}" ]; then + target_cargo "${cmd}" --workspace --all-targets --features test else # free runners have 2 or 3(mac) cores - target_cargo "${cmd}" --workspace --tests -- --test-threads 2 + target_cargo "${cmd}" --workspace --features test --tests -- --test-threads 2 fi if [ "build" != "${cmd}" ]; then - target_cargo "${cmd}" --doc --workspace + target_cargo "${cmd}" --doc --workspace --features test fi } if [ -z "$SKIP_TESTS" ]; then - cargo run --locked --profile "$BUILD_PROFILE" --target "$TARGET" "${FEATURES[@]}" -- --dump-testament + cargo run --locked --profile "$BUILD_PROFILE" --features test --target "$TARGET" "${FEATURES[@]}" -- --dump-testament build_test build build_test test fi diff --git a/doc/src/environment-variables.md b/doc/src/environment-variables.md index baac718ea1d..8ef3351945a 100644 --- a/doc/src/environment-variables.md +++ b/doc/src/environment-variables.md @@ -4,9 +4,10 @@ root `rustup` folder, used for storing installed toolchains and configuration options. -- `RUSTUP_TOOLCHAIN` (default: none) If set, will [override] the toolchain - used for all rust tool invocations. A toolchain with this name should be - installed, or invocations will fail. +- `RUSTUP_TOOLCHAIN` (default: none) If set, will [override] the toolchain used + for all rust tool invocations. A toolchain with this name should be installed, + or invocations will fail. This can specify custom toolchains, installable + toolchains, or the absolute path to a toolchain. - `RUSTUP_DIST_SERVER` (default: `https://static.rust-lang.org`) Sets the root URL for downloading static resources related to Rust. You can change this to diff --git a/download/src/lib.rs b/download/src/lib.rs index 812a4e3370c..b13361b4784 100644 --- a/download/src/lib.rs +++ b/download/src/lib.rs @@ -48,6 +48,7 @@ fn download_with_backend( } type DownloadCallback<'a> = &'a dyn Fn(Event<'_>) -> Result<()>; + pub fn download_to_path_with_backend( backend: Backend, url: &Url, @@ -138,6 +139,9 @@ pub fn download_to_path_with_backend( }) } +#[cfg(all(not(feature = "reqwest-backend"), not(feature = "curl-backend")))] +compile_error!("Must enable at least one backend"); + /// Download via libcurl; encrypt with the native (or OpenSSl) TLS /// stack via libcurl #[cfg(feature = "curl-backend")] @@ -255,10 +259,17 @@ pub mod curl { #[cfg(feature = "reqwest-backend")] pub mod reqwest_be { + #[cfg(all( + not(feature = "reqwest-rustls-tls"), + not(feature = "reqwest-default-tls") + ))] + compile_error!("Must select a reqwest TLS backend"); + use std::io; use std::time::Duration; use anyhow::{anyhow, Context, Result}; + #[cfg(feature = "reqwest-rustls-tls")] use lazy_static::lazy_static; use reqwest::blocking::{Client, ClientBuilder, Response}; use reqwest::{header, Proxy}; diff --git a/src/cli/common.rs b/src/cli/common.rs index 122c0a0f74a..36827de3237 100644 --- a/src/cli/common.rs +++ b/src/cli/common.rs @@ -1,5 +1,6 @@ //! Just a dumping ground for cli stuff +use std::fmt::Display; use std::fs; use std::io::{BufRead, ErrorKind, Write}; use std::path::Path; @@ -13,13 +14,15 @@ use term2::Terminal; use super::self_update; use super::term2; -use crate::dist::notifications as dist_notifications; -use crate::process; -use crate::toolchain::DistributableToolchain; use crate::utils::notifications as util_notifications; use crate::utils::notify::NotificationLevel; use crate::utils::utils; -use crate::{Cfg, Notification, Toolchain, UpdateStatus}; +use crate::{dist::dist::ToolchainDesc, install::UpdateStatus}; +use crate::{ + dist::notifications as dist_notifications, toolchain::distributable::DistributableToolchain, +}; +use crate::{process, toolchain::toolchain::Toolchain}; +use crate::{Cfg, Notification}; pub(crate) const WARN_COMPLETE_PROFILE: &str = "downloading with complete profile isn't recommended unless you are a developer of the rust language"; @@ -178,51 +181,71 @@ pub(crate) fn set_globals(verbose: bool, quiet: bool) -> Result { pub(crate) fn show_channel_update( cfg: &Cfg, - name: &str, + name: PackageUpdate, updated: Result, ) -> Result<()> { - show_channel_updates(cfg, vec![(name.to_string(), updated)]) + show_channel_updates(cfg, vec![(name, updated)]) } -fn show_channel_updates(cfg: &Cfg, toolchains: Vec<(String, Result)>) -> Result<()> { - let data = toolchains.into_iter().map(|(name, result)| { - let toolchain = cfg.get_toolchain(&name, false)?; - let mut version: String = toolchain.rustc_version(); - - let banner; - let color; - let mut previous_version: Option = None; - match result { - Ok(UpdateStatus::Installed) => { - banner = "installed"; - color = Some(term2::color::GREEN); - } - Ok(UpdateStatus::Updated(v)) => { - if name == "rustup" { - previous_version = Some(env!("CARGO_PKG_VERSION").into()); - version = v; - } else { - previous_version = Some(v); - } - banner = "updated"; - color = Some(term2::color::GREEN); - } - Ok(UpdateStatus::Unchanged) => { - if name == "rustup" { - version = env!("CARGO_PKG_VERSION").into(); - } - banner = "unchanged"; - color = None; +pub(crate) enum PackageUpdate { + Rustup, + Toolchain(ToolchainDesc), +} + +impl Display for PackageUpdate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PackageUpdate::Rustup => write!(f, "rustup"), + PackageUpdate::Toolchain(t) => write!(f, "{t}"), + } + } +} + +fn show_channel_updates( + cfg: &Cfg, + updates: Vec<(PackageUpdate, Result)>, +) -> Result<()> { + let data = updates.into_iter().map(|(pkg, result)| { + let (banner, color) = match &result { + Ok(UpdateStatus::Installed) => ("installed", Some(term2::color::GREEN)), + Ok(UpdateStatus::Updated(_)) => ("updated", Some(term2::color::GREEN)), + Ok(UpdateStatus::Unchanged) => ("unchanged", None), + Err(_) => ("update failed", Some(term2::color::RED)), + }; + + let (previous_version, version) = match &pkg { + PackageUpdate::Rustup => { + let previous_version: Option = match result { + Ok(UpdateStatus::Installed) | Ok(UpdateStatus::Unchanged) | Err(_) => None, + _ => Some(env!("CARGO_PKG_VERSION").into()), + }; + let version = match result { + Err(_) | Ok(UpdateStatus::Installed) | Ok(UpdateStatus::Unchanged) => { + env!("CARGO_PKG_VERSION").into() + } + Ok(UpdateStatus::Updated(v)) => v, + }; + (previous_version, version) } - Err(_) => { - banner = "update failed"; - color = Some(term2::color::RED); + PackageUpdate::Toolchain(name) => { + // this is a bit strange: we don't supply the version we + // presumably had (for Installed and Unchanged), so we query it + // again. Perhaps we can do better. + let version = match Toolchain::new(cfg, name.into()) { + Ok(t) => t.rustc_version(), + Err(_) => String::from("(toolchain not installed)"), + }; + let previous_version: Option = match result { + Ok(UpdateStatus::Installed) | Ok(UpdateStatus::Unchanged) | Err(_) => None, + Ok(UpdateStatus::Updated(v)) => Some(v), + }; + (previous_version, version) } - } + }; - let width = name.len() + 1 + banner.len(); + let width = pkg.to_string().len() + 1 + banner.len(); - Ok((name, banner, width, color, version, previous_version)) + Ok((pkg, banner, width, color, version, previous_version)) }); let mut t = term2::stdout(); @@ -232,7 +255,7 @@ fn show_channel_updates(cfg: &Cfg, toolchains: Vec<(String, Result .iter() .fold(0, |a, &(_, _, width, _, _, _)| cmp::max(a, width)); - for (name, banner, width, color, version, previous_version) in data { + for (pkg, banner, width, color, version, previous_version) in data { let padding = max_width - width; let padding: String = " ".repeat(padding); let _ = write!(t, " {padding}"); @@ -240,8 +263,7 @@ fn show_channel_updates(cfg: &Cfg, toolchains: Vec<(String, Result if let Some(color) = color { let _ = t.fg(color); } - let _ = write!(t, "{name} "); - let _ = write!(t, "{banner}"); + let _ = write!(t, "{pkg} {banner}"); let _ = t.reset(); let _ = write!(t, " - {version}"); if let Some(previous_version) = previous_version { @@ -269,7 +291,11 @@ pub(crate) fn update_all_channels( if !toolchains.is_empty() { writeln!(process().stdout())?; - show_channel_updates(cfg, toolchains)?; + let t = toolchains + .into_iter() + .map(|(p, s)| (PackageUpdate::Toolchain(p), s)) + .collect(); + show_channel_updates(cfg, t)?; } Ok(utils::ExitCode(0)) }; @@ -342,10 +368,12 @@ where Ok(utils::ExitCode(0)) } -pub(crate) fn list_targets(toolchain: &Toolchain<'_>) -> Result { +pub(crate) fn list_targets(distributable: DistributableToolchain<'_>) -> Result { let mut t = term2::stdout(); - let distributable = DistributableToolchain::new_for_components(toolchain)?; - let components = distributable.list_components()?; + let manifestation = distributable.get_manifestation()?; + let config = manifestation.read_config()?.unwrap_or_default(); + let manifest = distributable.get_manifest()?; + let components = manifest.query_components(distributable.desc(), &config)?; for component in components { if component.component.short_name_in_manifest() == "rust-std" { let target = component @@ -366,10 +394,14 @@ pub(crate) fn list_targets(toolchain: &Toolchain<'_>) -> Result Ok(utils::ExitCode(0)) } -pub(crate) fn list_installed_targets(toolchain: &Toolchain<'_>) -> Result { +pub(crate) fn list_installed_targets( + distributable: DistributableToolchain<'_>, +) -> Result { let mut t = term2::stdout(); - let distributable = DistributableToolchain::new_for_components(toolchain)?; - let components = distributable.list_components()?; + let manifestation = distributable.get_manifestation()?; + let config = manifestation.read_config()?.unwrap_or_default(); + let manifest = distributable.get_manifest()?; + let components = manifest.query_components(distributable.desc(), &config)?; for component in components { if component.component.short_name_in_manifest() == "rust-std" { let target = component @@ -385,10 +417,14 @@ pub(crate) fn list_installed_targets(toolchain: &Toolchain<'_>) -> Result) -> Result { +pub(crate) fn list_components( + distributable: DistributableToolchain<'_>, +) -> Result { let mut t = term2::stdout(); - let distributable = DistributableToolchain::new_for_components(toolchain)?; - let components = distributable.list_components()?; + let manifestation = distributable.get_manifestation()?; + let config = manifestation.read_config()?.unwrap_or_default(); + let manifest = distributable.get_manifest()?; + let components = manifest.query_components(distributable.desc(), &config)?; for component in components { let name = component.name; if component.installed { @@ -403,16 +439,19 @@ pub(crate) fn list_components(toolchain: &Toolchain<'_>) -> Result) -> Result { +pub(crate) fn list_installed_components(distributable: DistributableToolchain<'_>) -> Result<()> { let mut t = term2::stdout(); - let distributable = DistributableToolchain::new_for_components(toolchain)?; - let components = distributable.list_components()?; + let manifestation = distributable.get_manifestation()?; + let config = manifestation.read_config()?.unwrap_or_default(); + let manifest = distributable.get_manifest()?; + let components = manifest.query_components(distributable.desc(), &config)?; + for component in components { if component.installed { writeln!(t, "{}", component.name)?; } } - Ok(utils::ExitCode(0)) + Ok(()) } fn print_toolchain_path( @@ -422,11 +461,7 @@ fn print_toolchain_path( if_override: &str, verbose: bool, ) -> Result<()> { - let toolchain_path = { - let mut t_path = cfg.toolchains_dir.clone(); - t_path.push(toolchain); - t_path - }; + let toolchain_path = cfg.toolchains_dir.join(toolchain); let toolchain_meta = fs::symlink_metadata(&toolchain_path)?; let toolchain_path = if verbose { if toolchain_meta.is_dir() { @@ -449,35 +484,42 @@ fn print_toolchain_path( } pub(crate) fn list_toolchains(cfg: &Cfg, verbose: bool) -> Result { - let toolchains = cfg.list_toolchains()?; + // Work with LocalToolchainName to accomdate path based overrides + let toolchains = cfg + .list_toolchains()? + .iter() + .map(Into::into) + .collect::>(); if toolchains.is_empty() { writeln!(process().stdout(), "no installed toolchains")?; } else { - let def_toolchain_name = if let Ok(Some(def_toolchain)) = cfg.find_default() { - def_toolchain.name().to_string() - } else { - String::new() - }; + let def_toolchain_name = cfg.get_default()?.map(|t| (&t).into()); let cwd = utils::current_dir()?; let ovr_toolchain_name = if let Ok(Some((toolchain, _reason))) = cfg.find_override(&cwd) { - toolchain.name().to_string() + Some(toolchain) } else { - String::new() + None }; for toolchain in toolchains { - let if_default = if def_toolchain_name == *toolchain { + let if_default = if def_toolchain_name.as_ref() == Some(&toolchain) { " (default)" } else { "" }; - let if_override = if ovr_toolchain_name == *toolchain { + let if_override = if ovr_toolchain_name.as_ref() == Some(&toolchain) { " (override)" } else { "" }; - print_toolchain_path(cfg, &toolchain, if_default, if_override, verbose) - .context("Failed to list toolchains' directories")?; + print_toolchain_path( + cfg, + &toolchain.to_string(), + if_default, + if_override, + verbose, + ) + .context("Failed to list toolchains' directories")?; } } Ok(utils::ExitCode(0)) diff --git a/src/cli/help.rs b/src/cli/help.rs index 20e22bc8799..40591b9b6b3 100644 --- a/src/cli/help.rs +++ b/src/cli/help.rs @@ -48,8 +48,8 @@ pub(crate) static TOOLCHAIN_HELP: &str = r"DISCUSSION: installation of the Rust compiler. `rustup` supports multiple types of toolchains. The most basic track the official release channels: 'stable', 'beta' and 'nightly'; but `rustup` can also - install toolchains from the official archives, for alternate host - platforms, and from local builds. + install specific toolchains from the official archives, toolchains for + alternate host platforms, and from local builds ('custom toolchains'). Standard release channel toolchain names have the following form: @@ -87,10 +87,12 @@ pub(crate) static TOOLCHAIN_HELP: &str = r"DISCUSSION: pub(crate) static TOOLCHAIN_LINK_HELP: &str = r"DISCUSSION: 'toolchain' is the custom name to be assigned to the new toolchain. - Any name is permitted as long as it does not fully match an initial - substring of a standard release channel. For example, you can use - the names 'latest' or '2017-04-01' but you cannot use 'stable' or - 'beta-i686' or 'nightly-x86_64-unknown-linux-gnu'. + Any name is permitted as long as: + - it does not include '/' or '\' except as the last character + - it is not equal to 'none' + - it does not fully match an initialsubstring of a standard release channel. + For example, you can use the names 'latest' or '2017-04-01' but you cannot + use 'stable' or 'beta-i686' or 'nightly-x86_64-unknown-linux-gnu'. 'path' specifies the directory where the binaries and libraries for the custom toolchain can be found. For example, when used for @@ -274,9 +276,19 @@ pub(crate) static COMPLETIONS_HELP: &str = r"DISCUSSION: $ rustup completions zsh cargo > ~/.zfunc/_cargo"; -pub(crate) static TOOLCHAIN_ARG_HELP: &str = "Toolchain name, such as 'stable', 'nightly', \ +pub(crate) static OFFICIAL_TOOLCHAIN_ARG_HELP: &str = + "Toolchain name, such as 'stable', 'nightly', \ or '1.8.0'. For more information see `rustup \ help toolchain`"; +pub(crate) static RESOLVABLE_LOCAL_TOOLCHAIN_ARG_HELP: &str = "Toolchain name, such as 'stable', 'nightly', \ + '1.8.0', or a custom toolchain name, or an absolute path. For more \ + information see `rustup help toolchain`"; +pub(crate) static RESOLVABLE_TOOLCHAIN_ARG_HELP: &str = "Toolchain name, such as 'stable', 'nightly', \ + '1.8.0', or a custom toolchain name. For more information see `rustup \ + help toolchain`"; +pub(crate) static MAYBE_RESOLVABLE_TOOLCHAIN_ARG_HELP: &str = "'none', a toolchain name, such as 'stable', 'nightly', \ + '1.8.0', or a custom toolchain name. For more information see `rustup \ + help toolchain`"; pub(crate) static TOPIC_ARG_HELP: &str = "Topic such as 'core', 'fn', 'usize', 'eprintln!', \ 'core::arch', 'alloc::format!', 'std::fs', \ diff --git a/src/cli/proxy_mode.rs b/src/cli/proxy_mode.rs index 52813f4f016..becb49a4e0e 100644 --- a/src/cli/proxy_mode.rs +++ b/src/cli/proxy_mode.rs @@ -2,12 +2,13 @@ use std::ffi::OsString; use anyhow::Result; -use super::common::set_globals; -use super::job; -use super::self_update; -use crate::command::run_command_for_dir; -use crate::utils::utils::{self, ExitCode}; -use crate::Cfg; +use crate::{ + cli::{common::set_globals, job, self_update}, + command::run_command_for_dir, + toolchain::names::{LocalToolchainName, ResolvableLocalToolchainName}, + utils::utils::{self, ExitCode}, + Cfg, +}; #[cfg_attr(feature = "otel", tracing::instrument)] pub fn main(arg0: &str) -> Result { @@ -18,23 +19,26 @@ pub fn main(arg0: &str) -> Result { let mut args = crate::process().args_os().skip(1); - // Check for a toolchain specifier. + // Check for a + toolchain specifier let arg1 = args.next(); - let toolchain_arg = arg1 + let toolchain = arg1 .as_ref() .map(|arg| arg.to_string_lossy()) - .filter(|arg| arg.starts_with('+')); - let toolchain = toolchain_arg.as_ref().map(|a| &a[1..]); + .filter(|arg| arg.starts_with('+')) + .map(|name| ResolvableLocalToolchainName::try_from(&name.as_ref()[1..])) + .transpose()?; // Build command args now while we know whether or not to skip arg 1. - let cmd_args: Vec<_> = if toolchain.is_none() { - crate::process().args_os().skip(1).collect() - } else { - crate::process().args_os().skip(2).collect() - }; + let cmd_args: Vec<_> = crate::process() + .args_os() + .skip(1 + toolchain.is_some() as usize) + .collect(); let cfg = set_globals(false, true)?; cfg.check_metadata_version()?; + let toolchain = toolchain + .map(|t| t.resolve(&cfg.get_default_host_triple()?)) + .transpose()?; direct_proxy(&cfg, arg0, toolchain, &cmd_args)? }; @@ -45,12 +49,12 @@ pub fn main(arg0: &str) -> Result { fn direct_proxy( cfg: &Cfg, arg0: &str, - toolchain: Option<&str>, + toolchain: Option, args: &[OsString], ) -> Result { let cmd = match toolchain { None => cfg.create_command_for_dir(&utils::current_dir()?, arg0)?, - Some(tc) => cfg.create_command_for_toolchain(tc, false, arg0)?, + Some(tc) => cfg.create_command_for_toolchain(&tc, false, arg0)?, }; run_command_for_dir(cmd, arg0, args) } diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 8960d9de290..56d3c5c0cd6 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -4,31 +4,43 @@ use std::path::{Path, PathBuf}; use std::process; use std::str::FromStr; -use anyhow::{anyhow, bail, Error, Result}; +use anyhow::{anyhow, Error, Result}; use clap::{ builder::{EnumValueParser, PossibleValuesParser}, AppSettings, Arg, ArgAction, ArgEnum, ArgGroup, ArgMatches, Command, PossibleValue, }; use clap_complete::Shell; -use super::help::*; -use super::self_update; -use super::term2; -use super::term2::Terminal; -use super::topical_doc; -use super::{ - common, - self_update::{check_rustup_update, SelfUpdateMode}, +use crate::{ + cli::{ + common::{self, PackageUpdate}, + errors::CLIError, + help::*, + self_update::{self, check_rustup_update, SelfUpdateMode}, + term2::{self, Terminal}, + topical_doc, + }, + command, + dist::{ + dist::{PartialToolchainDesc, Profile, TargetTriple}, + manifest::{Component, ComponentStatus}, + }, + errors::RustupError, + install::UpdateStatus, + process, + toolchain::{ + distributable::DistributableToolchain, + names::{ + custom_toolchain_name_parser, maybe_resolvable_toolchainame_parser, + partial_toolchain_desc_parser, resolvable_local_toolchainame_parser, + resolvable_toolchainame_parser, CustomToolchainName, MaybeResolvableToolchainName, + ResolvableLocalToolchainName, ResolvableToolchainName, ToolchainName, + }, + toolchain::Toolchain, + }, + utils::utils, + Cfg, Notification, }; -use crate::cli::errors::CLIError; -use crate::dist::dist::{PartialTargetTriple, PartialToolchainDesc, Profile, TargetTriple}; -use crate::dist::manifest::Component; -use crate::errors::RustupError; -use crate::process; -use crate::toolchain::{CustomToolchain, DistributableToolchain}; -use crate::utils::utils; -use crate::Notification; -use crate::{command, Cfg, ComponentStatus, Toolchain}; const TOOLCHAIN_OVERRIDE_ERROR: &str = "To override the toolchain using the 'rustup +toolchain' syntax, \ @@ -90,7 +102,7 @@ pub fn main() -> Result { if let Some(t) = process().args().find(|x| x.starts_with('+')) { debug!("Fetching rustc version from toolchain `{}`", t); - cfg.set_toolchain_override(&t[1..]); + cfg.set_toolchain_override(&ResolvableToolchainName::try_from(&t[1..])?); } let toolchain = cfg.find_or_install_override_toolchain_or_default(&cwd)?.0; @@ -127,8 +139,8 @@ pub fn main() -> Result { let quiet = matches.get_flag("quiet"); let cfg = &mut common::set_globals(verbose, quiet)?; - if let Some(t) = matches.get_one::("+toolchain") { - cfg.set_toolchain_override(&t[1..]); + if let Some(t) = matches.get_one::("+toolchain") { + cfg.set_toolchain_override(t); } if maybe_upgrade_data(cfg, &matches)? { @@ -252,12 +264,10 @@ pub(crate) fn cli() -> Command<'static> { Arg::new("+toolchain") .help("release channel (e.g. +stable) or custom toolchain to set override") .value_parser(|s: &str| { - if s.starts_with('+') { - Ok(s.to_owned()) + if let Some(stripped) = s.strip_prefix('+') { + ResolvableToolchainName::try_from(stripped).map_err(|e| clap::Error::raw(clap::ErrorKind::InvalidValue, e)) } else { - Err(format!( - "\"{s}\" is not a valid subcommand, so it was interpreted as a toolchain name, but it is also invalid. {TOOLCHAIN_OVERRIDE_ERROR}" - )) + Err(clap::Error::raw(clap::ErrorKind::InvalidSubcommand, format!("\"{s}\" is not a valid subcommand, so it was interpreted as a toolchain name, but it is also invalid. {TOOLCHAIN_OVERRIDE_ERROR}"))) } }), ) @@ -294,8 +304,9 @@ pub(crate) fn cli() -> Command<'static> { .hide(true) // synonym for 'toolchain install' .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .required(true) + .value_parser(partial_toolchain_desc_parser) .takes_value(true) .multiple_values(true) ) @@ -329,8 +340,9 @@ pub(crate) fn cli() -> Command<'static> { .hide(true) // synonym for 'toolchain uninstall' .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(RESOLVABLE_TOOLCHAIN_ARG_HELP) .required(true) + .value_parser(resolvable_toolchainame_parser) .takes_value(true) .multiple_values(true), ), @@ -342,8 +354,9 @@ pub(crate) fn cli() -> Command<'static> { .after_help(UPDATE_HELP) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .required(false) + .value_parser(partial_toolchain_desc_parser) .takes_value(true) .multiple_values(true), ) @@ -373,8 +386,9 @@ pub(crate) fn cli() -> Command<'static> { .after_help(DEFAULT_HELP) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(MAYBE_RESOLVABLE_TOOLCHAIN_ARG_HELP) .required(false) + .value_parser(maybe_resolvable_toolchainame_parser) ), ) .subcommand( @@ -395,8 +409,9 @@ pub(crate) fn cli() -> Command<'static> { .aliases(&["update", "add"]) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .required(true) + .value_parser( partial_toolchain_desc_parser) .takes_value(true) .multiple_values(true), ) @@ -461,8 +476,9 @@ pub(crate) fn cli() -> Command<'static> { .alias("remove") .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(RESOLVABLE_TOOLCHAIN_ARG_HELP) .required(true) + .value_parser(resolvable_toolchainame_parser) .takes_value(true) .multiple_values(true), ), @@ -474,7 +490,8 @@ pub(crate) fn cli() -> Command<'static> { .arg( Arg::new("toolchain") .help("Custom toolchain name") - .required(true), + .required(true) + .value_parser(custom_toolchain_name_parser), ) .arg( Arg::new("path") @@ -492,8 +509,9 @@ pub(crate) fn cli() -> Command<'static> { .about("List installed and available targets") .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .long("toolchain") + .value_parser(partial_toolchain_desc_parser) .takes_value(true), ) .arg( @@ -519,9 +537,10 @@ pub(crate) fn cli() -> Command<'static> { ) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .long("toolchain") - .takes_value(true), + .takes_value(true) + .value_parser(partial_toolchain_desc_parser), ), ) .subcommand( @@ -537,9 +556,10 @@ pub(crate) fn cli() -> Command<'static> { ) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .long("toolchain") - .takes_value(true), + .takes_value(true) + .value_parser(partial_toolchain_desc_parser), ), ), ) @@ -552,9 +572,10 @@ pub(crate) fn cli() -> Command<'static> { .about("List installed and available components") .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .long("toolchain") - .takes_value(true), + .takes_value(true) + .value_parser(partial_toolchain_desc_parser), ) .arg( Arg::new("installed") @@ -570,9 +591,10 @@ pub(crate) fn cli() -> Command<'static> { .takes_value(true).multiple_values(true)) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .long("toolchain") - .takes_value(true), + .takes_value(true) + .value_parser( partial_toolchain_desc_parser), ) .arg( Arg::new("target") @@ -587,9 +609,10 @@ pub(crate) fn cli() -> Command<'static> { .takes_value(true).multiple_values(true)) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .long("toolchain") - .takes_value(true), + .takes_value(true) + .value_parser( partial_toolchain_desc_parser), ) .arg( Arg::new("target") @@ -612,9 +635,10 @@ pub(crate) fn cli() -> Command<'static> { .alias("add") .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(RESOLVABLE_TOOLCHAIN_ARG_HELP) .required(true) - .takes_value(true), + .takes_value(true) + .value_parser(resolvable_toolchainame_parser), ) .arg( Arg::new("path") @@ -649,9 +673,10 @@ pub(crate) fn cli() -> Command<'static> { .trailing_var_arg(true) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(RESOLVABLE_LOCAL_TOOLCHAIN_ARG_HELP) .required(true) - .takes_value(true), + .takes_value(true) + .value_parser(resolvable_local_toolchainame_parser), ) .arg( Arg::new("command") @@ -673,9 +698,10 @@ pub(crate) fn cli() -> Command<'static> { .arg(Arg::new("command").required(true)) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(RESOLVABLE_TOOLCHAIN_ARG_HELP) .long("toolchain") - .takes_value(true), + .takes_value(true) + .value_parser(resolvable_toolchainame_parser), ), ) .subcommand( @@ -691,9 +717,10 @@ pub(crate) fn cli() -> Command<'static> { ) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .long("toolchain") - .takes_value(true), + .takes_value(true) + .value_parser(partial_toolchain_desc_parser), ) .arg(Arg::new("topic").help(TOPIC_ARG_HELP)) .group( @@ -719,9 +746,10 @@ pub(crate) fn cli() -> Command<'static> { .arg(Arg::new("command").required(true)) .arg( Arg::new("toolchain") - .help(TOOLCHAIN_ARG_HELP) + .help(OFFICIAL_TOOLCHAIN_ARG_HELP) .long("toolchain") - .takes_value(true), + .takes_value(true) + .value_parser(partial_toolchain_desc_parser), ), ); } @@ -807,123 +835,37 @@ fn maybe_upgrade_data(cfg: &Cfg, m: &ArgMatches) -> Result { } } -fn update_bare_triple_check(cfg: &Cfg, name: &str) -> Result<()> { - if let Some(triple) = PartialTargetTriple::new(name) { - warn!("(partial) target triple specified instead of toolchain name"); - let installed_toolchains = cfg.list_toolchains()?; - let default = cfg.find_default()?; - let default_name = default.map(|t| t.name().to_string()).unwrap_or_default(); - let mut candidates = vec![]; - for t in installed_toolchains { - if t == default_name { - continue; - } - if let Ok(desc) = PartialToolchainDesc::from_str(&t) { - fn triple_comp_eq(given: &str, from_desc: Option<&String>) -> bool { - from_desc.map_or(false, |s| *s == *given) - } - - let triple_matches = triple - .arch - .as_ref() - .map_or(true, |s| triple_comp_eq(s, desc.target.arch.as_ref())) - && triple - .os - .as_ref() - .map_or(true, |s| triple_comp_eq(s, desc.target.os.as_ref())) - && triple - .env - .as_ref() - .map_or(true, |s| triple_comp_eq(s, desc.target.env.as_ref())); - if triple_matches { - candidates.push(t); - } - } - } - match candidates.len() { - 0 => err!("no candidate toolchains found"), - 1 => writeln!( - process().stdout(), - "\nyou may use the following toolchain: {}\n", - candidates[0] - )?, - _ => { - writeln!( - process().stdout(), - "\nyou may use one of the following toolchains:" - )?; - for n in &candidates { - writeln!(process().stdout(), "{n}")?; - } - writeln!(process().stdout(),)?; +fn default_(cfg: &Cfg, m: &ArgMatches) -> Result { + if let Some(toolchain) = m.get_one::("toolchain") { + match toolchain.to_owned() { + MaybeResolvableToolchainName::None => { + cfg.set_default(None)?; } - } - bail!(RustupError::ToolchainNotInstalled(name.to_string())); - } - Ok(()) -} - -fn default_bare_triple_check(cfg: &Cfg, name: &str) -> Result<()> { - if let Some(triple) = PartialTargetTriple::new(name) { - warn!("(partial) target triple specified instead of toolchain name"); - let default = cfg.find_default()?; - let default_name = default.map(|t| t.name().to_string()).unwrap_or_default(); - if let Ok(mut desc) = PartialToolchainDesc::from_str(&default_name) { - desc.target = triple; - let maybe_toolchain = format!("{desc}"); - let toolchain = cfg.get_toolchain(maybe_toolchain.as_ref(), false)?; - if toolchain.name() == default_name { - warn!( - "(partial) triple '{}' resolves to a toolchain that is already default", - name - ); - } else { - writeln!( - process().stdout(), - "\nyou may use the following toolchain: {}\n", - toolchain.name() - )?; + MaybeResolvableToolchainName::Some(ResolvableToolchainName::Custom(toolchain_name)) => { + Toolchain::new(cfg, (&toolchain_name).into())?; + cfg.set_default(Some(&toolchain_name.into()))?; } - return Err(RustupError::ToolchainNotInstalled(name.to_string()).into()); - } - } - Ok(()) -} + MaybeResolvableToolchainName::Some(ResolvableToolchainName::Official(toolchain)) => { + let desc = toolchain.resolve(&cfg.get_default_host_triple()?)?; + let status = DistributableToolchain::install_if_not_installed(cfg, &desc)?; -fn default_(cfg: &Cfg, m: &ArgMatches) -> Result { - if let Some(toolchain) = m.get_one::("toolchain") { - default_bare_triple_check(cfg, toolchain)?; - let toolchain = cfg.get_toolchain(toolchain, false)?; - - let status = if !toolchain.is_custom() { - let distributable = DistributableToolchain::new(&toolchain)?; - Some(distributable.install_from_dist_if_not_installed()?) - } else if !toolchain.exists() && toolchain.name() != "none" { - return Err(RustupError::ToolchainNotInstalled(toolchain.name().to_string()).into()); - } else { - None - }; + cfg.set_default(Some(&(&desc).into()))?; - toolchain.make_default()?; + writeln!(process().stdout())?; - if let Some(status) = status { - writeln!(process().stdout())?; - common::show_channel_update(cfg, toolchain.name(), Ok(status))?; - } + common::show_channel_update(cfg, PackageUpdate::Toolchain(desc), Ok(status))?; + } + }; let cwd = utils::current_dir()?; if let Some((toolchain, reason)) = cfg.find_override(&cwd)? { - info!( - "note that the toolchain '{}' is currently in use ({})", - toolchain.name(), - reason - ); + info!("note that the toolchain '{toolchain}' is currently in use ({reason})"); } } else { - let default_toolchain: Result = cfg + let default_toolchain = cfg .get_default()? - .ok_or_else(|| anyhow!("no default toolchain configured")); - writeln!(process().stdout(), "{} (default)", default_toolchain?)?; + .ok_or_else(|| anyhow!("no default toolchain configured"))?; + writeln!(process().stdout(), "{default_toolchain} (default)")?; } Ok(utils::ExitCode(0)) @@ -934,39 +876,34 @@ fn check_updates(cfg: &Cfg) -> Result { let channels = cfg.list_channels()?; for channel in channels { - match channel { - (ref name, Ok(ref toolchain)) => { - let distributable = DistributableToolchain::new(toolchain)?; - let current_version = distributable.show_version()?; - let dist_version = distributable.show_dist_version()?; - let _ = t.attr(term2::Attr::Bold); - write!(t, "{name} - ")?; - match (current_version, dist_version) { - (None, None) => { - let _ = t.fg(term2::color::RED); - writeln!(t, "Cannot identify installed or update versions")?; - } - (Some(cv), None) => { - let _ = t.fg(term2::color::GREEN); - write!(t, "Up to date")?; - let _ = t.reset(); - writeln!(t, " : {cv}")?; - } - (Some(cv), Some(dv)) => { - let _ = t.fg(term2::color::YELLOW); - write!(t, "Update available")?; - let _ = t.reset(); - writeln!(t, " : {cv} -> {dv}")?; - } - (None, Some(dv)) => { - let _ = t.fg(term2::color::YELLOW); - write!(t, "Update available")?; - let _ = t.reset(); - writeln!(t, " : (Unknown version) -> {dv}")?; - } - } + let (name, distributable) = channel; + let current_version = distributable.show_version()?; + let dist_version = distributable.show_dist_version()?; + let _ = t.attr(term2::Attr::Bold); + write!(t, "{name} - ")?; + match (current_version, dist_version) { + (None, None) => { + let _ = t.fg(term2::color::RED); + writeln!(t, "Cannot identify installed or update versions")?; + } + (Some(cv), None) => { + let _ = t.fg(term2::color::GREEN); + write!(t, "Up to date")?; + let _ = t.reset(); + writeln!(t, " : {cv}")?; + } + (Some(cv), Some(dv)) => { + let _ = t.fg(term2::color::YELLOW); + write!(t, "Update available")?; + let _ = t.reset(); + writeln!(t, " : {cv} -> {dv}")?; + } + (None, Some(dv)) => { + let _ = t.fg(term2::color::YELLOW); + write!(t, "Update available")?; + let _ = t.reset(); + writeln!(t, " : (Unknown version) -> {dv}")?; } - (_, Err(err)) => return Err(err), } } @@ -993,72 +930,68 @@ fn update(cfg: &mut Cfg, m: &ArgMatches) -> Result { if cfg.get_profile()? == Profile::Complete { warn!("{}", common::WARN_COMPLETE_PROFILE); } - if let Ok(Some(names)) = m.try_get_many::("toolchain") { - for name in names { - update_bare_triple_check(cfg, name)?; - - let toolchain_has_triple = match PartialToolchainDesc::from_str(name) { - Ok(x) => x.has_triple(), - _ => false, - }; - - if toolchain_has_triple { + if let Ok(Some(names)) = m.try_get_many::("toolchain") { + for name in names.map(|n| n.to_owned()) { + // This needs another pass to fix it all up + if name.has_triple() { let host_arch = TargetTriple::from_host_or_build(); - if let Ok(partial_toolchain_desc) = PartialToolchainDesc::from_str(name) { - let target_triple = partial_toolchain_desc.resolve(&host_arch)?.target; - if !forced && !host_arch.can_run(&target_triple)? { - err!("DEPRECATED: future versions of rustup will require --force-non-host to install a non-host toolchain as the default."); - warn!( - "toolchain '{}' may not be able to run on this system.", - name - ); - warn!( + + let target_triple = name.clone().resolve(&host_arch)?.target; + if !forced && !host_arch.can_run(&target_triple)? { + err!("DEPRECATED: future versions of rustup will require --force-non-host to install a non-host toolchain."); + warn!("toolchain '{name}' may not be able to run on this system."); + warn!( "If you meant to build software to target that platform, perhaps try `rustup target add {}` instead?", target_triple.to_string() ); - } } } + let desc = name.resolve(&cfg.get_default_host_triple()?)?; - let toolchain = cfg.get_toolchain(name, false)?; - - let status = if !toolchain.is_custom() { - let components: Vec<_> = m - .try_get_many::("components") - .ok() - .flatten() - .map_or_else(Vec::new, |v| v.map(|s| &**s).collect()); - let targets: Vec<_> = m - .try_get_many::("targets") - .ok() - .flatten() - .map_or_else(Vec::new, |v| v.map(|s| &**s).collect()); - let distributable = DistributableToolchain::new(&toolchain)?; - Some(distributable.install_from_dist( - m.get_flag("force"), - matches!(m.try_get_one::("allow-downgrade"), Ok(Some(true))), - &components, - &targets, - None, - )?) - } else if !toolchain.exists() { - bail!(RustupError::InvalidToolchainName( - toolchain.name().to_string() - )); - } else { - None + let components: Vec<_> = m + .try_get_many::("components") + .ok() + .flatten() + .map_or_else(Vec::new, |v| v.map(|s| &**s).collect()); + let targets: Vec<_> = m + .try_get_many::("targets") + .ok() + .flatten() + .map_or_else(Vec::new, |v| v.map(|s| &**s).collect()); + + let force = m.get_flag("force"); + let allow_downgrade = + matches!(m.try_get_one::("allow-downgrade"), Ok(Some(true))); + let profile = cfg.get_profile()?; + let status = match crate::toolchain::distributable::DistributableToolchain::new( + cfg, + desc.clone(), + ) { + Ok(mut d) => { + d.update_extra(&components, &targets, profile, force, allow_downgrade)? + } + Err(RustupError::ToolchainNotInstalled(_)) => { + crate::toolchain::distributable::DistributableToolchain::install( + cfg, + &desc, + &components, + &targets, + profile, + force, + )? + .0 + } + Err(e) => Err(e)?, }; - if let Some(status) = status.clone() { - writeln!(process().stdout())?; - common::show_channel_update(cfg, toolchain.name(), Ok(status))?; - } - - if cfg.get_default()?.is_none() { - use crate::UpdateStatus; - if let Some(UpdateStatus::Installed) = status { - toolchain.make_default()?; - } + writeln!(process().stdout())?; + common::show_channel_update( + cfg, + PackageUpdate::Toolchain(desc.clone()), + Ok(status.clone()), + )?; + if cfg.get_default()?.is_none() && matches!(status, UpdateStatus::Installed) { + cfg.set_default(Some(&desc.into()))?; } } if self_update { @@ -1084,10 +1017,13 @@ fn update(cfg: &mut Cfg, m: &ArgMatches) -> Result { } fn run(cfg: &Cfg, m: &ArgMatches) -> Result { - let toolchain = m.get_one::("toolchain").unwrap(); + let toolchain = m + .get_one::("toolchain") + .unwrap(); let args = m.get_many::("command").unwrap(); let args: Vec<_> = args.collect(); - let cmd = cfg.create_command_for_toolchain(toolchain, m.get_flag("install"), args[0])?; + let toolchain = toolchain.resolve(&cfg.get_default_host_triple()?)?; + let cmd = cfg.create_command_for_toolchain(&toolchain, m.get_flag("install"), args[0])?; let code = command::run_command_for_dir(cmd, args[0], &args[1..])?; Ok(code) @@ -1095,8 +1031,9 @@ fn run(cfg: &Cfg, m: &ArgMatches) -> Result { fn which(cfg: &Cfg, m: &ArgMatches) -> Result { let binary = m.get_one::("command").unwrap(); - let binary_path = if let Some(toolchain) = m.get_one::("toolchain") { - cfg.which_binary_by_toolchain(toolchain, binary)? + let binary_path = if let Some(toolchain) = m.get_one::("toolchain") { + let desc = toolchain.resolve(&cfg.get_default_host_triple()?)?; + Toolchain::new(cfg, desc.into())?.binary_file(binary) } else { cfg.which_binary(&utils::current_dir()?, binary)? }; @@ -1137,8 +1074,15 @@ fn show(cfg: &Cfg, m: &ArgMatches) -> Result { // active_toolchain will carry the reason we don't have one in its detail. let active_targets = if let Ok(ref at) = active_toolchain { - if let Ok(distributable) = DistributableToolchain::new(&at.0) { - match distributable.list_components() { + if let Ok(distributable) = DistributableToolchain::try_from(&at.0) { + let components = (|| { + let manifestation = distributable.get_manifestation()?; + let config = manifestation.read_config()?.unwrap_or_default(); + let manifest = distributable.get_manifest()?; + manifest.query_components(distributable.desc(), &config) + })(); + + match components { Ok(cs_vec) => cs_vec .into_iter() .filter(|c| c.component.short_name_in_manifest() == "rust-std") @@ -1174,10 +1118,9 @@ fn show(cfg: &Cfg, m: &ArgMatches) -> Result { if show_headers { print_header::(&mut t, "installed toolchains")?; } - let default_name: Result = cfg + let default_name = cfg .get_default()? - .ok_or_else(|| anyhow!("no default toolchain configured")); - let default_name = default_name?; + .ok_or_else(|| anyhow!("no default toolchain configured"))?; for it in installed_toolchains { if default_name == it { writeln!(t, "{it} (default)")?; @@ -1185,9 +1128,8 @@ fn show(cfg: &Cfg, m: &ArgMatches) -> Result { writeln!(t, "{it}")?; } if verbose { - if let Ok(toolchain) = cfg.get_toolchain(&it, false) { - writeln!(process().stdout(), "{}", toolchain.rustc_version())?; - } + let toolchain = Toolchain::new(cfg, it.into())?; + writeln!(process().stdout(), "{}", toolchain.rustc_version())?; // To make it easy to see what rustc that belongs to what // toolchain we separate each pair with an extra newline writeln!(process().stdout())?; @@ -1304,23 +1246,35 @@ fn show_rustup_home(cfg: &Cfg) -> Result { } fn target_list(cfg: &Cfg, m: &ArgMatches) -> Result { - let toolchain = explicit_or_dir_toolchain(cfg, m)?; + let toolchain = m + .get_one::("toolchain") + .map(Into::into); + let toolchain = explicit_or_dir_toolchain2(cfg, toolchain)?; + // downcasting required because the toolchain files can name any toolchain + let distributable = (&toolchain).try_into()?; if m.get_flag("installed") { - common::list_installed_targets(&toolchain) + common::list_installed_targets(distributable) } else { - common::list_targets(&toolchain) + common::list_targets(distributable) } } fn target_add(cfg: &Cfg, m: &ArgMatches) -> Result { - let toolchain = explicit_or_dir_toolchain(cfg, m)?; + let toolchain_name = m + .get_one::("toolchain") + .map(Into::into); + let toolchain = explicit_or_dir_toolchain2(cfg, toolchain_name)?; // XXX: long term move this error to cli ? the normal .into doesn't work // because Result here is the wrong sort and expression type ascription // isn't a feature yet. // list_components *and* add_component would both be inappropriate for // custom toolchains. - let distributable = DistributableToolchain::new_for_components(&toolchain)?; + let distributable = DistributableToolchain::try_from(&toolchain)?; + let manifestation = distributable.get_manifestation()?; + let config = manifestation.read_config()?.unwrap_or_default(); + let manifest = distributable.get_manifest()?; + let components = manifest.query_components(distributable.desc(), &config)?; let mut targets: Vec<_> = m .get_many::("target") @@ -1337,7 +1291,7 @@ fn target_add(cfg: &Cfg, m: &ArgMatches) -> Result { } targets.clear(); - for component in distributable.list_components()? { + for component in components { if component.component.short_name_in_manifest() == "rust-std" && component.available && !component.installed @@ -1365,7 +1319,11 @@ fn target_add(cfg: &Cfg, m: &ArgMatches) -> Result { } fn target_remove(cfg: &Cfg, m: &ArgMatches) -> Result { - let toolchain = explicit_or_dir_toolchain(cfg, m)?; + let toolchain = m + .get_one::("toolchain") + .map(Into::into); + let toolchain = explicit_or_dir_toolchain2(cfg, toolchain)?; + let distributable = DistributableToolchain::try_from(&toolchain)?; for target in m.get_many::("target").unwrap() { let new_component = Component::new( @@ -1373,7 +1331,6 @@ fn target_remove(cfg: &Cfg, m: &ArgMatches) -> Result { Some(TargetTriple::new(target)), false, ); - let distributable = DistributableToolchain::new_for_components(&toolchain)?; distributable.remove_component(new_component)?; } @@ -1382,29 +1339,24 @@ fn target_remove(cfg: &Cfg, m: &ArgMatches) -> Result { fn component_list(cfg: &Cfg, m: &ArgMatches) -> Result { let toolchain = explicit_or_dir_toolchain(cfg, m)?; + // downcasting required because the toolchain files can name any toolchain + let distributable = (&toolchain).try_into()?; if m.get_flag("installed") { - common::list_installed_components(&toolchain) + common::list_installed_components(distributable)?; } else { - common::list_components(&toolchain)?; - Ok(utils::ExitCode(0)) + common::list_components(distributable)?; } + Ok(utils::ExitCode(0)) } fn component_add(cfg: &Cfg, m: &ArgMatches) -> Result { - let toolchain = explicit_or_dir_toolchain(cfg, m)?; - let distributable = DistributableToolchain::new(&toolchain)?; - let target = m - .get_one::("target") - .map(|s| &**s) - .map(TargetTriple::new) - .or_else(|| { - distributable - .desc() - .as_ref() - .ok() - .map(|desc| desc.target.clone()) - }); + let toolchain = m + .get_one::("toolchain") + .map(Into::into); + let toolchain = explicit_or_dir_toolchain2(cfg, toolchain)?; + let distributable = DistributableToolchain::try_from(&toolchain)?; + let target = get_target(m, &distributable); for component in m.get_many::("component").unwrap() { let new_component = Component::new_with_target(component, false) @@ -1415,20 +1367,17 @@ fn component_add(cfg: &Cfg, m: &ArgMatches) -> Result { Ok(utils::ExitCode(0)) } -fn component_remove(cfg: &Cfg, m: &ArgMatches) -> Result { - let toolchain = explicit_or_dir_toolchain(cfg, m)?; - let distributable = DistributableToolchain::new_for_components(&toolchain)?; - let target = m - .get_one::("target") +fn get_target(m: &ArgMatches, distributable: &DistributableToolchain<'_>) -> Option { + m.get_one::("target") .map(|s| &**s) .map(TargetTriple::new) - .or_else(|| { - distributable - .desc() - .as_ref() - .ok() - .map(|desc| desc.target.clone()) - }); + .or_else(|| Some(distributable.desc().target.clone())) +} + +fn component_remove(cfg: &Cfg, m: &ArgMatches) -> Result { + let toolchain = explicit_or_dir_toolchain(cfg, m)?; + let distributable = DistributableToolchain::try_from(&toolchain)?; + let target = get_target(m, &distributable); for component in m.get_many::("component").unwrap() { let new_component = Component::new_with_target(component, false) @@ -1440,16 +1389,26 @@ fn component_remove(cfg: &Cfg, m: &ArgMatches) -> Result { } fn explicit_or_dir_toolchain<'a>(cfg: &'a Cfg, m: &ArgMatches) -> Result> { - let toolchain = m.get_one::("toolchain"); - if let Some(toolchain) = toolchain { - let toolchain = cfg.get_toolchain(toolchain, false)?; - return Ok(toolchain); - } + let toolchain = m.get_one::("toolchain"); + explicit_or_dir_toolchain2(cfg, toolchain.cloned()) +} - let cwd = utils::current_dir()?; - let (toolchain, _) = cfg.toolchain_for_dir(&cwd)?; +fn explicit_or_dir_toolchain2( + cfg: &Cfg, + toolchain: Option, +) -> Result> { + match toolchain { + Some(toolchain) => { + let desc = toolchain.resolve(&cfg.get_default_host_triple()?)?; + Ok(Toolchain::new(cfg, desc.into())?) + } + None => { + let cwd = utils::current_dir()?; + let (toolchain, _) = cfg.find_or_install_override_toolchain_or_default(&cwd)?; - Ok(toolchain) + Ok(toolchain) + } + } } fn toolchain_list(cfg: &Cfg, m: &ArgMatches) -> Result { @@ -1457,54 +1416,62 @@ fn toolchain_list(cfg: &Cfg, m: &ArgMatches) -> Result { } fn toolchain_link(cfg: &Cfg, m: &ArgMatches) -> Result { - let toolchain = m.get_one::("toolchain").unwrap(); + let toolchain = m.get_one::("toolchain").unwrap(); let path = m.get_one::("path").unwrap(); - let toolchain = cfg.get_toolchain(toolchain, true)?; - - if let Ok(custom) = CustomToolchain::new(&toolchain) { - custom.install_from_dir(Path::new(path), true)?; - Ok(utils::ExitCode(0)) - } else { - Err(anyhow!( - "invalid custom toolchain name: '{}'", - toolchain.name().to_string() - )) - } + cfg.ensure_toolchains_dir()?; + crate::toolchain::custom::CustomToolchain::install_from_dir( + cfg, + Path::new(path), + toolchain, + true, + )?; + Ok(utils::ExitCode(0)) } fn toolchain_remove(cfg: &mut Cfg, m: &ArgMatches) -> Result { - for toolchain in m.get_many::("toolchain").unwrap() { - let toolchain = cfg.get_toolchain(toolchain, false)?; - toolchain.remove()?; + for toolchain_name in m.get_many::("toolchain").unwrap() { + let toolchain_name = toolchain_name.resolve(&cfg.get_default_host_triple()?)?; + Toolchain::ensure_removed(cfg, (&toolchain_name).into())?; } Ok(utils::ExitCode(0)) } fn override_add(cfg: &Cfg, m: &ArgMatches) -> Result { - let toolchain = m.get_one::("toolchain").unwrap(); - let toolchain = cfg.get_toolchain(toolchain, false)?; - - let status = if !toolchain.is_custom() { - let distributable = DistributableToolchain::new(&toolchain)?; - Some(distributable.install_from_dist_if_not_installed()?) - } else if !toolchain.exists() { - return Err(RustupError::ToolchainNotInstalled(toolchain.name().to_string()).into()); - } else { - None - }; + let toolchain_name = m.get_one::("toolchain").unwrap(); + let toolchain_name = toolchain_name.resolve(&cfg.get_default_host_triple()?)?; let path = if let Some(path) = m.get_one::("path") { PathBuf::from(path) } else { utils::current_dir()? }; - toolchain.make_override(&path)?; - if let Some(status) = status { - writeln!(process().stdout(),)?; - common::show_channel_update(cfg, toolchain.name(), Ok(status))?; + match Toolchain::new(cfg, (&toolchain_name).into()) { + Ok(_) => {} + Err(e @ RustupError::ToolchainNotInstalled(_)) => match &toolchain_name { + ToolchainName::Custom(_) => Err(e)?, + ToolchainName::Official(desc) => { + let status = DistributableToolchain::install( + cfg, + desc, + &[], + &[], + cfg.get_profile()?, + false, + )? + .0; + writeln!(process().stdout())?; + common::show_channel_update( + cfg, + PackageUpdate::Toolchain(desc.clone()), + Ok(status), + )?; + } + }, + Err(e) => Err(e)?, } + cfg.make_override(&path, &toolchain_name)?; Ok(utils::ExitCode(0)) } @@ -1572,9 +1539,16 @@ const DOCS_DATA: &[(&str, &str, &str)] = &[ ]; fn doc(cfg: &Cfg, m: &ArgMatches) -> Result { - let toolchain = explicit_or_dir_toolchain(cfg, m)?; - if let Ok(distributable) = DistributableToolchain::new(&toolchain) { - let components = distributable.list_components()?; + let toolchain = m + .get_one::("toolchain") + .map(Into::into); + let toolchain = explicit_or_dir_toolchain2(cfg, toolchain)?; + + if let Ok(distributable) = DistributableToolchain::try_from(&toolchain) { + let manifestation = distributable.get_manifestation()?; + let config = manifestation.read_config()?.unwrap_or_default(); + let manifest = distributable.get_manifest()?; + let components = manifest.query_components(distributable.desc(), &config)?; if let [_] = components .into_iter() .filter(|cstatus| { @@ -1586,17 +1560,18 @@ fn doc(cfg: &Cfg, m: &ArgMatches) -> Result { { info!( "`rust-docs` not installed in toolchain `{}`", - toolchain.name() + distributable.desc() ); info!( "To install, try `rustup component add --toolchain {} rust-docs`", - toolchain.name() + distributable.desc() ); return Err(anyhow!( "unable to view documentation which is not installed" )); } - } + }; + let topical_path: PathBuf; let doc_url = if let Some(topic) = m.get_one::("topic") { @@ -1622,12 +1597,12 @@ fn man(cfg: &Cfg, m: &ArgMatches) -> Result { let command = m.get_one::("command").unwrap(); let toolchain = explicit_or_dir_toolchain(cfg, m)?; - let mut toolchain = toolchain.path().to_path_buf(); - toolchain.push("share"); - toolchain.push("man"); - utils::assert_is_directory(&toolchain)?; + let mut path = toolchain.path().to_path_buf(); + path.push("share"); + path.push("man"); + utils::assert_is_directory(&path)?; - let mut manpaths = std::ffi::OsString::from(toolchain); + let mut manpaths = std::ffi::OsString::from(path); manpaths.push(":"); // prepend to the default MANPATH list if let Some(path) = process().var_os("MANPATH") { manpaths.push(path); diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 3a97ae4c01f..8fe4b2f50c9 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -32,6 +32,7 @@ #[cfg(unix)] mod shell; +#[cfg(feature = "test")] pub(crate) mod test; #[cfg(unix)] mod unix; @@ -57,26 +58,33 @@ use anyhow::{anyhow, Context, Result}; use cfg_if::cfg_if; use same_file::Handle; -use super::common::{self, ignorable_error, report_error, Confirm}; -use super::errors::*; -use super::markdown::md; -use super::term2; -use crate::cli::term2::Terminal; -use crate::dist::dist::{self, Profile, TargetTriple}; -use crate::process; -use crate::toolchain::{DistributableToolchain, Toolchain}; -use crate::utils::utils; -use crate::utils::Notification; -use crate::{Cfg, UpdateStatus}; -use crate::{DUP_TOOLS, TOOLS}; +use crate::{ + cli::{ + common::{self, ignorable_error, report_error, Confirm, PackageUpdate}, + errors::*, + markdown::md, + term2::{self, Terminal}, + }, + dist::dist::{self, PartialToolchainDesc, Profile, TargetTriple, ToolchainDesc}, + install::UpdateStatus, + process, + toolchain::{ + distributable::DistributableToolchain, + names::{MaybeOfficialToolchainName, ResolvableToolchainName, ToolchainName}, + toolchain::Toolchain, + }, + utils::{utils, Notification}, + Cfg, DUP_TOOLS, TOOLS, +}; + use os::*; pub(crate) use os::{delete_rustup_and_cargo_home, run_update, self_replace}; #[cfg(windows)] pub use windows::complete_windows_uninstall; -pub struct InstallOpts<'a> { +pub(crate) struct InstallOpts<'a> { pub default_host_triple: Option, - pub default_toolchain: Option, + pub default_toolchain: Option, pub profile: String, pub no_modify_path: bool, pub no_update_toolchain: bool, @@ -439,7 +447,7 @@ pub(crate) fn install( } utils::create_rustup_home()?; maybe_install_rust( - opts.default_toolchain.as_deref(), + opts.default_toolchain, &opts.profile, opts.default_host_triple.as_deref(), !opts.no_update_toolchain, @@ -587,17 +595,14 @@ fn do_pre_install_options_sanity_checks(opts: &InstallOpts<'_>) -> Result<()> { .as_ref() .map(|s| dist::TargetTriple::new(s)) .unwrap_or_else(TargetTriple::from_host_or_build); - let toolchain_to_use = match &opts.default_toolchain { - None => "stable", - Some(s) if s == "none" => "stable", - Some(s) => s, + let partial_channel = match &opts.default_toolchain { + None | Some(MaybeOfficialToolchainName::None) => { + ResolvableToolchainName::try_from("stable")? + } + Some(MaybeOfficialToolchainName::Some(s)) => s.into(), }; - let partial_channel = dist::PartialToolchainDesc::from_str(toolchain_to_use)?; - let resolved = partial_channel.resolve(&host_triple)?.to_string(); - debug!( - "Successfully resolved installation toolchain as: {}", - resolved - ); + let resolved = partial_channel.resolve(&host_triple)?; + debug!("Successfully resolved installation toolchain as: {resolved}"); Ok(()) })() .map_err(|e: Box| { @@ -606,7 +611,7 @@ fn do_pre_install_options_sanity_checks(opts: &InstallOpts<'_>) -> Result<()> { If you are unsure of suitable values, the 'stable' toolchain is the default.\n\ Valid host triples look something like: {}", e, - dist::TargetTriple::from_host_or_build() + TargetTriple::from_host_or_build() ) })?; Ok(()) @@ -667,8 +672,9 @@ fn current_install_opts(opts: &InstallOpts<'_>) -> String { .map(|s| TargetTriple::new(s)) .unwrap_or_else(TargetTriple::from_host_or_build), opts.default_toolchain - .as_deref() - .unwrap_or("stable (default)"), + .as_ref() + .map(ToString::to_string) + .unwrap_or("stable (default)".into()), opts.profile, if !opts.no_modify_path { "yes" } else { "no" } ) @@ -691,10 +697,14 @@ fn customize_install(mut opts: InstallOpts<'_>) -> Result> { .unwrap_or_else(|| TargetTriple::from_host_or_build().to_string()), )?); - opts.default_toolchain = Some(common::question_str( + opts.default_toolchain = Some(MaybeOfficialToolchainName::try_from(common::question_str( "Default toolchain? (stable/beta/nightly/none)", - opts.default_toolchain.as_deref().unwrap_or("stable"), - )?); + &opts + .default_toolchain + .as_ref() + .map(ToString::to_string) + .unwrap_or("stable".into()), + )?)?); opts.profile = common::question_str( &format!( @@ -805,7 +815,7 @@ pub(crate) fn install_proxies() -> Result<()> { } fn maybe_install_rust( - toolchain: Option<&str>, + toolchain: Option, profile_str: &str, default_host_triple: Option<&str>, update_existing_toolchain: bool, @@ -825,29 +835,44 @@ fn maybe_install_rust( components, targets, )?; - if let Some(toolchain) = toolchain { - if toolchain.exists() { + if let Some(ref desc) = toolchain { + let status = if Toolchain::exists(&cfg, &desc.into())? { warn!("Updating existing toolchain, profile choice will be ignored"); - } - let distributable = DistributableToolchain::new(&toolchain)?; - let status = distributable.install_from_dist(true, false, components, targets, None)?; - let toolchain_str = toolchain.name().to_owned(); - toolchain.cfg().set_default(&toolchain_str)?; + // If we have a partial install we might not be able to read content here. We could: + // - fail and folk have to delete the partially present toolchain to recover + // - silently ignore it (and provide inconsistent metadata for reporting the install/update change) + // - delete the partial install and start over + // For now, we error. + let mut toolchain = DistributableToolchain::new(&cfg, desc.clone())?; + toolchain.update(components, targets, cfg.get_profile()?)? + } else { + DistributableToolchain::install( + &cfg, + desc, + components, + targets, + cfg.get_profile()?, + true, + )? + .0 + }; + + cfg.set_default(Some(&desc.into()))?; writeln!(process().stdout())?; - common::show_channel_update(toolchain.cfg(), &toolchain_str, Ok(status))?; + common::show_channel_update(&cfg, PackageUpdate::Toolchain(desc.clone()), Ok(status))?; } Ok(()) } -fn _install_selection<'a>( - cfg: &'a mut Cfg, - toolchain_opt: Option<&str>, +fn _install_selection( + cfg: &mut Cfg, + toolchain_opt: Option, profile_str: &str, default_host_triple: Option<&str>, update_existing_toolchain: bool, components: &[&str], targets: &[&str], -) -> Result>> { +) -> Result> { cfg.set_profile(profile_str)?; if let Some(default_host_triple) = default_host_triple { @@ -868,39 +893,54 @@ fn _install_selection<'a>( // a toolchain (updating if it's already present) and then if neither of // those are true, we have a user who doesn't mind, and already has an // install, so we leave their setup alone. - Ok(if toolchain_opt == Some("none") { - info!("skipping toolchain installation"); - if !components.is_empty() { - warn!( - "ignoring requested component{}: {}", - if components.len() == 1 { "" } else { "s" }, - components.join(", ") - ); - } - if !targets.is_empty() { - warn!( - "ignoring requested target{}: {}", - if targets.len() == 1 { "" } else { "s" }, - targets.join(", ") - ); - } - writeln!(process().stdout())?; - None - } else if user_specified_something - || (update_existing_toolchain && cfg.find_default()?.is_none()) - { - Some(match toolchain_opt { - Some(s) => cfg.get_toolchain(s, false)?, - None => match cfg.find_default()? { - Some(t) => t, - None => cfg.get_toolchain("stable", false)?, - }, - }) - } else { - info!("updating existing rustup installation - leaving toolchains alone"); - writeln!(process().stdout())?; - None - }) + Ok( + if matches!(toolchain_opt, Some(MaybeOfficialToolchainName::None)) { + info!("skipping toolchain installation"); + if !components.is_empty() { + warn!( + "ignoring requested component{}: {}", + if components.len() == 1 { "" } else { "s" }, + components.join(", ") + ); + } + if !targets.is_empty() { + warn!( + "ignoring requested target{}: {}", + if targets.len() == 1 { "" } else { "s" }, + targets.join(", ") + ); + } + writeln!(process().stdout())?; + None + } else if user_specified_something + || (update_existing_toolchain && cfg.find_default()?.is_none()) + { + match toolchain_opt { + Some(s) => { + let toolchain_name = match s { + MaybeOfficialToolchainName::None => unreachable!(), + MaybeOfficialToolchainName::Some(n) => n, + }; + Some(toolchain_name.resolve(&cfg.get_default_host_triple()?)?) + } + None => match cfg.get_default()? { + // Default is installable + Some(ToolchainName::Official(t)) => Some(t), + // Default is custom, presumably from a prior install. Do nothing. + Some(ToolchainName::Custom(_)) => None, + None => Some( + "stable" + .parse::()? + .resolve(&cfg.get_default_host_triple()?)?, + ), + }, + } + } else { + info!("updating existing rustup installation - leaving toolchains alone"); + writeln!(process().stdout())?; + None + }, + ) } pub(crate) fn uninstall(no_prompt: bool) -> Result { @@ -1047,11 +1087,19 @@ pub(crate) fn update(cfg: &Cfg) -> Result { } }; - let _ = common::show_channel_update(cfg, "rustup", Ok(UpdateStatus::Updated(version))); + let _ = common::show_channel_update( + cfg, + PackageUpdate::Rustup, + Ok(UpdateStatus::Updated(version)), + ); return run_update(&setup_path); } None => { - let _ = common::show_channel_update(cfg, "rustup", Ok(UpdateStatus::Unchanged)); + let _ = common::show_channel_update( + cfg, + PackageUpdate::Rustup, + Ok(UpdateStatus::Unchanged), + ); // Try again in case we emitted "tool `{}` is already installed" last time. install_proxies()? } @@ -1239,7 +1287,7 @@ mod tests { use rustup_macros::unit_test as test; use crate::cli::common; - use crate::dist::dist::ToolchainDesc; + use crate::dist::dist::PartialToolchainDesc; use crate::test::{test_dir, with_rustup_home, Env}; use crate::{currentprocess, for_host}; @@ -1257,7 +1305,11 @@ mod tests { // callbacks rather than output to the tp sink. let mut cfg = common::set_globals(false, false).unwrap(); assert_eq!( - "stable", + "stable" + .parse::() + .unwrap() + .resolve(&cfg.get_default_host_triple().unwrap()) + .unwrap(), super::_install_selection( &mut cfg, None, // No toolchain specified @@ -1269,10 +1321,6 @@ mod tests { ) .unwrap() // result .unwrap() // option - .name() - .parse::() - .unwrap() - .channel ); Ok(()) })?; diff --git a/src/cli/setup_mode.rs b/src/cli/setup_mode.rs index 4f8aec8cb60..21f93a087eb 100644 --- a/src/cli/setup_mode.rs +++ b/src/cli/setup_mode.rs @@ -1,11 +1,16 @@ use anyhow::Result; use clap::{builder::PossibleValuesParser, AppSettings, Arg, ArgAction, Command}; -use super::common; -use super::self_update::{self, InstallOpts}; -use crate::dist::dist::Profile; -use crate::process; -use crate::utils::utils; +use crate::{ + cli::{ + common, + self_update::{self, InstallOpts}, + }, + dist::dist::Profile, + process, + toolchain::names::{maybe_official_toolchainame_parser, MaybeOfficialToolchainName}, + utils::utils, +}; #[cfg_attr(feature = "otel", tracing::instrument)] pub fn main() -> Result { @@ -59,7 +64,8 @@ pub fn main() -> Result { Arg::new("default-toolchain") .long("default-toolchain") .takes_value(true) - .help("Choose a default toolchain to install. Use 'none' to not install any toolchains at all"), + .help("Choose a default toolchain to install. Use 'none' to not install any toolchains at all") + .value_parser(maybe_official_toolchainame_parser) ) .arg( Arg::new("profile") @@ -118,7 +124,7 @@ pub fn main() -> Result { .get_one::("default-host") .map(ToOwned::to_owned); let default_toolchain = matches - .get_one::("default-toolchain") + .get_one::("default-toolchain") .map(ToOwned::to_owned); let profile = matches .get_one::("profile") diff --git a/src/command.rs b/src/command.rs index 07b9e3c4244..b6d48250a98 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,13 +1,17 @@ -use std::ffi::OsStr; -use std::io; -use std::process::{self, Command}; +use std::{ + ffi::OsStr, + fmt::Debug, + io, + process::{self, Command}, +}; use anyhow::{Context, Result}; use crate::errors::*; use crate::utils::utils::ExitCode; -pub(crate) fn run_command_for_dir>( +#[cfg_attr(feature = "otel", tracing::instrument(err))] +pub(crate) fn run_command_for_dir + Debug>( mut cmd: Command, arg0: &str, args: &[S], diff --git a/src/config.rs b/src/config.rs index acb77d4b5a6..3e1fbcd8a1e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,22 +7,33 @@ use std::str::FromStr; use std::sync::Arc; use anyhow::{anyhow, bail, Context, Result}; +use derivative::Derivative; use serde::Deserialize; use thiserror::Error as ThisError; -use crate::cli::self_update::SelfUpdateMode; -use crate::dist::download::DownloadCfg; -use crate::dist::{ - dist::{self, Profile}, - temp, +use crate::{ + cli::self_update::SelfUpdateMode, + dist::{ + dist::{self, PartialToolchainDesc, Profile, ToolchainDesc}, + download::DownloadCfg, + temp, + }, + errors::RustupError, + fallback_settings::FallbackSettings, + install::UpdateStatus, + notifications::*, + process, + settings::{Settings, SettingsFile, DEFAULT_METADATA_VERSION}, + toolchain::{ + distributable::DistributableToolchain, + names::{ + LocalToolchainName, PathBasedToolchainName, ResolvableLocalToolchainName, + ResolvableToolchainName, ToolchainName, + }, + toolchain::Toolchain, + }, + utils::utils, }; -use crate::errors::RustupError; -use crate::fallback_settings::FallbackSettings; -use crate::notifications::*; -use crate::process; -use crate::settings::{Settings, SettingsFile, DEFAULT_METADATA_VERSION}; -use crate::toolchain::{DistributableToolchain, Toolchain, UpdateStatus}; -use crate::utils::utils; #[derive(Debug, ThisError)] enum OverrideFileConfigError { @@ -104,22 +115,22 @@ impl Display for OverrideReason { } #[derive(Default, Debug)] -struct OverrideCfg<'a> { - toolchain: Option>, +struct OverrideCfg { + toolchain: Option, components: Vec, targets: Vec, profile: Option, } -impl<'a> OverrideCfg<'a> { - fn from_file( - cfg: &'a Cfg, - cfg_path: Option>, - file: OverrideFile, - ) -> Result { +impl OverrideCfg { + fn from_file(cfg: &Cfg, file: OverrideFile) -> Result { Ok(Self { toolchain: match (file.toolchain.channel, file.toolchain.path) { - (Some(name), None) => Some(Toolchain::from(cfg, &name)?), + (Some(name), None) => Some( + (&ResolvableToolchainName::try_from(name)? + .resolve(&cfg.get_default_host_triple()?)?) + .into(), + ), (None, Some(path)) => { if file.toolchain.targets.is_some() || file.toolchain.components.is_some() @@ -130,7 +141,12 @@ impl<'a> OverrideCfg<'a> { path.display() ) } - Some(Toolchain::from_path(cfg, cfg_path, &path)?) + // We -do not- support relative paths, they permit trivial + // completely arbitrary code execution in a directory. + // Longer term we'll not support path based toolchains at + // all, because they also permit arbitrary code execution, + // though with more challenges to exploit. + Some((&PathBasedToolchainName::try_from(&path as &Path)?).into()) } (Some(channel), Some(path)) => { bail!( @@ -155,7 +171,9 @@ impl<'a> OverrideCfg<'a> { pub(crate) const UNIX_FALLBACK_SETTINGS: &str = "/etc/rustup/settings.toml"; -pub struct Cfg { +#[derive(Derivative)] +#[derivative(Debug)] +pub(crate) struct Cfg { profile_override: Option, pub rustup_dir: PathBuf, pub settings_file: SettingsFile, @@ -164,9 +182,10 @@ pub struct Cfg { pub update_hash_dir: PathBuf, pub download_dir: PathBuf, pub temp_cfg: temp::Cfg, - pub toolchain_override: Option, - pub env_override: Option, + pub toolchain_override: Option, + pub env_override: Option, pub dist_root_url: String, + #[derivative(Debug = "ignore")] pub notify_handler: Arc)>, } @@ -197,11 +216,17 @@ impl Cfg { let update_hash_dir = rustup_dir.join("update-hashes"); let download_dir = rustup_dir.join("downloads"); + // Figure out get_default_host_triple before Config is populated + let default_host_triple = settings_file.with(|s| Ok(get_default_host_triple(s)))?; // Environment override let env_override = process() .var("RUSTUP_TOOLCHAIN") .ok() - .and_then(utils::if_not_empty); + .and_then(utils::if_not_empty) + .map(ResolvableLocalToolchainName::try_from) + .transpose()? + .map(|t| t.resolve(&default_host_triple)) + .transpose()?; let dist_root_server = match process().var("RUSTUP_DIST_SERVER") { Ok(ref s) if !s.is_empty() => s.clone(), @@ -244,8 +269,10 @@ impl Cfg { // Run some basic checks against the constructed configuration // For now, that means simply checking that 'stable' can resolve // for the current configuration. - cfg.resolve_toolchain("stable") - .context("Unable parse configuration")?; + ResolvableToolchainName::try_from("stable")?.resolve( + &cfg.get_default_host_triple() + .context("Unable parse configuration")?, + )?; Ok(cfg) } @@ -267,13 +294,9 @@ impl Cfg { self.profile_override = Some(profile); } - pub(crate) fn set_default(&self, toolchain: &str) -> Result<()> { + pub(crate) fn set_default(&self, toolchain: Option<&ToolchainName>) -> Result<()> { self.settings_file.with_mut(|s| { - s.default_toolchain = if toolchain == "none" { - None - } else { - Some(toolchain.to_owned()) - }; + s.default_toolchain = toolchain.map(|t| t.to_string()); Ok(()) })?; (self.notify_handler)(Notification::SetDefaultToolchain(toolchain)); @@ -309,7 +332,7 @@ impl Cfg { } } - pub(crate) fn set_toolchain_override(&mut self, toolchain_override: &str) { + pub(crate) fn set_toolchain_override(&mut self, toolchain_override: &ResolvableToolchainName) { self.toolchain_override = Some(toolchain_override.to_owned()); } @@ -343,21 +366,18 @@ impl Cfg { }) } - pub(crate) fn get_toolchain(&self, name: &str, create_parent: bool) -> Result> { - if create_parent { - utils::ensure_dir_exists("toolchains", &self.toolchains_dir, &|n| { - (self.notify_handler)(n) - })?; - } - - if name.is_empty() { - anyhow::bail!("toolchain names must not be empty"); - } - - Toolchain::from(self, name) + pub(crate) fn ensure_toolchains_dir(&self) -> Result<(), anyhow::Error> { + utils::ensure_dir_exists("toolchains", &self.toolchains_dir, &|n| { + (self.notify_handler)(n) + })?; + Ok(()) } - pub(crate) fn get_hash_file(&self, toolchain: &str, create_parent: bool) -> Result { + pub(crate) fn get_hash_file( + &self, + toolchain: &ToolchainDesc, + create_parent: bool, + ) -> Result { if create_parent { utils::ensure_dir_exists( "update-hash", @@ -366,20 +386,7 @@ impl Cfg { )?; } - Ok(self.update_hash_dir.join(toolchain)) - } - - pub(crate) fn which_binary_by_toolchain( - &self, - toolchain_name: &str, - binary: &str, - ) -> Result { - let toolchain = self.get_toolchain(toolchain_name, false)?; - if toolchain.exists() { - Ok(toolchain.binary_file(binary)) - } else { - Err(RustupError::ToolchainNotInstalled(toolchain_name.to_string()).into()) - } + Ok(self.update_hash_dir.join(toolchain.to_string())) } pub(crate) fn which_binary(&self, path: &Path, binary: &str) -> Result { @@ -429,41 +436,36 @@ impl Cfg { } pub(crate) fn find_default(&self) -> Result>> { - let opt_name = self.get_default()?; - - if let Some(name) = opt_name { - let toolchain = Toolchain::from(self, &name)?; - Ok(Some(toolchain)) - } else { - Ok(None) - } + Ok(self + .get_default()? + .map(|n| Toolchain::new(self, (&n).into())) + .transpose()?) } pub(crate) fn find_override( &self, path: &Path, - ) -> Result, OverrideReason)>> { - self.find_override_config(path).map(|opt| { - opt.and_then(|(override_cfg, reason)| { - override_cfg.toolchain.map(|toolchain| (toolchain, reason)) - }) - }) + ) -> Result> { + Ok(self + .find_override_config(path)? + .and_then(|(override_cfg, reason)| override_cfg.toolchain.map(|t| (t, reason)))) } - fn find_override_config( - &self, - path: &Path, - ) -> Result, OverrideReason)>> { + fn find_override_config(&self, path: &Path) -> Result> { let mut override_ = None; // First check toolchain override from command if let Some(ref name) = self.toolchain_override { - override_ = Some((name.into(), OverrideReason::CommandLine)); + override_ = Some((name.to_string().into(), OverrideReason::CommandLine)); } // Check RUSTUP_TOOLCHAIN if let Some(ref name) = self.env_override { - override_ = Some((name.into(), OverrideReason::Environment)); + // Because path based toolchain files exist, this has to support + // custom, distributable, and absolute path toolchains otherwise + // rustup's export of a RUSTUP_TOOLCHAIN when running a process will + // error when a nested rustup invocation occurs + override_ = Some((name.to_string().into(), OverrideReason::Environment)); } // Then walk up the directory tree from 'path' looking for either the @@ -500,25 +502,25 @@ impl Cfg { ), }; - let cfg_file = if let OverrideReason::ToolchainFile(ref path) = reason { - Some(path) - } else { - None - }; - - let override_cfg = OverrideCfg::from_file(self, cfg_file, file)?; - if let Some(toolchain) = &override_cfg.toolchain { - // Overridden toolchains can be literally any string, but only - // distributable toolchains will be auto-installed by the wrapping - // code; provide a nice error for this common case. (default could - // be set badly too, but that is much less common). - if !toolchain.exists() && toolchain.is_custom() { - // Strip the confusing NotADirectory error and only mention that the - // override toolchain is not installed. - return Err(anyhow!(reason_err)).with_context(|| { - format!("override toolchain '{}' is not installed", toolchain.name()) - }); + let override_cfg = OverrideCfg::from_file(self, file)?; + // Overridden toolchains can be literally any string, but only + // distributable toolchains will be auto-installed by the wrapping + // code; provide a nice error for this common case. (default could + // be set badly too, but that is much less common). + match &override_cfg.toolchain { + Some(t @ LocalToolchainName::Named(ToolchainName::Custom(_))) + | Some(t @ LocalToolchainName::Path(_)) => { + if let Err(RustupError::ToolchainNotInstalled(_)) = + Toolchain::new(self, t.to_owned()) + { + // Strip the confusing NotADirectory error and only mention that the + // override toolchain is not installed. + return Err(anyhow!(reason_err)) + .with_context(|| format!("override toolchain '{t}' is not installed")); + } } + // Either official (can auto install) or no toolchain specified + _ => {} } Ok(Some((override_cfg, reason))) @@ -572,15 +574,44 @@ impl Cfg { if let Ok(contents) = contents { let add_file_context = || format!("in {}", toolchain_file.to_string_lossy()); + // XXX Should not return the unvalidated contents; but a new + // internal only safe struct let override_file = Cfg::parse_override_file(contents, parse_mode) .with_context(add_file_context)?; - if let Some(toolchain_name) = &override_file.toolchain.channel { - let all_toolchains = self.list_toolchains().with_context(add_file_context)?; - if !all_toolchains.iter().any(|s| s == toolchain_name) { - // The given name is not resolvable as a toolchain, so - // instead check it's plausible for installation later - dist::validate_channel_name(toolchain_name) - .with_context(add_file_context)?; + if let Some(toolchain_name_str) = &override_file.toolchain.channel { + let toolchain_name = ResolvableToolchainName::try_from(toolchain_name_str)?; + let default_host_triple = get_default_host_triple(settings); + // Do not permit architecture/os selection in channels as + // these are host specific and toolchain files are portable. + if let ResolvableToolchainName::Official(ref name) = toolchain_name { + if name.has_triple() { + // Permit fully qualified names IFF the toolchain is installed. TODO(robertc): consider + // disabling this and backing out https://github.com/rust-lang/rustup/pull/2141 (but provide + // the base name in the error to help users) + let resolved_name = &ToolchainName::try_from(toolchain_name_str)?; + let ts = self.list_toolchains()?; + eprintln!("{resolved_name:?} {ts:?}"); + if !self.list_toolchains()?.iter().any(|s| s == resolved_name) { + return Err(anyhow!(format!( + "target triple in channel name '{name}'" + ))); + } + } + } + + // XXX: this awkwardness deals with settings file being locked already + let toolchain_name = toolchain_name.resolve(&default_host_triple)?; + match Toolchain::new(self, (&toolchain_name).into()) { + Err(RustupError::ToolchainNotInstalled(_)) => { + if matches!(toolchain_name, ToolchainName::Custom(_)) { + bail!( + "Toolchain {toolchain_name} in {} is custom and not installed", + toolchain_file.display() + ) + } + } + Ok(_) => {} + Err(e) => Err(e)?, } } @@ -629,50 +660,7 @@ impl Cfg { &self, path: &Path, ) -> Result<(Toolchain<'_>, Option)> { - fn components_exist( - distributable: &DistributableToolchain<'_>, - components: &[&str], - targets: &[&str], - ) -> Result { - let components_requested = !components.is_empty() || !targets.is_empty(); - // If we're here, the toolchain exists on disk and is a dist toolchain - // so we should attempt to load its manifest - let desc = if let Some(desc) = distributable.get_toolchain_desc_with_manifest()? { - desc - } else { - // We can't read the manifest. If this is a v1 install that's understandable - // and we assume the components are all good, otherwise we need to have a go - // at re-fetching the manifest to try again. - return Ok(distributable.guess_v1_manifest()); - }; - match (desc.list_components(), components_requested) { - // If the toolchain does not support components but there were components requested, bubble up the error - (Err(e), true) => Err(e), - // Otherwise check if all the components we want are installed - (Ok(installed_components), _) => Ok(components.iter().all(|name| { - installed_components.iter().any(|status| { - let cname = status.component.short_name(&desc.manifest); - let cname = cname.as_str(); - let cnameim = status.component.short_name_in_manifest(); - let cnameim = cnameim.as_str(); - (cname == *name || cnameim == *name) && status.installed - }) - }) - // And that all the targets we want are installed - && targets.iter().all(|name| { - installed_components - .iter() - .filter(|c| c.component.short_name_in_manifest() == "rust-std") - .any(|status| { - let ctarg = status.component.target(); - (ctarg == *name) && status.installed - }) - })), - _ => Ok(true), - } - } - - if let Some((toolchain, components, targets, reason, profile)) = + let (toolchain, components, targets, reason, profile) = match self.find_override_config(path)? { Some(( OverrideCfg { @@ -682,67 +670,92 @@ impl Cfg { profile, }, reason, - )) => { - let default = if toolchain.is_none() { - self.find_default()? - } else { - None - }; - - toolchain - .or(default) - .map(|toolchain| (toolchain, components, targets, Some(reason), profile)) - } - None => self - .find_default()? - .map(|toolchain| (toolchain, vec![], vec![], None, None)), + )) => (toolchain, components, targets, Some(reason), profile), + None => (None, vec![], vec![], None, None), + }; + let toolchain = match toolchain { + t @ Some(_) => t, + None => self.get_default()?.map(Into::into), + }; + match toolchain { + // No override and no default set + None => Err(RustupError::ToolchainNotSelected.into()), + Some(toolchain @ LocalToolchainName::Named(ToolchainName::Custom(_))) + | Some(toolchain @ LocalToolchainName::Path(_)) => { + Ok((Toolchain::new(self, toolchain)?, reason)) } - { - if toolchain.is_custom() { - if !toolchain.exists() { - return Err( - RustupError::ToolchainNotInstalled(toolchain.name().to_string()).into(), - ); - } - } else { + Some(LocalToolchainName::Named(ToolchainName::Official(desc))) => { let components: Vec<_> = components.iter().map(AsRef::as_ref).collect(); let targets: Vec<_> = targets.iter().map(AsRef::as_ref).collect(); - - let distributable = DistributableToolchain::new(&toolchain)?; - if !toolchain.exists() || !components_exist(&distributable, &components, &targets)? - { - distributable.install_from_dist(true, false, &components, &targets, profile)?; + let toolchain = match DistributableToolchain::new(self, desc.clone()) { + Err(RustupError::ToolchainNotInstalled(_)) => { + DistributableToolchain::install( + self, + &desc, + &components, + &targets, + profile.unwrap_or(Profile::Default), + false, + )? + .1 + } + Ok(mut distributable) => { + if !distributable.components_exist(&components, &targets)? { + distributable.update( + &components, + &targets, + profile.unwrap_or(Profile::Default), + )?; + } + distributable + } + Err(e) => return Err(e.into()), } + .into(); + Ok((toolchain, reason)) } - - Ok((toolchain, reason)) - } else { - // No override and no default set - Err(RustupError::ToolchainNotSelected.into()) } } - pub(crate) fn get_default(&self) -> Result> { + /// Get the configured default toolchain. + /// If none is configured, returns None + /// If a bad toolchain name is configured, errors. + pub(crate) fn get_default(&self) -> Result> { let user_opt = self.settings_file.with(|s| Ok(s.default_toolchain.clone())); - if let Some(fallback_settings) = &self.fallback_settings { + let toolchain_maybe_str = if let Some(fallback_settings) = &self.fallback_settings { match user_opt { - Err(_) | Ok(None) => return Ok(fallback_settings.default_toolchain.clone()), - _ => {} - }; - }; - user_opt + Err(_) | Ok(None) => Ok(fallback_settings.default_toolchain.clone()), + o => o, + } + } else { + user_opt + }?; + toolchain_maybe_str + .map(ResolvableToolchainName::try_from) + .transpose()? + .map(|t| t.resolve(&self.get_default_host_triple()?)) + .transpose() } + /// List all the installed toolchains: that is paths in the toolchain dir + /// that are: + /// - not files + /// - named with a valid resolved toolchain name + /// Currently no notification of incorrect names or entry type is done. #[cfg_attr(feature = "otel", tracing::instrument(skip_all))] - pub(crate) fn list_toolchains(&self) -> Result> { + pub(crate) fn list_toolchains(&self) -> Result> { if utils::is_directory(&self.toolchains_dir) { let mut toolchains: Vec<_> = utils::read_dir("toolchains", &self.toolchains_dir)? + // TODO: this discards errors reading the directory, is that + // correct? could we get a short-read and report less toolchains + // than exist? .filter_map(io::Result::ok) .filter(|e| e.file_type().map(|f| !f.is_file()).unwrap_or(false)) .filter_map(|e| e.file_name().into_string().ok()) + .filter_map(|n| ToolchainName::try_from(&n).ok()) .collect(); - utils::toolchain_sort(&mut toolchains); + crate::toolchain::names::toolchain_sort(&mut toolchains); Ok(toolchains) } else { @@ -750,38 +763,49 @@ impl Cfg { } } - pub(crate) fn list_channels(&self) -> Result>)>> { - let toolchains = self.list_toolchains()?; - - // Convert the toolchain strings to Toolchain values - let toolchains = toolchains.into_iter(); - let toolchains = toolchains.map(|n| (n.clone(), self.get_toolchain(&n, true))); + pub(crate) fn list_channels(&self) -> Result)>> { + self.list_toolchains()? + .into_iter() + .filter_map(|t| { + if let ToolchainName::Official(desc) = t { + Some(desc) + } else { + None + } + }) + .filter(ToolchainDesc::is_tracking) + .map(|n| { + DistributableToolchain::new(self, n.clone()) + .map_err(Into::into) + .map(|t| (n.clone(), t)) + }) + .collect::>>() + } - // Filter out toolchains that don't track a release channel - Ok(toolchains - .filter(|(_, ref t)| t.as_ref().map(Toolchain::is_tracking).unwrap_or(false)) - .collect()) + /// Create an override for a toolchain + pub(crate) fn make_override(&self, path: &Path, toolchain: &ToolchainName) -> Result<()> { + self.settings_file.with_mut(|s| { + s.add_override(path, toolchain.to_string(), self.notify_handler.as_ref()); + Ok(()) + }) } pub(crate) fn update_all_channels( &self, force_update: bool, - ) -> Result)>> { + ) -> Result)>> { let channels = self.list_channels()?; let channels = channels.into_iter(); + let profile = self.get_profile()?; // Update toolchains and collect the results - let channels = channels.map(|(n, t)| { - let st = t.and_then(|t| { - let distributable = DistributableToolchain::new(&t)?; - let st = distributable.install_from_dist(force_update, false, &[], &[], None); - if let Err(ref e) = st { - (self.notify_handler)(Notification::NonFatalError(e)); - } - st - }); + let channels = channels.map(|(desc, mut distributable)| { + let st = distributable.update_extra(&[], &[], profile, force_update, false); - (n, st) + if let Err(ref e) = st { + (self.notify_handler)(Notification::NonFatalError(e)); + } + (desc, st) }); Ok(channels.collect()) @@ -803,45 +827,68 @@ impl Cfg { }) } - pub(crate) fn toolchain_for_dir( - &self, - path: &Path, - ) -> Result<(Toolchain<'_>, Option)> { - self.find_or_install_override_toolchain_or_default(path) - } - pub(crate) fn create_command_for_dir(&self, path: &Path, binary: &str) -> Result { - let (ref toolchain, _) = self.toolchain_for_dir(path)?; - - if let Some(cmd) = self.maybe_do_cargo_fallback(toolchain, binary)? { - Ok(cmd) - } else { - // NB this can only fail in race conditions since we used toolchain - // for dir. - let installed = toolchain.as_installed_common()?; - installed.create_command(binary) - } + let (toolchain, _) = self.find_or_install_override_toolchain_or_default(path)?; + self.create_command_for_toolchain_(toolchain, binary) } pub(crate) fn create_command_for_toolchain( &self, - toolchain: &str, + toolchain_name: &LocalToolchainName, install_if_missing: bool, binary: &str, ) -> Result { - let toolchain = self.get_toolchain(toolchain, false)?; - if install_if_missing && !toolchain.exists() { - let distributable = DistributableToolchain::new(&toolchain)?; - distributable.install_from_dist(true, false, &[], &[], None)?; + match toolchain_name { + LocalToolchainName::Named(ToolchainName::Official(desc)) => { + match DistributableToolchain::new(self, desc.clone()) { + Err(RustupError::ToolchainNotInstalled(_)) => { + if install_if_missing { + DistributableToolchain::install( + self, + desc, + &[], + &[], + self.get_profile()?, + true, + )?; + } + } + o => { + o?; + } + } + } + n => { + if !Toolchain::exists(self, n)? { + return Err(RustupError::ToolchainNotInstallable(n.to_string()).into()); + } + } } - if let Some(cmd) = self.maybe_do_cargo_fallback(&toolchain, binary)? { - Ok(cmd) - } else { - // NB note this really can't fail due to to having installed the toolchain if needed - let installed = toolchain.as_installed_common()?; - installed.create_command(binary) + let toolchain = Toolchain::new(self, toolchain_name.clone())?; + + // NB this can only fail in race conditions since we handle existence above + // for dir. + self.create_command_for_toolchain_(toolchain, binary) + } + + fn create_command_for_toolchain_( + &self, + toolchain: Toolchain<'_>, + binary: &str, + ) -> Result { + // Should push the cargo fallback into a custom toolchain type? And then + // perhaps a trait that create command layers on? + if !matches!( + toolchain.name(), + LocalToolchainName::Named(ToolchainName::Official(_)) + ) { + if let Some(cmd) = self.maybe_do_cargo_fallback(&toolchain, binary)? { + return Ok(cmd); + } } + + toolchain.create_command(binary) } // Custom toolchains don't have cargo, so here we detect that situation and @@ -851,27 +898,31 @@ impl Cfg { toolchain: &Toolchain<'_>, binary: &str, ) -> Result> { - if !toolchain.is_custom() { - return Ok(None); - } - if binary != "cargo" && binary != "cargo.exe" { return Ok(None); } - let cargo_path = toolchain.path().join("bin/cargo"); - let cargo_exe_path = toolchain.path().join("bin/cargo.exe"); + let cargo_path = toolchain.binary_file("cargo"); - if cargo_path.exists() || cargo_exe_path.exists() { + // breadcrumb in case of regression: we used to get the cargo path and + // cargo.exe path separately, not using the binary_file helper. This may + // matter if calling a binary with some personality that allows .exe and + // not .exe to coexist (e.g. wine) - but thats not something we aim to + // support : the host should always be correct. + if cargo_path.exists() { return Ok(None); } - // XXX: This could actually consider all distributable toolchains in principle. - for fallback in &["nightly", "beta", "stable"] { - let fallback = self.get_toolchain(fallback, false)?; - if fallback.exists() { - let distributable = DistributableToolchain::new(&fallback)?; - let cmd = distributable.create_fallback_command("cargo", toolchain)?; + let default_host_triple = self.get_default_host_triple()?; + // XXX: This could actually consider all installed distributable + // toolchains in principle. + for fallback in ["nightly", "beta", "stable"] { + let resolved = + PartialToolchainDesc::from_str(fallback)?.resolve(&default_host_triple)?; + if let Ok(fallback) = + crate::toolchain::distributable::DistributableToolchain::new(self, resolved) + { + let cmd = fallback.create_fallback_command("cargo", toolchain)?; return Ok(Some(cmd)); } } @@ -893,28 +944,25 @@ impl Cfg { #[cfg_attr(feature = "otel", tracing::instrument(skip_all))] pub(crate) fn get_default_host_triple(&self) -> Result { - Ok(self - .settings_file - .with(|s| { - Ok(s.default_host_triple - .as_ref() - .map(|s| dist::TargetTriple::new(s))) - })? - .unwrap_or_else(dist::TargetTriple::from_host_or_build)) + self.settings_file.with(|s| Ok(get_default_host_triple(s))) } - pub(crate) fn resolve_toolchain(&self, name: &str) -> Result { - // remove trailing slashes in toolchain name - let normalized_name = name.trim_end_matches('/'); - if let Ok(desc) = dist::PartialToolchainDesc::from_str(normalized_name) { - let host = self.get_default_host_triple()?; - Ok(desc.resolve(&host)?.to_string()) - } else { - Ok(normalized_name.to_owned()) + /// The path on disk of any concrete toolchain + pub(crate) fn toolchain_path(&self, toolchain: &LocalToolchainName) -> PathBuf { + match toolchain { + LocalToolchainName::Named(name) => self.toolchains_dir.join(name.to_string()), + LocalToolchainName::Path(p) => p.to_path_buf(), } } } +fn get_default_host_triple(s: &Settings) -> dist::TargetTriple { + s.default_host_triple + .as_ref() + .map(|s| dist::TargetTriple::new(s)) + .unwrap_or_else(dist::TargetTriple::from_host_or_build) +} + /// Specifies how a `rust-toolchain`/`rust-toolchain.toml` configuration file should be parsed. enum ParseMode { /// Only permit TOML format in a configuration file. diff --git a/src/dist/component/mod.rs b/src/dist/component/mod.rs index 4f7c3f7ef08..9df43bbf112 100644 --- a/src/dist/component/mod.rs +++ b/src/dist/component/mod.rs @@ -11,3 +11,6 @@ mod transaction; mod package; // The representation of *installed* components, and uninstallation mod components; + +#[cfg(test)] +mod tests; diff --git a/tests/suite/dist_transactions.rs b/src/dist/component/tests.rs similarity index 91% rename from tests/suite/dist_transactions.rs rename to src/dist/component/tests.rs index b82164c598e..1c3bf6e5fa5 100644 --- a/tests/suite/dist_transactions.rs +++ b/src/dist/component/tests.rs @@ -2,22 +2,23 @@ use std::fs; use std::io::Write; use std::path::PathBuf; -use rustup::dist::component::Transaction; -use rustup::dist::dist::DEFAULT_DIST_SERVER; -use rustup::dist::prefix::InstallPrefix; -use rustup::dist::temp; -use rustup::dist::Notification; -use rustup::utils::raw as utils_raw; -use rustup::utils::utils; -use rustup::RustupError; -use rustup_macros::integration_test as test; +use rustup_macros::unit_test as test; + +use crate::dist::component::Transaction; +use crate::dist::dist::DEFAULT_DIST_SERVER; +use crate::dist::prefix::InstallPrefix; +use crate::dist::temp; +use crate::dist::Notification; +use crate::utils::raw as utils_raw; +use crate::utils::utils; +use crate::RustupError; #[test] fn add_file() { let prefixdir = tempfile::Builder::new().prefix("rustup").tempdir().unwrap(); let txdir = tempfile::Builder::new().prefix("rustup").tempdir().unwrap(); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let tmpcfg = temp::Cfg::new( txdir.path().to_owned(), @@ -45,7 +46,7 @@ fn add_file_then_rollback() { let prefixdir = tempfile::Builder::new().prefix("rustup").tempdir().unwrap(); let txdir = tempfile::Builder::new().prefix("rustup").tempdir().unwrap(); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let tmpcfg = temp::Cfg::new( txdir.path().to_owned(), @@ -73,7 +74,7 @@ fn add_file_that_exists() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix, &tmpcfg, ¬ify); @@ -104,7 +105,7 @@ fn copy_file() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -131,7 +132,7 @@ fn copy_file_then_rollback() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -158,7 +159,7 @@ fn copy_file_that_exists() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix, &tmpcfg, ¬ify); @@ -194,7 +195,7 @@ fn copy_dir() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -228,7 +229,7 @@ fn copy_dir_then_rollback() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -262,7 +263,7 @@ fn copy_dir_that_exists() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -293,7 +294,7 @@ fn remove_file() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix, &tmpcfg, ¬ify); @@ -318,7 +319,7 @@ fn remove_file_then_rollback() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix, &tmpcfg, ¬ify); @@ -343,7 +344,7 @@ fn remove_file_that_not_exists() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix, &tmpcfg, ¬ify); @@ -370,7 +371,7 @@ fn remove_dir() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix, &tmpcfg, ¬ify); @@ -396,7 +397,7 @@ fn remove_dir_then_rollback() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix, &tmpcfg, ¬ify); @@ -422,7 +423,7 @@ fn remove_dir_that_not_exists() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix, &tmpcfg, ¬ify); @@ -449,7 +450,7 @@ fn write_file() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -476,7 +477,7 @@ fn write_file_then_rollback() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -500,7 +501,7 @@ fn write_file_that_exists() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -531,7 +532,7 @@ fn modify_file_that_not_exists() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -555,7 +556,7 @@ fn modify_file_that_exists() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -579,7 +580,7 @@ fn modify_file_that_not_exists_then_rollback() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -601,7 +602,7 @@ fn modify_file_that_exists_then_rollback() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -628,7 +629,7 @@ fn modify_twice_then_rollback() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -655,7 +656,7 @@ fn do_multiple_op_transaction(rollback: bool) { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); @@ -756,7 +757,7 @@ fn rollback_failure_keeps_going() { Box::new(|_| ()), ); - let prefix = InstallPrefix::from(prefixdir.path().to_owned()); + let prefix = InstallPrefix::from(prefixdir.path()); let notify = |_: Notification<'_>| (); let mut tx = Transaction::new(prefix.clone(), &tmpcfg, ¬ify); diff --git a/src/dist/dist.rs b/src/dist/dist.rs index cd6f8e6acf4..a38657f3921 100644 --- a/src/dist/dist.rs +++ b/src/dist/dist.rs @@ -12,16 +12,21 @@ use lazy_static::lazy_static; use regex::Regex; use thiserror::Error as ThisError; -use crate::dist::download::DownloadCfg; -use crate::dist::manifest::{Component, Manifest as ManifestV2}; -use crate::dist::manifestation::{Changes, Manifestation, UpdateStatus}; -use crate::dist::notifications::*; -use crate::dist::prefix::InstallPrefix; -use crate::dist::temp; pub(crate) use crate::dist::triple::*; -use crate::errors::RustupError; -use crate::process; -use crate::utils::utils; +use crate::{ + dist::{ + download::DownloadCfg, + manifest::{Component, Manifest as ManifestV2}, + manifestation::{Changes, Manifestation, UpdateStatus}, + notifications::*, + prefix::InstallPrefix, + temp, + }, + errors::RustupError, + process, + toolchain::names::ToolchainName, + utils::utils, +}; pub static DEFAULT_DIST_SERVER: &str = "https://static.rust-lang.org"; @@ -83,11 +88,13 @@ fn components_missing_msg(cs: &[Component], manifest: &ManifestV2, toolchain: &s } #[derive(Debug, ThisError)] -enum DistError { +pub(crate) enum DistError { #[error("{}", components_missing_msg(.0, .1, .2))] ToolchainComponentsMissing(Vec, Box, String), #[error("no release found for '{0}'")] MissingReleaseForToolchain(String), + #[error("invalid toolchain name: '{0}'")] + InvalidOfficialName(String), } #[derive(Debug, PartialEq)] @@ -102,7 +109,7 @@ struct ParsedToolchainDesc { // 'stable-msvc' to work. Partial target triples though are parsed // from a hardcoded set of known triples, whereas target triples // are nearly-arbitrary strings. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] pub struct PartialToolchainDesc { // Either "nightly", "stable", "beta", or an explicit version number pub channel: String, @@ -113,7 +120,10 @@ pub struct PartialToolchainDesc { // Fully-resolved toolchain descriptors. These always have full target // triples attached to them and are used for canonical identification, // such as naming their installation directory. -#[derive(Debug, Clone)] +// +// as strings they look like stable-x86_64-pc-windows-msvc or +/// 1.55-x86_64-pc-windows-msvc +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] pub struct ToolchainDesc { // Either "nightly", "stable", "beta", or an explicit version number pub channel: String, @@ -426,6 +436,7 @@ impl FromStr for PartialToolchainDesc { } impl PartialToolchainDesc { + /// Create a toolchain desc using input_host to fill in missing fields pub(crate) fn resolve(self, input_host: &TargetTriple) -> Result { let host = PartialTargetTriple::new(&input_host.0).ok_or_else(|| { anyhow!(format!( @@ -534,13 +545,14 @@ impl ToolchainDesc { } } -// A little convenience for just parsing a channel name or archived channel name -pub(crate) fn validate_channel_name(name: &str) -> Result<()> { - let toolchain = PartialToolchainDesc::from_str(name)?; - if toolchain.has_triple() { - Err(anyhow!(format!("target triple in channel name '{name}'"))) - } else { - Ok(()) +impl TryFrom<&ToolchainName> for ToolchainDesc { + type Error = DistError; + + fn try_from(value: &ToolchainName) -> std::result::Result { + match value { + ToolchainName::Custom(n) => Err(DistError::InvalidOfficialName(n.str().into())), + ToolchainName::Official(n) => Ok(n.clone()), + } } } @@ -650,7 +662,7 @@ pub(crate) fn valid_profile_names() -> String { // an upgrade then all the existing components will be upgraded. // // Returns the manifest's hash if anything changed. -#[cfg_attr(feature = "otel", tracing::instrument(skip_all))] +#[cfg_attr(feature = "otel", tracing::instrument(err, skip_all, fields(profile=format!("{profile:?}"), prefix=prefix.path().to_string_lossy().to_string())))] pub(crate) fn update_from_dist( download: DownloadCfg<'_>, update_hash: Option<&Path>, @@ -789,7 +801,7 @@ fn update_from_dist_( // no need to even print anything for missing nightlies, // since we don't really "skip" them } - None => { + _ => { // All other errors break the loop break Err(e); } diff --git a/src/dist/manifest.rs b/src/dist/manifest.rs index 04ff941c6d7..c46dd8d2a4e 100644 --- a/src/dist/manifest.rs +++ b/src/dist/manifest.rs @@ -22,8 +22,18 @@ use crate::dist::dist::{PartialTargetTriple, Profile, TargetTriple}; use crate::errors::*; use crate::utils::toml_utils::*; +use super::{config::Config, dist::ToolchainDesc}; + pub(crate) const SUPPORTED_MANIFEST_VERSIONS: [&str; 1] = ["2"]; +/// Used by the `installed_components` function +pub(crate) struct ComponentStatus { + pub component: Component, + pub name: String, + pub installed: bool, + pub available: bool, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct Manifest { manifest_version: String, @@ -345,6 +355,57 @@ impl Manifest { c }) } + + /// Determine installed components from an installed manifest. + pub(crate) fn query_components( + &self, + desc: &ToolchainDesc, + config: &Config, + ) -> Result> { + // Return all optional components of the "rust" package for the + // toolchain's target triple. + let mut res = Vec::new(); + + let rust_pkg = self + .packages + .get("rust") + .expect("manifest should contain a rust package"); + let targ_pkg = rust_pkg + .targets + .get(&desc.target) + .expect("installed manifest should have a known target"); + + for component in &targ_pkg.components { + let installed = component.contained_within(&config.components); + + let component_target = TargetTriple::new(&component.target()); + + // Get the component so we can check if it is available + let component_pkg = self + .get_package(component.short_name_in_manifest()) + .unwrap_or_else(|_| { + panic!( + "manifest should contain component {}", + &component.short_name(self) + ) + }); + let component_target_pkg = component_pkg + .targets + .get(&component_target) + .expect("component should have target toolchain"); + + res.push(ComponentStatus { + component: component.clone(), + name: component.name(self), + installed, + available: component_target_pkg.available(), + }); + } + + res.sort_by(|a, b| a.component.cmp(&b.component)); + + Ok(res) + } } impl Package { @@ -645,3 +706,116 @@ impl Component { } } } + +#[cfg(test)] +mod tests { + use crate::dist::dist::TargetTriple; + use crate::dist::manifest::Manifest; + use crate::RustupError; + + // Example manifest from https://public.etherpad-mozilla.org/p/Rust-infra-work-week + static EXAMPLE: &str = include_str!("manifest/tests/channel-rust-nightly-example.toml"); + // From brson's live build-rust-manifest.py script + static EXAMPLE2: &str = include_str!("manifest/tests/channel-rust-nightly-example2.toml"); + + #[test] + fn parse_smoke_test() { + let x86_64_unknown_linux_gnu = TargetTriple::new("x86_64-unknown-linux-gnu"); + let x86_64_unknown_linux_musl = TargetTriple::new("x86_64-unknown-linux-musl"); + + let pkg = Manifest::parse(EXAMPLE).unwrap(); + + pkg.get_package("rust").unwrap(); + pkg.get_package("rustc").unwrap(); + pkg.get_package("cargo").unwrap(); + pkg.get_package("rust-std").unwrap(); + pkg.get_package("rust-docs").unwrap(); + + let rust_pkg = pkg.get_package("rust").unwrap(); + assert!(rust_pkg.version.contains("1.3.0")); + + let rust_target_pkg = rust_pkg + .get_target(Some(&x86_64_unknown_linux_gnu)) + .unwrap(); + assert!(rust_target_pkg.available()); + assert_eq!(rust_target_pkg.bins[0].1.url, "example.com"); + assert_eq!(rust_target_pkg.bins[0].1.hash, "..."); + + let component = &rust_target_pkg.components[0]; + assert_eq!(component.short_name_in_manifest(), "rustc"); + assert_eq!(component.target.as_ref(), Some(&x86_64_unknown_linux_gnu)); + + let component = &rust_target_pkg.components[4]; + assert_eq!(component.short_name_in_manifest(), "rust-std"); + assert_eq!(component.target.as_ref(), Some(&x86_64_unknown_linux_musl)); + + let docs_pkg = pkg.get_package("rust-docs").unwrap(); + let docs_target_pkg = docs_pkg + .get_target(Some(&x86_64_unknown_linux_gnu)) + .unwrap(); + assert_eq!(docs_target_pkg.bins[0].1.url, "example.com"); + } + + #[test] + fn renames() { + let manifest = Manifest::parse(EXAMPLE2).unwrap(); + assert_eq!(1, manifest.renames.len()); + assert_eq!(manifest.renames["cargo-old"], "cargo"); + assert_eq!(1, manifest.reverse_renames.len()); + assert_eq!(manifest.reverse_renames["cargo"], "cargo-old"); + } + + #[test] + fn parse_round_trip() { + let original = Manifest::parse(EXAMPLE).unwrap(); + let serialized = original.clone().stringify(); + let new = Manifest::parse(&serialized).unwrap(); + assert_eq!(original, new); + + let original = Manifest::parse(EXAMPLE2).unwrap(); + let serialized = original.clone().stringify(); + let new = Manifest::parse(&serialized).unwrap(); + assert_eq!(original, new); + } + + #[test] + fn validate_components_have_corresponding_packages() { + let manifest = r#" +manifest-version = "2" +date = "2015-10-10" +[pkg.rust] + version = "rustc 1.3.0 (9a92aaf19 2015-09-15)" + [pkg.rust.target.x86_64-unknown-linux-gnu] + available = true + url = "example.com" + hash = "..." + [[pkg.rust.target.x86_64-unknown-linux-gnu.components]] + pkg = "rustc" + target = "x86_64-unknown-linux-gnu" + [[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] + pkg = "rust-std" + target = "x86_64-unknown-linux-musl" +[pkg.rustc] + version = "rustc 1.3.0 (9a92aaf19 2015-09-15)" + [pkg.rustc.target.x86_64-unknown-linux-gnu] + available = true + url = "example.com" + hash = "..." +"#; + + let err = Manifest::parse(manifest).unwrap_err(); + + match err.downcast::().unwrap() { + RustupError::MissingPackageForComponent(_) => {} + _ => panic!(), + } + } + + // #248 + #[test] + fn manifest_can_contain_unknown_targets() { + let manifest = EXAMPLE.replace("x86_64-unknown-linux-gnu", "mycpu-myvendor-myos"); + + assert!(Manifest::parse(&manifest).is_ok()); + } +} diff --git a/tests/suite/channel-rust-nightly-example.toml b/src/dist/manifest/tests/channel-rust-nightly-example.toml similarity index 100% rename from tests/suite/channel-rust-nightly-example.toml rename to src/dist/manifest/tests/channel-rust-nightly-example.toml diff --git a/tests/suite/channel-rust-nightly-example2.toml b/src/dist/manifest/tests/channel-rust-nightly-example2.toml similarity index 100% rename from tests/suite/channel-rust-nightly-example2.toml rename to src/dist/manifest/tests/channel-rust-nightly-example2.toml diff --git a/src/dist/manifestation.rs b/src/dist/manifestation.rs index 48ad3842116..729224de958 100644 --- a/src/dist/manifestation.rs +++ b/src/dist/manifestation.rs @@ -1,6 +1,9 @@ //! Maintains a Rust installation by installing individual Rust //! platform components from a distribution server. +#[cfg(test)] +mod tests; + use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; diff --git a/tests/suite/dist.rs b/src/dist/manifestation/tests.rs similarity index 98% rename from tests/suite/dist.rs rename to src/dist/manifestation/tests.rs index d473792a856..d97bddb1793 100644 --- a/tests/suite/dist.rs +++ b/src/dist/manifestation/tests.rs @@ -13,21 +13,22 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use url::Url; -use rustup::currentprocess; -use rustup::dist::dist::{Profile, TargetTriple, ToolchainDesc, DEFAULT_DIST_SERVER}; -use rustup::dist::download::DownloadCfg; -use rustup::dist::manifest::{Component, Manifest}; -use rustup::dist::manifestation::{Changes, Manifestation, UpdateStatus}; -use rustup::dist::prefix::InstallPrefix; -use rustup::dist::temp; -use rustup::dist::Notification; -use rustup::errors::RustupError; -use rustup::utils::raw as utils_raw; -use rustup::utils::utils; -use rustup_macros::integration_test as test; - -use crate::mock::dist::*; -use crate::mock::{MockComponentBuilder, MockFile, MockInstallerBuilder}; +use rustup_macros::unit_test as test; + +use crate::{ + currentprocess, + dist::{ + dist::{Profile, TargetTriple, ToolchainDesc, DEFAULT_DIST_SERVER}, + download::DownloadCfg, + manifest::{Component, Manifest}, + manifestation::{Changes, Manifestation, UpdateStatus}, + prefix::InstallPrefix, + temp, Notification, + }, + errors::RustupError, + test::mock::{dist::*, MockComponentBuilder, MockFile, MockInstallerBuilder}, + utils::{raw as utils_raw, utils}, +}; const SHA256_HASH_LEN: usize = 64; @@ -546,7 +547,7 @@ fn setup_from_dist_server( ); let toolchain = ToolchainDesc::from_str("nightly-x86_64-apple-darwin").unwrap(); - let prefix = InstallPrefix::from(prefix_tempdir.path().to_owned()); + let prefix = InstallPrefix::from(prefix_tempdir.path()); let download_cfg = DownloadCfg { dist_root: "phony", temp_cfg: &temp_cfg, diff --git a/src/dist/prefix.rs b/src/dist/prefix.rs index 76d03461b96..874b27850ef 100644 --- a/src/dist/prefix.rs +++ b/src/dist/prefix.rs @@ -1,34 +1,66 @@ use std::path::{Path, PathBuf}; +use crate::utils::utils; + const REL_MANIFEST_DIR: &str = "lib/rustlib"; +static V1_COMMON_COMPONENT_LIST: &[&str] = &["cargo", "rustc", "rust-docs"]; #[derive(Clone, Debug)] pub struct InstallPrefix { path: PathBuf, } impl InstallPrefix { - pub fn from(path: PathBuf) -> Self { - Self { path } - } pub fn path(&self) -> &Path { &self.path } + pub(crate) fn abs_path>(&self, path: P) -> PathBuf { self.path.join(path) } + pub(crate) fn manifest_dir(&self) -> PathBuf { let mut path = self.path.clone(); path.push(REL_MANIFEST_DIR); path } + pub fn manifest_file(&self, name: &str) -> PathBuf { let mut path = self.manifest_dir(); path.push(name); path } + pub(crate) fn rel_manifest_file(&self, name: &str) -> PathBuf { let mut path = PathBuf::from(REL_MANIFEST_DIR); path.push(name); path } + + /// Guess whether this is a V1 or V2 manifest distribution. + pub(crate) fn guess_v1_manifest(&self) -> bool { + // If all the v1 common components are present this is likely to be + // a v1 manifest install. The v1 components are not called the same + // in a v2 install. + for component in V1_COMMON_COMPONENT_LIST { + let manifest = format!("manifest-{component}"); + let manifest_path = self.manifest_file(&manifest); + if !utils::path_exists(manifest_path) { + return false; + } + } + // It's reasonable to assume this is a v1 manifest installation + true + } +} + +impl From<&Path> for InstallPrefix { + fn from(value: &Path) -> Self { + Self { path: value.into() } + } +} + +impl From for InstallPrefix { + fn from(path: PathBuf) -> Self { + Self { path } + } } diff --git a/src/dist/triple.rs b/src/dist/triple.rs index ad27adb1469..db64f6b3058 100644 --- a/src/dist/triple.rs +++ b/src/dist/triple.rs @@ -47,7 +47,7 @@ static LIST_ENVS: &[&str] = &[ "musl", ]; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PartialTargetTriple { pub arch: Option, pub os: Option, diff --git a/src/errors.rs b/src/errors.rs index 5fbf96b28db..3fd2fe961a7 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,14 +2,19 @@ use std::ffi::OsString; use std::fmt::Debug; -use std::io::{self, Write}; +#[cfg(not(windows))] +use std::io; +use std::io::Write; use std::path::PathBuf; use thiserror::Error as ThisError; use url::Url; -use crate::currentprocess::process; -use crate::dist::manifest::{Component, Manifest}; +use crate::{currentprocess::process, dist::dist::ToolchainDesc}; +use crate::{ + dist::manifest::{Component, Manifest}, + toolchain::names::{PathBasedToolchainName, ToolchainName}, +}; const TOOLSTATE_MSG: &str = "If you require these components, please install and use the latest successful build version,\n\ @@ -25,7 +30,7 @@ const TOOLSTATE_MSG: &str = pub struct OperationError(pub anyhow::Error); #[derive(ThisError, Debug)] -pub enum RustupError { +pub(crate) enum RustupError { #[error("partially downloaded file may have been damaged and was removed, please try again")] BrokenPartialFile, #[error("component download failed for {0}")] @@ -36,17 +41,13 @@ pub enum RustupError { ComponentMissingFile { name: String, path: PathBuf }, #[error("could not create {name} directory: '{}'", .path.display())] CreatingDirectory { name: &'static str, path: PathBuf }, - #[error("unable to read the PGP key '{}'", .path.display())] - InvalidPgpKey { - path: PathBuf, - source: anyhow::Error, - }, #[error("invalid toolchain name: '{0}'")] InvalidToolchainName(String), #[error("could not create link from '{}' to '{}'", .src.display(), .dest.display())] LinkingFile { src: PathBuf, dest: PathBuf }, #[error("Unable to proceed. Could not locate working directory.")] LocatingWorkingDir, + #[cfg(not(windows))] #[error("failed to set permissions for '{}'", .p.display())] SettingPermissions { p: PathBuf, source: io::Error }, #[error("checksum failed for '{url}', expected: '{expected}', calculated: '{calculated}'")] @@ -59,14 +60,16 @@ pub enum RustupError { ComponentConflict { name: String, path: PathBuf }, #[error("toolchain '{0}' does not support components")] ComponentsUnsupported(String), + #[error("toolchain '{0}' does not support components (v1 manifest)")] + ComponentsUnsupportedV1(String), #[error("component manifest for '{0}' is corrupt")] CorruptComponent(String), #[error("could not download file from '{url}' to '{}'", .path.display())] DownloadingFile { url: Url, path: PathBuf }, #[error("could not download file from '{url}' to '{}'", .path.display())] DownloadNotExists { url: Url, path: PathBuf }, - #[error("Missing manifest in toolchain '{}'", .name)] - MissingManifest { name: String }, + #[error("Missing manifest in toolchain '{}'", .0)] + MissingManifest(ToolchainDesc), #[error("server sent a broken manifest: missing package for component {0}")] MissingPackageForComponent(String), #[error("could not read {name} directory: '{}'", .path.display())] @@ -88,24 +91,26 @@ pub enum RustupError { #[error("toolchain '{0}' is not installable")] ToolchainNotInstallable(String), #[error("toolchain '{0}' is not installed")] - ToolchainNotInstalled(String), + ToolchainNotInstalled(ToolchainName), + #[error("path '{0}' not found")] + PathToolchainNotInstalled(PathBasedToolchainName), #[error( "rustup could not choose a version of {} to run, because one wasn't specified explicitly, and no default is configured.\n{}", process().name().unwrap_or_else(|| "Rust".into()), "help: run 'rustup default stable' to download the latest stable release of Rust and set it as your default toolchain." )] ToolchainNotSelected, - #[error("toolchain '{}' does not contain component {}{}{}", .name, .component, if let Some(suggestion) = .suggestion { + #[error("toolchain '{}' does not contain component {}{}{}", .desc, .component, if let Some(suggestion) = .suggestion { format!("; did you mean '{suggestion}'?") } else { "".to_string() }, if .component.contains("rust-std") { format!("\nnote: not all platforms have the standard library pre-compiled: https://doc.rust-lang.org/nightly/rustc/platform-support.html{}", - if name.contains("nightly") { "\nhelp: consider using `cargo build -Z build-std` instead" } else { "" } + if desc.channel == "nightly" { "\nhelp: consider using `cargo build -Z build-std` instead" } else { "" } ) } else { "".to_string() })] UnknownComponent { - name: String, + desc: ToolchainDesc, component: String, suggestion: Option, }, @@ -115,6 +120,8 @@ pub enum RustupError { UnsupportedVersion(String), #[error("could not write {name} file: '{}'", .path.display())] WritingFile { name: &'static str, path: PathBuf }, + #[error("I/O Error")] + IOError(#[from] std::io::Error), } fn remove_component_msg(cs: &Component, manifest: &Manifest, toolchain: &str) -> String { diff --git a/src/install.rs b/src/install.rs index 6cc93452083..732026755a0 100644 --- a/src/install.rs +++ b/src/install.rs @@ -1,93 +1,111 @@ //! Installation and upgrade of both distribution-managed and local //! toolchains -use std::path::Path; +use std::path::{Path, PathBuf}; use anyhow::Result; -use crate::dist::dist; -use crate::dist::download::DownloadCfg; -use crate::dist::prefix::InstallPrefix; -use crate::dist::Notification; -use crate::errors::RustupError; -use crate::notifications::Notification as RootNotification; -use crate::toolchain::{CustomToolchain, Toolchain, UpdateStatus}; -use crate::utils::utils; +use crate::{ + config::Cfg, + dist::{dist, download::DownloadCfg, prefix::InstallPrefix, Notification}, + errors::RustupError, + notifications::Notification as RootNotification, + toolchain::{ + names::{CustomToolchainName, LocalToolchainName}, + toolchain::Toolchain, + }, + utils::utils, +}; + +#[derive(Clone, Debug)] +pub(crate) enum UpdateStatus { + Installed, + Updated(String), // Stores the version of rustc *before* the update + Unchanged, +} -#[derive(Copy, Clone)] +#[derive(Clone)] pub(crate) enum InstallMethod<'a> { - Copy(&'a Path, &'a CustomToolchain<'a>), - Link(&'a Path, &'a CustomToolchain<'a>), - // bool is whether to force an update + Copy { + src: &'a Path, + dest: &'a CustomToolchainName, + cfg: &'a Cfg, + }, + Link { + src: &'a Path, + dest: &'a CustomToolchainName, + cfg: &'a Cfg, + }, Dist { + cfg: &'a Cfg, desc: &'a dist::ToolchainDesc, profile: dist::Profile, update_hash: Option<&'a Path>, dl_cfg: DownloadCfg<'a>, - // --force - force_update: bool, - // --allow-downgrade + /// --force bool is whether to force an update/install + force: bool, + /// --allow-downgrade allow_downgrade: bool, - // toolchain already exists + /// toolchain already exists exists: bool, - // currently installed date - old_date: Option<&'a str>, - // Extra components to install from dist + /// currently installed date and version + old_date_version: Option<(String, String)>, + /// Extra components to install from dist components: &'a [&'a str], - // Extra targets to install from dist + /// Extra targets to install from dist targets: &'a [&'a str], }, } impl<'a> InstallMethod<'a> { // Install a toolchain - pub(crate) fn install(&self, toolchain: &Toolchain<'a>) -> Result { - let previous_version = if toolchain.exists() { - Some(toolchain.rustc_version()) - } else { - None - }; - if previous_version.is_some() { - (toolchain.cfg().notify_handler)(RootNotification::UpdatingToolchain(toolchain.name())); - } else { - (toolchain.cfg().notify_handler)(RootNotification::InstallingToolchain( - toolchain.name(), - )); + #[cfg_attr(feature = "otel", tracing::instrument(err, skip_all))] + pub(crate) fn install(&self) -> Result { + let nh = self.cfg().notify_handler.clone(); + match self { + InstallMethod::Copy { .. } + | InstallMethod::Link { .. } + | InstallMethod::Dist { + old_date_version: None, + .. + } => (nh)(RootNotification::InstallingToolchain(&self.dest_basename())), + _ => (nh)(RootNotification::UpdatingToolchain(&self.dest_basename())), } - (toolchain.cfg().notify_handler)(RootNotification::ToolchainDirectory( - toolchain.path(), - toolchain.name(), + + (self.cfg().notify_handler)(RootNotification::ToolchainDirectory( + &self.dest_path(), + &self.dest_basename(), )); - let updated = self.run(toolchain.path(), &|n| { - (toolchain.cfg().notify_handler)(n.into()) + let updated = self.run(&self.dest_path(), &|n| { + (self.cfg().notify_handler)(n.into()) })?; - if !updated { - (toolchain.cfg().notify_handler)(RootNotification::UpdateHashMatches); - } else { - (toolchain.cfg().notify_handler)(RootNotification::InstalledToolchain( - toolchain.name(), - )); - } - - let status = match (updated, previous_version) { - (true, None) => UpdateStatus::Installed, - (true, Some(v)) => UpdateStatus::Updated(v), - (false, _) => UpdateStatus::Unchanged, + let status = match updated { + false => { + (nh)(RootNotification::UpdateHashMatches); + UpdateStatus::Unchanged + } + true => { + (nh)(RootNotification::InstalledToolchain(&self.dest_basename())); + match self { + InstallMethod::Dist { + old_date_version: Some((_, v)), + .. + } => UpdateStatus::Updated(v.clone()), + InstallMethod::Copy { .. } + | InstallMethod::Link { .. } + | InstallMethod::Dist { .. } => UpdateStatus::Installed, + } + } }; // Final check, to ensure we're installed - if !toolchain.exists() { - Err(RustupError::ToolchainNotInstallable(toolchain.name().to_string()).into()) - } else { - Ok(status) + match Toolchain::exists(self.cfg(), &self.local_name())? { + true => Ok(status), + false => Err(RustupError::ToolchainNotInstallable(self.dest_basename()).into()), } } - pub(crate) fn run( - self, - path: &Path, - notify_handler: &dyn Fn(Notification<'_>), - ) -> Result { + fn run(&self, path: &Path, notify_handler: &dyn Fn(Notification<'_>)) -> Result { if path.exists() { // Don't uninstall first for Dist method match self { @@ -99,11 +117,11 @@ impl<'a> InstallMethod<'a> { } match self { - InstallMethod::Copy(src, ..) => { + InstallMethod::Copy { src, .. } => { utils::copy_dir(src, path, notify_handler)?; Ok(true) } - InstallMethod::Link(src, ..) => { + InstallMethod::Link { src, .. } => { utils::symlink_dir(src, path, notify_handler)?; Ok(true) } @@ -112,24 +130,24 @@ impl<'a> InstallMethod<'a> { profile, update_hash, dl_cfg, - force_update, + force: force_update, allow_downgrade, exists, - old_date, + old_date_version, components, targets, .. } => { let prefix = &InstallPrefix::from(path.to_owned()); let maybe_new_hash = dist::update_from_dist( - dl_cfg, - update_hash, + *dl_cfg, + update_hash.as_deref(), desc, - if exists { None } else { Some(profile) }, + if *exists { None } else { Some(*profile) }, prefix, - force_update, - allow_downgrade, - old_date, + *force_update, + *allow_downgrade, + old_date_version.as_ref().map(|dv| dv.0.as_str()), components, targets, )?; @@ -146,6 +164,34 @@ impl<'a> InstallMethod<'a> { } } } + + fn cfg(&self) -> &Cfg { + match self { + InstallMethod::Copy { cfg, .. } => cfg, + InstallMethod::Link { cfg, .. } => cfg, + InstallMethod::Dist { cfg, .. } => cfg, + } + } + + fn local_name(&self) -> LocalToolchainName { + match self { + InstallMethod::Copy { dest, .. } => (*dest).into(), + InstallMethod::Link { dest, .. } => (*dest).into(), + InstallMethod::Dist { desc, .. } => (*desc).into(), + } + } + + fn dest_basename(&self) -> String { + self.local_name().to_string() + } + + fn dest_path(&self) -> PathBuf { + match self { + InstallMethod::Copy { cfg, dest, .. } => cfg.toolchain_path(&(*dest).into()), + InstallMethod::Link { cfg, dest, .. } => cfg.toolchain_path(&(*dest).into()), + InstallMethod::Dist { cfg, desc, .. } => cfg.toolchain_path(&(*desc).into()), + } + } } pub(crate) fn uninstall(path: &Path, notify_handler: &dyn Fn(Notification<'_>)) -> Result<()> { diff --git a/src/lib.rs b/src/lib.rs index ffe0d8da51e..2305183470c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,14 +4,15 @@ clippy::type_complexity, clippy::upper_case_acronyms, // see https://github.com/rust-lang/rust-clippy/issues/6974 clippy::vec_init_then_push, // uses two different styles of initialization + clippy::box_default, // its ugly and outside of inner loops irrelevant + clippy::result_large_err, // 288 bytes is our 'large' variant today, which is unlikely to be a performance problem )] #![recursion_limit = "1024"] -pub use crate::config::*; +pub(crate) use crate::config::*; use crate::currentprocess::*; pub use crate::errors::*; -pub use crate::notifications::*; -use crate::toolchain::*; +pub(crate) use crate::notifications::*; pub(crate) use crate::utils::toml_utils; use anyhow::{anyhow, Result}; @@ -94,6 +95,7 @@ mod fallback_settings; mod install; pub mod notifications; mod settings; +#[cfg(feature = "test")] pub mod test; mod toolchain; pub mod utils; diff --git a/src/notifications.rs b/src/notifications.rs index 4420bd4e427..565ef92cd49 100644 --- a/src/notifications.rs +++ b/src/notifications.rs @@ -1,36 +1,36 @@ use std::fmt::{self, Display}; use std::path::{Path, PathBuf}; -use crate::dist::temp; -use crate::utils::notify::NotificationLevel; +use crate::{ + dist::{dist::ToolchainDesc, temp}, + toolchain::names::ToolchainName, + utils::notify::NotificationLevel, +}; #[derive(Debug)] -pub enum Notification<'a> { +pub(crate) enum Notification<'a> { Install(crate::dist::Notification<'a>), Utils(crate::utils::Notification<'a>), Temp(temp::Notification<'a>), - SetDefaultToolchain(&'a str), + SetDefaultToolchain(Option<&'a ToolchainName>), SetOverrideToolchain(&'a Path, &'a str), SetProfile(&'a str), SetSelfUpdate(&'a str), - LookingForToolchain(&'a str), + LookingForToolchain(&'a ToolchainDesc), ToolchainDirectory(&'a Path, &'a str), UpdatingToolchain(&'a str), InstallingToolchain(&'a str), InstalledToolchain(&'a str), - UsingExistingToolchain(&'a str), - UninstallingToolchain(&'a str), - UninstalledToolchain(&'a str), - ToolchainNotInstalled(&'a str), + UsingExistingToolchain(&'a ToolchainDesc), + UninstallingToolchain(&'a ToolchainName), + UninstalledToolchain(&'a ToolchainName), UpdateHashMatches, UpgradingMetadata(&'a str, &'a str), MetadataUpgradeNotNeeded(&'a str), - WritingMetadataVersion(&'a str), ReadMetadataVersion(&'a str), NonFatalError(&'a anyhow::Error), UpgradeRemovesToolchains, - MissingFileDuringSelfUninstall(PathBuf), PlainVerboseMessage(&'a str), /// Both `rust-toolchain` and `rust-toolchain.toml` exist within a directory DuplicateToolchainFile { @@ -64,7 +64,6 @@ impl<'a> Notification<'a> { Temp(n) => n.level(), ToolchainDirectory(_, _) | LookingForToolchain(_) - | WritingMetadataVersion(_) | InstallingToolchain(_) | UpdatingToolchain(_) | ReadMetadataVersion(_) @@ -78,13 +77,10 @@ impl<'a> Notification<'a> { | UsingExistingToolchain(_) | UninstallingToolchain(_) | UninstalledToolchain(_) - | ToolchainNotInstalled(_) | UpgradingMetadata(_, _) | MetadataUpgradeNotNeeded(_) => NotificationLevel::Info, NonFatalError(_) => NotificationLevel::Error, - UpgradeRemovesToolchains - | MissingFileDuringSelfUninstall(_) - | DuplicateToolchainFile { .. } => NotificationLevel::Warn, + UpgradeRemovesToolchains | DuplicateToolchainFile { .. } => NotificationLevel::Warn, } } } @@ -96,8 +92,8 @@ impl<'a> Display for Notification<'a> { Install(n) => n.fmt(f), Utils(n) => n.fmt(f), Temp(n) => n.fmt(f), - SetDefaultToolchain("none") => write!(f, "default toolchain unset"), - SetDefaultToolchain(name) => write!(f, "default toolchain set to '{name}'"), + SetDefaultToolchain(None) => write!(f, "default toolchain unset"), + SetDefaultToolchain(Some(name)) => write!(f, "default toolchain set to '{name}'"), SetOverrideToolchain(path, name) => write!( f, "override toolchain for '{}' set to '{}'", @@ -114,7 +110,6 @@ impl<'a> Display for Notification<'a> { UsingExistingToolchain(name) => write!(f, "using existing install for '{name}'"), UninstallingToolchain(name) => write!(f, "uninstalling toolchain '{name}'"), UninstalledToolchain(name) => write!(f, "toolchain '{name}' uninstalled"), - ToolchainNotInstalled(name) => write!(f, "no toolchain installed for '{name}'"), UpdateHashMatches => write!(f, "toolchain is already up to date"), UpgradingMetadata(from_ver, to_ver) => write!( f, @@ -123,18 +118,12 @@ impl<'a> Display for Notification<'a> { MetadataUpgradeNotNeeded(ver) => { write!(f, "nothing to upgrade: metadata version is already '{ver}'") } - WritingMetadataVersion(ver) => write!(f, "writing metadata version: '{ver}'"), ReadMetadataVersion(ver) => write!(f, "read metadata version: '{ver}'"), NonFatalError(e) => write!(f, "{e}"), UpgradeRemovesToolchains => write!( f, "this upgrade will remove all existing toolchains. you will need to reinstall them" ), - MissingFileDuringSelfUninstall(p) => write!( - f, - "expected file does not exist to uninstall: {}", - p.display() - ), PlainVerboseMessage(r) => write!(f, "{r}"), DuplicateToolchainFile { rust_toolchain, diff --git a/src/test.rs b/src/test.rs index 6914a3ff7c2..2dd3626f671 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,5 +1,8 @@ #![allow(clippy::box_default)] //! Test support module; public to permit use from integration tests. + +pub mod mock; + use std::collections::HashMap; use std::env; use std::ffi::OsStr; diff --git a/tests/mock/clitools.rs b/src/test/mock/clitools.rs similarity index 96% rename from tests/mock/clitools.rs rename to src/test/mock/clitools.rs index 64aa9dfaf9d..3d879729e5a 100644 --- a/tests/mock/clitools.rs +++ b/src/test/mock/clitools.rs @@ -1,35 +1,38 @@ //! A mock distribution server used by tests/cli-v1.rs and //! tests/cli-v2.rs -use std::cell::RefCell; -use std::collections::HashMap; -use std::env; -use std::env::consts::EXE_SUFFIX; -use std::ffi::OsStr; -use std::fs; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::sync::{Arc, RwLock, RwLockWriteGuard}; -use std::time::Instant; +use std::{ + cell::RefCell, + collections::HashMap, + env::{self, consts::EXE_SUFFIX}, + ffi::OsStr, + fmt::Debug, + fs, + io::{self, Write}, + path::{Path, PathBuf}, + process::Command, + sync::{Arc, RwLock, RwLockWriteGuard}, + time::Instant, +}; use enum_map::{enum_map, Enum, EnumMap}; use lazy_static::lazy_static; use once_cell::sync::Lazy; -use rustup::test::const_dist_dir; use url::Url; -use rustup::cli::rustup_mode; -use rustup::currentprocess; -use rustup::test as rustup_test; -use rustup::test::this_host_triple; -use rustup::utils::{raw, utils}; - -use crate::mock::dist::{ - change_channel_date, ManifestVersion, MockChannel, MockComponent, MockDistServer, MockPackage, - MockTargetedPackage, +use crate::cli::rustup_mode; +use crate::currentprocess; +use crate::test as rustup_test; +use crate::test::const_dist_dir; +use crate::test::this_host_triple; +use crate::utils::{raw, utils}; + +use super::{ + dist::{ + change_channel_date, ManifestVersion, MockChannel, MockComponent, MockDistServer, + MockPackage, MockTargetedPackage, + }, + topical_doc_data, MockComponentBuilder, MockFile, MockInstallerBuilder, }; -use crate::mock::topical_doc_data; -use crate::mock::{MockComponentBuilder, MockFile, MockInstallerBuilder}; /// The configuration used by the tests in this module #[derive(Debug)] @@ -169,7 +172,7 @@ impl ConstState { Some(ref path) => Ok(path.clone()), None => { - let dist_path = self.const_dist_dir.path().join(format!("{:?}", s)); + let dist_path = self.const_dist_dir.path().join(format!("{s:?}")); create_mock_dist_server(&dist_path, s); *lock = Some(dist_path.clone()); Ok(dist_path) @@ -257,7 +260,7 @@ pub fn setup_test_state(test_dist_dir: tempfile::TempDir) -> (tempfile::TempDir, fn link_or_copy( original: &Path, link: &Path, - lock: &mut RwLockWriteGuard, + lock: &mut RwLockWriteGuard<'_, usize>, ) -> io::Result<()> { **lock += 1; if **lock < MAX_TESTS_PER_RUSTUP_EXE { @@ -285,10 +288,10 @@ pub fn setup_test_state(test_dist_dir: tempfile::TempDir) -> (tempfile::TempDir, // Make sure the host triple matches the build triple. Otherwise testing a 32-bit build of // rustup on a 64-bit machine will fail, because the tests do not have the host detection // functionality built in. - config.run("rustup", &["set", "default-host", &this_host_triple()], &[]); + config.run("rustup", ["set", "default-host", &this_host_triple()], &[]); // Set the auto update mode to disable, as most tests do not want to update rustup itself during the test. - config.run("rustup", &["set", "auto-self-update", "disable"], &[]); + config.run("rustup", ["set", "auto-self-update", "disable"], &[]); // Create some custom toolchains create_custom_toolchains(&config.customdir); @@ -302,7 +305,7 @@ pub fn test(s: Scenario, f: &dyn Fn(&mut Config)) { // Things we might cache or what not // Mutable dist server - working toward elimination - let test_dist_dir = rustup::test::test_dist_dir().unwrap(); + let test_dist_dir = crate::test::test_dist_dir().unwrap(); create_mock_dist_server(test_dist_dir.path(), s); // Things that are just about the test itself @@ -509,6 +512,8 @@ impl Config { } } + /// Expect an ok status + #[track_caller] pub fn expect_ok(&mut self, args: &[&str]) { let out = self.run(args[0], &args[1..], &[]); if !out.ok { @@ -518,6 +523,8 @@ impl Config { } } + /// Expect an err status and a string in stderr + #[track_caller] pub fn expect_err(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]); if out.ok || !out.stderr.contains(expected) { @@ -528,6 +535,8 @@ impl Config { } } + /// Expect an ok status and a string in stdout + #[track_caller] pub fn expect_stdout_ok(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]); if !out.ok || !out.stdout.contains(expected) { @@ -538,6 +547,7 @@ impl Config { } } + #[track_caller] pub fn expect_not_stdout_ok(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]); if !out.ok || out.stdout.contains(expected) { @@ -548,6 +558,7 @@ impl Config { } } + #[track_caller] pub fn expect_not_stderr_ok(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]); if !out.ok || out.stderr.contains(expected) { @@ -558,6 +569,7 @@ impl Config { } } + #[track_caller] pub fn expect_not_stderr_err(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]); if out.ok || out.stderr.contains(expected) { @@ -568,6 +580,8 @@ impl Config { } } + /// Expect an ok status and a string in stderr + #[track_caller] pub fn expect_stderr_ok(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]); if !out.ok || !out.stderr.contains(expected) { @@ -578,6 +592,8 @@ impl Config { } } + /// Expect an exact strings on stdout/stderr with an ok status code + #[track_caller] pub fn expect_ok_ex(&mut self, args: &[&str], stdout: &str, stderr: &str) { let out = self.run(args[0], &args[1..], &[]); if !out.ok || out.stdout != stdout || out.stderr != stderr { @@ -591,6 +607,8 @@ impl Config { } } + /// Expect an exact strings on stdout/stderr with an error status code + #[track_caller] pub fn expect_err_ex(&self, args: &[&str], stdout: &str, stderr: &str) { let out = self.run(args[0], &args[1..], &[]); if out.ok || out.stdout != stdout || out.stderr != stderr { @@ -610,6 +628,7 @@ impl Config { } } + #[track_caller] pub fn expect_ok_contains(&self, args: &[&str], stdout: &str, stderr: &str) { let out = self.run(args[0], &args[1..], &[]); if !out.ok || !out.stdout.contains(stdout) || !out.stderr.contains(stderr) { @@ -621,6 +640,7 @@ impl Config { } } + #[track_caller] pub fn expect_ok_eq(&self, args1: &[&str], args2: &[&str]) { let out1 = self.run(args1[0], &args1[1..], &[]); let out2 = self.run(args2[0], &args2[1..], &[]); @@ -633,8 +653,9 @@ impl Config { } } + #[track_caller] pub fn expect_component_executable(&self, cmd: &str) { - let out1 = self.run(cmd, &["--version"], &[]); + let out1 = self.run(cmd, ["--version"], &[]); if !out1.ok { print_command(&[cmd, "--version"], &out1); println!("expected.ok: true"); @@ -642,8 +663,9 @@ impl Config { } } + #[track_caller] pub fn expect_component_not_executable(&self, cmd: &str) { - let out1 = self.run(cmd, &["--version"], &[]); + let out1 = self.run(cmd, ["--version"], &[]); if out1.ok { print_command(&[cmd, "--version"], &out1); println!("expected.ok: false"); @@ -653,15 +675,15 @@ impl Config { pub fn run(&self, name: &str, args: I, env: &[(&str, &str)]) -> SanitizedOutput where - I: IntoIterator + Clone, + I: IntoIterator + Clone + Debug, A: AsRef, { let inprocess = allow_inprocess(name, args.clone()); let start = Instant::now(); let out = if inprocess { - self.run_inprocess(name, args, env) + self.run_inprocess(name, args.clone(), env) } else { - self.run_subprocess(name, args, env) + self.run_subprocess(name, args.clone(), env) }; let duration = Instant::now() - start; let output = SanitizedOutput { @@ -670,6 +692,7 @@ impl Config { stderr: String::from_utf8(out.stderr).unwrap(), }; + println!("ran: {} {:?}", name, args); println!("inprocess: {inprocess}"); println!("status: {:?}", out.status); println!("duration: {:.3}s", duration.as_secs_f32()); @@ -712,7 +735,7 @@ impl Config { let ec = match process_res { Ok(process_res) => process_res, Err(e) => { - currentprocess::with(tp.clone(), || rustup::cli::common::report_error(&e)); + currentprocess::with(tp.clone(), || crate::cli::common::report_error(&e)); utils::ExitCode(1) } }; @@ -723,6 +746,7 @@ impl Config { } } + #[track_caller] pub fn run_subprocess(&self, name: &str, args: I, env: &[(&str, &str)]) -> Output where I: IntoIterator, @@ -770,7 +794,7 @@ pub fn set_current_dist_date(config: &Config, date: &str) { } } -pub(crate) fn print_command(args: &[&str], out: &SanitizedOutput) { +pub fn print_command(args: &[&str], out: &SanitizedOutput) { print!("\n>"); for arg in args { if arg.contains(' ') { @@ -785,7 +809,7 @@ pub(crate) fn print_command(args: &[&str], out: &SanitizedOutput) { print_indented("out.stderr", &out.stderr); } -pub(crate) fn print_indented(heading: &str, text: &str) { +pub fn print_indented(heading: &str, text: &str) { let mut lines = text.lines().count(); // The standard library treats `a\n` and `a` as both being one line. // This is confusing when the test fails because of a missing newline. diff --git a/tests/mock/dist.rs b/src/test/mock/dist.rs similarity index 99% rename from tests/mock/dist.rs rename to src/test/mock/dist.rs index 1be0807a8f9..0b42a9ceb62 100644 --- a/tests/mock/dist.rs +++ b/src/test/mock/dist.rs @@ -1,7 +1,6 @@ //! Tools for building and working with the filesystem of a mock Rust //! distribution server, with v1 and v2 manifests. -use crate::mock::MockInstallerBuilder; use lazy_static::lazy_static; use sha2::{Digest, Sha256}; use std::collections::HashMap; @@ -11,7 +10,8 @@ use std::path::{Path, PathBuf}; use std::sync::Mutex; use url::Url; -use crate::mock::clitools::hard_link; +use super::clitools::hard_link; +use super::MockInstallerBuilder; // This function changes the mock manifest for a given channel to that // of a particular date. For advancing the build from e.g. 2016-02-1 diff --git a/tests/mock/mock_bin_src.rs b/src/test/mock/mock_bin_src.rs similarity index 100% rename from tests/mock/mock_bin_src.rs rename to src/test/mock/mock_bin_src.rs diff --git a/tests/mock/mod.rs b/src/test/mock/mod.rs similarity index 98% rename from tests/mock/mod.rs rename to src/test/mock/mod.rs index f636f90d793..aee84232a4a 100644 --- a/tests/mock/mod.rs +++ b/src/test/mock/mod.rs @@ -40,7 +40,7 @@ struct MockContents { } impl std::fmt::Debug for MockContents { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MockContents") .field("content_len", &self.contents.len()) .field("executable", &self.executable) diff --git a/tests/mock/topical_doc_data.rs b/src/test/mock/topical_doc_data.rs similarity index 100% rename from tests/mock/topical_doc_data.rs rename to src/test/mock/topical_doc_data.rs diff --git a/src/toolchain.rs b/src/toolchain.rs index b88eb4cf6c8..637f2cca4bf 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -1,1004 +1,5 @@ -use std::env::consts::EXE_SUFFIX; -use std::ffi::OsStr; -use std::ffi::OsString; -use std::io::{BufRead, BufReader}; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::str::FromStr; -use std::time::Duration; -use std::{env, fmt::Debug}; - -use anyhow::{anyhow, bail, Context, Result}; -use thiserror::Error as ThisError; -use wait_timeout::ChildExt; - -use crate::component_for_bin; -use crate::config::Cfg; -use crate::dist::dist::Profile; -use crate::dist::dist::TargetTriple; -use crate::dist::dist::ToolchainDesc; -use crate::dist::download::DownloadCfg; -use crate::dist::manifest::Component; -use crate::dist::manifest::Manifest; -use crate::dist::manifestation::{Changes, Manifestation}; -use crate::dist::prefix::InstallPrefix; -use crate::env_var; -use crate::errors::*; -use crate::install::{self, InstallMethod}; -use crate::notifications::*; -use crate::process; -use crate::utils::utils; - -/// An installed toolchain -trait InstalledToolchain<'a> { - /// What (root) paths are associated with this installed toolchain. - fn installed_paths(&self) -> Result>>; -} - -/// Installed paths -enum InstalledPath<'a> { - File { name: &'static str, path: PathBuf }, - Dir { path: &'a Path }, -} - -/// A fully resolved reference to a toolchain which may or may not exist -pub struct Toolchain<'a> { - cfg: &'a Cfg, - name: String, - path: PathBuf, - dist_handler: Box) + 'a>, -} - -/// Used by the `list_component` function -pub struct ComponentStatus { - pub component: Component, - pub name: String, - pub installed: bool, - pub available: bool, -} - -#[derive(Clone, Debug)] -pub enum UpdateStatus { - Installed, - Updated(String), // Stores the version of rustc *before* the update - Unchanged, -} - -static V1_COMMON_COMPONENT_LIST: &[&str] = &["cargo", "rustc", "rust-docs"]; - -impl<'a> Toolchain<'a> { - pub(crate) fn from(cfg: &'a Cfg, name: &str) -> Result { - let resolved_name = cfg.resolve_toolchain(name)?; - let path = cfg.toolchains_dir.join(&resolved_name); - Ok(Toolchain { - cfg, - name: resolved_name, - path, - dist_handler: Box::new(move |n| (cfg.notify_handler)(n.into())), - }) - } - - pub(crate) fn from_path( - cfg: &'a Cfg, - cfg_file: Option>, - path: impl AsRef, - ) -> Result { - let path = if let Some(cfg_file) = cfg_file { - cfg_file.as_ref().parent().unwrap().join(path) - } else { - path.as_ref().to_path_buf() - }; - - #[derive(Debug, ThisError)] - #[error("invalid toolchain path: '{}'", .0.to_string_lossy())] - struct InvalidToolchainPath(PathBuf); - - // Perform minimal validation; there should at least be a `bin/` that might - // contain things for us to run. - if !path.join("bin").is_dir() { - bail!(InvalidToolchainPath(path)); - } - - Ok(Toolchain { - cfg, - name: utils::canonicalize_path(&path, cfg.notify_handler.as_ref()) - .to_str() - .ok_or_else(|| anyhow!(InvalidToolchainPath(path.clone())))? - .to_owned(), - path, - dist_handler: Box::new(move |n| (cfg.notify_handler)(n.into())), - }) - } - - pub fn as_installed_common(&'a self) -> Result> { - if !self.exists() { - // Should be verify perhaps? - return Err(RustupError::ToolchainNotInstalled(self.name.to_owned()).into()); - } - Ok(InstalledCommonToolchain(self)) - } - - fn as_installed(&'a self) -> Result + 'a>> { - if self.is_custom() { - let toolchain = CustomToolchain::new(self)?; - Ok(Box::new(toolchain) as Box>) - } else { - let toolchain = DistributableToolchain::new(self)?; - Ok(Box::new(toolchain) as Box>) - } - } - pub(crate) fn cfg(&self) -> &Cfg { - self.cfg - } - pub fn name(&self) -> &str { - &self.name - } - pub fn path(&self) -> &Path { - &self.path - } - fn is_symlink(&self) -> bool { - use std::fs; - fs::symlink_metadata(&self.path) - .map(|m| m.file_type().is_symlink()) - .unwrap_or(false) - } - /// Is there a filesystem component with the name of the toolchain in the toolchains dir - valid install or not. - /// Used to determine whether this toolchain should be uninstallable. - /// Custom and Distributable. Installed and uninstalled. (perhaps onstalled only?) - pub fn exists(&self) -> bool { - // HACK: linked toolchains are symlinks, and, contrary to what std docs - // lead me to believe `fs::metadata`, used by `is_directory` does not - // seem to follow symlinks on windows. - let is_symlink = if cfg!(windows) { - self.is_symlink() - } else { - false - }; - utils::is_directory(&self.path) || is_symlink - } - /// Is there a valid usable toolchain with this name, either in the toolchains dir, or symlinked from it. - // Could in future check for rustc perhaps. - // Custom and Distributable. Installed only? - pub fn verify(&self) -> Result<()> { - utils::assert_is_directory(&self.path) - } - // Custom and Distributable. Installed only. - pub fn remove(&self) -> Result<()> { - if self.exists() || self.is_symlink() { - (self.cfg.notify_handler)(Notification::UninstallingToolchain(&self.name)); - } else { - (self.cfg.notify_handler)(Notification::ToolchainNotInstalled(&self.name)); - return Ok(()); - } - let installed = self.as_installed()?; - for path in installed.installed_paths()? { - match path { - InstalledPath::File { name, path } => utils::ensure_file_removed(name, &path)?, - InstalledPath::Dir { path } => { - install::uninstall(path, &|n| (self.cfg.notify_handler)(n.into()))? - } - } - } - if !self.exists() { - (self.cfg.notify_handler)(Notification::UninstalledToolchain(&self.name)); - } - Ok(()) - } - - // Custom only - pub fn is_custom(&self) -> bool { - Toolchain::is_custom_name(&self.name) - } - - pub(crate) fn is_custom_name(name: &str) -> bool { - ToolchainDesc::from_str(name).is_err() - } - - // Distributable only - pub fn is_tracking(&self) -> bool { - ToolchainDesc::from_str(&self.name) - .ok() - .map(|d| d.is_tracking()) - == Some(true) - } - - // Custom and Distributable. Installed only. - pub fn doc_path(&self, relative: &str) -> Result { - self.verify()?; - - let parts = vec!["share", "doc", "rust", "html"]; - let mut doc_dir = self.path.clone(); - for part in parts { - doc_dir.push(part); - } - doc_dir.push(relative); - - Ok(doc_dir) - } - // Custom and Distributable. Installed only. - pub fn open_docs(&self, relative: &str) -> Result<()> { - self.verify()?; - - utils::open_browser(&self.doc_path(relative)?) - } - // Custom and Distributable. Installed only. - pub fn make_default(&self) -> Result<()> { - self.cfg.set_default(&self.name) - } - // Custom and Distributable. Installed only. - pub fn make_override(&self, path: &Path) -> Result<()> { - self.cfg.settings_file.with_mut(|s| { - s.add_override(path, self.name.clone(), self.cfg.notify_handler.as_ref()); - Ok(()) - }) - } - // Distributable and Custom. Installed only. - pub fn binary_file(&self, name: &str) -> PathBuf { - let mut path = self.path.clone(); - path.push("bin"); - path.push(name.to_owned() + env::consts::EXE_SUFFIX); - path - } - // Distributable and Custom. Installed only. - #[cfg_attr(feature = "otel", tracing::instrument)] - pub fn rustc_version(&self) -> String { - if let Ok(installed) = self.as_installed_common() { - let rustc_path = self.binary_file("rustc"); - if utils::is_file(&rustc_path) { - let mut cmd = Command::new(&rustc_path); - cmd.arg("--version"); - cmd.stdin(Stdio::null()); - cmd.stdout(Stdio::piped()); - cmd.stderr(Stdio::piped()); - installed.set_ldpath(&mut cmd); - - // some toolchains are faulty with some combinations of platforms and - // may fail to launch but also to timely terminate. - // (known cases include Rust 1.3.0 through 1.10.0 in recent macOS Sierra.) - // we guard against such cases by enforcing a reasonable timeout to read. - let mut line1 = None; - if let Ok(mut child) = cmd.spawn() { - let timeout = Duration::new(10, 0); - match child.wait_timeout(timeout) { - Ok(Some(status)) if status.success() => { - let out = child - .stdout - .expect("Child::stdout requested but not present"); - let mut line = String::new(); - if BufReader::new(out).read_line(&mut line).is_ok() { - let lineend = line.trim_end_matches(&['\r', '\n'][..]).len(); - line.truncate(lineend); - line1 = Some(line); - } - } - Ok(None) => { - let _ = child.kill(); - return String::from("(timeout reading rustc version)"); - } - Ok(Some(_)) | Err(_) => {} - } - } - - if let Some(line1) = line1 { - line1 - } else { - String::from("(error reading rustc version)") - } - } else { - String::from("(rustc does not exist)") - } - } else { - String::from("(toolchain not installed)") - } - } -} - -impl<'a> std::fmt::Debug for Toolchain<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Toolchain") - .field("name", &self.name) - .field("path", &self.path) - .finish() - } -} - -fn install_msg(bin: &str, toolchain: &str, is_default: bool) -> String { - if Toolchain::is_custom_name(toolchain) { - return "\nnote: this is a custom toolchain, which cannot use `rustup component add`\n\ - help: if you built this toolchain from source, and used `rustup toolchain link`, then you may be able to build the component with `x.py`".to_string(); - } - match component_for_bin(bin) { - Some(c) => format!("\nTo install, run `rustup component add {}{}`", c, { - if is_default { - String::new() - } else { - format!(" --toolchain {toolchain}") - } - }), - None => String::new(), - } -} -/// Newtype hosting functions that apply to both custom and distributable toolchains that are installed. -pub struct InstalledCommonToolchain<'a>(&'a Toolchain<'a>); - -impl<'a> InstalledCommonToolchain<'a> { - pub fn create_command>(&self, binary: T) -> Result { - // Create the path to this binary within the current toolchain sysroot - let binary = if let Some(binary_str) = binary.as_ref().to_str() { - if binary_str.to_lowercase().ends_with(EXE_SUFFIX) { - binary.as_ref().to_owned() - } else { - OsString::from(format!("{binary_str}{EXE_SUFFIX}")) - } - } else { - // Very weird case. Non-unicode command. - binary.as_ref().to_owned() - }; - - let bin_path = self.0.path.join("bin").join(&binary); - let path = if utils::is_file(&bin_path) { - &bin_path - } else { - let recursion_count = process() - .var("RUST_RECURSION_COUNT") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - if recursion_count > env_var::RUST_RECURSION_COUNT_MAX - 1 { - let binary_lossy: String = binary.to_string_lossy().into(); - if let Ok(distributable) = DistributableToolchain::new(self.0) { - if let (Some(component_name), Ok(component_statuses), Ok(Some(manifest))) = ( - component_for_bin(&binary_lossy), - distributable.list_components(), - distributable.get_manifest(), - ) { - let component_status = component_statuses - .iter() - .find(|cs| cs.component.short_name(&manifest) == component_name) - .unwrap_or_else(|| { - panic!("component {component_name} should be in the manifest") - }); - if !component_status.available { - return Err(anyhow!(format!( - "the '{}' component which provides the command '{}' is not available for the '{}' toolchain", component_status.component.short_name(&manifest), binary_lossy, self.0.name))); - } - if component_status.installed { - return Err(anyhow!(format!( - "the '{}' binary, normally provided by the '{}' component, is not applicable to the '{}' toolchain", binary_lossy, component_status.component.short_name(&manifest), self.0.name))); - } - } - } - let defaults = self.0.cfg.get_default()?; - return Err(anyhow!(format!( - "'{}' is not installed for the toolchain '{}'{}", - binary.to_string_lossy(), - self.0.name, - install_msg( - &binary.to_string_lossy(), - &self.0.name, - Some(&self.0.name) == defaults.as_ref() - ) - ))); - } - Path::new(&binary) - }; - let mut cmd = Command::new(path); - self.set_env(&mut cmd); - Ok(cmd) - } - - fn set_env(&self, cmd: &mut Command) { - self.set_ldpath(cmd); - - // Older versions of Cargo used a slightly different definition of - // cargo home. Rustup does not read HOME on Windows whereas the older - // versions of Cargo did. Rustup and Cargo should be in sync now (both - // using the same `home` crate), but this is retained to ensure cargo - // and rustup agree in older versions. - if let Ok(cargo_home) = utils::cargo_home() { - cmd.env("CARGO_HOME", &cargo_home); - } - - env_var::inc("RUST_RECURSION_COUNT", cmd); - - cmd.env("RUSTUP_TOOLCHAIN", &self.0.name); - cmd.env("RUSTUP_HOME", &self.0.cfg.rustup_dir); - } - - fn set_ldpath(&self, cmd: &mut Command) { - let mut new_path = vec![self.0.path.join("lib")]; - - #[cfg(not(target_os = "macos"))] - mod sysenv { - pub const LOADER_PATH: &str = "LD_LIBRARY_PATH"; - } - #[cfg(target_os = "macos")] - mod sysenv { - // When loading and linking a dynamic library or bundle, dlopen - // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and - // DYLD_FALLBACK_LIBRARY_PATH. - // In the Mach-O format, a dynamic library has an "install path." - // Clients linking against the library record this path, and the - // dynamic linker, dyld, uses it to locate the library. - // dyld searches DYLD_LIBRARY_PATH *before* the install path. - // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot - // find the library in the install path. - // Setting DYLD_LIBRARY_PATH can easily have unintended - // consequences. - pub const LOADER_PATH: &str = "DYLD_FALLBACK_LIBRARY_PATH"; - } - if cfg!(target_os = "macos") - && process() - .var_os(sysenv::LOADER_PATH) - .filter(|x| x.len() > 0) - .is_none() - { - // These are the defaults when DYLD_FALLBACK_LIBRARY_PATH isn't - // set or set to an empty string. Since we are explicitly setting - // the value, make sure the defaults still work. - if let Some(home) = process().var_os("HOME") { - new_path.push(PathBuf::from(home).join("lib")); - } - new_path.push(PathBuf::from("/usr/local/lib")); - new_path.push(PathBuf::from("/usr/lib")); - } - - env_var::prepend_path(sysenv::LOADER_PATH, new_path, cmd); - - // Prepend CARGO_HOME/bin to the PATH variable so that we're sure to run - // cargo/rustc via the proxy bins. There is no fallback case for if the - // proxy bins don't exist. We'll just be running whatever happens to - // be on the PATH. - let mut path_entries = vec![]; - if let Ok(cargo_home) = utils::cargo_home() { - path_entries.push(cargo_home.join("bin")); - } - - if cfg!(target_os = "windows") { - // Historically rustup has included the bin directory in PATH to - // work around some bugs (see - // https://github.com/rust-lang/rustup/pull/3178 for more - // information). This shouldn't be needed anymore, and it causes - // problems because calling tools recursively (like `cargo - // +nightly metadata` from within a cargo subcommand). The - // recursive call won't work because it is not executing the - // proxy, so the `+` toolchain override doesn't work. - // - // This is opt-in to allow us to get more real-world testing. - if process() - .var_os("RUSTUP_WINDOWS_PATH_ADD_BIN") - .map_or(true, |s| s == "1") - { - path_entries.push(self.0.path.join("bin")); - } - } - - env_var::prepend_path("PATH", path_entries, cmd); - } -} - -/// Newtype to facilitate splitting out custom-toolchain specific code. -pub struct CustomToolchain<'a>(&'a Toolchain<'a>); - -impl<'a> CustomToolchain<'a> { - pub fn new(toolchain: &'a Toolchain<'a>) -> Result> { - if toolchain.is_custom() { - Ok(CustomToolchain(toolchain)) - } else { - Err(anyhow!(format!( - "{} is not a custom toolchain", - toolchain.name() - ))) - } - } - - // Not installed only. - pub fn install_from_dir(&self, src: &Path, link: bool) -> Result<()> { - let mut pathbuf = PathBuf::from(src); - - pathbuf.push("lib"); - utils::assert_is_directory(&pathbuf)?; - pathbuf.pop(); - pathbuf.push("bin"); - utils::assert_is_directory(&pathbuf)?; - pathbuf.push(format!("rustc{EXE_SUFFIX}")); - utils::assert_is_file(&pathbuf)?; - - if link { - InstallMethod::Link(&utils::to_absolute(src)?, self).install(self.0)?; - } else { - InstallMethod::Copy(src, self).install(self.0)?; - } - - Ok(()) - } -} - -impl<'a> InstalledToolchain<'a> for CustomToolchain<'a> { - fn installed_paths(&self) -> Result>> { - let path = &self.0.path; - Ok(vec![InstalledPath::Dir { path }]) - } -} - -/// Newtype to facilitate splitting out distributable-toolchain specific code. -#[derive(Debug)] -pub struct DistributableToolchain<'a>(&'a Toolchain<'a>); - -impl<'a> DistributableToolchain<'a> { - pub fn new(toolchain: &'a Toolchain<'a>) -> Result> { - if toolchain.is_custom() { - Err(anyhow!(format!( - "{} is a custom toolchain", - toolchain.name() - ))) - } else { - Ok(DistributableToolchain(toolchain)) - } - } - - /// Temporary helper until we further split this into a newtype for - /// InstalledDistributableToolchain - one where the type can protect component operations. - pub fn new_for_components(toolchain: &'a Toolchain<'a>) -> Result> { - DistributableToolchain::new(toolchain).context(RustupError::ComponentsUnsupported( - toolchain.name().to_string(), - )) - } - - // Installed only. - pub(crate) fn add_component(&self, mut component: Component) -> Result<()> { - if let Some(desc) = self.get_toolchain_desc_with_manifest()? { - // Rename the component if necessary. - if let Some(c) = desc.manifest.rename_component(&component) { - component = c; - } - - // Validate the component name - let rust_pkg = desc - .manifest - .packages - .get("rust") - .expect("manifest should contain a rust package"); - let targ_pkg = rust_pkg - .targets - .get(&desc.toolchain.target) - .expect("installed manifest should have a known target"); - - if !targ_pkg.components.contains(&component) { - let wildcard_component = component.wildcard(); - if targ_pkg.components.contains(&wildcard_component) { - component = wildcard_component; - } else { - return Err(RustupError::UnknownComponent { - name: self.0.name.to_string(), - component: component.description(&desc.manifest), - suggestion: self.get_component_suggestion( - &component, - &desc.manifest, - false, - ), - } - .into()); - } - } - - let changes = Changes { - explicit_add_components: vec![component], - remove_components: vec![], - }; - - desc.manifestation.update( - &desc.manifest, - changes, - false, - &self.download_cfg(), - &self.download_cfg().notify_handler, - &desc.toolchain.manifest_name(), - false, - )?; - - Ok(()) - } else { - Err(RustupError::MissingManifest { - name: self.0.name.to_string(), - } - .into()) - } - } - - // Create a command as a fallback for another toolchain. This is used - // to give custom toolchains access to cargo - // Installed only. - pub fn create_fallback_command>( - &self, - binary: T, - primary_toolchain: &Toolchain<'_>, - ) -> Result { - // With the hacks below this only works for cargo atm - assert!(binary.as_ref() == "cargo" || binary.as_ref() == "cargo.exe"); - - if !self.0.exists() { - return Err(RustupError::ToolchainNotInstalled(self.0.name.to_owned()).into()); - } - let installed_primary = primary_toolchain.as_installed_common()?; - - let src_file = self.0.path.join("bin").join(format!("cargo{EXE_SUFFIX}")); - - // MAJOR HACKS: Copy cargo.exe to its own directory on windows before - // running it. This is so that the fallback cargo, when it in turn runs - // rustc.exe, will run the rustc.exe out of the PATH environment - // variable, _not_ the rustc.exe sitting in the same directory as the - // fallback. See the `fallback_cargo_calls_correct_rustc` test case and - // PR 812. - // - // On Windows, spawning a process will search the running application's - // directory for the exe to spawn before searching PATH, and we don't want - // it to do that, because cargo's directory contains the _wrong_ rustc. See - // the documentation for the lpCommandLine argument of CreateProcess. - let exe_path = if cfg!(windows) { - use std::fs; - let fallback_dir = self.0.cfg.rustup_dir.join("fallback"); - fs::create_dir_all(&fallback_dir) - .context("unable to create dir to hold fallback exe")?; - let fallback_file = fallback_dir.join("cargo.exe"); - if fallback_file.exists() { - fs::remove_file(&fallback_file).context("unable to unlink old fallback exe")?; - } - fs::hard_link(&src_file, &fallback_file).context("unable to hard link fallback exe")?; - fallback_file - } else { - src_file - }; - let mut cmd = Command::new(exe_path); - installed_primary.set_env(&mut cmd); // set up the environment to match rustc, not cargo - cmd.env("RUSTUP_TOOLCHAIN", &primary_toolchain.name); - Ok(cmd) - } - - // Installed and not-installed? - pub(crate) fn desc(&self) -> Result { - ToolchainDesc::from_str(&self.0.name) - } - - fn download_cfg(&self) -> DownloadCfg<'_> { - self.0.cfg.download_cfg(&*self.0.dist_handler) - } - - // Installed only? - fn get_component_suggestion( - &self, - component: &Component, - manifest: &Manifest, - only_installed: bool, - ) -> Option { - use strsim::damerau_levenshtein; - - // Suggest only for very small differences - // High number can result in inaccurate suggestions for short queries e.g. `rls` - const MAX_DISTANCE: usize = 3; - - let components = self.list_components(); - if let Ok(components) = components { - let short_name_distance = components - .iter() - .filter(|c| !only_installed || c.installed) - .map(|c| { - ( - damerau_levenshtein( - &c.component.name(manifest)[..], - &component.name(manifest)[..], - ), - c, - ) - }) - .min_by_key(|t| t.0) - .expect("There should be always at least one component"); - - let long_name_distance = components - .iter() - .filter(|c| !only_installed || c.installed) - .map(|c| { - ( - damerau_levenshtein( - &c.component.name_in_manifest()[..], - &component.name(manifest)[..], - ), - c, - ) - }) - .min_by_key(|t| t.0) - .expect("There should be always at least one component"); - - let mut closest_distance = short_name_distance; - let mut closest_match = short_name_distance.1.component.short_name(manifest); - - // Find closer suggestion - if short_name_distance.0 > long_name_distance.0 { - closest_distance = long_name_distance; - - // Check if only targets differ - if closest_distance.1.component.short_name_in_manifest() - == component.short_name_in_manifest() - { - closest_match = long_name_distance.1.component.target(); - } else { - closest_match = long_name_distance - .1 - .component - .short_name_in_manifest() - .to_string(); - } - } else { - // Check if only targets differ - if closest_distance.1.component.short_name(manifest) - == component.short_name(manifest) - { - closest_match = short_name_distance.1.component.target(); - } - } - - // If suggestion is too different don't suggest anything - if closest_distance.0 > MAX_DISTANCE { - None - } else { - Some(closest_match) - } - } else { - None - } - } - - // Installed only. - pub(crate) fn get_manifest(&self) -> Result> { - Ok(self.get_toolchain_desc_with_manifest()?.map(|d| d.manifest)) - } - - // Not installed only? - pub(crate) fn install_from_dist( - &self, - force_update: bool, - allow_downgrade: bool, - components: &[&str], - targets: &[&str], - profile: Option, - ) -> Result { - let update_hash = self.update_hash()?; - let old_date = self.get_manifest().ok().and_then(|m| m.map(|m| m.date)); - InstallMethod::Dist { - desc: &self.desc()?, - profile: profile - .map(Ok) - .unwrap_or_else(|| self.0.cfg.get_profile())?, - update_hash: Some(&update_hash), - dl_cfg: self.download_cfg(), - force_update, - allow_downgrade, - exists: self.0.exists(), - old_date: old_date.as_deref(), - components, - targets, - } - .install(self.0) - } - - // Installed or not installed. - pub fn install_from_dist_if_not_installed(&self) -> Result { - let update_hash = self.update_hash()?; - (self.0.cfg.notify_handler)(Notification::LookingForToolchain(&self.0.name)); - if !self.0.exists() { - Ok(InstallMethod::Dist { - desc: &self.desc()?, - profile: self.0.cfg.get_profile()?, - update_hash: Some(&update_hash), - dl_cfg: self.download_cfg(), - force_update: false, - allow_downgrade: false, - exists: false, - old_date: None, - components: &[], - targets: &[], - } - .install(self.0)?) - } else { - (self.0.cfg.notify_handler)(Notification::UsingExistingToolchain(&self.0.name)); - Ok(UpdateStatus::Unchanged) - } - } - - #[cfg_attr(feature = "otel", tracing::instrument(skip_all))] - pub(crate) fn get_toolchain_desc_with_manifest( - &self, - ) -> Result> { - if !self.0.exists() { - bail!(RustupError::ToolchainNotInstalled(self.0.name.to_owned())); - } - let toolchain = &self.0.name; - let toolchain = ToolchainDesc::from_str(toolchain) - .context(RustupError::ComponentsUnsupported(self.0.name.to_string()))?; - - let prefix = InstallPrefix::from(self.0.path.to_owned()); - let manifestation = Manifestation::open(prefix, toolchain.target.clone())?; - Ok(manifestation - .load_manifest()? - .map(|manifest| ToolchainDescWithManifest { - toolchain, - manifestation, - manifest, - })) - } - - #[cfg_attr(feature = "otel", tracing::instrument)] - pub fn list_components(&self) -> Result> { - if let Some(toolchain) = self.get_toolchain_desc_with_manifest()? { - toolchain.list_components() - } else { - Err(RustupError::ComponentsUnsupported(self.0.name.to_string()).into()) - } - } - - // Installed only. - pub(crate) fn remove_component(&self, mut component: Component) -> Result<()> { - if let Some(desc) = self.get_toolchain_desc_with_manifest()? { - // Rename the component if necessary. - if let Some(c) = desc.manifest.rename_component(&component) { - component = c; - } - - let dist_config = desc.manifestation.read_config()?.unwrap(); - if !dist_config.components.contains(&component) { - let wildcard_component = component.wildcard(); - if dist_config.components.contains(&wildcard_component) { - component = wildcard_component; - } else { - return Err(RustupError::UnknownComponent { - name: self.0.name.to_string(), - component: component.description(&desc.manifest), - suggestion: self.get_component_suggestion(&component, &desc.manifest, true), - } - .into()); - } - } - - let changes = Changes { - explicit_add_components: vec![], - remove_components: vec![component], - }; - - desc.manifestation.update( - &desc.manifest, - changes, - false, - &self.download_cfg(), - &self.download_cfg().notify_handler, - &desc.toolchain.manifest_name(), - false, - )?; - - Ok(()) - } else { - Err(RustupError::MissingManifest { - name: self.0.name.to_string(), - } - .into()) - } - } - - // Installed only. - pub fn show_dist_version(&self) -> Result> { - let update_hash = self.update_hash()?; - - match crate::dist::dist::dl_v2_manifest( - self.download_cfg(), - Some(&update_hash), - &self.desc()?, - )? { - Some((manifest, _)) => Ok(Some(manifest.get_rust_version()?.to_string())), - None => Ok(None), - } - } - - // Installed only. - pub fn show_version(&self) -> Result> { - match self.get_manifest()? { - Some(manifest) => Ok(Some(manifest.get_rust_version()?.to_string())), - None => Ok(None), - } - } - - // Installed only. - fn update_hash(&self) -> Result { - self.0.cfg.get_hash_file(&self.0.name, true) - } - - // Installed only. - pub fn guess_v1_manifest(&self) -> bool { - let prefix = InstallPrefix::from(self.0.path().to_owned()); - // If all the v1 common components are present this is likely to be - // a v1 manifest install. The v1 components are not called the same - // in a v2 install. - for component in V1_COMMON_COMPONENT_LIST { - let manifest = format!("manifest-{component}"); - let manifest_path = prefix.manifest_file(&manifest); - if !utils::path_exists(manifest_path) { - return false; - } - } - // It's reasonable to assume this is a v1 manifest installation - true - } -} - -/// Helper type to avoid parsing a manifest more than once -pub(crate) struct ToolchainDescWithManifest { - toolchain: ToolchainDesc, - manifestation: Manifestation, - pub manifest: Manifest, -} - -impl ToolchainDescWithManifest { - pub(crate) fn list_components(&self) -> Result> { - let config = self.manifestation.read_config()?; - - // Return all optional components of the "rust" package for the - // toolchain's target triple. - let mut res = Vec::new(); - - let rust_pkg = self - .manifest - .packages - .get("rust") - .expect("manifest should contain a rust package"); - let targ_pkg = rust_pkg - .targets - .get(&self.toolchain.target) - .expect("installed manifest should have a known target"); - - for component in &targ_pkg.components { - let installed = config - .as_ref() - .map(|c| component.contained_within(&c.components)) - .unwrap_or(false); - - let component_target = TargetTriple::new(&component.target()); - - // Get the component so we can check if it is available - let component_pkg = self - .manifest - .get_package(component.short_name_in_manifest()) - .unwrap_or_else(|_| { - panic!( - "manifest should contain component {}", - &component.short_name(&self.manifest) - ) - }); - let component_target_pkg = component_pkg - .targets - .get(&component_target) - .expect("component should have target toolchain"); - - res.push(ComponentStatus { - component: component.clone(), - name: component.name(&self.manifest), - installed, - available: component_target_pkg.available(), - }); - } - - res.sort_by(|a, b| a.component.cmp(&b.component)); - - Ok(res) - } -} - -impl<'a> InstalledToolchain<'a> for DistributableToolchain<'a> { - fn installed_paths(&self) -> Result>> { - let path = &self.0.path; - Ok(vec![ - InstalledPath::File { - name: "update hash", - path: self.update_hash()?, - }, - InstalledPath::Dir { path }, - ]) - } -} +pub(crate) mod custom; +pub(crate) mod distributable; +pub(crate) mod names; +#[allow(clippy::module_inception)] +pub(crate) mod toolchain; diff --git a/src/toolchain/custom.rs b/src/toolchain/custom.rs new file mode 100644 index 00000000000..5f47f8cccd0 --- /dev/null +++ b/src/toolchain/custom.rs @@ -0,0 +1,47 @@ +use std::{ + env::consts::EXE_SUFFIX, + path::{Path, PathBuf}, +}; + +use crate::{config::Cfg, install::InstallMethod, utils::utils}; + +use super::{names::CustomToolchainName, toolchain::InstalledPath}; + +/// An official toolchain installed on the local disk +#[derive(Debug)] +pub(crate) struct CustomToolchain; + +impl CustomToolchain { + pub(crate) fn install_from_dir( + cfg: &Cfg, + src: &Path, + dest: &CustomToolchainName, + link: bool, + ) -> anyhow::Result { + let mut pathbuf = PathBuf::from(src); + + pathbuf.push("lib"); + utils::assert_is_directory(&pathbuf)?; + pathbuf.pop(); + pathbuf.push("bin"); + utils::assert_is_directory(&pathbuf)?; + pathbuf.push(format!("rustc{EXE_SUFFIX}")); + utils::assert_is_file(&pathbuf)?; + + if link { + InstallMethod::Link { + src: &utils::to_absolute(src)?, + dest, + cfg, + } + .install()?; + } else { + InstallMethod::Copy { src, dest, cfg }.install()?; + } + Ok(Self) + } + + pub(crate) fn installed_paths(path: &Path) -> anyhow::Result>> { + Ok(vec![InstalledPath::Dir { path }]) + } +} diff --git a/src/toolchain/distributable.rs b/src/toolchain/distributable.rs new file mode 100644 index 00000000000..479ef12b1b9 --- /dev/null +++ b/src/toolchain/distributable.rs @@ -0,0 +1,535 @@ +use std::{ + convert::Infallible, env::consts::EXE_SUFFIX, ffi::OsStr, fs, path::Path, process::Command, +}; + +use anyhow::{anyhow, Context}; + +use crate::{ + component_for_bin, + config::Cfg, + dist::{ + config::Config, + dist::{Profile, ToolchainDesc}, + manifest::{Component, Manifest}, + manifestation::{Changes, Manifestation}, + prefix::InstallPrefix, + }, + install::{InstallMethod, UpdateStatus}, + notifications::Notification, + RustupError, +}; + +use super::{ + names::{LocalToolchainName, ToolchainName}, + toolchain::{InstalledPath, Toolchain}, +}; + +/// An official toolchain installed on the local disk +#[derive(Debug)] +pub(crate) struct DistributableToolchain<'a> { + toolchain: Toolchain<'a>, + cfg: &'a Cfg, + desc: ToolchainDesc, +} + +impl<'a> DistributableToolchain<'a> { + pub(crate) fn new(cfg: &'a Cfg, desc: ToolchainDesc) -> Result { + Toolchain::new(cfg, (&desc).into()).map(|toolchain| Self { + toolchain, + cfg, + desc, + }) + } + + pub(crate) fn desc(&self) -> &ToolchainDesc { + &self.desc + } + + pub(crate) fn add_component(&self, mut component: Component) -> anyhow::Result<()> { + // TODO: take multiple components? + let manifestation = self.get_manifestation()?; + let manifest = self.get_manifest()?; + // Rename the component if necessary. + if let Some(c) = manifest.rename_component(&component) { + component = c; + } + + // Validate the component name + let rust_pkg = manifest + .packages + .get("rust") + .expect("manifest should contain a rust package"); + let targ_pkg = rust_pkg + .targets + .get(&self.desc.target) + .expect("installed manifest should have a known target"); + + if !targ_pkg.components.contains(&component) { + let wildcard_component = component.wildcard(); + if targ_pkg.components.contains(&wildcard_component) { + component = wildcard_component; + } else { + let config = manifestation.read_config()?.unwrap_or_default(); + return Err(RustupError::UnknownComponent { + desc: self.desc.clone(), + component: component.description(&manifest), + suggestion: self + .get_component_suggestion(&component, &config, &manifest, false), + } + .into()); + } + } + + let changes = Changes { + explicit_add_components: vec![component], + remove_components: vec![], + }; + + let notify_handler = + &|n: crate::dist::Notification<'_>| (self.cfg.notify_handler)(n.into()); + let download_cfg = self.cfg.download_cfg(¬ify_handler); + + manifestation.update( + &manifest, + changes, + false, + &download_cfg, + &download_cfg.notify_handler, + &self.desc.manifest_name(), + false, + )?; + + Ok(()) + } + + /// Are all the components installed in this distribution + pub(crate) fn components_exist( + &self, + components: &[&str], + targets: &[&str], + ) -> anyhow::Result { + let manifestation = self.get_manifestation()?; + let manifest = manifestation.load_manifest()?; + let manifest = match manifest { + None => { + // No manifest found. If this is a v1 install that's understandable + // and we assume the components are all good, otherwise we need to + // have a go at re-fetching the manifest to try again. + return Ok(self.guess_v1_manifest()); + } + Some(manifest) => manifest, + }; + let config = manifestation.read_config()?.unwrap_or_default(); + let installed_components = manifest.query_components(&self.desc, &config)?; + // check if all the components we want are installed + let wanted_components = components.iter().all(|name| { + installed_components.iter().any(|status| { + let cname = status.component.short_name(&manifest); + let cname = cname.as_str(); + let cnameim = status.component.short_name_in_manifest(); + let cnameim = cnameim.as_str(); + (cname == *name || cnameim == *name) && status.installed + }) + }); + // And that all the targets we want are installed + let wanted_targets = targets.iter().all(|name| { + installed_components + .iter() + .filter(|c| c.component.short_name_in_manifest() == "rust-std") + .any(|status| { + let ctarg = status.component.target(); + (ctarg == *name) && status.installed + }) + }); + Ok(wanted_components && wanted_targets) + } + + /// Create a command as a fallback for another toolchain. This is used + /// to give custom toolchains access to cargo + pub fn create_fallback_command>( + &self, + binary: T, + installed_primary: &Toolchain<'_>, + ) -> Result { + // With the hacks below this only works for cargo atm + let binary = binary.as_ref(); + assert!(binary == "cargo" || binary == "cargo.exe"); + + let src_file = self + .toolchain + .path() + .join("bin") + .join(format!("cargo{EXE_SUFFIX}")); + + // MAJOR HACKS: Copy cargo.exe to its own directory on windows before + // running it. This is so that the fallback cargo, when it in turn runs + // rustc.exe, will run the rustc.exe out of the PATH environment + // variable, _not_ the rustc.exe sitting in the same directory as the + // fallback. See the `fallback_cargo_calls_correct_rustc` test case and + // PR 812. + // + // On Windows, spawning a process will search the running application's + // directory for the exe to spawn before searching PATH, and we don't want + // it to do that, because cargo's directory contains the _wrong_ rustc. See + // the documentation for the lpCommandLine argument of CreateProcess. + let exe_path = if cfg!(windows) { + let fallback_dir = self.cfg.rustup_dir.join("fallback"); + fs::create_dir_all(&fallback_dir) + .context("unable to create dir to hold fallback exe")?; + let fallback_file = fallback_dir.join("cargo.exe"); + if fallback_file.exists() { + fs::remove_file(&fallback_file).context("unable to unlink old fallback exe")?; + } + fs::hard_link(&src_file, &fallback_file).context("unable to hard link fallback exe")?; + fallback_file + } else { + src_file + }; + let mut cmd = Command::new(exe_path); + installed_primary.set_env(&mut cmd); // set up the environment to match rustc, not cargo + cmd.env("RUSTUP_TOOLCHAIN", installed_primary.name().to_string()); + Ok(cmd) + } + + fn get_component_suggestion( + &self, + component: &Component, + config: &Config, + manifest: &Manifest, + only_installed: bool, + ) -> Option { + use strsim::damerau_levenshtein; + + // Suggest only for very small differences + // High number can result in inaccurate suggestions for short queries e.g. `rls` + const MAX_DISTANCE: usize = 3; + + let components = manifest.query_components(&self.desc, config); + if let Ok(components) = components { + let short_name_distance = components + .iter() + .filter(|c| !only_installed || c.installed) + .map(|c| { + ( + damerau_levenshtein( + &c.component.name(manifest)[..], + &component.name(manifest)[..], + ), + c, + ) + }) + .min_by_key(|t| t.0) + .expect("There should be always at least one component"); + + let long_name_distance = components + .iter() + .filter(|c| !only_installed || c.installed) + .map(|c| { + ( + damerau_levenshtein( + &c.component.name_in_manifest()[..], + &component.name(manifest)[..], + ), + c, + ) + }) + .min_by_key(|t| t.0) + .expect("There should be always at least one component"); + + let mut closest_distance = short_name_distance; + let mut closest_match = short_name_distance.1.component.short_name(manifest); + + // Find closer suggestion + if short_name_distance.0 > long_name_distance.0 { + closest_distance = long_name_distance; + + // Check if only targets differ + if closest_distance.1.component.short_name_in_manifest() + == component.short_name_in_manifest() + { + closest_match = long_name_distance.1.component.target(); + } else { + closest_match = long_name_distance + .1 + .component + .short_name_in_manifest() + .to_string(); + } + } else { + // Check if only targets differ + if closest_distance.1.component.short_name(manifest) + == component.short_name(manifest) + { + closest_match = short_name_distance.1.component.target(); + } + } + + // If suggestion is too different don't suggest anything + if closest_distance.0 > MAX_DISTANCE { + None + } else { + Some(closest_match) + } + } else { + None + } + } + + #[cfg_attr(feature = "otel", tracing::instrument(skip_all))] + pub(crate) fn get_manifestation(&self) -> anyhow::Result { + let prefix = InstallPrefix::from(self.toolchain.path()); + Manifestation::open(prefix, self.desc.target.clone()) + } + + /// Get the manifest associated with this distribution + #[cfg_attr(feature = "otel", tracing::instrument(skip_all))] + pub(crate) fn get_manifest(&self) -> anyhow::Result { + self.get_manifestation()? + .load_manifest() + .transpose() + .unwrap_or_else(|| match self.guess_v1_manifest() { + true => Err(RustupError::ComponentsUnsupportedV1(self.desc.to_string()).into()), + false => Err(RustupError::MissingManifest(self.desc.clone()).into()), + }) + } + + /// Guess whether this is a V1 or V2 manifest distribution. + pub(crate) fn guess_v1_manifest(&self) -> bool { + InstallPrefix::from(self.toolchain.path().to_owned()).guess_v1_manifest() + } + + #[cfg_attr(feature = "otel", tracing::instrument(err, skip_all))] + pub(crate) fn install( + cfg: &'a Cfg, + desc: &'_ ToolchainDesc, + components: &[&str], + targets: &[&str], + profile: Profile, + force: bool, + ) -> anyhow::Result<(UpdateStatus, Self)> { + let hash_path = cfg.get_hash_file(desc, true)?; + let update_hash = Some(&hash_path as &Path); + + let status = InstallMethod::Dist { + cfg, + desc, + profile, + update_hash, + dl_cfg: cfg.download_cfg(&|n| (cfg.notify_handler)(n.into())), + force, + allow_downgrade: false, + exists: false, + old_date_version: None, + components, + targets, + } + .install()?; + Ok((status, Self::new(cfg, desc.clone())?)) + } + + #[cfg_attr(feature = "otel", tracing::instrument(err, skip_all))] + pub fn install_if_not_installed( + cfg: &'a Cfg, + desc: &'a ToolchainDesc, + ) -> anyhow::Result { + (cfg.notify_handler)(Notification::LookingForToolchain(desc)); + if Toolchain::exists(cfg, &desc.into())? { + (cfg.notify_handler)(Notification::UsingExistingToolchain(desc)); + Ok(UpdateStatus::Unchanged) + } else { + Ok(Self::install(cfg, desc, &[], &[], cfg.get_profile()?, false)?.0) + } + } + + #[cfg_attr(feature = "otel", tracing::instrument(err, skip_all))] + pub(crate) fn update( + &mut self, + components: &[&str], + targets: &[&str], + profile: Profile, + ) -> anyhow::Result { + self.update_extra(components, targets, profile, true, false) + } + + /// Update a toolchain with control over the channel behaviour + #[cfg_attr(feature = "otel", tracing::instrument(err, skip_all))] + pub(crate) fn update_extra( + &mut self, + components: &[&str], + targets: &[&str], + profile: Profile, + force: bool, + allow_downgrade: bool, + ) -> anyhow::Result { + let old_date_version = + // Ignore a missing manifest: we can't report the old version + // correctly, and it probably indicates an incomplete install, so do + // not report an old rustc version either. + self.get_manifest() + .map(|m| { + ( + m.date, + // should rustc_version be a free function on a trait? + // note that prev_version can be junk if the rustc component is missing ... + self.toolchain.rustc_version(), + ) + }) + .ok(); + + let hash_path = self.cfg.get_hash_file(&self.desc, true)?; + let update_hash = Some(&hash_path as &Path); + + InstallMethod::Dist { + cfg: self.cfg, + desc: &self.desc, + profile, + update_hash, + dl_cfg: self + .cfg + .download_cfg(&|n| (self.cfg.notify_handler)(n.into())), + force, + allow_downgrade, + exists: true, + old_date_version, + components, + targets, + } + .install() + } + + pub fn recursion_error(&self, binary_lossy: String) -> Result { + let prefix = InstallPrefix::from(self.toolchain.path()); + let manifestation = Manifestation::open(prefix, self.desc.target.clone())?; + let manifest = self.get_manifest()?; + let config = manifestation.read_config()?.unwrap_or_default(); + let component_statuses = manifest.query_components(&self.desc, &config)?; + let desc = &self.desc; + if let Some(component_name) = component_for_bin(&binary_lossy) { + let component_status = component_statuses + .iter() + .find(|cs| cs.component.short_name(&manifest) == component_name) + .ok_or_else(|| anyhow!("component {component_name} should be in the manifest"))?; + let short_name = component_status.component.short_name(&manifest); + if !component_status.available { + Err(anyhow!( + "the '{short_name}' component which provides the command '{binary_lossy}' is not available for the '{desc}' toolchain")) + } else if component_status.installed { + Err(anyhow!( + "the '{binary_lossy}' binary, normally provided by the '{short_name}' component, is not applicable to the '{desc}' toolchain")) + } else { + // available, not installed, recommend installation + let selector = match self.cfg.get_default()? { + Some(ToolchainName::Official(n)) if n == self.desc => "", + _ => " --toolchain {toolchain}", + }; + Err(anyhow!("'{binary_lossy}' is not installed for the toolchain '{desc}'.\nTo install, run `rustup component add {selector}{component_name}`")) + } + } else { + // Unknown binary - no component to recommend + Err(anyhow!( + "Unknown binary '{binary_lossy}' in official toolchain '{desc}'." + )) + } + } + + pub(crate) fn remove_component(&self, mut component: Component) -> anyhow::Result<()> { + // TODO: take multiple components? + let manifestation = self.get_manifestation()?; + let config = manifestation.read_config()?.unwrap_or_default(); + let manifest = self.get_manifest()?; + + // Rename the component if necessary. + if let Some(c) = manifest.rename_component(&component) { + component = c; + } + + if !config.components.contains(&component) { + let wildcard_component = component.wildcard(); + if config.components.contains(&wildcard_component) { + component = wildcard_component; + } else { + return Err(RustupError::UnknownComponent { + desc: self.desc.clone(), + component: component.description(&manifest), + suggestion: self.get_component_suggestion(&component, &config, &manifest, true), + } + .into()); + } + } + + let changes = Changes { + explicit_add_components: vec![], + remove_components: vec![component], + }; + + let notify_handler = + &|n: crate::dist::Notification<'_>| (self.cfg.notify_handler)(n.into()); + let download_cfg = self.cfg.download_cfg(¬ify_handler); + + manifestation.update( + &manifest, + changes, + false, + &download_cfg, + &download_cfg.notify_handler, + &self.desc.manifest_name(), + false, + )?; + + Ok(()) + } + + pub fn show_dist_version(&self) -> anyhow::Result> { + let update_hash = self.cfg.get_hash_file(&self.desc, false)?; + let notify_handler = + &|n: crate::dist::Notification<'_>| (self.cfg.notify_handler)(n.into()); + let download_cfg = self.cfg.download_cfg(¬ify_handler); + + match crate::dist::dist::dl_v2_manifest(download_cfg, Some(&update_hash), &self.desc)? { + Some((manifest, _)) => Ok(Some(manifest.get_rust_version()?.to_string())), + None => Ok(None), + } + } + + pub fn show_version(&self) -> anyhow::Result> { + match self.get_manifestation()?.load_manifest()? { + Some(manifest) => Ok(Some(manifest.get_rust_version()?.to_string())), + None => Ok(None), + } + } + + pub(crate) fn installed_paths<'b>( + cfg: &'b Cfg, + desc: &ToolchainDesc, + path: &'b Path, + ) -> anyhow::Result>> { + Ok(vec![ + InstalledPath::File { + name: "update hash", + path: cfg.get_hash_file(desc, false)?, + }, + InstalledPath::Dir { path }, + ]) + } +} + +impl<'a> TryFrom<&Toolchain<'a>> for DistributableToolchain<'a> { + type Error = RustupError; + + fn try_from(value: &Toolchain<'a>) -> Result { + match value.name() { + LocalToolchainName::Named(ToolchainName::Official(desc)) => Ok(Self { + toolchain: value.clone(), + cfg: value.cfg(), + desc: desc.clone(), + }), + n => Err(RustupError::ComponentsUnsupported(n.to_string())), + } + } +} + +impl<'a> From> for Toolchain<'a> { + fn from(value: DistributableToolchain<'a>) -> Self { + value.toolchain + } +} diff --git a/src/toolchain/names.rs b/src/toolchain/names.rs new file mode 100644 index 00000000000..c09047e849d --- /dev/null +++ b/src/toolchain/names.rs @@ -0,0 +1,712 @@ +//! Overview of toolchain modeling. +//! +//! From the user (including config files, toolchain files and manifests) we get +//! a String. Strings are convertable into `MaybeOfficialToolchainName`, +//! `ResolvableToolchainName`, and `ResolvableLocalToolchainName`. +//! +//! `MaybeOfficialToolchainName` represents a toolchain passed to rustup-init: +//! 'none' to select no toolchain to install, and otherwise a partial toolchain +//! description - channel and optional triple and optional date. +//! +//! `ResolvableToolchainName` represents a toolchain name from a user. Either a +//! partial toolchain description or a single path component that is not 'none'. +//! +//! `MaybeResolvableToolchainName` is analogous to MaybeOfficialToolchainName +//! for both custom and official names. +//! +//! `ToolchainName` is the result of resolving `ResolvableToolchainName` with a +//! host triple, or parsing an installed toolchain name directly. +//! +//! `ResolvableLocalToolchainName` represents the values permittable in +//! `RUSTUP_TOOLCHAIN`: resolved or not resolved official names, custom names, +//! and absolute paths. +//! +//! `LocalToolchainName` represents all the toolchain names that can make sense +//! for referring to actually present toolchains. One of a `ToolchainName` or an +//! absolute path. +//! +//! From the toolchains directory we can iterate directly over +//! `ResolvedToolchainName`. +//! +//! OfficialToolchainName represents a resolved official toolchain name and can +//! be used to download or install toolchains via a downloader. +//! +//! CustomToolchainName can be used to link toolchains to local paths on disk. +//! +//! PathBasedToolchainName can obtained from rustup toolchain files. +//! +//! State from toolchains on disk can be loaded in an InstalledToolchain struct +//! and passed around and queried. The details on that are still vague :). +//! +//! Generally there are infallible Convert impl's for any safe conversion and +//! fallible ones otherwise. + +use std::{ + fmt::Display, + ops::Deref, + path::{Path, PathBuf}, + str::FromStr, +}; + +use thiserror::Error; + +use crate::dist::dist::{PartialToolchainDesc, TargetTriple, ToolchainDesc}; + +/// Errors related to toolchains +#[derive(Error, Debug)] +pub enum InvalidName { + #[error("invalid official toolchain name '{0}'")] + OfficialName(String), + #[error("invalid custom toolchain name '{0}'")] + CustomName(String), + #[error("invalid path toolchain '{0}'")] + PathToolchain(String), + #[error("relative path toolchain '{0}'")] + PathToolchainRelative(String), + #[error("invalid toolchain: the path '{0}' has no bin/ directory")] + ToolchainPath(String), + #[error("invalid toolchain name '{0}'")] + ToolchainName(String), +} + +macro_rules! from_variant { + ($from:ident, $to:ident, $variant:expr) => { + impl From<$from> for $to { + fn from(value: $from) -> Self { + $variant(value) + } + } + impl From<&$from> for $to { + fn from(value: &$from) -> Self { + $variant(value.to_owned()) + } + } + }; +} + +macro_rules! try_from_str { + ($to:ident) => { + try_from_str!(&str, $to); + try_from_str!(&String, $to); + impl TryFrom for $to { + type Error = InvalidName; + + fn try_from(value: String) -> std::result::Result { + $to::validate(&value) + } + } + }; + ($from:ty, $to:ident) => { + impl TryFrom<$from> for $to { + type Error = InvalidName; + + fn try_from(value: $from) -> std::result::Result { + $to::validate(value) + } + } + }; +} + +/// Common validate rules for all sorts of toolchain names +fn validate(candidate: &str) -> Result<&str, InvalidName> { + let normalized_name = candidate.trim_end_matches('/'); + if normalized_name.is_empty() { + Err(InvalidName::ToolchainName(candidate.into())) + } else { + Ok(normalized_name) + } +} + +/// Thunk to avoid errors like +/// = note: `fn(&'2 str) -> Result>::Error> {>::try_from}` must implement `FnOnce<(&'1 str,)>`, for any lifetime `'1`... +/// = note: ...but it actually implements `FnOnce<(&'2 str,)>`, for some specific lifetime `'2` +pub(crate) fn partial_toolchain_desc_parser( + value: &str, +) -> Result { + value.parse::() +} + +/// A toolchain name from user input. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum ResolvableToolchainName { + Custom(CustomToolchainName), + Official(PartialToolchainDesc), +} + +impl ResolvableToolchainName { + /// Resolve to a concrete toolchain name + pub fn resolve(&self, host: &TargetTriple) -> Result { + match self.clone() { + ResolvableToolchainName::Custom(c) => Ok(ToolchainName::Custom(c)), + ResolvableToolchainName::Official(desc) => { + let resolved = desc.resolve(host)?; + Ok(ToolchainName::Official(resolved)) + } + } + } + + // If candidate could be resolved, return a ready to resolve version of it. + // Otherwise error. + fn validate(candidate: &str) -> Result { + let candidate = validate(candidate)?; + candidate + .parse::() + .map(ResolvableToolchainName::Official) + .or_else(|_| { + CustomToolchainName::try_from(candidate) + .map(ResolvableToolchainName::Custom) + .map_err(|_| InvalidName::ToolchainName(candidate.into())) + }) + } +} + +try_from_str!(ResolvableToolchainName); + +impl From<&PartialToolchainDesc> for ResolvableToolchainName { + fn from(value: &PartialToolchainDesc) -> Self { + ResolvableToolchainName::Official(value.to_owned()) + } +} + +impl Display for ResolvableToolchainName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResolvableToolchainName::Custom(c) => write!(f, "{c}"), + ResolvableToolchainName::Official(o) => write!(f, "{o}"), + } + } +} + +/// Thunk to avoid errors like +/// = note: `fn(&'2 str) -> Result>::Error> {>::try_from}` must implement `FnOnce<(&'1 str,)>`, for any lifetime `'1`... +/// = note: ...but it actually implements `FnOnce<(&'2 str,)>`, for some specific lifetime `'2` +pub(crate) fn resolvable_toolchainame_parser( + value: &str, +) -> Result { + ResolvableToolchainName::try_from(value) +} + +/// A toolchain name from user input. MaybeToolchainName accepts 'none' or a +/// custom or resolvable official name. Possibly this should be an Option with a +/// local trait for our needs. +#[derive(Debug, Clone)] +pub(crate) enum MaybeResolvableToolchainName { + Some(ResolvableToolchainName), + None, +} + +impl MaybeResolvableToolchainName { + // If candidate could be resolved, return a ready to resolve version of it. + // Otherwise error. + fn validate(candidate: &str) -> Result { + let candidate = validate(candidate)?; + if candidate == "none" { + Ok(MaybeResolvableToolchainName::None) + } else { + Ok(MaybeResolvableToolchainName::Some( + ResolvableToolchainName::validate(candidate)?, + )) + } + } +} + +try_from_str!(MaybeResolvableToolchainName); + +impl Display for MaybeResolvableToolchainName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MaybeResolvableToolchainName::Some(t) => write!(f, "{t}"), + MaybeResolvableToolchainName::None => write!(f, "none"), + } + } +} + +/// Thunk to avoid errors like +/// = note: `fn(&'2 str) -> Result>::Error> {>::try_from}` must implement `FnOnce<(&'1 str,)>`, for any lifetime `'1`... +/// = note: ...but it actually implements `FnOnce<(&'2 str,)>`, for some specific lifetime `'2` +pub(crate) fn maybe_resolvable_toolchainame_parser( + value: &str, +) -> Result { + MaybeResolvableToolchainName::try_from(value) +} + +/// ResolvableToolchainName + none, for overriding default-has-a-value +/// situations in the CLI with an official toolchain name or none +#[derive(Clone)] +pub(crate) enum MaybeOfficialToolchainName { + None, + Some(PartialToolchainDesc), +} + +impl MaybeOfficialToolchainName { + fn validate(candidate: &str) -> Result { + let candidate = validate(candidate)?; + if candidate == "none" { + Ok(MaybeOfficialToolchainName::None) + } else { + Ok(MaybeOfficialToolchainName::Some( + validate(candidate)? + .parse::() + .map_err(|_| InvalidName::OfficialName(candidate.into()))?, + )) + } + } +} + +try_from_str!(MaybeOfficialToolchainName); + +impl Display for MaybeOfficialToolchainName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MaybeOfficialToolchainName::None => write!(f, "none"), + MaybeOfficialToolchainName::Some(t) => write!(f, "{t}"), + } + } +} + +/// Thunk to avoid errors like +/// = note: `fn(&'2 str) -> Result>::Error> {>::try_from}` must implement `FnOnce<(&'1 str,)>`, for any lifetime `'1`... +/// = note: ...but it actually implements `FnOnce<(&'2 str,)>`, for some specific lifetime `'2` +pub(crate) fn maybe_official_toolchainame_parser( + value: &str, +) -> Result { + MaybeOfficialToolchainName::try_from(value) +} + +/// ToolchainName can be used in calls to Cfg that alter configuration, +/// like setting overrides, or that depend on configuration, like calculating +/// the toolchain directory. +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub(crate) enum ToolchainName { + Custom(CustomToolchainName), + Official(ToolchainDesc), +} + +impl ToolchainName { + /// If the string is already resolved, allow direct conversion + fn validate(candidate: &str) -> Result { + let candidate = validate(candidate)?; + candidate + .parse::() + .map(ToolchainName::Official) + .or_else(|_| CustomToolchainName::try_from(candidate).map(ToolchainName::Custom)) + .map_err(|_| InvalidName::ToolchainName(candidate.into())) + } +} + +from_variant!(ToolchainDesc, ToolchainName, ToolchainName::Official); +from_variant!(CustomToolchainName, ToolchainName, ToolchainName::Custom); + +try_from_str!(ToolchainName); + +impl Display for ToolchainName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ToolchainName::Custom(t) => write!(f, "{t}"), + ToolchainName::Official(t) => write!(f, "{t}"), + } + } +} + +pub(crate) fn toolchain_sort(v: &mut [ToolchainName]) { + use semver::{BuildMetadata, Prerelease, Version}; + + fn special_version(ord: u64, s: &str) -> Version { + Version { + major: 0, + minor: 0, + patch: 0, + pre: Prerelease::new(&format!("pre.{}.{}", ord, s.replace('_', "-"))).unwrap(), + build: BuildMetadata::EMPTY, + } + } + + fn toolchain_sort_key(s: &str) -> Version { + if s.starts_with("stable") { + special_version(0, s) + } else if s.starts_with("beta") { + special_version(1, s) + } else if s.starts_with("nightly") { + special_version(2, s) + } else { + Version::parse(&s.replace('_', "-")).unwrap_or_else(|_| special_version(3, s)) + } + } + + v.sort_by(|a, b| { + let a_str = &format!("{a}"); + let b_str = &format!("{b}"); + let a_key = toolchain_sort_key(a_str); + let b_key = toolchain_sort_key(b_str); + a_key.cmp(&b_key) + }); +} + +/// ResolvableLocalToolchainName is used to process values set in +/// RUSTUP_TOOLCHAIN: resolvable and resolved official names, custom names and +/// absolute paths. +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub(crate) enum ResolvableLocalToolchainName { + Named(ResolvableToolchainName), + Path(PathBasedToolchainName), +} + +impl ResolvableLocalToolchainName { + /// Resolve to a concrete toolchain name + pub fn resolve(&self, host: &TargetTriple) -> Result { + match self.clone() { + ResolvableLocalToolchainName::Named(t) => { + Ok(LocalToolchainName::Named(t.resolve(host)?)) + } + ResolvableLocalToolchainName::Path(t) => Ok(LocalToolchainName::Path(t)), + } + } + + /// Validates if the string is a resolvable toolchain, or a path based toolchain. + fn validate(candidate: &str) -> Result { + let candidate = validate(candidate)?; + ResolvableToolchainName::try_from(candidate) + .map(ResolvableLocalToolchainName::Named) + .or_else(|_| { + PathBasedToolchainName::try_from(&PathBuf::from(candidate) as &Path) + .map(ResolvableLocalToolchainName::Path) + }) + } +} + +try_from_str!(ResolvableLocalToolchainName); + +impl Display for ResolvableLocalToolchainName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResolvableLocalToolchainName::Named(t) => write!(f, "{t}"), + ResolvableLocalToolchainName::Path(t) => write!(f, "{t}"), + } + } +} + +pub(crate) fn resolvable_local_toolchainame_parser( + value: &str, +) -> Result { + ResolvableLocalToolchainName::try_from(value) +} + +/// LocalToolchainName can be used in calls to Cfg that alter configuration, +/// like setting overrides, or that depend on configuration, like calculating +/// the toolchain directory. It is not used to model the RUSTUP_TOOLCHAIN +/// variable, because that can take unresolved toolchain values that are not +/// invalid for referring to an installed toolchain. +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub(crate) enum LocalToolchainName { + Named(ToolchainName), + Path(PathBasedToolchainName), +} + +impl From<&ToolchainDesc> for LocalToolchainName { + fn from(value: &ToolchainDesc) -> Self { + ToolchainName::Official(value.to_owned()).into() + } +} + +impl From<&CustomToolchainName> for LocalToolchainName { + fn from(value: &CustomToolchainName) -> Self { + ToolchainName::Custom(value.to_owned()).into() + } +} + +impl From for LocalToolchainName { + fn from(value: CustomToolchainName) -> Self { + ToolchainName::Custom(value).into() + } +} + +from_variant!(ToolchainName, LocalToolchainName, LocalToolchainName::Named); +from_variant!( + PathBasedToolchainName, + LocalToolchainName, + LocalToolchainName::Path +); + +impl Display for LocalToolchainName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LocalToolchainName::Named(t) => write!(f, "{t}"), + LocalToolchainName::Path(t) => write!(f, "{t}"), + } + } +} + +/// A custom toolchain name, but not an official toolchain name +/// (e.g. my-custom-toolchain) +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub(crate) struct CustomToolchainName(String); + +impl CustomToolchainName { + pub fn str(&self) -> &str { + &self.0 + } + + fn validate(candidate: &str) -> Result { + let candidate = validate(candidate)?; + if candidate.parse::().is_ok() + || candidate == "none" + || candidate.contains('/') + || candidate.contains('\\') + { + Err(InvalidName::CustomName(candidate.into())) + } else { + Ok(CustomToolchainName(candidate.into())) + } + } +} + +try_from_str!(CustomToolchainName); + +impl Display for CustomToolchainName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Thunk to avoid +/// = note: `fn(&'2 str) -> Result>::Error> {>::try_from}` must implement `FnOnce<(&'1 str,)>`, for any lifetime `'1`... +/// = note: ...but it actually implements `FnOnce<(&'2 str,)>`, for some specific lifetime `'2` +pub(crate) fn custom_toolchain_name_parser( + value: &str, +) -> Result { + CustomToolchainName::try_from(value) +} + +/// An toolchain specified just via its path. Relative paths enable arbitrary +/// code execution in a rust dir, so as a partial mitigation is limited to +/// absolute paths. +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub(crate) struct PathBasedToolchainName(PathBuf, String); + +impl Display for PathBasedToolchainName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.display()) + } +} + +impl TryFrom<&Path> for PathBasedToolchainName { + type Error = InvalidName; + + fn try_from(value: &Path) -> std::result::Result { + // if official || at least a single path component + let as_str = value.display().to_string(); + if PartialToolchainDesc::from_str(&as_str).is_ok() + || !(as_str.contains('/') || as_str.contains('\\')) + { + Err(InvalidName::PathToolchain(as_str)) + } else { + // Perform minimal validation; there should at least be a `bin/` that might + // contain things for us to run. + if !value.is_absolute() { + Err(InvalidName::PathToolchainRelative(as_str)) + } else if !value.join("bin").is_dir() { + Err(InvalidName::ToolchainPath(as_str)) + } else { + Ok(PathBasedToolchainName(value.into(), as_str)) + } + } + } +} + +impl TryFrom<&LocalToolchainName> for PathBasedToolchainName { + type Error = InvalidName; + + fn try_from(value: &LocalToolchainName) -> std::result::Result { + match value { + LocalToolchainName::Named(_) => Err(InvalidName::PathToolchain(format!("{value}"))), + LocalToolchainName::Path(n) => Ok(n.clone()), + } + } +} + +impl Deref for PathBasedToolchainName { + type Target = PathBuf; + + fn deref(&self) -> &PathBuf { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use proptest::{collection::vec, prelude::*, string::string_regex}; + + use crate::{ + dist::dist::PartialToolchainDesc, + toolchain::names::{CustomToolchainName, ResolvableToolchainName, ToolchainName}, + }; + + //Duplicated from triple.rs as a pragmatic step. TODO: remove duplication. + static LIST_ARCHS: &[&str] = &[ + "i386", + "i586", + "i686", + "x86_64", + "arm", + "armv7", + "armv7s", + "aarch64", + "mips", + "mipsel", + "mips64", + "mips64el", + "powerpc", + "powerpc64", + "powerpc64le", + "riscv64gc", + "s390x", + "loongarch64", + ]; + static LIST_OSES: &[&str] = &[ + "pc-windows", + "unknown-linux", + "apple-darwin", + "unknown-netbsd", + "apple-ios", + "linux", + "rumprun-netbsd", + "unknown-freebsd", + "unknown-illumos", + ]; + static LIST_ENVS: &[&str] = &[ + "gnu", + "gnux32", + "msvc", + "gnueabi", + "gnueabihf", + "gnuabi64", + "androideabi", + "android", + "musl", + ]; + + fn partial_toolchain_desc_re() -> String { + let triple_re = format!( + r"(-({}))?(?:-({}))?(?:-({}))?", + LIST_ARCHS.join("|"), + LIST_OSES.join("|"), + LIST_ENVS.join("|") + ); + let partial_toolchain_desc_re = format!( + r"(nightly|beta|stable|\d{{1}}\.\d{{1,3}}(\.\d{{1,2}})?)(-(\d{{4}}-\d{{2}}-\d{{2}}))?{triple_re}" + ); + + partial_toolchain_desc_re + } + + prop_compose! { + fn arb_partial_toolchain_desc() + (s in string_regex(&partial_toolchain_desc_re()).unwrap()) -> String { + s + } + } + + prop_compose! { + fn arb_custom_name() + (s in r"[^\\/]+") -> String { + // perhaps need to filter 'none' and partial toolchains - but they won't typically be generated anyway. + s + } + } + + prop_compose! { + fn arb_resolvable_name() + (case in (0..=1), desc in arb_custom_name(), name in arb_partial_toolchain_desc() ) -> String { + match case { + 0 => name, + _d => desc + } + } + } + + prop_compose! { + fn arb_abspath_name() + (case in (0..=1), segments in vec("[^\\/]", 0..5)) -> String { + match case { + 0 => format!("/{}", segments.join("/")), + _ => format!(r"c:\{}", segments.join(r"\")) + } + } + } + + proptest! { + #[test] + fn test_parse_partial_desc(desc in arb_partial_toolchain_desc()) { + PartialToolchainDesc::from_str(&desc).unwrap(); + } + + #[test] + fn test_parse_custom(name in arb_custom_name()) { + CustomToolchainName::try_from(name).unwrap(); + } + + #[test] + fn test_parse_resolvable_name(name in arb_resolvable_name()) { + ResolvableToolchainName::try_from(name).unwrap(); + } + + // TODO: This needs some thought + // #[test] + // fn test_parse_abs_path_name(name in arb_abspath_name()) { + // let tempdir = tempfile::Builder::new().tempdir().unwrap(); + // let d = tempdir.into_path(); + // fs::create_dir(d.create_directory("bin").unwrap()).unwrap(); + // // .into_path()) + + // PathBasedToolchainName::try_from(Path::new(&name)).unwrap(); + // } + + } + + // fn validate(candidate: &str) -> Result { + // let candidate = validate(candidate)?; + // if candidate.parse::().is_ok() + // || candidate == "none" + // || candidate.contains('/') + // || candidate.contains('\\') + // { + // Err(InvalidName::CustomName(candidate.into())) + // } else { + // Ok(CustomToolchainName(candidate.into())) + // } + + #[test] + fn test_toolchain_sort() { + let expected = vec![ + "stable-x86_64-unknown-linux-gnu", + "beta-x86_64-unknown-linux-gnu", + "nightly-x86_64-unknown-linux-gnu", + "1.0.0-x86_64-unknown-linux-gnu", + "1.2.0-x86_64-unknown-linux-gnu", + "1.8.0-x86_64-unknown-linux-gnu", + "1.10.0-x86_64-unknown-linux-gnu", + ] + .into_iter() + .map(|s| ToolchainName::try_from(s).unwrap()) + .collect::>(); + + let mut v = vec![ + "1.8.0-x86_64-unknown-linux-gnu", + "1.0.0-x86_64-unknown-linux-gnu", + "nightly-x86_64-unknown-linux-gnu", + "stable-x86_64-unknown-linux-gnu", + "1.10.0-x86_64-unknown-linux-gnu", + "beta-x86_64-unknown-linux-gnu", + "1.2.0-x86_64-unknown-linux-gnu", + ] + .into_iter() + .map(|s| ToolchainName::try_from(s).unwrap()) + .collect::>(); + + super::toolchain_sort(&mut v); + + assert_eq!(expected, v); + } +} diff --git a/src/toolchain/toolchain.rs b/src/toolchain/toolchain.rs new file mode 100644 index 00000000000..767d3a152a5 --- /dev/null +++ b/src/toolchain/toolchain.rs @@ -0,0 +1,358 @@ +use std::{ + env::{self, consts::EXE_SUFFIX}, + ffi::{OsStr, OsString}, + fmt::Debug, + fs, + io::{self, BufRead, BufReader}, + path::{Path, PathBuf}, + process::{Command, Stdio}, + time::Duration, +}; + +use anyhow::{anyhow, bail}; +use derivative::Derivative; +use fs_at::OpenOptions; +use wait_timeout::ChildExt; + +use crate::{ + config::Cfg, + currentprocess::process, + env_var, install, + notifications::Notification, + utils::{raw::open_dir, utils}, + RustupError, +}; + +use super::{ + custom::CustomToolchain, + distributable::DistributableToolchain, + names::{LocalToolchainName, ToolchainName}, +}; + +/// A toolchain installed on the local disk +#[derive(Derivative)] +#[derivative(Clone, Debug)] +pub(crate) struct Toolchain<'a> { + cfg: &'a Cfg, + name: LocalToolchainName, + path: PathBuf, +} + +impl<'a> Toolchain<'a> { + pub(crate) fn new(cfg: &'a Cfg, name: LocalToolchainName) -> Result { + let path = cfg.toolchain_path(&name); + if !Toolchain::exists(cfg, &name)? { + return Err(match name { + LocalToolchainName::Named(name) => RustupError::ToolchainNotInstalled(name), + LocalToolchainName::Path(name) => RustupError::PathToolchainNotInstalled(name), + }); + } + Ok(Self { cfg, name, path }) + } + + /// Ok(True) if the toolchain exists. Ok(False) if the toolchain or its + /// containing directory don't exist. Err otherwise. + pub(crate) fn exists(cfg: &'a Cfg, name: &LocalToolchainName) -> Result { + let path = cfg.toolchain_path(name); + // toolchain validation should have prevented a situation where there is + // no base dir, but defensive programming is defensive. + let parent = path + .parent() + .ok_or_else(|| RustupError::InvalidToolchainName(name.to_string()))?; + let base_name = path + .file_name() + .ok_or_else(|| RustupError::InvalidToolchainName(name.to_string()))?; + let parent_dir = match open_dir(parent) { + Ok(d) => d, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false), + e => e?, + }; + let opened = OpenOptions::default() + .read(true) + .follow(true) + .open_dir_at(&parent_dir, base_name); + Ok(opened.is_ok()) + } + + pub(crate) fn cfg(&self) -> &'a Cfg { + self.cfg + } + + pub(crate) fn name(&self) -> &LocalToolchainName { + &self.name + } + + pub(crate) fn path(&self) -> &Path { + &self.path + } + + /// The path to a binary within the toolchain, without regard for cargo-fallback logic + pub fn binary_file(&self, name: &str) -> PathBuf { + let mut path = self.path.clone(); + path.push("bin"); + path.push(name.to_owned() + env::consts::EXE_SUFFIX); + path + } + + /// Not intended to be public, but more code golf required to get it hidden. + /// pub because of create_fallback_command + pub fn set_env(&self, cmd: &mut Command) { + self.set_ldpath(cmd); + + // Older versions of Cargo used a slightly different definition of + // cargo home. Rustup does not read HOME on Windows whereas the older + // versions of Cargo did. Rustup and Cargo should be in sync now (both + // using the same `home` crate), but this is retained to ensure cargo + // and rustup agree in older versions. + if let Ok(cargo_home) = utils::cargo_home() { + cmd.env("CARGO_HOME", &cargo_home); + } + + env_var::inc("RUST_RECURSION_COUNT", cmd); + + cmd.env("RUSTUP_TOOLCHAIN", format!("{}", self.name)); + cmd.env("RUSTUP_HOME", &self.cfg().rustup_dir); + } + + /// Apply the appropriate LD path for a command being run from a toolchain. + fn set_ldpath(&self, cmd: &mut Command) { + let mut new_path = vec![self.path.join("lib")]; + + #[cfg(not(target_os = "macos"))] + mod sysenv { + pub const LOADER_PATH: &str = "LD_LIBRARY_PATH"; + } + #[cfg(target_os = "macos")] + mod sysenv { + // When loading and linking a dynamic library or bundle, dlopen + // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and + // DYLD_FALLBACK_LIBRARY_PATH. + // In the Mach-O format, a dynamic library has an "install path." + // Clients linking against the library record this path, and the + // dynamic linker, dyld, uses it to locate the library. + // dyld searches DYLD_LIBRARY_PATH *before* the install path. + // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot + // find the library in the install path. + // Setting DYLD_LIBRARY_PATH can easily have unintended + // consequences. + pub const LOADER_PATH: &str = "DYLD_FALLBACK_LIBRARY_PATH"; + } + if cfg!(target_os = "macos") + && process() + .var_os(sysenv::LOADER_PATH) + .filter(|x| x.len() > 0) + .is_none() + { + // These are the defaults when DYLD_FALLBACK_LIBRARY_PATH isn't + // set or set to an empty string. Since we are explicitly setting + // the value, make sure the defaults still work. + if let Some(home) = process().var_os("HOME") { + new_path.push(PathBuf::from(home).join("lib")); + } + new_path.push(PathBuf::from("/usr/local/lib")); + new_path.push(PathBuf::from("/usr/lib")); + } + + env_var::prepend_path(sysenv::LOADER_PATH, new_path, cmd); + + // Prepend CARGO_HOME/bin to the PATH variable so that we're sure to run + // cargo/rustc via the proxy bins. There is no fallback case for if the + // proxy bins don't exist. We'll just be running whatever happens to + // be on the PATH. + let mut path_entries = vec![]; + if let Ok(cargo_home) = utils::cargo_home() { + path_entries.push(cargo_home.join("bin")); + } + + if cfg!(target_os = "windows") { + // Historically rustup has included the bin directory in PATH to + // work around some bugs (see + // https://github.com/rust-lang/rustup/pull/3178 for more + // information). This shouldn't be needed anymore, and it causes + // problems because calling tools recursively (like `cargo + // +nightly metadata` from within a cargo subcommand). The + // recursive call won't work because it is not executing the + // proxy, so the `+` toolchain override doesn't work. + // + // This is opt-in to allow us to get more real-world testing. + if process() + .var_os("RUSTUP_WINDOWS_PATH_ADD_BIN") + .map_or(true, |s| s == "1") + { + path_entries.push(self.path.join("bin")); + } + } + + env_var::prepend_path("PATH", path_entries, cmd); + } + + /// Infallible function that describes the version of rustc in an installed distribution + #[cfg_attr(feature = "otel", tracing::instrument)] + pub fn rustc_version(&self) -> String { + // TODO: use create_command instead of manual construction! + let rustc_path = self.binary_file("rustc"); + if utils::is_file(&rustc_path) { + let mut cmd = Command::new(&rustc_path); + cmd.arg("--version"); + cmd.stdin(Stdio::null()); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + self.set_ldpath(&mut cmd); + + // some toolchains are faulty with some combinations of platforms and + // may fail to launch but also to timely terminate. + // (known cases include Rust 1.3.0 through 1.10.0 in recent macOS Sierra.) + // we guard against such cases by enforcing a reasonable timeout to read. + let mut line1 = None; + if let Ok(mut child) = cmd.spawn() { + let timeout = Duration::new(10, 0); + match child.wait_timeout(timeout) { + Ok(Some(status)) if status.success() => { + let out = child + .stdout + .expect("Child::stdout requested but not present"); + let mut line = String::new(); + if BufReader::new(out).read_line(&mut line).is_ok() { + let lineend = line.trim_end_matches(&['\r', '\n'][..]).len(); + line.truncate(lineend); + line1 = Some(line); + } + } + Ok(None) => { + let _ = child.kill(); + return String::from("(timeout reading rustc version)"); + } + Ok(Some(_)) | Err(_) => {} + } + } + + if let Some(line1) = line1 { + line1 + } else { + String::from("(error reading rustc version)") + } + } else { + String::from("(rustc does not exist)") + } + } + + #[cfg_attr(feature="otel", tracing::instrument(err,fields(binary, recursion=process().var("RUST_RECURSION_COUNT").ok())))] + pub fn create_command + Debug>( + &self, + binary: T, + ) -> Result { + // Create the path to this binary within the current toolchain sysroot + let binary = if let Some(binary_str) = binary.as_ref().to_str() { + if binary_str.to_lowercase().ends_with(EXE_SUFFIX) { + binary.as_ref().to_owned() + } else { + OsString::from(format!("{binary_str}{EXE_SUFFIX}")) + } + } else { + // Very weird case. Non-unicode command. + binary.as_ref().to_owned() + }; + + let bin_path = self.path.join("bin").join(&binary); + let path = if utils::is_file(&bin_path) { + &bin_path + } else { + let recursion_count = process() + .var("RUST_RECURSION_COUNT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + if recursion_count > env_var::RUST_RECURSION_COUNT_MAX - 1 { + let binary_lossy: String = binary.to_string_lossy().into(); + if matches!( + &self.name, + LocalToolchainName::Named(ToolchainName::Official(_)) + ) { + let distributable = DistributableToolchain::try_from(self)?; + // Design note: this is a bit of an awkward cast from + // general (toolchain) to more specialised (distributable); + // perhaps this function should something implemented on a + // trait, permitting removal of that case. + return Err(distributable.recursion_error(binary_lossy).unwrap_err()); + } else { + let t = &self.name; + return Err(anyhow!( + "'{binary_lossy}' is not installed for the custom toolchain '{t}'.\nnote: this is a custom toolchain, which cannot use `rustup component add`\n\ + help: if you built this toolchain from source, and used `rustup toolchain link`, then you may be able to build the component with `x.py`" + )); + } + } + Path::new(&binary) + }; + let mut cmd = Command::new(path); + self.set_env(&mut cmd); + Ok(cmd) + } + + pub fn doc_path(&self, relative: &str) -> anyhow::Result { + let parts = vec!["share", "doc", "rust", "html"]; + let mut doc_dir = self.path.clone(); + for part in parts { + doc_dir.push(part); + } + doc_dir.push(relative); + + Ok(doc_dir) + } + + pub fn open_docs(&self, relative: &str) -> anyhow::Result<()> { + utils::open_browser(&self.doc_path(relative)?) + } + + /// Remove the toolchain from disk + /// + /// + pub fn ensure_removed(cfg: &'a Cfg, name: LocalToolchainName) -> anyhow::Result<()> { + let path = cfg.toolchain_path(&name); + let name = match name { + LocalToolchainName::Named(t) => t, + LocalToolchainName::Path(_) => bail!("Cannot remove a path based toolchain"), + }; + match Self::exists(cfg, &(&name).into())? { + true => { + (cfg.notify_handler)(Notification::UninstallingToolchain(&name)); + let installed_paths = match &name { + ToolchainName::Custom(_) => CustomToolchain::installed_paths(&path), + ToolchainName::Official(desc) => { + DistributableToolchain::installed_paths(cfg, desc, &path) + } + }?; + for path in installed_paths { + match path { + InstalledPath::File { name, path } => { + utils::ensure_file_removed(name, &path)? + } + InstalledPath::Dir { path } => { + install::uninstall(path, &|n| (cfg.notify_handler)(n.into()))? + } + } + } + } + false => { + // Might be a dangling symlink + if path.is_symlink() { + (cfg.notify_handler)(Notification::UninstallingToolchain(&name)); + fs::remove_dir_all(&path)?; + } else { + info!("no toolchain installed for '{name}'"); + } + } + } + + if !path.is_symlink() && !path.exists() { + (cfg.notify_handler)(Notification::UninstalledToolchain(&name)); + } + Ok(()) + } +} + +/// Installed paths +pub(crate) enum InstalledPath<'a> { + File { name: &'static str, path: PathBuf }, + Dir { path: &'a Path }, +} diff --git a/src/utils/raw.rs b/src/utils/raw.rs index b6412d8795b..a56133d2568 100644 --- a/src/utils/raw.rs +++ b/src/utils/raw.rs @@ -1,11 +1,15 @@ #[cfg(not(windows))] use std::env; use std::fs; +use std::fs::File; use std::io; use std::io::Write; use std::path::Path; use std::str; +#[cfg(not(windows))] +use libc; + #[cfg(not(windows))] use crate::process; @@ -29,6 +33,29 @@ pub fn is_file>(path: P) -> bool { fs::metadata(path).ok().as_ref().map(fs::Metadata::is_file) == Some(true) } +#[cfg(windows)] +pub fn open_dir(p: &Path) -> std::io::Result { + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + + use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS; + + let mut options = OpenOptions::new(); + options.read(true); + options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); + options.open(p) +} +#[cfg(not(windows))] +pub fn open_dir(p: &Path) -> std::io::Result { + use std::fs::OpenOptions; + use std::os::unix::fs::OpenOptionsExt; + + let mut options = OpenOptions::new(); + options.read(true); + options.custom_flags(libc::O_NOFOLLOW); + options.open(p) +} + pub fn path_exists>(path: P) -> bool { fs::metadata(path).is_ok() } diff --git a/src/utils/utils.rs b/src/utils/utils.rs index 04ab85117a0..3b62dd254fb 100644 --- a/src/utils/utils.rs +++ b/src/utils/utils.rs @@ -1,4 +1,3 @@ -use std::cmp::Ord; use std::env; use std::fs::{self, File}; use std::io::{self, BufReader, Write}; @@ -536,40 +535,6 @@ pub(crate) fn format_path_for_display(path: &str) -> String { } } -pub(crate) fn toolchain_sort>(v: &mut [T]) { - use semver::{BuildMetadata, Prerelease, Version}; - - fn special_version(ord: u64, s: &str) -> Version { - Version { - major: 0, - minor: 0, - patch: 0, - pre: Prerelease::new(&format!("pre.{}.{}", ord, s.replace('_', "-"))).unwrap(), - build: BuildMetadata::EMPTY, - } - } - - fn toolchain_sort_key(s: &str) -> Version { - if s.starts_with("stable") { - special_version(0, s) - } else if s.starts_with("beta") { - special_version(1, s) - } else if s.starts_with("nightly") { - special_version(2, s) - } else { - Version::parse(&s.replace('_', "-")).unwrap_or_else(|_| special_version(3, s)) - } - } - - v.sort_by(|a, b| { - let a_str: &str = a.as_ref(); - let b_str: &str = b.as_ref(); - let a_key = toolchain_sort_key(a_str); - let b_key = toolchain_sort_key(b_str); - a_key.cmp(&b_key) - }); -} - #[cfg(target_os = "linux")] fn copy_and_delete<'a, N>( name: &'static str, @@ -745,33 +710,6 @@ mod tests { use super::*; - #[test] - fn test_toolchain_sort() { - let expected = vec![ - "stable-x86_64-unknown-linux-gnu", - "beta-x86_64-unknown-linux-gnu", - "nightly-x86_64-unknown-linux-gnu", - "1.0.0-x86_64-unknown-linux-gnu", - "1.2.0-x86_64-unknown-linux-gnu", - "1.8.0-x86_64-unknown-linux-gnu", - "1.10.0-x86_64-unknown-linux-gnu", - ]; - - let mut v = vec![ - "1.8.0-x86_64-unknown-linux-gnu", - "1.0.0-x86_64-unknown-linux-gnu", - "nightly-x86_64-unknown-linux-gnu", - "stable-x86_64-unknown-linux-gnu", - "1.10.0-x86_64-unknown-linux-gnu", - "beta-x86_64-unknown-linux-gnu", - "1.2.0-x86_64-unknown-linux-gnu", - ]; - - toolchain_sort(&mut v); - - assert_eq!(expected, v); - } - #[test] fn test_remove_file() { let tempdir = tempfile::Builder::new().prefix("rustup").tempdir().unwrap(); diff --git a/tests/suite/cli-ui/rustup/rustup_default_cmd_help_flag_stdout.toml b/tests/suite/cli-ui/rustup/rustup_default_cmd_help_flag_stdout.toml index 834081ef852..cd47e4f43c7 100644 --- a/tests/suite/cli-ui/rustup/rustup_default_cmd_help_flag_stdout.toml +++ b/tests/suite/cli-ui/rustup/rustup_default_cmd_help_flag_stdout.toml @@ -8,8 +8,8 @@ USAGE: rustup[EXE] default [toolchain] ARGS: - Toolchain name, such as 'stable', 'nightly', or '1.8.0'. For more information - see `rustup help toolchain` + 'none', a toolchain name, such as 'stable', 'nightly', '1.8.0', or a custom + toolchain name. For more information see `rustup help toolchain` OPTIONS: -h, --help Print help information diff --git a/tests/suite/cli-ui/rustup/rustup_override_cmd_add_cmd_help_flag_stdout.toml b/tests/suite/cli-ui/rustup/rustup_override_cmd_add_cmd_help_flag_stdout.toml index d18211f0d79..95a2e7da31c 100644 --- a/tests/suite/cli-ui/rustup/rustup_override_cmd_add_cmd_help_flag_stdout.toml +++ b/tests/suite/cli-ui/rustup/rustup_override_cmd_add_cmd_help_flag_stdout.toml @@ -8,8 +8,8 @@ USAGE: rustup[EXE] override set [OPTIONS] ARGS: - Toolchain name, such as 'stable', 'nightly', or '1.8.0'. For more information - see `rustup help toolchain` + Toolchain name, such as 'stable', 'nightly', '1.8.0', or a custom toolchain + name. For more information see `rustup help toolchain` OPTIONS: --path Path to the directory diff --git a/tests/suite/cli-ui/rustup/rustup_override_cmd_set_cmd_help_flag_stdout.toml b/tests/suite/cli-ui/rustup/rustup_override_cmd_set_cmd_help_flag_stdout.toml index b1dc9680b52..e8c9ead5c05 100644 --- a/tests/suite/cli-ui/rustup/rustup_override_cmd_set_cmd_help_flag_stdout.toml +++ b/tests/suite/cli-ui/rustup/rustup_override_cmd_set_cmd_help_flag_stdout.toml @@ -8,8 +8,8 @@ USAGE: rustup[EXE] override set [OPTIONS] ARGS: - Toolchain name, such as 'stable', 'nightly', or '1.8.0'. For more information - see `rustup help toolchain` + Toolchain name, such as 'stable', 'nightly', '1.8.0', or a custom toolchain + name. For more information see `rustup help toolchain` OPTIONS: --path Path to the directory diff --git a/tests/suite/cli-ui/rustup/rustup_run_cmd_help_flag_stdout.toml b/tests/suite/cli-ui/rustup/rustup_run_cmd_help_flag_stdout.toml index 9b38e693516..0760ac34656 100644 --- a/tests/suite/cli-ui/rustup/rustup_run_cmd_help_flag_stdout.toml +++ b/tests/suite/cli-ui/rustup/rustup_run_cmd_help_flag_stdout.toml @@ -1,5 +1,6 @@ +args = ["run", "--help"] bin.name = "rustup" -args = ["run","--help"] +stderr = "" stdout = """ ... Run a command with an environment configured for a given toolchain @@ -8,8 +9,8 @@ USAGE: rustup[EXE] run [OPTIONS] ... ARGS: - Toolchain name, such as 'stable', 'nightly', or '1.8.0'. For more - information see `rustup help toolchain` + Toolchain name, such as 'stable', 'nightly', '1.8.0', or a custom toolchain + name, or an absolute path. For more information see `rustup help toolchain` ... OPTIONS: @@ -31,4 +32,3 @@ DISCUSSION: $ rustup run nightly cargo build """ -stderr = "" diff --git a/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_help_flag_stdout.toml b/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_help_flag_stdout.toml index 7a9bc0faae2..a555542b272 100644 --- a/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_help_flag_stdout.toml +++ b/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_help_flag_stdout.toml @@ -22,8 +22,8 @@ DISCUSSION: installation of the Rust compiler. `rustup` supports multiple types of toolchains. The most basic track the official release channels: 'stable', 'beta' and 'nightly'; but `rustup` can also - install toolchains from the official archives, for alternate host - platforms, and from local builds. + install specific toolchains from the official archives, toolchains for + alternate host platforms, and from local builds ('custom toolchains'). Standard release channel toolchain names have the following form: diff --git a/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_link_cmd_help_flag_stdout.toml b/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_link_cmd_help_flag_stdout.toml index 57bb36057b7..e931c63ae30 100644 --- a/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_link_cmd_help_flag_stdout.toml +++ b/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_link_cmd_help_flag_stdout.toml @@ -16,10 +16,12 @@ OPTIONS: DISCUSSION: 'toolchain' is the custom name to be assigned to the new toolchain. - Any name is permitted as long as it does not fully match an initial - substring of a standard release channel. For example, you can use - the names 'latest' or '2017-04-01' but you cannot use 'stable' or - 'beta-i686' or 'nightly-x86_64-unknown-linux-gnu'. + Any name is permitted as long as: + - it does not include '/' or '/' except as the last character + - it is not equal to 'none' + - it does not fully match an initialsubstring of a standard release channel. + For example, you can use the names 'latest' or '2017-04-01' but you cannot + use 'stable' or 'beta-i686' or 'nightly-x86_64-unknown-linux-gnu'. 'path' specifies the directory where the binaries and libraries for the custom toolchain can be found. For example, when used for diff --git a/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_uninstall_cmd_help_flag_stdout.toml b/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_uninstall_cmd_help_flag_stdout.toml index 66a9f9b1d12..08aa5aac0a6 100644 --- a/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_uninstall_cmd_help_flag_stdout.toml +++ b/tests/suite/cli-ui/rustup/rustup_toolchain_cmd_uninstall_cmd_help_flag_stdout.toml @@ -8,8 +8,8 @@ USAGE: rustup[EXE] toolchain uninstall ... ARGS: - ... Toolchain name, such as 'stable', 'nightly', or '1.8.0'. For more - information see `rustup help toolchain` + ... Toolchain name, such as 'stable', 'nightly', '1.8.0', or a custom + toolchain name. For more information see `rustup help toolchain` OPTIONS: -h, --help Print help information diff --git a/tests/suite/cli-ui/rustup/rustup_unknown_arg_stdout.toml b/tests/suite/cli-ui/rustup/rustup_unknown_arg_stdout.toml index 7508714f03e..f6e15c12765 100644 --- a/tests/suite/cli-ui/rustup/rustup_unknown_arg_stdout.toml +++ b/tests/suite/cli-ui/rustup/rustup_unknown_arg_stdout.toml @@ -3,7 +3,7 @@ args = ["random"] status.code = 1 stdout = "" stderr = """ -error: Invalid value \"random\" for '<+toolchain>': \"random\" is not a valid subcommand, so it was interpreted as a toolchain name, but it is also invalid. To override the toolchain using the 'rustup +toolchain' syntax, make sure to prefix the toolchain override with a '+' +error: Invalid value \"random\" for '<+toolchain>': error: \"random\" is not a valid subcommand, so it was interpreted as a toolchain name, but it is also invalid. To override the toolchain using the 'rustup +toolchain' syntax, make sure to prefix the toolchain override with a '+' For more information try --help """ diff --git a/tests/suite/cli-ui/rustup/rustup_which_cmd_help_flag_stdout.toml b/tests/suite/cli-ui/rustup/rustup_which_cmd_help_flag_stdout.toml index 97a4c96cd1a..799e5ae9af7 100644 --- a/tests/suite/cli-ui/rustup/rustup_which_cmd_help_flag_stdout.toml +++ b/tests/suite/cli-ui/rustup/rustup_which_cmd_help_flag_stdout.toml @@ -1,5 +1,6 @@ +args = ["which", "--help"] bin.name = "rustup" -args = ["which","--help"] +stderr = "" stdout = """ ... Display which binary will be run for a given command @@ -11,8 +12,7 @@ ARGS: OPTIONS: - --toolchain Toolchain name, such as 'stable', 'nightly', or '1.8.0'. For more - information see `rustup help toolchain` + --toolchain Toolchain name, such as 'stable', 'nightly', '1.8.0', or a custom + toolchain name. For more information see `rustup help toolchain` -h, --help Print help information """ -stderr = "" diff --git a/tests/suite/cli_exact.rs b/tests/suite/cli_exact.rs index 21a6d8a1d29..9c53486809d 100644 --- a/tests/suite/cli_exact.rs +++ b/tests/suite/cli_exact.rs @@ -2,11 +2,13 @@ //! is exactly as expected. use rustup::for_host; -use rustup::test::this_host_triple; +use rustup::test::{ + mock::clitools::{self, set_current_dist_date, with_update_server, Config, Scenario}, + this_host_triple, +}; use rustup_macros::integration_test as test; -use crate::mock::clitools::{self, set_current_dist_date, with_update_server, Config, Scenario}; - +/// Start a test with Scenario::None fn test(f: &dyn Fn(&mut Config)) { clitools::test(Scenario::None, f); } @@ -313,15 +315,9 @@ fn override_again() { config.expect_ok(&["rustup", "override", "add", "nightly"]); config.expect_ok_ex( &["rustup", "override", "add", "nightly"], - for_host!( - r" - nightly-{} unchanged - 1.3.0 (hash-nightly-2) - -" - ), + "", &format!( - r"info: using existing install for 'nightly-{1}' -info: override toolchain for '{}' set to 'nightly-{1}' + r"info: override toolchain for '{}' set to 'nightly-{1}' ", cwd.display(), &this_host_triple() @@ -551,41 +547,32 @@ error: no release found for 'nightly-2016-01-01' // Issue #111 #[test] -fn update_invalid_toolchain() { +fn update_custom_toolchain() { test(&|config| { - config.with_scenario(Scenario::SimpleV2, &|config| { - config.expect_err_ex( + // installable toolchains require 2 digits in the DD and MM fields, so this is + // treated as a custom toolchain, which can't be used with update. + config.expect_err( &["rustup", "update", "nightly-2016-03-1"], - r"", - r"info: syncing channel updates for 'nightly-2016-03-1' -info: latest update on 2015-01-02, rust version 1.3.0 (hash-nightly-2) -error: target '2016-03-1' not found in channel. Perhaps check https://doc.rust-lang.org/nightly/rustc/platform-support.html for available targets -", + "invalid toolchain name: 'nightly-2016-03-1'", ); - }) }); } #[test] -fn default_invalid_toolchain() { +fn default_custom_not_installed_toolchain() { test(&|config| { - config.with_scenario(Scenario::SimpleV2, &|config| { - config.expect_err_ex( + // installable toolchains require 2 digits in the DD and MM fields, so this is + // treated as a custom toolchain, which isn't installed. + config.expect_err( &["rustup", "default", "nightly-2016-03-1"], - r"", - r"info: syncing channel updates for 'nightly-2016-03-1' -info: latest update on 2015-01-02, rust version 1.3.0 (hash-nightly-2) -error: target '2016-03-1' not found in channel. Perhaps check https://doc.rust-lang.org/nightly/rustc/platform-support.html for available targets -", + "toolchain 'nightly-2016-03-1' is not installed", ); - }) }); } #[test] fn default_none() { test(&|config| { - config.with_scenario(Scenario::SimpleV2, &|config| { config.expect_stderr_ok( &["rustup", "default", "none"], "info: default toolchain unset", @@ -598,7 +585,6 @@ help: run 'rustup default stable' to download the latest stable release of Rust ", ); }) - }) } #[test] @@ -672,7 +658,7 @@ fn undefined_linked_toolchain() { config.expect_err_ex( &["cargo", "+bogus", "test"], r"", - "error: toolchain 'bogus' is not installed\n", + "error: toolchain 'bogus' is not installable\n", ); }) }); diff --git a/tests/suite/cli_inst_interactive.rs b/tests/suite/cli_inst_interactive.rs index 4d73cea9bf3..61382a6b7e9 100644 --- a/tests/suite/cli_inst_interactive.rs +++ b/tests/suite/cli_inst_interactive.rs @@ -5,13 +5,13 @@ use std::io::Write; use std::process::Stdio; use rustup::for_host; -use rustup::test::this_host_triple; -use rustup::test::with_saved_path; +use rustup::test::{ + mock::clitools::{self, set_current_dist_date, Config, SanitizedOutput, Scenario}, + this_host_triple, with_saved_path, +}; use rustup::utils::raw; use rustup_macros::integration_test as test; -use crate::mock::clitools::{self, set_current_dist_date, Config, SanitizedOutput, Scenario}; - fn run_input(config: &Config, args: &[&str], input: &str) -> SanitizedOutput { run_input_with_env(config, args, input, &[]) } @@ -526,7 +526,7 @@ fn install_stops_if_rustc_exists() { clitools::test(Scenario::SimpleV2, &|config| { let out = config.run( "rustup-init", - &["--no-modify-path"], + ["--no-modify-path"], &[ ("RUSTUP_INIT_SKIP_PATH_CHECK", "no"), ("PATH", temp_dir_path), @@ -556,7 +556,7 @@ fn install_stops_if_cargo_exists() { clitools::test(Scenario::SimpleV2, &|config| { let out = config.run( "rustup-init", - &["--no-modify-path"], + ["--no-modify-path"], &[ ("RUSTUP_INIT_SKIP_PATH_CHECK", "no"), ("PATH", temp_dir_path), @@ -586,7 +586,7 @@ fn with_no_prompt_install_succeeds_if_rustc_exists() { clitools::test(Scenario::SimpleV2, &|config| { let out = config.run( "rustup-init", - &["-y", "--no-modify-path"], + ["-y", "--no-modify-path"], &[ ("RUSTUP_INIT_SKIP_PATH_CHECK", "no"), ("PATH", temp_dir_path), diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index 1b0f92953e8..27f5f861521 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -5,12 +5,13 @@ use std::str; use std::{env::consts::EXE_SUFFIX, path::Path}; use rustup::for_host; -use rustup::test::this_host_triple; +use rustup::test::{ + mock::clitools::{self, set_current_dist_date, Config, Scenario}, + this_host_triple, +}; use rustup::utils::utils; use rustup_macros::integration_test as test; -use crate::mock::clitools::{self, set_current_dist_date, Config, Scenario}; - pub fn setup(f: &dyn Fn(&mut Config)) { clitools::test(Scenario::SimpleV2, f); } @@ -25,13 +26,13 @@ fn smoke_test() { #[test] fn version_mentions_rustc_version_confusion() { setup(&|config| { - let out = config.run("rustup", &vec!["--version"], &[]); + let out = config.run("rustup", vec!["--version"], &[]); assert!(out.ok); assert!(out .stderr .contains("This is the version for the rustup toolchain manager")); - let out = config.run("rustup", &vec!["+nightly", "--version"], &[]); + let out = config.run("rustup", vec!["+nightly", "--version"], &[]); assert!(out.ok); assert!(out .stderr @@ -43,7 +44,7 @@ fn version_mentions_rustc_version_confusion() { fn no_colors_in_piped_error_output() { setup(&|config| { let args: Vec<&str> = vec![]; - let out = config.run("rustc", &args, &[]); + let out = config.run("rustc", args, &[]); assert!(!out.ok); assert!(!out.stderr.contains('\x1b')); }); @@ -53,7 +54,7 @@ fn no_colors_in_piped_error_output() { fn rustc_with_bad_rustup_toolchain_env_var() { setup(&|config| { let args: Vec<&str> = vec![]; - let out = config.run("rustc", &args, &[("RUSTUP_TOOLCHAIN", "bogus")]); + let out = config.run("rustc", args, &[("RUSTUP_TOOLCHAIN", "bogus")]); assert!(!out.ok); assert!(out.stderr.contains("toolchain 'bogus' is not installed")); }); @@ -64,15 +65,15 @@ fn custom_invalid_names() { setup(&|config| { config.expect_err( &["rustup", "toolchain", "link", "nightly", "foo"], - for_host!("invalid custom toolchain name: 'nightly-{0}'"), + "invalid custom toolchain name 'nightly'", ); config.expect_err( &["rustup", "toolchain", "link", "beta", "foo"], - for_host!("invalid custom toolchain name: 'beta-{0}'"), + "invalid custom toolchain name 'beta'", ); config.expect_err( &["rustup", "toolchain", "link", "stable", "foo"], - for_host!("invalid custom toolchain name: 'stable-{0}'"), + "invalid custom toolchain name 'stable'", ); }); } @@ -82,15 +83,15 @@ fn custom_invalid_names_with_archive_dates() { setup(&|config| { config.expect_err( &["rustup", "toolchain", "link", "nightly-2015-01-01", "foo"], - for_host!("invalid custom toolchain name: 'nightly-2015-01-01-{0}'"), + "invalid custom toolchain name 'nightly-2015-01-01'", ); config.expect_err( &["rustup", "toolchain", "link", "beta-2015-01-01", "foo"], - for_host!("invalid custom toolchain name: 'beta-2015-01-01-{0}'"), + "invalid custom toolchain name 'beta-2015-01-01'", ); config.expect_err( &["rustup", "toolchain", "link", "stable-2015-01-01", "foo"], - for_host!("invalid custom toolchain name: 'stable-2015-01-01-{0}'"), + "invalid custom toolchain name 'stable-2015-01-01'", ); }); } @@ -532,7 +533,7 @@ fn run_rls_when_not_installed() { config.expect_err( &["rls", "--version"], &format!( - "'rls{}' is not installed for the toolchain 'stable-{}'\nTo install, run `rustup component add rls`", + "'rls{}' is not installed for the toolchain 'stable-{}'.\nTo install, run `rustup component add rls`", EXE_SUFFIX, this_host_triple(), ), @@ -611,7 +612,7 @@ fn rename_rls_list() { config.expect_ok(&["rustup", "update"]); config.expect_ok(&["rustup", "component", "add", "rls"]); - let out = config.run("rustup", &["component", "list"], &[]); + let out = config.run("rustup", ["component", "list"], &[]); assert!(out.ok); assert!(out.stdout.contains(&format!("rls-{}", this_host_triple()))); }); @@ -627,7 +628,7 @@ fn rename_rls_preview_list() { config.expect_ok(&["rustup", "update"]); config.expect_ok(&["rustup", "component", "add", "rls-preview"]); - let out = config.run("rustup", &["component", "list"], &[]); + let out = config.run("rustup", ["component", "list"], &[]); assert!(out.ok); assert!(out.stdout.contains(&format!("rls-{}", this_host_triple()))); }); @@ -679,7 +680,7 @@ fn toolchain_broken_symlink() { fs::symlink_dir(src, dst).unwrap(); } - setup(&|config| { + clitools::test(Scenario::None, &|config| { // We artificially create a broken symlink toolchain -- but this can also happen "legitimately" // by having a proper toolchain there, using "toolchain link", and later removing the directory. fs::create_dir(config.rustupdir.join("toolchains")).unwrap(); @@ -697,11 +698,9 @@ fn toolchain_broken_symlink() { info: toolchain 'test' uninstalled ", ); - config.expect_ok_ex( + config.expect_stderr_ok( &["rustup", "toolchain", "uninstall", "test"], - "", - r"info: no toolchain installed for 'test' -", + "no toolchain installed for 'test'", ); }); } @@ -900,7 +899,7 @@ fn which_asking_uninstalled_toolchain() { ); config.expect_err( &["rustup", "which", "--toolchain=nightly", "rustc"], - "toolchain 'nightly' is not installed", + for_host!("toolchain 'nightly-{}' is not installed"), ); }); } diff --git a/tests/suite/cli_paths.rs b/tests/suite/cli_paths.rs index 7c155dcd5af..762af45d21d 100644 --- a/tests/suite/cli_paths.rs +++ b/tests/suite/cli_paths.rs @@ -15,7 +15,7 @@ mod unix { use rustup_macros::integration_test as test; use super::INIT_NONE; - use crate::mock::clitools::{self, Scenario}; + use rustup::test::mock::clitools::{self, Scenario}; // Let's write a fake .rc which looks vaguely like a real script. const FAKE_RC: &str = r#" @@ -351,11 +351,11 @@ export PATH="$HOME/apple/bin" #[cfg(windows)] mod windows { + use rustup::test::mock::clitools::{self, Scenario}; use rustup::test::{get_path, with_saved_path}; use rustup_macros::integration_test as test; use super::INIT_NONE; - use crate::mock::clitools::{self, Scenario}; #[test] /// Smoke test for end-to-end code connectivity of the installer path mgmt on windows. diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 8b8588e90c9..304ff48bfd1 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -1,15 +1,15 @@ //! Test cases for new rustup UI -use std::env::consts::EXE_SUFFIX; use std::fs; use std::path::{PathBuf, MAIN_SEPARATOR}; +use std::{env::consts::EXE_SUFFIX, path::Path}; use rustup::for_host; use rustup::test::this_host_triple; use rustup::utils::raw; use rustup_macros::integration_test as test; -use crate::mock::{ +use rustup::test::mock::{ self, clitools::{self, Config, Scenario}, }; @@ -579,7 +579,7 @@ fn recursive_cargo() { // The solution here is to copy from the "mock" `cargo.exe` into // `~/.cargo/bin/cargo-foo`. This is just for convenience to avoid // needing to build another executable just for this test. - let output = config.run("rustup", &["which", "cargo"], &[]); + let output = config.run("rustup", ["which", "cargo"], &[]); let real_mock_cargo = output.stdout.trim(); let cargo_bin_path = config.cargodir.join("bin"); let cargo_subcommand = cargo_bin_path.join(format!("cargo-foo{}", EXE_SUFFIX)); @@ -1069,14 +1069,10 @@ fn show_toolchain_env() { test(&|config| { config.with_scenario(Scenario::SimpleV2, &|config| { config.expect_ok(&["rustup", "default", "nightly"]); - let mut cmd = clitools::cmd(config, "rustup", ["show"]); - clitools::env(config, &mut cmd); - cmd.env("RUSTUP_TOOLCHAIN", "nightly"); - let out = cmd.output().unwrap(); - assert!(out.status.success()); - let stdout = String::from_utf8(out.stdout).unwrap(); + let out = config.run("rustup", ["show"], &[("RUSTUP_TOOLCHAIN", "nightly")]); + assert!(out.ok); assert_eq!( - &stdout, + &out.stdout, for_host_and_home!( config, r"Default host: {0} @@ -1323,13 +1319,12 @@ fn toolchain_update_is_like_update() { #[test] fn toolchain_uninstall_is_like_uninstall() { test(&|config| { + config.with_scenario(Scenario::SimpleV2, &|config| { + config.expect_ok(&["rustup", "toolchain", "install", "nightly"]); + }); + config.expect_ok(&["rustup", "default", "none"]); config.expect_ok(&["rustup", "uninstall", "nightly"]); - let mut cmd = clitools::cmd(config, "rustup", ["show"]); - clitools::env(config, &mut cmd); - let out = cmd.output().unwrap(); - assert!(out.status.success()); - let stdout = String::from_utf8(out.stdout).unwrap(); - assert!(!stdout.contains(for_host!("'nightly-2015-01-01-{}'"))); + config.expect_not_stdout_ok(&["rustup", "show"], for_host!("'nightly-{}'")); }); } @@ -1446,30 +1441,52 @@ fn env_override_path() { .join("toolchains") .join(format!("nightly-{}", this_host_triple())); - let mut cmd = clitools::cmd(config, "rustc", ["--version"]); - clitools::env(config, &mut cmd); - cmd.env("RUSTUP_TOOLCHAIN", toolchain_path.to_str().unwrap()); + let out = config.run( + "rustc", + ["--version"], + &[("RUSTUP_TOOLCHAIN", toolchain_path.to_str().unwrap())], + ); + assert!(out.ok); + assert!(out.stdout.contains("hash-nightly-2")); + }) + }); +} - let out = cmd.output().unwrap(); - assert!(String::from_utf8(out.stdout) - .unwrap() - .contains("hash-nightly-2")); +#[test] +fn plus_override_relpath_is_not_supported() { + test(&|config| { + config.with_scenario(Scenario::SimpleV2, &|config| { + config.expect_ok(&["rustup", "default", "stable"]); + config.expect_ok(&["rustup", "toolchain", "install", "nightly"]); + + let toolchain_path = Path::new("..") + .join(config.rustupdir.rustupdir.file_name().unwrap()) + .join("toolchains") + .join(format!("nightly-{}", this_host_triple())); + config.expect_err( + &[ + "rustc", + format!("+{}", toolchain_path.to_str().unwrap()).as_str(), + "--version", + ], + "error: relative path toolchain", + ); }) }); } #[test] -fn plus_override_path() { +fn run_with_relpath_is_not_supported() { test(&|config| { config.with_scenario(Scenario::SimpleV2, &|config| { config.expect_ok(&["rustup", "default", "stable"]); config.expect_ok(&["rustup", "toolchain", "install", "nightly"]); - let toolchain_path = config - .rustupdir + let toolchain_path = Path::new("..") + .join(config.rustupdir.rustupdir.file_name().unwrap()) .join("toolchains") .join(format!("nightly-{}", this_host_triple())); - config.expect_stdout_ok( + config.expect_err( &[ "rustup", "run", @@ -1477,12 +1494,58 @@ fn plus_override_path() { "rustc", "--version", ], - "hash-nightly-2", + "relative path toolchain", ); }) }); } +#[test] +fn plus_override_abspath_is_supported() { + test(&|config| { + config.with_scenario(Scenario::SimpleV2, &|config| { + config.expect_ok(&["rustup", "default", "stable"]); + config.expect_ok(&["rustup", "toolchain", "install", "nightly"]); + + let toolchain_path = config + .rustupdir + .join("toolchains") + .join(format!("nightly-{}", this_host_triple())) + .canonicalize() + .unwrap(); + config.expect_ok(&[ + "rustc", + format!("+{}", toolchain_path.to_str().unwrap()).as_str(), + "--version", + ]); + }) + }); +} + +#[test] +fn run_with_abspath_is_supported() { + test(&|config| { + config.with_scenario(Scenario::SimpleV2, &|config| { + config.expect_ok(&["rustup", "default", "stable"]); + config.expect_ok(&["rustup", "toolchain", "install", "nightly"]); + + let toolchain_path = config + .rustupdir + .join("toolchains") + .join(format!("nightly-{}", this_host_triple())) + .canonicalize() + .unwrap(); + config.expect_ok(&[ + "rustup", + "run", + toolchain_path.to_str().unwrap(), + "rustc", + "--version", + ]); + }) + }); +} + #[test] fn file_override_path() { test(&|config| { @@ -1536,7 +1599,7 @@ fn proxy_override_path() { } #[test] -fn file_override_path_relative() { +fn file_override_path_relative_not_supported() { test(&|config| { config.with_scenario(Scenario::SimpleV2, &|config| { config.expect_ok(&["rustup", "default", "stable"]); @@ -1580,7 +1643,7 @@ fn file_override_path_relative() { let ephemeral = config.current_dir().join("ephemeral"); fs::create_dir_all(&ephemeral).unwrap(); config.change_dir(&ephemeral, &|config| { - config.expect_stdout_ok(&["rustc", "--version"], "hash-nightly-2"); + config.expect_err(&["rustc", "--version"], "relative path toolchain"); }); }) }); @@ -1965,13 +2028,25 @@ fn plus_override_beats_file_override() { } #[test] -fn bad_file_override() { +fn file_override_not_installed_custom() { test(&|config| { let cwd = config.current_dir(); let toolchain_file = cwd.join("rust-toolchain"); raw::write_file(&toolchain_file, "gumbo").unwrap(); - config.expect_err(&["rustc", "--version"], "invalid toolchain name: 'gumbo'"); + config.expect_err(&["rustc", "--version"], "custom and not installed"); + }); +} + +#[test] +fn bad_file_override() { + test(&|config| { + let cwd = config.current_dir(); + let toolchain_file = cwd.join("rust-toolchain"); + // invalid name - cannot specify no toolchain in a toolchain file + raw::write_file(&toolchain_file, "none").unwrap(); + + config.expect_err(&["rustc", "--version"], "invalid toolchain name 'none'"); }); } @@ -1984,6 +2059,7 @@ fn valid_override_settings() { config.expect_ok(&["rustup", "default", "nightly"]); raw::write_file(&toolchain_file, "nightly").unwrap(); config.expect_ok(&["rustc", "--version"]); + // Special case: same version as is installed is permitted. raw::write_file(&toolchain_file, for_host!("nightly-{}")).unwrap(); config.expect_ok(&["rustc", "--version"]); let fullpath = config @@ -2006,17 +2082,17 @@ fn valid_override_settings() { #[test] fn file_override_with_target_info() { + // Target info is not portable between machines, so we reject toolchain + // files that include it. test(&|config| { - config.with_scenario(Scenario::SimpleV2, &|config| { - let cwd = config.current_dir(); - let toolchain_file = cwd.join("rust-toolchain"); - raw::write_file(&toolchain_file, "nightly-x86_64-unknown-linux-gnu").unwrap(); + let cwd = config.current_dir(); + let toolchain_file = cwd.join("rust-toolchain"); + raw::write_file(&toolchain_file, "nightly-x86_64-unknown-linux-gnu").unwrap(); - config.expect_err( - &["rustc", "--version"], - "target triple in channel name 'nightly-x86_64-unknown-linux-gnu'", - ); - }) + config.expect_err( + &["rustc", "--version"], + "target triple in channel name 'nightly-x86_64-unknown-linux-gnu'", + ); }); } @@ -2034,15 +2110,10 @@ fn docs_with_path() { let path = format!("share{MAIN_SEPARATOR}doc{MAIN_SEPARATOR}rust{MAIN_SEPARATOR}html"); assert!(String::from_utf8(out.stdout).unwrap().contains(&path)); - let mut cmd = clitools::cmd( - config, - "rustup", - ["doc", "--path", "--toolchain", "nightly"], + config.expect_stdout_ok( + &["rustup", "doc", "--path", "--toolchain", "nightly"], + "nightly", ); - clitools::env(config, &mut cmd); - - let out = cmd.output().unwrap(); - assert!(String::from_utf8(out.stdout).unwrap().contains("nightly")); }) }); } @@ -2088,13 +2159,11 @@ fn docs_missing() { #[test] fn docs_custom() { test(&|config| { - config.with_scenario(Scenario::SimpleV2, &|config| { - let path = config.customdir.join("custom-1"); - let path = path.to_string_lossy(); - config.expect_ok(&["rustup", "toolchain", "link", "custom", &path]); - config.expect_ok(&["rustup", "default", "custom"]); - config.expect_stdout_ok(&["rustup", "doc", "--path"], "custom"); - }) + let path = config.customdir.join("custom-1"); + let path = path.to_string_lossy(); + config.expect_ok(&["rustup", "toolchain", "link", "custom", &path]); + config.expect_ok(&["rustup", "default", "custom"]); + config.expect_stdout_ok(&["rustup", "doc", "--path"], "custom"); }); } @@ -2132,7 +2201,7 @@ fn non_utf8_arg() { config.expect_ok(&["rustup", "default", "nightly"]); let out = config.run( "rustc", - &[ + [ OsString::from("--echo-args".to_string()), OsString::from("echoed non-utf8 arg:".to_string()), OsString::from_wide(&[0xd801, 0xd801]), @@ -2158,7 +2227,7 @@ fn non_utf8_toolchain() { &[OsStr::from_bytes(b"+\xc3\x28")], &[("RUST_BACKTRACE", "1")], ); - assert!(out.stderr.contains("toolchain '�(' is not installed")); + assert!(out.stderr.contains("toolchain '�(' is not installable")); }) }); } @@ -2174,10 +2243,10 @@ fn non_utf8_toolchain() { config.expect_ok(&["rustup", "default", "nightly"]); let out = config.run( "rustc", - &[OsString::from_wide(&[u16::from(b'+'), 0xd801, 0xd801])], + [OsString::from_wide(&[u16::from(b'+'), 0xd801, 0xd801])], &[("RUST_BACKTRACE", "1")], ); - assert!(out.stderr.contains("toolchain '��' is not installed")); + assert!(out.stderr.contains("toolchain '��' is not installable")); }) }); } diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index 0ad0be95f82..c21c99abc8e 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -8,16 +8,21 @@ use std::process::Command; use remove_dir_all::remove_dir_all; -use rustup::test::{this_host_triple, with_saved_path}; +use retry::{ + delay::{jitter, Fibonacci}, + retry, OperationResult, +}; +use rustup::test::{ + mock::{ + clitools::{self, output_release_file, self_update_setup, Config, Scenario}, + dist::calc_hash, + }, + this_host_triple, with_saved_path, +}; use rustup::utils::{raw, utils}; -use rustup::{for_host, Notification, DUP_TOOLS, TOOLS}; +use rustup::{for_host, DUP_TOOLS, TOOLS}; use rustup_macros::integration_test as test; -use crate::mock::{ - clitools::{self, output_release_file, self_update_setup, Config, Scenario}, - dist::calc_hash, -}; - const TEST_VERSION: &str = "1.1.1"; pub fn update_setup(f: &dyn Fn(&mut Config, &Path)) { @@ -254,21 +259,30 @@ fn uninstall_self_delete_works() { // file in CONFIG.CARGODIR/.. ; check that it doesn't exist. #[test] fn uninstall_doesnt_leave_gc_file() { - use std::thread; - use std::time::Duration; - setup_empty_installed(&|config| { config.expect_ok(&["rustup", "self", "uninstall", "-y"]); - - // The gc removal happens after rustup terminates. Give it a moment. - thread::sleep(Duration::from_millis(100)); - let parent = config.cargodir.parent().unwrap(); - // Actually, there just shouldn't be any files here - for dirent in fs::read_dir(parent).unwrap() { - let dirent = dirent.unwrap(); - println!("{}", dirent.path().display()); - panic!(); + + // The gc removal happens after rustup terminates. Typically under + // 100ms, but during the contention of test suites can be substantially + // longer while still succeeding. + + #[derive(thiserror::Error, Debug)] + #[error("garbage remaining: {:?}", .0)] + struct GcErr(Vec); + + match retry(Fibonacci::from_millis(1).map(jitter).take(23), || { + let garbage = fs::read_dir(parent) + .unwrap() + .map(|d| d.unwrap().path().to_string_lossy().to_string()) + .collect::>(); + match garbage.len() { + 0 => OperationResult::Ok(()), + _ => OperationResult::Retry(GcErr(garbage)), + } + }) { + Ok(_) => (), + Err(e) => panic!("{e}"), } }) } @@ -345,7 +359,7 @@ fn update_bogus_version() { config.expect_ok(&["rustup-init", "-y", "--no-modify-path"]); config.expect_err( &["rustup", "update", "1.0.0-alpha"], - "could not download nonexistent rust version `1.0.0-alpha`", + "Invalid value \"1.0.0-alpha\" for '...': invalid toolchain name: '1.0.0-alpha'", ); }); } @@ -660,7 +674,7 @@ fn rustup_init_works_with_weird_names() { clitools::test(Scenario::SimpleV2, &|config| { let old = config.exedir.join(format!("rustup-init{EXE_SUFFIX}")); let new = config.exedir.join(format!("rustup-init(2){EXE_SUFFIX}")); - utils::rename_file("test", &old, &new, &|_: Notification<'_>| {}).unwrap(); + fs::rename(old, new).unwrap(); config.expect_ok(&["rustup-init(2)", "-y", "--no-modify-path"]); let rustup = config.cargodir.join(format!("bin/rustup{EXE_SUFFIX}")); assert!(rustup.exists()); @@ -688,7 +702,7 @@ fn install_but_rustup_sh_is_installed() { fn test_warn_succeed_if_rustup_sh_already_installed_y_flag() { clitools::test(Scenario::SimpleV2, &|config| { config.create_rustup_sh_metadata(); - let out = config.run("rustup-init", &["-y", "--no-modify-path"], &[]); + let out = config.run("rustup-init", ["-y", "--no-modify-path"], &[]); assert!(out.ok); assert!(out .stderr @@ -709,7 +723,7 @@ fn test_succeed_if_rustup_sh_already_installed_env_var_set() { config.create_rustup_sh_metadata(); let out = config.run( "rustup-init", - &["-y", "--no-modify-path"], + ["-y", "--no-modify-path"], &[("RUSTUP_INIT_SKIP_EXISTENCE_CHECKS", "yes")], ); assert!(out.ok); @@ -728,7 +742,10 @@ fn test_succeed_if_rustup_sh_already_installed_env_var_set() { #[test] fn rls_proxy_set_up_after_install() { - setup_installed(&|config| { + clitools::test(Scenario::None, &|config| { + config.with_scenario(Scenario::SimpleV2, &|config| { + config.expect_ok(&["rustup-init", "-y", "--no-modify-path"]); + }); config.expect_err( &["rls", "--version"], &format!( diff --git a/tests/suite/cli_v1.rs b/tests/suite/cli_v1.rs index 274701fef95..4a3d6174236 100644 --- a/tests/suite/cli_v1.rs +++ b/tests/suite/cli_v1.rs @@ -6,7 +6,7 @@ use std::fs; use rustup::for_host; use rustup_macros::integration_test as test; -use crate::mock::clitools::{self, set_current_dist_date, Config, Scenario}; +use rustup::test::mock::clitools::{self, set_current_dist_date, Config, Scenario}; pub fn setup(f: &dyn Fn(&mut Config)) { clitools::test(Scenario::SimpleV1, f); diff --git a/tests/suite/cli_v2.rs b/tests/suite/cli_v2.rs index 54d2c4c3aba..f796368029d 100644 --- a/tests/suite/cli_v2.rs +++ b/tests/suite/cli_v2.rs @@ -6,11 +6,10 @@ use std::io::Write; use rustup::dist::dist::TargetTriple; use rustup::for_host; +use rustup::test::mock::clitools::{self, set_current_dist_date, Config, Scenario}; use rustup::test::this_host_triple; use rustup_macros::integration_test as test; -use crate::mock::clitools::{self, set_current_dist_date, Config, Scenario}; - pub fn setup(f: &dyn Fn(&mut Config)) { clitools::test(Scenario::SimpleV2, f); } @@ -772,7 +771,7 @@ fn add_target_v1_toolchain() { clitools::CROSS_ARCH1, "--toolchain=nightly", ], - for_host!("Missing manifest in toolchain 'nightly-{0}'"), + for_host!("toolchain 'nightly-{0}' does not support components (v1 manifest)"), ); }); } @@ -798,7 +797,7 @@ fn cannot_add_empty_named_custom_toolchain() { let path = path.to_string_lossy(); config.expect_err( &["rustup", "toolchain", "link", "", &path], - "toolchain names must not be empty", + "Invalid value \"\" for '': invalid toolchain name ''", ); }); } @@ -914,7 +913,7 @@ fn remove_target_v1_toolchain() { clitools::CROSS_ARCH1, "--toolchain=nightly", ], - for_host!("Missing manifest in toolchain 'nightly-{0}'"), + for_host!("toolchain 'nightly-{0}' does not support components (v1 manifest)"), ); }); } @@ -975,32 +974,30 @@ fn remove_target_missing_update_hash() { // Issue #1777 #[test] fn warn_about_and_remove_stray_hash() { - setup(&|config| { + clitools::test(Scenario::None, &|config| { let mut hash_path = config.rustupdir.join("update-hashes"); fs::create_dir_all(&hash_path).expect("Unable to make the update-hashes directory"); - hash_path.push(for_host!("nightly-{}")); - let mut file = fs::File::create(&hash_path).expect("Unable to open update-hash file"); file.write_all(b"LEGITHASH") .expect("Unable to write update-hash"); drop(file); - config.expect_stderr_ok( - &["rustup", "toolchain", "install", "nightly"], - &format!( - "removing stray hash found at '{}' in order to continue", - hash_path.display() - ), - ); - config.expect_ok(&["rustup", "default", "nightly"]); - config.expect_stdout_ok(&["rustc", "--version"], "1.3.0"); + config.with_scenario(Scenario::SimpleV2, &|config| { + config.expect_stderr_ok( + &["rustup", "toolchain", "install", "nightly"], + &format!( + "removing stray hash found at '{}' in order to continue", + hash_path.display() + ), + ); + }) }); } fn make_component_unavailable(config: &Config, name: &str, target: &str) { - use crate::mock::dist::create_hash; use rustup::dist::manifest::Manifest; + use rustup::test::mock::dist::create_hash; let manifest_path = config .distdir diff --git a/tests/suite/dist_install.rs b/tests/suite/dist_install.rs index c26576a8f54..b6f20dfcc5c 100644 --- a/tests/suite/dist_install.rs +++ b/tests/suite/dist_install.rs @@ -11,7 +11,7 @@ use rustup::dist::Notification; use rustup::utils::utils; use rustup_macros::integration_test as test; -use crate::mock::{MockComponentBuilder, MockFile, MockInstallerBuilder}; +use rustup::test::mock::{MockComponentBuilder, MockFile, MockInstallerBuilder}; // Just testing that the mocks work #[test] diff --git a/tests/suite/dist_manifest.rs b/tests/suite/dist_manifest.rs deleted file mode 100644 index 6d4a75c2da3..00000000000 --- a/tests/suite/dist_manifest.rs +++ /dev/null @@ -1,110 +0,0 @@ -use rustup::dist::dist::TargetTriple; -use rustup::dist::manifest::Manifest; -use rustup::RustupError; -use rustup_macros::integration_test as test; - -// Example manifest from https://public.etherpad-mozilla.org/p/Rust-infra-work-week -static EXAMPLE: &str = include_str!("channel-rust-nightly-example.toml"); -// From brson's live build-rust-manifest.py script -static EXAMPLE2: &str = include_str!("channel-rust-nightly-example2.toml"); - -#[test] -fn parse_smoke_test() { - let x86_64_unknown_linux_gnu = TargetTriple::new("x86_64-unknown-linux-gnu"); - let x86_64_unknown_linux_musl = TargetTriple::new("x86_64-unknown-linux-musl"); - - let pkg = Manifest::parse(EXAMPLE).unwrap(); - - pkg.get_package("rust").unwrap(); - pkg.get_package("rustc").unwrap(); - pkg.get_package("cargo").unwrap(); - pkg.get_package("rust-std").unwrap(); - pkg.get_package("rust-docs").unwrap(); - - let rust_pkg = pkg.get_package("rust").unwrap(); - assert!(rust_pkg.version.contains("1.3.0")); - - let rust_target_pkg = rust_pkg - .get_target(Some(&x86_64_unknown_linux_gnu)) - .unwrap(); - assert!(rust_target_pkg.available()); - assert_eq!(rust_target_pkg.bins[0].1.url, "example.com"); - assert_eq!(rust_target_pkg.bins[0].1.hash, "..."); - - let component = &rust_target_pkg.components[0]; - assert_eq!(component.short_name_in_manifest(), "rustc"); - assert_eq!(component.target.as_ref(), Some(&x86_64_unknown_linux_gnu)); - - let component = &rust_target_pkg.components[4]; - assert_eq!(component.short_name_in_manifest(), "rust-std"); - assert_eq!(component.target.as_ref(), Some(&x86_64_unknown_linux_musl)); - - let docs_pkg = pkg.get_package("rust-docs").unwrap(); - let docs_target_pkg = docs_pkg - .get_target(Some(&x86_64_unknown_linux_gnu)) - .unwrap(); - assert_eq!(docs_target_pkg.bins[0].1.url, "example.com"); -} - -#[test] -fn renames() { - let manifest = Manifest::parse(EXAMPLE2).unwrap(); - assert_eq!(1, manifest.renames.len()); - assert_eq!(manifest.renames["cargo-old"], "cargo"); - assert_eq!(1, manifest.reverse_renames.len()); - assert_eq!(manifest.reverse_renames["cargo"], "cargo-old"); -} - -#[test] -fn parse_round_trip() { - let original = Manifest::parse(EXAMPLE).unwrap(); - let serialized = original.clone().stringify(); - let new = Manifest::parse(&serialized).unwrap(); - assert_eq!(original, new); - - let original = Manifest::parse(EXAMPLE2).unwrap(); - let serialized = original.clone().stringify(); - let new = Manifest::parse(&serialized).unwrap(); - assert_eq!(original, new); -} - -#[test] -fn validate_components_have_corresponding_packages() { - let manifest = r#" -manifest-version = "2" -date = "2015-10-10" -[pkg.rust] - version = "rustc 1.3.0 (9a92aaf19 2015-09-15)" - [pkg.rust.target.x86_64-unknown-linux-gnu] - available = true - url = "example.com" - hash = "..." - [[pkg.rust.target.x86_64-unknown-linux-gnu.components]] - pkg = "rustc" - target = "x86_64-unknown-linux-gnu" - [[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] - pkg = "rust-std" - target = "x86_64-unknown-linux-musl" -[pkg.rustc] - version = "rustc 1.3.0 (9a92aaf19 2015-09-15)" - [pkg.rustc.target.x86_64-unknown-linux-gnu] - available = true - url = "example.com" - hash = "..." -"#; - - let err = Manifest::parse(manifest).unwrap_err(); - - match err.downcast::().unwrap() { - RustupError::MissingPackageForComponent(_) => {} - _ => panic!(), - } -} - -// #248 -#[test] -fn manifest_can_contain_unknown_targets() { - let manifest = EXAMPLE.replace("x86_64-unknown-linux-gnu", "mycpu-myvendor-myos"); - - assert!(Manifest::parse(&manifest).is_ok()); -} diff --git a/tests/suite/mod.rs b/tests/suite/mod.rs index afb91fd4a25..ddd7f59cb82 100644 --- a/tests/suite/mod.rs +++ b/tests/suite/mod.rs @@ -7,7 +7,4 @@ mod cli_self_upd; mod cli_ui; mod cli_v1; mod cli_v2; -mod dist; mod dist_install; -mod dist_manifest; -mod dist_transactions; diff --git a/tests/test_bonanza.rs b/tests/test_bonanza.rs index 0941d0b40a4..7a4d0e3b249 100644 --- a/tests/test_bonanza.rs +++ b/tests/test_bonanza.rs @@ -1,2 +1 @@ -mod mock; mod suite;