xmake/
lib.rs

1//! A build dependency for running `xmake` to build a native library
2//!
3//! This crate provides some necessary boilerplate and shim support for running
4//! the system `xmake` command to build a native library.
5//!
6//! The builder-style configuration allows for various variables and such to be
7//! passed down into the build as well.
8//!
9//! ## Installation
10//!
11//! Add this to your `Cargo.toml`:
12//!
13//! ```toml
14//! [build-dependencies]
15//! xmake = "0.3.3"
16//! ```
17//!
18//! ## Examples
19//!
20//! ```no_run
21//! use xmake;
22//!
23//! // Builds the project in the directory located in `libfoo`, and link it
24//! xmake::build("libfoo");
25//! ```
26//!
27//! ```no_run
28//! use xmake::Config;
29//!
30//! Config::new("libfoo")
31//!        .option("bar", "true")
32//!        .env("XMAKE", "path/to/xmake")
33//!        .build();
34//! ```
35#![deny(missing_docs)]
36
37use std::collections::{HashMap, HashSet};
38use std::env;
39use std::io::{BufRead, BufReader, ErrorKind};
40use std::path::{Path, PathBuf};
41use std::process::{Command, Stdio};
42use std::str::FromStr;
43
44// The version of xmake that is required for this crate to work.
45// https://github.com/xmake-io/xmake/releases/tag/v2.9.6
46const XMAKE_MINIMUM_VERSION: Version = Version::new(2, 9, 6);
47
48/// Represents the different kinds of linkage for a library.
49///
50/// The `LinkKind` enum represents the different ways a library can be linked:
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum LinkKind {
53    /// The library is statically linked, meaning its code is included directly in the final binary.
54    Static,
55    /// The library is dynamically linked, meaning the final binary references the library at runtime.
56    Dynamic,
57    /// The library is a system library, meaning it is provided by the operating system and not included in the final binary.
58    System,
59    /// The library is a framework, like [`LinkKind::System`], it is provided by the operating system but used only on macos.
60    Framework,
61    /// The library is unknown, meaning its kind could not be determined.
62    Unknown,
63}
64
65/// Represents the source when querying some information from [`BuildInfo`].
66pub enum Source {
67    /// Coming from an xmake target
68    Target,
69    /// Coming from an xmake package
70    Package,
71    /// Both of them
72    Both,
73}
74
75/// Represents a single linked library.
76///
77/// The `Link` struct contains information about a single linked library, including its name and the kind of linkage.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct Link {
80    /// The name of the linked library.
81    name: String,
82    /// The kind of linkage for the library.
83    kind: LinkKind,
84}
85
86/// Represents the link information for a build.
87///
88/// The `BuildLinkInfo` struct contains information about the libraries that are linked in a build, including the directories they are located in and the individual `Link` structs.
89#[derive(Default)]
90pub struct BuildInfo {
91    /// The directories that contain the linked libraries.
92    linkdirs: Vec<PathBuf>,
93    /// The individual linked libraries.
94    links: Vec<Link>,
95    /// All the includirs coming from the packages
96    includedirs_package: HashMap<String, Vec<PathBuf>>,
97    /// All the includirs coming from the targets
98    includedirs_target: HashMap<String, Vec<PathBuf>>,
99    /// Whether the build uses the C++.
100    use_cxx: bool,
101    /// Whether the build uses the C++ standard library.
102    use_stl: bool,
103}
104
105/// Represents errors that can occur when parsing a string to it's `BuildInfo` representation.
106#[derive(Debug, PartialEq, Eq)]
107pub enum ParsingError {
108    /// Given kind did not match any of the `LinkKind` variants.
109    InvalidKind,
110    /// Missing at least one key to construct `BuildInfo`.
111    MissingKey,
112    /// Link string is malformed.
113    MalformedLink,
114    /// Multiple values when it's not supposed to
115    MultipleValues,
116    /// Error when converting string a type
117    ParseError,
118}
119
120impl Link {
121    /// Returns the name of the library as a string.
122    pub fn name(&self) -> &str {
123        &self.name
124    }
125    /// Returns the kind of linkage for the library.
126    pub fn kind(&self) -> &LinkKind {
127        &self.kind
128    }
129
130    /// Creates a new `Link` with the given name and kind.
131    pub fn new(name: &str, kind: LinkKind) -> Link {
132        Link {
133            name: name.to_string(),
134            kind: kind,
135        }
136    }
137}
138
139impl BuildInfo {
140    /// Returns the directories that contain the linked libraries.
141    pub fn linkdirs(&self) -> &[PathBuf] {
142        &self.linkdirs
143    }
144
145    /// Returns the individual linked libraries.
146    pub fn links(&self) -> &[Link] {
147        &self.links
148    }
149
150    /// Returns whether the build uses C++.
151    pub fn use_cxx(&self) -> bool {
152        self.use_cxx
153    }
154
155    /// Returns whether the build uses C++ standard library.
156    pub fn use_stl(&self) -> bool {
157        self.use_stl
158    }
159
160    /// Retrieves the include directories for the specific target/package given it's name.
161    /// If `*` is given as a name all the includedirs will be returned.
162    pub fn includedirs<S: AsRef<str>>(&self, source: Source, name: S) -> Vec<PathBuf> {
163        let name = name.as_ref();
164        let mut result = Vec::new();
165
166        let sources = match source {
167            Source::Target => vec![&self.includedirs_target],
168            Source::Package => vec![&self.includedirs_package],
169            Source::Both => vec![&self.includedirs_target, &self.includedirs_package],
170        };
171
172        for map in sources {
173            if name == "*" {
174                result.extend(map.values().cloned().flatten());
175            } else if let Some(dirs) = map.get(name) {
176                result.extend(dirs.clone());
177            }
178        }
179
180        result
181    }
182}
183
184impl FromStr for LinkKind {
185    type Err = ParsingError;
186    fn from_str(s: &str) -> Result<Self, Self::Err> {
187        match s {
188            "static" => Ok(LinkKind::Static),
189            "shared" => Ok(LinkKind::Dynamic),
190            "system" | "syslinks" => Ok(LinkKind::System),
191            "framework" => Ok(LinkKind::Framework),
192            "unknown" => Ok(LinkKind::Unknown),
193            _ => Err(ParsingError::InvalidKind),
194        }
195    }
196}
197
198impl FromStr for Link {
199    type Err = ParsingError;
200    fn from_str(s: &str) -> Result<Self, Self::Err> {
201        const NUMBER_OF_PARTS: usize = 2;
202
203        let parts: Vec<_> = s.split("/").collect();
204        if parts.len() != NUMBER_OF_PARTS {
205            return Err(ParsingError::MalformedLink);
206        }
207
208        let kind_result: LinkKind = parts[1].parse()?;
209        Ok(Link {
210            name: parts[0].to_string(),
211            kind: kind_result,
212        })
213    }
214}
215
216impl FromStr for BuildInfo {
217    type Err = ParsingError;
218    fn from_str(s: &str) -> Result<Self, Self::Err> {
219        let map = parse_info_pairs(s);
220
221        let directories: Vec<PathBuf> = parse_field(&map, "linkdirs")?;
222        let links: Vec<Link> = parse_field(&map, "links")?;
223
224        let use_cxx: bool = parse_field(&map, "cxx_used")?;
225        let use_stl: bool = parse_field(&map, "stl_used")?;
226
227        let packages = subkeys_of(&map, "includedirs_package");
228        let mut includedirs_package = HashMap::new();
229        for package in packages {
230            let dirs: Vec<PathBuf> = parse_field(&map, format!("includedirs_package.{}", package))?;
231            includedirs_package.insert(package.to_string(), dirs);
232        }
233
234        let targets = subkeys_of(&map, "includedirs_target");
235        let mut includedirs_target = HashMap::new();
236        for target in targets {
237            let dirs: Vec<PathBuf> = parse_field(&map, format!("includedirs_target.{}", target))?;
238            includedirs_target.insert(target.to_string(), dirs);
239        }
240
241        Ok(BuildInfo {
242            linkdirs: directories,
243            links: links,
244            use_cxx: use_cxx,
245            use_stl: use_stl,
246            includedirs_package: includedirs_package,
247            includedirs_target: includedirs_target,
248        })
249    }
250}
251
252#[derive(Default)]
253struct ConfigCache {
254    build_info: BuildInfo,
255    plat: Option<String>,
256    arch: Option<String>,
257    xmake_version: Option<Version>,
258    env: HashMap<String, Option<String>>,
259}
260
261impl ConfigCache {
262    /// Returns the platform string for this configuration.
263    /// Panic if the config has not been done yet
264    fn plat(&self) -> &String {
265        return self.plat.as_ref().unwrap();
266    }
267
268    /// Returns the architecture string for this configuration.
269    /// Panic if the config has not been done yet
270    fn arch(&self) -> &String {
271        return self.arch.as_ref().unwrap();
272    }
273}
274
275/// Builder style configuration for a pending XMake build.
276pub struct Config {
277    path: PathBuf,
278    targets: Option<String>,
279    verbose: bool,
280    auto_link: bool,
281    out_dir: Option<PathBuf>,
282    mode: Option<String>,
283    options: Vec<(String, String)>,
284    env: Vec<(String, String)>,
285    static_crt: Option<bool>,
286    runtimes: Option<String>,
287    no_stl_link: bool,
288    cache: ConfigCache,
289}
290
291/// Builds the native library rooted at `path` with the default xmake options.
292/// This will return the directory in which the library was installed.
293///
294/// # Examples
295///
296/// ```no_run
297/// use xmake;
298///
299/// // Builds the project in the directory located in `libfoo`, and link it
300/// xmake::build("libfoo");
301/// ```
302///
303pub fn build<P: AsRef<Path>>(path: P) {
304    Config::new(path.as_ref()).build()
305}
306
307impl Config {
308    /// Creates a new blank set of configuration to build the project specified
309    /// at the path `path`.
310    pub fn new<P: AsRef<Path>>(path: P) -> Config {
311        Config {
312            path: env::current_dir().unwrap().join(path),
313            targets: None,
314            verbose: false,
315            auto_link: true,
316            out_dir: None,
317            mode: None,
318            options: Vec::new(),
319            env: Vec::new(),
320            static_crt: None,
321            runtimes: None,
322            no_stl_link: false,
323            cache: ConfigCache::default(),
324        }
325    }
326
327    /// Sets the xmake targets for this compilation.
328    /// Note: This is different from rust target (os and arch).
329    /// ```
330    /// use xmake::Config;
331    /// let mut config = xmake::Config::new("libfoo");
332    /// config.targets("foo");
333    /// config.targets("foo,bar");
334    /// config.targets(["foo", "bar"]); // You can also pass a Vec<String> or Vec<&str>
335    /// ```
336    pub fn targets<T: CommaSeparated>(&mut self, targets: T) -> &mut Config {
337        self.targets = Some(targets.as_comma_separated());
338        self
339    }
340
341    /// Sets verbose output.
342    pub fn verbose(&mut self, value: bool) -> &mut Config {
343        self.verbose = value;
344        self
345    }
346
347    /// Configures if targets and their dependencies should be linked.
348    /// <div class="warning">Without configuring `no_stl_link`, the C++ standard library will be linked, if used in the project. </div>
349    /// This option defaults to `true`.
350    pub fn auto_link(&mut self, value: bool) -> &mut Config {
351        self.auto_link = value;
352        self
353    }
354
355    /// Configures if the C++ standard library should be linked.
356    ///
357    /// This option defaults to `false`.
358    /// If false and no runtimes options is set, the runtime flag passed to xmake configuration will be not set at all.
359    pub fn no_stl_link(&mut self, value: bool) -> &mut Config {
360        self.no_stl_link = value;
361        self
362    }
363
364    /// Sets the output directory for this compilation.
365    ///
366    /// This is automatically scraped from `$OUT_DIR` which is set for Cargo
367    /// build scripts so it's not necessary to call this from a build script.
368    pub fn out_dir<P: AsRef<Path>>(&mut self, out: P) -> &mut Config {
369        self.out_dir = Some(out.as_ref().to_path_buf());
370        self
371    }
372
373    /// Sets the xmake mode for this compilation.
374    pub fn mode(&mut self, mode: &str) -> &mut Config {
375        self.mode = Some(mode.to_string());
376        self
377    }
378
379    /// Configure an option for the `xmake` processes spawned by
380    /// this crate in the `build` step.
381    pub fn option<K, V>(&mut self, key: K, value: V) -> &mut Config
382    where
383        K: AsRef<str>,
384        V: AsRef<str>,
385    {
386        self.options
387            .push((key.as_ref().to_owned(), value.as_ref().to_owned()));
388        self
389    }
390
391    /// Configure an environment variable for the `xmake` processes spawned by
392    /// this crate in the `build` step.
393    pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Config
394    where
395        K: AsRef<str>,
396        V: AsRef<str>,
397    {
398        self.env
399            .push((key.as_ref().to_owned(), value.as_ref().to_owned()));
400        self
401    }
402
403    /// Configures runtime type (static or not)
404    ///
405    /// This option defaults to `false`.
406    pub fn static_crt(&mut self, static_crt: bool) -> &mut Config {
407        self.static_crt = Some(static_crt);
408        self
409    }
410
411    /// Sets the runtimes to use for this compilation.
412    ///
413    /// This method takes a collection of runtime names, which will be passed to
414    /// the `xmake` command during the build process. The runtimes specified here
415    /// will be used to determine the appropriate C++ standard library to link
416    /// against.
417    /// Common values:
418    /// - `MT`
419    /// - `MTd`
420    /// - `MD`
421    /// - `MDd`
422    /// - `c++_static`
423    /// - `c++_shared`
424    /// - `stdc++_static`
425    /// - `stdc++_shared`
426    /// - `gnustl_static`
427    /// - `gnustl_shared`
428    /// - `stlport_shared`
429    /// - `stlport_static`
430    /// ```
431    /// use xmake::Config;
432    /// let mut config = xmake::Config::new("libfoo");
433    /// config.runtimes("MT,c++_static");
434    /// config.runtimes(["MT", "c++_static"]); // You can also pass a Vec<String> or Vec<&str>
435    /// ```
436    pub fn runtimes<T: CommaSeparated>(&mut self, runtimes: T) -> &mut Config {
437        self.runtimes = Some(runtimes.as_comma_separated());
438        self
439    }
440
441    /// Run this configuration, compiling the library with all the configured
442    /// options.
443    ///
444    /// This will run both the configuration command as well as the
445    /// command to build the library.
446    pub fn build(&mut self) {
447        self.config();
448
449        let mut cmd = self.xmake_command();
450
451        // In case of xmake is waiting to download something
452        cmd.arg("--yes");
453
454        if let Some(targets) = &self.targets {
455            // :: is used to handle namespaces in xmake but it interferes with the env separator
456            // on linux, so we use a different separator
457            cmd.env("XMAKERS_TARGETS", targets.replace("::", "||"));
458        }
459
460        cmd.run_script("build.lua");
461
462        if let Some(info) = self.get_build_info() {
463            self.cache.build_info = info;
464        }
465
466        if self.auto_link {
467            self.link();
468        }
469    }
470
471    /// Returns a reference to the `BuildInfo` associated with this build.
472    /// <div class="warning">Note: Accessing this information before the build step will result in non-representative data.</div>
473    pub fn build_info(&self) -> &BuildInfo {
474        &self.cache.build_info
475    }
476
477    // Run the configuration with all the configured
478    /// options.
479    fn config(&mut self) {
480        self.check_version();
481
482        let mut cmd = self.xmake_command();
483        cmd.task("config");
484
485        // In case of xmake is waiting to download something
486        cmd.arg("--yes");
487
488        let dst = self
489            .out_dir
490            .clone()
491            .unwrap_or_else(|| PathBuf::from(getenv_unwrap("OUT_DIR")));
492
493        cmd.arg(format!("--buildir={}", dst.display()));
494
495        // Cross compilation
496        let host = getenv_unwrap("HOST");
497        let target = getenv_unwrap("TARGET");
498
499        let os = getenv_unwrap("CARGO_CFG_TARGET_OS");
500
501        let plat = self.get_xmake_plat();
502        cmd.arg(format!("--plat={}", plat));
503
504        if host != target {
505            let arch = self.get_xmake_arch();
506            cmd.arg(format!("--arch={}", arch));
507
508            if plat == "android" {
509                if let Ok(ndk) = env::var("ANDROID_NDK_HOME") {
510                    cmd.arg(format!("--ndk={}", ndk));
511                }
512                cmd.arg(format!("--toolchain={}", "ndk"));
513            }
514
515            if plat == "wasm" {
516                if let Ok(emscripten) = env::var("EMSCRIPTEN_HOME") {
517                    cmd.arg(format!("--emsdk={}", emscripten));
518                }
519                cmd.arg(format!("--toolchain={}", "emcc"));
520            }
521
522            if plat == "cross" {
523                let mut c_cfg = cc::Build::new();
524                c_cfg
525                    .cargo_metadata(false)
526                    .opt_level(0)
527                    .debug(false)
528                    .warnings(false)
529                    .host(&host)
530                    .target(&target);
531
532                // Attempt to find the cross compilation sdk
533                // Let cc find it for us
534                // Usually a compiler is inside bin folder and xmake expect the entire
535                // sdk folder
536                let compiler = c_cfg.get_compiler();
537                let sdk = compiler.path().ancestors().nth(2).unwrap();
538
539                cmd.arg(format!("--sdk={}", sdk.display()));
540                cmd.arg(format!("--cross={}-{}", arch, os));
541                cmd.arg(format!("--toolchain={}", "cross"));
542            }
543        }
544
545        // Configure the runtimes
546        if let Some(runtimes) = &self.runtimes {
547            cmd.arg(format!("--runtimes={}", runtimes));
548        } else if let Some(runtimes) = self.get_runtimes() {
549            if !self.no_stl_link {
550                cmd.arg(format!("--runtimes={}", runtimes));
551            }
552        }
553
554        // Compilation mode: release, debug...
555        let mode = self.get_mode();
556        cmd.arg("-m").arg(mode);
557
558        // Option
559        for (key, val) in self.options.iter() {
560            let option = format!("--{}={}", key.clone(), val.clone(),);
561            cmd.arg(option);
562        }
563
564        cmd.run();
565    }
566
567    fn link(&mut self) {
568        let dst = self.install();
569        let plat = self.get_xmake_plat();
570
571        let build_info = &mut self.cache.build_info;
572
573        for directory in build_info.linkdirs() {
574            // Reference: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-search
575            println!("cargo:rustc-link-search=all={}", directory.display());
576        }
577
578        // Special link search path for dynamic libraries, because
579        // the path are appended to the dynamic library search path environment variable
580        // only if there are within OUT_DIR
581        let linux_shared_libs_folder = dst.join("lib");
582        println!(
583            "cargo:rustc-link-search=native={}",
584            linux_shared_libs_folder.display()
585        );
586        println!(
587            "cargo:rustc-link-search=native={}",
588            dst.join("bin").display()
589        );
590
591        build_info.linkdirs.push(linux_shared_libs_folder.clone());
592        build_info.linkdirs.push(dst.join("bin"));
593
594        let mut shared_libs = HashSet::new();
595
596        for link in build_info.links() {
597            match link.kind() {
598                LinkKind::Static => println!("cargo:rustc-link-lib=static={}", link.name()),
599                LinkKind::Dynamic => {
600                    println!("cargo:rustc-link-lib=dylib={}", link.name());
601                    shared_libs.insert(link.name());
602                }
603                LinkKind::Framework if plat == "macosx" => {
604                    println!("cargo:rustc-link-lib=framework={}", link.name())
605                }
606                // For rust, framework type is only for macosx but can be used on multiple system in xmake
607                // so fallback to the system libraries case
608                LinkKind::System | LinkKind::Framework => {
609                    println!("cargo:rustc-link-lib={}", link.name())
610                }
611                // Let try cargo handle the rest
612                LinkKind::Unknown => println!("cargo:rustc-link-lib={}", link.name()),
613            }
614        }
615
616        // In some cases, xmake does not include all the shared libraries in the link cmd (for example, in the sht-shf-shb test),
617        // leading to build failures on the rust side because it expected to link them, so the solution is to fetches all the libs from the install directory.
618        // Since I cannot know the real order of the links, this can cause some problems on some projects.
619        if plat == "linux" && linux_shared_libs_folder.exists() {
620            let files = std::fs::read_dir(dst.join("lib")).unwrap();
621            for entry in files {
622                if let Ok(file) = entry {
623                    let file_name = file.file_name();
624                    let file_name = file_name.to_str().unwrap();
625                    if file_name.ends_with(".so") || file_name.matches(r"\.so\.\d+").count() > 0 {
626                        if let Some(lib_name) = file_name.strip_prefix("lib") {
627                            let name = if let Some(dot_pos) = lib_name.find(".so") {
628                                &lib_name[..dot_pos]
629                            } else {
630                                lib_name
631                            };
632
633                            if !shared_libs.contains(name) {
634                                println!("cargo:rustc-link-lib=dylib={}", name);
635                            }
636                        }
637                    }
638                }
639            }
640        }
641
642        if !self.no_stl_link && self.build_info().use_stl() {
643            if let Some(runtimes) = &self.runtimes {
644                let plat = self.cache.plat();
645
646                let stl: Option<&[&str]> = match plat.as_str() {
647                    "linux" => {
648                        Some(&["c++_static", "c++_shared", "stdc++_static", "stdc++_shared"])
649                    }
650                    "android" => Some(&[
651                        "c++_static",
652                        "c++_shared",
653                        "gnustl_static",
654                        "gnustl_shared",
655                        "stlport_static",
656                        "stlport_shared",
657                    ]),
658                    _ => None,
659                };
660
661                if let Some(stl) = stl {
662                    // Try to match the selected runtime with the available runtimes
663                    for runtime in runtimes.split(",") {
664                        if stl.contains(&runtime) {
665                            let (name, _) = runtime.split_once("_").unwrap();
666                            let kind = match runtime.contains("static") {
667                                true => "static",
668                                false => "dylib",
669                            };
670                            println!(r"cargo:rustc-link-lib={}={}", kind, name);
671                            break;
672                        }
673                    }
674                }
675            } else {
676                if let Some(runtime) = self.get_runtimes() {
677                    let (name, _) = runtime.split_once("_").unwrap();
678                    let kind = match runtime.contains("static") {
679                        true => "static",
680                        false => "dylib",
681                    };
682                    println!(r"cargo:rustc-link-lib={}={}", kind, name);
683                }
684            }
685        }
686    }
687
688    /// Install target in OUT_DIR.
689    fn install(&mut self) -> PathBuf {
690        let mut cmd = self.xmake_command();
691
692        let dst = self
693            .out_dir
694            .clone()
695            .unwrap_or_else(|| PathBuf::from(getenv_unwrap("OUT_DIR")));
696
697        cmd.env("XMAKERS_INSTALL_DIR", dst.clone());
698        cmd.run_script("install.lua");
699        dst
700    }
701
702    fn get_build_info(&mut self) -> Option<BuildInfo> {
703        let mut cmd = self.xmake_command();
704
705        if let Some(targets) = &self.targets {
706            // :: is used to handle namespaces in xmake but it interferes with the env separator
707            // on linux, so use a different separator
708            cmd.env("XMAKERS_TARGETS", targets.replace("::", "||"));
709        }
710
711        if let Some(output) = cmd.run_script("build_info.lua") {
712            return output.parse().ok();
713        }
714        None
715    }
716
717    fn get_static_crt(&self) -> bool {
718        return self.static_crt.unwrap_or_else(|| {
719            let feature = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or(String::new());
720            if feature.contains("crt-static") {
721                true
722            } else {
723                false
724            }
725        });
726    }
727
728    // In case no runtimes has been set, get one
729    fn get_runtimes(&mut self) -> Option<String> {
730        // These runtimes may not be the most appropriate for each platform, but
731        // taken the GNU standard libary is the most common one on linux, and same for
732        // the clang equivalent on android.
733        // TODO Explore which runtimes is more approriate for macosx
734        let static_crt = self.get_static_crt();
735        let platform = self.get_xmake_plat();
736
737        let kind = match static_crt {
738            true => "static",
739            false => "shared",
740        };
741
742        match platform.as_str() {
743            "linux" => Some(format!("stdc++_{}", kind)),
744            "android" => Some(format!("c++_{}", kind)),
745            "windows" => {
746                let msvc_runtime = if static_crt { "MT" } else { "MD" };
747                Some(msvc_runtime.to_owned())
748            }
749            _ => None,
750        }
751    }
752
753    /// Convert rust platform to xmake one
754    fn get_xmake_plat(&mut self) -> String {
755        if let Some(ref plat) = self.cache.plat {
756            return plat.clone();
757        }
758
759        // List of xmake platform https://github.com/xmake-io/xmake/tree/master/xmake/platforms
760        // Rust targets: https://doc.rust-lang.org/rustc/platform-support.html
761        let plat = match self.getenv_os("CARGO_CFG_TARGET_OS").unwrap().as_str() {
762            "windows" => Some("windows"),
763            "linux" => Some("linux"),
764            "android" => Some("android"),
765            "androideabi" => Some("android"),
766            "emscripten" => Some("wasm"),
767            "macos" => Some("macosx"),
768            "ios" => Some("iphoneos"),
769            "tvos" => Some("appletvos"),
770            "fuchsia" => None,
771            "solaris" => None,
772            _ if getenv_unwrap("CARGO_CFG_TARGET_FAMILY") == "wasm" => Some("wasm"),
773            _ => Some("cross"),
774        }
775        .expect("unsupported rust target");
776
777        self.cache.plat = Some(plat.to_string());
778        self.cache.plat.clone().unwrap()
779    }
780
781    fn get_xmake_arch(&mut self) -> String {
782        if let Some(ref arch) = self.cache.arch {
783            return arch.clone();
784        }
785
786        // List rust targets with rustc --print target-list
787        let os = self.getenv_os("CARGO_CFG_TARGET_OS").unwrap();
788        let target_arch = self.getenv_os("CARGO_CFG_TARGET_ARCH").unwrap();
789        let plat = self.get_xmake_plat();
790
791        // From v2.9.9 (not released) onwards, XMake used arm64 instead of arm64-v8a
792        let arm64_changes = self
793            .cache
794            .xmake_version
795            .as_ref()
796            .unwrap_or(&XMAKE_MINIMUM_VERSION)
797            < &Version::new(2, 9, 9);
798
799        let arch = match (plat.as_str(), target_arch.as_str()) {
800            ("android", a) if os == "androideabi" => match a {
801                "arm" => "armeabi", // TODO Check with cc-rs if it's true
802                "armv7" => "armeabi-v7a",
803                a => a,
804            },
805            ("android", "aarch64") => "arm64-v8a",
806            ("android", "i686") => "x86",
807            ("linux", "loongarch64") => "loong64",
808            // From v2.9.9 (not released) onwards, XMake used arm64 instead of arm64-v8a
809            ("linux", "aarch64") if arm64_changes => "arm64-v8a",
810            ("watchos", "arm64_32") => "armv7k",
811            ("watchos", "armv7k") => "armv7k",
812            ("iphoneos", "aarch64") => "arm64",
813            ("macosx", "aarch64") => "arm64",
814            ("windows", "i686") => "x86",
815            (_, "aarch64") => "arm64",
816            (_, "i686") => "i386",
817            (_, a) => a,
818        }
819        .to_string();
820
821        self.cache.arch = Some(arch);
822        self.cache.arch.clone().unwrap()
823    }
824
825    /// Return xmake mode or inferred from Rust's compilation profile.
826    ///
827    /// * if `opt-level=0` then `debug`,
828    /// * if `opt-level={1,2,3}` and:
829    ///   * `debug=false` then `release`
830    ///   * otherwise `releasedbg`
831    /// * if `opt-level={s,z}` then `minsizerel`
832    fn get_mode(&self) -> &str {
833        if let Some(profile) = self.mode.as_ref() {
834            profile
835        } else {
836            #[derive(PartialEq)]
837            enum RustProfile {
838                Debug,
839                Release,
840            }
841            #[derive(PartialEq, Debug)]
842            enum OptLevel {
843                Debug,
844                Release,
845                Size,
846            }
847
848            let rust_profile = match &getenv_unwrap("PROFILE")[..] {
849                "debug" => RustProfile::Debug,
850                "release" | "bench" => RustProfile::Release,
851                unknown => {
852                    eprintln!(
853                        "Warning: unknown Rust profile={}; defaulting to a release build.",
854                        unknown
855                    );
856                    RustProfile::Release
857                }
858            };
859
860            let opt_level = match &getenv_unwrap("OPT_LEVEL")[..] {
861                "0" => OptLevel::Debug,
862                "1" | "2" | "3" => OptLevel::Release,
863                "s" | "z" => OptLevel::Size,
864                unknown => {
865                    let default_opt_level = match rust_profile {
866                        RustProfile::Debug => OptLevel::Debug,
867                        RustProfile::Release => OptLevel::Release,
868                    };
869                    eprintln!(
870                        "Warning: unknown opt-level={}; defaulting to a {:?} build.",
871                        unknown, default_opt_level
872                    );
873                    default_opt_level
874                }
875            };
876
877            let debug_info: bool = match &getenv_unwrap("DEBUG")[..] {
878                "false" => false,
879                "true" => true,
880                unknown => {
881                    eprintln!("Warning: unknown debug={}; defaulting to `true`.", unknown);
882                    true
883                }
884            };
885
886            match (opt_level, debug_info) {
887                (OptLevel::Debug, _) => "debug",
888                (OptLevel::Release, false) => "release",
889                (OptLevel::Release, true) => "releasedbg",
890                (OptLevel::Size, _) => "minsizerel",
891            }
892        }
893    }
894
895    fn check_version(&mut self) {
896        let version = Version::from_command();
897        if version.is_none() {
898            println!("cargo:warning=xmake version could not be determined, it might not work");
899            return;
900        }
901
902        let version = version.unwrap();
903        if version < XMAKE_MINIMUM_VERSION {
904            panic!(
905                "xmake version {:?} is too old, please update to at least {:?}",
906                version, XMAKE_MINIMUM_VERSION
907            );
908        }
909        self.cache.xmake_version = Some(version);
910    }
911
912    fn xmake_command(&mut self) -> XmakeCommand {
913        let mut cmd = XmakeCommand::new();
914
915        // Add envs
916        for &(ref k, ref v) in self.env.iter().chain(&self.env) {
917            cmd.env(k, v);
918        }
919
920        if self.verbose {
921            cmd.verbose(true);
922        }
923
924        cmd.project_dir(self.path.as_path());
925
926        cmd
927    }
928
929    fn getenv_os(&mut self, v: &str) -> Option<String> {
930        if let Some(val) = self.cache.env.get(v) {
931            return val.clone();
932        }
933
934        let r = env::var(v).ok();
935        println!("{} = {:?}", v, r);
936        self.cache.env.insert(v.to_string(), r.clone());
937        r
938    }
939}
940
941trait CommaSeparated {
942    fn as_comma_separated(self) -> String;
943}
944
945impl<const N: usize> CommaSeparated for [&str; N] {
946    fn as_comma_separated(self) -> String {
947        self.join(",")
948    }
949}
950
951impl CommaSeparated for Vec<String> {
952    fn as_comma_separated(self) -> String {
953        self.join(",")
954    }
955}
956
957impl CommaSeparated for Vec<&str> {
958    fn as_comma_separated(self) -> String {
959        self.join(",")
960    }
961}
962
963impl CommaSeparated for String {
964    fn as_comma_separated(self) -> String {
965        self
966    }
967}
968
969impl CommaSeparated for &str {
970    fn as_comma_separated(self) -> String {
971        self.to_string()
972    }
973}
974
975/// Parses a string representation of a map of key-value pairs, where the values are
976/// separated by the '|' character.
977///
978/// The input string is expected to be in the format "key:value1|value2|...|valueN",
979/// where the values are separated by the '|' character. Any empty values are
980/// filtered out.
981///
982fn parse_info_pairs<S: AsRef<str>>(s: S) -> HashMap<String, Vec<String>> {
983    let str: String = s.as_ref().trim().to_string();
984    let mut map: HashMap<String, Vec<String>> = HashMap::new();
985
986    for l in str.lines() {
987        // Split between key values
988        if let Some((key, values)) = l.split_once(":") {
989            let v: Vec<_> = values
990                .split('|')
991                .map(|x| x.to_string())
992                .filter(|s| !s.is_empty())
993                .collect();
994            map.insert(key.to_string(), v);
995        }
996    }
997    map
998}
999
1000fn subkeys_of<S: AsRef<str>>(map: &HashMap<String, Vec<String>>, main_key: S) -> Vec<&str> {
1001    let main_key = main_key.as_ref();
1002    let prefix = format!("{main_key}.");
1003    map.keys().filter_map(|k| k.strip_prefix(&prefix)).collect()
1004}
1005
1006// This trait may be replaced by the unstable auto trait feature
1007// References:
1008// https://users.rust-lang.org/t/how-to-exclude-a-type-from-generic-trait-implementation/26156/9
1009// https://doc.rust-lang.org/beta/unstable-book/language-features/auto-traits.html
1010// https://doc.rust-lang.org/beta/unstable-book/language-features/negative-impls.html
1011trait DirectParse {}
1012
1013// Implement for all primitive types that should use the scalar implementation
1014impl DirectParse for bool {}
1015impl DirectParse for u32 {}
1016impl DirectParse for String {}
1017
1018trait ParseField<T> {
1019    fn parse_field<S: AsRef<str>>(
1020        map: &HashMap<String, Vec<String>>,
1021        field: S,
1022    ) -> Result<T, ParsingError>;
1023}
1024
1025// Only implement for types that implement DirectParse
1026impl<T> ParseField<T> for T
1027where
1028    T: FromStr + DirectParse,
1029{
1030    fn parse_field<S: AsRef<str>>(
1031        map: &HashMap<String, Vec<String>>,
1032        field: S,
1033    ) -> Result<T, ParsingError> {
1034        let field = field.as_ref();
1035        let values = map.get(field).ok_or(ParsingError::MissingKey)?;
1036        if values.len() > 1 {
1037            return Err(ParsingError::MultipleValues);
1038        }
1039
1040        let parsed: Vec<T> = values
1041            .iter()
1042            .map(|s| s.parse::<T>().map_err(|_| ParsingError::ParseError))
1043            .collect::<Result<Vec<T>, ParsingError>>()?;
1044        parsed.into_iter().next().ok_or(ParsingError::MissingKey)
1045    }
1046}
1047
1048// Vector implementation remains unchanged
1049impl<T> ParseField<Vec<T>> for Vec<T>
1050where
1051    T: FromStr,
1052{
1053    fn parse_field<S: AsRef<str>>(
1054        map: &HashMap<String, Vec<String>>,
1055        field: S,
1056    ) -> Result<Vec<T>, ParsingError> {
1057        let field = field.as_ref();
1058        let values = map.get(field).ok_or(ParsingError::MissingKey)?;
1059        values
1060            .iter()
1061            .map(|s| s.parse::<T>().map_err(|_| ParsingError::ParseError))
1062            .collect::<Result<Vec<T>, ParsingError>>()
1063    }
1064}
1065
1066fn parse_field<T, S: AsRef<str>>(
1067    map: &HashMap<String, Vec<String>>,
1068    field: S,
1069) -> Result<T, ParsingError>
1070where
1071    T: ParseField<T>,
1072{
1073    T::parse_field(map, field)
1074}
1075
1076fn getenv_unwrap(v: &str) -> String {
1077    match env::var(v) {
1078        Ok(s) => s,
1079        Err(..) => fail(&format!("environment variable `{}` not defined", v)),
1080    }
1081}
1082
1083fn fail(s: &str) -> ! {
1084    panic!("\n{}\n\nbuild script failed, must exit now", s)
1085}
1086
1087struct XmakeCommand {
1088    verbose: bool,
1089    diagnosis: bool,
1090    raw_output: bool,
1091    command: Command,
1092    args: Vec<std::ffi::OsString>,
1093    task: Option<String>,
1094    project_dir: Option<PathBuf>,
1095}
1096
1097impl XmakeCommand {
1098    /// Create a new XmakeCommand instance.
1099    fn new() -> Self {
1100        let mut command = Command::new(Self::xmake_executable());
1101        command.env("XMAKE_THEME", "plain");
1102        Self {
1103            verbose: false,
1104            diagnosis: false,
1105            raw_output: false,
1106            task: None,
1107            command: command,
1108            args: Vec::new(),
1109            project_dir: None,
1110        }
1111    }
1112
1113    fn xmake_executable() -> String {
1114        env::var("XMAKE").unwrap_or(String::from("xmake"))
1115    }
1116
1117    /// Same as [`Command::arg`]
1118    pub fn arg<S: AsRef<std::ffi::OsStr>>(&mut self, arg: S) -> &mut Self {
1119        self.args.push(arg.as_ref().to_os_string());
1120        self
1121    }
1122
1123    /// Same as [`Command::env`]
1124    pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
1125    where
1126        K: AsRef<std::ffi::OsStr>,
1127        V: AsRef<std::ffi::OsStr>,
1128    {
1129        self.command.env(key, val);
1130        self
1131    }
1132
1133    /// Enable/disable verbose mode of xmake (default is false).
1134    /// Correspond to the -v flag.
1135    pub fn verbose(&mut self, value: bool) -> &mut Self {
1136        self.verbose = value;
1137        self
1138    }
1139
1140    // Enable/disable diagnosis mode of xmake (default is false).
1141    /// Correspond to the -D flag.
1142    pub fn diagnosis(&mut self, value: bool) -> &mut Self {
1143        self.diagnosis = value;
1144        self
1145    }
1146
1147    /// Sets the xmake tasks to run.
1148    pub fn task<S: Into<String>>(&mut self, task: S) -> &mut Self {
1149        self.task = Some(task.into());
1150        self
1151    }
1152
1153    /// Sets the project directory.
1154    pub fn project_dir<P: AsRef<Path>>(&mut self, project_dir: P) -> &mut Self {
1155        use crate::path_clean::PathClean;
1156        self.project_dir = Some(project_dir.as_ref().to_path_buf().clean());
1157        self
1158    }
1159
1160    /// Controls whether to capture raw, unfiltered command output (default is false).
1161    ///
1162    /// When enabled (true):
1163    /// - All command output is captured and returned
1164    ///
1165    /// When disabled (false, default):
1166    /// - Only captures output between special markers (`__xmakers_start__` and `__xmakers_end__`)
1167    /// - Filters out diagnostic and setup information
1168    ///
1169    /// This setting is passed to the [`run`] function to control output processing.
1170    pub fn raw_output(&mut self, value: bool) -> &mut Self {
1171        self.raw_output = value;
1172        self
1173    }
1174
1175    /// Run the command and return the output as a string.
1176    /// Alias of [`run`]
1177    pub fn run(&mut self) -> Option<String> {
1178        if let Some(task) = &self.task {
1179            self.command.arg(task);
1180        }
1181
1182        if self.verbose {
1183            self.command.arg("-v");
1184        }
1185        if self.diagnosis {
1186            self.command.arg("-D");
1187        }
1188
1189        if let Some(project_dir) = &self.project_dir {
1190            // Project directory are evaluated like this:
1191            // 1. The Given Command Argument
1192            // 2. The Environment Variable: XMAKE_PROJECT_DIR
1193            // 3. The Current Directory
1194            //
1195            // The env doesn't work here because it is global, so it breaks
1196            // packages. Just to be sure set both argument and current directory.
1197            let project_dir = project_dir.as_path();
1198            self.command.current_dir(project_dir);
1199            self.command.arg("-P").arg(project_dir);
1200        }
1201
1202        for arg in &self.args {
1203            self.command.arg(arg);
1204        }
1205        run(&mut self.command, "xmake", self.raw_output)
1206    }
1207
1208    /// Execute a lua script, located in the src folder of this crate.
1209    /// Note that this method overide any previously configured taks to be `lua`.
1210    pub fn run_script<S: AsRef<str>>(&mut self, script: S) -> Option<String> {
1211        let script = script.as_ref();
1212
1213        // Get absolute path to the crate root
1214        let crate_root = Path::new(env!("CARGO_MANIFEST_DIR"));
1215        let script_file = crate_root.join("src").join(script);
1216
1217        // Script to execute are positional argument so always last
1218        self.args.push(script_file.into());
1219        self.task("lua"); // For the task to be lua
1220
1221        self.run()
1222    }
1223}
1224
1225fn run(cmd: &mut Command, program: &str, raw_output: bool) -> Option<String> {
1226    println!("running: {:?}", cmd);
1227    let mut child = match cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn() {
1228        Ok(child) => child,
1229        Err(ref e) if e.kind() == ErrorKind::NotFound => {
1230            fail(&format!(
1231                "failed to execute command: {}\nis `{}` not installed?",
1232                e, program
1233            ));
1234        }
1235        Err(e) => fail(&format!("failed to execute command: {}", e)),
1236    };
1237
1238    let mut output = String::new();
1239    let mut take_output = false;
1240
1241    // Read stdout in real-time
1242    if let Some(stdout) = child.stdout.take() {
1243        let reader = BufReader::new(stdout);
1244        for line in reader.lines() {
1245            if let Ok(line) = line {
1246                // Print stdout for logging
1247                println!("{}", line);
1248
1249                take_output &= !line.starts_with("__xmakers_start__");
1250                if take_output || raw_output {
1251                    output.push_str(line.as_str());
1252                    output.push('\n');
1253                }
1254                take_output |= line.starts_with("__xmakers_start__");
1255            }
1256        }
1257    }
1258
1259    // Wait for the command to complete
1260    let status = child.wait().expect("failed to wait on child process");
1261
1262    if !status.success() {
1263        fail(&format!(
1264            "command did not execute successfully, got: {}",
1265            status
1266        ));
1267    }
1268
1269    Some(output)
1270}
1271
1272#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1273struct Version {
1274    major: u32,
1275    minor: u32,
1276    patch: u32,
1277}
1278
1279impl Version {
1280    const fn new(major: u32, minor: u32, patch: u32) -> Self {
1281        Self {
1282            major,
1283            minor,
1284            patch,
1285        }
1286    }
1287
1288    fn parse(s: &str) -> Option<Self> {
1289        // As of v2.9.5, the format of the version output is "xmake v2.9.5+dev.478972cd9, A cross-platform build utility based on Lua".
1290        // ```
1291        // $ xmake --version
1292        // xmake v2.9.8+HEAD.13fc39238, A cross-platform build utility based on Lua
1293        // Copyright (C) 2015-present Ruki Wang, tboox.org, xmake.io
1294        //                         _
1295        //    __  ___ __  __  __ _| | ______
1296        //    \ \/ / |  \/  |/ _  | |/ / __ \
1297        //     >  <  | \__/ | /_| |   <  ___/
1298        //    /_/\_\_|_|  |_|\__ \|_|\_\____|
1299        //                          by ruki, xmake.io
1300        //
1301        //     point_right  Manual: https://xmake.io/#/getting_started
1302        //     pray  Donate: https://xmake.io/#/sponsor
1303        // ```
1304        let version = s.lines().next()?.strip_prefix("xmake v")?;
1305        let mut parts = version.splitn(2, '+'); // split at the '+' to separate the version and commit
1306
1307        let version_part = parts.next()?;
1308        // Get commit and branch
1309        // let commit_part = parts.next().unwrap_or(""); // if there's no commit part, use an empty string
1310        // let mut commit_parts = commit_part.splitn(2, '.'); // split commit part to get branch and commit hash
1311        // let branch = commit_parts.next().unwrap_or("");
1312        // let commit = commit_parts.next().unwrap_or("");
1313
1314        let mut digits = version_part.splitn(3, '.');
1315        let major = digits.next()?.parse::<u32>().ok()?;
1316        let minor = digits.next()?.parse::<u32>().ok()?;
1317        let patch = digits.next()?.parse::<u32>().ok()?;
1318
1319        Some(Version::new(major, minor, patch))
1320    }
1321
1322    fn from_command() -> Option<Self> {
1323        let output = XmakeCommand::new()
1324            .raw_output(true)
1325            .arg("--version")
1326            .run()?;
1327        Self::parse(output.as_str())
1328    }
1329}
1330
1331mod path_clean {
1332    // Taken form the path-clean crate.
1333    // Crates.io: https://crates.io/crates/path-clean
1334    // GitHub: https://github.com/danreeves/path-clean
1335
1336    use std::path::{Component, Path, PathBuf};
1337    pub(super) trait PathClean {
1338        fn clean(&self) -> PathBuf;
1339    }
1340
1341    impl PathClean for Path {
1342        fn clean(&self) -> PathBuf {
1343            clean(self)
1344        }
1345    }
1346
1347    impl PathClean for PathBuf {
1348        fn clean(&self) -> PathBuf {
1349            clean(self)
1350        }
1351    }
1352
1353    pub(super) fn clean<P>(path: P) -> PathBuf
1354    where
1355        P: AsRef<Path>,
1356    {
1357        let mut out = Vec::new();
1358
1359        for comp in path.as_ref().components() {
1360            match comp {
1361                Component::CurDir => (),
1362                Component::ParentDir => match out.last() {
1363                    Some(Component::RootDir) => (),
1364                    Some(Component::Normal(_)) => {
1365                        out.pop();
1366                    }
1367                    None
1368                    | Some(Component::CurDir)
1369                    | Some(Component::ParentDir)
1370                    | Some(Component::Prefix(_)) => out.push(comp),
1371                },
1372                comp => out.push(comp),
1373            }
1374        }
1375
1376        if !out.is_empty() {
1377            out.iter().collect()
1378        } else {
1379            PathBuf::from(".")
1380        }
1381    }
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386    use std::{path::PathBuf, vec};
1387
1388    use crate::{
1389        parse_field, parse_info_pairs, subkeys_of, BuildInfo, Link, LinkKind, ParsingError, Source,
1390    };
1391
1392    fn to_set<T: std::cmp::Eq + std::hash::Hash>(vec: Vec<T>) -> std::collections::HashSet<T> {
1393        vec.into_iter().collect()
1394    }
1395
1396    #[test]
1397    fn parse_line() {
1398        let expected_values: Vec<_> = ["value1", "value2", "value3"].map(String::from).to_vec();
1399        let map = parse_info_pairs("key:value1|value2|value3");
1400        assert!(map.contains_key("key"));
1401        assert_eq!(map["key"], expected_values);
1402    }
1403
1404    #[test]
1405    fn parse_line_empty_values() {
1406        let expected_values: Vec<_> = ["value1", "value2"].map(String::from).to_vec();
1407        let map = parse_info_pairs("key:value1||value2");
1408        assert!(map.contains_key("key"));
1409        assert_eq!(map["key"], expected_values);
1410    }
1411
1412    #[test]
1413    fn parse_field_multiple_values() {
1414        let map = parse_info_pairs("key:value1|value2|value3");
1415        let result: Result<String, _> = parse_field(&map, "key");
1416        assert!(map.contains_key("key"));
1417        assert!(result.is_err());
1418        assert_eq!(result.err().unwrap(), ParsingError::MultipleValues);
1419    }
1420
1421    #[test]
1422    fn parse_with_subkeys() {
1423        let map = parse_info_pairs("main:value\nmain.subkey:value1|value2|value3\nmain.sub2:vv");
1424        let subkeys = to_set(subkeys_of(&map, "main"));
1425        assert_eq!(subkeys, to_set(vec!["sub2", "subkey"]));
1426    }
1427
1428    #[test]
1429    fn parse_build_info_missing_key() {
1430        let mut s = String::new();
1431        s.push_str("linkdirs:path/to/libA|path/to/libB|path\\to\\libC\n");
1432        s.push_str("links:linkA/static|linkB/shared\n");
1433
1434        let build_info: Result<BuildInfo, _> = s.parse();
1435        assert!(build_info.is_err());
1436        assert_eq!(build_info.err().unwrap(), ParsingError::MissingKey);
1437    }
1438
1439    #[test]
1440    fn parse_build_info_missing_kind() {
1441        let mut s = String::new();
1442        s.push_str("cxx_used:true\n");
1443        s.push_str("stl_used:false\n");
1444        s.push_str("links:linkA|linkB\n");
1445        s.push_str("linkdirs:path/to/libA|path/to/libB|path\\to\\libC\n");
1446
1447        let build_info: Result<BuildInfo, _> = s.parse();
1448        assert!(build_info.is_err());
1449
1450        // For now the returned error is not MalformedLink because map_err in parse_field shallow
1451        // all the errors which are converted to ParsingError::ParseError
1452        // assert_eq!(build_info.err().unwrap(), ParsingError::MalformedLink);
1453    }
1454
1455    #[test]
1456    fn parse_build_info_missing_info() {
1457        let mut s = String::new();
1458        s.push_str("links:linkA/static|linkB/shared\n");
1459        s.push_str("linkdirs:path/to/libA|path/to/libB|path\\to\\libC\n");
1460
1461        let build_info: Result<BuildInfo, _> = s.parse();
1462        assert!(build_info.is_err());
1463    }
1464
1465    #[test]
1466    fn parse_build_info() {
1467        let expected_links = [
1468            Link::new("linkA", LinkKind::Static),
1469            Link::new("linkB", LinkKind::Dynamic),
1470        ];
1471        let expected_directories = ["path/to/libA", "path/to/libB", "path\\to\\libC"]
1472            .map(PathBuf::from)
1473            .to_vec();
1474
1475        let expected_includedirs_package_a = to_set(
1476            ["includedir/a", "includedir\\aa"]
1477                .map(PathBuf::from)
1478                .to_vec(),
1479        );
1480        let expected_includedirs_package_b = to_set(
1481            ["includedir/bb", "includedir\\b"]
1482                .map(PathBuf::from)
1483                .to_vec(),
1484        );
1485
1486        let expected_includedirs_target_c = to_set(["includedir/c"].map(PathBuf::from).to_vec());
1487
1488        let expected_includedirs_both_greedy = to_set(
1489            [
1490                "includedir/c",
1491                "includedir/bb",
1492                "includedir\\b",
1493                "includedir/a",
1494                "includedir\\aa",
1495            ]
1496            .map(PathBuf::from)
1497            .to_vec(),
1498        );
1499
1500        let expected_cxx = true;
1501        let expected_stl = false;
1502
1503        let mut s = String::new();
1504        s.push_str("cxx_used:true\n");
1505        s.push_str("stl_used:false\n");
1506        s.push_str("links:linkA/static|linkB/shared\n");
1507        s.push_str("linkdirs:path/to/libA|path/to/libB|path\\to\\libC\n");
1508        s.push_str("includedirs_package.a:includedir/a|includedir\\aa\n");
1509        s.push_str("includedirs_package.b:includedir/bb|includedir\\b\n");
1510        s.push_str("includedirs_target.c:includedir/c");
1511
1512        let build_info: BuildInfo = s.parse().unwrap();
1513
1514        assert_eq!(build_info.links(), &expected_links);
1515        assert_eq!(build_info.linkdirs(), &expected_directories);
1516        assert_eq!(build_info.use_cxx(), expected_cxx);
1517        assert_eq!(build_info.use_stl(), expected_stl);
1518
1519        assert_eq!(
1520            to_set(build_info.includedirs(Source::Package, "a")),
1521            expected_includedirs_package_a
1522        );
1523        assert_eq!(
1524            to_set(build_info.includedirs(Source::Package, "b")),
1525            expected_includedirs_package_b
1526        );
1527        assert_eq!(
1528            to_set(build_info.includedirs(Source::Target, "c")),
1529            expected_includedirs_target_c
1530        );
1531        assert_eq!(
1532            to_set(build_info.includedirs(Source::Both, "*")),
1533            expected_includedirs_both_greedy
1534        );
1535    }
1536}