blob: e4cdf965eb609049d86baaf9b94a1b06df65f46d [file] [log] [blame]
use anyhow::Error;
use flate2::read::GzDecoder;
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use tar::Archive;
use xz2::read::XzDecoder;
const DEFAULT_TARGET: &str = "x86_64-unknown-linux-gnu";
macro_rules! pkg_type {
( $($variant:ident = $component:literal $(; preview = true $(@$is_preview:tt)? )? ),+ $(,)? ) => {
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub(crate) enum PkgType {
$($variant,)+
}
impl PkgType {
pub(crate) fn is_preview(&self) -> bool {
match self {
$( $( $($is_preview)? PkgType::$variant => true, )? )+
_ => false,
}
}
/// First part of the tarball name.
pub(crate) fn tarball_component_name(&self) -> &str {
match self {
$( PkgType::$variant => $component,)+
}
}
pub(crate) fn all() -> &'static [PkgType] {
&[ $(PkgType::$variant),+ ]
}
}
}
}
pkg_type! {
Rust = "rust",
RustSrc = "rust-src",
Rustc = "rustc",
RustcDev = "rustc-dev",
RustcDocs = "rustc-docs",
ReproducibleArtifacts = "reproducible-artifacts",
RustMingw = "rust-mingw",
RustStd = "rust-std",
Cargo = "cargo",
HtmlDocs = "rust-docs",
RustAnalysis = "rust-analysis",
Rls = "rls"; preview = true,
RustAnalyzer = "rust-analyzer"; preview = true,
Clippy = "clippy"; preview = true,
Rustfmt = "rustfmt"; preview = true,
LlvmTools = "llvm-tools"; preview = true,
Miri = "miri"; preview = true,
JsonDocs = "rust-docs-json"; preview = true,
RustcCodegenCranelift = "rustc-codegen-cranelift"; preview = true,
}
impl PkgType {
/// Component name in the manifest. In particular, this includes the `-preview` suffix where appropriate.
pub(crate) fn manifest_component_name(&self) -> String {
if self.is_preview() {
format!("{}-preview", self.tarball_component_name())
} else {
self.tarball_component_name().to_string()
}
}
/// Whether this package has the same version as Rust itself, or has its own `version` and
/// `git-commit-hash` files inside the tarball.
fn should_use_rust_version(&self) -> bool {
match self {
PkgType::Cargo => false,
PkgType::Rls => false,
PkgType::RustAnalyzer => false,
PkgType::Clippy => false,
PkgType::Rustfmt => false,
PkgType::LlvmTools => false,
PkgType::Miri => false,
PkgType::RustcCodegenCranelift => false,
PkgType::Rust => true,
PkgType::RustStd => true,
PkgType::RustSrc => true,
PkgType::Rustc => true,
PkgType::JsonDocs => true,
PkgType::HtmlDocs => true,
PkgType::RustcDev => true,
PkgType::RustcDocs => true,
PkgType::ReproducibleArtifacts => true,
PkgType::RustMingw => true,
PkgType::RustAnalysis => true,
}
}
pub(crate) fn targets(&self) -> &[&str] {
use crate::{HOSTS, MINGW, TARGETS};
use PkgType::*;
match self {
Rust => HOSTS, // doesn't matter in practice, but return something to avoid panicking
Rustc => HOSTS,
RustcDev => HOSTS,
ReproducibleArtifacts => HOSTS,
RustcDocs => HOSTS,
Cargo => HOSTS,
RustcCodegenCranelift => HOSTS,
RustMingw => MINGW,
RustStd => TARGETS,
HtmlDocs => HOSTS,
JsonDocs => HOSTS,
RustSrc => &["*"],
Rls => HOSTS,
RustAnalyzer => HOSTS,
Clippy => HOSTS,
Miri => HOSTS,
Rustfmt => HOSTS,
RustAnalysis => TARGETS,
LlvmTools => TARGETS,
}
}
/// Whether this package is target-independent or not.
fn target_independent(&self) -> bool {
*self == PkgType::RustSrc
}
/// Whether to package these target-specific docs for another similar target.
pub(crate) fn use_docs_fallback(&self) -> bool {
match self {
PkgType::JsonDocs | PkgType::HtmlDocs => true,
_ => false,
}
}
}
#[derive(Debug, Default, Clone)]
pub(crate) struct VersionInfo {
pub(crate) version: Option<String>,
pub(crate) git_commit: Option<String>,
pub(crate) present: bool,
}
pub(crate) struct Versions {
channel: String,
dist_path: PathBuf,
versions: HashMap<PkgType, VersionInfo>,
}
impl Versions {
pub(crate) fn new(channel: &str, dist_path: &Path) -> Result<Self, Error> {
Ok(Self { channel: channel.into(), dist_path: dist_path.into(), versions: HashMap::new() })
}
pub(crate) fn channel(&self) -> &str {
&self.channel
}
pub(crate) fn version(&mut self, mut package: &PkgType) -> Result<VersionInfo, Error> {
if package.should_use_rust_version() {
package = &PkgType::Rust;
}
match self.versions.get(package) {
Some(version) => Ok(version.clone()),
None => {
let version_info = self.load_version_from_tarball(package)?;
if *package == PkgType::Rust && version_info.version.is_none() {
panic!("missing version info for toolchain");
}
self.versions.insert(package.clone(), version_info.clone());
Ok(version_info)
}
}
}
fn load_version_from_tarball(&mut self, package: &PkgType) -> Result<VersionInfo, Error> {
for ext in ["xz", "gz"] {
let info =
self.load_version_from_tarball_inner(&self.dist_path.join(self.archive_name(
package,
DEFAULT_TARGET,
&format!("tar.{}", ext),
)?))?;
if info.present {
return Ok(info);
}
}
// If neither tarball is present, we fallback to returning the non-present info.
Ok(VersionInfo::default())
}
fn load_version_from_tarball_inner(&mut self, tarball: &Path) -> Result<VersionInfo, Error> {
let file = match File::open(&tarball) {
Ok(file) => file,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
// Missing tarballs do not return an error, but return empty data.
println!("warning: missing tarball {}", tarball.display());
return Ok(VersionInfo::default());
}
Err(err) => return Err(err.into()),
};
let mut tar: Archive<Box<dyn std::io::Read>> =
Archive::new(if tarball.extension().map_or(false, |e| e == "gz") {
Box::new(GzDecoder::new(file))
} else if tarball.extension().map_or(false, |e| e == "xz") {
Box::new(XzDecoder::new(file))
} else {
unimplemented!("tarball extension not recognized: {}", tarball.display())
});
let mut version = None;
let mut git_commit = None;
for entry in tar.entries()? {
let mut entry = entry?;
let dest;
match entry.path()?.components().nth(1).and_then(|c| c.as_os_str().to_str()) {
Some("version") => dest = &mut version,
Some("git-commit-hash") => dest = &mut git_commit,
_ => continue,
}
let mut buf = String::new();
entry.read_to_string(&mut buf)?;
*dest = Some(buf);
// Short circuit to avoid reading the whole tar file if not necessary.
if version.is_some() && git_commit.is_some() {
break;
}
}
Ok(VersionInfo { version, git_commit, present: true })
}
pub(crate) fn archive_name(
&self,
package: &PkgType,
target: &str,
extension: &str,
) -> Result<String, Error> {
let component_name = package.tarball_component_name();
let version = match self.channel.as_str() {
"stable" => self.rustc_version().into(),
"beta" => "beta".into(),
"nightly" => "nightly".into(),
_ => format!("{}-dev", self.rustc_version()),
};
if package.target_independent() {
Ok(format!("{}-{}.{}", component_name, version, extension))
} else {
Ok(format!("{}-{}-{}.{}", component_name, version, target, extension))
}
}
pub(crate) fn tarball_name(&self, package: &PkgType, target: &str) -> Result<String, Error> {
self.archive_name(package, target, "tar.gz")
}
pub(crate) fn rustc_version(&self) -> &str {
const RUSTC_VERSION: &str = include_str!("../../../version");
RUSTC_VERSION.trim()
}
}