wasm_bindgen.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. use crate::{CliSettings, Result, Workspace};
  2. use anyhow::{anyhow, Context};
  3. use flate2::read::GzDecoder;
  4. use std::path::{Path, PathBuf};
  5. use tar::Archive;
  6. use tempfile::TempDir;
  7. use tokio::{fs, process::Command};
  8. pub(crate) struct WasmBindgen {
  9. version: String,
  10. input_path: PathBuf,
  11. out_dir: PathBuf,
  12. out_name: String,
  13. target: String,
  14. debug: bool,
  15. keep_debug: bool,
  16. demangle: bool,
  17. remove_name_section: bool,
  18. remove_producers_section: bool,
  19. keep_lld_exports: bool,
  20. }
  21. impl WasmBindgen {
  22. pub(crate) fn new(version: &str) -> Self {
  23. Self {
  24. version: version.to_string(),
  25. input_path: PathBuf::new(),
  26. out_dir: PathBuf::new(),
  27. out_name: String::new(),
  28. target: String::new(),
  29. debug: true,
  30. keep_debug: true,
  31. demangle: true,
  32. remove_name_section: false,
  33. remove_producers_section: false,
  34. keep_lld_exports: false,
  35. }
  36. }
  37. pub(crate) fn input_path(self, input_path: &Path) -> Self {
  38. Self {
  39. input_path: input_path.to_path_buf(),
  40. ..self
  41. }
  42. }
  43. pub(crate) fn out_dir(self, out_dir: &Path) -> Self {
  44. Self {
  45. out_dir: out_dir.to_path_buf(),
  46. ..self
  47. }
  48. }
  49. pub(crate) fn out_name(self, out_name: &str) -> Self {
  50. Self {
  51. out_name: out_name.to_string(),
  52. ..self
  53. }
  54. }
  55. pub(crate) fn target(self, target: &str) -> Self {
  56. Self {
  57. target: target.to_string(),
  58. ..self
  59. }
  60. }
  61. pub(crate) fn debug(self, debug: bool) -> Self {
  62. Self { debug, ..self }
  63. }
  64. pub(crate) fn keep_debug(self, keep_debug: bool) -> Self {
  65. Self { keep_debug, ..self }
  66. }
  67. pub(crate) fn demangle(self, demangle: bool) -> Self {
  68. Self { demangle, ..self }
  69. }
  70. pub(crate) fn remove_name_section(self, remove_name_section: bool) -> Self {
  71. Self {
  72. remove_name_section,
  73. ..self
  74. }
  75. }
  76. pub(crate) fn remove_producers_section(self, remove_producers_section: bool) -> Self {
  77. Self {
  78. remove_producers_section,
  79. ..self
  80. }
  81. }
  82. pub(crate) fn keep_lld_sections(self, keep_lld_sections: bool) -> Self {
  83. Self {
  84. keep_lld_exports: keep_lld_sections,
  85. ..self
  86. }
  87. }
  88. /// Run the bindgen command with the current settings
  89. pub(crate) async fn run(&self) -> Result<std::process::Output> {
  90. let binary = self.get_binary_path().await?;
  91. let mut args = Vec::new();
  92. // Target
  93. args.push("--target");
  94. args.push(&self.target);
  95. // Options
  96. if self.debug {
  97. args.push("--debug");
  98. }
  99. if !self.demangle {
  100. args.push("--no-demangle");
  101. }
  102. if self.keep_debug {
  103. args.push("--keep-debug");
  104. }
  105. if self.remove_name_section {
  106. args.push("--remove-name-section");
  107. }
  108. if self.remove_producers_section {
  109. args.push("--remove-producers-section");
  110. }
  111. if self.keep_lld_exports {
  112. args.push("--keep-lld-exports");
  113. }
  114. // Out name
  115. args.push("--out-name");
  116. args.push(&self.out_name);
  117. // wbg generates typescript bindnings by default - we don't want those
  118. args.push("--no-typescript");
  119. // Out dir
  120. let out_dir = self
  121. .out_dir
  122. .to_str()
  123. .expect("input_path should be valid utf8");
  124. args.push("--out-dir");
  125. args.push(out_dir);
  126. // Input path
  127. let input_path = self
  128. .input_path
  129. .to_str()
  130. .expect("input_path should be valid utf8");
  131. args.push(input_path);
  132. tracing::debug!("wasm-bindgen args: {:#?}", args);
  133. // Run bindgen
  134. let output = Command::new(binary).args(args).output().await?;
  135. // Check for errors
  136. if !output.stderr.is_empty() {
  137. if output.status.success() {
  138. tracing::debug!(
  139. "wasm-bindgen warnings: {}",
  140. String::from_utf8_lossy(&output.stderr)
  141. );
  142. } else {
  143. tracing::error!(
  144. "wasm-bindgen error: {}",
  145. String::from_utf8_lossy(&output.stderr)
  146. );
  147. }
  148. }
  149. Ok(output)
  150. }
  151. /// Verify the installed version of wasm-bindgen-cli
  152. ///
  153. /// For local installations, this will check that the installed version matches the specified version.
  154. /// For managed installations, this will check that the version managed by `dx` is the specified version.
  155. pub async fn verify_install(version: &str) -> anyhow::Result<()> {
  156. let settings = Self::new(version);
  157. if CliSettings::prefer_no_downloads() {
  158. settings.verify_local_install().await
  159. } else {
  160. settings.verify_managed_install().await
  161. }
  162. }
  163. /// Install the specified wasm-bingen version.
  164. ///
  165. /// This will overwrite any existing wasm-bindgen binaries of the same version.
  166. ///
  167. /// This will attempt to install wasm-bindgen from:
  168. /// 1. Direct GitHub release download.
  169. /// 2. `cargo binstall` if installed.
  170. /// 3. Compile from source with `cargo install`.
  171. async fn install(&self) -> anyhow::Result<()> {
  172. tracing::info!("Installing wasm-bindgen-cli@{}...", self.version);
  173. // Attempt installation from GitHub
  174. if let Err(e) = self.install_github().await {
  175. tracing::error!("Failed to install wasm-bindgen-cli@{}: {e}", self.version);
  176. } else {
  177. tracing::info!(
  178. "wasm-bindgen-cli@{} was successfully installed from GitHub.",
  179. self.version
  180. );
  181. return Ok(());
  182. }
  183. // Attempt installation from binstall.
  184. if let Err(e) = self.install_binstall().await {
  185. tracing::error!("Failed to install wasm-bindgen-cli@{}: {e}", self.version);
  186. tracing::info!("Failed to install prebuilt binary for wasm-bindgen-cli@{}. Compiling from source instead. This may take a while.", self.version);
  187. } else {
  188. tracing::info!(
  189. "wasm-bindgen-cli@{} was successfully installed from cargo-binstall.",
  190. self.version
  191. );
  192. return Ok(());
  193. }
  194. // Attempt installation from cargo.
  195. self.install_cargo()
  196. .await
  197. .context("failed to install wasm-bindgen-cli from cargo")?;
  198. tracing::info!(
  199. "wasm-bindgen-cli@{} was successfully installed from source.",
  200. self.version
  201. );
  202. Ok(())
  203. }
  204. async fn install_github(&self) -> anyhow::Result<()> {
  205. tracing::debug!(
  206. "Attempting to install wasm-bindgen-cli@{} from GitHub",
  207. self.version
  208. );
  209. let url = self.git_install_url().ok_or_else(|| {
  210. anyhow!(
  211. "no available GitHub binary for wasm-bindgen-cli@{}",
  212. self.version
  213. )
  214. })?;
  215. // Get the final binary location.
  216. let binary_path = self.get_binary_path().await?;
  217. // Download then extract wasm-bindgen-cli.
  218. let bytes = reqwest::get(url).await?.bytes().await?;
  219. // Unpack the first tar entry to the final binary location
  220. Archive::new(GzDecoder::new(bytes.as_ref()))
  221. .entries()?
  222. .find(|entry| {
  223. entry
  224. .as_ref()
  225. .map(|e| {
  226. e.path_bytes()
  227. .ends_with(self.downloaded_bin_name().as_bytes())
  228. })
  229. .unwrap_or(false)
  230. })
  231. .context("Failed to find entry")??
  232. .unpack(&binary_path)
  233. .context("failed to unpack wasm-bindgen-cli binary")?;
  234. Ok(())
  235. }
  236. async fn install_binstall(&self) -> anyhow::Result<()> {
  237. tracing::debug!(
  238. "Attempting to install wasm-bindgen-cli@{} from cargo-binstall",
  239. self.version
  240. );
  241. let package = self.cargo_bin_name();
  242. let tempdir = TempDir::new()?;
  243. // Run install command
  244. Command::new("cargo")
  245. .args([
  246. "binstall",
  247. &package,
  248. "--no-confirm",
  249. "--force",
  250. "--no-track",
  251. "--install-path",
  252. ])
  253. .arg(tempdir.path())
  254. .output()
  255. .await?;
  256. fs::copy(
  257. tempdir.path().join(self.downloaded_bin_name()),
  258. self.get_binary_path().await?,
  259. )
  260. .await?;
  261. Ok(())
  262. }
  263. async fn install_cargo(&self) -> anyhow::Result<()> {
  264. tracing::debug!(
  265. "Attempting to install wasm-bindgen-cli@{} from cargo-install",
  266. self.version
  267. );
  268. let package = self.cargo_bin_name();
  269. let tempdir = TempDir::new()?;
  270. // Run install command
  271. Command::new("cargo")
  272. .args([
  273. "install",
  274. &package,
  275. "--bin",
  276. "wasm-bindgen",
  277. "--no-track",
  278. "--force",
  279. "--root",
  280. ])
  281. .arg(tempdir.path())
  282. .output()
  283. .await
  284. .context("failed to install wasm-bindgen-cli from cargo-install")?;
  285. tracing::info!("Copying into path: {}", tempdir.path().display());
  286. // copy the wasm-bindgen out of the tempdir to the final location
  287. fs::copy(
  288. tempdir.path().join("bin").join(self.downloaded_bin_name()),
  289. self.get_binary_path().await?,
  290. )
  291. .await
  292. .context("failed to copy wasm-bindgen binary")?;
  293. Ok(())
  294. }
  295. async fn verify_local_install(&self) -> anyhow::Result<()> {
  296. tracing::trace!(
  297. "Verifying wasm-bindgen-cli@{} is installed in the path",
  298. self.version
  299. );
  300. let binary = self.get_binary_path().await?;
  301. let output = Command::new(binary)
  302. .args(["--version"])
  303. .output()
  304. .await
  305. .context("Failed to check wasm-bindgen-cli version")?;
  306. let stdout = String::from_utf8(output.stdout)
  307. .context("Failed to extract wasm-bindgen-cli output")?;
  308. let installed_version = stdout.trim_start_matches("wasm-bindgen").trim();
  309. if installed_version != self.version {
  310. return Err(anyhow!(
  311. "Incorrect wasm-bindgen-cli version: project requires version {} but version {} is installed",
  312. self.version,
  313. installed_version,
  314. ));
  315. }
  316. Ok(())
  317. }
  318. async fn verify_managed_install(&self) -> anyhow::Result<()> {
  319. tracing::trace!(
  320. "Verifying wasm-bindgen-cli@{} is installed in the tool directory",
  321. self.version
  322. );
  323. let binary_name = self.installed_bin_name();
  324. let path = self.install_dir().await?.join(binary_name);
  325. if !path.exists() {
  326. self.install().await?;
  327. }
  328. Ok(())
  329. }
  330. async fn get_binary_path(&self) -> anyhow::Result<PathBuf> {
  331. if CliSettings::prefer_no_downloads() {
  332. which::which("wasm-bindgen")
  333. .map_err(|_| anyhow!("Missing wasm-bindgen-cli@{}", self.version))
  334. } else {
  335. let installed_name = self.installed_bin_name();
  336. let install_dir = self.install_dir().await?;
  337. Ok(install_dir.join(installed_name))
  338. }
  339. }
  340. async fn install_dir(&self) -> anyhow::Result<PathBuf> {
  341. let bindgen_dir = Workspace::dioxus_home_dir().join("wasm-bindgen/");
  342. fs::create_dir_all(&bindgen_dir).await?;
  343. Ok(bindgen_dir)
  344. }
  345. fn installed_bin_name(&self) -> String {
  346. let mut name = format!("wasm-bindgen-{}", self.version);
  347. if cfg!(windows) {
  348. name = format!("{name}.exe");
  349. }
  350. name
  351. }
  352. fn cargo_bin_name(&self) -> String {
  353. format!("wasm-bindgen-cli@{}", self.version)
  354. }
  355. fn downloaded_bin_name(&self) -> &'static str {
  356. if cfg!(windows) {
  357. "wasm-bindgen.exe"
  358. } else {
  359. "wasm-bindgen"
  360. }
  361. }
  362. fn git_install_url(&self) -> Option<String> {
  363. let platform = if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
  364. "x86_64-pc-windows-msvc"
  365. } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
  366. "x86_64-unknown-linux-musl"
  367. } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
  368. "aarch64-unknown-linux-gnu"
  369. } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
  370. "x86_64-apple-darwin"
  371. } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
  372. "aarch64-apple-darwin"
  373. } else {
  374. return None;
  375. };
  376. Some(format!(
  377. "https://github.com/rustwasm/wasm-bindgen/releases/download/{}/wasm-bindgen-{}-{}.tar.gz",
  378. self.version, self.version, platform
  379. ))
  380. }
  381. }
  382. #[cfg(test)]
  383. mod test {
  384. use super::*;
  385. const VERSION: &str = "0.2.99";
  386. /// Test the github installer.
  387. #[tokio::test]
  388. async fn test_github_install() {
  389. if !crate::devcfg::test_installs() {
  390. return;
  391. }
  392. let binary = WasmBindgen::new(VERSION);
  393. reset_test().await;
  394. binary.install_github().await.unwrap();
  395. test_verify_install().await;
  396. verify_installation(&binary).await;
  397. }
  398. /// Test the cargo installer.
  399. #[tokio::test]
  400. async fn test_cargo_install() {
  401. if !crate::devcfg::test_installs() {
  402. return;
  403. }
  404. let binary = WasmBindgen::new(VERSION);
  405. reset_test().await;
  406. binary.install_cargo().await.unwrap();
  407. test_verify_install().await;
  408. verify_installation(&binary).await;
  409. }
  410. // CI doesn't have binstall.
  411. // Test the binstall installer
  412. #[tokio::test]
  413. async fn test_binstall_install() {
  414. if !crate::devcfg::test_installs() {
  415. return;
  416. }
  417. let binary = WasmBindgen::new(VERSION);
  418. reset_test().await;
  419. binary.install_binstall().await.unwrap();
  420. test_verify_install().await;
  421. verify_installation(&binary).await;
  422. }
  423. /// Helper to test `verify_install` after an installation.
  424. async fn test_verify_install() {
  425. WasmBindgen::verify_install(VERSION).await.unwrap();
  426. }
  427. /// Helper to test that the installed binary actually exists.
  428. async fn verify_installation(binary: &WasmBindgen) {
  429. let path = binary.install_dir().await.unwrap();
  430. let name = binary.installed_bin_name();
  431. let binary_path = path.join(name);
  432. assert!(
  433. binary_path.exists(),
  434. "wasm-bindgen binary doesn't exist after installation"
  435. );
  436. }
  437. /// Delete the installed binary. The temp folder should be automatically deleted.
  438. async fn reset_test() {
  439. let binary = WasmBindgen::new(VERSION);
  440. let path = binary.install_dir().await.unwrap();
  441. let name = binary.installed_bin_name();
  442. let binary_path = path.join(name);
  443. let _ = tokio::fs::remove_file(binary_path).await;
  444. }
  445. }