Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ version = "0.1.5"
authors = ["Satoshi Terasaki <terasakisatoshi.math@gmail.com>"]

[deps]
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Scratch = "6c6a2e73-6563-6170-7368-637461726353"

[compat]
Pkg = "1"
Scratch = "1"
julia = "1.10"

[extras]
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Downloads", "TOML", "Test"]
test = ["TOML", "Test"]
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ run(`$(cargo()) build --release`)

## Internal Implementation

This package uses Julia's Artifacts system to automatically download and manage Rust toolchains. On first use, the appropriate Rust toolchain for your platform will be automatically downloaded.
This package uses Julia's Artifacts system to automatically download and manage Rust toolchains on Unix-like platforms. On first use, the appropriate Rust toolchain for your platform will be automatically downloaded.

Windows uses a different installation path. Instead of installing the large Rust distribution tarball through Julia Artifacts, RustToolChain.jl downloads `rustup-init.exe` from `https://win.rustup.rs` and installs the Rust version recorded in `Artifacts.toml` with `rustup toolchain install --profile complete`. The installation is isolated under this package's Julia scratchspace by setting package-local `RUSTUP_HOME` and `CARGO_HOME` directories. It does not modify the user's `PATH` or the user's existing Rust installation.

The `cargo()` and `rustc()` functions return commands that use the isolated Windows toolchain when no system `cargo` or `rustc` is available.

## Development

Expand Down
155 changes: 152 additions & 3 deletions src/RustToolChain.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,137 @@ module RustToolChain

const ARTIFACTS_TOML = joinpath(pkgdir(@__MODULE__), "Artifacts.toml")
const EXE_EXT = Sys.iswindows() ? ".exe" : ""
const REQUIRED_TOOLS = ("cargo", "rustc")
const WINDOWS_REQUIRED_TOOLS = ("cargo", "rustc", "rustfmt", "clippy-driver")
const DIST_METADATA_FILES = Set((
"components",
"install.sh",
"manifest.in",
"rust-installer-version",
"uninstall.sh",
))

export cargo, rustc

using Pkg.Artifacts: ensure_artifact_installed, artifact_path, artifact_hash
using Downloads: download
using Pkg.Artifacts: artifact_hash, artifact_meta, artifact_path, ensure_artifact_installed
using Scratch: @get_scratch!

function _toolchain_prefix_ready(prefix::AbstractString)
return all(tool -> isfile(joinpath(prefix, "bin", tool * EXE_EXT)), REQUIRED_TOOLS)
end

function _copy_tree!(src::AbstractString, dst::AbstractString)
if isdir(src)
mkpath(dst)
for name in readdir(src)
_copy_tree!(joinpath(src, name), joinpath(dst, name))
end
else
mkpath(dirname(dst))
cp(src, dst; force=true)
end
return nothing
end

function _dist_components(rust_dir::AbstractString)
components_file = joinpath(rust_dir, "components")
if isfile(components_file)
return filter(!isempty, strip.(readlines(components_file)))
end

return filter(name -> isdir(joinpath(rust_dir, name)), readdir(rust_dir))
end

function _install_from_unpacked_dist!(rust_dir::AbstractString, prefix::AbstractString)
mkpath(prefix)

for component in _dist_components(rust_dir)
component_dir = joinpath(rust_dir, component)
isdir(component_dir) || continue

for name in readdir(component_dir)
name in DIST_METADATA_FILES && continue
_copy_tree!(joinpath(component_dir, name), joinpath(prefix, name))
end
end

_toolchain_prefix_ready(prefix) || error(
"Rust toolchain artifact was installed to $prefix, but cargo/rustc were not found",
)

return prefix
end

function _rust_version_from_artifact_metadata()
meta = artifact_meta("RustToolChain", ARTIFACTS_TOML)
downloads = get(meta, "download", [])
isempty(downloads) && error("RustToolChain artifact metadata does not contain a download URL")

url = get(first(downloads), "url", "")
matched = match(r"rust-(\d+\.\d+\.\d+)-", url)
matched === nothing && error("Could not determine Rust version from artifact URL: $url")
return matched.captures[1]
end

function _windows_rustup_init_url()
arch = Sys.ARCH === :aarch64 ? "aarch64" : "x86_64"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Map i686 Windows to the correct rustup architecture

This architecture mapping falls back to "x86_64" for every non-aarch64 target, so Sys.ARCH == :i686 will incorrectly download/use x86_64 tooling. On 32-bit Windows hosts this breaks toolchain installation even though Artifacts.toml includes an i686 Windows target. Add an explicit i686 mapping (and a hard error for unsupported arches) instead of defaulting to x86_64.

Useful? React with 👍 / 👎.

return "https://win.rustup.rs/$arch"
end

function _windows_host_triple()
arch = Sys.ARCH === :aarch64 ? "aarch64" : "x86_64"
return "$arch-pc-windows-msvc"
end

function _windows_rustup_env(toolchain::AbstractString)
scratch = @get_scratch!("rustup-windows")
rustup_home = joinpath(scratch, "rustup")
cargo_home = joinpath(scratch, "cargo")

env = copy(ENV)
env["RUSTUP_HOME"] = rustup_home
env["CARGO_HOME"] = cargo_home
env["RUSTUP_TOOLCHAIN"] = toolchain

return scratch, rustup_home, cargo_home, env
end

function _windows_rustup_toolchain_ready(
rustup_home::AbstractString,
cargo_home::AbstractString,
toolchain::AbstractString,
)
toolchain_dir = joinpath(rustup_home, "toolchains", "$toolchain-$(_windows_host_triple())")
return isdir(toolchain_dir) &&
all(tool -> isfile(joinpath(cargo_home, "bin", tool * ".exe")), WINDOWS_REQUIRED_TOOLS)
end

function _ensure_windows_rust_toolchain_installed()
toolchain = _rust_version_from_artifact_metadata()
scratch, rustup_home, cargo_home, env = _windows_rustup_env(toolchain)
rustup_path = joinpath(cargo_home, "bin", "rustup.exe")

if !isfile(rustup_path)
mkpath(scratch)
rustup_init = joinpath(scratch, "rustup-init.exe")
@info "Downloading rustup-init for isolated Windows Rust toolchain" url = _windows_rustup_init_url()
download(_windows_rustup_init_url(), rustup_init)
run(setenv(`$rustup_init --default-toolchain none --no-modify-path -y`, env))
Comment on lines +120 to +121
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add integrity check for downloaded rustup-init

The Windows bootstrap path downloads rustup-init.exe and executes it immediately, but there is no checksum or signature verification before run(...). This removes the artifact-level hash pinning the package otherwise relies on and allows a compromised endpoint/proxy/CA path to run arbitrary code during installation. Please verify the installer against a pinned digest (or fetch it through a hashed artifact) before execution.

Useful? React with 👍 / 👎.

end

if !_windows_rustup_toolchain_ready(rustup_home, cargo_home, toolchain)
@info "Installing isolated Windows Rust toolchain" toolchain profile = "complete"
run(setenv(`$rustup_path toolchain install $toolchain --profile complete --no-self-update`, env))
run(setenv(`$rustup_path default $toolchain`, env))
end

_windows_rustup_toolchain_ready(rustup_home, cargo_home, toolchain) || error(
"Windows Rust toolchain installation completed, but cargo/rustc were not found in $cargo_home",
)

return cargo_home, env
end

"""
ensure_rust_toolchain_installed()
Expand All @@ -15,6 +142,11 @@ If not already installed, downloads and installs the Rust toolchain artifact.
Returns the installation prefix directory containing the Rust binaries.
"""
function ensure_rust_toolchain_installed()
if Sys.iswindows()
cargo_home, _ = _ensure_windows_rust_toolchain_installed()
return cargo_home
end

ensure_artifact_installed("RustToolChain", ARTIFACTS_TOML)
toolchain_dir = artifact_path(artifact_hash("RustToolChain", ARTIFACTS_TOML))
# The Rust toolchain is unpacked inside a rust-*-*/ directory
Expand All @@ -23,10 +155,15 @@ function ensure_rust_toolchain_installed()
rust_dir = first(rust_dirs)
prefix = joinpath(toolchain_dir, rust_dir, "prefix")

if isdir(prefix)
if _toolchain_prefix_ready(prefix)
return prefix
elseif Sys.iswindows()
return _install_from_unpacked_dist!(joinpath(toolchain_dir, rust_dir), prefix)
else
run(`bash $(joinpath(toolchain_dir, rust_dir, "install.sh")) --prefix=$(prefix) --disable-ldconfig`)
_toolchain_prefix_ready(prefix) || error(
"Rust toolchain installer completed, but cargo/rustc were not found in $prefix",
)
return prefix
end
end
Expand All @@ -38,6 +175,12 @@ Get the cargo executable command from Julia's Artifacts system.
Downloads and installs the Rust toolchain if not already present.
"""
function cargo_cmd_from_artifacts()
if Sys.iswindows()
cargo_home, env = _ensure_windows_rust_toolchain_installed()
cargo_path = joinpath(cargo_home, "bin", "cargo.exe")
return setenv(`$cargo_path`, env)
end

prefix = ensure_rust_toolchain_installed()
cargo_path = joinpath(prefix, "bin", "cargo" * EXE_EXT)
@assert isfile(cargo_path) "Cargo executable not found at $cargo_path"
Expand All @@ -54,7 +197,13 @@ Get the rustc executable command from Julia's Artifacts system.
Downloads and installs the Rust toolchain if not already present.
"""
function rustc_cmd_from_artifacts()
prefix =ensure_rust_toolchain_installed()
if Sys.iswindows()
cargo_home, env = _ensure_windows_rust_toolchain_installed()
rustc_path = joinpath(cargo_home, "bin", "rustc.exe")
return setenv(`$rustc_path`, env)
end

prefix = ensure_rust_toolchain_installed()
rustc_path = joinpath(prefix, "bin", "rustc" * EXE_EXT)
return `$rustc_path --sysroot $(prefix)`
end
Expand Down
20 changes: 20 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ include("generator_platforms.jl")
@test rustc() isa Cmd
end

@testset "unpacked dist installer" begin
mktempdir() do dir
rust_dir = joinpath(dir, "rust-test")
cargo_exe = "cargo" * RustToolChain.EXE_EXT
rustc_exe = "rustc" * RustToolChain.EXE_EXT

mkpath(joinpath(rust_dir, "cargo", "bin"))
mkpath(joinpath(rust_dir, "rustc", "bin"))
write(joinpath(rust_dir, "components"), "cargo\nrustc\n")
write(joinpath(rust_dir, "cargo", "bin", cargo_exe), "")
write(joinpath(rust_dir, "rustc", "bin", rustc_exe), "")

prefix = joinpath(rust_dir, "prefix")
RustToolChain._install_from_unpacked_dist!(rust_dir, prefix)

@test isfile(joinpath(prefix, "bin", cargo_exe))
@test isfile(joinpath(prefix, "bin", rustc_exe))
end
end

@testset "cargo --version" begin
@test success(`$(cargo()) --version`)
end
Expand Down
Loading