pub mod pesde;
pub mod wally;
use crate::error::Error;
use crate::net::http;
use crate::project::manifest::Environment;
use crate::sys::git;
use crate::sys::paths;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
pub struct Index {
url: String,
root: PathBuf,
kind: Kind,
}
enum Kind {
Wally(wally::Config),
Pesde(pesde::Config),
}
pub struct ResolvedPackage {
pub version: semver::Version,
pub environment: Option<Environment>,
pub dependencies: Vec<TransitiveDependency>,
pub source: DownloadSource,
}
pub struct TransitiveDependency {
pub name: String,
pub version_req: String,
pub index_url: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum DownloadSource {
Zip { url: String },
TarGz { url: String },
}
impl Index {
pub fn open(url: &str, refresh: bool) -> Result<Self, Error> {
let root = ensure_cached(url, refresh)?;
let kind = if root.join("config.json").exists() {
Kind::Wally(wally::load_config(&root)?)
} else if root.join("config.toml").exists() {
Kind::Pesde(pesde::load_config(&root)?)
} else {
return Err(Error::IndexFetch {
url: url.to_string(),
reason: "the index has no config.json or config.toml at its root".to_string(),
});
};
Ok(Index {
url: url.to_string(),
root,
kind,
})
}
pub fn resolve(
&self,
name: &str,
req: &semver::VersionReq,
prefer_environment: Option<Environment>,
) -> Result<ResolvedPackage, Error> {
match &self.kind {
Kind::Wally(config) => wally::resolve(&self.root, &self.url, config, name, req),
Kind::Pesde(config) => {
pesde::resolve(&self.root, &self.url, config, name, req, prefer_environment)
}
}
}
}
pub fn download(source: &DownloadSource, dest: &Path) -> Result<(), Error> {
std::fs::create_dir_all(dest)?;
let (DownloadSource::Zip { url } | DownloadSource::TarGz { url }) = source;
let mut headers: Vec<(&str, &str)> = Vec::new();
if matches!(source, DownloadSource::Zip { .. }) {
headers.push(("Wally-Version", wally::VERSION_HEADER));
}
let bytes = http::get_bytes(url, &headers)?;
match source {
DownloadSource::Zip { .. } => {
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes))?;
archive.extract(dest)?;
}
DownloadSource::TarGz { .. } => {
if bytes.starts_with(&[0x1f, 0x8b]) {
let decoder = flate2::read::GzDecoder::new(bytes.as_slice());
tar::Archive::new(decoder).unpack(dest)?;
} else {
tar::Archive::new(bytes.as_slice()).unpack(dest)?;
}
}
}
Ok(())
}
fn cache_dir(url: &str) -> Result<PathBuf, Error> {
let slug: String = url
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or("index")
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect();
let mut hasher = DefaultHasher::new();
url.hash(&mut hasher);
Ok(paths::index_cache_dir()?.join(format!("{slug}-{:016x}", hasher.finish())))
}
fn ensure_cached(url: &str, refresh: bool) -> Result<PathBuf, Error> {
let dir = cache_dir(url)?;
if dir.join(".git").exists() {
if refresh
&& let Err(reason) = git::run(&["-C", &dir.to_string_lossy(), "pull", "--ff-only"])
{
eprintln!("warning: could not refresh index {url} ({reason}); using cached copy");
}
return Ok(dir);
}
std::fs::create_dir_all(dir.parent().expect("cache dir has a parent"))?;
git::run(&["clone", "--depth", "1", url, &dir.to_string_lossy()]).map_err(|reason| {
Error::IndexFetch {
url: url.to_string(),
reason,
}
})?;
Ok(dir)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_dirs_are_per_url() {
let Ok(wally) = cache_dir("https://github.com/UpliftGames/wally-index") else {
return; };
let pesde = cache_dir("https://github.com/pesde-pkg/index").unwrap();
assert!(wally.starts_with(paths::index_cache_dir().unwrap()));
assert!(
wally
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("wally-index-")
);
assert_ne!(wally, pesde);
assert_eq!(
pesde,
cache_dir("https://github.com/pesde-pkg/index").unwrap()
);
}
}