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 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 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 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 "z" | "s" | "1" if is_msvc() => flags.push("-O1".into()),
211 "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}