Skip to content

feat: add cmake/pkg-config tests #1606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/xtensor/recipe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ build:
requirements:
build:
- ${{ compiler('cxx') }}
- cmake
- cmake <4
- ninja
- nushell
host:
Expand All @@ -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
Expand Down
169 changes: 167 additions & 2 deletions src/package_test/run_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(())
}
}

4 changes: 2 additions & 2 deletions src/recipe/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};

Expand Down
70 changes: 69 additions & 1 deletion src/recipe/parser/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ pub struct PerlTest {
pub uses: Vec<String>,
}

/// 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<String>,
}

/// 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<String>,
}

/// A test that runs the tests of a downstream package.
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct DownstreamTest {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -281,10 +305,18 @@ impl TryConvertNode<TestType> 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(())
Expand Down Expand Up @@ -480,6 +512,42 @@ impl TryConvertNode<PackageContentsTest> for RenderedMappingNode {
}
}

///////////////////////////
/// CMake Test ///
///////////////////////////
impl TryConvertNode<CMakeTest> for RenderedMappingNode {
fn try_convert(&self, _name: &str) -> Result<CMakeTest, Vec<PartialParsingError>> {
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<PkgConfigTest> for RenderedMappingNode {
fn try_convert(&self, _name: &str) -> Result<PkgConfigTest, Vec<PartialParsingError>> {
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 ///
///////////////////////////
Expand Down
Loading