1
0

wasm_bindgen.rs 15 KB

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