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