use crate::aqua::aqua_template;
use crate::backend::aqua;
use crate::backend::aqua::{arch, os};
use crate::config::SETTINGS;
use crate::duration::{DAILY, WEEKLY};
use crate::git::Git;
use crate::{dirs, file, hashmap, http};
use expr::{Context, Parser, Program, Value};
use eyre::{eyre, ContextCompat, Result};
use indexmap::IndexSet;
use itertools::Itertools;
use serde_derive::Deserialize;
use std::cmp::PartialEq;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::LazyLock as Lazy;
use url::Url;
#[allow(clippy::invisible_characters)]
pub static AQUA_STANDARD_REGISTRY_FILES: Lazy<HashMap<&'static str, &'static str>> =
Lazy::new(|| include!(concat!(env!("OUT_DIR"), "/aqua_standard_registry.rs")));
pub static AQUA_REGISTRY: Lazy<AquaRegistry> = Lazy::new(|| {
AquaRegistry::standard().unwrap_or_else(|err| {
warn!("failed to initialize aqua registry: {err:?}");
AquaRegistry::default()
})
});
static AQUA_REGISTRY_PATH: Lazy<PathBuf> = Lazy::new(|| dirs::CACHE.join("aqua-registry"));
#[derive(Default)]
pub struct AquaRegistry {
path: PathBuf,
repo_exists: bool,
}
#[derive(Debug, Deserialize, Default, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AquaPackageType {
GithubArchive,
GithubContent,
#[default]
GithubRelease,
Http,
}
#[derive(Debug, Deserialize, Default, Clone)]
#[serde(default)]
pub struct AquaPackage {
pub r#type: AquaPackageType,
pub repo_owner: String,
pub repo_name: String,
pub asset: String,
pub url: String,
pub description: Option<String>,
pub format: String,
pub rosetta2: bool,
pub windows_arm_emulation: bool,
pub complete_windows_ext: bool,
pub supported_envs: Vec<String>,
pub files: Vec<AquaFile>,
pub replacements: HashMap<String, String>,
pub version_prefix: Option<String>,
version_filter: Option<String>,
#[serde(skip)]
version_filter_expr: Option<Program>,
pub version_source: Option<String>,
pub checksum: Option<AquaChecksum>,
pub slsa_provenance: Option<AquaSlsaProvenance>,
overrides: Vec<AquaOverride>,
version_constraint: String,
version_overrides: Vec<AquaPackage>,
}
#[derive(Debug, Deserialize, Clone)]
struct AquaOverride {
#[serde(flatten)]
pkg: AquaPackage,
goos: Option<String>,
goarch: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AquaFile {
pub name: String,
pub src: Option<String>,
}
#[derive(Debug, Deserialize, Clone, strum::AsRefStr, strum::Display)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum AquaChecksumAlgorithm {
Sha1,
Sha256,
Sha512,
Md5,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum AquaChecksumType {
GithubRelease,
Http,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AquaCosignSignature {
pub r#type: Option<String>,
pub repo_owner: Option<String>,
pub repo_name: Option<String>,
pub url: Option<String>,
pub asset: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AquaCosign {
pub enabled: Option<bool>,
pub experimental: Option<bool>,
pub signature: Option<AquaCosignSignature>,
pub key: Option<AquaCosignSignature>,
pub certificate: Option<AquaCosignSignature>,
opts: Vec<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AquaSlsaProvenance {
pub enabled: Option<bool>,
pub r#type: Option<String>,
pub repo_owner: Option<String>,
pub repo_name: Option<String>,
pub url: Option<String>,
pub asset: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AquaChecksum {
pub r#type: Option<AquaChecksumType>,
pub algorithm: Option<AquaChecksumAlgorithm>,
pub pattern: Option<AquaChecksumPattern>,
pub cosign: Option<AquaCosign>,
file_format: Option<String>,
enabled: Option<bool>,
asset: Option<String>,
url: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AquaChecksumPattern {
pub checksum: String,
pub file: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RegistryYaml {
packages: Vec<AquaPackage>,
}
impl AquaRegistry {
pub fn standard() -> Result<Self> {
let path = AQUA_REGISTRY_PATH.clone();
let repo = Git::new(&path);
let mut repo_exists = repo.exists();
if repo_exists {
fetch_latest_repo(&repo)?;
} else if let Some(aqua_registry_url) = &SETTINGS.aqua.registry_url {
info!("cloning aqua registry to {path:?}");
repo.clone(aqua_registry_url, None)?;
repo_exists = true;
}
Ok(Self { path, repo_exists })
}
pub fn package(&self, id: &str) -> Result<AquaPackage> {
let path_id = id.split('/').join(std::path::MAIN_SEPARATOR_STR);
let path = self.path.join("pkgs").join(&path_id).join("registry.yaml");
let registry: RegistryYaml = if !self.repo_exists {
if let Some(registry) = AQUA_STANDARD_REGISTRY_FILES.get(id) {
trace!("reading baked-in aqua-registry for {id}");
serde_yaml::from_str(registry)?
} else if !path.exists() || file::modified_duration(&path)? > DAILY {
trace!("downloading aqua-registry for {id} to {path:?}");
let url: Url =
format!("https://mise-versions.jdx.dev/aqua-registry/{path_id}/registry.yaml")
.parse()?;
http::HTTP_FETCH.download_file(url, &path, None)?;
serde_yaml::from_reader(file::open(&path)?)?
} else {
trace!("reading cached aqua-registry for {id} from {path:?}");
serde_yaml::from_reader(file::open(&path)?)?
}
} else {
trace!("reading aqua-registry for {id} from repo at {path:?}");
serde_yaml::from_reader(file::open(&path)?)?
};
let mut pkg = registry
.packages
.into_iter()
.next()
.wrap_err(format!("no package found for {id} in {path:?}"))?;
if let Some(version_filter) = &pkg.version_filter {
pkg.version_filter_expr = Some(Parser::new().compile(version_filter)?);
}
Ok(pkg)
}
pub fn package_with_version(&self, id: &str, v: &str) -> Result<AquaPackage> {
Ok(self.package(id)?.with_version(v))
}
}
fn fetch_latest_repo(repo: &Git) -> Result<()> {
if file::modified_duration(&repo.dir)? < WEEKLY {
return Ok(());
}
info!("updating aqua registry repo");
repo.update(None)?;
Ok(())
}
impl AquaPackage {
fn with_version(mut self, v: &str) -> AquaPackage {
self = apply_override(self.clone(), self.version_override(v));
if let Some(avo) = self.overrides.clone().into_iter().find(|o| {
if let (Some(goos), Some(goarch)) = (&o.goos, &o.goarch) {
goos == aqua::os() && goarch == aqua::arch()
} else if let Some(goos) = &o.goos {
goos == aqua::os()
} else if let Some(goarch) = &o.goarch {
goarch == aqua::arch()
} else {
false
}
}) {
self = apply_override(self, &avo.pkg)
}
self
}
fn version_override(&self, v: &str) -> &AquaPackage {
let ver = versions::Versioning::new(v.strip_prefix('v').unwrap_or(v));
let mut expr = Parser::new();
let ctx = self.expr_ctx(v);
expr.add_function("semver", |c| {
if c.args.len() != 1 {
return Err("semver() takes exactly one argument".to_string().into());
}
let semver = c.args[0].as_string().unwrap().replace(' ', "");
if let Some(check_version) = versions::Requirement::new(&semver) {
if let Some(ver) = &ver {
Ok(check_version.matches(ver).into())
} else {
Err("invalid version".to_string().into())
}
} else {
Err("invalid semver".to_string().into())
}
});
vec![self]
.into_iter()
.chain(self.version_overrides.iter())
.find(|vo| {
if vo.version_constraint.is_empty() {
true
} else {
expr.eval(&vo.version_constraint, &ctx)
.map_err(|e| debug!("error parsing {}: {e}", vo.version_constraint))
.unwrap_or(false.into())
.as_bool()
.unwrap()
}
})
.unwrap_or(self)
}
pub fn format(&self, v: &str) -> Result<&str> {
if self.r#type == AquaPackageType::GithubArchive {
return Ok("tar.gz");
}
let format = if self.format.is_empty() {
let asset = if !self.asset.is_empty() {
self.asset(v)?
} else if !self.url.is_empty() {
self.url.to_string()
} else {
debug!("no asset or url for {}/{}", self.repo_owner, self.repo_name);
"".to_string()
};
if asset.ends_with(".tar.gz") || asset.ends_with(".tgz") {
"tar.gz"
} else if asset.ends_with(".tar.xz") || asset.ends_with(".txz") {
"tar.xz"
} else if asset.ends_with(".tar.bz2") || asset.ends_with(".tbz2") {
"tar.bz2"
} else if asset.ends_with(".gz") {
"gz"
} else if asset.ends_with(".xz") {
"xz"
} else if asset.ends_with(".bz2") {
"bz2"
} else if asset.ends_with(".zip") {
"zip"
} else {
"raw"
}
} else {
match self.format.as_str() {
"tgz" => "tar.gz",
"txz" => "tar.xz",
"tbz2" => "tar.bz2",
format => format,
}
};
Ok(format)
}
pub fn asset(&self, v: &str) -> Result<String> {
self.parse_aqua_str(&self.asset, v, &Default::default())
}
pub fn asset_strs(&self, v: &str) -> Result<IndexSet<String>> {
let mut strs = IndexSet::from([self.asset(v)?]);
if cfg!(macos) {
let mut ctx = HashMap::default();
ctx.insert("Arch".to_string(), "universal".to_string());
strs.insert(self.parse_aqua_str(&self.asset, v, &ctx)?);
} else if cfg!(windows) {
let mut ctx = HashMap::default();
let with_exe = format!("{}.exe", self.parse_aqua_str(&self.asset, v, &ctx)?);
strs.insert(with_exe);
if cfg!(target_arch = "aarch64") {
ctx.insert("Arch".to_string(), "amd64".to_string());
strs.insert(self.parse_aqua_str(&self.asset, v, &ctx)?);
strs.insert(format!(
"{}.exe",
self.parse_aqua_str(&self.asset, v, &ctx)?
));
}
}
Ok(strs)
}
pub fn url(&self, v: &str) -> Result<String> {
self.parse_aqua_str(&self.url, v, &Default::default())
}
fn parse_aqua_str(
&self,
s: &str,
v: &str,
overrides: &HashMap<String, String>,
) -> Result<String> {
let os = os();
let mut arch = arch();
if os == "darwin" && arch == "arm64" && self.rosetta2 {
arch = "amd64";
}
if os == "windows" && arch == "arm64" && self.windows_arm_emulation {
arch = "amd64";
}
let replace = |s: &str| {
self.replacements
.get(s)
.map(|s| s.to_string())
.unwrap_or_else(|| s.to_string())
};
let semver = if let Some(prefix) = &self.version_prefix {
v.strip_prefix(prefix).unwrap_or(v)
} else {
v
};
let mut ctx = hashmap! {
"Version".to_string() => replace(v),
"SemVer".to_string() => replace(semver),
"OS".to_string() => replace(os),
"GOOS".to_string() => replace(os),
"GOARCH".to_string() => replace(arch),
"Arch".to_string() => replace(arch),
"Format".to_string() => replace(&self.format),
};
ctx.extend(overrides.clone());
aqua_template::render(s, &ctx)
}
fn expr(&self, v: &str, program: Program) -> Result<Value> {
let expr = Parser::new();
expr.run(program, &self.expr_ctx(v)).map_err(|e| eyre!(e))
}
fn expr_ctx(&self, v: &str) -> Context {
let mut ctx = Context::default();
ctx.insert("Version", v);
ctx
}
pub fn version_filter_ok(&self, v: &str) -> Result<bool> {
if let Some(filter) = self.version_filter_expr.clone() {
if let Value::Bool(expr) = self.expr(v, filter)? {
Ok(expr)
} else {
warn!(
"invalid response from version filter: {}",
self.version_filter.as_ref().unwrap()
);
Ok(true)
}
} else {
Ok(true)
}
}
}
impl AquaFile {
pub fn src(&self, pkg: &AquaPackage, v: &str) -> Result<Option<String>> {
let asset = pkg.asset(v)?;
let asset = asset.strip_suffix(".tar.gz").unwrap_or(&asset);
let asset = asset.strip_suffix(".tar.xz").unwrap_or(asset);
let asset = asset.strip_suffix(".tar.bz2").unwrap_or(asset);
let asset = asset.strip_suffix(".gz").unwrap_or(asset);
let asset = asset.strip_suffix(".xz").unwrap_or(asset);
let asset = asset.strip_suffix(".bz2").unwrap_or(asset);
let asset = asset.strip_suffix(".zip").unwrap_or(asset);
let asset = asset.strip_suffix(".tar").unwrap_or(asset);
let asset = asset.strip_suffix(".tgz").unwrap_or(asset);
let asset = asset.strip_suffix(".txz").unwrap_or(asset);
let asset = asset.strip_suffix(".tbz2").unwrap_or(asset);
let ctx = hashmap! {
"AssetWithoutExt".to_string() => asset.to_string(),
};
self.src
.as_ref()
.map(|src| pkg.parse_aqua_str(src, v, &ctx))
.transpose()
}
}
fn apply_override(mut orig: AquaPackage, avo: &AquaPackage) -> AquaPackage {
if !avo.repo_owner.is_empty() {
orig.repo_owner = avo.repo_owner.clone();
}
if !avo.repo_name.is_empty() {
orig.repo_name = avo.repo_name.clone();
}
if !avo.asset.is_empty() {
orig.asset = avo.asset.clone();
}
if !avo.url.is_empty() {
orig.url = avo.url.clone();
}
if !avo.format.is_empty() {
orig.format = avo.format.clone();
}
if avo.rosetta2 {
orig.rosetta2 = true;
}
if avo.windows_arm_emulation {
orig.windows_arm_emulation = true;
}
if avo.complete_windows_ext {
orig.complete_windows_ext = true;
}
if !avo.supported_envs.is_empty() {
orig.supported_envs = avo.supported_envs.clone();
}
if !avo.files.is_empty() {
orig.files = avo.files.clone();
}
orig.replacements.extend(avo.replacements.clone());
if !avo.overrides.is_empty() {
orig.overrides = avo.overrides.clone();
}
if let Some(avo_checksum) = avo.checksum.clone() {
let mut checksum = orig.checksum.unwrap_or_else(|| avo_checksum.clone());
checksum.merge(avo_checksum);
orig.checksum = Some(checksum);
}
if let Some(avo_slsa_provenance) = avo.slsa_provenance.clone() {
let mut slsa_provenance = orig
.slsa_provenance
.unwrap_or_else(|| avo_slsa_provenance.clone());
slsa_provenance.merge(avo_slsa_provenance);
orig.slsa_provenance = Some(slsa_provenance);
}
orig
}
impl AquaChecksum {
pub fn _type(&self) -> &AquaChecksumType {
self.r#type.as_ref().unwrap()
}
pub fn algorithm(&self) -> &AquaChecksumAlgorithm {
self.algorithm.as_ref().unwrap()
}
pub fn asset_strs(&self, pkg: &AquaPackage, v: &str) -> Result<IndexSet<String>> {
let mut asset_strs = IndexSet::new();
for asset in pkg.asset_strs(v)? {
let checksum_asset = self.asset.as_ref().unwrap();
let ctx = hashmap! {
"Asset".to_string() => asset.to_string(),
};
asset_strs.insert(pkg.parse_aqua_str(checksum_asset, v, &ctx)?);
}
Ok(asset_strs)
}
pub fn pattern(&self) -> &AquaChecksumPattern {
self.pattern.as_ref().unwrap()
}
pub fn enabled(&self) -> bool {
self.enabled.unwrap_or(true)
}
pub fn file_format(&self) -> &str {
self.file_format.as_deref().unwrap_or("raw")
}
pub fn url(&self, pkg: &AquaPackage, v: &str) -> Result<String> {
pkg.parse_aqua_str(self.url.as_ref().unwrap(), v, &Default::default())
}
fn merge(&mut self, other: Self) {
if let Some(r#type) = other.r#type {
self.r#type = Some(r#type);
}
if let Some(algorithm) = other.algorithm {
self.algorithm = Some(algorithm);
}
if let Some(pattern) = other.pattern {
self.pattern = Some(pattern);
}
if let Some(enabled) = other.enabled {
self.enabled = Some(enabled);
}
if let Some(asset) = other.asset {
self.asset = Some(asset);
}
if let Some(url) = other.url {
self.url = Some(url);
}
if let Some(file_format) = other.file_format {
self.file_format = Some(file_format);
}
if let Some(cosign) = other.cosign {
if self.cosign.is_none() {
self.cosign = Some(cosign.clone());
}
self.cosign.as_mut().unwrap().merge(cosign);
}
}
}
impl AquaCosign {
pub fn opts(&self, pkg: &AquaPackage, v: &str) -> Result<Vec<String>> {
self.opts
.iter()
.map(|opt| pkg.parse_aqua_str(opt, v, &Default::default()))
.collect()
}
fn merge(&mut self, other: Self) {
if let Some(enabled) = other.enabled {
self.enabled = Some(enabled);
}
if let Some(experimental) = other.experimental {
self.experimental = Some(experimental);
}
if let Some(signature) = other.signature.clone() {
if self.signature.is_none() {
self.signature = Some(signature.clone());
}
self.signature.as_mut().unwrap().merge(signature);
}
if let Some(key) = other.key.clone() {
if self.key.is_none() {
self.key = Some(key.clone());
}
self.key.as_mut().unwrap().merge(key);
}
if let Some(certificate) = other.certificate.clone() {
if self.certificate.is_none() {
self.certificate = Some(certificate.clone());
}
self.certificate.as_mut().unwrap().merge(certificate);
}
if !other.opts.is_empty() {
self.opts = other.opts.clone();
}
}
}
impl AquaCosignSignature {
pub fn url(&self, pkg: &AquaPackage, v: &str) -> Result<String> {
pkg.parse_aqua_str(self.url.as_ref().unwrap(), v, &Default::default())
}
pub fn asset(&self, pkg: &AquaPackage, v: &str) -> Result<String> {
pkg.parse_aqua_str(self.asset.as_ref().unwrap(), v, &Default::default())
}
pub fn arg(&self, pkg: &AquaPackage, v: &str) -> Result<String> {
match self.r#type.as_deref().unwrap_or_default() {
"github_release" => {
let asset = self.asset(pkg, v)?;
let repo_owner = self
.repo_owner
.clone()
.unwrap_or_else(|| pkg.repo_owner.clone());
let repo_name = self
.repo_name
.clone()
.unwrap_or_else(|| pkg.repo_name.clone());
let repo = format!("{repo_owner}/{repo_name}");
Ok(format!(
"https://github.com/{repo}/releases/download/{v}/{asset}"
))
}
"http" => self.url(pkg, v),
t => {
warn!(
"unsupported cosign signature type for {}/{}: {t}",
pkg.repo_owner, pkg.repo_name
);
Ok("".to_string())
}
}
}
fn merge(&mut self, other: Self) {
if let Some(r#type) = other.r#type {
self.r#type = Some(r#type);
}
if let Some(repo_owner) = other.repo_owner {
self.repo_owner = Some(repo_owner);
}
if let Some(repo_name) = other.repo_name {
self.repo_name = Some(repo_name);
}
if let Some(url) = other.url {
self.url = Some(url);
}
if let Some(asset) = other.asset {
self.asset = Some(asset);
}
}
}
impl AquaSlsaProvenance {
pub fn asset(&self, pkg: &AquaPackage, v: &str) -> Result<String> {
pkg.parse_aqua_str(self.asset.as_ref().unwrap(), v, &Default::default())
}
pub fn url(&self, pkg: &AquaPackage, v: &str) -> Result<String> {
pkg.parse_aqua_str(self.url.as_ref().unwrap(), v, &Default::default())
}
fn merge(&mut self, other: Self) {
if let Some(enabled) = other.enabled {
self.enabled = Some(enabled);
}
if let Some(r#type) = other.r#type {
self.r#type = Some(r#type);
}
if let Some(repo_owner) = other.repo_owner {
self.repo_owner = Some(repo_owner);
}
if let Some(repo_name) = other.repo_name {
self.repo_name = Some(repo_name);
}
if let Some(url) = other.url {
self.url = Some(url);
}
if let Some(asset) = other.asset {
self.asset = Some(asset);
}
}
}