use std::collections::HashMap;
use anyhow::Result;
use cabin_core::ResolvedProfileFlags;
use cabin_env::{EnvBuildFlags, EnvBuildFlagsError, parse_env_build_flags};
use cabin_workspace::PackageGraph;
use crate::cli::term_verbosity::Reporter;
use crate::plural;
pub(crate) fn augment_build_flags_with_env<F>(
graph: &PackageGraph,
mut build_flags: HashMap<usize, ResolvedProfileFlags>,
env: F,
reporter: Reporter,
) -> Result<(HashMap<usize, ResolvedProfileFlags>, EnvBuildFlags)>
where
F: Fn(&str) -> Option<std::ffi::OsString>,
{
let parsed = match parse_env_build_flags(&env) {
Ok(flags) => flags,
Err(err) => return Err(format_parse_error(err)),
};
if parsed.is_empty() {
return Ok((build_flags, parsed));
}
apply_to_primary_packages(graph, &mut build_flags, &parsed);
if reporter.verbosity().shows_very_verbose() {
log_very_verbose(&parsed, reporter);
} else if reporter.verbosity().shows_verbose() {
log_verbose(&parsed, reporter);
}
Ok((build_flags, parsed))
}
fn format_parse_error(err: EnvBuildFlagsError) -> anyhow::Error {
anyhow::Error::new(err)
}
fn apply_to_primary_packages(
graph: &PackageGraph,
build_flags: &mut HashMap<usize, ResolvedProfileFlags>,
parsed: &EnvBuildFlags,
) {
for &idx in &graph.primary_packages {
let entry = build_flags.entry(idx).or_default();
entry
.extra_compile_args
.extend(parsed.cppflags.iter().cloned());
entry.cflags.extend(parsed.cflags.iter().cloned());
entry.cxxflags.extend(parsed.cxxflags.iter().cloned());
entry.ldflags.extend(parsed.ldflags.iter().cloned());
}
}
fn log_verbose(parsed: &EnvBuildFlags, reporter: Reporter) {
if !parsed.cppflags.is_empty() {
reporter.aux_verbose(format_args!(
"cabin: applying CPPFLAGS ({} arg{})",
parsed.cppflags.len(),
plural(parsed.cppflags.len()),
));
}
if !parsed.cflags.is_empty() {
reporter.aux_verbose(format_args!(
"cabin: applying CFLAGS ({} arg{})",
parsed.cflags.len(),
plural(parsed.cflags.len()),
));
}
if !parsed.cxxflags.is_empty() {
reporter.aux_verbose(format_args!(
"cabin: applying CXXFLAGS ({} arg{})",
parsed.cxxflags.len(),
plural(parsed.cxxflags.len()),
));
}
if !parsed.ldflags.is_empty() {
reporter.aux_verbose(format_args!(
"cabin: applying LDFLAGS ({} arg{})",
parsed.ldflags.len(),
plural(parsed.ldflags.len()),
));
}
}
fn log_very_verbose(parsed: &EnvBuildFlags, reporter: Reporter) {
if !parsed.cppflags.is_empty() {
reporter.aux_very_verbose(format_args!("cabin: CPPFLAGS = {}", join(&parsed.cppflags)));
}
if !parsed.cflags.is_empty() {
reporter.aux_very_verbose(format_args!("cabin: CFLAGS = {}", join(&parsed.cflags)));
}
if !parsed.cxxflags.is_empty() {
reporter.aux_very_verbose(format_args!("cabin: CXXFLAGS = {}", join(&parsed.cxxflags)));
}
if !parsed.ldflags.is_empty() {
reporter.aux_very_verbose(format_args!("cabin: LDFLAGS = {}", join(&parsed.ldflags)));
}
}
fn join(args: &[String]) -> String {
args.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
use cabin_core::{Package, PackageName, ResolvedProfileFlags, Target};
use cabin_workspace::{PackageGraph, PackageKind, WorkspacePackage};
use std::ffi::OsString;
use std::path::PathBuf;
fn version() -> semver::Version {
semver::Version::parse("0.1.0").unwrap()
}
fn make_pkg(name: &str) -> WorkspacePackage {
let package = Package::new(
PackageName::new(name).unwrap(),
version(),
Vec::<Target>::new(),
Vec::new(),
)
.unwrap();
WorkspacePackage {
package,
manifest_dir: PathBuf::from("/tmp"),
manifest_path: PathBuf::from("/tmp/cabin.toml"),
kind: PackageKind::Local,
deps: Vec::new(),
is_port: false,
}
}
fn one_primary_graph() -> PackageGraph {
let pkg = make_pkg("root");
PackageGraph {
root_manifest_path: PathBuf::from("/tmp/cabin.toml"),
root_dir: PathBuf::from("/tmp"),
is_workspace_root: false,
root_package: Some(0),
root_settings: Default::default(),
primary_packages: vec![0],
default_members: vec![0],
excluded_members: Vec::new(),
packages: vec![pkg],
}
}
fn quiet_reporter() -> Reporter {
Reporter::new(cabin_core::Verbosity::Normal)
}
fn env_from<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<OsString> + 'a {
move |key: &str| {
pairs
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| OsString::from(*v))
}
}
#[test]
fn empty_env_leaves_flags_unchanged() {
let graph = one_primary_graph();
let mut start = HashMap::new();
start.insert(0, ResolvedProfileFlags::default());
let (out, parsed) =
augment_build_flags_with_env(&graph, start.clone(), |_| None, quiet_reporter())
.unwrap();
assert!(parsed.is_empty());
assert_eq!(out, start);
}
#[test]
fn cppflags_land_in_extra_compile_args() {
let graph = one_primary_graph();
let mut start: HashMap<usize, ResolvedProfileFlags> = HashMap::new();
start.insert(
0,
ResolvedProfileFlags {
extra_compile_args: vec!["-fPIC".into()],
..Default::default()
},
);
let env = env_from(&[("CPPFLAGS", "-DFOO=1 -DBAR")]);
let (out, _) = augment_build_flags_with_env(&graph, start, env, quiet_reporter()).unwrap();
let entry = out.get(&0).unwrap();
assert_eq!(
entry.extra_compile_args,
vec!["-fPIC", "-DFOO=1", "-DBAR"],
"CPPFLAGS append *after* existing language-neutral args"
);
assert!(entry.cflags.is_empty());
assert!(entry.cxxflags.is_empty());
assert!(entry.ldflags.is_empty());
}
#[test]
fn cflags_only_reach_c_bucket() {
let graph = one_primary_graph();
let start: HashMap<usize, ResolvedProfileFlags> =
HashMap::from_iter([(0, ResolvedProfileFlags::default())]);
let env = env_from(&[("CFLAGS", "-std=c11 -Wmissing-prototypes")]);
let (out, _) = augment_build_flags_with_env(&graph, start, env, quiet_reporter()).unwrap();
let entry = out.get(&0).unwrap();
assert!(entry.extra_compile_args.is_empty());
assert_eq!(entry.cflags, vec!["-std=c11", "-Wmissing-prototypes"],);
assert!(entry.cxxflags.is_empty());
}
#[test]
fn cxxflags_only_reach_cxx_bucket() {
let graph = one_primary_graph();
let start: HashMap<usize, ResolvedProfileFlags> =
HashMap::from_iter([(0, ResolvedProfileFlags::default())]);
let env = env_from(&[("CXXFLAGS", "-fno-rtti")]);
let (out, _) = augment_build_flags_with_env(&graph, start, env, quiet_reporter()).unwrap();
let entry = out.get(&0).unwrap();
assert!(entry.cflags.is_empty());
assert_eq!(entry.cxxflags, vec!["-fno-rtti".to_owned()]);
}
#[test]
fn ldflags_only_reach_link_bucket() {
let graph = one_primary_graph();
let start: HashMap<usize, ResolvedProfileFlags> = HashMap::from_iter([(
0,
ResolvedProfileFlags {
ldflags: vec!["-Wl,--as-needed".into()],
..Default::default()
},
)]);
let env = env_from(&[("LDFLAGS", "-L/opt/lib -lfoo")]);
let (out, _) = augment_build_flags_with_env(&graph, start, env, quiet_reporter()).unwrap();
let entry = out.get(&0).unwrap();
assert_eq!(
entry.ldflags,
vec!["-Wl,--as-needed", "-L/opt/lib", "-lfoo"],
);
assert!(entry.extra_compile_args.is_empty());
assert!(entry.cflags.is_empty());
assert!(entry.cxxflags.is_empty());
}
#[test]
fn malformed_quote_error_names_variable() {
let graph = one_primary_graph();
let start: HashMap<usize, ResolvedProfileFlags> = HashMap::new();
let env = env_from(&[("CFLAGS", "-DFOO='hello")]);
let err = augment_build_flags_with_env(&graph, start, env, quiet_reporter()).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("CFLAGS"), "{msg}");
assert!(msg.contains("shell"), "{msg}");
}
#[test]
fn env_flag_append_preserves_pkg_config_order() {
let graph = one_primary_graph();
let start: HashMap<usize, ResolvedProfileFlags> = HashMap::from_iter([(
0,
ResolvedProfileFlags {
extra_compile_args: vec!["-pthread".into()],
ldflags: vec!["-L/usr/local/lib".into(), "-lssl".into()],
..Default::default()
},
)]);
let env = env_from(&[
("CPPFLAGS", "-DENV_CPP=1"),
("LDFLAGS", "-L/opt/lib -lextra"),
]);
let (out, _) = augment_build_flags_with_env(&graph, start, env, quiet_reporter()).unwrap();
let entry = out.get(&0).unwrap();
assert_eq!(entry.extra_compile_args, vec!["-pthread", "-DENV_CPP=1"],);
assert_eq!(
entry.ldflags,
vec!["-L/usr/local/lib", "-lssl", "-L/opt/lib", "-lextra"],
);
}
#[test]
fn merge_touches_every_primary_package() {
let mut graph = one_primary_graph();
graph.packages.push(make_pkg("worker"));
graph.primary_packages = vec![0, 1];
let start: HashMap<usize, ResolvedProfileFlags> = HashMap::new();
let env = env_from(&[("CFLAGS", "-DSHARED")]);
let (out, _) = augment_build_flags_with_env(&graph, start, env, quiet_reporter()).unwrap();
for idx in [0, 1] {
let e = out.get(&idx).unwrap();
assert_eq!(e.cflags, vec!["-DSHARED".to_owned()]);
}
}
}