use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use toml_edit::DocumentMut;
use crate::fetch;
use crate::versions::Version;
const DIST_BASE: &str = "https://static.rust-lang.org/dist";
const RELEASES_URL: &str = "https://api.github.com/repos/rust-lang/rust/releases?per_page=100";
const COMPONENTS: &[&str] = &[
"rustc",
"cargo",
"rust-std",
"clippy-preview",
"rustfmt-preview",
];
pub fn target_triple() -> Result<&'static str> {
let triple = match (std::env::consts::OS, std::env::consts::ARCH) {
("macos", "aarch64") => "aarch64-apple-darwin",
("macos", "x86_64") => "x86_64-apple-darwin",
("linux", "aarch64") if cfg!(target_env = "musl") => "aarch64-unknown-linux-musl",
("linux", "x86_64") if cfg!(target_env = "musl") => "x86_64-unknown-linux-musl",
("linux", "aarch64") => "aarch64-unknown-linux-gnu",
("linux", "x86_64") => "x86_64-unknown-linux-gnu",
("windows", "x86_64") => "x86_64-pc-windows-msvc",
("windows", "aarch64") => "aarch64-pc-windows-msvc",
(os, arch) => bail!("unsupported platform for rust: {os}/{arch}"),
};
Ok(triple)
}
#[derive(Debug, Deserialize)]
struct Release {
tag_name: String,
}
pub fn fetch_available() -> Result<Vec<Version>> {
let http = fetch::client()?;
let releases: Vec<Release> = fetch::github_api_get(&http, RELEASES_URL)
.send()
.context("failed to query rust-lang/rust releases")?
.error_for_status()
.context("rust release query failed")?
.json()
.context("failed to parse rust release list")?;
let mut versions: Vec<Version> = releases
.into_iter()
.filter_map(|r| r.tag_name.parse().ok())
.collect();
versions.sort();
Ok(versions)
}
pub struct Manifest {
pub doc: DocumentMut,
pub date: String,
pub rust_version: String,
}
impl Manifest {
pub fn release_version(&self) -> Result<Version> {
self.rust_version
.split_whitespace()
.next()
.unwrap_or(&self.rust_version)
.parse()
.with_context(|| {
format!(
"unexpected rust version '{}' in manifest",
self.rust_version
)
})
}
}
pub fn fetch_manifest(channel: &str, date: Option<&str>) -> Result<Manifest> {
let url = match date {
Some(date) => format!("{DIST_BASE}/{date}/channel-rust-{channel}.toml"),
None => format!("{DIST_BASE}/channel-rust-{channel}.toml"),
};
let text = fetch::client()?
.get(&url)
.send()
.and_then(|r| r.error_for_status())
.with_context(|| format!("no rust channel manifest for '{channel}' ({url})"))?
.text()?;
let doc: DocumentMut = text.parse().context("failed to parse channel manifest")?;
let date = doc
.get("date")
.and_then(|d| d.as_str())
.context("channel manifest has no date")?
.to_string();
let rust_version = doc
.get("pkg")
.and_then(|p| p.get("rust"))
.and_then(|r| r.get("version"))
.and_then(|v| v.as_str())
.context("channel manifest has no rust version")?
.to_string();
Ok(Manifest {
doc,
date,
rust_version,
})
}
fn component_package(doc: &DocumentMut, name: &str) -> Result<String> {
let pkg = doc.get("pkg").context("manifest has no packages")?;
if pkg.get(name).is_some() {
return Ok(name.to_string());
}
let preview = format!("{name}-preview");
if pkg.get(&preview).is_some() {
return Ok(preview);
}
bail!("unknown component '{name}' (not in this toolchain's manifest)");
}
fn component_target_key<'a>(doc: &DocumentMut, package: &str, triple: &'a str) -> &'a str {
let has_star = doc
.get("pkg")
.and_then(|p| p.get(package))
.and_then(|c| c.get("target"))
.and_then(|t| t.get("*"))
.is_some();
if has_star { "*" } else { triple }
}
pub fn add_components(
doc: &DocumentMut,
dest: &Path,
components: &[String],
targets: &[String],
) -> Result<()> {
let triple = target_triple()?;
let mut wanted: Vec<(String, String)> = Vec::new();
for name in components {
let package = component_package(doc, name)?;
let key = component_target_key(doc, &package, triple).to_string();
wanted.push((package, key));
}
for target in targets {
wanted.push(("rust-std".to_string(), target.clone()));
}
install_packages(doc, dest, &wanted)
}
fn component_build<'a>(
doc: &'a DocumentMut,
component: &str,
triple: &str,
) -> Result<(&'a str, &'a str)> {
let target = doc
.get("pkg")
.and_then(|p| p.get(component))
.and_then(|c| c.get("target"))
.and_then(|t| t.get(triple))
.with_context(|| format!("manifest has no {component} entry for {triple}"))?;
if target.get("available").and_then(|a| a.as_bool()) != Some(true) {
bail!("{component} is not available for {triple} in this release");
}
let url = target
.get("url")
.and_then(|u| u.as_str())
.with_context(|| format!("no archive url for {component}/{triple}"))?;
let hash = target
.get("hash")
.and_then(|h| h.as_str())
.with_context(|| format!("no checksum for {component}/{triple}"))?;
Ok((url, hash))
}
fn merge_tree(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst).with_context(|| format!("failed to create {}", dst.display()))?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let from = entry.path();
let to = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
merge_tree(&from, &to)?;
} else if entry.file_name() != "manifest.in" {
std::fs::rename(&from, &to)
.with_context(|| format!("failed to place {}", to.display()))?;
}
}
Ok(())
}
fn install_packages(doc: &DocumentMut, dest: &Path, packages: &[(String, String)]) -> Result<()> {
let http = fetch::client()?;
let parent = dest
.parent()
.context("install destination has no parent directory")?;
for (package, target_key) in packages {
let (url, hash) = component_build(doc, package, target_key)?;
let archive_name = url.rsplit('/').next().unwrap_or(url).to_string();
eprintln!("downloading {url}");
let archive = fetch::download(&http, url)?;
fetch::verify_sha256(&archive, hash, &archive_name)?;
let staging = tempfile::tempdir_in(parent).context("failed to create staging directory")?;
fetch::extract_archive_root(&archive, &archive_name, staging.path())?;
let top = staging
.path()
.join(archive_name.trim_end_matches(".tar.gz"));
if !top.is_dir() {
bail!("unexpected archive layout in {archive_name}");
}
for entry in std::fs::read_dir(&top)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
merge_tree(&entry.path(), dest)?;
}
}
}
Ok(())
}
pub fn install_channel(
doc: &DocumentMut,
dest: &Path,
components: &[String],
targets: &[String],
) -> Result<()> {
let triple = target_triple()?;
let mut packages: Vec<(String, String)> = COMPONENTS
.iter()
.map(|c| (c.to_string(), triple.to_string()))
.collect();
for name in components {
let package = component_package(doc, name)?;
if packages.iter().any(|(p, _)| p == &package) {
continue;
}
let key = component_target_key(doc, &package, triple).to_string();
packages.push((package, key));
}
for target in targets {
if target == triple {
continue;
}
packages.push(("rust-std".to_string(), target.clone()));
}
install_packages(doc, dest, &packages)
}
pub fn bin_dir(toolchain: &Path) -> PathBuf {
toolchain.join("bin")
}