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     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 
67     let isle_dir = if let Ok(path) = std::env::var("ISLE_SOURCE_DIR") {
68         // This will canonicalize any relative path in terms of the
69         // crate root, and will take any absolute path as overriding the
70         // `crate_dir`.
71         crate_dir.join(&path)
72     } else {
73         out_dir.into()
74     };
75 
76     std::fs::create_dir_all(&isle_dir).expect("Could not create ISLE source directory");
77 
78     if let Err(err) = meta::generate(&isas, &out_dir, &isle_dir) {
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.display());
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 = \"{version}\";"
132         ),
133     )
134     .unwrap();
135 }
136 
137 /// Strip the current directory from the file paths, because `islec`
138 /// includes them in the generated source, and this helps us maintain
139 /// deterministic builds that don't include those local file paths.
140 fn make_isle_source_path_relative(
141     cur_dir: &std::path::Path,
142     filename: &std::path::Path,
143 ) -> std::path::PathBuf {
144     if let Ok(suffix) = filename.strip_prefix(&cur_dir) {
145         suffix.to_path_buf()
146     } else {
147         filename.to_path_buf()
148     }
149 }
150 
151 fn build_isle(
152     crate_dir: &std::path::Path,
153     isle_dir: &std::path::Path,
154 ) -> Result<(), Box<dyn std::error::Error + 'static>> {
155     let cur_dir = std::env::current_dir()?;
156     let isle_compilations = meta::isle::get_isle_compilations(
157         &make_isle_source_path_relative(&cur_dir, &crate_dir),
158         &make_isle_source_path_relative(&cur_dir, &isle_dir),
159     );
160 
161     let mut had_error = false;
162     for compilation in &isle_compilations.items {
163         for file in &compilation.inputs {
164             println!("cargo:rerun-if-changed={}", file.display());
165         }
166 
167         if let Err(e) = run_compilation(compilation) {
168             had_error = true;
169             eprintln!("Error building ISLE files:");
170             eprintln!("{e:?}");
171             #[cfg(not(feature = "isle-errors"))]
172             {
173                 eprintln!("To see a more detailed error report, run: ");
174                 eprintln!();
175                 eprintln!("    $ cargo check -p cranelift-codegen --features isle-errors");
176                 eprintln!();
177             }
178         }
179     }
180 
181     if had_error {
182         std::process::exit(1);
183     }
184 
185     println!("cargo:rustc-env=ISLE_DIR={}", isle_dir.to_str().unwrap());
186 
187     Ok(())
188 }
189 
190 /// Build ISLE DSL source text into generated Rust code.
191 ///
192 /// NB: This must happen *after* the `cranelift-codegen-meta` functions, since
193 /// it consumes files generated by them.
194 fn run_compilation(compilation: &IsleCompilation) -> Result<(), Errors> {
195     use cranelift_isle as isle;
196 
197     eprintln!("Rebuilding {}", compilation.output.display());
198 
199     let code = {
200         let file_paths = compilation
201             .inputs
202             .iter()
203             .chain(compilation.untracked_inputs.iter());
204 
205         let mut options = isle::codegen::CodegenOptions::default();
206         // Because we include!() the generated ISLE source, we cannot
207         // put the global pragmas (`#![allow(...)]`) in the ISLE
208         // source itself; we have to put them in the source that
209         // include!()s it. (See
210         // https://github.com/rust-lang/rust/issues/47995.)
211         options.exclude_global_allow_pragmas = true;
212 
213         isle::compile::from_files(file_paths, &options)?
214     };
215 
216     eprintln!(
217         "Writing ISLE-generated Rust code to {}",
218         compilation.output.display()
219     );
220     std::fs::write(&compilation.output, code)
221         .map_err(|e| Errors::from_io(e, "failed writing output"))?;
222 
223     Ok(())
224 }
225