Skip to content

Commit

Permalink
config: Parse [env] block
Browse files Browse the repository at this point in the history
  • Loading branch information
MarijnS95 committed Feb 28, 2022
1 parent 421131a commit 55269e6
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 20 deletions.
151 changes: 147 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
use crate::error::Error;
use serde::Deserialize;
use std::path::Path;
use std::{
borrow::Cow,
collections::BTreeMap,
path::{Path, PathBuf},
};

#[derive(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
pub build: Option<Build>,
/// <https://doc.rust-lang.org/cargo/reference/config.html#env>
pub env: BTreeMap<String, EnvOption>,
}

impl Config {
Expand All @@ -14,8 +21,144 @@ impl Config {
}
}

#[derive(Debug, Deserialize)]
#[derive(Clone, Debug)]
pub struct LocalizedConfig {
pub config: Config,
/// The directory containing `./.cargo/config.toml`
pub workspace: PathBuf,
}

impl std::ops::Deref for LocalizedConfig {
type Target = Config;

fn deref(&self) -> &Self::Target {
&self.config
}
}

impl LocalizedConfig {
pub fn new(workspace: PathBuf) -> Result<Self, Error> {
Ok(Self {
config: Config::parse_from_toml(&workspace.join(".cargo/config.toml"))?,
workspace,
})
}

/// Search for `.cargo/config.toml` in any parent of the workspace root path.
/// Returns the directory which contains this path, not the path to the config file.
fn find_cargo_config_parent(workspace: impl AsRef<Path>) -> Result<Option<PathBuf>, Error> {
let workspace = dunce::canonicalize(workspace)?;
Ok(workspace
.ancestors()
.find(|dir| dir.join(".cargo/config.toml").is_file())
.map(|p| p.to_path_buf()))
}

/// Search for and open `.cargo/config.toml` in any parent of the workspace root path.
pub fn find_cargo_config_for_workspace(
workspace: impl AsRef<Path>,
) -> Result<Option<Self>, Error> {
let config = Self::find_cargo_config_parent(workspace)?;
config.map(LocalizedConfig::new).transpose()
}

pub fn resolve_env(
&self,
key_name: &str,
) -> std::io::Result<Option<std::borrow::Cow<'_, str>>> {
self.config
.env
.get(key_name)
.map(|v| v.resolve_value(key_name, &self.workspace))
.transpose()
}
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct Build {
#[serde(rename = "target-dir")]
pub target_dir: Option<String>,
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum EnvOption {
String(String),
Value {
value: String,
#[serde(default)]
force: bool,
#[serde(default)]
relative: bool,
},
}

impl EnvOption {
/// `config_parent` is the directory containing `.cargo/config.toml` where this was parsed from
pub fn resolve_value(
&self,
key_name: &str,
config_parent: impl AsRef<Path>,
) -> std::io::Result<Cow<'_, str>> {
// Environment variables always have precedence, unless the extended format
// is used to set force=true
if !matches!(self, Self::Value { force: true, .. }) {
// TODO: Differentiate between invalid strings and key not found
if let Ok(env) = std::env::var(key_name) {
return Ok(env.into());
}
}

Ok(match self {
Self::Value {
value,
relative: true,
force: _,
} => dunce::canonicalize(config_parent.as_ref().join(value))?
.to_str()
.unwrap()
.to_owned()
.into(),
Self::String(value) | Self::Value { value, .. } => value.into(),
})
}
}

#[test]
fn test_env_parsing() {
let toml = 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 }"#;

let mut env = BTreeMap::new();
env.insert(
"ENV_VAR_NAME".to_string(),
EnvOption::String("value".into()),
);
env.insert(
"ENV_VAR_NAME_2".to_string(),
EnvOption::Value {
value: "value".into(),
force: true,
relative: false,
},
);
env.insert(
"ENV_VAR_NAME_3".to_string(),
EnvOption::Value {
value: "relative/path".into(),
force: false,
relative: true,
},
);

assert_eq!(
toml::from_str::<Config>(toml),
Ok(Config { build: None, env })
);
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod subcommand;
mod utils;

pub use artifact::{Artifact, CrateType};
pub use config::LocalizedConfig;
pub use error::Error;
pub use profile::Profile;
pub use subcommand::Subcommand;
31 changes: 27 additions & 4 deletions src/subcommand.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::artifact::Artifact;
use crate::error::Error;
use crate::profile::Profile;
use crate::utils;
use crate::{utils, LocalizedConfig};
use std::io::BufRead;
use std::path::{Path, PathBuf};
use std::process::Command;
Expand All @@ -18,6 +18,7 @@ pub struct Subcommand {
profile: Profile,
artifacts: Vec<Artifact>,
quiet: bool,
config: Option<LocalizedConfig>,
}

impl Subcommand {
Expand Down Expand Up @@ -103,13 +104,15 @@ impl Subcommand {
}
});

let config = LocalizedConfig::find_cargo_config_for_workspace(&root_dir).unwrap();

let target_dir = target_dir.unwrap_or_else(|| {
utils::find_workspace(&manifest, &package)
.unwrap()
.unwrap_or_else(|| manifest.clone())
.parent()
.unwrap()
.join(utils::get_target_dir_name(root_dir).unwrap())
.join(utils::get_target_dir_name(config.as_deref()).unwrap())
});
if examples {
for file in utils::list_rust_files(&root_dir.join("examples"))? {
Expand Down Expand Up @@ -145,6 +148,7 @@ impl Subcommand {
profile,
artifacts,
quiet,
config,
})
}

Expand Down Expand Up @@ -187,6 +191,10 @@ impl Subcommand {
pub fn quiet(&self) -> bool {
self.quiet
}

pub fn config(&self) -> Option<&LocalizedConfig> {
self.config.as_ref()
}
}

#[cfg(test)]
Expand All @@ -195,14 +203,29 @@ mod tests {

#[test]
fn test_separator_space() {
let args = ["cargo", "subcommand", "build", "--target", "x86_64-unknown-linux-gnu"].iter().map(|s| s.to_string());
let args = [
"cargo",
"subcommand",
"build",
"--target",
"x86_64-unknown-linux-gnu",
]
.iter()
.map(|s| s.to_string());
let cmd = Subcommand::new(args, "subcommand", |_, _| Ok(false)).unwrap();
assert_eq!(cmd.target(), Some("x86_64-unknown-linux-gnu"));
}

#[test]
fn test_separator_equals() {
let args = ["cargo", "subcommand", "build", "--target=x86_64-unknown-linux-gnu"].iter().map(|s| s.to_string());
let args = [
"cargo",
"subcommand",
"build",
"--target=x86_64-unknown-linux-gnu",
]
.iter()
.map(|s| s.to_string());
let cmd = Subcommand::new(args, "subcommand", |_, _| Ok(false)).unwrap();
assert_eq!(cmd.target(), Some("x86_64-unknown-linux-gnu"));
}
Expand Down
17 changes: 5 additions & 12 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,11 @@ pub fn find_workspace(manifest: &Path, name: &str) -> Result<Option<PathBuf>, Er
Ok(None)
}

/// Search for .cargo/config.toml file relative to the workspace root path.
pub fn find_cargo_config(path: &Path) -> Result<Option<PathBuf>, Error> {
let path = dunce::canonicalize(path)?;
Ok(path
.ancestors()
.map(|dir| dir.join(".cargo/config.toml"))
.find(|dir| dir.is_file()))
}

pub fn get_target_dir_name(path: &Path) -> Result<String, Error> {
if let Some(config_path) = find_cargo_config(path)? {
let config = Config::parse_from_toml(&config_path)?;
/// Returns the [`target-dir`] configured in `.cargo/config.toml` or `"target"` if not set.
///
/// [`target-dir`](https://doc.rust-lang.org/cargo/reference/config.html#buildtarget-dir)
pub fn get_target_dir_name(config: Option<&Config>) -> Result<String, Error> {
if let Some(config) = config {
if let Some(build) = config.build.as_ref() {
if let Some(target_dir) = &build.target_dir {
return Ok(target_dir.clone());
Expand Down

0 comments on commit 55269e6

Please sign in to comment.