1use super::which::which;
2use anyhow::anyhow;
3use l1x_wasm_llvmir::translate_module_to_file_by_path;
4use std::fs;
5use std::fs::OpenOptions;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::process;
9use std::process::Command;
10
11use thiserror::Error;
12
13const OBJECT_FILE_VERSION: i64 = 1;
14const EXPECTED_RUNTIME_VERSION: i64 = 4;
15const EBPF_STACK_FRAME_SIZE: u32 = 8192;
16
17#[derive(Error, Debug)]
18pub enum BuildError {
19 #[error("Invalid target directory. Should not happen")]
20 TargetDirError,
21 #[error("Failed to execute cargo: {0}")]
22 CargoBuildError(std::io::Error),
23 #[error("Failed to build wasm")]
24 WasmBuildError,
25 #[error("Could not build ll file: {0}")]
26 LlBuildError(anyhow::Error),
27 #[error("filesystem error")]
28 IoError(anyhow::Error, std::io::Error),
29 #[error("Failed to run llc command. Please ensure that your version of llc is > 17, or you have llc-17, 18 or 19 installed")]
30 LlcRunError(anyhow::Error),
31 #[error("Failed to build object file")]
32 ObjectBuildError,
33 #[error(
34 "Failed to run llvm strip on object file. Please ensure that you have llvm-strip installed"
35 )]
36 LlvmStripRunError(anyhow::Error),
37 #[error("Failed to strip object file")]
38 LlvmStripError,
39}
40
41pub fn build(mut args: Vec<String>, target_dir: PathBuf) -> Result<(), BuildError> {
42 let mut command = process::Command::new("cargo");
43
44 let mut no_strip = false;
45 if args.contains(&"--no-strip".to_string()) {
46 args = args.into_iter().filter(|x| x != "--no-strip").collect();
47 no_strip = true;
48 } else {
49 command.env("RUSTFLAGS", "-C link-arg=-s");
50 }
51
52 command
53 .arg("build")
54 .arg("--target")
55 .arg("wasm32-unknown-unknown")
56 .args(&args);
57
58 if !args.contains(&"--release".to_string()) {
59 command.arg("--release");
61 }
62
63 let mut output = command
64 .spawn()
65 .map_err(|e| BuildError::CargoBuildError(e))?;
66
67 let status = output.wait().map_err(|e| BuildError::CargoBuildError(e))?;
68
69 if !status.success() {
70 println!("Failed to build wasm");
71 return Err(BuildError::WasmBuildError);
72 }
73
74 let bin_dir = target_dir.join("l1x/release");
75
76 fs::create_dir_all(bin_dir.clone())
77 .map_err(|e| BuildError::IoError(anyhow!("Could not create target directory"), e))?;
78
79 let output = command
80 .arg("--message-format")
81 .arg("json")
82 .output()
83 .map_err(|e| BuildError::CargoBuildError(e))?;
84
85 let output_str = String::from_utf8_lossy(&output.stdout);
86 let lines: Vec<&str> = output_str.split("\n").collect();
87
88 for line in lines {
89 if let Ok(cargo_metadata::Message::CompilerArtifact(artifact)) =
90 serde_json::from_str::<cargo_metadata::Message>(line)
91 {
92 let wasm_file_path = artifact.filenames[0].clone();
93 if wasm_file_path.extension() == Some("wasm") {
94 let ll_file_path = wasm_file_path.with_extension("ll");
95 let ll_file_path = bin_dir.join(
96 &ll_file_path
97 .file_name()
98 .expect("Generated .ll file should have a file name"),
99 );
100 translate_module_to_file_by_path(
101 &wasm_file_path.clone().into(),
102 &ll_file_path.clone().into(),
103 )
104 .map_err(|e| BuildError::LlBuildError(e))?;
105
106 build_ebpf(&ll_file_path, no_strip)?;
107
108 let object_file_path = wasm_file_path.with_extension("o");
109 println!(
110 "✅ Contract object file '{}' has been built",
111 object_file_path
112 .file_name()
113 .expect("Generated .o file should have a file name")
114 );
115 }
116 }
117 }
118
119 Ok(())
120}
121
122pub fn build_ebpf<P: AsRef<Path> + Clone>(path: P, no_strip: bool) -> Result<(), BuildError> {
123 let source_file = path.clone();
124 let versioned_file = path.as_ref().with_extension("versioned.ll");
125 let target_file = path.as_ref().with_extension("o");
126
127 std::fs::copy(source_file, &versioned_file)
129 .map_err(|e| BuildError::IoError(anyhow!("Failed to copy source file"), e))?;
130
131 add_version_info(&versioned_file)?;
133
134 fix_version_file(&versioned_file)?;
136
137 compile_to_object(&versioned_file, &target_file)?;
139
140 if !no_strip {
141 strip_object_file(&target_file)?;
143 }
144
145 Ok(())
146}
147
148fn add_version_info<P: AsRef<Path>>(versioned_file: P) -> Result<(), BuildError> {
149 let mut file = OpenOptions::new()
150 .append(true)
151 .open(versioned_file)
152 .map_err(|e| BuildError::IoError(anyhow!("Failed to open versioned file"), e))?;
153
154 writeln!(
155 file,
156 "@_OBJECT_VERSION = global i64 {}, section \"_version\", align 1",
157 OBJECT_FILE_VERSION
158 )
159 .map_err(|e| BuildError::IoError(anyhow!("Failed to write version info"), e))?;
160 writeln!(
161 file,
162 "@_EXPECTED_RUNTIME_VERSION = global i64 {}, section \"_version\", align 1",
163 EXPECTED_RUNTIME_VERSION
164 )
165 .map_err(|e| BuildError::IoError(anyhow!("Failed to write version info"), e))?;
166 Ok(())
167}
168
169pub fn fix_version_file<P: AsRef<Path>>(versioned_file: P) -> Result<(), BuildError> {
170 let mut content = fs::read_to_string(versioned_file.as_ref())
171 .map_err(|e| BuildError::IoError(anyhow::anyhow!("Failed to read version file"), e))?;
172
173 content = content.replace("section \",_memory\"", "section \"_memory\"");
176 content = content.replace("section \",_init_memory\"", "section \"_init_memory\"");
177
178 let mut file = fs::OpenOptions::new()
179 .write(true)
180 .truncate(true)
181 .open(versioned_file.as_ref())
182 .map_err(|e| BuildError::IoError(anyhow::anyhow!("Failed to open version file"), e))?;
183
184 file.write_all(content.as_bytes())
185 .map_err(|e| BuildError::IoError(anyhow::anyhow!("Failed to write to version file"), e))?;
186
187 Ok(())
188}
189
190fn compile_to_object<P: AsRef<Path>>(input_file: P, output_file: P) -> Result<(), BuildError> {
216 let command = get_llc_command()?.to_string();
217
218 let output = Command::new(command)
219 .args(&[
220 "-march=bpf",
221 "-mcpu=v3",
222 "-filetype=obj",
223 "--nozero-initialized-in-bss",
224 "--bpf-stack-size",
225 EBPF_STACK_FRAME_SIZE.to_string().as_str(),
226 input_file
227 .as_ref()
228 .to_str()
229 .expect("Path should be valid unicode"),
230 "-o",
231 output_file
232 .as_ref()
233 .to_str()
234 .expect("Path should be valid unicode"),
235 ])
236 .output()
237 .map_err(|e| BuildError::LlcRunError(e.into()))?;
238
239 if !output.status.success() {
240 eprintln!(
241 "Error compiling to object file: {}",
242 String::from_utf8_lossy(&output.stderr)
243 );
244 return Err(BuildError::ObjectBuildError);
245 }
246 Ok(())
247}
248
249fn strip_object_file<P: AsRef<Path>>(target_file: P) -> Result<(), BuildError> {
250 let command = get_llvm_command()?.to_string();
251
252 let output = Command::new(command)
253 .arg("-x")
254 .arg(
255 target_file
256 .as_ref()
257 .to_str()
258 .expect("Path should be valid unicode"),
259 )
260 .output()
261 .map_err(|e| BuildError::LlvmStripRunError(e.into()))?;
262
263 if !output.status.success() {
264 eprintln!(
265 "Error stripping object file: {}",
266 String::from_utf8_lossy(&output.stderr)
267 );
268 return Err(BuildError::LlvmStripError);
269 }
270 Ok(())
271}
272
273fn get_llc_command() -> Result<String, BuildError> {
274 if let Ok(path_str) = std::env::var("LLVM_BIN_PATH") {
275 let path = format!("{}/llc", path_str);
276 if std::path::Path::new(&path).exists() {
277 return Ok(path);
278 }
279 }
280 if which("llc-17".to_string()).is_some() {
281 return Ok("llc-17".into());
282 } else if which("llc-18".to_string()).is_some() {
283 return Ok("llc-18".into());
284 } else if which("llc-19".to_string()).is_some() {
285 return Ok("llc-19".into());
286 } else if which("llc".to_string()).is_some() {
287 let output = Command::new("llc").arg("--version").output();
288
289 if let Ok(output) = output {
290 let version_str = String::from_utf8_lossy(&output.stdout);
291 if version_str.contains("version 17.")
292 || version_str.contains("version 18.")
293 || version_str.contains("version 19.")
294 {
295 return Ok("llc".into());
296 } else {
297 return Err(BuildError::LlcRunError(anyhow!("")));
298 }
299 } else {
300 return Err(BuildError::LlcRunError(anyhow!("")));
301 }
302 } else {
303 return Err(BuildError::LlcRunError(anyhow!("")));
304 }
305}
306
307fn get_llvm_command() -> Result<String, BuildError> {
308 if std::env::var("LLVM_BIN_PATH").is_ok() {
309 let path = format!(
310 "{}/llvm-strip",
311 std::env::var("LLVM_BIN_PATH").expect("checked")
312 );
313 if std::path::Path::new(&path).exists() {
314 return Ok(path);
315 }
316 }
317 if which("llvm-strip-17".to_string()).is_some() {
318 return Ok("llvm-strip-17".into());
319 } else if which("llvm-strip-18".to_string()).is_some() {
320 return Ok("llvm-strip-18".into());
321 } else if which("llvm-strip-19".to_string()).is_some() {
322 return Ok("llvm-strip-19".into());
323 } else if which("llvm-strip".to_string()).is_some() {
324 let output = Command::new("llvm-strip").arg("--version").output();
325
326 if let Ok(output) = output {
327 let version_str = String::from_utf8_lossy(&output.stdout);
328 if version_str.contains("version 17.")
329 || version_str.contains("version 18.")
330 || version_str.contains("version 19.")
331 {
332 return Ok("llvm-strip".into());
333 } else {
334 return Err(BuildError::LlvmStripRunError(anyhow!("")));
335 }
336 } else {
337 return Err(BuildError::LlvmStripRunError(anyhow!("")));
338 }
339 } else {
340 return Err(BuildError::LlvmStripRunError(anyhow!("")));
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_fix_version_file() {
350 let versioned_file = "tests/fixtures/macos.versioned.ll";
351 let content = fs::read_to_string(versioned_file).unwrap();
352
353 let mut temp_file = tempfile::NamedTempFile::new().unwrap();
354 temp_file.write_all(content.as_bytes()).unwrap();
355 fix_version_file(temp_file.path()).unwrap();
356
357 let content = fs::read_to_string(temp_file).unwrap();
358 assert!(
359 content.contains(" section \"_init_memory\""),
360 "Can't find 'section \"_init_memory\"' in .versioned.ll"
361 );
362 assert!(
363 content.contains(" section \"_memory\""),
364 "Can't find 'section \"_memory\"' in .versioned.ll"
365 );
366 }
367}