nasm_rs/
lib.rs

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    // ARCH-VENDOR-OS-ENVIRONMENT
32    // or ARCH-VENDOR-OS
33    // we don't care about environ (yes, we do... gnux32) so doesn't matter if triple doesn't have it
34    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
51/// # Example
52///
53/// ```no_run
54/// nasm_rs::compile_library("libfoo.a", &["foo.s", "bar.s"]).unwrap();
55/// ```
56pub fn compile_library(output: &str, files: &[&str]) -> Result<(), String> {
57    compile_library_args(output, files, &[])
58}
59
60/// # Example
61///
62/// ```no_run
63/// nasm_rs::compile_library_args("libfoo.a", &["foo.s", "bar.s"], &["-Fdwarf"]).unwrap();
64/// ```
65pub 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    /// Add a file which will be compiled
108    ///
109    /// e.g. `"foo.s"`
110    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    /// Set multiple files
116    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    /// Add a directory to the `-I` include path
124    pub fn include<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
125        let mut flag = format!("-I{}", dir.as_ref().display());
126        // nasm requires trailing slash, but `Path` may omit it.
127        if !flag.ends_with('/') {
128            flag += "/";
129        }
130        self.flags.push(flag);
131        self
132    }
133
134    /// Pre-define a macro with an optional value
135    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    /// Configures whether the assembler will generate debug information.
147    ///
148    /// This option is automatically scraped from the `DEBUG` environment
149    /// variable by build scripts (only enabled when the profile is "debug"), so
150    /// it's not required to call this function.
151    pub fn debug(&mut self, enable: bool) -> &mut Self {
152        self.debug = enable;
153        self
154    }
155
156    /// Add an arbitrary flag to the invocation of the assembler
157    ///
158    /// e.g. `"-Fdwarf"`
159    pub fn flag(&mut self, flag: &str) -> &mut Self {
160        self.flags.push(flag.to_owned());
161        self
162    }
163
164    /// Configures the target this configuration will be compiling for.
165    ///
166    /// This option is automatically scraped from the `TARGET` environment
167    /// variable by build scripts, so it's not required to call this function.
168    pub fn target(&mut self, target: &str) -> &mut Self {
169        self.target = Some(target.to_owned());
170        self
171    }
172
173    /// Configures the output directory where all object files and static libraries will be located.
174    ///
175    /// This option is automatically scraped from the OUT_DIR environment variable by build scripts,
176    /// so it's not required to call this function.
177    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    /// Configures the tool used to assemble archives.
183    ///
184    /// This option is automatically determined from the target platform or a
185    /// number of environment variables, so it's not required to call this
186    /// function.
187    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    /// Configures the default archiver tool as well as the command syntax.
193    ///
194    /// This option is automatically determined from `cfg!(target_env = "msvc")`,
195    /// so it's not required to call this function.
196    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    /// Configures path to `nasm` command
202    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    /// Set the minimum version required
208    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    /// Run the compiler, generating the file output
214    ///
215    /// The name output should be the base name of the library,
216    /// without file extension, and without "lib" prefix.
217    ///
218    /// The output file will have target-specific name,
219    /// such as `lib*.a` (non-MSVC) or `*.lib` (MSVC).
220    pub fn compile(&mut self, lib_name: &str) -> Result<(), String> {
221        // Trim name for backwards comatibility
222        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    /// Run the compiler, generating .o files
244    ///
245    /// The files can be linked in a separate step, e.g. passed to `cc`
246    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            // Try getting a jobserver from the environment (cargo, make, ...)
274            unsafe { Client::from_env() }.unwrap_or_else(|| {
275                // If that fails, try to create our own jobserver based on NUM_JOBS
276                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                // Reserve a job token for this process so the behavior
285                // is consistent with external job servers.
286                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        // Release the implicit job token for this process while NASM is running.
293        // Without this, the maximum number of NASM processes would be (NUM_JOBS - 1).
294        // This would mean that a build process with NUM_JOBS=1 would have
295        // no tokens left for NASM to run, causing the build to stall.
296        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                // Wait for a job token before starting the build
303                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                    // Release the token ASAP so that another job can start
307                    drop(token);
308                    result
309                });
310                handles.push(handle);
311            }
312
313            // Collect results from all threads without handling panics
314            handles.into_iter().map(|h| h.join()).collect()
315        });
316
317        // Reacquire the implicit job token (see comments above for more info).
318        jobserver.acquire_raw().expect("Failed to reacquire implicit token");
319
320        // Only handle thread panics after all threads have stopped
321        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    /// Returns version string if nasm is too old,
411    /// or error message string if it's unusable.
412    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                // Xcode has an outdated verison of nasm,
435                // and puts its own SDK first in the PATH.
436                // The proper Homebrew nasm is later in the PATH.
437                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    //this will probably break at some point...
464    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}