bundle.rs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  1. use super::templates::InfoPlistData;
  2. use crate::wasm_bindgen::WasmBindgenBuilder;
  3. use crate::{BuildRequest, Platform};
  4. use crate::{Result, TraceSrc};
  5. use anyhow::Context;
  6. use dioxus_cli_opt::{process_file_to, AssetManifest};
  7. use manganis_core::AssetOptions;
  8. use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
  9. use std::collections::HashSet;
  10. use std::future::Future;
  11. use std::path::{Path, PathBuf};
  12. use std::pin::Pin;
  13. use std::{sync::atomic::AtomicUsize, time::Duration};
  14. use tokio::process::Command;
  15. /// The end result of a build.
  16. ///
  17. /// Contains the final asset manifest, the executables, and the workdir.
  18. ///
  19. /// Every dioxus app can have an optional server executable which will influence the final bundle.
  20. /// This is built in parallel with the app executable during the `build` phase and the progres/status
  21. /// of the build is aggregated.
  22. ///
  23. /// The server will *always* be dropped into the `web` folder since it is considered "web" in nature,
  24. /// and will likely need to be combined with the public dir to be useful.
  25. ///
  26. /// We do our best to assemble read-to-go bundles here, such that the "bundle" step for each platform
  27. /// can just use the build dir
  28. ///
  29. /// When we write the AppBundle to a folder, it'll contain each bundle for each platform under the app's name:
  30. /// ```
  31. /// dog-app/
  32. /// build/
  33. /// web/
  34. /// server.exe
  35. /// assets/
  36. /// some-secret-asset.txt (a server-side asset)
  37. /// public/
  38. /// index.html
  39. /// assets/
  40. /// logo.png
  41. /// desktop/
  42. /// App.app
  43. /// App.appimage
  44. /// App.exe
  45. /// server/
  46. /// server
  47. /// assets/
  48. /// some-secret-asset.txt (a server-side asset)
  49. /// ios/
  50. /// App.app
  51. /// App.ipa
  52. /// android/
  53. /// App.apk
  54. /// bundle/
  55. /// build.json
  56. /// Desktop.app
  57. /// Mobile_x64.ipa
  58. /// Mobile_arm64.ipa
  59. /// Mobile_rosetta.ipa
  60. /// web.appimage
  61. /// web/
  62. /// server.exe
  63. /// assets/
  64. /// some-secret-asset.txt
  65. /// public/
  66. /// index.html
  67. /// assets/
  68. /// logo.png
  69. /// style.css
  70. /// ```
  71. ///
  72. /// When deploying, the build.json file will provide all the metadata that dx-deploy will use to
  73. /// push the app to stores, set up infra, manage versions, etc.
  74. ///
  75. /// The format of each build will follow the name plus some metadata such that when distributing you
  76. /// can easily trim off the metadata.
  77. ///
  78. /// The idea here is that we can run any of the programs in the same way that they're deployed.
  79. ///
  80. ///
  81. /// ## Bundle structure links
  82. /// - apple: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle
  83. /// - appimage: https://docs.appimage.org/packaging-guide/manual.html#ref-manual
  84. ///
  85. /// ## Extra links
  86. /// - xbuild: https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs
  87. #[derive(Debug)]
  88. pub(crate) struct AppBundle {
  89. pub(crate) build: BuildRequest,
  90. pub(crate) app: BuildArtifacts,
  91. pub(crate) server: Option<BuildArtifacts>,
  92. }
  93. #[derive(Debug)]
  94. pub struct BuildArtifacts {
  95. pub(crate) exe: PathBuf,
  96. pub(crate) assets: AssetManifest,
  97. pub(crate) time_taken: Duration,
  98. }
  99. impl AppBundle {
  100. /// ## Web:
  101. /// Create a folder that is somewhat similar to an app-image (exe + asset)
  102. /// The server is dropped into the `web` folder, even if there's no `public` folder.
  103. /// If there's no server (SPA), we still use the `web` folder, but it only contains the
  104. /// public folder.
  105. /// ```
  106. /// web/
  107. /// server
  108. /// assets/
  109. /// public/
  110. /// index.html
  111. /// wasm/
  112. /// app.wasm
  113. /// glue.js
  114. /// snippets/
  115. /// ...
  116. /// assets/
  117. /// logo.png
  118. /// ```
  119. ///
  120. /// ## Linux:
  121. /// https://docs.appimage.org/reference/appdir.html#ref-appdir
  122. /// current_exe.join("Assets")
  123. /// ```
  124. /// app.appimage/
  125. /// AppRun
  126. /// app.desktop
  127. /// package.json
  128. /// assets/
  129. /// logo.png
  130. /// ```
  131. ///
  132. /// ## Macos
  133. /// We simply use the macos format where binaries are in `Contents/MacOS` and assets are in `Contents/Resources`
  134. /// We put assets in an assets dir such that it generally matches every other platform and we can
  135. /// output `/assets/blah` from manganis.
  136. /// ```
  137. /// App.app/
  138. /// Contents/
  139. /// Info.plist
  140. /// MacOS/
  141. /// Frameworks/
  142. /// Resources/
  143. /// assets/
  144. /// blah.icns
  145. /// blah.png
  146. /// CodeResources
  147. /// _CodeSignature/
  148. /// ```
  149. ///
  150. /// ## iOS
  151. /// Not the same as mac! ios apps are a bit "flattened" in comparison. simpler format, presumably
  152. /// since most ios apps don't ship frameworks/plugins and such.
  153. ///
  154. /// todo(jon): include the signing and entitlements in this format diagram.
  155. /// ```
  156. /// App.app/
  157. /// main
  158. /// assets/
  159. /// ```
  160. ///
  161. /// ## Android:
  162. ///
  163. /// Currently we need to generate a `src` type structure, not a pre-packaged apk structure, since
  164. /// we need to compile kotlin and java. This pushes us into using gradle and following a structure
  165. /// similar to that of cargo mobile2. Eventually I'd like to slim this down (drop buildSrc) and
  166. /// drive the kotlin build ourselves. This would let us drop gradle (yay! no plugins!) but requires
  167. /// us to manage dependencies (like kotlinc) ourselves (yuck!).
  168. ///
  169. /// https://github.com/WanghongLin/miscellaneous/blob/master/tools/build-apk-manually.sh
  170. ///
  171. /// Unfortunately, it seems that while we can drop the `android` build plugin, we still will need
  172. /// gradle since kotlin is basically gradle-only.
  173. ///
  174. /// Pre-build:
  175. /// ```
  176. /// app.apk/
  177. /// .gradle
  178. /// app/
  179. /// src/
  180. /// main/
  181. /// assets/
  182. /// jniLibs/
  183. /// java/
  184. /// kotlin/
  185. /// res/
  186. /// AndroidManifest.xml
  187. /// build.gradle.kts
  188. /// proguard-rules.pro
  189. /// buildSrc/
  190. /// build.gradle.kts
  191. /// src/
  192. /// main/
  193. /// kotlin/
  194. /// BuildTask.kt
  195. /// build.gradle.kts
  196. /// gradle.properties
  197. /// gradlew
  198. /// gradlew.bat
  199. /// settings.gradle
  200. /// ```
  201. ///
  202. /// Final build:
  203. /// ```
  204. /// app.apk/
  205. /// AndroidManifest.xml
  206. /// classes.dex
  207. /// assets/
  208. /// logo.png
  209. /// lib/
  210. /// armeabi-v7a/
  211. /// libmyapp.so
  212. /// arm64-v8a/
  213. /// libmyapp.so
  214. /// ```
  215. /// Notice that we *could* feasibly build this ourselves :)
  216. ///
  217. /// ## Windows:
  218. /// https://superuser.com/questions/749447/creating-a-single-file-executable-from-a-directory-in-windows
  219. /// Windows does not provide an AppImage format, so instead we're going build the same folder
  220. /// structure as an AppImage, but when distributing, we'll create a .exe that embeds the resources
  221. /// as an embedded .zip file. When the app runs, it will implicitly unzip its resources into the
  222. /// Program Files folder. Any subsequent launches of the parent .exe will simply call the AppRun.exe
  223. /// entrypoint in the associated Program Files folder.
  224. ///
  225. /// This is, in essence, the same as an installer, so we might eventually just support something like msi/msix
  226. /// which functionally do the same thing but with a sleeker UI.
  227. ///
  228. /// This means no installers are required and we can bake an updater into the host exe.
  229. ///
  230. /// ## Handling asset lookups:
  231. /// current_exe.join("assets")
  232. /// ```
  233. /// app.appimage/
  234. /// main.exe
  235. /// main.desktop
  236. /// package.json
  237. /// assets/
  238. /// logo.png
  239. /// ```
  240. ///
  241. /// Since we support just a few locations, we could just search for the first that exists
  242. /// - usr
  243. /// - ../Resources
  244. /// - assets
  245. /// - Assets
  246. /// - $cwd/assets
  247. ///
  248. /// ```
  249. /// assets::root() ->
  250. /// mac -> ../Resources/
  251. /// ios -> ../Resources/
  252. /// android -> assets/
  253. /// server -> assets/
  254. /// liveview -> assets/
  255. /// web -> /assets/
  256. /// root().join(bundled)
  257. /// ```
  258. pub(crate) async fn new(
  259. build: BuildRequest,
  260. app: BuildArtifacts,
  261. server: Option<BuildArtifacts>,
  262. ) -> Result<Self> {
  263. let bundle = Self { app, server, build };
  264. tracing::debug!("Assembling app bundle");
  265. bundle.build.status_start_bundle();
  266. /*
  267. assume the build dir is already created by BuildRequest
  268. todo(jon): maybe refactor this a bit to force AppBundle to be created before it can be filled in
  269. */
  270. bundle
  271. .write_main_executable()
  272. .await
  273. .context("Failed to write main executable")?;
  274. bundle.write_server_executable().await?;
  275. bundle
  276. .write_assets()
  277. .await
  278. .context("Failed to write assets")?;
  279. bundle.write_metadata().await?;
  280. bundle.optimize().await?;
  281. bundle
  282. .assemble()
  283. .await
  284. .context("Failed to assemble app bundle")?;
  285. tracing::debug!("Bundle created at {}", bundle.build.root_dir().display());
  286. Ok(bundle)
  287. }
  288. /// Take the output of rustc and make it into the main exe of the bundle
  289. ///
  290. /// For wasm, we'll want to run `wasm-bindgen` to make it a wasm binary along with some other optimizations
  291. /// Other platforms we might do some stripping or other optimizations
  292. /// Move the executable to the workdir
  293. async fn write_main_executable(&self) -> Result<()> {
  294. match self.build.build.platform() {
  295. // Run wasm-bindgen on the wasm binary and set its output to be in the bundle folder
  296. // Also run wasm-opt on the wasm binary, and sets the index.html since that's also the "executable".
  297. //
  298. // The wasm stuff will be in a folder called "wasm" in the workdir.
  299. //
  300. // Final output format:
  301. // ```
  302. // dx/
  303. // app/
  304. // web/
  305. // bundle/
  306. // build/
  307. // public/
  308. // index.html
  309. // wasm/
  310. // app.wasm
  311. // glue.js
  312. // snippets/
  313. // ...
  314. // assets/
  315. // logo.png
  316. // ```
  317. Platform::Web => {
  318. // Run wasm-bindgen and drop its output into the assets folder under "dioxus"
  319. self.build.status_wasm_bindgen_start();
  320. self.run_wasm_bindgen(&self.app.exe.with_extension("wasm"), &self.build.exe_dir())
  321. .await?;
  322. // Only run wasm-opt if the feature is enabled
  323. // Wasm-opt has an expensive build script that makes it annoying to keep enabled for iterative dev
  324. // We put it behind the "wasm-opt" feature flag so that it can be disabled when iterating on the cli
  325. self.run_wasm_opt(&self.build.exe_dir())?;
  326. // Write the index.html file with the pre-configured contents we got from pre-rendering
  327. std::fs::write(
  328. self.build.root_dir().join("index.html"),
  329. self.build.prepare_html()?,
  330. )?;
  331. }
  332. // this will require some extra oomf to get the multi architecture builds...
  333. // for now, we just copy the exe into the current arch (which, sorry, is hardcoded for my m1)
  334. // we'll want to do multi-arch builds in the future, so there won't be *one* exe dir to worry about
  335. // eventually `exe_dir` and `main_exe` will need to take in an arch and return the right exe path
  336. //
  337. // todo(jon): maybe just symlink this rather than copy it?
  338. Platform::Android => {
  339. self.copy_android_exe(&self.app.exe, &self.main_exe())
  340. .await?;
  341. }
  342. // These are all super simple, just copy the exe into the folder
  343. // eventually, perhaps, maybe strip + encrypt the exe?
  344. Platform::MacOS
  345. | Platform::Windows
  346. | Platform::Linux
  347. | Platform::Ios
  348. | Platform::Liveview
  349. | Platform::Server => {
  350. std::fs::copy(&self.app.exe, self.main_exe())?;
  351. }
  352. }
  353. Ok(())
  354. }
  355. /// Copy the assets out of the manifest and into the target location
  356. ///
  357. /// Should be the same on all platforms - just copy over the assets from the manifest into the output directory
  358. async fn write_assets(&self) -> Result<()> {
  359. // Server doesn't need assets - web will provide them
  360. if self.build.build.platform() == Platform::Server {
  361. return Ok(());
  362. }
  363. let asset_dir = self.build.asset_dir();
  364. // First, clear the asset dir of any files that don't exist in the new manifest
  365. _ = tokio::fs::create_dir_all(&asset_dir).await;
  366. // Create a set of all the paths that new files will be bundled to
  367. let bundled_output_paths: HashSet<_> = self
  368. .app
  369. .assets
  370. .assets
  371. .values()
  372. .map(|a| asset_dir.join(a.bundled_path()))
  373. .collect();
  374. // one possible implementation of walking a directory only visiting files
  375. fn remove_old_assets<'a>(
  376. path: &'a Path,
  377. bundled_output_paths: &'a HashSet<PathBuf>,
  378. ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + 'a>> {
  379. Box::pin(async move {
  380. // If this asset is in the manifest, we don't need to remove it
  381. if bundled_output_paths.contains(path.canonicalize()?.as_path()) {
  382. return Ok(());
  383. }
  384. // Otherwise, if it is a directory, we need to walk it and remove child files
  385. if path.is_dir() {
  386. for entry in std::fs::read_dir(path)?.flatten() {
  387. let path = entry.path();
  388. remove_old_assets(&path, bundled_output_paths).await?;
  389. }
  390. if path.read_dir()?.next().is_none() {
  391. // If the directory is empty, remove it
  392. tokio::fs::remove_dir(path).await?;
  393. }
  394. } else {
  395. // If it is a file, remove it
  396. tokio::fs::remove_file(path).await?;
  397. }
  398. Ok(())
  399. })
  400. }
  401. tracing::debug!("Removing old assets");
  402. remove_old_assets(&asset_dir, &bundled_output_paths).await?;
  403. // todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't
  404. let mut assets_to_transfer = vec![];
  405. // Queue the bundled assets
  406. for (asset, bundled) in &self.app.assets.assets {
  407. let from = asset.clone();
  408. let to = asset_dir.join(bundled.bundled_path());
  409. tracing::debug!("Copying asset {from:?} to {to:?}");
  410. assets_to_transfer.push((from, to, *bundled.options()));
  411. }
  412. // And then queue the legacy assets
  413. // ideally, one day, we can just check the rsx!{} calls for references to assets
  414. for from in self.build.krate.legacy_asset_dir_files() {
  415. let to = asset_dir.join(from.file_name().unwrap());
  416. tracing::debug!("Copying legacy asset {from:?} to {to:?}");
  417. assets_to_transfer.push((from, to, AssetOptions::Unknown));
  418. }
  419. let asset_count = assets_to_transfer.len();
  420. let current_asset = AtomicUsize::new(0);
  421. // Parallel Copy over the assets and keep track of progress with an atomic counter
  422. let progress = self.build.progress.clone();
  423. // Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task
  424. tokio::task::spawn_blocking(move || {
  425. assets_to_transfer
  426. .par_iter()
  427. .try_for_each(|(from, to, options)| {
  428. let current = current_asset.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
  429. tracing::trace!("Starting asset copy {current}/{asset_count} from {from:?}");
  430. let res = process_file_to(options, from, to);
  431. if let Err(err) = res.as_ref() {
  432. tracing::error!("Failed to copy asset {from:?}: {err}");
  433. }
  434. BuildRequest::status_copied_asset(
  435. &progress,
  436. current,
  437. asset_count,
  438. from.to_path_buf(),
  439. );
  440. res.map(|_| ())
  441. })
  442. })
  443. .await
  444. .map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??;
  445. Ok(())
  446. }
  447. /// The item that we'll try to run directly if we need to.
  448. ///
  449. /// todo(jon): we should name the app properly instead of making up the exe name. It's kinda okay for dev mode, but def not okay for prod
  450. pub fn main_exe(&self) -> PathBuf {
  451. self.build.exe_dir().join(self.build.platform_exe_name())
  452. }
  453. /// We always put the server in the `web` folder!
  454. /// Only the `web` target will generate a `public` folder though
  455. async fn write_server_executable(&self) -> Result<()> {
  456. if let Some(server) = &self.server {
  457. let to = self
  458. .server_exe()
  459. .expect("server should be set if we're building a server");
  460. std::fs::create_dir_all(self.server_exe().unwrap().parent().unwrap())?;
  461. tracing::debug!("Copying server executable to: {to:?} {server:#?}");
  462. // Remove the old server executable if it exists, since copying might corrupt it :(
  463. // todo(jon): do this in more places, I think
  464. _ = std::fs::remove_file(&to);
  465. std::fs::copy(&server.exe, to)?;
  466. }
  467. Ok(())
  468. }
  469. /// todo(jon): use handlebars templates instead of these prebaked templates
  470. async fn write_metadata(&self) -> Result<()> {
  471. // write the Info.plist file
  472. match self.build.build.platform() {
  473. Platform::MacOS => {
  474. let dest = self.build.root_dir().join("Contents").join("Info.plist");
  475. let plist = self.macos_plist_contents()?;
  476. std::fs::write(dest, plist)?;
  477. }
  478. Platform::Ios => {
  479. let dest = self.build.root_dir().join("Info.plist");
  480. let plist = self.ios_plist_contents()?;
  481. std::fs::write(dest, plist)?;
  482. }
  483. // AndroidManifest.xml
  484. // er.... maybe even all the kotlin/java/gradle stuff?
  485. Platform::Android => {}
  486. // Probably some custom format or a plist file (haha)
  487. // When we do the proper bundle, we'll need to do something with wix templates, I think?
  488. Platform::Windows => {}
  489. // eventually we'll create the .appimage file, I guess?
  490. Platform::Linux => {}
  491. // These are served as folders, not appimages, so we don't need to do anything special (I think?)
  492. // Eventually maybe write some secrets/.env files for the server?
  493. // We could also distribute them as a deb/rpm for linux and msi for windows
  494. Platform::Web => {}
  495. Platform::Server => {}
  496. Platform::Liveview => {}
  497. }
  498. Ok(())
  499. }
  500. /// Run the optimizers, obfuscators, minimizers, signers, etc
  501. pub(crate) async fn optimize(&self) -> Result<()> {
  502. match self.build.build.platform() {
  503. Platform::Web => {
  504. // Compress the asset dir
  505. // If pre-compressing is enabled, we can pre_compress the wasm-bindgen output
  506. let pre_compress = self
  507. .build
  508. .krate
  509. .should_pre_compress_web_assets(self.build.build.release);
  510. let bindgen_dir = self.build.exe_dir();
  511. tokio::task::spawn_blocking(move || {
  512. crate::fastfs::pre_compress_folder(&bindgen_dir, pre_compress)
  513. })
  514. .await
  515. .unwrap()?;
  516. // Run SSG and cache static routes
  517. if self.build.build.ssg {
  518. self.run_ssg().await?;
  519. }
  520. }
  521. Platform::MacOS => {}
  522. Platform::Windows => {}
  523. Platform::Linux => {}
  524. Platform::Ios => {}
  525. Platform::Android => {}
  526. Platform::Server => {}
  527. Platform::Liveview => {}
  528. }
  529. Ok(())
  530. }
  531. pub(crate) fn server_exe(&self) -> Option<PathBuf> {
  532. if let Some(_server) = &self.server {
  533. let mut path = self
  534. .build
  535. .krate
  536. .build_dir(Platform::Server, self.build.build.release);
  537. if cfg!(windows) {
  538. path.push("server.exe");
  539. } else {
  540. path.push("server");
  541. }
  542. return Some(path);
  543. }
  544. None
  545. }
  546. pub(crate) async fn run_wasm_bindgen(
  547. &self,
  548. input_path: &Path,
  549. bindgen_outdir: &Path,
  550. ) -> anyhow::Result<()> {
  551. tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen");
  552. let input_path = input_path.to_path_buf();
  553. let bindgen_outdir = bindgen_outdir.to_path_buf();
  554. let name = self.build.krate.executable_name().to_string();
  555. let keep_debug =
  556. // if we're in debug mode, or we're generating debug symbols, keep debug info
  557. (self.build.krate.config.web.wasm_opt.debug || self.build.build.debug_symbols)
  558. // but only if we're not in release mode
  559. && !self.build.build.release;
  560. let start = std::time::Instant::now();
  561. let bindgen_version = self
  562. .build
  563. .krate
  564. .wasm_bindgen_version()
  565. .expect("this should have been checked by tool verification");
  566. WasmBindgenBuilder::new(bindgen_version)
  567. .input_path(&input_path)
  568. .target("web")
  569. .debug(keep_debug)
  570. .demangle(keep_debug)
  571. .keep_debug(keep_debug)
  572. .remove_name_section(!keep_debug)
  573. .remove_producers_section(!keep_debug)
  574. .out_name(&name)
  575. .out_dir(&bindgen_outdir)
  576. .build()
  577. .run()
  578. .await
  579. .context("Failed to generate wasm-bindgen bindings")?;
  580. tracing::debug!(dx_src = ?TraceSrc::Bundle, "wasm-bindgen complete in {:?}", start.elapsed());
  581. Ok(())
  582. }
  583. #[allow(unused)]
  584. pub(crate) fn run_wasm_opt(&self, bindgen_outdir: &std::path::Path) -> Result<()> {
  585. if !self.build.build.release {
  586. return Ok(());
  587. };
  588. self.build.status_optimizing_wasm();
  589. #[cfg(feature = "optimizations")]
  590. {
  591. use crate::config::WasmOptLevel;
  592. tracing::info!(dx_src = ?TraceSrc::Build, "Running optimization with wasm-opt...");
  593. let mut options = match self.build.krate.config.web.wasm_opt.level {
  594. WasmOptLevel::Z => {
  595. wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively()
  596. }
  597. WasmOptLevel::S => wasm_opt::OptimizationOptions::new_optimize_for_size(),
  598. WasmOptLevel::Zero => wasm_opt::OptimizationOptions::new_opt_level_0(),
  599. WasmOptLevel::One => wasm_opt::OptimizationOptions::new_opt_level_1(),
  600. WasmOptLevel::Two => wasm_opt::OptimizationOptions::new_opt_level_2(),
  601. WasmOptLevel::Three => wasm_opt::OptimizationOptions::new_opt_level_3(),
  602. WasmOptLevel::Four => wasm_opt::OptimizationOptions::new_opt_level_4(),
  603. };
  604. let wasm_file =
  605. bindgen_outdir.join(format!("{}_bg.wasm", self.build.krate.executable_name()));
  606. let old_size = wasm_file.metadata()?.len();
  607. options
  608. // WASM bindgen relies on reference types
  609. .enable_feature(wasm_opt::Feature::ReferenceTypes)
  610. .debug_info(self.build.krate.config.web.wasm_opt.debug)
  611. .run(&wasm_file, &wasm_file)
  612. .map_err(|err| crate::Error::Other(anyhow::anyhow!(err)))?;
  613. let new_size = wasm_file.metadata()?.len();
  614. tracing::debug!(
  615. dx_src = ?TraceSrc::Build,
  616. "wasm-opt reduced WASM size from {} to {} ({:2}%)",
  617. old_size,
  618. new_size,
  619. (new_size as f64 - old_size as f64) / old_size as f64 * 100.0
  620. );
  621. }
  622. Ok(())
  623. }
  624. async fn run_ssg(&self) -> anyhow::Result<()> {
  625. use futures_util::stream::FuturesUnordered;
  626. use futures_util::StreamExt;
  627. use tokio::process::Command;
  628. let fullstack_address = dioxus_cli_config::fullstack_address_or_localhost();
  629. let address = fullstack_address.ip().to_string();
  630. let port = fullstack_address.port().to_string();
  631. tracing::info!("Running SSG");
  632. // Run the server executable
  633. let _child = Command::new(
  634. self.server_exe()
  635. .context("Failed to find server executable")?,
  636. )
  637. .env(dioxus_cli_config::SERVER_PORT_ENV, port.clone())
  638. .env(dioxus_cli_config::SERVER_IP_ENV, address.clone())
  639. .stdout(std::process::Stdio::piped())
  640. .stderr(std::process::Stdio::piped())
  641. .kill_on_drop(true)
  642. .spawn()?;
  643. // Wait a second for the server to start
  644. tokio::time::sleep(std::time::Duration::from_secs(1)).await;
  645. // Get the routes from the `/static_routes` endpoint
  646. let mut routes = reqwest::Client::builder()
  647. .build()?
  648. .post(format!("http://{address}:{port}/api/static_routes"))
  649. .send()
  650. .await
  651. .context("Failed to get static routes from server")?
  652. .text()
  653. .await
  654. .map(|raw| serde_json::from_str::<Vec<String>>(&raw).unwrap())
  655. .inspect(|text| tracing::debug!("Got static routes: {text:?}"))
  656. .context("Failed to parse static routes from server")?
  657. .into_iter()
  658. .map(|line| {
  659. let port = port.clone();
  660. let address = address.clone();
  661. async move {
  662. tracing::info!("SSG: {line}");
  663. reqwest::Client::builder()
  664. .build()?
  665. .get(format!("http://{address}:{port}{line}"))
  666. .header("Accept", "text/html")
  667. .send()
  668. .await
  669. }
  670. })
  671. .collect::<FuturesUnordered<_>>();
  672. while let Some(route) = routes.next().await {
  673. match route {
  674. Ok(route) => tracing::debug!("ssg success: {route:?}"),
  675. Err(err) => tracing::error!("ssg error: {err:?}"),
  676. }
  677. }
  678. // Wait a second for the cache to be written by the server
  679. tracing::info!("Waiting a moment for isrg to propagate...");
  680. tokio::time::sleep(std::time::Duration::from_secs(10)).await;
  681. tracing::info!("SSG complete");
  682. drop(_child);
  683. Ok(())
  684. }
  685. fn macos_plist_contents(&self) -> Result<String> {
  686. handlebars::Handlebars::new()
  687. .render_template(
  688. include_str!("../../assets/macos/mac.plist.hbs"),
  689. &InfoPlistData {
  690. display_name: self.build.krate.bundled_app_name(),
  691. bundle_name: self.build.krate.bundled_app_name(),
  692. executable_name: self.build.platform_exe_name(),
  693. bundle_identifier: self.build.krate.bundle_identifier(),
  694. },
  695. )
  696. .map_err(|e| e.into())
  697. }
  698. fn ios_plist_contents(&self) -> Result<String> {
  699. handlebars::Handlebars::new()
  700. .render_template(
  701. include_str!("../../assets/ios/ios.plist.hbs"),
  702. &InfoPlistData {
  703. display_name: self.build.krate.bundled_app_name(),
  704. bundle_name: self.build.krate.bundled_app_name(),
  705. executable_name: self.build.platform_exe_name(),
  706. bundle_identifier: self.build.krate.bundle_identifier(),
  707. },
  708. )
  709. .map_err(|e| e.into())
  710. }
  711. /// Run any final tools to produce apks or other artifacts we might need.
  712. async fn assemble(&self) -> Result<()> {
  713. if let Platform::Android = self.build.build.platform() {
  714. self.build.status_running_gradle();
  715. // make sure we can execute the gradlew script
  716. #[cfg(unix)]
  717. {
  718. use std::os::unix::prelude::PermissionsExt;
  719. std::fs::set_permissions(
  720. self.build.root_dir().join("gradlew"),
  721. std::fs::Permissions::from_mode(0o755),
  722. )?;
  723. }
  724. let gradle_exec_name = match cfg!(windows) {
  725. true => "gradlew.bat",
  726. false => "gradlew",
  727. };
  728. let gradle_exec = self.build.root_dir().join(gradle_exec_name);
  729. let output = Command::new(gradle_exec)
  730. .arg("assembleDebug")
  731. .current_dir(self.build.root_dir())
  732. .stderr(std::process::Stdio::piped())
  733. .stdout(std::process::Stdio::piped())
  734. .output()
  735. .await?;
  736. if !output.status.success() {
  737. return Err(anyhow::anyhow!("Failed to assemble apk: {output:?}").into());
  738. }
  739. }
  740. Ok(())
  741. }
  742. pub(crate) fn apk_path(&self) -> PathBuf {
  743. self.build
  744. .root_dir()
  745. .join("app")
  746. .join("build")
  747. .join("outputs")
  748. .join("apk")
  749. .join("debug")
  750. .join("app-debug.apk")
  751. }
  752. /// Copy the Android executable to the target directory, and rename the hardcoded com_hardcoded_dioxuslabs entries
  753. /// to the user's app name.
  754. async fn copy_android_exe(&self, source: &Path, destination: &Path) -> Result<()> {
  755. // we might want to eventually use the objcopy logic to handle this
  756. //
  757. // https://github.com/rust-mobile/xbuild/blob/master/xbuild/template/lib.rs
  758. // https://github.com/rust-mobile/xbuild/blob/master/apk/src/lib.rs#L19
  759. std::fs::copy(source, destination)?;
  760. Ok(())
  761. }
  762. }