1#[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#[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#[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#[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
289pub 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
305pub 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
312pub(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
324pub 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
352pub 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}