use crate::analyze::AnalyzedError;
use crate::buildsystem::{BuildSystem, DependencyCategory, Error};
use crate::dependencies::pkgconfig::{PkgComparison, PkgConstraint};
use crate::dependencies::vague::VagueDependency;
use crate::dependencies::PkgConfigDependency;
use crate::dependency::Dependency;
use crate::dist_catcher::DistCatcher;
use crate::fix_build::BuildFixer;
use crate::installer::Error as InstallerError;
use crate::session::Session;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct Meson {
#[allow(dead_code)]
path: PathBuf,
}
#[derive(Debug, Default, PartialEq, Eq)]
enum MaybeUnknown<T> {
#[default]
Unknown,
Known(T),
}
impl<T> MaybeUnknown<T> {
fn known(self) -> Option<T> {
match self {
MaybeUnknown::Known(v) => Some(v),
MaybeUnknown::Unknown => None,
}
}
}
impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for MaybeUnknown<T> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
if value.as_str() == Some("unknown") {
return Ok(MaybeUnknown::Unknown);
}
T::deserialize(value)
.map(MaybeUnknown::Known)
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
struct MesonDependency {
pub name: MaybeUnknown<String>,
#[serde(default, deserialize_with = "version_as_vec")]
pub version: MaybeUnknown<Vec<String>>,
#[serde(default)]
pub required: MaybeUnknown<bool>,
#[serde(default)]
pub has_fallback: bool,
#[serde(default)]
pub conditional: bool,
}
fn version_as_vec<'de, D>(deserializer: D) -> Result<MaybeUnknown<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
String(String),
Vec(Vec<String>),
}
Ok(
match MaybeUnknown::<StringOrVec>::deserialize(deserializer)? {
MaybeUnknown::Unknown => MaybeUnknown::Unknown,
MaybeUnknown::Known(StringOrVec::String(s)) => {
MaybeUnknown::Known(if s.is_empty() { vec![] } else { vec![s] })
}
MaybeUnknown::Known(StringOrVec::Vec(v)) => MaybeUnknown::Known(v),
},
)
}
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
struct MesonTarget {
r#type: String,
installed: bool,
filename: Vec<PathBuf>,
}
fn meson_dependency(name: &str, versions: Option<&[String]>) -> Box<dyn Dependency> {
let mut constraint = None;
match versions {
Some([text]) => match text.parse::<PkgConstraint>() {
Ok(parsed) => constraint = Some(parsed),
Err(e) => log::warn!("Unable to parse version constraint {:?}: {}", text, e),
},
Some(versions) if versions.len() > 1 => {
log::warn!("Ignoring version constraints on {}: {:?}", name, versions);
}
Some(_) => {}
None => {
log::debug!("Version constraint for {} is not statically known", name);
}
}
match constraint {
Some(constraint) if constraint.comparison != PkgComparison::GreaterEqual => {
Box::new(PkgConfigDependency::new_with_constraint(name, constraint))
}
constraint => Box::new(VagueDependency {
name: name.to_string(),
minimum_version: constraint.map(|c| c.version.to_string()),
}),
}
}
impl Meson {
pub fn new(path: &Path) -> Self {
Self {
path: path.to_path_buf(),
}
}
fn setup(&self, session: &dyn Session) -> Result<(), Error> {
let project_dir = self
.path
.parent()
.expect("meson.build should have a parent directory");
let build_dir = project_dir.join("build");
if !session.exists(&build_dir) {
session.mkdir(&build_dir).unwrap();
}
session
.command(vec!["meson", "setup", "build"])
.cwd(project_dir)
.quiet(true)
.run_detecting_problems()?;
Ok(())
}
fn introspect<T: for<'a> serde::Deserialize<'a>>(
&self,
session: &dyn Session,
fixers: Option<&[&dyn BuildFixer<InstallerError>]>,
args: &[&str],
) -> Result<T, InstallerError> {
let project_dir = self
.path
.parent()
.expect("meson.build should have a parent directory");
let build_dir = project_dir.join("build");
let use_build_dir =
session.exists(&build_dir) && session.exists(&build_dir.join("build.ninja"));
let ret = if use_build_dir {
let build_dir_str = build_dir.to_string_lossy();
let introspect_args = [&["meson", "introspect", &build_dir_str], args].concat();
if let Some(fixers) = fixers {
session
.command(introspect_args)
.cwd(project_dir)
.quiet(true)
.run_fixing_problems::<_, Error>(fixers)
.unwrap()
} else {
session
.command(introspect_args)
.cwd(project_dir)
.quiet(true)
.run_detecting_problems()?
}
} else {
self.setup_temp_build_for_introspect(session, fixers, args)?
};
let text = ret.concat();
Ok(serde_json::from_str(&text).unwrap())
}
fn setup_temp_build_for_introspect(
&self,
session: &dyn Session,
fixers: Option<&[&dyn BuildFixer<InstallerError>]>,
args: &[&str],
) -> Result<Vec<String>, InstallerError> {
let project_dir = self
.path
.parent()
.expect("meson.build should have a parent directory");
let temp_build_dir = project_dir.join(".ognibuild-temp-build");
if session.exists(&temp_build_dir) {
session.rmtree(&temp_build_dir).ok();
}
session.mkdir(&temp_build_dir).map_err(|e| {
InstallerError::Other(format!("Failed to create temp build dir: {}", e))
})?;
let temp_build_str = temp_build_dir.to_string_lossy();
let setup_result = session
.command(vec!["meson", "setup", &temp_build_str])
.cwd(project_dir)
.quiet(true)
.run_detecting_problems();
match setup_result {
Ok(_) => {
let temp_build_str = temp_build_dir.to_string_lossy();
let introspect_args = [&["meson", "introspect", &temp_build_str], args].concat();
let result = if let Some(fixers) = fixers {
session
.command(introspect_args)
.cwd(project_dir)
.quiet(true)
.run_fixing_problems::<_, Error>(fixers)
.unwrap()
} else {
session
.command(introspect_args)
.cwd(project_dir)
.quiet(true)
.run_detecting_problems()?
};
session.rmtree(&temp_build_dir).ok();
Ok(result)
}
Err(e) => {
session.rmtree(&temp_build_dir).ok();
Err(e.into())
}
}
}
pub fn probe(path: &Path) -> Option<Box<dyn BuildSystem>> {
let path = path.join("meson.build");
if path.exists() {
log::debug!("Found meson.build, assuming meson package.");
Some(Box::new(Self::new(&path)))
} else {
None
}
}
}
impl BuildSystem for Meson {
fn name(&self) -> &str {
"meson"
}
fn dist(
&self,
session: &dyn Session,
_installer: &dyn crate::installer::Installer,
target_directory: &Path,
_quiet: bool,
) -> Result<std::ffi::OsString, Error> {
self.setup(session)?;
let project_dir = self
.path
.parent()
.expect("meson.build should have a parent directory");
let dc = DistCatcher::new(vec![
session.external_path(&project_dir.join("build/meson-dist"))
]);
match session
.command(vec!["ninja", "-C", "build", "dist"])
.cwd(project_dir)
.quiet(true)
.run_detecting_problems()
{
Ok(_) => {}
Err(AnalyzedError::Unidentified { lines, .. })
if lines.contains(
&"ninja: error: unknown target 'dist', did you mean 'dino'?".to_string(),
) =>
{
unimplemented!();
}
Err(e) => return Err(e.into()),
}
Ok(dc.copy_single(target_directory).unwrap().unwrap())
}
fn test(
&self,
session: &dyn Session,
_installer: &dyn crate::installer::Installer,
) -> Result<(), Error> {
self.setup(session)?;
let project_dir = self
.path
.parent()
.expect("meson.build should have a parent directory");
session
.command(vec!["ninja", "-C", "build", "test"])
.cwd(project_dir)
.quiet(true)
.run_detecting_problems()?;
Ok(())
}
fn build(
&self,
session: &dyn Session,
_installer: &dyn crate::installer::Installer,
) -> Result<(), Error> {
self.setup(session)?;
let project_dir = self
.path
.parent()
.expect("meson.build should have a parent directory");
session
.command(vec!["ninja", "-C", "build"])
.cwd(project_dir)
.quiet(true)
.run_detecting_problems()?;
Ok(())
}
fn clean(
&self,
session: &dyn Session,
_installer: &dyn crate::installer::Installer,
) -> Result<(), Error> {
self.setup(session)?;
let project_dir = self
.path
.parent()
.expect("meson.build should have a parent directory");
session
.command(vec!["ninja", "-C", "build", "clean"])
.cwd(project_dir)
.quiet(true)
.run_detecting_problems()?;
Ok(())
}
fn install(
&self,
session: &dyn Session,
_installer: &dyn crate::installer::Installer,
_install_target: &crate::buildsystem::InstallTarget,
) -> Result<(), Error> {
self.setup(session)?;
let project_dir = self
.path
.parent()
.expect("meson.build should have a parent directory");
session
.command(vec!["ninja", "-C", "build", "install"])
.cwd(project_dir)
.quiet(true)
.run_detecting_problems()?;
Ok(())
}
fn get_declared_dependencies(
&self,
session: &dyn Session,
fixers: Option<&[&dyn crate::fix_build::BuildFixer<crate::installer::Error>]>,
) -> Result<Vec<(crate::buildsystem::DependencyCategory, Box<dyn Dependency>)>, Error> {
let mut ret: Vec<(DependencyCategory, Box<dyn Dependency>)> = Vec::new();
let project_dir = self
.path
.parent()
.expect("meson.build should have a parent directory");
let meson_file_str = self.path.to_string_lossy();
let scan_args = vec![
"meson",
"introspect",
"--scan-dependencies",
&meson_file_str,
];
let output = if let Some(fixers) = fixers {
session
.command(scan_args)
.cwd(project_dir)
.quiet(true)
.run_fixing_problems::<_, Error>(fixers)
.map_err(|e| {
InstallerError::Other(format!("Failed to run scan-dependencies: {:?}", e))
})?
} else {
session
.command(scan_args)
.cwd(project_dir)
.quiet(true)
.run_detecting_problems()
.map_err(|e| {
InstallerError::Other(format!("Failed to run scan-dependencies: {:?}", e))
})?
};
let text = output.concat();
let resp: Vec<MesonDependency> = serde_json::from_str(&text).map_err(|e| {
InstallerError::Other(format!("Failed to parse scan-dependencies JSON: {}", e))
})?;
for entry in resp {
let Some(name) = entry.name.known() else {
log::warn!("Ignoring dependency with a name meson could not determine statically");
continue;
};
ret.push((
DependencyCategory::Universal,
meson_dependency(&name, entry.version.known().as_deref()),
));
}
Ok(ret)
}
fn get_declared_outputs(
&self,
session: &dyn Session,
fixers: Option<&[&dyn crate::fix_build::BuildFixer<crate::installer::Error>]>,
) -> Result<Vec<Box<dyn crate::output::Output>>, Error> {
let mut ret: Vec<Box<dyn crate::output::Output>> = Vec::new();
let resp = self.introspect::<Vec<MesonTarget>>(session, fixers, &["--targets"])?;
for entry in resp {
if !entry.installed {
continue;
}
if entry.r#type == "executable" {
for p in entry.filename {
ret.push(Box::new(crate::output::BinaryOutput::new(
p.file_name().unwrap().to_str().unwrap(),
)));
}
}
}
Ok(ret)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buildsystem::detect_buildsystems;
use crate::installer::NullInstaller;
use crate::session::plain::PlainSession;
use std::fs;
use tempfile::TempDir;
const SCAN_DEPENDENCIES_WITH_UNKNOWNS: &str = r#"[
{"name": "glib-2.0", "required": "unknown", "version": [">=2.50"],
"has_fallback": false, "conditional": false},
{"name": "zlib", "required": true, "version": "unknown",
"has_fallback": false, "conditional": false},
{"name": "unknown", "required": true, "version": [],
"has_fallback": false, "conditional": false},
{"name": "m", "required": false, "version": [">=1.0", "<2.0"],
"has_fallback": false, "conditional": false},
{"name": "libcurl", "required": true, "version": [],
"has_fallback": true, "conditional": true}
]"#;
#[test]
fn test_parse_scan_dependencies_with_unknowns() {
let deps: Vec<MesonDependency> =
serde_json::from_str(SCAN_DEPENDENCIES_WITH_UNKNOWNS).unwrap();
assert_eq!(5, deps.len());
assert_eq!(MaybeUnknown::Known("glib-2.0".to_string()), deps[0].name);
assert_eq!(MaybeUnknown::Unknown, deps[0].required);
assert_eq!(
MaybeUnknown::Known(vec![">=2.50".to_string()]),
deps[0].version
);
assert_eq!(MaybeUnknown::Unknown, deps[1].version);
assert_eq!(MaybeUnknown::Known(true), deps[1].required);
assert_eq!(MaybeUnknown::Unknown, deps[2].name);
assert_eq!(MaybeUnknown::Known("m".to_string()), deps[3].name);
assert_eq!(MaybeUnknown::Known(false), deps[3].required);
assert_eq!(
MaybeUnknown::Known(vec![">=1.0".to_string(), "<2.0".to_string()]),
deps[3].version
);
assert_eq!(MaybeUnknown::Known(vec![]), deps[4].version);
assert!(deps[4].has_fallback);
assert!(deps[4].conditional);
}
#[test]
fn test_parse_scan_dependencies_pre_meson_1_9() {
let text = r#"[
{"name": "glib-2.0", "required": false, "version": [">=2.50"],
"has_fallback": false, "conditional": false}
]"#;
let deps: Vec<MesonDependency> = serde_json::from_str(text).unwrap();
assert_eq!(1, deps.len());
assert_eq!(MaybeUnknown::Known("glib-2.0".to_string()), deps[0].name);
assert_eq!(MaybeUnknown::Known(false), deps[0].required);
assert_eq!(
MaybeUnknown::Known(vec![">=2.50".to_string()]),
deps[0].version
);
}
fn vague(dep: &dyn Dependency) -> &VagueDependency {
dep.as_any()
.downcast_ref::<VagueDependency>()
.expect("expected a VagueDependency")
}
fn pkg_config(dep: &dyn Dependency) -> &PkgConfigDependency {
dep.as_any()
.downcast_ref::<PkgConfigDependency>()
.expect("expected a PkgConfigDependency")
}
#[test]
fn test_meson_dependency_vague() {
let dep = meson_dependency("glib-2.0", Some(&[">=2.50".to_string()]));
assert_eq!("glib-2.0", vague(dep.as_ref()).name);
assert_eq!(Some("2.50"), vague(dep.as_ref()).minimum_version.as_deref());
let dep = meson_dependency("glib-2.0", Some(&[]));
assert_eq!(None, vague(dep.as_ref()).minimum_version);
let dep = meson_dependency("glib-2.0", None);
assert_eq!(None, vague(dep.as_ref()).minimum_version);
}
#[test]
fn test_meson_dependency_pkg_config() {
for (text, expected) in [
("<2.0", PkgComparison::Less),
("<=2.0", PkgComparison::LessEqual),
("=1.4", PkgComparison::Equal),
("!=1.4", PkgComparison::NotEqual),
(">1.4", PkgComparison::Greater),
] {
let dep = meson_dependency("glib-2.0", Some(&[text.to_string()]));
let dep = pkg_config(dep.as_ref());
assert_eq!("glib-2.0", dep.module());
assert_eq!(
Some(expected),
dep.constraint().map(|c| c.comparison),
"for {text}"
);
}
}
#[test]
fn test_meson_dependency_bad_constraint() {
let dep = meson_dependency("glib-2.0", Some(&["=> 2.50".to_string()]));
assert_eq!(None, vague(dep.as_ref()).minimum_version);
}
#[test]
fn test_meson_dependency_multiple_constraints() {
let dep = meson_dependency("glib-2.0", Some(&[">=1.0".to_string(), "<2.0".to_string()]));
assert_eq!(None, vague(dep.as_ref()).minimum_version);
}
fn create_meson_project(dir: &Path) -> std::io::Result<()> {
fs::write(
dir.join("meson.build"),
r#"project('test-project', 'c',
version : '1.0.0',
license : 'MIT',
default_options : ['warning_level=2'])
# A simple dependency for testing
glib_dep = dependency('glib-2.0', required: false)
# Define a simple executable
executable('test-app',
'main.c',
dependencies : glib_dep,
install : true)
"#,
)?;
fs::write(
dir.join("main.c"),
r#"#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Hello from Meson test!\n");
return 0;
}
"#,
)?;
Ok(())
}
#[test]
fn test_meson_detection() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let buildsystems = detect_buildsystems(project_dir);
assert!(
!buildsystems.iter().any(|bs| bs.name() == "meson"),
"Should not detect Meson without meson.build"
);
create_meson_project(project_dir).unwrap();
let buildsystems = detect_buildsystems(project_dir);
assert!(
buildsystems.iter().any(|bs| bs.name() == "meson"),
"Should detect Meson with meson.build"
);
if !buildsystems.is_empty() {
let first = &buildsystems[0];
assert_eq!(
first.name(),
"meson",
"Meson should be the primary build system"
);
}
}
#[test]
fn test_meson_introspect_with_different_cwd() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("my-project");
fs::create_dir(&project_dir).unwrap();
create_meson_project(&project_dir).unwrap();
let mut session = PlainSession::new();
session.chdir(temp_dir.path()).unwrap();
let buildsystems = detect_buildsystems(&project_dir);
assert!(!buildsystems.is_empty(), "Should detect Meson buildsystem");
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
match meson.get_declared_dependencies(&session, None) {
Ok(deps) => {
let _has_glib = deps
.iter()
.any(|(_, dep)| dep.family() == "glib" || dep.family() == "pkg-config");
log::debug!("Found {} dependencies", deps.len());
if !deps.is_empty() {
log::debug!("Dependencies: {:?}", deps);
}
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("Missing Meson file"),
"Should not fail with 'Missing Meson file' error: {}",
error_str
);
assert!(
!error_str.contains("./meson.build"),
"Should not reference relative path './meson.build': {}",
error_str
);
}
}
}
#[test]
fn test_meson_with_nested_project_structure() {
let temp_dir = TempDir::new().unwrap();
let workspace_dir = temp_dir.path().join("workspace");
let project_dir = workspace_dir.join("subdir").join("project");
fs::create_dir_all(&project_dir).unwrap();
create_meson_project(&project_dir).unwrap();
let mut session = PlainSession::new();
session.chdir(&workspace_dir).unwrap();
let buildsystems = detect_buildsystems(&project_dir);
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
match meson.get_declared_dependencies(&session, None) {
Ok(_) => {
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("Missing Meson file")
&& !error_str.contains("./meson.build"),
"Path handling error: {}",
error_str
);
}
}
}
#[test]
#[ignore] fn test_meson_build_operations() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("build-test");
fs::create_dir(&project_dir).unwrap();
create_meson_project(&project_dir).unwrap();
let mut session = PlainSession::new();
session.chdir(temp_dir.path()).unwrap();
let buildsystems = detect_buildsystems(&project_dir);
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
let installer = NullInstaller;
match meson.build(&session, &installer) {
Ok(_) => log::debug!("Build succeeded"),
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("./meson.build"),
"Should not have path errors: {}",
error_str
);
}
}
match meson.clean(&session, &installer) {
Ok(_) => log::debug!("Clean succeeded"),
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("./meson.build"),
"Should not have path errors: {}",
error_str
);
}
}
}
#[test]
fn test_meson_project_with_subprojects() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
fs::write(
project_dir.join("meson.build"),
r#"project('main-project', 'c',
version : '2.0.0',
license : 'GPL-3.0')
# Subproject (would normally be in subprojects/ dir)
# This tests that we handle the main meson.build correctly
executable('main-app', 'main.c')
"#,
)
.unwrap();
fs::write(project_dir.join("main.c"), r#"int main() { return 0; }"#).unwrap();
let buildsystems = detect_buildsystems(project_dir);
assert!(
buildsystems.iter().any(|bs| bs.name() == "meson"),
"Should detect Meson for project with subprojects structure"
);
}
#[test]
fn test_meson_handles_symlinks() {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let real_project = temp_dir.path().join("real-project");
let link_project = temp_dir.path().join("link-project");
fs::create_dir(&real_project).unwrap();
create_meson_project(&real_project).unwrap();
symlink(&real_project, &link_project).unwrap();
let buildsystems = detect_buildsystems(&link_project);
assert!(
buildsystems.iter().any(|bs| bs.name() == "meson"),
"Should detect Meson through symlink"
);
let mut session = PlainSession::new();
session.chdir(temp_dir.path()).unwrap();
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
match meson.get_declared_dependencies(&session, None) {
Ok(_) => {
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("Missing Meson file"),
"Should handle symlinks correctly: {}",
error_str
);
}
}
}
}
#[test]
fn test_meson_introspect_unconfigured_project() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("unconfigured-project");
fs::create_dir(&project_dir).unwrap();
create_meson_project(&project_dir).unwrap();
let mut session = PlainSession::new();
session.chdir(temp_dir.path()).unwrap();
let buildsystems = detect_buildsystems(&project_dir);
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
match meson.get_declared_dependencies(&session, None) {
Ok(deps) => {
log::debug!(
"Successfully introspected unconfigured project with {} dependencies",
deps.len()
);
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("Current directory is not a meson build directory"),
"Should not fail with build directory error for unconfigured projects: {}",
error_str
);
assert!(
!error_str.contains("Please specify a valid build dir"),
"Should not fail asking for build dir when introspecting source: {}",
error_str
);
log::debug!(
"Got acceptable error (likely meson not installed): {}",
error_str
);
}
}
}
#[test]
fn test_meson_introspect_scan_dependencies() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("scan-deps-test");
fs::create_dir(&project_dir).unwrap();
fs::write(
project_dir.join("meson.build"),
r#"project('scan-deps-test', 'c',
version : '1.0.0',
license : 'MIT')
# Test various types of dependencies
glib_dep = dependency('glib-2.0', required: false)
threads_dep = dependency('threads', required: false)
math_dep = declare_dependency(
dependencies: meson.get_compiler('c').find_library('m', required: false)
)
executable('test-app',
'main.c',
dependencies : [glib_dep, threads_dep, math_dep])
"#,
)
.unwrap();
fs::write(
project_dir.join("main.c"),
r#"#include <stdio.h>
#include <math.h>
int main() { printf("Result: %f\n", sqrt(16.0)); return 0; }"#,
)
.unwrap();
let mut session = PlainSession::new();
session.chdir(temp_dir.path()).unwrap();
let buildsystems = detect_buildsystems(&project_dir);
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
match meson.get_declared_dependencies(&session, None) {
Ok(deps) => {
log::debug!("Found {} dependencies", deps.len());
for (category, dep) in &deps {
let min_ver =
if let Some(vague_dep) = dep.as_any().downcast_ref::<VagueDependency>() {
vague_dep.minimum_version.as_deref().unwrap_or("any")
} else {
"any"
};
log::debug!(" {:?}: {} ({})", category, dep.family(), min_ver);
}
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("No command specified"),
"Should not fail with 'No command specified' after fixing scan-dependencies usage: {}",
error_str
);
log::debug!("Got acceptable error: {}", error_str);
}
}
}
#[test]
fn test_meson_introspect_targets() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("targets-test");
fs::create_dir(&project_dir).unwrap();
fs::write(
project_dir.join("meson.build"),
r#"project('targets-test', 'c',
version : '1.0.0')
# Test various target types
executable('main-app',
'main.c',
install : true)
executable('test-util',
'test.c',
install : false)
static_library('helper',
'helper.c',
install : false)
"#,
)
.unwrap();
fs::write(project_dir.join("main.c"), "int main() { return 0; }").unwrap();
fs::write(project_dir.join("test.c"), "int main() { return 1; }").unwrap();
fs::write(project_dir.join("helper.c"), "void helper() {}").unwrap();
let mut session = PlainSession::new();
session.chdir(temp_dir.path()).unwrap();
let buildsystems = detect_buildsystems(&project_dir);
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
match meson.get_declared_outputs(&session, None) {
Ok(outputs) => {
log::debug!("Found {} outputs", outputs.len());
for output in &outputs {
log::debug!(" Output: {:?}", output);
}
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("Missing Meson file")
&& !error_str.contains("./meson.build")
&& !error_str.contains("No command specified"),
"Should not have known path/command errors: {}",
error_str
);
log::debug!("Got acceptable error: {}", error_str);
}
}
}
#[test]
fn test_meson_introspect_complex_project() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("complex-test");
fs::create_dir_all(project_dir.join("src")).unwrap();
fs::create_dir_all(project_dir.join("include")).unwrap();
fs::write(
project_dir.join("meson.build"),
r#"project('complex-test', 'c',
version : '2.1.0',
license : 'GPL-2.0',
default_options : [
'warning_level=3',
'werror=true'
])
# Include directories
inc = include_directories('include')
# Dependencies with version constraints
json_dep = dependency('json-c', version: '>=0.13', required: false)
curl_dep = dependency('libcurl', version: '>=7.60', required: false)
zlib_dep = dependency('zlib', required: false)
# Subdirectory
subdir('src')
# Main executable
executable('complex-app',
sources: ['main.c', src_files],
include_directories: inc,
dependencies: [json_dep, curl_dep, zlib_dep],
install: true)
"#,
)
.unwrap();
fs::write(
project_dir.join("src/meson.build"),
"src_files = files('utils.c', 'parser.c')",
)
.unwrap();
fs::write(
project_dir.join("main.c"),
"#include <stdio.h>\nint main() { printf(\"Complex app\\n\"); return 0; }",
)
.unwrap();
fs::write(project_dir.join("src/utils.c"), "void utils_init() {}").unwrap();
fs::write(project_dir.join("src/parser.c"), "void parse() {}").unwrap();
fs::write(
project_dir.join("include/common.h"),
"#pragma once\nvoid utils_init();",
)
.unwrap();
let mut session = PlainSession::new();
session.chdir(temp_dir.path()).unwrap();
let buildsystems = detect_buildsystems(&project_dir);
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
match meson.get_declared_dependencies(&session, None) {
Ok(deps) => {
log::debug!("Complex project dependencies: {} found", deps.len());
let versioned_deps: Vec<_> = deps
.iter()
.filter(|(_, dep)| {
if let Some(vague_dep) = dep.as_any().downcast_ref::<VagueDependency>() {
vague_dep.minimum_version.is_some()
} else {
false
}
})
.collect();
if !versioned_deps.is_empty() {
log::debug!("Dependencies with version constraints:");
for (cat, dep) in versioned_deps {
let min_ver = if let Some(vague_dep) =
dep.as_any().downcast_ref::<VagueDependency>()
{
vague_dep.minimum_version.as_deref().unwrap_or("any")
} else {
"any"
};
log::debug!(" {:?}: {} >= {}", cat, dep.family(), min_ver);
}
}
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("No command specified")
&& !error_str.contains("Missing Meson file")
&& !error_str.contains("./meson.build"),
"Should not have known errors on complex projects: {}",
error_str
);
log::debug!("Got acceptable error on complex project: {}", error_str);
}
}
match meson.get_declared_outputs(&session, None) {
Ok(outputs) => {
log::debug!("Complex project outputs: {} found", outputs.len());
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("No command specified"),
"Should not have command errors: {}",
error_str
);
}
}
}
#[test]
fn test_meson_setup_command_with_source_and_build_dirs() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("setup-test");
fs::create_dir(&project_dir).unwrap();
create_meson_project(&project_dir).unwrap();
let mut session = PlainSession::new();
session.chdir(temp_dir.path()).unwrap();
let meson = Meson::new(&project_dir.join("meson.build"));
let result = meson.setup_temp_build_for_introspect(&session, None, &["--projectinfo"]);
match result {
Ok(_) => {
log::debug!("Setup with source and build dirs succeeded");
let temp_build = project_dir.join(".ognibuild-temp-build");
assert!(
!session.exists(&temp_build),
"Temporary build directory should be cleaned up"
);
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("No command specified"),
"Setup should not fail with 'No command specified': {}",
error_str
);
log::debug!("Got acceptable setup error: {}", error_str);
}
}
}
#[test]
fn test_meson_commands_work_from_different_cwd_regression() {
let temp_dir = TempDir::new().unwrap();
let workspace = temp_dir.path().join("workspace");
let other_dir = temp_dir.path().join("other_directory");
let project_dir = workspace.join("projects").join("my-meson-project");
fs::create_dir_all(&workspace).unwrap();
fs::create_dir_all(&other_dir).unwrap();
fs::create_dir_all(&project_dir).unwrap();
create_meson_project(&project_dir).unwrap();
let mut session = PlainSession::new();
session.chdir(&other_dir).unwrap();
assert_ne!(session.pwd(), project_dir);
assert_ne!(session.pwd().parent(), Some(project_dir.as_path()));
let buildsystems = detect_buildsystems(&project_dir);
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
match meson.get_declared_dependencies(&session, None) {
Ok(deps) => {
let has_glib = deps
.iter()
.any(|(_, dep)| dep.family() == "glib-2.0" || dep.family().contains("glib"));
assert!(!deps.is_empty() || has_glib, "Should find dependencies");
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("Missing Meson file")
&& !error_str.contains("nor build directory")
&& !error_str.contains("contain a meson.build"),
"BUG REPRODUCED: Meson command failed due to working directory issue: {}",
error_str
);
}
}
match meson.get_declared_outputs(&session, None) {
Ok(_outputs) => {
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("Missing Meson file")
&& !error_str.contains("nor build directory")
&& !error_str.contains("contain a meson.build"),
"BUG REPRODUCED: Meson introspect failed due to working directory issue: {}",
error_str
);
}
}
let installer = NullInstaller;
match meson.build(&session, &installer) {
Ok(_) => {
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("source directory") && !error_str.contains("meson.build"),
"BUG REPRODUCED: Meson setup failed due to working directory issue: {}",
error_str
);
}
}
let actual_pwd = session
.pwd()
.canonicalize()
.unwrap_or_else(|_| session.pwd().to_path_buf());
let expected_pwd = other_dir
.canonicalize()
.unwrap_or_else(|_| other_dir.clone());
assert_eq!(
actual_pwd, expected_pwd,
"Session cwd should not have changed"
);
}
#[test]
fn test_meson_error_handling_robustness() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("error-test");
fs::create_dir(&project_dir).unwrap();
fs::write(
project_dir.join("meson.build"),
r#"project('error-test', 'c', version : '1.0.0')
# Dependency that might not exist
missing_dep = dependency('this-probably-does-not-exist-anywhere',
required: false)
# Still define something so meson doesn't completely fail
executable('test', 'main.c', dependencies: missing_dep)
"#,
)
.unwrap();
fs::write(project_dir.join("main.c"), "int main() { return 0; }").unwrap();
let mut session = PlainSession::new();
session.chdir(temp_dir.path()).unwrap();
let buildsystems = detect_buildsystems(&project_dir);
let meson = buildsystems
.iter()
.find(|bs| bs.name() == "meson")
.expect("Should find Meson buildsystem");
match meson.get_declared_dependencies(&session, None) {
Ok(deps) => {
log::debug!(
"Handled potentially problematic project: {} deps",
deps.len()
);
let missing_deps: Vec<_> = deps
.iter()
.filter(|(_, dep)| dep.family().contains("this-probably-does-not-exist"))
.collect();
if !missing_deps.is_empty() {
log::debug!(
"Found declared but unavailable dependency: {:?}",
missing_deps[0].1.family()
);
}
}
Err(e) => {
let error_str = format!("{:?}", e);
assert!(
!error_str.contains("No command specified"),
"Should not get 'No command specified' after fixing scan-dependencies: {}",
error_str
);
log::debug!("Handled error case appropriately: {}", error_str);
}
}
}
}