1 // Build script.
2 //
3 // This program is run by Cargo when building cranelift-codegen. It is used to generate Rust code from
4 // the language definitions in the cranelift-codegen/meta directory.
5 //
6 // Environment:
7 //
8 // OUT_DIR
9 //     Directory where generated files should be placed.
10 //
11 // TARGET
12 //     Target triple provided by Cargo.
13 //
14 // The build script expects to be run from the directory where this build.rs file lives. The
15 // current directory is used to find the sources.
16 
17 use cranelift_codegen_meta as meta;
18 use cranelift_isle::error::Errors;
19 use meta::isle::IsleCompilation;
20 
21 use std::env;
22 use std::io::Read;
23 use std::process;
24 use std::time::Instant;
25 
main()26 fn main() {
27     let start_time = Instant::now();
28 
29     let out_dir = env::var("OUT_DIR").expect("The OUT_DIR environment variable must be set");
30     let out_dir = std::path::Path::new(&out_dir);
31     let target_triple = env::var("TARGET").expect("The TARGET environment variable must be set");
32 
33     let all_arch = env::var("CARGO_FEATURE_ALL_ARCH").is_ok();
34     let all_native_arch = env::var("CARGO_FEATURE_ALL_NATIVE_ARCH").is_ok();
35 
36     let mut isas = meta::isa::Isa::all()
37         .iter()
38         .cloned()
39         .filter(|isa| {
40             let env_key = match isa {
41                 meta::isa::Isa::Pulley32 | meta::isa::Isa::Pulley64 => {
42                     "CARGO_FEATURE_PULLEY".to_string()
43                 }
44                 _ => format!("CARGO_FEATURE_{}", isa.to_string().to_uppercase()),
45             };
46             all_arch || env::var(env_key).is_ok()
47         })
48         .collect::<Vec<_>>();
49 
50     // Don't require host isa if under 'all-arch' feature.
51     let host_isa = env::var("CARGO_FEATURE_HOST_ARCH").is_ok() && !all_native_arch;
52 
53     if isas.is_empty() || host_isa {
54         // Try to match native target.
55         let target_name = target_triple.split('-').next().unwrap();
56         if let Ok(isa) = meta::isa_from_arch(&target_name) {
57             println!("cargo:rustc-cfg=feature=\"{isa}\"");
58             isas.push(isa);
59         }
60     }
61 
62     let cur_dir = env::current_dir().expect("Can't access current working directory");
63     let crate_dir = cur_dir.as_path();
64 
65     println!("cargo:rerun-if-changed=build.rs");
66     println!("cargo:rerun-if-env-changed=ISLE_SOURCE_DIR");
67 
68     let isle_dir = if let Ok(path) = std::env::var("ISLE_SOURCE_DIR") {
69         // This will canonicalize any relative path in terms of the
70         // crate root, and will take any absolute path as overriding the
71         // `crate_dir`.
72         crate_dir.join(&path)
73     } else {
74         out_dir.into()
75     };
76 
77     std::fs::create_dir_all(&isle_dir).expect("Could not create ISLE source directory");
78 
79     if let Err(err) = meta::generate(&isas, &out_dir, &isle_dir) {
80         eprintln!("Error: {err}");
81         process::exit(1);
82     }
83 
84     if &std::env::var("SKIP_ISLE").unwrap_or("0".to_string()) != "1" {
85         if let Err(err) = build_isle(crate_dir, &isle_dir) {
86             eprintln!("Error: {err}");
87             process::exit(1);
88         }
89     }
90 
91     if env::var("CRANELIFT_VERBOSE").is_ok() {
92         for isa in &isas {
93             println!("cargo:warning=Includes support for {} ISA", isa.to_string());
94         }
95         println!(
96             "cargo:warning=Build step took {:?}.",
97             Instant::now() - start_time
98         );
99         println!("cargo:warning=Generated files are in {}", out_dir.display());
100     }
101 
102     let pkg_version = env::var("CARGO_PKG_VERSION").unwrap();
103     let mut cmd = std::process::Command::new("git");
104     cmd.arg("rev-parse")
105         .arg("HEAD")
106         .stdout(std::process::Stdio::piped())
107         .current_dir(env::var("CARGO_MANIFEST_DIR").unwrap());
108     let version = if let Ok(mut child) = cmd.spawn() {
109         let mut git_rev = String::new();
110         child
111             .stdout
112             .as_mut()
113             .unwrap()
114             .read_to_string(&mut git_rev)
115             .unwrap();
116         let status = child.wait().unwrap();
117         if status.success() {
118             let git_rev = git_rev.trim().chars().take(9).collect::<String>();
119             format!("{pkg_version}-{git_rev}")
120         } else {
121             // not a git repo
122             pkg_version
123         }
124     } else {
125         // git not available
126         pkg_version
127     };
128     std::fs::write(
129         std::path::Path::new(&out_dir).join("version.rs"),
130         format!(
131             "/// Version number of this crate. \n\
132             pub const VERSION: &str = \"{version}\";"
133         ),
134     )
135     .unwrap();
136 }
137 
138 /// Strip the current directory from the file paths, because `islec`
139 /// includes them in the generated source, and this helps us maintain
140 /// deterministic builds that don't include those local file paths.
make_isle_source_path_relative( cur_dir: &std::path::Path, filename: &std::path::Path, ) -> std::path::PathBuf141 fn make_isle_source_path_relative(
142     cur_dir: &std::path::Path,
143     filename: &std::path::Path,
144 ) -> std::path::PathBuf {
145     if let Ok(suffix) = filename.strip_prefix(&cur_dir) {
146         suffix.to_path_buf()
147     } else {
148         filename.to_path_buf()
149     }
150 }
151 
build_isle( crate_dir: &std::path::Path, isle_dir: &std::path::Path, ) -> Result<(), Box<dyn std::error::Error + 'static>>152 fn build_isle(
153     crate_dir: &std::path::Path,
154     isle_dir: &std::path::Path,
155 ) -> Result<(), Box<dyn std::error::Error + 'static>> {
156     let cur_dir = std::env::current_dir()?;
157     let isle_compilations = meta::isle::get_isle_compilations(
158         &make_isle_source_path_relative(&cur_dir, &crate_dir),
159         &make_isle_source_path_relative(&cur_dir, &isle_dir),
160     );
161 
162     let mut had_error = false;
163     for compilation in &isle_compilations.items {
164         for file in &compilation.inputs {
165             println!("cargo:rerun-if-changed={}", file.display());
166         }
167 
168         if let Err(e) = run_compilation(compilation) {
169             had_error = true;
170             eprintln!("Error building ISLE files:");
171             eprintln!("{e:?}");
172             #[cfg(not(feature = "isle-errors"))]
173             {
174                 eprintln!("To see a more detailed error report, run: ");
175                 eprintln!();
176                 eprintln!("    $ cargo check -p cranelift-codegen --features isle-errors");
177                 eprintln!();
178             }
179         }
180     }
181 
182     if had_error {
183         std::process::exit(1);
184     }
185 
186     println!("cargo:rustc-env=ISLE_DIR={}", isle_dir.to_str().unwrap());
187 
188     Ok(())
189 }
190 
191 /// Build ISLE DSL source text into generated Rust code.
192 ///
193 /// NB: This must happen *after* the `cranelift-codegen-meta` functions, since
194 /// it consumes files generated by them.
run_compilation(compilation: &IsleCompilation) -> Result<(), Errors>195 fn run_compilation(compilation: &IsleCompilation) -> Result<(), Errors> {
196     use cranelift_isle as isle;
197 
198     eprintln!("Rebuilding {}", compilation.output.display());
199 
200     let code = {
201         let file_paths = compilation
202             .inputs
203             .iter()
204             .chain(compilation.untracked_inputs.iter());
205 
206         let mut options = isle::codegen::CodegenOptions::default();
207         // Because we include!() the generated ISLE source, we cannot
208         // put the global pragmas (`#![allow(...)]`) in the ISLE
209         // source itself; we have to put them in the source that
210         // include!()s it. (See
211         // https://github.com/rust-lang/rust/issues/47995.)
212         options.exclude_global_allow_pragmas = true;
213 
214         // When `cranelift-codegen` is built with detailed tracing enabled, also
215         // ask the ISLE compiler to emit `log::{debug,trace}!` invocations in
216         // the generated code to help debug rule matching.
217         options.emit_logging = std::env::var("CARGO_FEATURE_TRACE_LOG").is_ok();
218 
219         // Enable optional match-arm splitting in iterator terms for
220         // faster compile times in release builds.
221         //
222         // In debug builds, *always* split with an aggressive
223         // threshold, because we cannot rely on rustc doing regalloc
224         // on all of the local bindings to shrink the stack frame to a
225         // reasonable size.
226         if cfg!(debug_assertions) {
227             options.split_match_arms = true;
228             options.match_arm_split_threshold = Some(4);
229         } else {
230             options.split_match_arms = std::env::var("CARGO_FEATURE_ISLE_SPLIT_MATCH").is_ok();
231             if let Ok(value) = std::env::var("ISLE_SPLIT_MATCH_THRESHOLD") {
232                 options.match_arm_split_threshold = Some(value.parse().unwrap_or_else(|err| {
233                     panic!("invalid ISLE_SPLIT_MATCH_THRESHOLD value '{value}': {err}");
234                 }));
235             }
236         }
237 
238         if let Ok(out_dir) = std::env::var("OUT_DIR") {
239             options.prefixes.push(isle::codegen::Prefix {
240                 prefix: out_dir,
241                 name: "<OUT_DIR>".to_string(),
242             })
243         };
244 
245         isle::compile::from_files(file_paths, &options)?
246     };
247 
248     eprintln!(
249         "Writing ISLE-generated Rust code to {}",
250         compilation.output.display()
251     );
252     std::fs::write(&compilation.output, code)
253         .map_err(|e| Errors::from_io(e, "failed writing output"))?;
254 
255     Ok(())
256 }
257