1use std::env;
2use std::ffi::OsString;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5use std::process::Stdio;
6
7#[cfg(feature = "parallel")]
8use std::sync::OnceLock;
9
10use log::info;
11
12#[cfg(feature = "parallel")]
13static JOBSERVER: OnceLock<jobserver::Client> = OnceLock::new();
14
15fn x86_triple(os: &str) -> (&'static str, &'static str) {
16 match os {
17 "darwin" | "ios" => ("-fmacho32", "-g"),
18 "windows" | "uefi" => ("-fwin32", "-g"),
19 _ => ("-felf32", "-gdwarf"),
20 }
21}
22
23fn x86_64_triple(os: &str) -> (&'static str, &'static str) {
24 match os {
25 "darwin" | "ios" => ("-fmacho64", "-g"),
26 "windows" | "uefi" => ("-fwin64", "-g"),
27 _ => ("-felf64", "-gdwarf"),
28 }
29}
30
31fn parse_triple(trip: &str) -> (&'static str, &'static str) {
32 let parts = trip.split('-').collect::<Vec<_>>();
33 if parts.len() < 3 {
37 return ("", "-g");
38 }
39
40 match parts[0] {
41 "x86_64" => {
42 if parts.len() >= 4 && parts[3] == "gnux32" {
43 ("-felfx32", "-gdwarf")
44 } else {
45 x86_64_triple(parts[2])
46 }
47 },
48 "x86" | "i386" | "i586" | "i686" => x86_triple(parts[2]),
49 _ => ("", "-g"),
50 }
51}
52
53pub fn compile_library(output: &str, files: &[&str]) -> Result<(), String> {
59 compile_library_args(output, files, &[])
60}
61
62pub fn compile_library_args<P: AsRef<Path>>(
68 output: &str,
69 files: &[P],
70 args: &[&str],
71) -> Result<(), String> {
72 let mut b = Build::new();
73 for file in files {
74 b.file(file);
75 }
76 for arg in args {
77 b.flag(arg);
78 }
79 b.compile(output)
80}
81
82pub struct Build {
83 files: Vec<PathBuf>,
84 flags: Vec<String>,
85 target: Option<String>,
86 out_dir: Option<PathBuf>,
87 archiver: Option<PathBuf>,
88 archiver_is_msvc: Option<bool>,
89 nasm: Option<PathBuf>,
90 debug: bool,
91 min_version: (usize, usize, usize),
92}
93
94impl Build {
95 pub fn new() -> Self {
96 Self {
97 files: Vec::new(),
98 flags: Vec::new(),
99 archiver: None,
100 archiver_is_msvc: None,
101 out_dir: None,
102 nasm: None,
103 target: None,
104 min_version: (1, 0, 0),
105 debug: env::var("DEBUG").ok().map_or(false, |d| d != "false"),
106 }
107 }
108
109 pub fn file<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
113 self.files.push(p.as_ref().to_owned());
114 self
115 }
116
117 pub fn files<P: AsRef<Path>, I: IntoIterator<Item = P>>(&mut self, files: I) -> &mut Self {
119 for file in files {
120 self.file(file);
121 }
122 self
123 }
124
125 pub fn include<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
127 let mut flag = format!("-I{}", dir.as_ref().display());
128 if !flag.ends_with('/') {
130 flag += "/";
131 }
132 self.flags.push(flag);
133 self
134 }
135
136 pub fn define<'a, V: Into<Option<&'a str>>>(&mut self, var: &str, val: V) -> &mut Self {
138 let val = val.into();
139 let flag = if let Some(val) = val {
140 format!("-D{}={}", var, val)
141 } else {
142 format!("-D{}", var)
143 };
144 self.flags.push(flag);
145 self
146 }
147
148 pub fn debug(&mut self, enable: bool) -> &mut Self {
154 self.debug = enable;
155 self
156 }
157
158 pub fn flag(&mut self, flag: &str) -> &mut Self {
162 self.flags.push(flag.to_owned());
163 self
164 }
165
166 pub fn target(&mut self, target: &str) -> &mut Self {
171 self.target = Some(target.to_owned());
172 self
173 }
174
175 pub fn out_dir<P: AsRef<Path>>(&mut self, out_dir: P) -> &mut Self {
180 self.out_dir = Some(out_dir.as_ref().to_owned());
181 self
182 }
183
184 pub fn archiver<P: AsRef<Path>>(&mut self, archiver: P) -> &mut Self {
190 self.archiver = Some(archiver.as_ref().to_owned());
191 self
192 }
193
194 pub fn archiver_is_msvc(&mut self, is_msvc: bool) -> &mut Self {
199 self.archiver_is_msvc = Some(is_msvc);
200 self
201 }
202
203 pub fn nasm<P: AsRef<Path>>(&mut self, nasm: P) -> &mut Self {
205 self.nasm = Some(nasm.as_ref().to_owned());
206 self
207 }
208
209 pub fn min_version(&mut self, major: usize, minor: usize, micro: usize) -> &mut Self {
211 self.min_version = (major, minor, micro);
212 self
213 }
214
215 pub fn compile(&mut self, lib_name: &str) -> Result<(), String> {
223 let lib_name = if lib_name.starts_with("lib") && lib_name.ends_with(".a") {
225 &lib_name[3..lib_name.len() - 2]
226 } else {
227 lib_name.trim_end_matches(".lib")
228 };
229
230 let target = self.get_target();
231 let output = if target.ends_with("-msvc") {
232 format!("{}.lib", lib_name)
233 } else {
234 format!("lib{}.a", lib_name)
235 };
236
237 let dst = &self.get_out_dir();
238 let objects = self.compile_objects()?;
239 self.archive(&dst, &output, &objects[..])?;
240
241 println!("cargo:rustc-link-search={}", dst.display());
242 Ok(())
243 }
244
245 pub fn compile_objects(&mut self) -> Result<Vec<PathBuf>, String> {
249 let target = self.get_target();
250
251 let nasm = self.find_nasm()?;
252 let args = self.get_args(&target);
253
254 let src = &PathBuf::from(
255 env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set"),
256 );
257 let dst = &self.get_out_dir();
258
259 self.compile_objects_inner(&nasm, &self.files, &args, src, dst)
260 }
261
262 #[cfg(feature = "parallel")]
263 fn compile_objects_inner(
264 &self,
265 nasm: &Path,
266 files: &[PathBuf],
267 args: &[&str],
268 src: &Path,
269 dst: &Path,
270 ) -> Result<Vec<PathBuf>, String> {
271 use jobserver::Client;
272 use std::panic;
273
274 let jobserver = JOBSERVER.get_or_init(|| {
275 unsafe { Client::from_env() }.unwrap_or_else(|| {
277 let job_limit: usize = match env::var("NUM_JOBS").map(|s| s.parse()) {
279 Ok(Ok(limit)) => limit,
280 _ => {
281 eprintln!("warn: NUM_JOBS is not set or could not be parsed. Defaulting to 1");
282 1
283 }
284 };
285
286 let client = Client::new(job_limit).expect("Failed to create a job server");
289 client.acquire_raw().expect("Failed to acquire initial job token");
290 client
291 })
292 });
293
294 jobserver.release_raw().unwrap();
299
300 let thread_results: Vec<_> = std::thread::scope(|s| {
301 let mut handles = Vec::with_capacity(files.len());
302
303 for file in files {
304 let token = jobserver.acquire().expect("Failed to acquire job token");
306 let handle = s.spawn(move || {
307 let result = self.compile_file(nasm, file, args, src, dst);
308 drop(token);
310 result
311 });
312 handles.push(handle);
313 }
314
315 handles.into_iter().map(|h| h.join()).collect()
317 });
318
319 jobserver.acquire_raw().expect("Failed to reacquire implicit token");
321
322 thread_results
324 .into_iter()
325 .map(|thread_res| thread_res.unwrap_or_else(|e| panic::resume_unwind(e)))
326 .collect()
327 }
328
329 #[cfg(not(feature = "parallel"))]
330 fn compile_objects_inner(
331 &self,
332 nasm: &Path,
333 files: &[PathBuf],
334 args: &[&str],
335 src: &Path,
336 dst: &Path,
337 ) -> Result<Vec<PathBuf>, String> {
338 files
339 .iter()
340 .map(|file| self.compile_file(&nasm, file, &args, src, dst))
341 .collect()
342 }
343
344 fn get_args(&self, target: &str) -> Vec<&str> {
345 let (arch_flag, debug_flag) = parse_triple(&target);
346 let mut args = vec![arch_flag];
347
348 if self.debug {
349 args.push(debug_flag);
350 }
351
352 for arg in &self.flags {
353 args.push(arg);
354 }
355
356 args
357 }
358
359 fn compile_file(
360 &self,
361 nasm: &Path,
362 file: &Path,
363 new_args: &[&str],
364 src: &Path,
365 dst: &Path,
366 ) -> Result<PathBuf, String> {
367 let obj = dst.join(file.file_name().unwrap()).with_extension("o");
368 let mut cmd = Command::new(nasm);
369 cmd.args(&new_args[..]);
370 std::fs::create_dir_all(&obj.parent().unwrap()).unwrap();
371
372 run(cmd.arg(src.join(file)).arg("-o").arg(&obj))?;
373 Ok(obj)
374 }
375
376 fn archive(&self, out_dir: &Path, lib: &str, objs: &[PathBuf]) -> Result<(), String> {
377 let ar_is_msvc = self.archiver_is_msvc.unwrap_or(cfg!(target_env = "msvc"));
378
379 let ar = if ar_is_msvc {
380 self.archiver.clone().unwrap_or_else(|| "lib".into())
381 } else {
382 self.archiver
383 .clone()
384 .or_else(|| env::var_os("AR").map(|a| a.into()))
385 .unwrap_or_else(|| "ar".into())
386 };
387 if ar_is_msvc {
388 let mut out_param = OsString::new();
389 out_param.push("/OUT:");
390 out_param.push(out_dir.join(lib).as_os_str());
391 run(Command::new(ar).arg(out_param).args(objs))
392 } else {
393 run(Command::new(ar)
394 .arg("crus")
395 .arg(out_dir.join(lib))
396 .args(objs))
397 }
398 }
399
400 fn get_out_dir(&self) -> PathBuf {
401 self.out_dir
402 .clone()
403 .unwrap_or_else(|| PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR must be set")))
404 }
405
406 fn get_target(&self) -> String {
407 self.target
408 .clone()
409 .unwrap_or_else(|| env::var("TARGET").expect("TARGET must be set"))
410 }
411
412 fn is_nasm_found_and_new_enough(&self, nasm_path: &Path) -> Result<(), String> {
415 let version = get_output(Command::new(nasm_path).arg("-v"))
416 .map_err(|e| format!("Unable to run {}: {}", nasm_path.display(), e))?;
417 let (major, minor, micro) = self.min_version;
418 let ver = parse_nasm_version(&version)?;
419 if major > ver.0
420 || (major == ver.0 && minor > ver.1)
421 || (major == ver.0 && minor == ver.1 && micro > ver.2)
422 {
423 Err(format!(
424 "This version of NASM is too old: {}. Required >= {}.{}.{}",
425 version, major, minor, micro
426 ))
427 } else {
428 Ok(())
429 }
430 }
431
432 fn find_nasm(&mut self) -> Result<PathBuf, String> {
433 let paths = match &self.nasm {
434 Some(p) => vec![p.to_owned()],
435 None => {
436 let path = env::var_os("PATH").unwrap_or_default();
440 std::iter::once(PathBuf::from("nasm"))
441 .chain(env::split_paths(&path).map(|p| p.join("nasm")))
442 .collect()
443 }
444 };
445
446 let mut first_error = None;
447 for nasm_path in paths {
448 match self.is_nasm_found_and_new_enough(&nasm_path) {
449 Ok(_) => return Ok(nasm_path),
450 Err(err) => {
451 let _ = first_error.get_or_insert(err);
452 }
453 }
454 }
455 Err(first_error.unwrap())
456 }
457}
458
459fn parse_nasm_version(version: &str) -> Result<(usize, usize, usize), String> {
460 let mut ver = version
461 .split(' ')
462 .nth(2)
463 .ok_or_else(|| format!("Invalid nasm version '{}'", version))?;
464
465 if let Some(ver_rc) = ver.find("rc") {
467 ver = &ver[0..ver_rc];
468 }
469 let ver: Vec<_> = ver
470 .split('.')
471 .map(|v| v.parse())
472 .take_while(Result::is_ok)
473 .map(Result::unwrap)
474 .collect();
475
476 Ok((
477 ver[0],
478 ver.get(1).copied().unwrap_or(0),
479 ver.get(2).copied().unwrap_or(0),
480 ))
481}
482
483fn get_output(cmd: &mut Command) -> Result<String, String> {
484 let out = cmd.output().map_err(|e| e.to_string())?;
485 if out.status.success() {
486 Ok(String::from_utf8_lossy(&out.stdout).to_string())
487 } else {
488 Err(String::from_utf8_lossy(&out.stderr).to_string())
489 }
490}
491
492fn run(cmd: &mut Command) -> Result<(), String> {
493 info!("running: {:?}", cmd);
494
495 let status = match cmd
496 .stdout(Stdio::inherit())
497 .stderr(Stdio::inherit())
498 .status()
499 {
500 Ok(status) => status,
501
502 Err(e) => return Err(format!("failed to spawn process: {}", e)),
503 };
504
505 if !status.success() {
506 return Err(format!("nonzero exit status: {}", status));
507 }
508 Ok(())
509}
510
511#[test]
512fn test_build() {
513 let mut build = Build::new();
514 build.file("test");
515 build.archiver("ar");
516 build.include("./");
517 build.include("dir");
518 build.define("foo", Some("1"));
519 build.define("bar", None);
520 build.flag("-test");
521 build.target("i686-unknown-linux-musl");
522 build.out_dir("/tmp");
523 build.min_version(0, 0, 0);
524
525 assert_eq!(
526 build.get_args("i686-unknown-linux-musl"),
527 &["-felf32", "-I./", "-Idir/", "-Dfoo=1", "-Dbar", "-test"]
528 );
529}
530
531#[test]
532fn test_parse_nasm_version() {
533 let ver_str = "NASM version 2.14.02 compiled on Jan 22 2019";
534 assert_eq!((2, 14, 2), parse_nasm_version(ver_str).unwrap());
535 let ver_str = "NASM version 2.14.02";
536 assert_eq!((2, 14, 2), parse_nasm_version(ver_str).unwrap());
537 let ver_str = "NASM version 2.14 compiled on Jan 22 2019";
538 assert_eq!((2, 14, 0), parse_nasm_version(ver_str).unwrap());
539 let ver_str = "NASM version 2.14";
540 assert_eq!((2, 14, 0), parse_nasm_version(ver_str).unwrap());
541 let ver_str = "NASM version 2.14rc2";
542 assert_eq!((2, 14, 0), parse_nasm_version(ver_str).unwrap());
543}
544
545#[test]
546fn test_parse_triple() {
547 let triple = "x86_64-unknown-linux-gnux32";
548 assert_eq!(parse_triple(&triple), ("-felfx32", "-gdwarf"));
549
550 let triple = "x86_64-unknown-linux";
551 assert_eq!(parse_triple(&triple), ("-felf64", "-gdwarf"));
552}