Skip to main content

nasm_rs/
lib.rs

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    // ARCH-VENDOR-OS-ENVIRONMENT
35    // or ARCH-VENDOR-OS
36    // we don't care about environ (yes, we do... gnux32) so doesn't matter if triple doesn't have it
37    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
54/// # Example
55///
56/// ```no_run
57/// nasm_rs::compile_library("libfoo.a", &["foo.s", "bar.s"]).unwrap();
58/// ```
59pub fn compile_library(output: &str, files: &[&str]) -> Result<(), String> {
60    compile_library_args(output, files, &[])
61}
62
63/// # Example
64///
65/// ```no_run
66/// nasm_rs::compile_library_args("libfoo.a", &["foo.s", "bar.s"], &["-Fdwarf"]).unwrap();
67/// ```
68pub 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    /// Add a file which will be compiled
116    ///
117    /// e.g. `"foo.s"`
118    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    /// Set multiple files
124    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    /// Add a directory to the `-I` include path
132    pub fn include<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
133        let mut flag = format!("-I{}", dir.as_ref().display());
134        // nasm requires trailing slash, but `Path` may omit it.
135        if !flag.ends_with('/') {
136            flag += "/";
137        }
138        self.flags.push(flag);
139        self
140    }
141
142    /// Pre-define a macro with an optional value
143    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    /// Configures whether the assembler will generate debug information.
155    ///
156    /// This option is automatically scraped from the `DEBUG` environment
157    /// variable by build scripts (only enabled when the profile is "debug"), so
158    /// it's not required to call this function.
159    pub fn debug(&mut self, enable: bool) -> &mut Self {
160        self.debug = enable;
161        self
162    }
163
164    /// Add an arbitrary flag to the invocation of the assembler
165    ///
166    /// e.g. `"-Fdwarf"`
167    pub fn flag(&mut self, flag: &str) -> &mut Self {
168        self.flags.push(flag.to_owned());
169        self
170    }
171
172    /// Configures the target this configuration will be compiling for.
173    ///
174    /// This option is automatically scraped from the `TARGET` environment
175    /// variable by build scripts, so it's not required to call this function.
176    pub fn target(&mut self, target: &str) -> &mut Self {
177        self.target = Some(target.to_owned());
178        self
179    }
180
181    /// Configures the output directory where all object files and static libraries will be located.
182    ///
183    /// This option is automatically scraped from the OUT_DIR environment variable by build scripts,
184    /// so it's not required to call this function.
185    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    /// Configures the tool used to assemble archives.
191    ///
192    /// This option is automatically determined from the target platform or a
193    /// number of environment variables, so it's not required to call this
194    /// function.
195    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    /// Configures the default archiver tool as well as the command syntax.
201    ///
202    /// This option is automatically determined from `cfg!(target_env = "msvc")`,
203    /// so it's not required to call this function.
204    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    /// Configures path to `nasm` command
210    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    /// Set the minimum version required
216    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    /// Run the compiler, generating the file output
222    ///
223    /// The name output should be the base name of the library,
224    /// without file extension, and without "lib" prefix.
225    ///
226    /// The output file will have target-specific name,
227    /// such as `lib*.a` (non-MSVC) or `*.lib` (MSVC).
228    pub fn compile(&mut self, lib_name: &str) -> Result<(), String> {
229        // Trim name for backwards comatibility
230        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    /// Run the compiler, generating .o files
252    ///
253    /// The files can be linked in a separate step, e.g. passed to `cc`
254    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            // Try getting a jobserver from the environment (cargo, make, ...)
282            unsafe { Client::from_env() }.unwrap_or_else(|| {
283                // If that fails, try to create our own jobserver based on NUM_JOBS
284                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                // Reserve a job token for this process so the behavior
293                // is consistent with external job servers.
294                let client = Client::new(job_limit).expect("Failed to create a job server");
295                client
296                    .acquire_raw() //acquire implicit token, do not return it.
297                    .expect("Failed to acquire initial job token");
298
299                client
300            })
301        });
302        //we'll be making use of Vec::pop() which removes the last element. Reverse the list so
303        //we're doing things in the order given (it might be important).
304        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                    // loop trying to acquire a token.
315                    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; //kill the helper thread and do not parellelize
322                            }
323                        }
324
325                        if helper_thread_list.read().unwrap().is_empty() {
326                            //out of files. break the outer loop so thread can exit
327                            break 'outer;
328                        }
329                        // so we're not spinning like crazy
330                        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                        // Release the token ASAP so that another job can start
346                        drop(token);
347                        result
348                    });
349                    handles.push(handle);
350                }
351                // Collect results from all threads without handling panics
352                let thread_res: Vec<_> = handles.into_iter().map(|h| h.join()).collect();
353                // Only handle thread panics after all threads have stopped
354                thread_res
355                    .into_iter()
356                    .map(|r| r.unwrap_or_else(|e| panic::resume_unwind(e)))
357                    .collect()
358            });
359
360            // From docs on jobserver behavior, the implicit token allows us to run one job without
361            // needing to talk to the job server. If number of available jobs is one, everything
362            // will be executed from here while the helper thread waits on a token that won't come.
363            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    /// Returns version string if nasm is too old,
481    /// or error message string if it's unusable.
482    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                // Xcode has an outdated verison of nasm,
505                // and puts its own SDK first in the PATH.
506                // The proper Homebrew nasm is later in the PATH.
507                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    //this will probably break at some point...
534    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}