1#![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
44const XMAKE_MINIMUM_VERSION: Version = Version::new(2, 9, 6);
47
48#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum LinkKind {
53 Static,
55 Dynamic,
57 System,
59 Framework,
61 Unknown,
63}
64
65pub enum Source {
67 Target,
69 Package,
71 Both,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct Link {
80 name: String,
82 kind: LinkKind,
84}
85
86#[derive(Default)]
90pub struct BuildInfo {
91 linkdirs: Vec<PathBuf>,
93 links: Vec<Link>,
95 includedirs_package: HashMap<String, Vec<PathBuf>>,
97 includedirs_target: HashMap<String, Vec<PathBuf>>,
99 use_cxx: bool,
101 use_stl: bool,
103}
104
105#[derive(Debug, PartialEq, Eq)]
107pub enum ParsingError {
108 InvalidKind,
110 MissingKey,
112 MalformedLink,
114 MultipleValues,
116 ParseError,
118}
119
120impl Link {
121 pub fn name(&self) -> &str {
123 &self.name
124 }
125 pub fn kind(&self) -> &LinkKind {
127 &self.kind
128 }
129
130 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 pub fn linkdirs(&self) -> &[PathBuf] {
142 &self.linkdirs
143 }
144
145 pub fn links(&self) -> &[Link] {
147 &self.links
148 }
149
150 pub fn use_cxx(&self) -> bool {
152 self.use_cxx
153 }
154
155 pub fn use_stl(&self) -> bool {
157 self.use_stl
158 }
159
160 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 fn plat(&self) -> &String {
265 return self.plat.as_ref().unwrap();
266 }
267
268 fn arch(&self) -> &String {
271 return self.arch.as_ref().unwrap();
272 }
273}
274
275pub 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
291pub fn build<P: AsRef<Path>>(path: P) {
304 Config::new(path.as_ref()).build()
305}
306
307impl Config {
308 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 pub fn targets<T: CommaSeparated>(&mut self, targets: T) -> &mut Config {
337 self.targets = Some(targets.as_comma_separated());
338 self
339 }
340
341 pub fn verbose(&mut self, value: bool) -> &mut Config {
343 self.verbose = value;
344 self
345 }
346
347 pub fn auto_link(&mut self, value: bool) -> &mut Config {
351 self.auto_link = value;
352 self
353 }
354
355 pub fn no_stl_link(&mut self, value: bool) -> &mut Config {
360 self.no_stl_link = value;
361 self
362 }
363
364 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 pub fn mode(&mut self, mode: &str) -> &mut Config {
375 self.mode = Some(mode.to_string());
376 self
377 }
378
379 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 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 pub fn static_crt(&mut self, static_crt: bool) -> &mut Config {
407 self.static_crt = Some(static_crt);
408 self
409 }
410
411 pub fn runtimes<T: CommaSeparated>(&mut self, runtimes: T) -> &mut Config {
437 self.runtimes = Some(runtimes.as_comma_separated());
438 self
439 }
440
441 pub fn build(&mut self) {
447 self.config();
448
449 let mut cmd = self.xmake_command();
450
451 cmd.arg("--yes");
453
454 if let Some(targets) = &self.targets {
455 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 pub fn build_info(&self) -> &BuildInfo {
474 &self.cache.build_info
475 }
476
477 fn config(&mut self) {
480 self.check_version();
481
482 let mut cmd = self.xmake_command();
483 cmd.task("config");
484
485 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 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 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 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 let mode = self.get_mode();
556 cmd.arg("-m").arg(mode);
557
558 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 println!("cargo:rustc-link-search=all={}", directory.display());
576 }
577
578 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 LinkKind::System | LinkKind::Framework => {
609 println!("cargo:rustc-link-lib={}", link.name())
610 }
611 LinkKind::Unknown => println!("cargo:rustc-link-lib={}", link.name()),
613 }
614 }
615
616 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 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 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 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 fn get_runtimes(&mut self) -> Option<String> {
730 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 fn get_xmake_plat(&mut self) -> String {
755 if let Some(ref plat) = self.cache.plat {
756 return plat.clone();
757 }
758
759 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 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 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", "armv7" => "armeabi-v7a",
803 a => a,
804 },
805 ("android", "aarch64") => "arm64-v8a",
806 ("android", "i686") => "x86",
807 ("linux", "loongarch64") => "loong64",
808 ("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 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 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
975fn 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 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
1006trait DirectParse {}
1012
1013impl 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
1025impl<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
1048impl<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 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 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 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 pub fn verbose(&mut self, value: bool) -> &mut Self {
1136 self.verbose = value;
1137 self
1138 }
1139
1140 pub fn diagnosis(&mut self, value: bool) -> &mut Self {
1143 self.diagnosis = value;
1144 self
1145 }
1146
1147 pub fn task<S: Into<String>>(&mut self, task: S) -> &mut Self {
1149 self.task = Some(task.into());
1150 self
1151 }
1152
1153 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 pub fn raw_output(&mut self, value: bool) -> &mut Self {
1171 self.raw_output = value;
1172 self
1173 }
1174
1175 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 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 pub fn run_script<S: AsRef<str>>(&mut self, script: S) -> Option<String> {
1211 let script = script.as_ref();
1212
1213 let crate_root = Path::new(env!("CARGO_MANIFEST_DIR"));
1215 let script_file = crate_root.join("src").join(script);
1216
1217 self.args.push(script_file.into());
1219 self.task("lua"); 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 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 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 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 let version = s.lines().next()?.strip_prefix("xmake v")?;
1305 let mut parts = version.splitn(2, '+'); let version_part = parts.next()?;
1308 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 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 }
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}