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