1use crate::version::{Version, VersionError};
6use crate::{CMakePackage, CMakeTarget};
7
8use itertools::Itertools;
9use serde::Deserialize;
10use std::path::{Path, PathBuf};
11use std::process::{Command, Stdio};
12use tempfile::TempDir;
13use which::which;
14
15pub const CMAKE_MIN_VERSION: &str = "3.19";
17
18#[derive(Debug, Clone)]
20pub struct CMakeProgram {
21 pub path: PathBuf,
23 pub version: Version,
25}
26
27fn script_path(script: &str) -> PathBuf {
28 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
29 .join("cmake")
30 .join(script)
31}
32
33#[derive(Debug)]
35pub enum Error {
36 CMakeNotFound,
38 UnsupportedCMakeVersion,
40 Internal,
42 IO(std::io::Error),
44 Version(VersionError),
46 PackageNotFound,
48}
49
50#[derive(Clone, Debug, Deserialize)]
51struct PackageResult {
52 name: Option<String>,
53 version: Option<String>,
54 components: Option<Vec<String>>,
55}
56
57pub fn find_cmake() -> Result<CMakeProgram, Error> {
65 let path = which("cmake").or(Err(Error::CMakeNotFound))?;
66
67 let output = Command::new(&path)
68 .env("CLICOLOR", "0")
69 .arg("-P")
70 .arg(script_path("cmake_version.cmake"))
71 .output()
72 .or(Err(Error::Internal))?;
73
74 let version = String::from_utf8_lossy(&output.stderr)
75 .trim()
76 .to_string()
77 .try_into()
78 .or(Err(Error::UnsupportedCMakeVersion))?;
79
80 if version
81 < CMAKE_MIN_VERSION
82 .try_into()
83 .map_err(|_| Error::UnsupportedCMakeVersion)?
84 {
85 return Err(Error::UnsupportedCMakeVersion);
86 }
87
88 Ok(CMakeProgram { path, version })
89}
90
91fn get_temporary_working_directory() -> Result<TempDir, Error> {
92 #[cfg(test)]
93 let out_dir = std::env::temp_dir();
94 #[cfg(not(test))]
95 let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap_or_else(|_| {
96 panic!("OUT_DIR is not set, are you running the crate from build.rs?")
97 }));
98
99 tempfile::Builder::new()
101 .prefix("cmake-package-rs")
102 .tempdir_in(out_dir)
103 .or(Err(Error::Internal))
104}
105
106fn setup_cmake_project(working_directory: &Path) -> Result<(), Error> {
107 std::fs::copy(
108 script_path("find_package.cmake"),
109 working_directory.join("CMakeLists.txt"),
110 )
111 .map_err(Error::IO)?;
112 Ok(())
113}
114
115fn stdio(verbose: bool) -> Stdio {
116 if verbose {
117 Stdio::inherit()
118 } else {
119 Stdio::null()
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
124enum CMakeBuildType {
125 Debug,
126 Release,
127 RelWithDebInfo,
128 MinSizeRel,
129}
130
131fn build_type() -> CMakeBuildType {
132 match std::env::var("PROFILE")
135 .as_ref()
136 .unwrap_or(&"debug".to_string())
137 .as_str()
138 {
139 "release" => {
140 let opt_level = std::env::var("OPT_LEVEL").unwrap_or("0".to_string());
146 if "sz".contains(&opt_level) {
147 return CMakeBuildType::MinSizeRel;
148 }
149
150 let debug = std::env::var("DEBUG").unwrap_or("0".to_string());
153 if !["0", "false", "none"].contains(&debug.as_str()) {
154 return CMakeBuildType::RelWithDebInfo;
155 }
156
157 CMakeBuildType::Release
159 }
160 _ => CMakeBuildType::Debug,
162 }
163}
164
165pub(crate) fn find_package(
167 name: String,
168 version: Option<Version>,
169 components: Option<Vec<String>>,
170 verbose: bool,
171) -> Result<CMakePackage, Error> {
172 let cmake = find_cmake()?;
174
175 let working_directory = get_temporary_working_directory()?;
176
177 setup_cmake_project(working_directory.path())?;
178
179 let output_file = working_directory.path().join("package.json");
180 let mut command = Command::new(&cmake.path);
182 command
183 .stdout(stdio(verbose))
184 .stderr(stdio(verbose))
185 .current_dir(&working_directory)
186 .arg(".")
187 .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type()))
188 .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
189 .arg(format!("-DPACKAGE={}", name))
190 .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
191 if let Some(version) = version {
192 command.arg(format!("-DVERSION={}", version));
193 }
194 if let Some(components) = components {
195 command.arg(format!("-DCOMPONENTS={}", components.join(";")));
196 }
197 command.output().map_err(Error::IO)?;
198
199 let reader = std::fs::File::open(output_file).map_err(Error::IO)?;
201 let package: PackageResult = serde_json::from_reader(reader).or(Err(Error::Internal))?;
202
203 let package_name = match package.name {
204 Some(name) => name,
205 None => return Err(Error::PackageNotFound),
206 };
207
208 let package_version = match package.version {
209 Some(version) => Some(version.try_into().map_err(Error::Version)?),
210 None => None, };
212
213 if let Some(version) = version {
214 if let Some(package_version) = package_version {
215 if package_version < version {
216 return Err(Error::Version(VersionError::VersionTooOld(package_version)));
217 }
218 }
219
220 }
222
223 Ok(CMakePackage::new(
224 cmake,
225 working_directory,
226 package_name,
227 package_version,
228 package.components,
229 verbose,
230 ))
231}
232
233#[derive(Clone, Debug, Deserialize)]
234#[serde(untagged)]
235enum PropertyValue {
236 String(String),
237 Target(Target),
238}
239
240impl From<PropertyValue> for Vec<String> {
241 fn from(value: PropertyValue) -> Self {
242 match value {
243 PropertyValue::String(value) => vec![value],
244 PropertyValue::Target(target) => match target.location {
245 Some(location) => vec![location],
246 None => vec![],
247 }
248 .into_iter()
249 .chain(
250 target
251 .interface_link_libraries
252 .unwrap_or_default()
253 .into_iter()
254 .flat_map(Into::<Vec<String>>::into),
255 )
256 .collect(),
257 }
258 }
259}
260
261#[derive(Debug, Default, Deserialize, Clone)]
262#[serde(default, rename_all = "UPPERCASE")]
263struct Target {
264 name: String,
265 location: Option<String>,
266 #[serde(rename = "LOCATION_Release")]
267 location_release: Option<String>,
268 #[serde(rename = "LOCATION_Debug")]
269 location_debug: Option<String>,
270 #[serde(rename = "LOCATION_RelWithDebInfo")]
271 location_relwithdebinfo: Option<String>,
272 #[serde(rename = "LOCATION_MinSizeRel")]
273 location_minsizerel: Option<String>,
274 imported_implib: Option<String>,
275 #[serde(rename = "IMPORTED_IMPLIB_Release")]
276 imported_implib_release: Option<String>,
277 #[serde(rename = "IMPORTED_IMPLIB_Debug")]
278 imported_implib_debug: Option<String>,
279 #[serde(rename = "IMPORTED_IMPLIB_RelWithDebInfo")]
280 imported_implib_relwithdebinfo: Option<String>,
281 #[serde(rename = "IMPORTED_IMPLIB_MinSizeRel")]
282 imported_implib_minsizerel: Option<String>,
283 interface_compile_definitions: Option<Vec<String>>,
284 interface_compile_options: Option<Vec<String>>,
285 interface_include_directories: Option<Vec<String>>,
286 interface_link_directories: Option<Vec<String>>,
287 interface_link_libraries: Option<Vec<PropertyValue>>,
288 interface_link_options: Option<Vec<String>>,
289}
290
291fn collect_from_targets<'a>(
308 target: &'a Target,
309 property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
310) -> Vec<String> {
311 property(target)
312 .as_ref()
313 .map_or(Vec::new(), Clone::clone)
314 .into_iter()
315 .chain(
316 target
317 .interface_link_libraries
318 .as_ref()
319 .map_or(Vec::new(), Clone::clone)
320 .iter()
321 .filter_map(|value| match value {
322 PropertyValue::String(_) => None,
323 PropertyValue::Target(target) => Some(target),
324 })
325 .flat_map(|target| collect_from_targets(target, property)),
326 )
327 .collect()
328}
329
330fn collect_from_targets_unique<'a>(
333 target: &'a Target,
334 property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
335) -> Vec<String> {
336 collect_from_targets(target, property)
337 .into_iter()
338 .sorted()
339 .dedup()
340 .collect()
341}
342
343fn implib_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
344 match build_type {
345 CMakeBuildType::Debug => target
346 .imported_implib_debug
347 .clone()
348 .or(target.imported_implib.clone()),
349 CMakeBuildType::Release => target
350 .imported_implib_release
351 .clone()
352 .or(target.imported_implib.clone()),
353 CMakeBuildType::RelWithDebInfo => target
354 .imported_implib_relwithdebinfo
355 .clone()
356 .or(target.imported_implib.clone()),
357 CMakeBuildType::MinSizeRel => target
358 .imported_implib_minsizerel
359 .clone()
360 .or(target.imported_implib.clone()),
361 }
362 .or_else(|| location_for_build_type(build_type, target))
363}
364
365fn location_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
366 match build_type {
367 CMakeBuildType::Debug => target.location_debug.clone().or(target.location.clone()),
368 CMakeBuildType::Release => target.location_release.clone().or(target.location.clone()),
369 CMakeBuildType::RelWithDebInfo => target
370 .location_relwithdebinfo
371 .clone()
372 .or(target.location.clone()),
373 CMakeBuildType::MinSizeRel => target
374 .location_minsizerel
375 .clone()
376 .or(target.location.clone()),
377 }
378}
379
380fn library_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
381 if cfg!(target_os = "windows") {
382 implib_for_build_type(build_type, target)
383 } else {
384 location_for_build_type(build_type, target)
385 }
386}
387
388impl Target {
389 fn into_cmake_target(self, build_type: CMakeBuildType) -> CMakeTarget {
390 CMakeTarget {
391 compile_definitions: collect_from_targets_unique(&self, |target| {
392 &target.interface_compile_definitions
393 }),
394 compile_options: collect_from_targets(&self, |target| {
395 &target.interface_compile_options
396 }),
397 include_directories: collect_from_targets_unique(&self, |target| {
398 &target.interface_include_directories
399 }),
400 link_directories: collect_from_targets_unique(&self, |target| {
401 &target.interface_link_directories
402 }),
403 link_options: collect_from_targets(&self, |target| &target.interface_link_options),
404 link_libraries: library_for_build_type(build_type, &self)
405 .as_ref()
406 .map_or(vec![], |location| vec![location.clone()])
407 .into_iter()
408 .chain(
409 self.interface_link_libraries
410 .as_ref()
411 .map_or(Vec::new(), Clone::clone)
412 .into_iter()
413 .flat_map(Into::<Vec<String>>::into),
414 )
415 .sorted() .dedup()
417 .collect(),
418 name: self.name,
419 location: self.location,
420 }
421 }
422}
423
424pub(crate) fn find_target(
427 package: &CMakePackage,
428 target: impl Into<String>,
429) -> Option<CMakeTarget> {
430 let target: String = target.into();
431
432 let output_file = package.working_directory.path().join(format!(
434 "target_{}.json",
435 target.to_lowercase().replace(":", "_")
436 ));
437 let build_type = build_type();
438 let mut command = Command::new(&package.cmake.path);
439 command
440 .stdout(stdio(package.verbose))
441 .stderr(stdio(package.verbose))
442 .current_dir(package.working_directory.path())
443 .arg(".")
444 .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type))
445 .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
446 .arg(format!("-DPACKAGE={}", package.name))
447 .arg(format!("-DTARGET={}", target))
448 .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
449 if let Some(version) = package.version {
450 command.arg(format!("-DVERSION={}", version));
451 }
452 if let Some(components) = &package.components {
453 command.arg(format!("-DCOMPONENTS={}", components.join(";")));
454 }
455 command.output().ok()?;
456
457 let reader = std::fs::File::open(&output_file).ok()?;
459 let target: Target = serde_json::from_reader(reader)
460 .map_err(|e| {
461 eprintln!("Failed to parse target JSON: {:?}", e);
462 })
463 .ok()?;
464 if target.name.is_empty() {
465 return None;
466 }
467 Some(target.into_cmake_target(build_type))
468}
469
470#[cfg(test)]
471mod testing {
472 use scopeguard::{guard, ScopeGuard};
473 use serial_test::serial;
474
475 use super::*;
476
477 #[test]
478 fn from_target() {
479 let target = Target {
480 name: "my_target".to_string(),
481 location: Some("/path/to/target.so".to_string()),
482 interface_compile_definitions: Some(vec!["DEFINE1".to_string(), "DEFINE2".to_string()]),
483 interface_compile_options: Some(vec!["-O2".to_string(), "-Wall".to_string()]),
484 interface_include_directories: Some(vec!["/path/to/include".to_string()]),
485 interface_link_directories: Some(vec!["/path/to/lib".to_string()]),
486 interface_link_options: Some(vec!["-L/path/to/lib".to_string()]),
487 interface_link_libraries: Some(vec![
488 PropertyValue::String("library1".to_string()),
489 PropertyValue::String("library2".to_string()),
490 PropertyValue::Target(Target {
491 name: "dependency".to_string(),
492 location: Some("/path/to/dependency.so".to_string()),
493 interface_compile_definitions: Some(vec!["DEFINE3".to_string()]),
494 interface_compile_options: Some(vec!["-O3".to_string()]),
495 interface_include_directories: Some(vec![
496 "/path/to/dependency/include".to_string()
497 ]),
498 interface_link_directories: Some(vec!["/path/to/dependency/lib".to_string()]),
499 interface_link_options: Some(vec!["-L/path/to/dependency/lib".to_string()]),
500 interface_link_libraries: Some(vec![PropertyValue::String(
501 "dependency_library".to_string(),
502 )]),
503 ..Default::default()
504 }),
505 ]),
506 ..Default::default()
507 };
508
509 let cmake_target: CMakeTarget = target.into_cmake_target(CMakeBuildType::Release);
510
511 assert_eq!(cmake_target.name, "my_target");
512 assert_eq!(
513 cmake_target.compile_definitions,
514 vec!["DEFINE1", "DEFINE2", "DEFINE3"]
515 );
516 assert_eq!(cmake_target.compile_options, vec!["-O2", "-Wall", "-O3"]);
517 assert_eq!(
518 cmake_target.include_directories,
519 vec!["/path/to/dependency/include", "/path/to/include"]
520 );
521 assert_eq!(
522 cmake_target.link_directories,
523 vec!["/path/to/dependency/lib", "/path/to/lib"]
524 );
525 assert_eq!(
526 cmake_target.link_options,
527 vec!["-L/path/to/lib", "-L/path/to/dependency/lib"]
528 );
529 assert_eq!(
530 cmake_target.link_libraries,
531 vec![
532 "/path/to/dependency.so",
533 "/path/to/target.so",
534 "dependency_library",
535 "library1",
536 "library2",
537 ]
538 );
539 }
540
541 #[test]
542 fn from_debug_target() {
543 let target = Target {
544 name: "test_target".to_string(),
545 location: Some("/path/to/target.so".to_string()),
546 location_debug: Some("/path/to/libtarget_debug.so".to_string()),
547 ..Default::default()
548 };
549
550 let cmake_target = target.into_cmake_target(CMakeBuildType::Debug);
551 assert_eq!(
552 cmake_target.link_libraries,
553 vec!["/path/to/libtarget_debug.so"]
554 );
555 }
556
557 #[test]
558 fn from_json() {
559 let json = r#"
560{
561 "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
562 "INTERFACE_LINK_LIBRARIES" :
563 [
564 {
565 "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
566 "LOCATION" : "/usr/lib/libcrypto.so",
567 "NAME" : "OpenSSL::Crypto"
568 }
569 ],
570 "LOCATION" : "/usr/lib/libssl.so",
571 "NAME" : "OpenSSL::SSL"
572}
573"#;
574 let target: Target = serde_json::from_str(json).expect("Failed to parse JSON");
575 assert_eq!(target.name, "OpenSSL::SSL");
576 assert_eq!(target.location, Some("/usr/lib/libssl.so".to_string()));
577 assert_eq!(
578 target.interface_include_directories,
579 Some(vec!["/usr/include".to_string()])
580 );
581 assert!(target.interface_link_libraries.is_some());
582 assert_eq!(target.interface_link_libraries.as_ref().unwrap().len(), 1);
583 let sub_target = target
584 .interface_link_libraries
585 .as_ref()
586 .unwrap()
587 .first()
588 .unwrap();
589 match sub_target {
590 PropertyValue::Target(sub_target) => {
591 assert_eq!(sub_target.name, "OpenSSL::Crypto");
592 assert_eq!(
593 sub_target.location,
594 Some("/usr/lib/libcrypto.so".to_string())
595 );
596 }
597 _ => panic!("Expected PropertyValue::Target"),
598 }
599 }
600
601 fn clear_env(name: &'static str) -> ScopeGuard<(), impl FnOnce(())> {
602 let value = std::env::var(name);
603 std::env::remove_var(name);
604 guard((), move |_| {
605 if let Ok(value) = value {
606 std::env::set_var(name, value);
607 } else {
608 std::env::remove_var(name);
609 }
610 })
611 }
612
613 #[test]
614 #[serial]
615 fn test_build_type() {
616 let _profile = clear_env("PROFILE");
617 let _debug = clear_env("DEBUG");
618 let _opt_level = clear_env("OPT_LEVEL");
619
620 assert_eq!(build_type(), CMakeBuildType::Debug);
621
622 std::env::set_var("PROFILE", "release");
623 assert_eq!(build_type(), CMakeBuildType::Release);
624
625 std::env::set_var("DEBUG", "1");
626 assert_eq!(build_type(), CMakeBuildType::RelWithDebInfo);
627
628 std::env::set_var("DEBUG", "0");
629 std::env::set_var("OPT_LEVEL", "s");
630 assert_eq!(build_type(), CMakeBuildType::MinSizeRel);
631 }
632}