use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use super::Distribution;
use crate::fetch;
use crate::versions::Version;
const TERRAFORM_INDEX_URL: &str = "https://releases.hashicorp.com/terraform/index.json";
const OPENTOFU_INDEX_URL: &str = "https://get.opentofu.org/tofu/api.json";
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", "aarch64") => ("linux", "arm64"),
("linux", "x86_64") => ("linux", "amd64"),
("windows", "x86_64") => ("windows", "amd64"),
("windows", "aarch64") => ("windows", "arm64"),
(os, arch) => bail!("unsupported platform for terraform: {os}/{arch}"),
};
Ok(pair)
}
#[derive(Debug, Deserialize)]
struct Index {
versions: HashMap<String, Release>,
}
#[derive(Debug, Deserialize)]
struct Release {
version: String,
shasums: String,
builds: Vec<Build>,
}
#[derive(Debug, Deserialize)]
struct Build {
os: String,
arch: String,
filename: String,
url: String,
}
pub struct AvailableBuild {
pub version: Version,
filename: String,
url: String,
shasums_url: String,
}
pub fn fetch_available(dist: Distribution) -> Result<Vec<AvailableBuild>> {
match dist {
Distribution::Terraform => fetch_terraform(),
Distribution::OpenTofu => fetch_opentofu(),
}
}
fn fetch_terraform() -> Result<Vec<AvailableBuild>> {
let (os, arch) = platform()?;
let index: Index = fetch::client()?
.get(TERRAFORM_INDEX_URL)
.send()
.context("failed to query releases.hashicorp.com")?
.error_for_status()
.context("terraform release index query failed")?
.json()
.context("failed to parse terraform release index")?;
let mut builds: Vec<AvailableBuild> = index
.versions
.into_values()
.filter_map(|release| {
let version: Version = release.version.parse().ok()?;
let build = release
.builds
.into_iter()
.find(|b| b.os == os && b.arch == arch)?;
Some(AvailableBuild {
version,
filename: build.filename,
url: build.url,
shasums_url: format!(
"https://releases.hashicorp.com/terraform/{}/{}",
release.version, release.shasums
),
})
})
.collect();
builds.sort_by_key(|b| b.version);
Ok(builds)
}
#[derive(Debug, Deserialize)]
struct TofuIndex {
versions: Vec<TofuVersion>,
}
#[derive(Debug, Deserialize)]
struct TofuVersion {
id: String,
files: Vec<String>,
}
fn fetch_opentofu() -> Result<Vec<AvailableBuild>> {
let (os, arch) = platform()?;
let index: TofuIndex = fetch::client()?
.get(OPENTOFU_INDEX_URL)
.send()
.context("failed to query get.opentofu.org")?
.error_for_status()
.context("opentofu release index query failed")?
.json()
.context("failed to parse opentofu release index")?;
let mut builds: Vec<AvailableBuild> = index
.versions
.into_iter()
.filter_map(|release| {
let version: Version = release.id.parse().ok()?;
let filename = format!("tofu_{}_{os}_{arch}.zip", release.id);
let shasums = format!("tofu_{}_SHA256SUMS", release.id);
if !release.files.contains(&filename) || !release.files.contains(&shasums) {
return None;
}
let base = format!(
"https://github.com/opentofu/opentofu/releases/download/v{}",
release.id
);
Some(AvailableBuild {
version,
url: format!("{base}/{filename}"),
filename,
shasums_url: format!("{base}/{shasums}"),
})
})
.collect();
builds.sort_by_key(|b| b.version);
Ok(builds)
}
pub fn install_build(build: &AvailableBuild, dest: &Path) -> Result<()> {
let http = fetch::client()?;
let sums = http
.get(&build.shasums_url)
.send()
.and_then(|r| r.error_for_status())
.with_context(|| format!("failed to fetch checksums from {}", build.shasums_url))?
.text()?;
let expected = fetch::find_sha256(&sums, &build.filename)
.with_context(|| format!("no SHA256SUMS entry for {}", build.filename))?;
eprintln!("downloading {}", build.url);
let archive = fetch::download(&http, &build.url)?;
fetch::verify_sha256(&archive, &expected, &build.filename)?;
fetch::extract_archive_root(&archive, &build.filename, dest)
}
pub fn bin_dir(toolchain: &Path) -> PathBuf {
toolchain.to_path_buf()
}