use std::env;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use camino::{Utf8Path, Utf8PathBuf};
use ini::Ini;
use serde::{Deserialize, Serialize};
use crate::macros::build_from_paths;
use crate::repo::ebuild::Repo as EbuildRepo;
use crate::repo::temp::Repo as TempRepo;
use crate::repo::Repo;
use crate::Error;
pub(crate) use repo::RepoConfig;
mod repo;
#[repr(C)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub enum RepoSetType {
All,
Ebuild,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct ConfigPath {
pub cache: Utf8PathBuf,
pub config: Utf8PathBuf,
pub data: Utf8PathBuf,
pub db: Utf8PathBuf,
pub run: Utf8PathBuf,
pub tmp: Utf8PathBuf,
}
impl ConfigPath {
fn new(name: &str, prefix: &str) -> Self {
let home = env::var("HOME").ok().unwrap_or_else(|| "/root".to_string());
let (config, cache, data, db, run, tmp): (
Utf8PathBuf,
Utf8PathBuf,
Utf8PathBuf,
Utf8PathBuf,
Utf8PathBuf,
Utf8PathBuf,
);
let prefixed = |p: Utf8PathBuf| -> Utf8PathBuf {
match prefix.is_empty() {
true => p,
false => Utf8PathBuf::from(prefix).join(p.strip_prefix("/").unwrap_or(&p)),
}
};
let user_config: Utf8PathBuf = match env::var("XDG_CONFIG_HOME") {
Ok(x) => prefixed(build_from_paths!(&x, name)),
Err(_) => prefixed(build_from_paths!(&home, ".config", name)),
};
let system_config = prefixed(Utf8PathBuf::from(format!("/etc/{name}")));
config = match (user_config.exists(), system_config.exists() || home == "/root") {
(false, true) => {
cache = prefixed(Utf8PathBuf::from(format!("/var/cache/{name}")));
data = prefixed(Utf8PathBuf::from(format!("/usr/share/{name}")));
db = prefixed(Utf8PathBuf::from(format!("/var/db/{name}")));
run = prefixed(Utf8PathBuf::from(format!("/run/{name}")));
tmp = prefixed(Utf8PathBuf::from(format!("/var/tmp/{name}")));
system_config
}
_ => {
cache = match env::var("XDG_CACHE_HOME") {
Ok(x) => prefixed(build_from_paths!(&x, name)),
Err(_) => prefixed(build_from_paths!(&home, ".cache", name)),
};
data = match env::var("XDG_DATA_HOME") {
Ok(x) => prefixed(build_from_paths!(&x, name)),
Err(_) => prefixed(build_from_paths!(&home, ".local", "share", name)),
};
run = match env::var("XDG_RUNTIME_DIR") {
Ok(x) => prefixed(build_from_paths!(&x, name)),
Err(_) => cache.clone(),
};
db = data.clone();
tmp = cache.clone();
user_config
}
};
Self {
cache,
config,
data,
db,
run,
tmp,
}
}
fn create_paths(&self) -> crate::Result<()> {
for path in [&self.cache, &self.config, &self.data, &self.db, &self.run] {
fs::create_dir_all(path).map_err(|e| Error::Config(e.to_string()))?;
}
Ok(())
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct Config {
pub path: ConfigPath,
pub repos: repo::Config,
}
impl Config {
pub fn new(name: &str, prefix: &str) -> Self {
let path = ConfigPath::new(name, prefix);
Config {
path,
..Default::default()
}
}
pub fn load_repos(&mut self) -> crate::Result<()> {
self.repos = repo::Config::new(&self.path.config, &self.path.db)?;
Ok(())
}
pub fn load_repos_conf<P: AsRef<Utf8Path>>(&mut self, path: P) -> crate::Result<Vec<Repo>> {
let path = path.as_ref();
let files: Vec<_> = match path.read_dir() {
Ok(entries) => Ok(entries.filter_map(|d| d.ok()).map(|d| d.path()).collect()),
Err(e) if e.raw_os_error() == Some(20) => Ok(vec![PathBuf::from(path)]),
Err(e) => Err(Error::Config(format!("failed reading repos.conf: {path:?}: {e}"))),
}?;
let mut repos = vec![];
for f in files {
Ini::load_from_file(&f)
.map_err(|e| Error::Config(format!("invalid repos.conf file: {f:?}: {e}")))
.and_then(|ini| {
for (name, settings) in ini.iter().filter_map(|(section, p)| match section {
Some(s) if s != "DEFAULT" => Some((s, p)),
_ => None,
}) {
let priority = settings.get("priority").unwrap_or("0").parse().unwrap_or(0);
let path = settings.get("location").ok_or_else(|| {
Error::Config(format!(
"invalid repos.conf file: {f:?}: missing location field: {name}"
))
})?;
let r = self.repos.add_path(name, priority, path)?;
repos.push(r);
}
Ok(())
})?;
}
if !repos.is_empty() {
self.repos.extend(&repos)?;
}
repos.sort();
Ok(repos)
}
pub fn create_paths(&self) -> crate::Result<()> {
self.path.create_paths()?;
self.repos.create_paths()?;
Ok(())
}
pub fn add_repo_path(&mut self, name: &str, priority: i32, path: &str) -> crate::Result<Repo> {
let r = self.repos.add_path(name, priority, path)?;
self.add_repo(&r)?;
Ok(r)
}
pub fn add_repo_uri(&mut self, name: &str, priority: i32, uri: &str) -> crate::Result<Repo> {
let r = self.repos.add_uri(name, priority, uri)?;
self.add_repo(&r)?;
Ok(r)
}
pub fn add_repo(&mut self, repo: &Repo) -> crate::Result<()> {
self.repos.extend([repo])
}
pub fn create_repo(&mut self, name: &str, priority: i32) -> crate::Result<Repo> {
let r = self.repos.create(name, priority)?;
self.add_repo(&r)?;
Ok(r)
}
pub fn del_repos<S: AsRef<str>>(&mut self, repos: &[S], clean: bool) -> crate::Result<()> {
self.repos.del(repos, clean)?;
Ok(())
}
pub fn temp_repo(
&mut self,
name: &str,
priority: i32,
) -> crate::Result<(TempRepo, Arc<EbuildRepo>)> {
let (temp_repo, r) = self.repos.create_temp(name, priority)?;
self.add_repo(&r)?;
let repo = r.as_ebuild().expect("invalid ebuild repo: {name}");
Ok((temp_repo, repo.clone()))
}
}
#[cfg(test)]
mod tests {
use std::env;
use tempfile::tempdir;
use crate::macros::assert_err_re;
use crate::repo::Repository;
use crate::test::assert_ordered_eq;
use super::*;
#[test]
fn test_config() {
env::set_var("XDG_CACHE_HOME", "/cache");
env::set_var("XDG_CONFIG_HOME", "/config");
env::set_var("XDG_RUNTIME_DIR", "/run/user/4321");
env::set_var("HOME", "/home/user");
let config = Config::new("pkgcraft", "");
assert_eq!(config.path.cache, Utf8PathBuf::from("/cache/pkgcraft"));
assert_eq!(config.path.config, Utf8PathBuf::from("/config/pkgcraft"));
assert_eq!(config.path.run, Utf8PathBuf::from("/run/user/4321/pkgcraft"));
let config = Config::new("pkgcraft", "/prefix");
assert_eq!(config.path.cache, Utf8PathBuf::from("/prefix/cache/pkgcraft"));
assert_eq!(config.path.config, Utf8PathBuf::from("/prefix/config/pkgcraft"));
assert_eq!(config.path.run, Utf8PathBuf::from("/prefix/run/user/4321/pkgcraft"));
env::remove_var("XDG_CACHE_HOME");
env::remove_var("XDG_CONFIG_HOME");
env::remove_var("XDG_RUNTIME_DIR");
let config = Config::new("pkgcraft", "");
assert_eq!(config.path.cache, Utf8PathBuf::from("/home/user/.cache/pkgcraft"));
assert_eq!(config.path.config, Utf8PathBuf::from("/home/user/.config/pkgcraft"));
assert_eq!(config.path.run, Utf8PathBuf::from("/home/user/.cache/pkgcraft"));
let config = Config::new("pkgcraft", "/prefix");
assert_eq!(config.path.cache, Utf8PathBuf::from("/prefix/home/user/.cache/pkgcraft"));
assert_eq!(config.path.config, Utf8PathBuf::from("/prefix/home/user/.config/pkgcraft"));
assert_eq!(config.path.run, Utf8PathBuf::from("/prefix/home/user/.cache/pkgcraft"));
env::remove_var("HOME");
let config = Config::new("pkgcraft", "");
assert_eq!(config.path.cache, Utf8PathBuf::from("/var/cache/pkgcraft"));
assert_eq!(config.path.config, Utf8PathBuf::from("/etc/pkgcraft"));
assert_eq!(config.path.run, Utf8PathBuf::from("/run/pkgcraft"));
}
#[test]
fn test_load_repos_conf() {
let mut config = Config::new("pkgcraft", "");
let tmpdir = tempdir().unwrap();
let conf_path = tmpdir.path().join("repos.conf");
let path = conf_path.to_str().unwrap();
let r = config.load_repos_conf("nonexistent");
assert_err_re!(r, "failed reading repos.conf");
let data = indoc::indoc! {r#"
[DEFAULT]
main-repo = gentoo
[overlay
location = /path/to/overlay
"#};
fs::write(path, data).unwrap();
let r = config.load_repos_conf(path);
assert_err_re!(r, "invalid repos.conf file");
let data = indoc::indoc! {r#"
[DEFAULT]
main-repo = gentoo
[overlay]
"#};
fs::write(path, data).unwrap();
let r = config.load_repos_conf(path);
assert_err_re!(r, "missing location field: overlay");
fs::write(path, "").unwrap();
let repos = config.load_repos_conf(path).unwrap();
assert!(repos.is_empty());
let t1 = TempRepo::new("test", None, None).unwrap();
let data = indoc::formatdoc! {r#"
[a]
location = {}
"#, t1.path()};
fs::write(path, data).unwrap();
let repos = config.load_repos_conf(path).unwrap();
assert_ordered_eq(repos.iter().map(|r| r.id()), ["a"]);
let t2 = TempRepo::new("r2", None, None).unwrap();
let data = indoc::formatdoc! {r#"
[b]
location = {}
[c]
location = {}
priority = 1
"#, t1.path(), t2.path()};
fs::write(path, data).unwrap();
let repos = config.load_repos_conf(path).unwrap();
assert_ordered_eq(repos.iter().map(|r| r.id()), ["c", "b"]);
let t3 = TempRepo::new("r3", None, None).unwrap();
let tmpdir = tempdir().unwrap();
let conf_dir = tmpdir.path();
let data = indoc::formatdoc! {r#"
[r1]
location = {}
"#, t1.path()};
fs::write(conf_dir.join("r1.conf"), data).unwrap();
let data = indoc::formatdoc! {r#"
[r2]
location = {}
priority = -1
"#, t2.path()};
fs::write(conf_dir.join("r2.conf"), data).unwrap();
let data = indoc::formatdoc! {r#"
[r3]
location = {}
priority = 1
"#, t3.path()};
fs::write(conf_dir.join("r3.conf"), data).unwrap();
let repos = config.load_repos_conf(conf_dir.to_str().unwrap()).unwrap();
assert_ordered_eq(repos.iter().map(|r| r.id()), ["r3", "r1", "r2"]);
let data = indoc::formatdoc! {r#"
[r1]
location = {}
"#, t1.path()};
fs::write(path, data).unwrap();
let r = config.load_repos_conf(path);
assert_err_re!(r, "existing repo: r1");
let t = TempRepo::new("bad", None, None).unwrap();
let repo = EbuildRepo::from_path("bad", 0, t.path()).unwrap();
repo.config().write(Some("masters = x y z")).unwrap();
let data = indoc::formatdoc! {r#"
[bad]
location = {}
"#, t.path()};
fs::write(path, data).unwrap();
let r = config.load_repos_conf(path);
assert_err_re!(r, "^.* unconfigured repos: x, y, z$");
}
}