mod fetcher_config;
mod internal_package;
use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
pub use fetcher_config::FetcherConfig;
use crate::arch_os::{Arch, ArchOs, Os};
use internal_package::InternalPackage;
pub const EXTRA_FILES_DIR_NAME: &str = "extra_files";
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Asset {
pub url: String,
pub sha256: String,
}
pub type ReleaseAssets = HashMap<ArchOs, Asset>;
#[derive(Debug, Clone, Default)]
pub struct Release {
pub published_at: Option<DateTime<Utc>>,
pub assets: ReleaseAssets,
}
impl Release {
pub fn with_assets(mut self, assets: ReleaseAssets) -> Self {
self.assets = assets;
self
}
pub fn with_published_at(mut self, published_at: Option<DateTime<Utc>>) -> Self {
self.published_at = published_at;
self
}
}
fn is_zero(x: &u32) -> bool {
*x == 0
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Install {
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub strip: u32,
pub files: BTreeMap<String, String>,
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub extra_files: BTreeMap<String, String>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tests: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Package {
pub name: String,
pub description: String,
pub homepage: String,
pub repository: String,
pub comment: String,
pub releases: BTreeMap<Version, Release>,
pub installs: BTreeMap<Version, HashMap<ArchOs, Install>>,
pub package_dir: PathBuf,
pub fetcher: FetcherConfig,
}
impl Package {
pub fn from_file(path: &Path) -> Result<Package> {
let file = File::open(path)?;
let internal_package: InternalPackage = serde_yaml::from_reader(file)?;
let package_dir = path
.parent()
.ok_or_else(|| anyhow!("No parent dir for package {}", path.display()))?;
internal_package.to_package(package_dir)
}
pub fn from_yaml_str(yaml_str: &str) -> Result<Package> {
let internal_package: InternalPackage = serde_yaml::from_str(yaml_str)?;
internal_package.to_package(&PathBuf::new())
}
pub fn to_file(&self, path: &Path) -> Result<()> {
let internal_package = InternalPackage::from(self);
let file = File::create(path)?;
serde_yaml::to_writer(file, &internal_package)?;
Ok(())
}
pub fn replace_release(&self, version: &Version, release: Release) -> Package {
let mut releases = self.releases.clone();
releases.insert(version.clone(), release);
Package {
name: self.name.clone(),
description: self.description.clone(),
homepage: self.homepage.clone(),
repository: self.repository.clone(),
comment: self.comment.clone(),
releases,
installs: self.installs.clone(),
package_dir: self.package_dir.clone(),
fetcher: self.fetcher.clone(),
}
}
pub fn get_version_matching(&self, requested_version: &VersionReq) -> Option<&Version> {
self.releases
.keys()
.rev()
.find(|&version| requested_version.matches(version))
}
pub fn get_latest_version(&self) -> Option<&Version> {
let entry = self.releases.iter().last()?;
Some(entry.0)
}
pub fn get_asset(&self, version: &Version, arch_os: &ArchOs) -> Option<&Asset> {
let release = self.releases.get(version)?;
let asset = release.assets.get(arch_os);
if asset.is_some() {
return asset;
}
if arch_os.arch != Arch::Any {
let asset = release.assets.get(&arch_os.with_any_arch());
if asset.is_some() {
return asset;
}
}
if arch_os.os != Os::Any {
let asset = release.assets.get(&arch_os.with_any_os());
if asset.is_some() {
return asset;
}
}
release.assets.get(&ArchOs::any())
}
pub fn get_install(&self, wanted_version: &Version, arch_os: &ArchOs) -> Option<&Install> {
let install = self.get_install_internal(wanted_version, arch_os);
if install.is_some() {
return install;
}
if arch_os.arch != Arch::Any {
let install = self.get_install_internal(wanted_version, &arch_os.with_any_arch());
if install.is_some() {
return install;
}
}
if arch_os.os != Os::Any {
let install = self.get_install_internal(wanted_version, &arch_os.with_any_os());
if install.is_some() {
return install;
}
}
self.get_install_internal(wanted_version, &ArchOs::any())
}
fn get_install_internal(&self, wanted_version: &Version, arch_os: &ArchOs) -> Option<&Install> {
let entry = self
.installs
.iter()
.rev()
.find(|(version, _)| *version <= wanted_version)?;
let installs_for_arch_os = entry.1;
installs_for_arch_os.get(arch_os)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs, str::FromStr};
use crate::store::INDEX_NAME;
const TEST_PACKAGE_YAML_CONTENT: &str = "
name: test
description: desc
homepage:
releases:
1.2.0:
any:
url: https://example.com/foo-1.2.0
sha256: '1234'
1.3.0:
published_at: '2024-01-02T12:34:56Z'
assets:
any:
url: https://example.com/foo-1.3.0
sha256: '5678'
installs:
1.2.0:
any:
strip: 1
files:
bin/foo-1.2: bin/foo
share:
tests:
- foo --help
- foo --version
fetcher: !GitHub
arch: x86_64
os: linux
";
fn read_yaml_from_path(path: &Path) -> serde_yaml::Mapping {
let file = File::open(path).unwrap();
let value: serde_yaml::Value = serde_yaml::from_reader(file).unwrap();
value.as_mapping().unwrap().clone()
}
#[test]
fn from_file_should_load_packages_defined_as_dirs() {
let dir = assert_fs::TempDir::new().unwrap();
let package_dir = dir.join("test");
fs::create_dir(&package_dir).unwrap();
let package_file = package_dir.join(INDEX_NAME);
fs::write(&package_file, TEST_PACKAGE_YAML_CONTENT).unwrap();
let package = Package::from_file(&package_file).unwrap();
assert_eq!(package.package_dir, package_dir);
let version = Version::new(1, 2, 0);
let install = package.get_install(&version, &ArchOs::current()).unwrap();
assert_eq!(install.tests, &["foo --help", "foo --version"]);
}
#[test]
fn from_file_should_load_packages_defined_as_files() {
let dir = assert_fs::TempDir::new().unwrap();
let package_file = dir.join("test.yaml");
fs::write(&package_file, TEST_PACKAGE_YAML_CONTENT).unwrap();
let package = Package::from_file(&package_file).unwrap();
assert_eq!(package.package_dir, dir.path());
let version = Version::new(1, 2, 0);
let install = package.get_install(&version, &ArchOs::current()).unwrap();
assert_eq!(install.tests, &["foo --help", "foo --version"]);
}
#[test]
fn test_loading_package() {
let package = Package::from_yaml_str(TEST_PACKAGE_YAML_CONTENT).unwrap();
let release_120 = package
.releases
.get(&Version::from_str("1.2.0").unwrap())
.unwrap();
assert!(release_120.published_at.is_none());
let asset_120 = release_120.assets.get(&ArchOs::any()).unwrap();
assert_eq!(asset_120.sha256, "1234");
let release_130 = package
.releases
.get(&Version::from_str("1.3.0").unwrap())
.unwrap();
assert_eq!(
release_130.published_at,
Some(
DateTime::parse_from_rfc3339("2024-01-02T12:34:56Z")
.unwrap()
.to_utc()
)
);
assert!(release_130.assets.contains_key(&ArchOs::any()));
let asset_130 = release_130.assets.get(&ArchOs::any()).unwrap();
assert_eq!(asset_130.sha256, "5678");
let install = package
.get_install(&Version::new(1, 2, 0), &ArchOs::current())
.unwrap();
assert_eq!(
install.files.get("bin/foo-1.2"),
Some(&"bin/foo".to_string())
);
assert_eq!(install.files.get("share"), Some(&"".to_string()));
}
#[test]
fn saving_package_must_write_correct_release_format() {
let package = Package::from_yaml_str(
"
name: test
description: desc
homepage:
releases:
2.0.0:
x86_64-linux:
url: https://example.com
sha256: '1234'
installs: {}
",
)
.unwrap();
let dir = assert_fs::TempDir::new().unwrap();
let path = dir.join("test.yaml");
package.to_file(&path).unwrap();
let root = read_yaml_from_path(&path);
let release = root
.get("releases")
.unwrap()
.as_mapping()
.unwrap()
.get("2.0.0")
.unwrap()
.as_mapping()
.unwrap();
let keys: Vec<String> = release
.keys()
.map(|x| x.as_str().unwrap().to_string())
.collect();
assert_eq!(keys, &["x86_64-linux"]);
}
#[test]
fn saving_package_keeps_comment() {
let package = Package::from_yaml_str(
"
name: test
description: desc
homepage:
comment: Careful with test
releases:
2.0.0:
x86_64-linux:
url: https://example.com
sha256: '1234'
installs: {}
",
)
.unwrap();
let dir = assert_fs::TempDir::new().unwrap();
let path = dir.join("test.yaml");
package.to_file(&path).unwrap();
let root = read_yaml_from_path(&path);
let comment = root.get("comment").unwrap();
assert_eq!(comment.as_str().unwrap(), "Careful with test");
}
#[test]
fn test_get_version_matching() {
let package = Package::from_yaml_str(
"
name: test
description: desc
homepage:
releases:
2.0.0:
any:
url: https://example.com
sha256: '1234'
1.2.1:
any:
url: https://example.com
sha256: '1234'
1.2.0:
any:
url: https://example.com
sha256: '1234'
installs: {}
",
)
.unwrap();
let req300 = VersionReq::parse("3.0.0").unwrap();
let req121 = VersionReq::parse("1.2.1").unwrap();
let req12 = VersionReq::parse("1.2.*").unwrap();
let req2 = VersionReq::parse(">=2").unwrap();
let v121 = Version::new(1, 2, 1);
let v200 = Version::new(2, 0, 0);
assert_eq!(package.get_version_matching(&req300), None);
assert_eq!(package.get_version_matching(&req121), Some(&v121));
assert_eq!(package.get_version_matching(&req12), Some(&v121));
assert_eq!(package.get_version_matching(&req2), Some(&v200));
}
#[test]
fn get_install_should_use_the_any_arch_specific_os_install() {
let package = Package::from_yaml_str(
"
name: test
description: desc
homepage:
releases: {}
installs:
1.0.0:
any:
strip: 1
files:
foo:
any-macos:
strip: 3
files:
foo:
",
)
.unwrap();
let install = package
.get_install(
&Version::new(1, 0, 0),
&ArchOs::new(Arch::X86_64, Os::MacOs),
)
.unwrap();
assert_eq!(install.strip, 3);
}
#[test]
fn strip_should_default_to_0_if_not_set() {
let package = Package::from_yaml_str(
"
name: test
description: desc
homepage:
releases: {}
installs:
1.0.0:
any:
files:
foo:
",
);
let package = package.unwrap();
let install = package
.get_install(&Version::new(1, 0, 0), &ArchOs::any())
.unwrap();
assert_eq!(install.strip, 0);
}
}