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 
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 
35     let mut isas = meta::isa::Isa::all()
36         .iter()
37         .cloned()
38         .filter(|isa| {
39             let env_key = format!("CARGO_FEATURE_{}", isa.to_string().to_uppercase());
40             all_arch || env::var(env_key).is_ok()
41         })
42         .collect::<Vec<_>>();
43 
44     // Don't require host isa if under 'all-arch' feature.
45     let host_isa = env::var("CARGO_FEATURE_HOST_ARCH").is_ok() && !all_arch;
46 
47     if isas.is_empty() || host_isa {
48         // Try to match native target.
49         let target_name = target_triple.split('-').next().unwrap();
50         let isa = meta::isa_from_arch(&target_name).expect("error when identifying target");
51         println!("cargo:rustc-cfg=feature=\"{}\"", isa);
52         isas.push(isa);
53     }
54 
55     let cur_dir = env::current_dir().expect("Can't access current working directory");
56     let crate_dir = cur_dir.as_path();
57 
58     println!("cargo:rerun-if-changed=build.rs");
59 
60     let explicit_isle_dir = &crate_dir.join("isle_generated_code");
61     #[cfg(feature = "isle-in-source-tree")]
62     let isle_dir = explicit_isle_dir;
63     #[cfg(not(feature = "isle-in-source-tree"))]
64     let isle_dir = &out_dir;
65 
66     #[cfg(feature = "isle-in-source-tree")]
67     {
68         std::fs::create_dir_all(isle_dir).expect("Could not create ISLE source directory");
69     }
70     #[cfg(not(feature = "isle-in-source-tree"))]
71     {
72         if explicit_isle_dir.is_dir() {
73             eprintln!(concat!(
74                 "Error: directory isle_generated_code/ exists but is only used when\n",
75                 "`--feature isle-in-source-tree` is specified. To prevent confusion,\n",
76                 "this build script requires the directory to be removed when reverting\n",
77                 "to the usual generated code in target/. Please delete the directory and\n",
78                 "re-run this build.\n",
79             ));
80             std::process::exit(1);
81         }
82     }
83 
84     if let Err(err) = meta::generate(&isas, &out_dir, isle_dir) {
85         eprintln!("Error: {}", err);
86         process::exit(1);
87     }
88 
89     if &std::env::var("SKIP_ISLE").unwrap_or("0".to_string()) != "1" {
90         if let Err(err) = build_isle(crate_dir, isle_dir) {
91             eprintln!("Error: {}", err);
92             process::exit(1);
93         }
94     }
95 
96     if env::var("CRANELIFT_VERBOSE").is_ok() {
97         for isa in &isas {
98             println!("cargo:warning=Includes support for {} ISA", isa.to_string());
99         }
100         println!(
101             "cargo:warning=Build step took {:?}.",
102             Instant::now() - start_time
103         );
104         println!("cargo:warning=Generated files are in {}", out_dir.display());
105     }
106 
107     let pkg_version = env::var("CARGO_PKG_VERSION").unwrap();
108     let mut cmd = std::process::Command::new("git");
109     cmd.arg("rev-parse")
110         .arg("HEAD")
111         .stdout(std::process::Stdio::piped())
112         .current_dir(env::var("CARGO_MANIFEST_DIR").unwrap());
113     let version = if let Ok(mut child) = cmd.spawn() {
114         let mut git_rev = String::new();
115         child
116             .stdout
117             .as_mut()
118             .unwrap()
119             .read_to_string(&mut git_rev)
120             .unwrap();
121         let status = child.wait().unwrap();
122         if status.success() {
123             let git_rev = git_rev.trim().chars().take(9).collect::<String>();
124             format!("{}-{}", pkg_version, git_rev)
125         } else {
126             // not a git repo
127             pkg_version
128         }
129     } else {
130         // git not available
131         pkg_version
132     };
133     std::fs::write(
134         std::path::Path::new(&out_dir).join("version.rs"),
135         format!(
136             "/// Version number of this crate. \n\
137             pub const VERSION: &str = \"{}\";",
138             version
139         ),
140     )
141     .unwrap();
142 }
143 
144 /// Strip the current directory from the file paths, because `islec`
145 /// includes them in the generated source, and this helps us maintain
146 /// deterministic builds that don't include those local file paths.
147 fn make_isle_source_path_relative(
148     cur_dir: &std::path::Path,
149     filename: &std::path::Path,
150 ) -> std::path::PathBuf {
151     if let Ok(suffix) = filename.strip_prefix(&cur_dir) {
152         suffix.to_path_buf()
153     } else {
154         filename.to_path_buf()
155     }
156 }
157 
158 fn build_isle(
159     crate_dir: &std::path::Path,
160     isle_dir: &std::path::Path,
161 ) -> Result<(), Box<dyn std::error::Error + 'static>> {
162     let cur_dir = std::env::current_dir()?;
163     let isle_compilations = meta::isle::get_isle_compilations(
164         &make_isle_source_path_relative(&cur_dir, &crate_dir),
165         &make_isle_source_path_relative(&cur_dir, &isle_dir),
166     );
167 
168     let mut had_error = false;
169     for compilation in &isle_compilations.items {
170         for file in &compilation.inputs {
171             println!("cargo:rerun-if-changed={}", file.display());
172         }
173 
174         if let Err(e) = run_compilation(compilation) {
175             had_error = true;
176             eprintln!("Error building ISLE files:");
177             eprintln!("{:?}", e);
178             #[cfg(not(feature = "isle-errors"))]
179             {
180                 eprintln!("To see a more detailed error report, run: ");
181                 eprintln!();
182                 eprintln!("    $ cargo check -p cranelift-codegen --features isle-errors");
183                 eprintln!();
184             }
185         }
186     }
187 
188     if had_error {
189         std::process::exit(1);
190     }
191 
192     println!("cargo:rustc-env=ISLE_DIR={}", isle_dir.to_str().unwrap());
193 
194     Ok(())
195 }
196 
197 /// Build ISLE DSL source text into generated Rust code.
198 ///
199 /// NB: This must happen *after* the `cranelift-codegen-meta` functions, since
200 /// it consumes files generated by them.
201 fn run_compilation(compilation: &IsleCompilation) -> Result<(), Errors> {
202     use cranelift_isle as isle;
203 
204     eprintln!("Rebuilding {}", compilation.output.display());
205 
206     let code = {
207         let file_paths = compilation
208             .inputs
209             .iter()
210             .chain(compilation.untracked_inputs.iter());
211 
212         let mut options = isle::codegen::CodegenOptions::default();
213         // Because we include!() the generated ISLE source, we cannot
214         // put the global pragmas (`#![allow(...)]`) in the ISLE
215         // source itself; we have to put them in the source that
216         // include!()s it. (See
217         // https://github.com/rust-lang/rust/issues/47995.)
218         options.exclude_global_allow_pragmas = true;
219 
220         isle::compile::from_files(file_paths, &options)?
221     };
222 
223     let code = rustfmt(&code).unwrap_or_else(|e| {
224         println!(
225             "cargo:warning=Failed to run `rustfmt` on ISLE-generated code: {:?}",
226             e
227         );
228         code
229     });
230 
231     eprintln!(
232         "Writing ISLE-generated Rust code to {}",
233         compilation.output.display()
234     );
235     std::fs::write(&compilation.output, code)
236         .map_err(|e| Errors::from_io(e, "failed writing output"))?;
237 
238     Ok(())
239 }
240 
241 fn rustfmt(code: &str) -> std::io::Result<String> {
242     use std::io::Write;
243 
244     let mut rustfmt = std::process::Command::new("rustfmt")
245         .stdin(std::process::Stdio::piped())
246         .stdout(std::process::Stdio::piped())
247         .spawn()?;
248 
249     let mut stdin = rustfmt.stdin.take().unwrap();
250     stdin.write_all(code.as_bytes())?;
251     drop(stdin);
252 
253     let mut stdout = rustfmt.stdout.take().unwrap();
254     let mut data = vec![];
255     stdout.read_to_end(&mut data)?;
256 
257     let status = rustfmt.wait()?;
258     if !status.success() {
259         return Err(std::io::Error::new(
260             std::io::ErrorKind::Other,
261             format!("`rustfmt` exited with status {}", status),
262         ));
263     }
264 
265     Ok(String::from_utf8(data).expect("rustfmt always writs utf-8 to stdout"))
266 }
267