1#[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#[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#[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#[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
274pub 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
290pub 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
297pub 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
316pub 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}