cargo_l1x/
build.rs

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        // avoid double --release
60        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    // Copy the source file to the versioned file
128    std::fs::copy(source_file, &versioned_file)
129        .map_err(|e| BuildError::IoError(anyhow!("Failed to copy source file"), e))?;
130
131    // Add the version information to the versioned file
132    add_version_info(&versioned_file)?;
133
134    // Fix the versioned file for mac os compatibility
135    fix_version_file(&versioned_file)?;
136
137    // Compile the versioned file to the target file
138    compile_to_object(&versioned_file, &target_file)?;
139
140    if !no_strip {
141        // Strip the target file
142        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    // MAC OS: wasm-llvmir tool adds a comma before a section name for unknown reason.
174    // This is a workaround until it's fixed in wasm-llvmir
175    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
190///  $ llc-17 -march=bpf -mattr=help
191///  Available CPUs for this target:
192///
193///    generic - Select the generic processor.
194///    probe   - Select the probe processor.
195///    v1      - Select the v1 processor.
196///    v2      - Select the v2 processor.
197///    v3      - Select the v3 processor.
198///
199///  Available features for this target:
200///
201///    alu32    - Enable ALU32 instructions.
202///    dummy    - unused feature.
203///    dwarfris - Disable MCAsmInfo DwarfUsesRelocationsAcrossSections.
204///
205/// Use +feature to enable a feature, or -feature to disable it.
206/// For example, llc -mcpu=mycpu -mattr=+feature1,-feature2
207///
208/// https://chromium.googlesource.com/external/github.com/llvm/llvm-project/+/refs/heads/upstream/release/17.x/llvm/lib/Target/BPF/BPFSubtarget.cpp
209///   if (CPU == "v3") {
210///    HasJmpExt = true;
211///    HasJmp32 = true;
212///    HasAlu32 = true;
213///    return;
214///  }
215fn 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}