Skip to main content

canic_host/binaryen/
mod.rs

1//! Module: binaryen
2//!
3//! Responsibility: own the published Binaryen release-tool authority, installation, and
4//! executable admission used by canonical release-Wasm builds.
5//! Does not own: Wasm transformation policy, build provenance shape, or release-set publication.
6//! Boundary: only the checksum-pinned official platform executable may reach the optimizer.
7
8#[cfg(test)]
9mod tests;
10
11use crate::{
12    output_with_executable_busy_retry,
13    tool_install::{self, ArchiveFormat, InstallError, InstallSpec, sha256_file},
14    tool_resolution,
15};
16use std::{
17    env,
18    ffi::OsStr,
19    io,
20    path::{Path, PathBuf},
21};
22
23use thiserror::Error as ThisError;
24
25pub const BINARYEN_REPAIR_COMMAND: &str = "canic toolchain install";
26pub const BINARYEN_VERSION: &str = "133";
27pub const BINARYEN_VERSION_IDENTITY: &str = "wasm-opt version 133 (version_133)";
28pub const WASM_OPT_TOOL: &str = "wasm-opt";
29
30///
31/// BinaryenAuthority
32///
33/// Official archive, executable and runtime-library identities for one supported host.
34///
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub struct BinaryenAuthority {
38    archive_platform: &'static str,
39    archive_sha256: &'static str,
40    executable_sha256: &'static str,
41    runtime_library_sha256: Option<&'static str>,
42}
43
44impl BinaryenAuthority {
45    #[must_use]
46    pub const fn archive_platform(self) -> &'static str {
47        self.archive_platform
48    }
49
50    #[must_use]
51    pub const fn archive_sha256(self) -> &'static str {
52        self.archive_sha256
53    }
54
55    #[must_use]
56    pub const fn executable_sha256(self) -> &'static str {
57        self.executable_sha256
58    }
59
60    fn archive_name(self) -> String {
61        format!(
62            "binaryen-version_{BINARYEN_VERSION}-{}.tar.gz",
63            self.archive_platform
64        )
65    }
66
67    fn archive_url(self) -> String {
68        format!(
69            "https://github.com/WebAssembly/binaryen/releases/download/version_{BINARYEN_VERSION}/{}",
70            self.archive_name()
71        )
72    }
73}
74
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76struct BinaryenPlatformAuthority {
77    os: &'static str,
78    arch: &'static str,
79    authority: BinaryenAuthority,
80}
81
82const SUPPORTED_BINARYEN_AUTHORITIES: [BinaryenPlatformAuthority; 3] = [
83    BinaryenPlatformAuthority {
84        os: "macos",
85        arch: "aarch64",
86        authority: BinaryenAuthority {
87            archive_platform: "arm64-macos",
88            archive_sha256: "ad66da82ac13f163e424b1643f16c6dfcccc98b5966296b43e52d3cab04f84a8",
89            executable_sha256: "81041e09f332df94db1c2009a64d8f3b85f0431a2c920ccd014d3ceebb343402",
90            runtime_library_sha256: Some(
91                "61055e190d84d5db6d1dec63456e0c24dad324ecfd4d2e23740dca217ed89e5a",
92            ),
93        },
94    },
95    BinaryenPlatformAuthority {
96        os: "macos",
97        arch: "x86_64",
98        authority: BinaryenAuthority {
99            archive_platform: "x86_64-macos",
100            archive_sha256: "13a9b90be775c6389ce3d1f879cb8627bea56708ba8c122983941d53a8199b95",
101            executable_sha256: "e26344b1d0d0986ac4a1090f58e478470eb2a52ba0625ae9a0f880511bb31d51",
102            runtime_library_sha256: Some(
103                "26388343133e968f58c18807552b83c43944d11cf512e3920798890adc554f38",
104            ),
105        },
106    },
107    BinaryenPlatformAuthority {
108        os: "linux",
109        arch: "x86_64",
110        authority: BinaryenAuthority {
111            archive_platform: "x86_64-linux",
112            archive_sha256: "2dc9c7813f5375db93d96ead4b78222fcc3e2677bbb832297af4797782a37489",
113            executable_sha256: "8f25e9fd5db0fc5f210003aaa432922feb2e52d309e430def2f929e34da9466b",
114            runtime_library_sha256: None,
115        },
116    },
117];
118
119///
120/// BinaryenExecutable
121///
122/// Exact admitted optimizer executable consumed by one release-Wasm finalization.
123///
124
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct BinaryenExecutable {
127    path: PathBuf,
128    version_identity: String,
129    sha256: String,
130}
131
132impl BinaryenExecutable {
133    #[must_use]
134    pub fn path(&self) -> &Path {
135        &self.path
136    }
137
138    #[must_use]
139    pub fn version_identity(&self) -> &str {
140        &self.version_identity
141    }
142
143    #[must_use]
144    pub fn sha256(&self) -> &str {
145        &self.sha256
146    }
147}
148
149///
150/// BinaryenToolError
151///
152/// Typed setup or admission failure for the mandatory release optimizer.
153///
154
155#[derive(Debug, ThisError)]
156pub enum BinaryenToolError {
157    #[error("Binaryen archive download failed with status {status}: {stderr}")]
158    ArchiveDownload { status: String, stderr: String },
159
160    #[error("downloaded Binaryen archive {path} has SHA-256 {actual}; required {expected}")]
161    ArchiveHashMismatch {
162        path: PathBuf,
163        actual: String,
164        expected: &'static str,
165    },
166
167    #[error("Binaryen archive extraction failed with status {status}: {stderr}")]
168    ArchiveExtraction { status: String, stderr: String },
169
170    #[error(
171        "selected Binaryen executable {path} has SHA-256 {actual}; required {expected}; run `{BINARYEN_REPAIR_COMMAND}`"
172    )]
173    ExecutableHashMismatch {
174        path: PathBuf,
175        actual: String,
176        expected: String,
177    },
178
179    #[error("failed to {operation} {path}: {source}")]
180    Io {
181        operation: &'static str,
182        path: PathBuf,
183        #[source]
184        source: io::Error,
185    },
186
187    #[error("HOME is unavailable; `{BINARYEN_REPAIR_COMMAND}` cannot select ~/.local/bin")]
188    MissingHome,
189
190    #[error(
191        "release Wasm optimization requires Binaryen {BINARYEN_VERSION}; `{WASM_OPT_TOOL}` was not found on PATH or at {canonical_path}; run `{BINARYEN_REPAIR_COMMAND}`"
192    )]
193    MissingOptimizer { canonical_path: PathBuf },
194
195    #[error(
196        "release Wasm optimization requires Binaryen {BINARYEN_VERSION}; `{WASM_OPT_TOOL}` was not found on PATH and HOME is unavailable; run `{BINARYEN_REPAIR_COMMAND}` with HOME set to the intended account home"
197    )]
198    MissingOptimizerWithoutHome,
199
200    #[error(
201        "release Wasm optimization requires Binaryen {BINARYEN_VERSION}; `{WASM_OPT_TOOL}` was not found on PATH or at {canonical_path}; HOME resolves to `/`, so confirm that root-level install location is intentional before running `{BINARYEN_REPAIR_COMMAND}`"
202    )]
203    MissingOptimizerWithRootHome { canonical_path: PathBuf },
204
205    #[error("installed Binaryen candidate is not executable: {path}")]
206    NotExecutable { path: PathBuf },
207
208    #[error(
209        "requested Binaryen executable {path} is missing or not executable; release Wasm optimization requires Binaryen {BINARYEN_VERSION}; run `{BINARYEN_REPAIR_COMMAND}`"
210    )]
211    RequestedExecutableMissing { path: PathBuf },
212
213    #[error(
214        "selected Binaryen runtime library {path} has SHA-256 {actual}; required {expected}; run `{BINARYEN_REPAIR_COMMAND}`"
215    )]
216    RuntimeLibraryHashMismatch {
217        path: PathBuf,
218        actual: String,
219        expected: String,
220    },
221
222    #[error("temporary Binaryen installation directory allocation was exhausted under {root}")]
223    TempDirectoryExhausted { root: PathBuf },
224
225    #[error("unsupported Binaryen platform: {os} {arch}")]
226    UnsupportedPlatform {
227        os: &'static str,
228        arch: &'static str,
229    },
230
231    #[error(
232        "selected Binaryen executable {path} reports `{actual}`; required `{expected}`; run `{BINARYEN_REPAIR_COMMAND}`"
233    )]
234    VersionMismatch {
235        path: PathBuf,
236        actual: String,
237        expected: &'static str,
238    },
239
240    #[error("failed to inspect Binaryen version at {path}: {source}")]
241    VersionProcess {
242        path: PathBuf,
243        #[source]
244        source: io::Error,
245    },
246}
247
248impl From<InstallError> for BinaryenToolError {
249    fn from(error: InstallError) -> Self {
250        match error {
251            InstallError::ArchiveDownload { status, stderr } => {
252                Self::ArchiveDownload { status, stderr }
253            }
254            InstallError::ArchiveExtraction { status, stderr } => {
255                Self::ArchiveExtraction { status, stderr }
256            }
257            InstallError::ArchiveHashMismatch {
258                path,
259                actual,
260                expected,
261            } => Self::ArchiveHashMismatch {
262                path,
263                actual,
264                expected,
265            },
266            InstallError::ExecutableHashMismatch {
267                path,
268                actual,
269                expected,
270            } => Self::ExecutableHashMismatch {
271                path,
272                actual,
273                expected,
274            },
275            InstallError::Io {
276                operation,
277                path,
278                source,
279            } => Self::Io {
280                operation,
281                path,
282                source,
283            },
284            InstallError::TempDirectoryExhausted { root } => Self::TempDirectoryExhausted { root },
285        }
286    }
287}
288
289/// Return the checksum authority for the current supported host platform.
290pub fn current_binaryen_authority() -> Result<BinaryenAuthority, BinaryenToolError> {
291    binaryen_authority_for(env::consts::OS, env::consts::ARCH)
292}
293
294fn binaryen_authority_for(
295    os: &'static str,
296    arch: &'static str,
297) -> Result<BinaryenAuthority, BinaryenToolError> {
298    SUPPORTED_BINARYEN_AUTHORITIES
299        .iter()
300        .find(|projection| (projection.os, projection.arch) == (os, arch))
301        .map(|projection| projection.authority)
302        .ok_or(BinaryenToolError::UnsupportedPlatform { os, arch })
303}
304
305/// Resolve and admit the governed install path, falling back to the first optimizer on PATH.
306pub fn resolve_required_binaryen() -> Result<BinaryenExecutable, BinaryenToolError> {
307    current_binaryen_authority()?;
308    let path = resolve_executable(OsStr::new(WASM_OPT_TOOL))?;
309    admit_required_binaryen(&path)
310}
311
312/// Admit an explicitly selected optimizer against the current platform authority.
313pub(crate) fn admit_required_binaryen(
314    path: &Path,
315) -> Result<BinaryenExecutable, BinaryenToolError> {
316    let authority = current_binaryen_authority()?;
317    admit_binaryen_executable(
318        path,
319        authority.executable_sha256(),
320        authority.runtime_library_sha256,
321    )
322}
323
324/// Install the official current-platform optimizer under `~/.local/bin`.
325///
326/// Canic invokes the returned admitted absolute path directly.
327pub fn install_required_binaryen() -> Result<BinaryenExecutable, BinaryenToolError> {
328    let authority = current_binaryen_authority()?;
329    let install_path = default_binaryen_install_path()?;
330    let runtime_library = format!("binaryen-version_{BINARYEN_VERSION}/lib/libbinaryen.dylib");
331    let spec = InstallSpec {
332        tool: WASM_OPT_TOOL,
333        archive_name: &authority.archive_name(),
334        archive_url: &authority.archive_url(),
335        archive_sha256: authority.archive_sha256(),
336        member: &format!("binaryen-version_{BINARYEN_VERSION}/bin/wasm-opt"),
337        runtime_library: authority
338            .archive_platform()
339            .ends_with("-macos")
340            .then_some(runtime_library.as_str()),
341        format: ArchiveFormat::Gzip,
342    };
343    tool_install::install(&spec, &install_path, |path| {
344        admit_binaryen_executable(
345            path,
346            authority.executable_sha256(),
347            authority.runtime_library_sha256,
348        )
349    })
350}
351
352/// Return the fixed downstream installation path named by repair diagnostics.
353pub fn default_binaryen_install_path() -> Result<PathBuf, BinaryenToolError> {
354    let home = env::var_os("HOME")
355        .filter(|value| !value.is_empty())
356        .ok_or(BinaryenToolError::MissingHome)?;
357    Ok(PathBuf::from(home).join(".local/bin/wasm-opt"))
358}
359
360fn resolve_executable(command: &OsStr) -> Result<PathBuf, BinaryenToolError> {
361    let requested = Path::new(command);
362    let canonical = default_binaryen_install_path().ok();
363    let mut directories = Vec::new();
364    if let Some(parent) = canonical.as_ref().and_then(|path| path.parent()) {
365        directories.push(parent.to_path_buf());
366    }
367    if let Some(path) = env::var_os("PATH") {
368        directories.extend(env::split_paths(&path));
369    }
370    let selected = tool_resolution::resolve(requested, &directories).map_err(|source| {
371        BinaryenToolError::Io {
372            operation: "resolve Binaryen executable",
373            path: requested.to_path_buf(),
374            source,
375        }
376    })?;
377    if let Some(path) = selected {
378        return Ok(path);
379    }
380    if command.as_encoded_bytes().contains(&b'/') {
381        return Err(BinaryenToolError::RequestedExecutableMissing {
382            path: requested.to_path_buf(),
383        });
384    }
385    match canonical {
386        Some(canonical_path) if home_is_root() => {
387            Err(BinaryenToolError::MissingOptimizerWithRootHome { canonical_path })
388        }
389        Some(canonical_path) => Err(BinaryenToolError::MissingOptimizer { canonical_path }),
390        None => Err(BinaryenToolError::MissingOptimizerWithoutHome),
391    }
392}
393
394fn admit_binaryen_executable(
395    path: &Path,
396    expected_sha256: &str,
397    expected_runtime_sha256: Option<&str>,
398) -> Result<BinaryenExecutable, BinaryenToolError> {
399    let path = tool_resolution::resolve(path, &[])
400        .map_err(|source| BinaryenToolError::Io {
401            operation: "resolve Binaryen executable",
402            path: path.to_path_buf(),
403            source,
404        })?
405        .ok_or_else(|| BinaryenToolError::NotExecutable {
406            path: path.to_path_buf(),
407        })?;
408    let sha256 = sha256_file(&path)?;
409    if sha256 != expected_sha256 {
410        return Err(BinaryenToolError::ExecutableHashMismatch {
411            path,
412            actual: sha256,
413            expected: expected_sha256.to_string(),
414        });
415    }
416    if let Some(expected) = expected_runtime_sha256 {
417        admit_runtime_library(&path, expected)?;
418    }
419
420    let mut command = crate::build_environment::command(&path);
421    command.arg("--version");
422    let output = output_with_executable_busy_retry(&mut command).map_err(|source| {
423        BinaryenToolError::VersionProcess {
424            path: path.clone(),
425            source,
426        }
427    })?;
428    let actual = if output.status.success() {
429        String::from_utf8_lossy(&output.stdout).trim().to_string()
430    } else {
431        String::from_utf8_lossy(&output.stderr).trim().to_string()
432    };
433    if !output.status.success() || actual != BINARYEN_VERSION_IDENTITY {
434        return Err(BinaryenToolError::VersionMismatch {
435            path,
436            actual,
437            expected: BINARYEN_VERSION_IDENTITY,
438        });
439    }
440
441    Ok(BinaryenExecutable {
442        path,
443        version_identity: actual,
444        sha256,
445    })
446}
447
448fn admit_runtime_library(executable: &Path, expected: &str) -> Result<(), BinaryenToolError> {
449    let library = executable.with_file_name("../lib/libbinaryen.dylib");
450    let metadata = std::fs::symlink_metadata(&library).map_err(|source| BinaryenToolError::Io {
451        operation: "inspect Binaryen runtime library",
452        path: library.clone(),
453        source,
454    })?;
455    if !metadata.is_file() {
456        return Err(BinaryenToolError::Io {
457            operation: "inspect Binaryen runtime library",
458            path: library,
459            source: io::Error::new(
460                io::ErrorKind::InvalidData,
461                "runtime library must be a regular file",
462            ),
463        });
464    }
465    let actual = sha256_file(&library)?;
466    if actual != expected {
467        return Err(BinaryenToolError::RuntimeLibraryHashMismatch {
468            path: library,
469            actual,
470            expected: expected.to_string(),
471        });
472    }
473    Ok(())
474}
475
476fn home_is_root() -> bool {
477    env::var_os("HOME").is_some_and(|home| Path::new(&home) == Path::new("/"))
478}
479
480#[cfg(test)]
481pub(crate) fn resolve_test_binaryen(
482    command: &str,
483) -> Result<BinaryenExecutable, BinaryenToolError> {
484    let path = resolve_executable(OsStr::new(command))?;
485    let expected = sha256_file(&path)?;
486    admit_binaryen_executable(&path, &expected, None)
487}