Skip to main content

rb_sys_build/
cc.rs

1use crate::{
2    debug_log, rb_config,
3    utils::{is_msvc, shellsplit},
4};
5use std::{
6    collections::{hash_map::DefaultHasher, HashSet},
7    env,
8    ffi::{OsStr, OsString},
9    fs,
10    hash::Hasher,
11    path::{Path, PathBuf},
12    process::{Command, ExitStatus, Stdio},
13};
14
15type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
16
17const WELL_KNOWN_WRAPPERS: &[&str] = &["sccache", "cachepot"];
18
19#[derive(Default, Debug)]
20pub struct Build {
21    files: Vec<PathBuf>,
22    flags: Vec<String>,
23}
24
25impl Build {
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    pub fn default_cflags() -> Vec<String> {
31        let mut cflags = vec![];
32
33        if cfg!(target_os = "openbsd") {
34            cflags.push("-fdeclspec".into());
35        } else {
36            cflags.push("-fms-extensions".into());
37        };
38
39        cflags
40    }
41
42    pub fn file(&mut self, file: PathBuf) {
43        println!("cargo:rerun-if-changed={}", file.display());
44        self.files.push(file);
45    }
46
47    pub fn try_compile(self, name: &str) -> Result<()> {
48        let compiler = get_compiler();
49        let archiver = get_archiver();
50        let out_dir = PathBuf::from(env::var("OUT_DIR")?).join("cc");
51        fs::create_dir_all(&out_dir)?;
52        let rb = rb_config();
53
54        let object_files = self.compile_each_file(compiler, &rb, &out_dir)?;
55        debug_log!("INFO: compiled object files: {:?}", object_files);
56        let (lib_path, lib_name) =
57            self.archive_object_files(archiver.copied(), name, &out_dir, object_files)?;
58        if let Err(e) = self.strip_archived_objects(archiver, &lib_path) {
59            debug_log!("WARN: failed to strip archived objects: {:?}", e);
60        }
61
62        println!("cargo:rustc-link-search=native={}", out_dir.display());
63        println!("cargo:rustc-link-lib=static={}", lib_name);
64
65        Ok(())
66    }
67
68    fn compile_each_file(
69        &self,
70        compiler: Command,
71        rb: &rb_config::RbConfig,
72        out_dir: &Path,
73    ) -> Result<HashSet<PathBuf>> {
74        self.files
75            .iter()
76            .map(|f| self.compile_file(f, compiler.copied(), rb, out_dir))
77            .collect()
78    }
79
80    fn compile_file(
81        &self,
82        f: &Path,
83        compiler: Command,
84        rb: &rb_config::RbConfig,
85        out_dir: &Path,
86    ) -> Result<PathBuf> {
87        let mut hasher = DefaultHasher::new();
88        hasher.write(fs::read(f)?.as_slice());
89
90        let object_file = out_dir
91            .join(hasher.finish().to_string())
92            .with_extension("o");
93
94        let mut cmd = compiler;
95        cmd.args(get_include_args(rb))
96            .arg("-c")
97            .arg(f)
98            .args(&rb.cflags)
99            .args(get_common_args())
100            .args(&self.flags)
101            .args(get_output_file_flag(&object_file));
102
103        run_command(cmd)?;
104
105        Ok(object_file)
106    }
107
108    fn archive_object_files(
109        &self,
110        archiver: Command,
111        name: &str,
112        out_dir: &Path,
113        object_files: HashSet<PathBuf>,
114    ) -> Result<(PathBuf, String)> {
115        let mut cmd = archiver;
116        let mut hasher = DefaultHasher::new();
117        object_files
118            .iter()
119            .for_each(|f| hasher.write(f.as_os_str().as_encoded_bytes()));
120        let lib_name = format!("{}-{}", name, hasher.finish());
121        let lib_filename = format!("lib{}.a", lib_name);
122        let dst = out_dir.join(lib_filename);
123
124        // The argument structure differs for MSVC and GCC.
125        if is_msvc() {
126            cmd.arg(format!("/OUT:{}", dst.display()));
127            cmd.args(&object_files);
128        } else {
129            cmd.env("ZERO_AR_DATE", "1").arg("crs").arg(&dst);
130            cmd.args(&object_files);
131        }
132
133        run_command(cmd)?;
134
135        // The Rust compiler will look for libfoo.a and foo.lib, but the
136        // MSVC linker will also be passed foo.lib, so be sure that both
137        // exist for now.
138        if is_msvc() {
139            let lib_dst = dst.with_file_name(format!("{}.lib", lib_name));
140            let _ = fs::remove_file(&lib_dst);
141            match fs::hard_link(&dst, &lib_dst).or_else(|_| {
142                // if hard-link fails, just copy (ignoring the number of bytes written)
143                fs::copy(&dst, &lib_dst).map(|_| ())
144            }) {
145                Ok(_) => (),
146                Err(_) => {
147                    return Err(
148                        "Could not copy or create a hard-link to the generated lib file.".into(),
149                    );
150                }
151            };
152        }
153
154        Ok((dst, lib_name))
155    }
156
157    fn strip_archived_objects(&self, archiver: Command, libpath: &Path) -> Result<()> {
158        let mut cmd = archiver;
159
160        if is_msvc() {
161            cmd.arg("/LTCG").arg(libpath);
162        } else {
163            cmd.arg("s").arg(libpath);
164        }
165
166        run_command(cmd)?;
167
168        Ok(())
169    }
170}
171
172fn get_include_args(rb: &rb_config::RbConfig) -> Vec<String> {
173    let mut args = vec![];
174    if let Some(include_dir) = rb.get("rubyhdrdir") {
175        args.push(format!("-I{}", include_dir));
176    }
177    if let Some(arch_include_dir) = rb.get("rubyarchhdrdir") {
178        args.push(format!("-I{}", arch_include_dir));
179    }
180    if let Some(internal_include_dir) = rb.get("rubyhdrdir") {
181        args.push(format!("-I{}/include/internal", internal_include_dir));
182    }
183    if let Some(impl_include_dir) = rb.get("rubyhdrdir") {
184        args.push(format!("-I{}/include/impl", impl_include_dir));
185    }
186
187    args
188}
189
190fn get_common_args() -> Vec<String> {
191    fn add_debug_flags(flags: &mut Vec<String>) {
192        match env::var("DEBUG") {
193            Ok(val) if val == "true" => {
194                if is_msvc() {
195                    flags.push("-Z7".into());
196                } else if cfg!(target_os = "linux") {
197                    flags.push("-gdwarf-4".into());
198                } else {
199                    flags.push("-gdwarf-2".into());
200                }
201            }
202            _ => {}
203        }
204    }
205
206    fn add_opt_level(flags: &mut Vec<String>) {
207        if let Ok(val) = env::var("OPT_LEVEL") {
208            match val.as_str() {
209                // Msvc uses /O1 to enable all optimizations that minimize code size.
210                "z" | "s" | "1" if is_msvc() => flags.push("-O1".into()),
211                // -O3 is a valid value for gcc and clang compilers, but not msvc. Cap to /O2.
212                "2" | "3" if is_msvc() => flags.push("-O2".into()),
213                lvl => flags.push(format!("-O{}", lvl)),
214            }
215        }
216    }
217
218    fn add_compiler_flags(flags: &mut Vec<String>) {
219        if !is_msvc() {
220            flags.push("-ffunction-sections".into());
221            flags.push("-fdata-sections".into());
222            flags.push("-fPIC".into());
223            flags.push("-fno-omit-frame-pointer".into());
224        }
225
226        flags.extend(Build::default_cflags());
227    }
228
229    let mut items = vec![];
230
231    add_debug_flags(&mut items);
232    add_compiler_flags(&mut items);
233    add_opt_level(&mut items);
234
235    items
236}
237
238fn get_compiler() -> Command {
239    let cmd = get_tool("CC", "cc");
240    let cmd_program = cmd.get_program().to_str().unwrap_or_default();
241    let already_wrapped = WELL_KNOWN_WRAPPERS.iter().any(|w| cmd_program.contains(w));
242
243    match get_tool_from_rb_config_or_env("CC_WRAPPER") {
244        Some(wrapper) if !wrapper.is_empty() && !already_wrapped => {
245            debug_log!("INFO: using CC_WRAPPER ({:?})", wrapper);
246            cmd.wrapped(wrapper)
247        }
248        _ => match rustc_wrapper_fallback() {
249            Some(wrapper) if !already_wrapped => cmd.wrapped(wrapper),
250            _ => cmd,
251        },
252    }
253}
254
255pub fn rustc_wrapper_fallback() -> Option<String> {
256    let rustc_wrapper = std::env::var("RUSTC_WRAPPER").ok()?;
257    rustc_wrapper_fallback_detect(rustc_wrapper)
258}
259
260pub fn rustc_wrapper_fallback_detect(rustc_wrapper: String) -> Option<String> {
261    let wrapper_path = Path::new(&rustc_wrapper);
262    let wrapper_stem = wrapper_path.file_stem()?;
263
264    if WELL_KNOWN_WRAPPERS.contains(&wrapper_stem.to_str()?) {
265        debug_log!("INFO: using RUSTC_WRAPPER ({:?})", rustc_wrapper);
266        Some(rustc_wrapper)
267    } else {
268        None
269    }
270}
271
272fn get_archiver() -> Command {
273    let cmd = get_tool("AR", "ar");
274
275    if cmd.get_program() == "libtool" {
276        new_command("ar")
277    } else {
278        cmd
279    }
280}
281
282fn get_tool(env_var: &str, default: &str) -> Command {
283    let tool_args = get_tool_from_rb_config_or_env(env_var)
284        .unwrap_or_else(|| panic!("no {} tool found", env_var));
285
286    let mut tool_args = shellsplit(tool_args).into_iter();
287    let tool = tool_args.next().unwrap_or_else(|| default.to_string());
288
289    fn tool_exists(tool_name: &str) -> std::io::Result<bool> {
290        let path = PathBuf::from(tool_name);
291
292        if path.is_file() {
293            return Ok(true);
294        }
295
296        match Command::new(tool_name).spawn() {
297            Ok(_) => Ok(true),
298            Err(e) => {
299                if e.kind() == std::io::ErrorKind::NotFound {
300                    Ok(false)
301                } else {
302                    Err(e)
303                }
304            }
305        }
306    }
307
308    let mut cmd = if tool_exists(&tool).unwrap_or(false) {
309        debug_log!("[INFO] using {tool} for {env_var}");
310        new_command(&tool)
311    } else {
312        debug_log!("[WARN] {tool} tool not found, falling back to {default}");
313        new_command(default)
314    };
315
316    cmd.args(tool_args.clone());
317
318    debug_log!("INFO: found {:?} tool ({:?})", env_var, &cmd);
319
320    cmd
321}
322
323fn get_tool_from_rb_config_or_env(env_var: &str) -> Option<String> {
324    let rb = rb_config();
325
326    get_tool_from_env(env_var)
327        .filter(|s| !s.is_empty())
328        .or_else(|| rb.get(env_var))
329}
330
331fn get_tool_from_env(env_var: &str) -> Option<String> {
332    let target_slug = env::var("TARGET").ok()?.replace('-', "_");
333    let env_var_with_target = format!("{}_{}", env_var, target_slug);
334
335    println!("cargo:rerun-if-env-changed={}", env_var);
336    println!("cargo:rerun-if-env-changed={}", env_var_with_target);
337
338    env::var(env_var)
339        .or_else(|_| env::var(env_var_with_target))
340        .ok()
341}
342
343fn run_command(mut cmd: Command) -> Result<ExitStatus> {
344    debug_log!("INFO: running command ({:?})", cmd);
345    let status = cmd.status()?;
346
347    if !status.success() {
348        Err(format!("Command '{:?}' failed with status: {}", cmd, status).into())
349    } else {
350        Ok(status)
351    }
352}
353
354fn new_command(name: &str) -> Command {
355    let mut cmd = Command::new(name);
356    cmd.stderr(Stdio::inherit()).stdout(Stdio::inherit());
357    cmd
358}
359
360fn get_output_file_flag(file: &Path) -> Vec<OsString> {
361    if is_msvc() {
362        vec![format!("-Fo{}", file.display()).into()]
363    } else {
364        vec!["-o".into(), file.into()]
365    }
366}
367
368pub trait CommandExt {
369    fn copied(&self) -> Command;
370    fn wrapped<W: AsRef<OsStr>>(&self, wrapper: W) -> Command;
371}
372
373impl CommandExt for Command {
374    fn copied(&self) -> Command {
375        let mut cmd = Command::new(self.get_program());
376        cmd.args(self.get_args());
377
378        for (k, v) in self.get_envs() {
379            if let Some(v) = v {
380                cmd.env(k, v);
381            } else {
382                cmd.env_remove(k);
383            }
384        }
385        cmd
386    }
387
388    fn wrapped<W: AsRef<OsStr>>(&self, wrapper: W) -> Command {
389        let mut new_cmd = Command::new(wrapper);
390
391        new_cmd.arg(self.get_program());
392
393        for arg in self.get_args() {
394            new_cmd.arg(arg);
395        }
396
397        for (k, v) in self.get_envs() {
398            if let Some(v) = v {
399                new_cmd.env(k, v);
400            } else {
401                new_cmd.env_remove(k);
402            }
403        }
404
405        new_cmd
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[test]
414    fn rustc_wrapper_detect() {
415        let wrapper = "/usr/local/bin/sccache";
416        assert_eq!(
417            rustc_wrapper_fallback_detect(wrapper.to_owned()),
418            Some(wrapper.to_owned())
419        );
420    }
421}