use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use crate::fetch;
use crate::versions::Version;
const INDEX_URL: &str = "https://go.dev/dl/?mode=json&include=all";
fn platform() -> Result<(&'static str, &'static str)> {
let pair = match (std::env::consts::OS, std::env::consts::ARCH) {
("macos", "aarch64") => ("darwin", "arm64"),
("macos", "x86_64") => ("darwin", "amd64"),
("linux", _) if cfg!(target_env = "musl") => {
bail!(
"official Go builds require glibc on Linux; on musl systems use the distro package (e.g. apk add go)"
)
}
("linux", "aarch64") => ("linux", "arm64"),
("linux", "x86_64") => ("linux", "amd64"),
("windows", "x86_64") => ("windows", "amd64"),
("windows", "aarch64") => ("windows", "arm64"),
(os, arch) => bail!("unsupported platform for go: {os}/{arch}"),
};
Ok(pair)
}
#[derive(Debug, Deserialize)]
struct Release {
version: String,
stable: bool,
files: Vec<File>,
}
#[derive(Debug, Deserialize)]
struct File {
filename: String,
os: String,
arch: String,
kind: String,
sha256: String,
}
pub struct AvailableBuild {
pub version: Version,
filename: String,
sha256: String,
}
fn parse_go_version(raw: &str) -> Option<Version> {
let numbers = raw.strip_prefix("go")?;
let mut parts = numbers.split('.');
let mut next = || -> Option<u32> {
match parts.next() {
Some(p) => p.parse().ok(),
None => Some(0),
}
};
let version = Version {
major: next()?,
minor: next()?,
patch: next()?,
};
parts.next().is_none().then_some(version)
}
pub fn fetch_available() -> Result<Vec<AvailableBuild>> {
let (os, arch) = platform()?;
let index: Vec<Release> = fetch::client()?
.get(INDEX_URL)
.send()
.context("failed to query go.dev/dl")?
.error_for_status()
.context("go.dev/dl index query failed")?
.json()
.context("failed to parse go.dev/dl version index")?;
let mut builds: Vec<AvailableBuild> = index
.into_iter()
.filter(|release| release.stable)
.filter_map(|release| {
let version = parse_go_version(&release.version)?;
let ext = if cfg!(windows) { ".zip" } else { ".tar.gz" };
let file = release.files.into_iter().find(|f| {
f.os == os && f.arch == arch && f.kind == "archive" && f.filename.ends_with(ext)
})?;
Some(AvailableBuild {
version,
filename: file.filename,
sha256: file.sha256.to_ascii_lowercase(),
})
})
.collect();
builds.sort_by_key(|b| b.version);
Ok(builds)
}
pub fn install_build(build: &AvailableBuild, dest: &Path) -> Result<()> {
let url = format!("https://go.dev/dl/{}", build.filename);
eprintln!("downloading {url}");
let archive = fetch::download(&fetch::client()?, &url)?;
fetch::verify_sha256(&archive, &build.sha256, &build.filename)?;
fetch::extract_archive_subdir(&archive, &build.filename, "go", dest)
}
pub fn bin_dir(toolchain: &Path) -> PathBuf {
toolchain.join("bin")
}
#[cfg(test)]
mod tests {
use super::*;
fn v(s: &str) -> Version {
s.parse().unwrap()
}
#[test]
fn parses_go_versions() {
assert_eq!(parse_go_version("go1.23.4"), Some(v("1.23.4")));
assert_eq!(parse_go_version("go1.4"), Some(v("1.4.0")));
assert_eq!(parse_go_version("go1"), Some(v("1.0.0")));
assert_eq!(parse_go_version("1.23.4"), None);
assert_eq!(parse_go_version("go1.23.4.5"), None);
assert_eq!(parse_go_version("go1.21rc2"), None);
}
}