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