use super::*;
use std::path::PathBuf;
fn fake_pkg_config_path() -> PathBuf {
let test_exe = std::env::current_exe().expect("current_exe");
let mut dir = test_exe
.parent()
.expect("test exe should live in a directory")
.to_path_buf();
if dir.file_name().and_then(|n| n.to_str()) == Some("deps") {
dir.pop();
}
let candidate = dir.join(format!(
"cabin-system-deps-fake-pkg-config{}",
std::env::consts::EXE_SUFFIX
));
assert!(
candidate.is_file(),
"expected fake pkg-config at {}; build cabin-system-deps with `--features test-fake-pkg-config`",
candidate.display()
);
candidate
}
pub(super) struct Fixtures {
dir: TempDir,
}
impl Fixtures {
pub(super) fn new() -> Self {
Self {
dir: TempDir::new().expect("tempdir"),
}
}
pub(super) fn write(&self, name: &str, body: &str) {
assert_fs::fixture::ChildPath::new(self.dir.path().join(format!("{name}.json")))
.write_str(body)
.unwrap();
}
pub(super) fn path(&self) -> &Path {
self.dir.path()
}
}
pub(super) fn cabin_with_fake_pkg_config(fixtures: &Fixtures) -> Command {
let mut cmd = cabin();
cmd.env("CABIN_PKG_CONFIG", fake_pkg_config_path());
cmd.env("CABIN_FAKE_PKG_CONFIG_FIXTURES", fixtures.path());
cmd
}
fn manifest_with_system_dep(version: &str, required_clause: &str) -> String {
format!(
"[package]\nname = \"hello\"\nversion = \"0.1.0\"\n\n[target.hello]\ntype = \"executable\"\nsources = [\"src/main.cc\"]\n\n[dependencies]\nzlib = {{ version = \"{version}\", system = true{required_clause} }}\n",
)
}
fn write_hello_main(root: &Path) {
assert_fs::fixture::ChildPath::new(root.join("src/main.cc"))
.write_str(HELLO_MAIN_CC)
.unwrap();
}
#[test]
fn build_succeeds_with_no_system_deps_even_when_pkg_config_missing() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml").write_str(VALID_MANIFEST).unwrap();
write_hello_main(dir.path());
let mut cmd = cabin();
cmd.env("CABIN_PKG_CONFIG", dir.path().join("missing-pkg-config"));
cmd.current_dir(dir.path())
.arg("metadata")
.assert()
.success();
}
#[test]
fn metadata_reflects_pkg_config_cflags_in_build_flags_per_package() {
let fixtures = Fixtures::new();
fixtures.write(
"zlib",
r#"{
"version": "1.2.13",
"cflags": "-I/opt/zlib/include -DZLIB_CONST",
"libs": "-L/opt/zlib/lib -lz"
}"#,
);
let dir = TempDir::new().unwrap();
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&manifest_with_system_dep("", ""))
.unwrap();
write_hello_main(dir.path());
let assertion = cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("metadata")
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let view: serde_json::Value =
serde_json::from_str(&stdout).expect("metadata output should be JSON");
let pkg = package_build_flags(&view);
let includes: Vec<String> = pkg["include_dirs"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_owned())
.collect();
assert!(
includes.iter().any(|p| p == "/opt/zlib/include"),
"include dirs must reflect pkg-config -I path: {includes:?}",
);
let extra_compile: Vec<String> = pkg["extra_compile_args"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_owned())
.collect();
assert!(
extra_compile.contains(&"-DZLIB_CONST".to_owned()),
"extra compile args must carry non-include cflags: {extra_compile:?}",
);
let extra_link: Vec<String> = pkg["ldflags"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_owned())
.collect();
assert_eq!(
extra_link,
vec!["-L/opt/zlib/lib".to_owned(), "-lz".to_owned()],
"pkg-config --libs must reach the planner verbatim and in order",
);
}
fn package_build_flags(view: &serde_json::Value) -> &serde_json::Value {
let per_package = view["toolchain"]["build_flags_per_package"]
.as_object()
.expect("toolchain.build_flags_per_package object");
per_package
.values()
.next()
.expect("at least one package with build flags")
}
#[test]
fn metadata_fails_when_system_dep_is_missing() {
let fixtures = Fixtures::new();
let dir = TempDir::new().unwrap();
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&manifest_with_system_dep("", ""))
.unwrap();
write_hello_main(dir.path());
let assertion = cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("metadata")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string();
assert!(
stderr.contains("zlib"),
"diagnostic should name the missing dep: {stderr}",
);
assert!(
stderr.contains("not found"),
"diagnostic should describe the failure mode: {stderr}",
);
}
#[test]
fn metadata_fails_when_system_dep_version_unsatisfied() {
let fixtures = Fixtures::new();
fixtures.write(
"zlib",
r#"{
"version": "1.1.0",
"cflags": "",
"libs": "-lz"
}"#,
);
let dir = TempDir::new().unwrap();
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&manifest_with_system_dep(">=2", ""))
.unwrap();
write_hello_main(dir.path());
let assertion = cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("metadata")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string();
assert!(
stderr.contains("zlib"),
"diagnostic should name the dep: {stderr}",
);
assert!(
stderr.contains(">=2"),
"diagnostic should quote the requirement: {stderr}",
);
assert!(
stderr.contains("1.1.0"),
"diagnostic should report the installed version: {stderr}",
);
}
#[test]
fn metadata_fails_when_pkg_config_missing_and_system_dep_declared() {
let fixtures = Fixtures::new();
let dir = TempDir::new().unwrap();
let missing_pkg_config = dir.path().join("nope-pkg-config");
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&manifest_with_system_dep("", ""))
.unwrap();
write_hello_main(dir.path());
let mut cmd = cabin();
cmd.env("CABIN_PKG_CONFIG", &missing_pkg_config);
cmd.env("CABIN_FAKE_PKG_CONFIG_FIXTURES", fixtures.path());
let assertion = cmd
.current_dir(dir.path())
.arg("metadata")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string();
assert!(
stderr.contains("not found"),
"diagnostic should mention `not found`: {stderr}",
);
assert!(
stderr.contains("CABIN_PKG_CONFIG"),
"diagnostic should mention the override env var: {stderr}",
);
}
#[test]
fn cabin_pkg_config_env_var_overrides_executable() {
let fixtures = Fixtures::new();
fixtures.write(
"zlib",
r#"{
"version": "1.2.13",
"cflags": "-I/opt/zlib/include",
"libs": "-lz"
}"#,
);
let dir = TempDir::new().unwrap();
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&manifest_with_system_dep("", ""))
.unwrap();
write_hello_main(dir.path());
cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("metadata")
.assert()
.success();
}
#[test]
fn manifest_rejects_required_field_on_system_dep() {
let dir = TempDir::new().unwrap();
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&manifest_with_system_dep(">=1", ", required = false"))
.unwrap();
write_hello_main(dir.path());
let assertion = cabin()
.current_dir(dir.path())
.arg("metadata")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string();
assert!(
stderr.contains("unknown field `required`"),
"diagnostic should call out the unknown field by name: {stderr}",
);
}
#[test]
fn build_compile_commands_carry_include_paths_from_pkg_config() {
require_cxx_build_tools();
let fixtures = Fixtures::new();
fixtures.write(
"zlib",
r#"{
"version": "1.2.13",
"cflags": "-I/opt/zlib/include -DZLIB_CONST",
"libs": "-L/opt/zlib/lib -lz"
}"#,
);
let dir = TempDir::new().unwrap();
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&manifest_with_system_dep("", ""))
.unwrap();
write_hello_main(dir.path());
if cfg!(windows) {
let assertion = cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("build")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string();
assert!(
stderr.contains("not supported with an MSVC toolchain"),
"MSVC build must reject system dependencies with a clear diagnostic: {stderr}",
);
return;
}
cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("build")
.assert()
.success();
let ccdb_path = dir.path().join("build/dev/compile_commands.json");
let ccdb = std::fs::read_to_string(&ccdb_path).expect("compile_commands.json");
assert!(
ccdb.contains("-I ") && ccdb.contains("opt/zlib/include"),
"compile_commands.json must carry the pkg-config include: {ccdb}",
);
assert!(
ccdb.contains("-DZLIB_CONST"),
"compile_commands.json must carry pkg-config -D: {ccdb}",
);
let ninja_path = dir.path().join("build/dev/build.ninja");
let ninja = std::fs::read_to_string(&ninja_path).expect("build.ninja");
assert!(
ninja.contains("-lz"),
"build.ninja link command must carry pkg-config -l: {ninja}",
);
assert!(
ninja.contains("-L/opt/zlib/lib"),
"build.ninja link command must carry pkg-config -L: {ninja}",
);
}
#[test]
fn fingerprint_moves_when_pkg_config_flags_change() {
let fixtures = Fixtures::new();
fixtures.write(
"zlib",
r#"{
"version": "1.2.13",
"cflags": "-I/opt/zlib/include",
"libs": "-lz"
}"#,
);
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str("[package]\nname = \"hello\"\nversion = \"0.1.0\"\n\n[target.hello]\ntype = \"executable\"\nsources = [\"src/main.cc\"]\n\n[features]\ndefault = []\nflag-a = []\n\n[dependencies]\nzlib = { version = \"\", system = true }\n")
.unwrap();
write_hello_main(dir.path());
let stdout1 = String::from_utf8_lossy(
&cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("metadata")
.assert()
.success()
.get_output()
.stdout,
)
.to_string();
let view1: serde_json::Value = serde_json::from_str(&stdout1).unwrap();
let fp1 = find_fingerprint(&view1);
fixtures.write(
"zlib",
r#"{
"version": "1.2.13",
"cflags": "-I/opt/zlib/include",
"libs": "-lz -lother"
}"#,
);
let stdout2 = String::from_utf8_lossy(
&cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("metadata")
.assert()
.success()
.get_output()
.stdout,
)
.to_string();
let view2: serde_json::Value = serde_json::from_str(&stdout2).unwrap();
let fp2 = find_fingerprint(&view2);
assert_ne!(
fp1, fp2,
"fingerprint must move when discovered pkg-config flags change",
);
}
fn find_fingerprint(value: &serde_json::Value) -> String {
fn walk(v: &serde_json::Value) -> Option<String> {
if let Some(map) = v.as_object() {
if let Some(fp) = map.get("fingerprint").and_then(|f| f.as_str()) {
return Some(fp.to_owned());
}
for child in map.values() {
if let Some(found) = walk(child) {
return Some(found);
}
}
}
if let Some(arr) = v.as_array() {
for item in arr {
if let Some(found) = walk(item) {
return Some(found);
}
}
}
None
}
walk(value).expect("metadata view should expose a fingerprint")
}
#[test]
fn non_matching_target_conditional_system_dep_does_not_require_pkg_config() {
let dir = TempDir::new().unwrap();
let unreachable = dir.path().join("never-reached-pkg-config");
dir.child("cabin.toml")
.write_str("[package]\nname = \"hello\"\nversion = \"0.1.0\"\n\n[target.hello]\ntype = \"executable\"\nsources = [\"src/main.cc\"]\n\n[target.'cfg(os = \"none-such\")'.dependencies]\nzlib = { version = \"\", system = true }\n")
.unwrap();
write_hello_main(dir.path());
let mut cmd = cabin();
cmd.env("CABIN_PKG_CONFIG", &unreachable);
cmd.current_dir(dir.path())
.arg("metadata")
.assert()
.success();
}
#[test]
fn matching_target_conditional_system_dep_is_probed() {
let fixtures = Fixtures::new();
fixtures.write(
"zlib",
r#"{
"version": "1.2.13",
"cflags": "-I/opt/zlib/include",
"libs": "-lz"
}"#,
);
let dir = TempDir::new().unwrap();
let host_os = if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else {
"linux"
};
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&format!(
"[package]\nname = \"hello\"\nversion = \"0.1.0\"\n\n[target.hello]\ntype = \"executable\"\nsources = [\"src/main.cc\"]\n\n[target.'cfg(os = \"{host_os}\")'.dependencies]\nzlib = {{ version = \"\", system = true }}\n",
))
.unwrap();
write_hello_main(dir.path());
let assertion = cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("metadata")
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let view: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let pkg = package_build_flags(&view);
let includes: Vec<String> = pkg["include_dirs"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_owned())
.collect();
assert!(
includes.iter().any(|p| p == "/opt/zlib/include"),
"matching conditional system dep must contribute flags: {includes:?}",
);
}
#[test]
fn verbose_mode_prints_probe_progress() {
let fixtures = Fixtures::new();
fixtures.write(
"zlib",
r#"{
"version": "1.2.13",
"cflags": "",
"libs": "-lz"
}"#,
);
let dir = TempDir::new().unwrap();
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&manifest_with_system_dep("", ""))
.unwrap();
write_hello_main(dir.path());
let assertion = cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("-v")
.arg("metadata")
.assert()
.success();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string();
assert!(
stderr.contains("probing"),
"verbose stderr should mention probing: {stderr}",
);
assert!(
stderr.contains("zlib"),
"verbose stderr should mention the dep name: {stderr}",
);
assert!(
stderr.contains("1.2.13"),
"verbose stderr should mention the resolved version: {stderr}",
);
}
#[test]
fn metadata_stdout_stays_clean_under_verbose_with_system_deps() {
let fixtures = Fixtures::new();
fixtures.write(
"zlib",
r#"{
"version": "1.2.13",
"cflags": "",
"libs": "-lz"
}"#,
);
let dir = TempDir::new().unwrap();
assert_fs::fixture::ChildPath::new(dir.path().join("cabin.toml"))
.write_str(&manifest_with_system_dep("", ""))
.unwrap();
write_hello_main(dir.path());
let assertion = cabin_with_fake_pkg_config(&fixtures)
.current_dir(dir.path())
.arg("-v")
.arg("metadata")
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let _view: serde_json::Value =
serde_json::from_str(&stdout).expect("metadata stdout must remain valid JSON under -v");
}