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