1use crate::version::{Version, VersionError};
6use crate::{CMakePackage, CMakeTarget};
7use std::{error, fmt};
8
9use itertools::Itertools;
10use serde::Deserialize;
11use std::path::{Path, PathBuf};
12use std::process::{Command, Stdio};
13use tempfile::TempDir;
14use which::which;
15
16pub const CMAKE_MIN_VERSION: &str = "3.19";
18
19#[derive(Debug, Clone)]
21pub struct CMakeProgram {
22 pub path: PathBuf,
24 pub version: Version,
26}
27
28fn script_path(script: &str) -> PathBuf {
29 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
30 .join("cmake")
31 .join(script)
32}
33
34#[derive(Debug)]
36pub enum Error {
37 CMakeNotFound,
39 UnsupportedCMakeVersion,
41 Internal,
43 IO(std::io::Error),
45 Version(VersionError),
47 PackageNotFound,
49}
50
51impl fmt::Display for Error {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "CMake Error: {:?}", self)
54 }
55}
56
57impl error::Error for Error {}
58
59#[derive(Clone, Debug, Deserialize)]
60struct PackageResult {
61 name: Option<String>,
62 version: Option<String>,
63 components: Option<Vec<String>>,
64}
65
66pub fn find_cmake() -> Result<CMakeProgram, Error> {
74 let path = which("cmake").or(Err(Error::CMakeNotFound))?;
75
76 let working_directory = get_temporary_working_directory()?;
77 let output_file = working_directory.path().join("version_info.json");
78
79 if Command::new(&path)
80 .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
81 .arg(format!("-DOUTPUT_FILE={}", output_file.display()))
82 .arg("-P")
83 .arg(script_path("cmake_version.cmake"))
84 .status()
85 .map_err(|_| Error::Internal)?
86 .success()
87 {
88 let reader = std::fs::File::open(output_file).map_err(Error::IO)?;
89 let version: Version = serde_json::from_reader(reader).or(Err(Error::Internal))?;
90 Ok(CMakeProgram { path, version })
91 } else {
92 Err(Error::UnsupportedCMakeVersion)
93 }
94}
95
96fn get_temporary_working_directory() -> Result<TempDir, Error> {
97 #[cfg(test)]
98 let out_dir = std::env::temp_dir();
99 #[cfg(not(test))]
100 let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap_or_else(|_| {
101 panic!("OUT_DIR is not set, are you running the crate from build.rs?")
102 }));
103
104 tempfile::Builder::new()
106 .prefix("cmake-package-rs")
107 .tempdir_in(out_dir)
108 .or(Err(Error::Internal))
109}
110
111fn setup_cmake_project(working_directory: &Path) -> Result<(), Error> {
112 std::fs::copy(
113 script_path("find_package.cmake"),
114 working_directory.join("CMakeLists.txt"),
115 )
116 .map_err(Error::IO)?;
117 Ok(())
118}
119
120fn stdio(verbose: bool) -> Stdio {
121 if verbose {
122 Stdio::inherit()
123 } else {
124 Stdio::null()
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
129enum CMakeBuildType {
130 Debug,
131 Release,
132 RelWithDebInfo,
133 MinSizeRel,
134}
135
136fn build_type() -> CMakeBuildType {
137 match std::env::var("PROFILE")
140 .as_ref()
141 .unwrap_or(&"debug".to_string())
142 .as_str()
143 {
144 "release" => {
145 let opt_level = std::env::var("OPT_LEVEL").unwrap_or("0".to_string());
151 if "sz".contains(&opt_level) {
152 return CMakeBuildType::MinSizeRel;
153 }
154
155 let debug = std::env::var("DEBUG").unwrap_or("0".to_string());
158 if !["0", "false", "none"].contains(&debug.as_str()) {
159 return CMakeBuildType::RelWithDebInfo;
160 }
161
162 CMakeBuildType::Release
164 }
165 _ => CMakeBuildType::Debug,
167 }
168}
169
170pub(crate) fn find_package(
172 name: String,
173 version: Option<Version>,
174 components: Option<Vec<String>>,
175 names: Option<Vec<String>>,
176 verbose: bool,
177 prefix_paths: Option<Vec<PathBuf>>,
178 defines: Vec<(String, String)>,
179) -> Result<CMakePackage, Error> {
180 let cmake = find_cmake()?;
182
183 let working_directory = get_temporary_working_directory()?;
184
185 setup_cmake_project(working_directory.path())?;
186
187 let output_file = working_directory.path().join("package.json");
188 let mut command = Command::new(&cmake.path);
190 command
191 .stdout(stdio(verbose))
192 .stderr(stdio(verbose))
193 .current_dir(&working_directory)
194 .arg(".")
195 .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type()))
196 .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
197 .arg(format!("-DPACKAGE={}", name))
198 .arg(format!("-DOUTPUT_FILE={}", output_file.display()))
199 .arg(format!(
200 "-DCMAKE_PREFIX_PATH={}",
201 prefix_paths
202 .unwrap_or_default()
203 .into_iter()
204 .map(|path| path.display().to_string())
205 .join(";")
206 ))
207 .arg(format!(
208 "-DCMAKE_FIND_DEBUG_MODE={}",
209 if verbose { "TRUE" } else { "FALSE" }
210 ));
211 if let Some(version) = version {
212 command.arg(format!("-DVERSION={}", version));
213 }
214 if let Some(components) = components {
215 command.arg(format!("-DCOMPONENTS={}", components.join(";")));
216 }
217 if let Some(ref names) = names {
218 command.arg(format!("-DNAMES={}", names.join(";")));
219 }
220 for (key, value) in &defines {
221 command.arg(format!("-D{}={}", key, value));
222 }
223 command.output().map_err(Error::IO)?;
224
225 let reader = std::fs::File::open(output_file).map_err(Error::IO)?;
227 let package: PackageResult = serde_json::from_reader(reader).or(Err(Error::Internal))?;
228
229 let package_name = match package.name {
230 Some(name) => name,
231 None => return Err(Error::PackageNotFound),
232 };
233
234 let package_version = match package.version {
235 Some(version) => Some(version.try_into().map_err(Error::Version)?),
236 None => None, };
238
239 if let Some(version) = version {
240 if let Some(package_version) = package_version {
241 if package_version < version {
242 return Err(Error::Version(VersionError::VersionTooOld(package_version)));
243 }
244 }
245
246 }
248
249 Ok(CMakePackage::new(
250 cmake,
251 working_directory,
252 package_name,
253 package_version,
254 package.components,
255 names,
256 verbose,
257 ))
258}
259
260#[derive(Clone, Debug, Deserialize)]
261#[serde(untagged)]
262enum PropertyValue {
263 String(String),
264 Target(Target),
265}
266
267impl From<PropertyValue> for Vec<String> {
268 fn from(value: PropertyValue) -> Self {
269 match value {
270 PropertyValue::String(value) => vec![value],
271 PropertyValue::Target(target) => match target.location {
272 Some(location) => vec![location],
273 None => vec![],
274 }
275 .into_iter()
276 .chain(
277 target
278 .interface_link_libraries
279 .unwrap_or_default()
280 .into_iter()
281 .flat_map(Into::<Vec<String>>::into),
282 )
283 .collect(),
284 }
285 }
286}
287
288#[derive(Debug, Default, Deserialize, Clone)]
289#[serde(default, rename_all = "UPPERCASE")]
290struct Target {
291 name: String,
292 location: Option<String>,
293 #[serde(rename = "LOCATION_Release")]
294 location_release: Option<String>,
295 #[serde(rename = "LOCATION_Debug")]
296 location_debug: Option<String>,
297 #[serde(rename = "LOCATION_RelWithDebInfo")]
298 location_relwithdebinfo: Option<String>,
299 #[serde(rename = "LOCATION_MinSizeRel")]
300 location_minsizerel: Option<String>,
301 imported_implib: Option<String>,
302 #[serde(rename = "IMPORTED_IMPLIB_Release")]
303 imported_implib_release: Option<String>,
304 #[serde(rename = "IMPORTED_IMPLIB_Debug")]
305 imported_implib_debug: Option<String>,
306 #[serde(rename = "IMPORTED_IMPLIB_RelWithDebInfo")]
307 imported_implib_relwithdebinfo: Option<String>,
308 #[serde(rename = "IMPORTED_IMPLIB_MinSizeRel")]
309 imported_implib_minsizerel: Option<String>,
310 interface_compile_definitions: Option<Vec<String>>,
311 interface_compile_options: Option<Vec<String>>,
312 interface_include_directories: Option<Vec<String>>,
313 interface_link_directories: Option<Vec<String>>,
314 interface_link_libraries: Option<Vec<PropertyValue>>,
315 interface_link_options: Option<Vec<String>>,
316}
317
318fn collect_from_targets<'a>(
335 target: &'a Target,
336 property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
337) -> Vec<String> {
338 property(target)
339 .as_ref()
340 .map_or(Vec::new(), Clone::clone)
341 .into_iter()
342 .chain(
343 target
344 .interface_link_libraries
345 .as_ref()
346 .map_or(Vec::new(), Clone::clone)
347 .iter()
348 .filter_map(|value| match value {
349 PropertyValue::String(_) => None,
350 PropertyValue::Target(target) => Some(target),
351 })
352 .flat_map(|target| collect_from_targets(target, property)),
353 )
354 .collect()
355}
356
357fn collect_from_targets_unique<'a>(
360 target: &'a Target,
361 property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
362) -> Vec<String> {
363 collect_from_targets(target, property)
364 .into_iter()
365 .sorted()
366 .dedup()
367 .collect()
368}
369
370fn implib_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
371 match build_type {
372 CMakeBuildType::Debug => target
373 .imported_implib_debug
374 .clone()
375 .or(target.imported_implib.clone()),
376 CMakeBuildType::Release => target
377 .imported_implib_release
378 .clone()
379 .or(target.imported_implib.clone()),
380 CMakeBuildType::RelWithDebInfo => target
381 .imported_implib_relwithdebinfo
382 .clone()
383 .or(target.imported_implib.clone()),
384 CMakeBuildType::MinSizeRel => target
385 .imported_implib_minsizerel
386 .clone()
387 .or(target.imported_implib.clone()),
388 }
389 .or_else(|| location_for_build_type(build_type, target))
390}
391
392fn location_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
393 match build_type {
394 CMakeBuildType::Debug => target.location_debug.clone().or(target.location.clone()),
395 CMakeBuildType::Release => target.location_release.clone().or(target.location.clone()),
396 CMakeBuildType::RelWithDebInfo => target
397 .location_relwithdebinfo
398 .clone()
399 .or(target.location.clone()),
400 CMakeBuildType::MinSizeRel => target
401 .location_minsizerel
402 .clone()
403 .or(target.location.clone()),
404 }
405}
406
407fn library_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
408 if cfg!(target_os = "windows") {
409 implib_for_build_type(build_type, target)
410 } else {
411 location_for_build_type(build_type, target)
412 }
413}
414
415impl Target {
416 fn into_cmake_target(self, build_type: CMakeBuildType) -> CMakeTarget {
417 CMakeTarget {
418 compile_definitions: collect_from_targets_unique(&self, |target| {
419 &target.interface_compile_definitions
420 }),
421 compile_options: collect_from_targets(&self, |target| {
422 &target.interface_compile_options
423 }),
424 include_directories: collect_from_targets_unique(&self, |target| {
425 &target.interface_include_directories
426 }),
427 link_directories: collect_from_targets_unique(&self, |target| {
428 &target.interface_link_directories
429 }),
430 link_options: collect_from_targets(&self, |target| &target.interface_link_options),
431 link_libraries: library_for_build_type(build_type, &self)
432 .as_ref()
433 .map_or(vec![], |location| vec![location.clone()])
434 .into_iter()
435 .chain(
436 self.interface_link_libraries
437 .as_ref()
438 .map_or(Vec::new(), Clone::clone)
439 .into_iter()
440 .flat_map(Into::<Vec<String>>::into),
441 )
442 .sorted() .dedup()
444 .collect(),
445 name: self.name,
446 location: self.location,
447 }
448 }
449}
450
451pub(crate) fn find_target(
454 package: &CMakePackage,
455 target: impl Into<String>,
456) -> Option<CMakeTarget> {
457 let target: String = target.into();
458
459 let output_file = package.working_directory.path().join(format!(
461 "target_{}.json",
462 target.to_lowercase().replace(":", "_")
463 ));
464 let build_type = build_type();
465 let mut command = Command::new(&package.cmake.path);
466 command
467 .stdout(stdio(package.verbose))
468 .stderr(stdio(package.verbose))
469 .current_dir(package.working_directory.path())
470 .arg(".")
471 .arg(format!("-DTARGET={}", target))
472 .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
473 command.output().ok()?;
474
475 let reader = std::fs::File::open(&output_file).ok()?;
477 let target: Target = serde_json::from_reader(reader)
478 .map_err(|e| {
479 eprintln!("Failed to parse target JSON: {:?}", e);
480 })
481 .ok()?;
482 if target.name.is_empty() {
483 return None;
484 }
485 Some(target.into_cmake_target(build_type))
486}
487
488#[derive(Debug, Deserialize)]
489struct TargetProperty {
490 value: Option<PropertyValue>,
491}
492
493pub(crate) fn target_property(
494 package: &CMakePackage,
495 target: &CMakeTarget,
496 property: impl Into<String>,
497) -> Option<String> {
498 let property: String = property.into();
499
500 let output_file = package.working_directory.path().join(format!(
502 "target_property_{}_{}.json",
503 target.name.to_lowercase().replace(":", "_"),
504 property.to_lowercase(),
505 ));
506 let mut command = Command::new(&package.cmake.path);
507 command
508 .stdout(stdio(package.verbose))
509 .stderr(stdio(package.verbose))
510 .current_dir(package.working_directory.path())
511 .arg(".")
512 .arg(format!("-DTARGET={}", target.name))
513 .arg(format!("-DPROPERTY={}", property))
514 .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
515 command.output().ok()?;
516
517 let reader = std::fs::File::open(&output_file).ok()?;
519 let property_result: TargetProperty = serde_json::from_reader(reader)
520 .map_err(|e| {
521 eprintln!("Failed to parse target property JSON: {:?}", e);
522 })
523 .ok()?;
524
525 match property_result.value {
526 Some(PropertyValue::String(value)) => Some(value),
527 Some(PropertyValue::Target(_)) => {
528 eprintln!("Returning PropertyValue::Target from target_property not supported");
529 None
530 }
531 None => None,
532 }
533}
534
535#[cfg(test)]
536mod testing {
537 use scopeguard::{guard, ScopeGuard};
538 use serial_test::serial;
539
540 use super::*;
541
542 #[test]
543 fn from_target() {
544 let target = Target {
545 name: "my_target".to_string(),
546 location: Some("/path/to/target.so".to_string()),
547 interface_compile_definitions: Some(vec!["DEFINE1".to_string(), "DEFINE2".to_string()]),
548 interface_compile_options: Some(vec!["-O2".to_string(), "-Wall".to_string()]),
549 interface_include_directories: Some(vec!["/path/to/include".to_string()]),
550 interface_link_directories: Some(vec!["/path/to/lib".to_string()]),
551 interface_link_options: Some(vec!["-L/path/to/lib".to_string()]),
552 interface_link_libraries: Some(vec![
553 PropertyValue::String("library1".to_string()),
554 PropertyValue::String("library2".to_string()),
555 PropertyValue::Target(Target {
556 name: "dependency".to_string(),
557 location: Some("/path/to/dependency.so".to_string()),
558 interface_compile_definitions: Some(vec!["DEFINE3".to_string()]),
559 interface_compile_options: Some(vec!["-O3".to_string()]),
560 interface_include_directories: Some(vec![
561 "/path/to/dependency/include".to_string()
562 ]),
563 interface_link_directories: Some(vec!["/path/to/dependency/lib".to_string()]),
564 interface_link_options: Some(vec!["-L/path/to/dependency/lib".to_string()]),
565 interface_link_libraries: Some(vec![PropertyValue::String(
566 "dependency_library".to_string(),
567 )]),
568 ..Default::default()
569 }),
570 ]),
571 ..Default::default()
572 };
573
574 let cmake_target: CMakeTarget = target.into_cmake_target(CMakeBuildType::Release);
575
576 assert_eq!(cmake_target.name, "my_target");
577 assert_eq!(
578 cmake_target.compile_definitions,
579 vec!["DEFINE1", "DEFINE2", "DEFINE3"]
580 );
581 assert_eq!(cmake_target.compile_options, vec!["-O2", "-Wall", "-O3"]);
582 assert_eq!(
583 cmake_target.include_directories,
584 vec!["/path/to/dependency/include", "/path/to/include"]
585 );
586 assert_eq!(
587 cmake_target.link_directories,
588 vec!["/path/to/dependency/lib", "/path/to/lib"]
589 );
590 assert_eq!(
591 cmake_target.link_options,
592 vec!["-L/path/to/lib", "-L/path/to/dependency/lib"]
593 );
594 assert_eq!(
595 cmake_target.link_libraries,
596 vec![
597 "/path/to/dependency.so",
598 "/path/to/target.so",
599 "dependency_library",
600 "library1",
601 "library2",
602 ]
603 );
604 }
605
606 #[test]
607 fn from_debug_target() {
608 let target = Target {
609 name: "test_target".to_string(),
610 location: Some("/path/to/target.so".to_string()),
611 location_debug: Some("/path/to/libtarget_debug.so".to_string()),
612 ..Default::default()
613 };
614
615 let cmake_target = target.into_cmake_target(CMakeBuildType::Debug);
616 assert_eq!(
617 cmake_target.link_libraries,
618 vec!["/path/to/libtarget_debug.so"]
619 );
620 }
621
622 #[test]
623 fn from_json() {
624 let json = r#"
625{
626 "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
627 "INTERFACE_LINK_LIBRARIES" :
628 [
629 {
630 "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
631 "LOCATION" : "/usr/lib/libcrypto.so",
632 "NAME" : "OpenSSL::Crypto"
633 }
634 ],
635 "LOCATION" : "/usr/lib/libssl.so",
636 "NAME" : "OpenSSL::SSL"
637}
638"#;
639 let target: Target = serde_json::from_str(json).expect("Failed to parse JSON");
640 assert_eq!(target.name, "OpenSSL::SSL");
641 assert_eq!(target.location, Some("/usr/lib/libssl.so".to_string()));
642 assert_eq!(
643 target.interface_include_directories,
644 Some(vec!["/usr/include".to_string()])
645 );
646 assert!(target.interface_link_libraries.is_some());
647 assert_eq!(target.interface_link_libraries.as_ref().unwrap().len(), 1);
648 let sub_target = target
649 .interface_link_libraries
650 .as_ref()
651 .unwrap()
652 .first()
653 .unwrap();
654 match sub_target {
655 PropertyValue::Target(sub_target) => {
656 assert_eq!(sub_target.name, "OpenSSL::Crypto");
657 assert_eq!(
658 sub_target.location,
659 Some("/usr/lib/libcrypto.so".to_string())
660 );
661 }
662 _ => panic!("Expected PropertyValue::Target"),
663 }
664 }
665
666 fn clear_env(name: &'static str) -> ScopeGuard<(), impl FnOnce(())> {
667 let value = std::env::var(name);
668 std::env::remove_var(name);
669 guard((), move |_| {
670 if let Ok(value) = value {
671 std::env::set_var(name, value);
672 } else {
673 std::env::remove_var(name);
674 }
675 })
676 }
677
678 #[test]
679 #[serial]
680 fn test_build_type() {
681 let _profile = clear_env("PROFILE");
682 let _debug = clear_env("DEBUG");
683 let _opt_level = clear_env("OPT_LEVEL");
684
685 assert_eq!(build_type(), CMakeBuildType::Debug);
686
687 std::env::set_var("PROFILE", "release");
688 assert_eq!(build_type(), CMakeBuildType::Release);
689
690 std::env::set_var("DEBUG", "1");
691 assert_eq!(build_type(), CMakeBuildType::RelWithDebInfo);
692
693 std::env::set_var("DEBUG", "0");
694 std::env::set_var("OPT_LEVEL", "s");
695 assert_eq!(build_type(), CMakeBuildType::MinSizeRel);
696 }
697}