diff --git a/src/env.rs b/src/env.rs index b6c8cb1..52eacc2 100644 --- a/src/env.rs +++ b/src/env.rs @@ -1,41 +1,27 @@ -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + error::Error, + path::PathBuf, + str::FromStr, +}; use crate::types::Installation; -pub fn map(installations: Vec) -> HashMap> { - let mut vars: HashMap<&str, OrderedSet> = HashMap::new(); +pub fn map(installations: &Vec) -> HashMap> { + let mut vars: HashMap> = HashMap::new(); let projects: HashSet<&str> = installations .iter() .map(|i| i.pkg.project.as_str()) .collect(); - let has_cmake = projects.contains("cmake.org"); - let archaic = true; - - let mut rv: HashMap> = HashMap::new(); - for installation in installations { - for key in &[ - EnvKey::Path, - EnvKey::Manpath, - EnvKey::PkgConfigPath, - EnvKey::LibraryPath, - EnvKey::LdLibraryPath, - EnvKey::Cpath, - EnvKey::XdgDataDirs, - EnvKey::CmakePrefixPath, - EnvKey::DyldFallbackLibraryPath, - EnvKey::SslCertFile, - EnvKey::Ldflags, - EnvKey::PkgxDir, - EnvKey::AclocalPath, - ] { - if let Some(suffixes) = suffixes(key) { + for key in EnvKey::iter() { + if let Some(suffixes) = suffixes(&key) { for suffix in suffixes { - let path = installation.path.join(suffix).to_string_lossy().to_string(); - if !path.is_empty() { - vars.entry(key.as_ref()) + let path = installation.path.join(suffix); + if path.is_dir() { + vars.entry(key.clone()) .or_insert_with(OrderedSet::new) .add(path); } @@ -43,56 +29,43 @@ pub fn map(installations: Vec) -> HashMap> { } } - if archaic { - let lib_path = installation.path.join("lib").to_string_lossy().to_string(); - vars.entry(EnvKey::LibraryPath.as_ref()) + if projects.contains("cmake.org") { + vars.entry(EnvKey::CmakePrefixPath) .or_insert_with(OrderedSet::new) - .add(lib_path); - - let include_path = installation - .path - .join("include") - .to_string_lossy() - .to_string(); - vars.entry(EnvKey::Cpath.as_ref()) - .or_insert_with(OrderedSet::new) - .add(include_path); - } - - if has_cmake { - vars.entry(EnvKey::CmakePrefixPath.as_ref()) - .or_insert_with(OrderedSet::new) - .add(installation.path.to_string_lossy().to_string()); + .add(installation.path.clone()); } } - if let Some(library_path) = vars.get(EnvKey::LibraryPath.as_ref()) { - let paths = library_path.to_vec(); - vars.entry(EnvKey::LdLibraryPath.as_ref()) - .or_insert_with(OrderedSet::new) - .items - .extend(paths.clone()); - - // We only need to set DYLD_FALLBACK_LIBRARY_PATH on macOS - #[cfg(target_os = "macos")] - vars.entry(EnvKey::DyldFallbackLibraryPath.as_ref()) - .or_insert_with(OrderedSet::new) - .items - .extend(paths); + // don’t break `man` + if vars.contains_key(&EnvKey::Manpath) { + vars.get_mut(&EnvKey::Manpath) + .unwrap() + .add(PathBuf::from_str("/usr/share/man").unwrap()); } - - for (key, set) in &vars { - if !set.is_empty() { - rv.insert(key.to_string(), set.to_vec()); - } + // https://github.com/pkgxdev/libpkgx/issues/70 + if vars.contains_key(&EnvKey::XdgDataDirs) { + let set = vars.get_mut(&EnvKey::XdgDataDirs).unwrap(); + set.add(PathBuf::from_str("/usr/local/share").unwrap()); + set.add(PathBuf::from_str("/usr/share").unwrap()); } + let mut rv: HashMap> = HashMap::new(); + for (key, set) in vars { + let set = set + .items + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + rv.insert(key.as_ref().to_string(), set); + } rv } -use strum_macros::{AsRefStr, EnumString}; +use rusqlite::Connection; +use strum::IntoEnumIterator; +use strum_macros::{AsRefStr, EnumIter, EnumString}; -#[derive(Debug, EnumString, AsRefStr, PartialEq, Eq, Hash, Clone)] +#[derive(Debug, EnumString, AsRefStr, PartialEq, Eq, Hash, Clone, EnumIter)] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] enum EnvKey { Path, @@ -103,6 +76,7 @@ enum EnvKey { Cpath, XdgDataDirs, CmakePrefixPath, + #[cfg(target_os = "macos")] DyldFallbackLibraryPath, SslCertFile, Ldflags, @@ -110,56 +84,44 @@ enum EnvKey { AclocalPath, } -pub struct OrderedSet { +struct OrderedSet { items: Vec, set: HashSet, } impl OrderedSet { - pub fn new() -> Self { + fn new() -> Self { OrderedSet { items: Vec::new(), set: HashSet::new(), } } - pub fn add(&mut self, item: T) { + fn add(&mut self, item: T) { if self.set.insert(item.clone()) { self.items.push(item); } } - - pub fn to_vec(&self) -> Vec { - self.items.clone() - } - - pub fn is_empty(&self) -> bool { - self.items.is_empty() - } } + fn suffixes(key: &EnvKey) -> Option> { match key { EnvKey::Path => Some(vec!["bin", "sbin"]), EnvKey::Manpath => Some(vec!["man", "share/man"]), EnvKey::PkgConfigPath => Some(vec!["share/pkgconfig", "lib/pkgconfig"]), EnvKey::XdgDataDirs => Some(vec!["share"]), - EnvKey::LibraryPath - | EnvKey::LdLibraryPath - | EnvKey::DyldFallbackLibraryPath - | EnvKey::Cpath - | EnvKey::CmakePrefixPath - | EnvKey::SslCertFile - | EnvKey::Ldflags - | EnvKey::PkgxDir - | EnvKey::AclocalPath => None, + EnvKey::AclocalPath => Some(vec!["share/aclocal"]), + EnvKey::LibraryPath | EnvKey::LdLibraryPath => Some(vec!["lib", "lib64"]), + #[cfg(target_os = "macos")] + EnvKey::DyldFallbackLibraryPath => Some(vec!["lib", "lib64"]), + EnvKey::Cpath => Some(vec!["include"]), + EnvKey::CmakePrefixPath | EnvKey::SslCertFile | EnvKey::Ldflags | EnvKey::PkgxDir => None, } } pub fn mix(input: HashMap>) -> HashMap { let mut rv = HashMap::new(); - //TODO handle empty values etc. - for (key, mut value) in std::env::vars() { if let Some(injected_values) = input.get(&key) { value = format!("{}:{}", injected_values.join(":"), value); @@ -169,3 +131,83 @@ pub fn mix(input: HashMap>) -> HashMap { rv } + +pub fn mix_runtime( + input: &HashMap, + installations: &Vec, + conn: &Connection, +) -> Result, Box> { + let mut output = input.clone(); + + for installation in installations.clone() { + let runtime_env = + crate::pantry_db::runtime_env_for_project(&installation.pkg.project, conn)?; + for (key, runtime_value) in runtime_env { + let runtime_value = expand_moustaches(&runtime_value, &installation, installations); + let new_value = match output.get(&key) { + Some(curr_value) => runtime_value.replace(&format!("${}", key), curr_value), + None => { + //TODO need to remove any $FOO, aware of `:` delimiters + runtime_value + .replace(&format!(":${}", key), "") + .replace(&format!("${}:", key), "") + .replace(&format!("${}", key), "") + } + }; + output.insert(key, new_value); + } + } + + Ok(output) +} + +pub fn expand_moustaches(input: &str, pkg: &Installation, deps: &Vec) -> String { + let mut output = input.to_string(); + + if output.starts_with("${{") { + output.replace_range(..1, ""); + } + + output = output.replace("{{prefix}}", &pkg.path.to_string_lossy()); + output = output.replace( + "{{version}}", + &crate::types::semverator_version_to_string(&pkg.pkg.version), + ); + output = output.replace("{{version.major}}", &format!("{}", pkg.pkg.version.major)); + output = output.replace("{{version.minor}}", &format!("{}", pkg.pkg.version.minor)); + output = output.replace("{{version.patch}}", &format!("{}", pkg.pkg.version.patch)); + output = output.replace( + "{{version.marketing}}", + &format!("{}.{}", pkg.pkg.version.major, pkg.pkg.version.minor), + ); + + for dep in deps { + let prefix = format!("deps.{}", dep.pkg.project); + output = output.replace( + &format!("{{{{{}.prefix}}}}", prefix), + &dep.path.to_string_lossy(), + ); + output = output.replace( + &format!("{{{{{}.version}}}}", prefix), + &crate::types::semverator_version_to_string(&dep.pkg.version), + ); + output = output.replace( + &format!("{{{{{}.version.major}}}}", prefix), + &format!("{}", dep.pkg.version.major), + ); + output = output.replace( + &format!("{{{{{}.version.minor}}}}", prefix), + &format!("{}", dep.pkg.version.minor), + ); + output = output.replace( + &format!("{{{{{}.version.patch}}}}", prefix), + &format!("{}", dep.pkg.version.patch), + ); + output = output.replace( + &format!("{{{{{}.version.marketing}}}}", prefix), + &format!("{}.{}", dep.pkg.version.major, dep.pkg.version.minor), + ); + } + + output +} diff --git a/src/main.rs b/src/main.rs index bccfc0d..cf32275 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ use types::PackageReq; #[tokio::main] async fn main() -> Result<(), Box> { let args::Args { - mut plus, + plus, mut args, mode, flags, @@ -54,9 +54,9 @@ async fn main() -> Result<(), Box> { let mut pkgs = vec![]; for arg in plus { - let arg = PackageReq::parse(&arg)?; - let pkg = pantry_db::resolve(arg, &conn)?; - pkgs.push(pkg); + let arg = PackageReq::parse(&arg)?; + let pkg = pantry_db::resolve(arg, &conn)?; + pkgs.push(pkg); } if find_program { @@ -79,11 +79,13 @@ async fn main() -> Result<(), Box> { installations.extend(installed); } - let env = env::map(installations); + let env = env::map(&installations); if args.is_empty() { - for (key, values) in env { - println!("{}={}", key, values.join(":")); + let env = env.iter().map(|(k, v)| (k.clone(), v.join(":"))).collect(); + let env = env::mix_runtime(&env, &installations, &conn)?; + for (key, value) in env { + println!("{}=\"{}${{{}:+:${}}}\"", key, value, key, key); } Ok(()) } else { @@ -111,6 +113,7 @@ async fn main() -> Result<(), Box> { utils::find_program(&args.remove(0), &paths, &config).await? }; let env = env::mix(env); + let env = env::mix_runtime(&env, &installations, &conn)?; execve(cmd, args, env) } } diff --git a/src/pantry_db.rs b/src/pantry_db.rs index 8be18a6..0673527 100644 --- a/src/pantry_db.rs +++ b/src/pantry_db.rs @@ -1,4 +1,4 @@ -use std::{error::Error, fs}; +use std::{collections::HashMap, error::Error, fs}; use rusqlite::{params, Connection}; @@ -91,7 +91,7 @@ pub fn deps_for_project( let mut stmt = conn.prepare("SELECT pkgspec FROM dependencies WHERE project = ?1")?; let rv = stmt.query_map(params![project], |row| { - let pkgspec = row.get(0)?; + let pkgspec: String = row.get(0)?; let pkgrq = PackageReq::parse(&pkgspec).unwrap(); //FIXME unwrap() Ok(pkgrq) })?; @@ -108,6 +108,22 @@ pub fn which(cmd: &String, conn: &Connection) -> Result> } } +pub fn runtime_env_for_project( + project: &String, + conn: &Connection, +) -> Result, Box> { + let sql = "SELECT envline FROM runtime_env WHERE project = ?1"; + let mut stmt = conn.prepare(sql)?; + let mut rows = stmt.query(params![project])?; + let mut env = HashMap::new(); + while let Some(row) = rows.next()? { + let envline: String = row.get(0)?; + let (key, value) = envline.split_once('=').unwrap(); + env.insert(key.to_string(), value.to_string()); + } + Ok(env) +} + // given pkgspec of program@version or project@version return PackageReq pub fn resolve(mut pkg: PackageReq, conn: &Connection) -> Result> { if let Ok(project) = which(&pkg.project, conn) { diff --git a/src/types.rs b/src/types.rs index 451463b..2304ea7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -34,7 +34,7 @@ pub struct PackageReq { use regex::Regex; impl PackageReq { - pub fn parse(pkgspec: &String) -> Result> { + pub fn parse(pkgspec: &str) -> Result> { let input = pkgspec.trim(); let captures = PACKAGE_REGEX .captures(input) @@ -55,7 +55,7 @@ impl PackageReq { } } -fn semverator_version_to_string(version: &Version) -> String { +pub fn semverator_version_to_string(version: &Version) -> String { version .components .iter()