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 
20 use std::env;
21 use std::io::Read;
22 use std::process;
23 use std::time::Instant;
24 
25 fn main() {
26     let start_time = Instant::now();
27 
28     let out_dir = env::var("OUT_DIR").expect("The OUT_DIR environment variable must be set");
29     let target_triple = env::var("TARGET").expect("The TARGET environment variable must be set");
30 
31     let mut isas = meta::isa::Isa::all()
32         .iter()
33         .cloned()
34         .filter(|isa| {
35             let env_key = format!("CARGO_FEATURE_{}", isa.to_string().to_uppercase());
36             env::var(env_key).is_ok()
37         })
38         .collect::<Vec<_>>();
39 
40     let host_isa = env::var("CARGO_FEATURE_HOST_ARCH").is_ok();
41 
42     if isas.is_empty() || host_isa {
43         // Try to match native target.
44         let target_name = target_triple.split('-').next().unwrap();
45         let isa = meta::isa_from_arch(&target_name).expect("error when identifying target");
46         println!("cargo:rustc-cfg=feature=\"{}\"", isa);
47         isas.push(isa);
48     }
49 
50     let cur_dir = env::current_dir().expect("Can't access current working directory");
51     let crate_dir = cur_dir.as_path();
52 
53     println!("cargo:rerun-if-changed=build.rs");
54 
55     let explicit_isle_dir = &crate_dir.join("isle_generated_code");
56     #[cfg(feature = "isle-in-source-tree")]
57     let isle_dir = explicit_isle_dir;
58     #[cfg(not(feature = "isle-in-source-tree"))]
59     let isle_dir = std::path::Path::new(&out_dir);
60 
61     #[cfg(feature = "isle-in-source-tree")]
62     {
63         std::fs::create_dir_all(isle_dir).expect("Could not create ISLE source directory");
64     }
65     #[cfg(not(feature = "isle-in-source-tree"))]
66     {
67         if explicit_isle_dir.is_dir() {
68             eprintln!(concat!(
69                 "Error: directory isle_generated_code/ exists but is only used when\n",
70                 "`--feature isle-in-source-tree` is specified. To prevent confusion,\n",
71                 "this build script requires the directory to be removed when reverting\n",
72                 "to the usual generated code in target/. Please delete the directory and\n",
73                 "re-run this build.\n",
74             ));
75             std::process::exit(1);
76         }
77     }
78 
79     if let Err(err) = meta::generate(&isas, &out_dir, isle_dir.to_str().unwrap()) {
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);
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 = \"{}\";",
133             version
134         ),
135     )
136     .unwrap();
137 }
138 
139 /// Strip the current directory from the file paths, because `islec`
140 /// includes them in the generated source, and this helps us maintain
141 /// deterministic builds that don't include those local file paths.
142 fn make_isle_source_path_relative(
143     cur_dir: &std::path::PathBuf,
144     filename: std::path::PathBuf,
145 ) -> std::path::PathBuf {
146     if let Ok(suffix) = filename.strip_prefix(&cur_dir) {
147         suffix.to_path_buf()
148     } else {
149         filename
150     }
151 }
152 
153 /// A list of compilations (transformations from ISLE source to
154 /// generated Rust source) that exist in the repository.
155 ///
156 /// This list is used either to regenerate the Rust source in-tree (if
157 /// the `rebuild-isle` feature is enabled), or to verify that the ISLE
158 /// source in-tree corresponds to the ISLE source that was last used
159 /// to rebuild the Rust source (if the `rebuild-isle` feature is not
160 /// enabled).
161 #[derive(Clone, Debug)]
162 struct IsleCompilations {
163     items: Vec<IsleCompilation>,
164 }
165 
166 #[derive(Clone, Debug)]
167 struct IsleCompilation {
168     output: std::path::PathBuf,
169     inputs: Vec<std::path::PathBuf>,
170     untracked_inputs: Vec<std::path::PathBuf>,
171 }
172 
173 /// Construct the list of compilations (transformations from ISLE
174 /// source to generated Rust source) that exist in the repository.
175 fn get_isle_compilations(
176     crate_dir: &std::path::Path,
177     out_dir: &std::path::Path,
178 ) -> Result<IsleCompilations, std::io::Error> {
179     let cur_dir = std::env::current_dir()?;
180 
181     // Preludes.
182     let clif_lower_isle = out_dir.join("clif_lower.isle");
183     let clif_opt_isle = out_dir.join("clif_opt.isle");
184     let prelude_isle =
185         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("prelude.isle"));
186     let prelude_opt_isle =
187         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("prelude_opt.isle"));
188     let prelude_lower_isle =
189         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("prelude_lower.isle"));
190 
191     // Directory for mid-end optimizations.
192     let src_opts = make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("opts"));
193     // Directories for lowering backends.
194     let src_isa_x64 =
195         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("x64"));
196     let src_isa_aarch64 =
197         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("aarch64"));
198     let src_isa_s390x =
199         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("s390x"));
200 
201     let src_isa_risc_v =
202         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("riscv64"));
203     // This is a set of ISLE compilation units.
204     //
205     // The format of each entry is:
206     //
207     //     (output Rust code file, input ISLE source files)
208     //
209     // There should be one entry for each backend that uses ISLE for lowering,
210     // and if/when we replace our peephole optimization passes with ISLE, there
211     // should be an entry for each of those as well.
212     //
213     // N.B.: add any new compilation outputs to
214     // `scripts/force-rebuild-isle.sh` if they do not fit the pattern
215     // `cranelift/codegen/src/isa/*/lower/isle/generated_code.rs`!
216     Ok(IsleCompilations {
217         items: vec![
218             // The mid-end optimization rules.
219             IsleCompilation {
220                 output: out_dir.join("isle_opt.rs"),
221                 inputs: vec![
222                     prelude_isle.clone(),
223                     prelude_opt_isle,
224                     src_opts.join("arithmetic.isle"),
225                     src_opts.join("bitops.isle"),
226                     src_opts.join("cprop.isle"),
227                     src_opts.join("extends.isle"),
228                     src_opts.join("icmp.isle"),
229                     src_opts.join("remat.isle"),
230                     src_opts.join("selects.isle"),
231                     src_opts.join("spaceship.isle"),
232                     src_opts.join("shifts.isle"),
233                     src_opts.join("vector.isle"),
234                 ],
235                 untracked_inputs: vec![clif_opt_isle],
236             },
237             // The x86-64 instruction selector.
238             IsleCompilation {
239                 output: out_dir.join("isle_x64.rs"),
240                 inputs: vec![
241                     prelude_isle.clone(),
242                     prelude_lower_isle.clone(),
243                     src_isa_x64.join("inst.isle"),
244                     src_isa_x64.join("lower.isle"),
245                 ],
246                 untracked_inputs: vec![clif_lower_isle.clone()],
247             },
248             // The aarch64 instruction selector.
249             IsleCompilation {
250                 output: out_dir.join("isle_aarch64.rs"),
251                 inputs: vec![
252                     prelude_isle.clone(),
253                     prelude_lower_isle.clone(),
254                     src_isa_aarch64.join("inst.isle"),
255                     src_isa_aarch64.join("inst_neon.isle"),
256                     src_isa_aarch64.join("lower.isle"),
257                     src_isa_aarch64.join("lower_dynamic_neon.isle"),
258                 ],
259                 untracked_inputs: vec![clif_lower_isle.clone()],
260             },
261             // The s390x instruction selector.
262             IsleCompilation {
263                 output: out_dir.join("isle_s390x.rs"),
264                 inputs: vec![
265                     prelude_isle.clone(),
266                     prelude_lower_isle.clone(),
267                     src_isa_s390x.join("inst.isle"),
268                     src_isa_s390x.join("lower.isle"),
269                 ],
270                 untracked_inputs: vec![clif_lower_isle.clone()],
271             },
272             // The risc-v instruction selector.
273             IsleCompilation {
274                 output: out_dir.join("isle_riscv64.rs"),
275                 inputs: vec![
276                     prelude_isle.clone(),
277                     prelude_lower_isle.clone(),
278                     src_isa_risc_v.join("inst.isle"),
279                     src_isa_risc_v.join("inst_vector.isle"),
280                     src_isa_risc_v.join("lower.isle"),
281                 ],
282                 untracked_inputs: vec![clif_lower_isle.clone()],
283             },
284         ],
285     })
286 }
287 
288 fn build_isle(
289     crate_dir: &std::path::Path,
290     isle_dir: &std::path::Path,
291 ) -> Result<(), Box<dyn std::error::Error + 'static>> {
292     let isle_compilations = get_isle_compilations(crate_dir, isle_dir)?;
293 
294     let mut had_error = false;
295     for compilation in &isle_compilations.items {
296         for file in &compilation.inputs {
297             println!("cargo:rerun-if-changed={}", file.display());
298         }
299 
300         if let Err(e) = run_compilation(compilation) {
301             had_error = true;
302             eprintln!("Error building ISLE files:");
303             eprintln!("{:?}", e);
304             #[cfg(not(feature = "isle-errors"))]
305             {
306                 eprintln!("To see a more detailed error report, run: ");
307                 eprintln!();
308                 eprintln!("    $ cargo check -p cranelift-codegen --features isle-errors");
309                 eprintln!();
310             }
311         }
312     }
313 
314     if had_error {
315         std::process::exit(1);
316     }
317 
318     println!("cargo:rustc-env=ISLE_DIR={}", isle_dir.to_str().unwrap());
319 
320     Ok(())
321 }
322 
323 /// Build ISLE DSL source text into generated Rust code.
324 ///
325 /// NB: This must happen *after* the `cranelift-codegen-meta` functions, since
326 /// it consumes files generated by them.
327 fn run_compilation(compilation: &IsleCompilation) -> Result<(), Errors> {
328     use cranelift_isle as isle;
329 
330     eprintln!("Rebuilding {}", compilation.output.display());
331 
332     let code = {
333         let file_paths = compilation
334             .inputs
335             .iter()
336             .chain(compilation.untracked_inputs.iter());
337 
338         let mut options = isle::codegen::CodegenOptions::default();
339         // Because we include!() the generated ISLE source, we cannot
340         // put the global pragmas (`#![allow(...)]`) in the ISLE
341         // source itself; we have to put them in the source that
342         // include!()s it. (See
343         // https://github.com/rust-lang/rust/issues/47995.)
344         options.exclude_global_allow_pragmas = true;
345 
346         isle::compile::from_files(file_paths, &options)?
347     };
348 
349     let code = rustfmt(&code).unwrap_or_else(|e| {
350         println!(
351             "cargo:warning=Failed to run `rustfmt` on ISLE-generated code: {:?}",
352             e
353         );
354         code
355     });
356 
357     eprintln!(
358         "Writing ISLE-generated Rust code to {}",
359         compilation.output.display()
360     );
361     std::fs::write(&compilation.output, code)
362         .map_err(|e| Errors::from_io(e, "failed writing output"))?;
363 
364     Ok(())
365 }
366 
367 fn rustfmt(code: &str) -> std::io::Result<String> {
368     use std::io::Write;
369 
370     let mut rustfmt = std::process::Command::new("rustfmt")
371         .stdin(std::process::Stdio::piped())
372         .stdout(std::process::Stdio::piped())
373         .spawn()?;
374 
375     let mut stdin = rustfmt.stdin.take().unwrap();
376     stdin.write_all(code.as_bytes())?;
377     drop(stdin);
378 
379     let mut stdout = rustfmt.stdout.take().unwrap();
380     let mut data = vec![];
381     stdout.read_to_end(&mut data)?;
382 
383     let status = rustfmt.wait()?;
384     if !status.success() {
385         return Err(std::io::Error::new(
386             std::io::ErrorKind::Other,
387             format!("`rustfmt` exited with status {}", status),
388         ));
389     }
390 
391     Ok(String::from_utf8(data).expect("rustfmt always writs utf-8 to stdout"))
392 }
393