1use std::env;
2use std::ffi::OsString;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5use std::process::Stdio;
6use std::{sync, thread, time};
7
8#[cfg(feature = "parallel")]
9use std::sync::OnceLock;
10
11use log::{error, info};
12
13#[cfg(feature = "parallel")]
14static JOBSERVER: OnceLock<jobserver::Client> = OnceLock::new();
15
16fn x86_triple(os: &str) -> (&'static str, &'static str) {
17 match os {
18 "darwin" | "ios" => ("-fmacho32", "-g"),
19 "windows" | "uefi" => ("-fwin32", "-g"),
20 _ => ("-felf32", "-gdwarf"),
21 }
22}
23
24fn x86_64_triple(os: &str) -> (&'static str, &'static str) {
25 match os {
26 "darwin" | "ios" => ("-fmacho64", "-g"),
27 "windows" | "uefi" => ("-fwin64", "-g"),
28 _ => ("-felf64", "-gdwarf"),
29 }
30}
31
32fn parse_triple(trip: &str) -> (&'static str, &'static str) {
33 let parts = trip.split('-').collect::<Vec<_>>();
34 if parts.len() < 3 {
38 return ("", "-g");
39 }
40
41 match parts[0] {
42 "x86_64" => {
43 if parts.len() >= 4 && parts[3] == "gnux32" {
44 ("-felfx32", "-gdwarf")
45 } else {
46 x86_64_triple(parts[2])
47 }
48 }
49 "x86" | "i386" | "i586" | "i686" => x86_triple(parts[2]),
50 _ => ("", "-g"),
51 }
52}
53
54pub fn compile_library(output: &str, files: &[&str]) -> Result<(), String> {
60 compile_library_args(output, files, &[])
61}
62
63pub fn compile_library_args<P: AsRef<Path>>(
69 output: &str,
70 files: &[P],
71 args: &[&str],
72) -> Result<(), String> {
73 let mut b = Build::new();
74 for file in files {
75 b.file(file);
76 }
77 for arg in args {
78 b.flag(arg);
79 }
80 b.compile(output)
81}
82
83pub struct Build {
84 files: Vec<PathBuf>,
85 flags: Vec<String>,
86 target: Option<String>,
87 out_dir: Option<PathBuf>,
88 archiver: Option<PathBuf>,
89 archiver_is_msvc: Option<bool>,
90 nasm: Option<PathBuf>,
91 debug: bool,
92 min_version: (usize, usize, usize),
93}
94
95impl Build {
96 pub fn new() -> Self {
97 let nasm = match env::var("NASM") {
98 Ok(n) => Some(PathBuf::from(n)),
99 Err(_) => None,
100 };
101
102 Self {
103 files: Vec::new(),
104 flags: Vec::new(),
105 archiver: None,
106 archiver_is_msvc: None,
107 out_dir: None,
108 nasm,
109 target: None,
110 min_version: (1, 0, 0),
111 debug: env::var("DEBUG").ok().map_or(false, |d| d != "false"),
112 }
113 }
114
115 pub fn file<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
119 self.files.push(p.as_ref().to_owned());
120 self
121 }
122
123 pub fn files<P: AsRef<Path>, I: IntoIterator<Item = P>>(&mut self, files: I) -> &mut Self {
125 for file in files {
126 self.file(file);
127 }
128 self
129 }
130
131 pub fn include<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
133 let mut flag = format!("-I{}", dir.as_ref().display());
134 if !flag.ends_with('/') {
136 flag += "/";
137 }
138 self.flags.push(flag);
139 self
140 }
141
142 pub fn define<'a, V: Into<Option<&'a str>>>(&mut self, var: &str, val: V) -> &mut Self {
144 let val = val.into();
145 let flag = if let Some(val) = val {
146 format!("-D{}={}", var, val)
147 } else {
148 format!("-D{}", var)
149 };
150 self.flags.push(flag);
151 self
152 }
153
154 pub fn debug(&mut self, enable: bool) -> &mut Self {
160 self.debug = enable;
161 self
162 }
163
164 pub fn flag(&mut self, flag: &str) -> &mut Self {
168 self.flags.push(flag.to_owned());
169 self
170 }
171
172 pub fn target(&mut self, target: &str) -> &mut Self {
177 self.target = Some(target.to_owned());
178 self
179 }
180
181 pub fn out_dir<P: AsRef<Path>>(&mut self, out_dir: P) -> &mut Self {
186 self.out_dir = Some(out_dir.as_ref().to_owned());
187 self
188 }
189
190 pub fn archiver<P: AsRef<Path>>(&mut self, archiver: P) -> &mut Self {
196 self.archiver = Some(archiver.as_ref().to_owned());
197 self
198 }
199
200 pub fn archiver_is_msvc(&mut self, is_msvc: bool) -> &mut Self {
205 self.archiver_is_msvc = Some(is_msvc);
206 self
207 }
208
209 pub fn nasm<P: AsRef<Path>>(&mut self, nasm: P) -> &mut Self {
211 self.nasm = Some(nasm.as_ref().to_owned());
212 self
213 }
214
215 pub fn min_version(&mut self, major: usize, minor: usize, micro: usize) -> &mut Self {
217 self.min_version = (major, minor, micro);
218 self
219 }
220
221 pub fn compile(&mut self, lib_name: &str) -> Result<(), String> {
229 let lib_name = if lib_name.starts_with("lib") && lib_name.ends_with(".a") {
231 &lib_name[3..lib_name.len() - 2]
232 } else {
233 lib_name.trim_end_matches(".lib")
234 };
235
236 let target = self.get_target();
237 let output = if target.ends_with("-msvc") {
238 format!("{}.lib", lib_name)
239 } else {
240 format!("lib{}.a", lib_name)
241 };
242
243 let dst = &self.get_out_dir();
244 let objects = self.compile_objects()?;
245 self.archive(dst, &output, &objects[..])?;
246
247 println!("cargo:rustc-link-search={}", dst.display());
248 Ok(())
249 }
250
251 pub fn compile_objects(&mut self) -> Result<Vec<PathBuf>, String> {
255 let target = self.get_target();
256
257 let nasm = self.find_nasm()?;
258 let args = self.get_args(&target);
259
260 let src = &PathBuf::from(
261 env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set"),
262 );
263 let dst = &self.get_out_dir();
264
265 self.compile_objects_inner(&nasm, &self.files, &args, src, dst)
266 }
267
268 #[cfg(feature = "parallel")]
269 fn compile_objects_inner(
270 &self,
271 nasm: &Path,
272 files: &[PathBuf],
273 args: &[&str],
274 src: &Path,
275 dst: &Path,
276 ) -> Result<Vec<PathBuf>, String> {
277 use jobserver::Client;
278 use std::panic;
279
280 let jobserver = JOBSERVER.get_or_init(|| {
281 unsafe { Client::from_env() }.unwrap_or_else(|| {
283 let job_limit: usize = match env::var("NUM_JOBS").map(|s| s.parse()) {
285 Ok(Ok(limit)) => limit,
286 _ => {
287 error!("warn: NUM_JOBS is not set or could not be parsed. Defaulting to 1");
288 1
289 }
290 };
291
292 let client = Client::new(job_limit).expect("Failed to create a job server");
295 client
296 .acquire_raw() .expect("Failed to acquire initial job token");
298
299 client
300 })
301 });
302 let list = sync::Arc::new(sync::RwLock::new(
305 files.iter().rev().collect::<Vec<&PathBuf>>(),
306 ));
307
308 let thread_results: Vec<_> = std::thread::scope(|s| {
309 let mut outputs: Vec<PathBuf> = Vec::with_capacity(files.len());
310 let helper_thread_list = sync::Arc::clone(&list);
311 let helper_thread_handle = s.spawn(move || {
312 let mut handles = Vec::with_capacity(files.len());
313 'outer: loop {
314 let token = loop {
316 match jobserver.try_acquire() {
317 Ok(Some(t)) => break t,
318 Ok(None) => {},
319 Err(e) => if e.kind() == std::io::ErrorKind::Unsupported {
320 error!("Non-blocking acquire from jobserver not supported on this platform.");
321 break 'outer; }
323 }
324
325 if helper_thread_list.read().unwrap().is_empty() {
326 break 'outer;
328 }
329 thread::sleep(time::Duration::from_millis(10));
331 };
332
333 let file = {
334 let mut wlist = helper_thread_list.write().unwrap();
335 (*wlist).pop()
336 };
337
338 if file.is_none() {
339 break;
340 }
341
342 let handle = s.spawn(move || {
343 let result =
344 self.compile_file(nasm, file.unwrap().as_path(), args, src, dst);
345 drop(token);
347 result
348 });
349 handles.push(handle);
350 }
351 let thread_res: Vec<_> = handles.into_iter().map(|h| h.join()).collect();
353 thread_res
355 .into_iter()
356 .map(|r| r.unwrap_or_else(|e| panic::resume_unwind(e)))
357 .collect()
358 });
359
360 loop {
364 let file = {
365 let mut wlist = list.write().unwrap();
366 (*wlist).pop()
367 };
368
369 if file.is_none() {
370 break;
371 }
372 match self.compile_file(nasm, file.unwrap().as_path(), args, src, dst) {
373 Ok(r) => outputs.push(r),
374 Err(e) => {
375 error!("{}", e);
376 }
377 }
378 }
379
380 let thread_outputs: Vec<_> = helper_thread_handle
381 .join()
382 .unwrap_or_else(|e| panic::resume_unwind(e));
383
384 for out in thread_outputs {
385 match out {
386 Ok(p) => outputs.push(p),
387 Err(e) => {
388 error!("{}", e)
389 }
390 }
391 }
392 outputs
393 });
394 Ok(thread_results)
395 }
396
397 #[cfg(not(feature = "parallel"))]
398 fn compile_objects_inner(
399 &self,
400 nasm: &Path,
401 files: &[PathBuf],
402 args: &[&str],
403 src: &Path,
404 dst: &Path,
405 ) -> Result<Vec<PathBuf>, String> {
406 files
407 .iter()
408 .map(|file| self.compile_file(&nasm, file, &args, src, dst))
409 .collect()
410 }
411
412 fn get_args(&self, target: &str) -> Vec<&str> {
413 let (arch_flag, debug_flag) = parse_triple(target);
414 let mut args = vec![arch_flag];
415
416 if self.debug {
417 args.push(debug_flag);
418 }
419
420 for arg in &self.flags {
421 args.push(arg);
422 }
423
424 args
425 }
426
427 fn compile_file(
428 &self,
429 nasm: &Path,
430 file: &Path,
431 new_args: &[&str],
432 src: &Path,
433 dst: &Path,
434 ) -> Result<PathBuf, String> {
435 let obj = dst.join(file.file_name().unwrap()).with_extension("o");
436 let mut cmd = Command::new(nasm);
437 cmd.args(new_args);
438 std::fs::create_dir_all(obj.parent().unwrap()).unwrap();
439
440 run(cmd.arg(src.join(file)).arg("-o").arg(&obj))?;
441 Ok(obj)
442 }
443
444 fn archive(&self, out_dir: &Path, lib: &str, objs: &[PathBuf]) -> Result<(), String> {
445 let ar_is_msvc = self.archiver_is_msvc.unwrap_or(cfg!(target_env = "msvc"));
446
447 let ar = if ar_is_msvc {
448 self.archiver.clone().unwrap_or_else(|| "lib".into())
449 } else {
450 self.archiver
451 .clone()
452 .or_else(|| env::var_os("AR").map(|a| a.into()))
453 .unwrap_or_else(|| "ar".into())
454 };
455 if ar_is_msvc {
456 let mut out_param = OsString::new();
457 out_param.push("/OUT:");
458 out_param.push(out_dir.join(lib).as_os_str());
459 run(Command::new(ar).arg(out_param).args(objs))
460 } else {
461 run(Command::new(ar)
462 .arg("crus")
463 .arg(out_dir.join(lib))
464 .args(objs))
465 }
466 }
467
468 fn get_out_dir(&self) -> PathBuf {
469 self.out_dir
470 .clone()
471 .unwrap_or_else(|| PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR must be set")))
472 }
473
474 fn get_target(&self) -> String {
475 self.target
476 .clone()
477 .unwrap_or_else(|| env::var("TARGET").expect("TARGET must be set"))
478 }
479
480 fn is_nasm_found_and_new_enough(&self, nasm_path: &Path) -> Result<(), String> {
483 let version = get_output(Command::new(nasm_path).arg("-v"))
484 .map_err(|e| format!("Unable to run {}: {}", nasm_path.display(), e))?;
485 let (major, minor, micro) = self.min_version;
486 let ver = parse_nasm_version(&version)?;
487 if major > ver.0
488 || (major == ver.0 && minor > ver.1)
489 || (major == ver.0 && minor == ver.1 && micro > ver.2)
490 {
491 Err(format!(
492 "This version of NASM is too old: {}. Required >= {}.{}.{}",
493 version, major, minor, micro
494 ))
495 } else {
496 Ok(())
497 }
498 }
499
500 fn find_nasm(&mut self) -> Result<PathBuf, String> {
501 let paths = match &self.nasm {
502 Some(p) => vec![p.to_owned()],
503 None => {
504 let path = env::var_os("PATH").unwrap_or_default();
508 std::iter::once(PathBuf::from("nasm"))
509 .chain(env::split_paths(&path).map(|p| p.join("nasm")))
510 .collect()
511 }
512 };
513
514 let mut first_error = None;
515 for nasm_path in paths {
516 match self.is_nasm_found_and_new_enough(&nasm_path) {
517 Ok(_) => return Ok(nasm_path),
518 Err(err) => {
519 let _ = first_error.get_or_insert(err);
520 }
521 }
522 }
523 Err(first_error.unwrap())
524 }
525}
526
527fn parse_nasm_version(version: &str) -> Result<(usize, usize, usize), String> {
528 let mut ver = version
529 .split(' ')
530 .nth(2)
531 .ok_or_else(|| format!("Invalid nasm version '{}'", version))?;
532
533 if let Some(ver_rc) = ver.find("rc") {
535 ver = &ver[0..ver_rc];
536 }
537 let ver: Vec<_> = ver
538 .split('.')
539 .map(|v| v.parse())
540 .take_while(Result::is_ok)
541 .map(Result::unwrap)
542 .collect();
543
544 Ok((
545 ver[0],
546 ver.get(1).copied().unwrap_or(0),
547 ver.get(2).copied().unwrap_or(0),
548 ))
549}
550
551fn get_output(cmd: &mut Command) -> Result<String, String> {
552 let out = cmd.output().map_err(|e| e.to_string())?;
553 if out.status.success() {
554 Ok(String::from_utf8_lossy(&out.stdout).to_string())
555 } else {
556 Err(String::from_utf8_lossy(&out.stderr).to_string())
557 }
558}
559
560fn run(cmd: &mut Command) -> Result<(), String> {
561 info!("running: {:?}", cmd);
562
563 let status = match cmd
564 .stdout(Stdio::inherit())
565 .stderr(Stdio::inherit())
566 .status()
567 {
568 Ok(status) => status,
569
570 Err(e) => return Err(format!("failed to spawn process: {}", e)),
571 };
572
573 if !status.success() {
574 return Err(format!("nonzero exit status: {}", status));
575 }
576 Ok(())
577}
578
579#[test]
580fn test_build() {
581 let mut build = Build::new();
582 build.file("test");
583 build.archiver("ar");
584 build.include("./");
585 build.include("dir");
586 build.define("foo", Some("1"));
587 build.define("bar", None);
588 build.flag("-test");
589 build.target("i686-unknown-linux-musl");
590 build.out_dir("/tmp");
591 build.min_version(0, 0, 0);
592
593 assert_eq!(
594 build.get_args("i686-unknown-linux-musl"),
595 &["-felf32", "-I./", "-Idir/", "-Dfoo=1", "-Dbar", "-test"]
596 );
597}
598
599#[test]
600fn test_parse_nasm_version() {
601 let ver_str = "NASM version 2.14.02 compiled on Jan 22 2019";
602 assert_eq!((2, 14, 2), parse_nasm_version(ver_str).unwrap());
603 let ver_str = "NASM version 2.14.02";
604 assert_eq!((2, 14, 2), parse_nasm_version(ver_str).unwrap());
605 let ver_str = "NASM version 2.14 compiled on Jan 22 2019";
606 assert_eq!((2, 14, 0), parse_nasm_version(ver_str).unwrap());
607 let ver_str = "NASM version 2.14";
608 assert_eq!((2, 14, 0), parse_nasm_version(ver_str).unwrap());
609 let ver_str = "NASM version 2.14rc2";
610 assert_eq!((2, 14, 0), parse_nasm_version(ver_str).unwrap());
611}
612
613#[test]
614fn test_parse_triple() {
615 let triple = "x86_64-unknown-linux-gnux32";
616 assert_eq!(parse_triple(triple), ("-felfx32", "-gdwarf"));
617
618 let triple = "x86_64-unknown-linux";
619 assert_eq!(parse_triple(triple), ("-felf64", "-gdwarf"));
620}