diff --git a/examples/xtensor/recipe.yaml b/examples/xtensor/recipe.yaml index 8dc268ce..73873aef 100644 --- a/examples/xtensor/recipe.yaml +++ b/examples/xtensor/recipe.yaml @@ -15,7 +15,7 @@ build: requirements: build: - ${{ compiler('cxx') }} - - cmake + - cmake <4 - ninja - nushell host: @@ -32,6 +32,10 @@ tests: files: - ${{ "Library/" if win }}share/cmake/xtensor/xtensorConfig.cmake - ${{ "Library/" if win }}share/cmake/xtensor/xtensorConfigVersion.cmake + - cmake: + find_package: [xtensor] + - pkg_config: + pkg_config: [xtensor] about: homepage: https://github.com/xtensor-stack/xtensor license: BSD-3-Clause diff --git a/src/package_test/run_test.rs b/src/package_test/run_test.rs index 199bf924..c00c88e0 100644 --- a/src/package_test/run_test.rs +++ b/src/package_test/run_test.rs @@ -33,8 +33,8 @@ use crate::{ env_vars, metadata::{Debug, PlatformWithVirtualPackages}, recipe::parser::{ - CommandsTest, DownstreamTest, PerlTest, PythonTest, PythonVersion, RTest, Script, - ScriptContent, TestType, + CMakeTest, CommandsTest, DownstreamTest, PerlTest, PythonTest, PythonVersion, RTest, + Script, ScriptContent, TestType, PkgConfigTest, }, render::solver::create_environment, source::copy_dir::CopyDir, @@ -457,6 +457,16 @@ pub async fn run_test( .await? } TestType::R { r } => r.run_test(&pkg, &package_folder, &prefix, &config).await?, + TestType::CMake { cmake } => { + cmake + .run_test(&pkg, &package_folder, &prefix, &config) + .await? + } + TestType::PkgConfig { pkg_config } => { + pkg_config + .run_test(&pkg, &package_folder, &prefix, &config) + .await? + } TestType::Downstream(downstream) if downstream_package.is_none() => { downstream .run_test(&pkg, package_file, &prefix, &config) @@ -975,3 +985,158 @@ impl RTest { Ok(()) } } + +impl CMakeTest { + /// Execute the CMake test + pub async fn run_test( + &self, + pkg: &ArchiveIdentifier, + path: &Path, + prefix: &Path, + config: &TestConfiguration, + ) -> Result<(), TestError> { + let span = tracing::info_span!("Running CMake test"); + let _guard = span.enter(); + + let match_spec = MatchSpec::from_str( + format!("{}={}={}", pkg.name, pkg.version, pkg.build_string).as_str(), + ParseStrictness::Lenient, + )?; + + let dependencies = vec!["cmake".parse().unwrap(), match_spec]; + + create_environment( + "test", + &dependencies, + config + .host_platform + .as_ref() + .unwrap_or(&config.current_platform), + prefix, + &config.channels, + &config.tool_configuration, + config.channel_priority, + config.solve_strategy, + ) + .await + .map_err(TestError::TestEnvironmentSetup)?; + + let tmp_dir = tempfile::tempdir()?; + let cmake_file = tmp_dir.path().join("CMakeLists.txt"); + + let mut cmake_content = String::from("cmake_minimum_required(VERSION 3.15)\nproject(cmake_test)\n\n"); + for package in &self.find_package { + writeln!(cmake_content, "find_package({} REQUIRED)", package)?; + writeln!(cmake_content, "message(STATUS \"Found {} version: ${{{}_VERSION}}\")", package, package)?; + writeln!(cmake_content, "message(STATUS \"Found {} components: ${{{}_LIBRARIES}}\")", package, package)?; + writeln!(cmake_content, "message(STATUS \"Found {} location: ${{{}_INCLUDE_DIRS}}\")", package, package)?; + } + + fs::write(cmake_file, cmake_content)?; + + let script = Script { + content: ScriptContent::Command("cmake .".into()), + ..Script::default() + }; + + let platform = Platform::current(); + let mut env_vars = env_vars::os_vars(prefix, &platform); + env_vars.retain(|key, _| key != ShellEnum::default().path_var(&platform)); + env_vars.insert( + "PREFIX".to_string(), + Some(prefix.to_string_lossy().to_string()), + ); + env_vars.insert( + "CMAKE_PREFIX_PATH".to_string(), + Some(prefix.to_string_lossy().to_string()), + ); + + script + .run_script( + env_vars, + tmp_dir.path(), + path, + prefix, + None, + None, + None, + Debug::new(true), + ) + .await + .map_err(|e| TestError::TestFailed(e.to_string()))?; + + Ok(()) + } +} + +impl PkgConfigTest { + /// Execute the pkg-config test + pub async fn run_test( + &self, + pkg: &ArchiveIdentifier, + path: &Path, + prefix: &Path, + config: &TestConfiguration, + ) -> Result<(), TestError> { + let span = tracing::info_span!("Running pkg-config test"); + let _guard = span.enter(); + + let match_spec = MatchSpec::from_str( + format!("{}={}={}", pkg.name, pkg.version, pkg.build_string).as_str(), + ParseStrictness::Lenient, + )?; + + let dependencies = vec!["pkg-config".parse().unwrap(), match_spec]; + + create_environment( + "test", + &dependencies, + config + .host_platform + .as_ref() + .unwrap_or(&config.current_platform), + prefix, + &config.channels, + &config.tool_configuration, + config.channel_priority, + config.solve_strategy, + ) + .await + .map_err(TestError::TestEnvironmentSetup)?; + + let platform = Platform::current(); + let mut env_vars = env_vars::os_vars(prefix, &platform); + env_vars.retain(|key, _| key != ShellEnum::default().path_var(&platform)); + env_vars.insert( + "PREFIX".to_string(), + Some(prefix.to_string_lossy().to_string()), + ); + + for package in &self.pkg_config { + let script = Script { + content: ScriptContent::Command(format!("pkg-config --exists {}", package)), + ..Script::default() + }; + + let tmp_dir = tempfile::tempdir()?; + script + .run_script( + env_vars.clone(), + tmp_dir.path(), + path, + prefix, + None, + None, + None, + Debug::new(true), + ) + .await + .map_err(|e| TestError::TestFailed(format!("Package {} not found: {}", package, e)))?; + + tracing::info!("Found pkg-config package: {}", package); + } + + Ok(()) + } +} + diff --git a/src/recipe/parser.rs b/src/recipe/parser.rs index 2f2e51ca..bf65142c 100644 --- a/src/recipe/parser.rs +++ b/src/recipe/parser.rs @@ -48,8 +48,8 @@ pub use self::{ script::{Script, ScriptContent}, source::{GitRev, GitSource, GitUrl, PathSource, Source, UrlSource}, test::{ - CommandsTest, CommandsTestFiles, CommandsTestRequirements, DownstreamTest, - PackageContentsTest, PerlTest, PythonTest, PythonVersion, RTest, TestType, + CMakeTest, CommandsTest, CommandsTestFiles, CommandsTestRequirements, DownstreamTest, + PackageContentsTest, PerlTest, PkgConfigTest, PythonTest, PythonVersion, RTest, TestType, }, }; diff --git a/src/recipe/parser/test.rs b/src/recipe/parser/test.rs index 5683a364..a7f731e0 100644 --- a/src/recipe/parser/test.rs +++ b/src/recipe/parser/test.rs @@ -130,6 +130,20 @@ pub struct PerlTest { pub uses: Vec, } +/// A special CMake test that checks if the libraries are available +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CMakeTest { + /// List of CMake libraries to find + pub find_package: Vec, +} + +/// A special PkgConfig test that checks if the libraries are available +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PkgConfigTest { + /// List of PkgConfig libraries to find + pub pkg_config: Vec, +} + /// A test that runs the tests of a downstream package. #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct DownstreamTest { @@ -173,6 +187,16 @@ pub enum TestType { // Note we use a struct for better serialization package_contents: PackageContentsTest, }, + /// A test that checks the CMake libraries + CMake { + /// The CMake libraries to find + cmake: CMakeTest, + }, + /// A test that checks the PkgConfig libraries + PkgConfig { + /// The PkgConfig libraries to find + pkg_config: PkgConfigTest, + }, } /// Package content test that compares the contents of the package with the expected contents. @@ -281,10 +305,18 @@ impl TryConvertNode for RenderedMappingNode { let rscript = as_mapping(value, key_str)?.try_convert(key_str)?; test = TestType::R { r: rscript }; } + "cmake" => { + let cmake = as_mapping(value, key_str)?.try_convert(key_str)?; + test = TestType::CMake { cmake }; + } + "pkg_config" => { + let pkg_config = as_mapping(value, key_str)?.try_convert(key_str)?; + test = TestType::PkgConfig { pkg_config }; + } invalid => Err(vec![_partialerror!( *key.span(), ErrorKind::InvalidField(invalid.to_string().into()), - help = format!("expected fields for {name} is one of `python`, `perl`, `r`, `script`, `downstream`, `package_contents`") + help = format!("expected fields for {name} is one of `python`, `perl`, `r`, `cmake`, `pkg_config`, `script`, `downstream`, `package_contents`") )])? } Ok(()) @@ -480,6 +512,42 @@ impl TryConvertNode for RenderedMappingNode { } } +/////////////////////////// +/// CMake Test /// +/////////////////////////// +impl TryConvertNode for RenderedMappingNode { + fn try_convert(&self, _name: &str) -> Result> { + let mut cmake_test = CMakeTest::default(); + validate_keys!(cmake_test, self.iter(), find_package); + if cmake_test.find_package.is_empty() { + Err(vec![_partialerror!( + *self.span(), + ErrorKind::MissingField("find_package".into()), + help = "expected field `find_package` in cmake test to be a list of packages" + )])?; + } + Ok(cmake_test) + } +} + +/////////////////////////// +/// PkgConfig Test /// +/////////////////////////// +impl TryConvertNode for RenderedMappingNode { + fn try_convert(&self, _name: &str) -> Result> { + let mut pkg_config_test = PkgConfigTest::default(); + validate_keys!(pkg_config_test, self.iter(), pkg_config); + if pkg_config_test.pkg_config.is_empty() { + Err(vec![_partialerror!( + *self.span(), + ErrorKind::MissingField("pkg_config".into()), + help = "expected field `pkg_config` in pkg-config test to be a list of packages" + )])?; + } + Ok(pkg_config_test) + } +} + /////////////////////////// /// Python Version /// ///////////////////////////