arrow_udf_runtime/wasm/
build.rs

1//! Build WASM binaries from source.
2
3use anyhow::{Context, Result};
4use std::path::PathBuf;
5use std::process::Command;
6
7/// Build a wasm binary from a Rust UDF script.
8///
9/// The `manifest` is a TOML string that will be appended to the generated `Cargo.toml`.
10/// The `script` is a Rust source code string that will be written to `src/lib.rs`.
11///
12/// This function will block the current thread until the build is finished.
13///
14/// # Example
15///
16/// ```ignore
17/// let manifest = r#"
18/// [dependencies]
19/// chrono = "0.4"
20/// "#;
21///
22/// let script = r#"
23/// use arrow_udf::function;
24///
25/// #[function("gcd(int, int) -> int")]
26/// fn gcd(mut a: i32, mut b: i32) -> i32 {
27///     while b != 0 {
28///         (a, b) = (b, a % b);
29///     }
30///     a
31/// }
32/// "#;
33///
34/// let binary = arrow_udf_runtime::wasm::build::build(manifest, script).unwrap();
35/// ```
36pub fn build(manifest: &str, script: &str) -> Result<Vec<u8>> {
37    let opts = BuildOpts {
38        manifest: manifest.to_string(),
39        script: script.to_string(),
40        ..Default::default()
41    };
42    build_with(&opts)
43}
44
45/// Options for building wasm binaries.
46#[derive(Debug, Default)]
47#[non_exhaustive]
48pub struct BuildOpts {
49    /// A TOML string that will be appended to the generated `Cargo.toml`.
50    pub manifest: String,
51    /// A Rust source code string that will be written to `src/lib.rs`.
52    pub script: String,
53    /// Whether to build offline.
54    pub offline: bool,
55    /// The toolchain to use.
56    pub toolchain: Option<String>,
57    /// The version of the `arrow-udf` crate to use.
58    /// If not specified, 0.2 will be used.
59    pub arrow_udf_version: Option<String>,
60    /// The temporary directory to use.
61    /// If not specified, a random temporary directory will be used.
62    pub tempdir: Option<PathBuf>,
63}
64
65/// Build a wasm binary with options.
66pub fn build_with(opts: &BuildOpts) -> Result<Vec<u8>> {
67    // install wasm32-wasip1 target
68    if !opts.offline {
69        let mut command = Command::new("rustup");
70        if let Some(toolchain) = &opts.toolchain {
71            command.arg(format!("+{}", toolchain));
72        }
73        let output = command
74            .arg("target")
75            .arg("add")
76            .arg("wasm32-wasip1")
77            .output()
78            .context("failed to run `rustup target add wasm32-wasip1`")?;
79        if !output.status.success() {
80            return Err(anyhow::anyhow!(
81                "failed to install wasm32-wasip1 target. ({})\n--- stdout\n{}\n--- stderr\n{}",
82                output.status,
83                String::from_utf8_lossy(&output.stdout),
84                String::from_utf8_lossy(&output.stderr)
85            ));
86        }
87    }
88
89    let manifest = format!(
90        r#"
91[package]
92name = "udf"
93version = "0.1.0"
94edition = "2021"
95
96[lib]
97crate-type = ["cdylib"]
98
99[dependencies.arrow-udf]
100version = "{}"
101
102{}"#,
103        opts.arrow_udf_version.as_deref().unwrap_or("0.2"),
104        opts.manifest
105    );
106
107    // create a temporary directory if not specified
108    let tempdir = if opts.tempdir.is_some() {
109        None
110    } else {
111        Some(tempfile::tempdir().context("failed to create tempdir")?)
112    };
113    let dir = match &opts.tempdir {
114        Some(dir) => dir,
115        None => tempdir.as_ref().unwrap().path(),
116    };
117    std::fs::create_dir_all(dir.join("src"))?;
118    std::fs::write(dir.join("src/lib.rs"), &opts.script)?;
119    std::fs::write(dir.join("Cargo.toml"), manifest)?;
120
121    let mut command = Command::new("cargo");
122    if let Some(toolchain) = &opts.toolchain {
123        command.arg(format!("+{}", toolchain));
124    }
125    command
126        .arg("build")
127        .arg("--release")
128        .arg("--target")
129        .arg("wasm32-wasip1")
130        .current_dir(dir);
131    if opts.offline {
132        command.arg("--offline");
133    }
134    let output = command.output().context("failed to run cargo build")?;
135    if !output.status.success() {
136        return Err(anyhow::anyhow!(
137            "failed to build wasm ({})\n--- stdout\n{}\n--- stderr\n{}",
138            output.status,
139            String::from_utf8_lossy(&output.stdout),
140            String::from_utf8_lossy(&output.stderr)
141        ));
142    }
143    let binary_path = dir.join("target/wasm32-wasip1/release/udf.wasm");
144    // strip the wasm binary if wasm-strip exists
145    if Command::new("wasm-strip").arg("--version").output().is_ok() {
146        let output = Command::new("wasm-strip")
147            .arg(&binary_path)
148            .output()
149            .context("failed to strip wasm")?;
150        if !output.status.success() {
151            return Err(anyhow::anyhow!(
152                "failed to strip wasm. ({})\n--- stdout\n{}\n--- stderr\n{}",
153                output.status,
154                String::from_utf8_lossy(&output.stdout),
155                String::from_utf8_lossy(&output.stderr)
156            ));
157        }
158    }
159    let binary = std::fs::read(binary_path).context("failed to read wasm binary")?;
160    Ok(binary)
161}