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) -> Result<CMakePackage, Error> {
179 let cmake = find_cmake()?;
181
182 let working_directory = get_temporary_working_directory()?;
183
184 setup_cmake_project(working_directory.path())?;
185
186 let output_file = working_directory.path().join("package.json");
187 let mut command = Command::new(&cmake.path);
189 command
190 .stdout(stdio(verbose))
191 .stderr(stdio(verbose))
192 .current_dir(&working_directory)
193 .arg(".")
194 .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type()))
195 .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
196 .arg(format!("-DPACKAGE={}", name))
197 .arg(format!("-DOUTPUT_FILE={}", output_file.display()))
198 .arg(format!(
199 "-DCMAKE_PREFIX_PATH={}",
200 prefix_paths
201 .unwrap_or_default()
202 .into_iter()
203 .map(|path| path.display().to_string())
204 .join(";")
205 ))
206 .arg(format!(
207 "-DCMAKE_FIND_DEBUG_MODE={}",
208 if verbose { "TRUE" } else { "FALSE" }
209 ));
210 if let Some(version) = version {
211 command.arg(format!("-DVERSION={}", version));
212 }
213 if let Some(components) = components {
214 command.arg(format!("-DCOMPONENTS={}", components.join(";")));
215 }
216 if let Some(ref names) = names {
217 command.arg(format!("-DNAMES={}", names.join(";")));
218 }
219 command.output().map_err(Error::IO)?;
220
221 let reader = std::fs::File::open(output_file).map_err(Error::IO)?;
223 let package: PackageResult = serde_json::from_reader(reader).or(Err(Error::Internal))?;
224
225 let package_name = match package.name {
226 Some(name) => name,
227 None => return Err(Error::PackageNotFound),
228 };
229
230 let package_version = match package.version {
231 Some(version) => Some(version.try_into().map_err(Error::Version)?),
232 None => None, };
234
235 if let Some(version) = version {
236 if let Some(package_version) = package_version {
237 if package_version < version {
238 return Err(Error::Version(VersionError::VersionTooOld(package_version)));
239 }
240 }
241
242 }
244
245 Ok(CMakePackage::new(
246 cmake,
247 working_directory,
248 package_name,
249 package_version,
250 package.components,
251 names,
252 verbose,
253 ))
254}
255
256#[derive(Clone, Debug, Deserialize)]
257#[serde(untagged)]
258enum PropertyValue {
259 String(String),
260 Target(Target),
261}
262
263impl From<PropertyValue> for Vec<String> {
264 fn from(value: PropertyValue) -> Self {
265 match value {
266 PropertyValue::String(value) => vec![value],
267 PropertyValue::Target(target) => match target.location {
268 Some(location) => vec![location],
269 None => vec![],
270 }
271 .into_iter()
272 .chain(
273 target
274 .interface_link_libraries
275 .unwrap_or_default()
276 .into_iter()
277 .flat_map(Into::<Vec<String>>::into),
278 )
279 .collect(),
280 }
281 }
282}
283
284#[derive(Debug, Default, Deserialize, Clone)]
285#[serde(default, rename_all = "UPPERCASE")]
286struct Target {
287 name: String,
288 location: Option<String>,
289 #[serde(rename = "LOCATION_Release")]
290 location_release: Option<String>,
291 #[serde(rename = "LOCATION_Debug")]
292 location_debug: Option<String>,
293 #[serde(rename = "LOCATION_RelWithDebInfo")]
294 location_relwithdebinfo: Option<String>,
295 #[serde(rename = "LOCATION_MinSizeRel")]
296 location_minsizerel: Option<String>,
297 imported_implib: Option<String>,
298 #[serde(rename = "IMPORTED_IMPLIB_Release")]
299 imported_implib_release: Option<String>,
300 #[serde(rename = "IMPORTED_IMPLIB_Debug")]
301 imported_implib_debug: Option<String>,
302 #[serde(rename = "IMPORTED_IMPLIB_RelWithDebInfo")]
303 imported_implib_relwithdebinfo: Option<String>,
304 #[serde(rename = "IMPORTED_IMPLIB_MinSizeRel")]
305 imported_implib_minsizerel: Option<String>,
306 interface_compile_definitions: Option<Vec<String>>,
307 interface_compile_options: Option<Vec<String>>,
308 interface_include_directories: Option<Vec<String>>,
309 interface_link_directories: Option<Vec<String>>,
310 interface_link_libraries: Option<Vec<PropertyValue>>,
311 interface_link_options: Option<Vec<String>>,
312}
313
314fn collect_from_targets<'a>(
331 target: &'a Target,
332 property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
333) -> Vec<String> {
334 property(target)
335 .as_ref()
336 .map_or(Vec::new(), Clone::clone)
337 .into_iter()
338 .chain(
339 target
340 .interface_link_libraries
341 .as_ref()
342 .map_or(Vec::new(), Clone::clone)
343 .iter()
344 .filter_map(|value| match value {
345 PropertyValue::String(_) => None,
346 PropertyValue::Target(target) => Some(target),
347 })
348 .flat_map(|target| collect_from_targets(target, property)),
349 )
350 .collect()
351}
352
353fn collect_from_targets_unique<'a>(
356 target: &'a Target,
357 property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
358) -> Vec<String> {
359 collect_from_targets(target, property)
360 .into_iter()
361 .sorted()
362 .dedup()
363 .collect()
364}
365
366fn implib_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
367 match build_type {
368 CMakeBuildType::Debug => target
369 .imported_implib_debug
370 .clone()
371 .or(target.imported_implib.clone()),
372 CMakeBuildType::Release => target
373 .imported_implib_release
374 .clone()
375 .or(target.imported_implib.clone()),
376 CMakeBuildType::RelWithDebInfo => target
377 .imported_implib_relwithdebinfo
378 .clone()
379 .or(target.imported_implib.clone()),
380 CMakeBuildType::MinSizeRel => target
381 .imported_implib_minsizerel
382 .clone()
383 .or(target.imported_implib.clone()),
384 }
385 .or_else(|| location_for_build_type(build_type, target))
386}
387
388fn location_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
389 match build_type {
390 CMakeBuildType::Debug => target.location_debug.clone().or(target.location.clone()),
391 CMakeBuildType::Release => target.location_release.clone().or(target.location.clone()),
392 CMakeBuildType::RelWithDebInfo => target
393 .location_relwithdebinfo
394 .clone()
395 .or(target.location.clone()),
396 CMakeBuildType::MinSizeRel => target
397 .location_minsizerel
398 .clone()
399 .or(target.location.clone()),
400 }
401}
402
403fn library_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
404 if cfg!(target_os = "windows") {
405 implib_for_build_type(build_type, target)
406 } else {
407 location_for_build_type(build_type, target)
408 }
409}
410
411impl Target {
412 fn into_cmake_target(self, build_type: CMakeBuildType) -> CMakeTarget {
413 CMakeTarget {
414 compile_definitions: collect_from_targets_unique(&self, |target| {
415 &target.interface_compile_definitions
416 }),
417 compile_options: collect_from_targets(&self, |target| {
418 &target.interface_compile_options
419 }),
420 include_directories: collect_from_targets_unique(&self, |target| {
421 &target.interface_include_directories
422 }),
423 link_directories: collect_from_targets_unique(&self, |target| {
424 &target.interface_link_directories
425 }),
426 link_options: collect_from_targets(&self, |target| &target.interface_link_options),
427 link_libraries: library_for_build_type(build_type, &self)
428 .as_ref()
429 .map_or(vec![], |location| vec![location.clone()])
430 .into_iter()
431 .chain(
432 self.interface_link_libraries
433 .as_ref()
434 .map_or(Vec::new(), Clone::clone)
435 .into_iter()
436 .flat_map(Into::<Vec<String>>::into),
437 )
438 .sorted() .dedup()
440 .collect(),
441 name: self.name,
442 location: self.location,
443 }
444 }
445}
446
447pub(crate) fn find_target(
450 package: &CMakePackage,
451 target: impl Into<String>,
452) -> Option<CMakeTarget> {
453 let target: String = target.into();
454
455 let output_file = package.working_directory.path().join(format!(
457 "target_{}.json",
458 target.to_lowercase().replace(":", "_")
459 ));
460 let build_type = build_type();
461 let mut command = Command::new(&package.cmake.path);
462 command
463 .stdout(stdio(package.verbose))
464 .stderr(stdio(package.verbose))
465 .current_dir(package.working_directory.path())
466 .arg(".")
467 .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type))
468 .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
469 .arg(format!("-DPACKAGE={}", package.name))
470 .arg(format!("-DTARGET={}", target))
471 .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
472 if let Some(version) = package.version {
473 command.arg(format!("-DVERSION={}", version));
474 }
475 if let Some(components) = &package.components {
476 command.arg(format!("-DCOMPONENTS={}", components.join(";")));
477 }
478 if let Some(names) = &package.names {
479 command.arg(format!("-DNAMES={}", names.join(";")));
480 }
481 command.output().ok()?;
482
483 let reader = std::fs::File::open(&output_file).ok()?;
485 let target: Target = serde_json::from_reader(reader)
486 .map_err(|e| {
487 eprintln!("Failed to parse target JSON: {:?}", e);
488 })
489 .ok()?;
490 if target.name.is_empty() {
491 return None;
492 }
493 Some(target.into_cmake_target(build_type))
494}
495
496#[derive(Debug, Deserialize)]
497struct TargetProperty {
498 value: Option<PropertyValue>,
499}
500
501pub(crate) fn target_property(
502 package: &CMakePackage,
503 target: &CMakeTarget,
504 property: impl Into<String>,
505) -> Option<String> {
506 let property: String = property.into();
507
508 let output_file = package.working_directory.path().join(format!(
510 "target_property_{}_{}.json",
511 target.name.to_lowercase().replace(":", "_"),
512 property.to_lowercase(),
513 ));
514 let build_type = build_type();
515 let mut command = Command::new(&package.cmake.path);
516 command
517 .stdout(stdio(package.verbose))
518 .stderr(stdio(package.verbose))
519 .current_dir(package.working_directory.path())
520 .arg(".")
521 .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type))
522 .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
523 .arg(format!("-DPACKAGE={}", package.name))
524 .arg(format!("-DTARGET={}", target.name))
525 .arg(format!("-DPROPERTY={}", property))
526 .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
527 if let Some(version) = package.version {
528 command.arg(format!("-DVERSION={}", version));
529 }
530 if let Some(components) = &package.components {
531 command.arg(format!("-DCOMPONENTS={}", components.join(";")));
532 }
533 if let Some(names) = &package.names {
534 command.arg(format!("-DNAMES={}", names.join(";")));
535 }
536 command.output().ok()?;
537
538 let reader = std::fs::File::open(&output_file).ok()?;
540 let property_result: TargetProperty = serde_json::from_reader(reader)
541 .map_err(|e| {
542 eprintln!("Failed to parse target property JSON: {:?}", e);
543 })
544 .ok()?;
545
546 match property_result.value {
547 Some(PropertyValue::String(value)) => Some(value),
548 Some(PropertyValue::Target(_)) => {
549 eprintln!("Returning PropertyValue::Target from target_property not supported");
550 None
551 }
552 None => None,
553 }
554}
555
556#[cfg(test)]
557mod testing {
558 use scopeguard::{guard, ScopeGuard};
559 use serial_test::serial;
560
561 use super::*;
562
563 #[test]
564 fn from_target() {
565 let target = Target {
566 name: "my_target".to_string(),
567 location: Some("/path/to/target.so".to_string()),
568 interface_compile_definitions: Some(vec!["DEFINE1".to_string(), "DEFINE2".to_string()]),
569 interface_compile_options: Some(vec!["-O2".to_string(), "-Wall".to_string()]),
570 interface_include_directories: Some(vec!["/path/to/include".to_string()]),
571 interface_link_directories: Some(vec!["/path/to/lib".to_string()]),
572 interface_link_options: Some(vec!["-L/path/to/lib".to_string()]),
573 interface_link_libraries: Some(vec![
574 PropertyValue::String("library1".to_string()),
575 PropertyValue::String("library2".to_string()),
576 PropertyValue::Target(Target {
577 name: "dependency".to_string(),
578 location: Some("/path/to/dependency.so".to_string()),
579 interface_compile_definitions: Some(vec!["DEFINE3".to_string()]),
580 interface_compile_options: Some(vec!["-O3".to_string()]),
581 interface_include_directories: Some(vec![
582 "/path/to/dependency/include".to_string()
583 ]),
584 interface_link_directories: Some(vec!["/path/to/dependency/lib".to_string()]),
585 interface_link_options: Some(vec!["-L/path/to/dependency/lib".to_string()]),
586 interface_link_libraries: Some(vec![PropertyValue::String(
587 "dependency_library".to_string(),
588 )]),
589 ..Default::default()
590 }),
591 ]),
592 ..Default::default()
593 };
594
595 let cmake_target: CMakeTarget = target.into_cmake_target(CMakeBuildType::Release);
596
597 assert_eq!(cmake_target.name, "my_target");
598 assert_eq!(
599 cmake_target.compile_definitions,
600 vec!["DEFINE1", "DEFINE2", "DEFINE3"]
601 );
602 assert_eq!(cmake_target.compile_options, vec!["-O2", "-Wall", "-O3"]);
603 assert_eq!(
604 cmake_target.include_directories,
605 vec!["/path/to/dependency/include", "/path/to/include"]
606 );
607 assert_eq!(
608 cmake_target.link_directories,
609 vec!["/path/to/dependency/lib", "/path/to/lib"]
610 );
611 assert_eq!(
612 cmake_target.link_options,
613 vec!["-L/path/to/lib", "-L/path/to/dependency/lib"]
614 );
615 assert_eq!(
616 cmake_target.link_libraries,
617 vec![
618 "/path/to/dependency.so",
619 "/path/to/target.so",
620 "dependency_library",
621 "library1",
622 "library2",
623 ]
624 );
625 }
626
627 #[test]
628 fn from_debug_target() {
629 let target = Target {
630 name: "test_target".to_string(),
631 location: Some("/path/to/target.so".to_string()),
632 location_debug: Some("/path/to/libtarget_debug.so".to_string()),
633 ..Default::default()
634 };
635
636 let cmake_target = target.into_cmake_target(CMakeBuildType::Debug);
637 assert_eq!(
638 cmake_target.link_libraries,
639 vec!["/path/to/libtarget_debug.so"]
640 );
641 }
642
643 #[test]
644 fn from_json() {
645 let json = r#"
646{
647 "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
648 "INTERFACE_LINK_LIBRARIES" :
649 [
650 {
651 "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
652 "LOCATION" : "/usr/lib/libcrypto.so",
653 "NAME" : "OpenSSL::Crypto"
654 }
655 ],
656 "LOCATION" : "/usr/lib/libssl.so",
657 "NAME" : "OpenSSL::SSL"
658}
659"#;
660 let target: Target = serde_json::from_str(json).expect("Failed to parse JSON");
661 assert_eq!(target.name, "OpenSSL::SSL");
662 assert_eq!(target.location, Some("/usr/lib/libssl.so".to_string()));
663 assert_eq!(
664 target.interface_include_directories,
665 Some(vec!["/usr/include".to_string()])
666 );
667 assert!(target.interface_link_libraries.is_some());
668 assert_eq!(target.interface_link_libraries.as_ref().unwrap().len(), 1);
669 let sub_target = target
670 .interface_link_libraries
671 .as_ref()
672 .unwrap()
673 .first()
674 .unwrap();
675 match sub_target {
676 PropertyValue::Target(sub_target) => {
677 assert_eq!(sub_target.name, "OpenSSL::Crypto");
678 assert_eq!(
679 sub_target.location,
680 Some("/usr/lib/libcrypto.so".to_string())
681 );
682 }
683 _ => panic!("Expected PropertyValue::Target"),
684 }
685 }
686
687 fn clear_env(name: &'static str) -> ScopeGuard<(), impl FnOnce(())> {
688 let value = std::env::var(name);
689 std::env::remove_var(name);
690 guard((), move |_| {
691 if let Ok(value) = value {
692 std::env::set_var(name, value);
693 } else {
694 std::env::remove_var(name);
695 }
696 })
697 }
698
699 #[test]
700 #[serial]
701 fn test_build_type() {
702 let _profile = clear_env("PROFILE");
703 let _debug = clear_env("DEBUG");
704 let _opt_level = clear_env("OPT_LEVEL");
705
706 assert_eq!(build_type(), CMakeBuildType::Debug);
707
708 std::env::set_var("PROFILE", "release");
709 assert_eq!(build_type(), CMakeBuildType::Release);
710
711 std::env::set_var("DEBUG", "1");
712 assert_eq!(build_type(), CMakeBuildType::RelWithDebInfo);
713
714 std::env::set_var("DEBUG", "0");
715 std::env::set_var("OPT_LEVEL", "s");
716 assert_eq!(build_type(), CMakeBuildType::MinSizeRel);
717 }
718}