diff --git a/crates/bin/src/install_path.rs b/crates/bin/src/install_path.rs index 71da8317..f37313fc 100644 --- a/crates/bin/src/install_path.rs +++ b/crates/bin/src/install_path.rs @@ -4,23 +4,33 @@ use std::{ }; use binstalk::home::cargo_home; +use binstalk_manifests::cargo_config::Config; use tracing::debug; pub fn get_cargo_roots_path(cargo_roots: Option) -> Option { if let Some(p) = cargo_roots { - return Some(p); - } - - // Environmental variables - if let Some(p) = var_os("CARGO_INSTALL_ROOT") { + Some(p) + } else if let Some(p) = var_os("CARGO_INSTALL_ROOT") { + // Environmental variables let p = PathBuf::from(p); debug!("using CARGO_INSTALL_ROOT ({})", p.display()); - return Some(p); - } - - if let Ok(p) = cargo_home() { - debug!("using ({}) as cargo home", p.display()); Some(p) + } else if let Ok(cargo_home) = cargo_home() { + let config_path = cargo_home.join("config.toml"); + if let Some(root) = Config::load_from_path(&config_path) + .ok() + .and_then(|config| config.install.root) + { + debug!( + "using `install.root` {} from config {}", + root.display(), + config_path.display() + ); + Some(root) + } else { + debug!("using ({}) as cargo home", cargo_home.display()); + Some(cargo_home) + } } else { None } diff --git a/crates/binstalk-manifests/src/cargo_config.rs b/crates/binstalk-manifests/src/cargo_config.rs new file mode 100644 index 00000000..f84b1887 --- /dev/null +++ b/crates/binstalk-manifests/src/cargo_config.rs @@ -0,0 +1,161 @@ +//! Cargo's `.cargo/config.toml` +//! +//! This manifest is used by Cargo to load configurations stored by users. +//! +//! Binstall reads from them to be compatible with `cargo-install`'s behavior. + +use std::{ + fs::File, + io, + path::{Path, PathBuf}, +}; + +use compact_str::CompactString; +use fs_lock::FileLock; +use home::cargo_home; +use miette::Diagnostic; +use serde::Deserialize; +use thiserror::Error; + +#[derive(Debug, Default, Deserialize)] +pub struct Install { + /// `cargo install` destination directory + pub root: Option, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Http { + /// HTTP proxy in libcurl format: "host:port" + pub proxy: Option, + /// timeout for each HTTP request, in seconds + pub timeout: Option, + /// path to Certificate Authority (CA) bundle + pub cainfo: Option, + // TODO: + // Support field ssl-version, ssl-version.max, ssl-version.min, + // which needs `toml_edit::Item`. +} + +#[derive(Debug, Default, Deserialize)] +pub struct Config { + pub install: Install, + pub http: Http, + // TODO: + // Add support for section patch, source and registry for alternative + // crates.io registry. + + // TODO: + // Add field env for specifying env vars + // which needs `toml_edit::Item`. +} + +fn join_if_relative(path: &mut Option, dir: &Path) { + match path { + Some(path) if path.is_relative() => *path = dir.join(&path), + _ => (), + } +} + +impl Config { + pub fn default_path() -> Result { + Ok(cargo_home()?.join("config.toml")) + } + + pub fn load() -> Result { + Self::load_from_path(Self::default_path()?) + } + + /// * `dir` - path to the dir where the config.toml is located. + /// For relative path in the config, `Config::load_from_reader` + /// will join the `dir` and the relative path to form the final + /// path. + pub fn load_from_reader( + mut reader: R, + dir: &Path, + ) -> Result { + fn inner(reader: &mut dyn io::Read, dir: &Path) -> Result { + let mut vec = Vec::new(); + reader.read_to_end(&mut vec)?; + + if vec.is_empty() { + Ok(Default::default()) + } else { + let mut config: Config = toml_edit::de::from_slice(&vec)?; + join_if_relative(&mut config.install.root, dir); + join_if_relative(&mut config.http.cainfo, dir); + Ok(config) + } + } + + inner(&mut reader, dir) + } + + pub fn load_from_path(path: impl AsRef) -> Result { + let path = path.as_ref(); + let file = FileLock::new_shared(File::open(path)?)?; + // Any regular file must have a parent dir + Self::load_from_reader(file, path.parent().unwrap()) + } +} + +#[derive(Debug, Diagnostic, Error)] +#[non_exhaustive] +pub enum ConfigLoadError { + #[error("I/O Error: {0}")] + Io(#[from] io::Error), + + #[error("Failed to deserialize toml: {0}")] + TomlParse(Box), +} + +impl From for ConfigLoadError { + fn from(e: toml_edit::de::Error) -> Self { + ConfigLoadError::TomlParse(Box::new(e)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::io::Cursor; + + const CONFIG: &str = r#" +[env] +# Set ENV_VAR_NAME=value for any process run by Cargo +ENV_VAR_NAME = "value" +# Set even if already present in environment +ENV_VAR_NAME_2 = { value = "value", force = true } +# Value is relative to .cargo directory containing `config.toml`, make absolute +ENV_VAR_NAME_3 = { value = "relative/path", relative = true } + +[http] +debug = false # HTTP debugging +proxy = "host:port" # HTTP proxy in libcurl format +timeout = 30 # timeout for each HTTP request, in seconds +cainfo = "cert.pem" # path to Certificate Authority (CA) bundle + +[install] +root = "/some/path" # `cargo install` destination directory + "#; + + #[test] + fn test_loading() { + let config = Config::load_from_reader(Cursor::new(&CONFIG), Path::new("/root")).unwrap(); + + assert_eq!( + config.install.root.as_deref().unwrap(), + Path::new("/some/path") + ); + assert_eq!( + config.http.proxy, + Some(CompactString::new_inline("host:port")) + ); + + assert_eq!(config.http.timeout, Some(30)); + assert_eq!( + config.http.cainfo.as_deref().unwrap(), + Path::new("/root/cert.pem") + ); + } +} diff --git a/crates/binstalk-manifests/src/lib.rs b/crates/binstalk-manifests/src/lib.rs index 418764f6..f3befa80 100644 --- a/crates/binstalk-manifests/src/lib.rs +++ b/crates/binstalk-manifests/src/lib.rs @@ -11,6 +11,7 @@ mod helpers; pub mod binstall_crates_v1; +pub mod cargo_config; pub mod cargo_crates_v1; pub use binstalk_types::{cargo_toml_binstall, crate_info};