conan_build/
lib.rs

1use serde_json::Value;
2use std::{
3    collections::HashMap,
4    fs::File,
5    io,
6    ops::Deref,
7    path::{Path, PathBuf},
8};
9
10const BUILD_INFO: &str = "conanbuildinfo.json";
11
12pub struct BuildInfoSet {
13    info: HashMap<&'static str, BuildInfo>,
14}
15impl BuildInfoSet {
16    pub fn find_all() -> io::Result<Self> {
17        let current_dir = std::env::current_dir()?;
18        let info = Self::path_from_filesystem(&current_dir)
19            .chain(Self::path_from_env())
20            .filter(|path| path.exists())
21            .map(|path| BuildInfo::read_build_info(&path).map_err(|e| (path, e)))
22            .filter_map(|r| {
23                r.map(|info| (info.target(), info))
24                    .map_err(|(path, e)| eprintln!("Error opening {path:?}: {e}"))
25                    .ok()
26            })
27            .collect();
28
29        Ok(Self { info })
30    }
31
32    pub fn path_from_env() -> impl Iterator<Item = PathBuf> {
33        std::env::vars().filter_map(|(key, path)| match key.split('_').next_back() {
34            Some("CONANBUILDINFO") => Some(path.into()),
35            None if key == "CONANBUILDINFO" => Some(path.into()),
36            _ => None,
37        })
38    }
39
40    pub fn path_from_filesystem(current_dir: &Path) -> impl Iterator<Item = PathBuf> + use<'_> {
41        current_dir
42            .ancestors()
43            .collect::<Vec<_>>()
44            .into_iter()
45            .rev()
46            .flat_map(|dir| {
47                dir.read_dir()
48                    .unwrap()
49                    .map(|path| path.expect("read dir may not fail").path().join(BUILD_INFO))
50                    .chain([dir.join(BUILD_INFO)])
51            })
52    }
53
54    pub fn get_current_target(&self, host: &str) -> Option<&BuildInfo> {
55        self.info.get(host)
56    }
57
58    pub fn all_targets<'a>(
59        &'a self,
60        host: &'a str,
61    ) -> impl Iterator<Item = (bool, &'a BuildInfo)> + use<'a> {
62        self.info.iter().map(move |(&target, info)| {
63            let is_host = target == host;
64
65            (is_host, info)
66        })
67    }
68
69    pub fn targets_and_paths(&self) -> impl Iterator<Item = (&'static str, &Path)> + use<'_> {
70        self.info
71            .iter()
72            .map(|(key, info)| (*key, info.path.deref()))
73    }
74}
75
76pub struct BuildInfo {
77    path: PathBuf,
78    info: HashMap<String, Value>,
79    libs: HashMap<String, Link>,
80    settings: Value,
81}
82impl BuildInfo {
83    pub fn read_build_info<P: AsRef<Path>>(path: P) -> io::Result<Self> {
84        let info: Value = serde_json::from_str(&std::fs::read_to_string(path.as_ref())?)
85            .expect("Invalid build info json");
86
87        let settings = info["settings"].clone();
88        let info = crate::build_info(&info);
89        let libs = crate::find_all_libs(info.iter())?;
90
91        Ok(Self {
92            path: path.as_ref().to_owned(),
93            info,
94            libs,
95            settings,
96        })
97    }
98
99    pub fn target(&self) -> &'static str {
100        Self::target_from_arch_and_os(self.arch(), self.os())
101    }
102
103    pub fn arch(&self) -> &str {
104        self.settings["arch"]
105            .as_str()
106            .expect("arch in settings must be present")
107    }
108
109    pub fn os(&self) -> &str {
110        self.settings["os"]
111            .as_str()
112            .expect("os in settings must be present")
113    }
114
115    pub fn all_deps(&self) -> impl Iterator<Item = &str> + Clone {
116        self.info.keys().map(String::as_str)
117    }
118
119    pub fn get_depends_on<'a, I: IntoIterator<Item = &'a str>>(&self, packages: I) -> DependsOn {
120        packages
121            .into_iter()
122            .map(|package| self.get_depends_on_package(package))
123            .fold(DependsOn::default(), |mut a, b| {
124                a.extend(b);
125                a
126            })
127    }
128
129    pub fn get_depends_on_package(&self, package: &str) -> DependsOn {
130        let libs = self
131            .libs_for(package)
132            .into_iter()
133            .map(|name| Lib {
134                is_static: !self.is_shared(name),
135                name: name.to_string(),
136            })
137            .collect();
138        let libdirs = self
139            .libdir_for(package)
140            .into_iter()
141            .map(|dir| LibDir(dir.to_string()))
142            .collect();
143
144        DependsOn { libs, libdirs }
145    }
146
147    pub fn is_shared(&self, lib: &str) -> bool {
148        self.libs.get(lib).copied().unwrap_or(Link::Shared) == Link::Shared
149    }
150
151    pub fn libdir_for(&self, package: &str) -> Vec<&str> {
152        Self::libdir_for_package(self.package(package)).collect()
153    }
154
155    pub fn libdir_for_package(value: &Value) -> impl Iterator<Item = &str> {
156        value["lib_paths"]
157            .as_array()
158            .unwrap()
159            .iter()
160            .map(|lib| lib.as_str().unwrap())
161    }
162
163    pub fn libs_for(&self, package: &str) -> Vec<&str> {
164        self.package(package)["libs"]
165            .as_array()
166            .unwrap()
167            .iter()
168            .map(|lib| lib.as_str().unwrap())
169            .collect()
170    }
171
172    pub fn includes_for(&self, package: &str) -> Vec<&str> {
173        self.package(package)["include_paths"]
174            .as_array()
175            .unwrap()
176            .iter()
177            .map(|json| json.as_str().unwrap())
178            .collect()
179    }
180
181    pub fn bindir_for(&self, package: &str) -> Vec<&str> {
182        self.package(package)["bin_paths"]
183            .as_array()
184            .unwrap()
185            .iter()
186            .map(|lib| lib.as_str().unwrap())
187            .collect()
188    }
189
190    pub fn rootpath_for(&self, package: &str) -> &str {
191        self.package(package)["rootpath"].as_str().unwrap()
192    }
193
194    pub fn package(&self, package: &str) -> &Value {
195        self.try_package(package)
196            .unwrap_or_else(|| panic!("No dependency {package:?} in conan info"))
197    }
198
199    pub fn try_package(&self, package: &str) -> Option<&Value> {
200        self.info.get(package)
201    }
202
203    pub fn write_env_source<W1, W2>(&self, is_host: bool, mut sh: W1, mut ps1: W2) -> io::Result<()>
204    where
205        W1: io::Write,
206        W2: io::Write,
207    {
208        let prefix = self.target().replace('-', "_");
209
210        writeln!(
211            sh,
212            "export {prefix}_CONANBUILDINFO={}",
213            self.path.to_string_lossy()
214        )?;
215        writeln!(
216            ps1,
217            "$env:{prefix}_CONANBUILDINFO=\"{}\"",
218            self.path.to_string_lossy()
219        )?;
220
221        let shared_deps = self.all_deps().filter(|package| {
222            self.libs_for(package)
223                .into_iter()
224                .any(|lib| self.is_shared(lib))
225        });
226
227        let libdirs = shared_deps
228            .clone()
229            .flat_map(|package| self.libdir_for(package).into_iter())
230            .collect::<Vec<_>>()
231            .join(":");
232
233        if !libdirs.is_empty() && is_host {
234            writeln!(sh, "export LD_LIBRARY_PATH={libdirs}")?;
235        }
236        let bindirs = shared_deps
237            .flat_map(|package| self.bindir_for(package))
238            .collect::<Vec<_>>()
239            .join(";")
240            .replace('\\', "\\\\");
241
242        if !bindirs.is_empty() && is_host {
243            writeln!(ps1, "$env:PATH=\"{bindirs};$env:PATH\"")?;
244        }
245
246        if self.try_package("openssl").is_some() {
247            let openssl_dir = self.rootpath_for("openssl");
248            writeln!(sh, "export {prefix}_OPENSSL_DIR={openssl_dir}",)?;
249            writeln!(ps1, "$env:{prefix}_OPENSSL_DIR=\"{openssl_dir}\"")?;
250
251            if is_host {
252                writeln!(sh, "export OPENSSL_DIR={openssl_dir}",)?;
253                writeln!(ps1, "$env:OPENSSL_DIR=\"{openssl_dir}\"")?;
254            }
255        }
256
257        sh.flush()?;
258        ps1.flush()?;
259
260        Ok(())
261    }
262
263    pub fn libcxx(&self) -> impl Iterator<Item = Lib> + use<'_> {
264        let main = self.libcxx_name();
265
266        let android_abi = self.libcxx_name().into_iter().filter_map(|name| {
267            (name == "c++_static" && self.os() == "Android").then_some("c++abi")
268        });
269
270        main.into_iter().chain(android_abi).map(|name| Lib {
271            is_static: false,
272            name: name.to_string(),
273        })
274    }
275
276    fn libcxx_name(&self) -> Option<&str> {
277        let libcxx = self
278            .settings
279            .as_object()
280            .expect("settings attribute is an object")
281            .get("compiler.libcxx")?
282            .as_str()
283            .expect("compiler.libcxx attribute is an string");
284
285        Some(match libcxx {
286            "libstdc++11" => "stdc++",
287            x if x.starts_with("lib") => &x[3..],
288            x => x,
289        })
290    }
291
292    fn target_from_arch_and_os(arch: &str, os: &str) -> &'static str {
293        match os {
294            "Linux" => match arch {
295                "x86_64" => "x86_64-unknown-linux-gnu",
296                "x86" => "i686-unknown-linux-gnu",
297                arch => unimplemented!("Unsupported architecture {arch:?}/{os:?}"),
298            },
299            "Windows" => match arch {
300                "x86_64" => "x86_64-pc-windows-msvc",
301                "x86" => "i686-pc-windows-msvc",
302                arch => unimplemented!("Unsupported architecture {arch:?}/{os:?}"),
303            },
304            "Macos" => match arch {
305                "armv8" => "aarch64-apple-darwin",
306                "x86_64" => "x86_64-apple-darwin",
307                arch => unimplemented!("Unsupported architecture {arch:?}/{os:?}"),
308            },
309            "iOS" => match arch {
310                "armv8" => "aarch64-apple-ios",
311                arch => unimplemented!("Unsupported architecture {arch:?}/{os:?}"),
312            },
313            "Android" => match arch {
314                "armv8" => "aarch64-linux-android",
315                "armv7" => "armv7-linux-androideabi",
316                "x86" => "i686-linux-android",
317                "x86_64" => "x86_64-linux-android",
318                arch => unimplemented!("Unsupported architecture {arch:?}/{os:?}"),
319            },
320            os => unimplemented!("Unsupported OS {os:?}"),
321        }
322    }
323}
324
325pub struct Conan {
326    build_info_set: BuildInfoSet,
327    host: String,
328    rerun_if_changed: bool,
329}
330impl Default for Conan {
331    fn default() -> Self {
332        Self::new()
333    }
334}
335impl Conan {
336    pub fn new() -> Conan {
337        let host = std::env::var("TARGET").expect("TARGET variable must be set");
338        Self::with_host(host)
339    }
340
341    pub fn with_host(host: String) -> Conan {
342        let build_info_set = BuildInfoSet::find_all().expect("Failure reading conanbuildinfo");
343
344        eprintln!("Targets:");
345        for (target, path) in build_info_set.targets_and_paths() {
346            eprintln!("    {target}: {}", path.to_string_lossy());
347        }
348
349        Conan {
350            build_info_set,
351            host,
352            rerun_if_changed: false,
353        }
354    }
355
356    pub fn mark_rerun_if_changed(&mut self) {
357        if self.rerun_if_changed {
358            return;
359        }
360
361        self.rerun_if_changed = true;
362        let build_info = self.build_info();
363        println!(
364            "cargo:rerun-if-changed={path}",
365            path = build_info.path.to_string_lossy()
366        );
367    }
368
369    pub fn build_info(&self) -> &BuildInfo {
370        self.build_info_set
371            .get_current_target(&self.host)
372            .unwrap_or_else(|| {
373                panic!(
374                    "Could not find build info for {:?}, available are: {:?}",
375                    self.host,
376                    self.build_info_set.info.keys().collect::<Vec<_>>()
377                )
378            })
379    }
380
381    pub fn depends_on<'a, I: IntoIterator<Item = &'a str>>(&mut self, packages: I) {
382        self.mark_rerun_if_changed();
383        let info = self.build_info();
384        DependsOn::extend_all(
385            packages
386                .into_iter()
387                .map(|package| info.get_depends_on_package(package)),
388        )
389        .apply()
390    }
391
392    pub fn depends_on_optional<'a, I: IntoIterator<Item = &'a str>>(&mut self, packages: I) {
393        self.mark_rerun_if_changed();
394        let info = self.build_info();
395        DependsOn::extend_all(
396            packages
397                .into_iter()
398                .filter(|package| info.try_package(package).is_some())
399                .map(|package| info.get_depends_on_package(package)),
400        )
401        .apply()
402    }
403
404    pub fn depends_on_libcxx(&mut self) {
405        self.mark_rerun_if_changed();
406        for cxx in self.build_info().libcxx() {
407            cxx.apply();
408        }
409    }
410
411    pub fn generate_env_source(&self) {
412        let mut sh = File::create("env.sh").unwrap();
413        let mut ps1 = File::create("env.ps1").unwrap();
414
415        for (is_host, info) in self.build_info_set.all_targets(&self.host) {
416            info.write_env_source(is_host, &mut sh, &mut ps1).unwrap();
417        }
418    }
419
420    pub fn package_is_shared(options: &HashMap<String, String>, package: &str) -> Option<bool> {
421        let option = format!("{package}:shared");
422
423        let r = options.get(&option)?.to_lowercase().parse().unwrap();
424
425        Some(r)
426    }
427}
428
429#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
430pub enum Link {
431    Static,
432    Shared,
433}
434
435fn build_info(root: &Value) -> HashMap<String, Value> {
436    root["dependencies"]
437        .as_array()
438        .unwrap()
439        .iter()
440        .map(|dep| {
441            let name = dep["name"].as_str().unwrap().to_string();
442            (name, dep.clone())
443        })
444        .collect()
445}
446
447fn find_all_libs<'a, I>(it: I) -> io::Result<HashMap<String, Link>>
448where
449    I: Iterator<Item = (&'a String, &'a Value)>,
450{
451    let mut result = HashMap::new();
452    for (_, v) in it {
453        for path in BuildInfo::libdir_for_package(v) {
454            let libs = Path::new(path)
455                .read_dir()
456                .map_err(|e| {
457                    io::Error::new(e.kind(), format!("Failure reading dir {path:?}: {e}"))
458                })?
459                .filter_map(|entry| {
460                    let entry = match entry {
461                        Ok(entry) => entry,
462                        Err(e) => return Some(Err(e)),
463                    };
464
465                    let lib = entry.file_name().to_string_lossy().into_owned();
466                    if lib.ends_with(".lib") {
467                        let lib = &lib[..lib.len() - 4];
468
469                        let mut dll = entry.path();
470                        dll.pop();
471                        dll.pop();
472                        dll.push("bin");
473                        dll.push(format!("{lib}.dll"));
474
475                        let link = match dll.exists() {
476                            true => Link::Shared,
477                            false => Link::Static,
478                        };
479
480                        return Some(Ok((lib.to_string(), link)));
481                    }
482
483                    let link;
484                    if lib.ends_with(".so") {
485                        link = Link::Shared;
486                    } else if lib.ends_with(".a") {
487                        link = Link::Static;
488                    } else {
489                        return None;
490                    }
491
492                    let lib = match lib.starts_with("lib") {
493                        true => &lib[3..],
494                        false => &lib,
495                    };
496                    let ext = lib.rfind('.');
497                    let lib = match ext {
498                        Some(ext) => &lib[..ext],
499                        None => lib,
500                    };
501
502                    Some(Ok((lib.to_string(), link)))
503                });
504
505            for lib_r in libs {
506                let (key, lib) = lib_r?;
507                result.insert(key, lib);
508            }
509        }
510    }
511
512    Ok(result)
513}
514
515pub trait Applyable {
516    fn apply(&self);
517}
518
519pub struct Lib {
520    pub is_static: bool,
521    pub name: String,
522}
523impl Applyable for Lib {
524    fn apply(&self) {
525        let name = &self.name;
526        let is_static = self.is_static;
527
528        let static_ = match is_static {
529            true => "static=",
530            false => "",
531        };
532
533        println!("cargo:rustc-link-lib={static_}{name}");
534    }
535}
536
537pub struct LibDir(pub String);
538impl Applyable for LibDir {
539    fn apply(&self) {
540        println!("cargo:rustc-link-search={dir}", dir = self.0);
541    }
542}
543
544#[derive(Default)]
545pub struct DependsOn {
546    pub libs: Vec<Lib>,
547    pub libdirs: Vec<LibDir>,
548}
549impl DependsOn {
550    pub fn extend(&mut self, rhs: DependsOn) {
551        self.libs.extend(rhs.libs);
552        self.libdirs.extend(rhs.libdirs);
553    }
554
555    fn extend_all<I: IntoIterator<Item = DependsOn>>(iter: I) -> DependsOn {
556        iter.into_iter()
557            .reduce(|mut a, b| {
558                a.extend(b);
559                a
560            })
561            .unwrap_or_default()
562    }
563}
564impl Applyable for DependsOn {
565    fn apply(&self) {
566        self.libs.iter().for_each(Applyable::apply);
567        self.libdirs.iter().for_each(Applyable::apply);
568    }
569}