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