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 
19 use std::env;
20 use std::io::Read;
21 use std::process;
22 use std::time::Instant;
23 
24 fn main() {
25     let start_time = Instant::now();
26 
27     let out_dir = env::var("OUT_DIR").expect("The OUT_DIR environment variable must be set");
28     let target_triple = env::var("TARGET").expect("The TARGET environment variable must be set");
29 
30     let isa_targets = meta::isa::Isa::all()
31         .iter()
32         .cloned()
33         .filter(|isa| {
34             let env_key = format!("CARGO_FEATURE_{}", isa.to_string().to_uppercase());
35             env::var(env_key).is_ok()
36         })
37         .collect::<Vec<_>>();
38 
39     let isas = if isa_targets.is_empty() {
40         // Try to match native target.
41         let target_name = target_triple.split('-').next().unwrap();
42         let isa = meta::isa_from_arch(&target_name).expect("error when identifying target");
43         println!("cargo:rustc-cfg=feature=\"{}\"", isa);
44         vec![isa]
45     } else {
46         isa_targets
47     };
48 
49     let cur_dir = env::current_dir().expect("Can't access current working directory");
50     let crate_dir = cur_dir.as_path();
51 
52     println!("cargo:rerun-if-changed=build.rs");
53 
54     let explicit_isle_dir = &crate_dir.join("isle_generated_code");
55     #[cfg(feature = "isle-in-source-tree")]
56     let isle_dir = explicit_isle_dir;
57     #[cfg(not(feature = "isle-in-source-tree"))]
58     let isle_dir = std::path::Path::new(&out_dir);
59 
60     #[cfg(feature = "isle-in-source-tree")]
61     {
62         std::fs::create_dir_all(isle_dir).expect("Could not create ISLE source directory");
63     }
64     #[cfg(not(feature = "isle-in-source-tree"))]
65     {
66         if explicit_isle_dir.is_dir() {
67             eprintln!(concat!(
68                 "Error: directory isle_generated_code/ exists but is only used when\n",
69                 "`--feature isle-in-source-tree` is specified. To prevent confusion,\n",
70                 "this build script requires the directory to be removed when reverting\n",
71                 "to the usual generated code in target/. Please delete the directory and\n",
72                 "re-run this build.\n",
73             ));
74             std::process::exit(1);
75         }
76     }
77 
78     if let Err(err) = meta::generate(&isas, &out_dir, isle_dir.to_str().unwrap()) {
79         eprintln!("Error: {}", err);
80         process::exit(1);
81     }
82 
83     if &std::env::var("SKIP_ISLE").unwrap_or("0".to_string()) != "1" {
84         if let Err(err) = build_isle(crate_dir, isle_dir) {
85             eprintln!("Error: {}", err);
86             process::exit(1);
87         }
88     }
89 
90     if env::var("CRANELIFT_VERBOSE").is_ok() {
91         for isa in &isas {
92             println!("cargo:warning=Includes support for {} ISA", isa.to_string());
93         }
94         println!(
95             "cargo:warning=Build step took {:?}.",
96             Instant::now() - start_time
97         );
98         println!("cargo:warning=Generated files are in {}", out_dir);
99     }
100 
101     let pkg_version = env::var("CARGO_PKG_VERSION").unwrap();
102     let mut cmd = std::process::Command::new("git");
103     cmd.arg("rev-parse")
104         .arg("HEAD")
105         .stdout(std::process::Stdio::piped())
106         .current_dir(env::var("CARGO_MANIFEST_DIR").unwrap());
107     let version = if let Ok(mut child) = cmd.spawn() {
108         let mut git_rev = String::new();
109         child
110             .stdout
111             .as_mut()
112             .unwrap()
113             .read_to_string(&mut git_rev)
114             .unwrap();
115         let status = child.wait().unwrap();
116         if status.success() {
117             let git_rev = git_rev.trim().chars().take(9).collect::<String>();
118             format!("{}-{}", pkg_version, git_rev)
119         } else {
120             // not a git repo
121             pkg_version
122         }
123     } else {
124         // git not available
125         pkg_version
126     };
127     std::fs::write(
128         std::path::Path::new(&out_dir).join("version.rs"),
129         format!(
130             "/// Version number of this crate. \n\
131             pub const VERSION: &str = \"{}\";",
132             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.
141 fn make_isle_source_path_relative(
142     cur_dir: &std::path::PathBuf,
143     filename: std::path::PathBuf,
144 ) -> std::path::PathBuf {
145     if let Ok(suffix) = filename.strip_prefix(&cur_dir) {
146         suffix.to_path_buf()
147     } else {
148         filename
149     }
150 }
151 
152 /// A list of compilations (transformations from ISLE source to
153 /// generated Rust source) that exist in the repository.
154 ///
155 /// This list is used either to regenerate the Rust source in-tree (if
156 /// the `rebuild-isle` feature is enabled), or to verify that the ISLE
157 /// source in-tree corresponds to the ISLE source that was last used
158 /// to rebuild the Rust source (if the `rebuild-isle` feature is not
159 /// enabled).
160 #[derive(Clone, Debug)]
161 struct IsleCompilations {
162     items: Vec<IsleCompilation>,
163 }
164 
165 #[derive(Clone, Debug)]
166 struct IsleCompilation {
167     output: std::path::PathBuf,
168     inputs: Vec<std::path::PathBuf>,
169     untracked_inputs: Vec<std::path::PathBuf>,
170 }
171 
172 /// Construct the list of compilations (transformations from ISLE
173 /// source to generated Rust source) that exist in the repository.
174 fn get_isle_compilations(
175     crate_dir: &std::path::Path,
176     out_dir: &std::path::Path,
177 ) -> Result<IsleCompilations, std::io::Error> {
178     let cur_dir = std::env::current_dir()?;
179 
180     let clif_isle = out_dir.join("clif.isle");
181     let prelude_isle =
182         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("prelude.isle"));
183     let src_isa_x64 =
184         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("x64"));
185     let src_isa_aarch64 =
186         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("aarch64"));
187     let src_isa_s390x =
188         make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("s390x"));
189 
190     // This is a set of ISLE compilation units.
191     //
192     // The format of each entry is:
193     //
194     //     (output Rust code file, input ISLE source files)
195     //
196     // There should be one entry for each backend that uses ISLE for lowering,
197     // and if/when we replace our peephole optimization passes with ISLE, there
198     // should be an entry for each of those as well.
199     //
200     // N.B.: add any new compilation outputs to
201     // `scripts/force-rebuild-isle.sh` if they do not fit the pattern
202     // `cranelift/codegen/src/isa/*/lower/isle/generated_code.rs`!
203     Ok(IsleCompilations {
204         items: vec![
205             // The x86-64 instruction selector.
206             IsleCompilation {
207                 output: out_dir.join("isle_x64.rs"),
208                 inputs: vec![
209                     prelude_isle.clone(),
210                     src_isa_x64.join("inst.isle"),
211                     src_isa_x64.join("lower.isle"),
212                 ],
213                 untracked_inputs: vec![clif_isle.clone()],
214             },
215             // The aarch64 instruction selector.
216             IsleCompilation {
217                 output: out_dir.join("isle_aarch64.rs"),
218                 inputs: vec![
219                     prelude_isle.clone(),
220                     src_isa_aarch64.join("inst.isle"),
221                     src_isa_aarch64.join("inst_neon.isle"),
222                     src_isa_aarch64.join("lower.isle"),
223                     src_isa_aarch64.join("lower_dynamic_neon.isle"),
224                 ],
225                 untracked_inputs: vec![clif_isle.clone()],
226             },
227             // The s390x instruction selector.
228             IsleCompilation {
229                 output: out_dir.join("isle_s390x.rs"),
230                 inputs: vec![
231                     prelude_isle.clone(),
232                     src_isa_s390x.join("inst.isle"),
233                     src_isa_s390x.join("lower.isle"),
234                 ],
235                 untracked_inputs: vec![clif_isle.clone()],
236             },
237         ],
238     })
239 }
240 
241 fn build_isle(
242     crate_dir: &std::path::Path,
243     isle_dir: &std::path::Path,
244 ) -> Result<(), Box<dyn std::error::Error + 'static>> {
245     let isle_compilations = get_isle_compilations(crate_dir, isle_dir)?;
246 
247     let mut had_error = false;
248     for compilation in &isle_compilations.items {
249         for file in &compilation.inputs {
250             println!("cargo:rerun-if-changed={}", file.display());
251         }
252 
253         if let Err(e) = run_compilation(compilation) {
254             eprintln!("Error building ISLE files: {:?}", e);
255             let mut source = e.source();
256             while let Some(e) = source {
257                 eprintln!("{:?}", e);
258                 source = e.source();
259             }
260             had_error = true;
261         }
262     }
263 
264     if had_error {
265         std::process::exit(1);
266     }
267 
268     println!("cargo:rustc-env=ISLE_DIR={}", isle_dir.to_str().unwrap());
269 
270     Ok(())
271 }
272 
273 /// Build ISLE DSL source text into generated Rust code.
274 ///
275 /// NB: This must happen *after* the `cranelift-codegen-meta` functions, since
276 /// it consumes files generated by them.
277 fn run_compilation(
278     compilation: &IsleCompilation,
279 ) -> Result<(), Box<dyn std::error::Error + 'static>> {
280     use cranelift_isle as isle;
281 
282     eprintln!("Rebuilding {}", compilation.output.display());
283 
284     let code = (|| {
285         let lexer = isle::lexer::Lexer::from_files(
286             compilation
287                 .inputs
288                 .iter()
289                 .chain(compilation.untracked_inputs.iter()),
290         )?;
291         let defs = isle::parser::parse(lexer)?;
292 
293         let mut options = isle::codegen::CodegenOptions::default();
294         // Because we include!() the generated ISLE source, we cannot
295         // put the global pragmas (`#![allow(...)]`) in the ISLE
296         // source itself; we have to put them in the source that
297         // include!()s it. (See
298         // https://github.com/rust-lang/rust/issues/47995.)
299         options.exclude_global_allow_pragmas = true;
300 
301         isle::compile::compile(&defs, &options)
302     })()
303     .map_err(|e| {
304         // Make sure to include the source snippets location info along with
305         // the error messages.
306 
307         #[cfg(feature = "isle-errors")]
308         {
309             let report = miette::Report::new(e);
310             return DebugReport(report);
311 
312             struct DebugReport(miette::Report);
313 
314             impl std::fmt::Display for DebugReport {
315                 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
316                     self.0.handler().debug(&*self.0, f)
317                 }
318             }
319 
320             impl std::fmt::Debug for DebugReport {
321                 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
322                     std::fmt::Display::fmt(self, f)
323                 }
324             }
325 
326             impl std::error::Error for DebugReport {}
327         }
328         #[cfg(not(feature = "isle-errors"))]
329         {
330             return DebugReport(format!("{}", e));
331 
332             struct DebugReport(String);
333 
334             impl std::fmt::Display for DebugReport {
335                 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
336                     writeln!(f, "ISLE errors:\n\n{}\n", self.0)?;
337                     writeln!(f, "To see a more detailed error report, run: ")?;
338                     writeln!(f, "")?;
339                     writeln!(
340                         f,
341                         "    $ cargo check -p cranelift-codegen --features isle-errors"
342                     )?;
343                     writeln!(f, "")?;
344                     Ok(())
345                 }
346             }
347 
348             impl std::fmt::Debug for DebugReport {
349                 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
350                     std::fmt::Display::fmt(self, f)
351                 }
352             }
353 
354             impl std::error::Error for DebugReport {}
355         }
356     })?;
357 
358     let code = rustfmt(&code).unwrap_or_else(|e| {
359         println!(
360             "cargo:warning=Failed to run `rustfmt` on ISLE-generated code: {:?}",
361             e
362         );
363         code
364     });
365 
366     eprintln!(
367         "Writing ISLE-generated Rust code to {}",
368         compilation.output.display()
369     );
370     std::fs::write(&compilation.output, code)?;
371 
372     Ok(())
373 }
374 
375 fn rustfmt(code: &str) -> std::io::Result<String> {
376     use std::io::Write;
377 
378     let mut rustfmt = std::process::Command::new("rustfmt")
379         .stdin(std::process::Stdio::piped())
380         .stdout(std::process::Stdio::piped())
381         .spawn()?;
382 
383     let mut stdin = rustfmt.stdin.take().unwrap();
384     stdin.write_all(code.as_bytes())?;
385     drop(stdin);
386 
387     let mut stdout = rustfmt.stdout.take().unwrap();
388     let mut data = vec![];
389     stdout.read_to_end(&mut data)?;
390 
391     let status = rustfmt.wait()?;
392     if !status.success() {
393         return Err(std::io::Error::new(
394             std::io::ErrorKind::Other,
395             format!("`rustfmt` exited with status {}", status),
396         ));
397     }
398 
399     Ok(String::from_utf8(data).expect("rustfmt always writs utf-8 to stdout"))
400 }
401