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