1
0

request.rs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  1. use super::{progress::ProgressTx, BuildArtifacts};
  2. use crate::dioxus_crate::DioxusCrate;
  3. use crate::{link::LinkAction, BuildArgs};
  4. use crate::{AppBundle, Platform, Result, TraceSrc};
  5. use anyhow::Context;
  6. use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
  7. use dioxus_cli_opt::AssetManifest;
  8. use serde::Deserialize;
  9. use std::{
  10. path::{Path, PathBuf},
  11. process::Stdio,
  12. time::Instant,
  13. };
  14. use tokio::{io::AsyncBufReadExt, process::Command};
  15. #[derive(Clone, Debug)]
  16. pub(crate) struct BuildRequest {
  17. /// The configuration for the crate we are building
  18. pub(crate) krate: DioxusCrate,
  19. /// The arguments for the build
  20. pub(crate) build: BuildArgs,
  21. /// Status channel to send our progress updates to
  22. pub(crate) progress: ProgressTx,
  23. /// The target directory for the build
  24. pub(crate) custom_target_dir: Option<PathBuf>,
  25. }
  26. impl BuildRequest {
  27. pub fn new(krate: DioxusCrate, build: BuildArgs, progress: ProgressTx) -> Self {
  28. Self {
  29. build,
  30. krate,
  31. progress,
  32. custom_target_dir: None,
  33. }
  34. }
  35. /// Run the build command with a pretty loader, returning the executable output location
  36. ///
  37. /// This will also run the fullstack build. Note that fullstack is handled separately within this
  38. /// code flow rather than outside of it.
  39. pub(crate) async fn build_all(self) -> Result<AppBundle> {
  40. tracing::debug!(
  41. "Running build command... {}",
  42. if self.build.force_sequential {
  43. "(sequentially)"
  44. } else {
  45. ""
  46. }
  47. );
  48. let (app, server) = match self.build.force_sequential {
  49. true => self.build_sequential().await?,
  50. false => self.build_concurrent().await?,
  51. };
  52. AppBundle::new(self, app, server).await
  53. }
  54. /// Run the build command with a pretty loader, returning the executable output location
  55. async fn build_concurrent(&self) -> Result<(BuildArtifacts, Option<BuildArtifacts>)> {
  56. let (app, server) =
  57. futures_util::future::try_join(self.build_app(), self.build_server()).await?;
  58. Ok((app, server))
  59. }
  60. async fn build_sequential(&self) -> Result<(BuildArtifacts, Option<BuildArtifacts>)> {
  61. let app = self.build_app().await?;
  62. let server = self.build_server().await?;
  63. Ok((app, server))
  64. }
  65. pub(crate) async fn build_app(&self) -> Result<BuildArtifacts> {
  66. tracing::debug!("Building app...");
  67. let start = Instant::now();
  68. self.prepare_build_dir()?;
  69. let exe = self.build_cargo().await?;
  70. let assets = self.collect_assets(&exe).await?;
  71. Ok(BuildArtifacts {
  72. exe,
  73. assets,
  74. time_taken: start.elapsed(),
  75. })
  76. }
  77. pub(crate) async fn build_server(&self) -> Result<Option<BuildArtifacts>> {
  78. tracing::debug!("Building server...");
  79. if !self.build.fullstack {
  80. return Ok(None);
  81. }
  82. let mut cloned = self.clone();
  83. cloned.build.platform = Some(Platform::Server);
  84. Ok(Some(cloned.build_app().await?))
  85. }
  86. /// Run `cargo`, returning the location of the final executable
  87. ///
  88. /// todo: add some stats here, like timing reports, crate-graph optimizations, etc
  89. pub(crate) async fn build_cargo(&self) -> Result<PathBuf> {
  90. tracing::debug!("Executing cargo...");
  91. // Extract the unit count of the crate graph so build_cargo has more accurate data
  92. let crate_count = self.get_unit_count_estimate().await;
  93. // Update the status to show that we're starting the build and how many crates we expect to build
  94. self.status_starting_build(crate_count);
  95. let mut cmd = Command::new("cargo");
  96. cmd.arg("rustc")
  97. .current_dir(self.krate.crate_dir())
  98. .arg("--message-format")
  99. .arg("json-diagnostic-rendered-ansi")
  100. .args(self.build_arguments())
  101. .envs(self.env_vars()?);
  102. if let Some(target_dir) = self.custom_target_dir.as_ref() {
  103. cmd.env("CARGO_TARGET_DIR", target_dir);
  104. }
  105. // Android needs a special linker since the linker is actually tied to the android toolchain.
  106. // For the sake of simplicity, we're going to pass the linker here using ourselves as the linker,
  107. // but in reality we could simply use the android toolchain's linker as the path.
  108. //
  109. // We don't want to overwrite the user's .cargo/config.toml since that gets committed to git
  110. // and we want everyone's install to be the same.
  111. if self.build.platform() == Platform::Android {
  112. let ndk = self
  113. .krate
  114. .android_ndk()
  115. .context("Could not autodetect android linker")?;
  116. let arch = self.build.target_args.arch();
  117. let linker = arch.android_linker(&ndk);
  118. let link_action = LinkAction::LinkAndroid {
  119. linker,
  120. extra_flags: vec![],
  121. }
  122. .to_json();
  123. cmd.env(LinkAction::ENV_VAR_NAME, link_action);
  124. }
  125. tracing::trace!(dx_src = ?TraceSrc::Build, "Rust cargo args: {:#?}", cmd);
  126. let mut child = cmd
  127. .stdout(Stdio::piped())
  128. .stderr(Stdio::piped())
  129. .spawn()
  130. .context("Failed to spawn cargo build")?;
  131. let stdout = tokio::io::BufReader::new(child.stdout.take().unwrap());
  132. let stderr = tokio::io::BufReader::new(child.stderr.take().unwrap());
  133. let mut output_location = None;
  134. let mut stdout = stdout.lines();
  135. let mut stderr = stderr.lines();
  136. let mut units_compiled = 0;
  137. let mut emitting_error = false;
  138. loop {
  139. use cargo_metadata::Message;
  140. let line = tokio::select! {
  141. Ok(Some(line)) = stdout.next_line() => line,
  142. Ok(Some(line)) = stderr.next_line() => line,
  143. else => break,
  144. };
  145. let Some(Ok(message)) = Message::parse_stream(std::io::Cursor::new(line)).next() else {
  146. continue;
  147. };
  148. match message {
  149. Message::BuildScriptExecuted(_) => units_compiled += 1,
  150. Message::TextLine(line) => {
  151. // For whatever reason, if there's an error while building, we still receive the TextLine
  152. // instead of an "error" message. However, the following messages *also* tend to
  153. // be the error message, and don't start with "error:". So we'll check if we've already
  154. // emitted an error message and if so, we'll emit all following messages as errors too.
  155. if line.trim_start().starts_with("error:") {
  156. emitting_error = true;
  157. }
  158. if emitting_error {
  159. self.status_build_error(line);
  160. } else {
  161. self.status_build_message(line)
  162. }
  163. }
  164. Message::CompilerMessage(msg) => self.status_build_diagnostic(msg),
  165. Message::CompilerArtifact(artifact) => {
  166. units_compiled += 1;
  167. match artifact.executable {
  168. Some(executable) => output_location = Some(executable.into()),
  169. None => self.status_build_progress(
  170. units_compiled,
  171. crate_count,
  172. artifact.target.name,
  173. ),
  174. }
  175. }
  176. Message::BuildFinished(finished) => {
  177. if !finished.success {
  178. return Err(anyhow::anyhow!(
  179. "Cargo build failed, signaled by the compiler"
  180. )
  181. .into());
  182. }
  183. }
  184. _ => {}
  185. }
  186. }
  187. if output_location.is_none() {
  188. tracing::error!("Cargo build failed - no output location");
  189. }
  190. let out_location = output_location.context("Build did not return an executable")?;
  191. tracing::debug!(
  192. "Build completed successfully - output location: {:?}",
  193. out_location
  194. );
  195. Ok(out_location)
  196. }
  197. /// Traverse the target directory and collect all assets from the incremental cache
  198. ///
  199. /// This uses "known paths" that have stayed relatively stable during cargo's lifetime.
  200. /// One day this system might break and we might need to go back to using the linker approach.
  201. pub(crate) async fn collect_assets(&self, exe: &Path) -> Result<AssetManifest> {
  202. tracing::debug!("Collecting assets ...");
  203. if self.build.skip_assets {
  204. return Ok(AssetManifest::default());
  205. }
  206. // Experimental feature for testing - if the env var is set, we'll use the deeplinker
  207. if std::env::var("DEEPLINK").is_ok() {
  208. tracing::debug!("Using deeplinker instead of incremental cache");
  209. return self.deep_linker_asset_extract().await;
  210. }
  211. // walk every file in the incremental cache dir, reading and inserting items into the manifest.
  212. let mut manifest = AssetManifest::default();
  213. // And then add from the exe directly, just in case it's LTO compiled and has no incremental cache
  214. _ = manifest.add_from_object_path(exe);
  215. Ok(manifest)
  216. }
  217. /// Create a list of arguments for cargo builds
  218. pub(crate) fn build_arguments(&self) -> Vec<String> {
  219. let mut cargo_args = Vec::new();
  220. // Set the target, profile and features that vary between the app and server builds
  221. if self.build.platform() == Platform::Server {
  222. cargo_args.push("--profile".to_string());
  223. match self.build.release {
  224. true => cargo_args.push("release".to_string()),
  225. false => cargo_args.push(self.build.server_profile.to_string()),
  226. };
  227. } else {
  228. // Add required profile flags. --release overrides any custom profiles.
  229. let custom_profile = &self.build.profile.as_ref();
  230. if custom_profile.is_some() || self.build.release {
  231. cargo_args.push("--profile".to_string());
  232. match self.build.release {
  233. true => cargo_args.push("release".to_string()),
  234. false => {
  235. cargo_args.push(
  236. custom_profile
  237. .expect("custom_profile should have been checked by is_some")
  238. .to_string(),
  239. );
  240. }
  241. };
  242. }
  243. // todo: use the right arch based on the current arch
  244. let custom_target = match self.build.platform() {
  245. Platform::Web => Some("wasm32-unknown-unknown"),
  246. Platform::Ios => match self.build.target_args.device {
  247. Some(true) => Some("aarch64-apple-ios"),
  248. _ => Some("aarch64-apple-ios-sim"),
  249. },
  250. Platform::Android => Some(self.build.target_args.arch().android_target_triplet()),
  251. Platform::Server => None,
  252. // we're assuming we're building for the native platform for now... if you're cross-compiling
  253. // the targets here might be different
  254. Platform::MacOS => None,
  255. Platform::Windows => None,
  256. Platform::Linux => None,
  257. Platform::Liveview => None,
  258. };
  259. if let Some(target) = custom_target.or(self.build.target_args.target.as_deref()) {
  260. cargo_args.push("--target".to_string());
  261. cargo_args.push(target.to_string());
  262. }
  263. }
  264. // We always run in verbose since the CLI itself is the one doing the presentation
  265. cargo_args.push("--verbose".to_string());
  266. if self.build.target_args.no_default_features {
  267. cargo_args.push("--no-default-features".to_string());
  268. }
  269. let features = self.target_features();
  270. if !features.is_empty() {
  271. cargo_args.push("--features".to_string());
  272. cargo_args.push(features.join(" "));
  273. }
  274. if let Some(ref package) = self.build.target_args.package {
  275. cargo_args.push(String::from("-p"));
  276. cargo_args.push(package.clone());
  277. }
  278. cargo_args.append(&mut self.build.cargo_args.clone());
  279. match self.krate.executable_type() {
  280. krates::cm::TargetKind::Bin => cargo_args.push("--bin".to_string()),
  281. krates::cm::TargetKind::Lib => cargo_args.push("--lib".to_string()),
  282. krates::cm::TargetKind::Example => cargo_args.push("--example".to_string()),
  283. _ => {}
  284. };
  285. cargo_args.push(self.krate.executable_name().to_string());
  286. tracing::debug!(dx_src = ?TraceSrc::Build, "cargo args: {:?}", cargo_args);
  287. cargo_args
  288. }
  289. #[allow(dead_code)]
  290. pub(crate) fn android_rust_flags(&self) -> String {
  291. let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_default();
  292. // todo(jon): maybe we can make the symbol aliasing logic here instead of using llvm-objcopy
  293. if self.build.platform() == Platform::Android {
  294. let cur_exe = std::env::current_exe().unwrap();
  295. rust_flags.push_str(format!(" -Clinker={}", cur_exe.display()).as_str());
  296. rust_flags.push_str(" -Clink-arg=-landroid");
  297. rust_flags.push_str(" -Clink-arg=-llog");
  298. rust_flags.push_str(" -Clink-arg=-lOpenSLES");
  299. rust_flags.push_str(" -Clink-arg=-Wl,--export-dynamic");
  300. }
  301. rust_flags
  302. }
  303. /// Create the list of features we need to pass to cargo to build the app by merging together
  304. /// either the client or server features depending on if we're building a server or not.
  305. pub(crate) fn target_features(&self) -> Vec<String> {
  306. let mut features = self.build.target_args.features.clone();
  307. if self.build.platform() == Platform::Server {
  308. features.extend(self.build.target_args.server_features.clone());
  309. } else {
  310. features.extend(self.build.target_args.client_features.clone());
  311. }
  312. features
  313. }
  314. pub(crate) fn all_target_features(&self) -> Vec<String> {
  315. let mut features = self.target_features();
  316. if !self.build.target_args.no_default_features {
  317. features.extend(
  318. self.krate
  319. .package()
  320. .features
  321. .get("default")
  322. .cloned()
  323. .unwrap_or_default(),
  324. );
  325. }
  326. features.dedup();
  327. features
  328. }
  329. /// Try to get the unit graph for the crate. This is a nightly only feature which may not be available with the current version of rustc the user has installed.
  330. pub(crate) async fn get_unit_count(&self) -> crate::Result<usize> {
  331. #[derive(Debug, Deserialize)]
  332. struct UnitGraph {
  333. units: Vec<serde_json::Value>,
  334. }
  335. let output = tokio::process::Command::new("cargo")
  336. .arg("+nightly")
  337. .arg("build")
  338. .arg("--unit-graph")
  339. .arg("-Z")
  340. .arg("unstable-options")
  341. .args(self.build_arguments())
  342. .envs(self.env_vars()?)
  343. .stdout(Stdio::piped())
  344. .stderr(Stdio::piped())
  345. .output()
  346. .await?;
  347. if !output.status.success() {
  348. return Err(anyhow::anyhow!("Failed to get unit count").into());
  349. }
  350. let output_text = String::from_utf8(output.stdout).context("Failed to get unit count")?;
  351. let graph: UnitGraph =
  352. serde_json::from_str(&output_text).context("Failed to get unit count")?;
  353. Ok(graph.units.len())
  354. }
  355. /// Get an estimate of the number of units in the crate. If nightly rustc is not available, this will return an estimate of the number of units in the crate based on cargo metadata.
  356. /// TODO: always use https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#unit-graph once it is stable
  357. pub(crate) async fn get_unit_count_estimate(&self) -> usize {
  358. // Try to get it from nightly
  359. self.get_unit_count().await.unwrap_or_else(|_| {
  360. // Otherwise, use cargo metadata
  361. (self
  362. .krate
  363. .krates
  364. .krates_filtered(krates::DepKind::Dev)
  365. .iter()
  366. .map(|k| k.targets.len())
  367. .sum::<usize>() as f64
  368. / 3.5) as usize
  369. })
  370. }
  371. /// We used to require traversing incremental artifacts for assets that were included but not
  372. /// directly exposed to the final binary. Now, however, we force APIs to carry items created
  373. /// from asset calls into top-level items such that they *do* get included in the final binary.
  374. ///
  375. /// There's a chance that's not actually true, so this function is kept around in case we do
  376. /// need to revert to "deep extraction".
  377. #[allow(unused)]
  378. async fn deep_linker_asset_extract(&self) -> Result<AssetManifest> {
  379. // Create a temp file to put the output of the args
  380. // We need to do this since rustc won't actually print the link args to stdout, so we need to
  381. // give `dx` a file to dump its env::args into
  382. let tmp_file = tempfile::NamedTempFile::new()?;
  383. // Run `cargo rustc` again, but this time with a custom linker (dx) and an env var to force
  384. // `dx` to act as a linker
  385. //
  386. // This will force `dx` to look through the incremental cache and find the assets from the previous build
  387. Command::new("cargo")
  388. .arg("rustc")
  389. .args(self.build_arguments())
  390. .envs(self.env_vars()?)
  391. .arg("--offline") /* don't use the network, should already be resolved */
  392. .arg("--")
  393. .arg(format!(
  394. "-Clinker={}",
  395. std::env::current_exe()
  396. .unwrap()
  397. .canonicalize()
  398. .unwrap()
  399. .display()
  400. ))
  401. .env(
  402. LinkAction::ENV_VAR_NAME,
  403. LinkAction::BuildAssetManifest {
  404. destination: tmp_file.path().to_path_buf().clone(),
  405. }
  406. .to_json(),
  407. )
  408. .stdout(Stdio::piped())
  409. .stderr(Stdio::piped())
  410. .output()
  411. .await?;
  412. // The linker wrote the manifest to the temp file, let's load it!
  413. let manifest = AssetManifest::load_from_file(tmp_file.path())?;
  414. if let Ok(path) = std::env::var("DEEPLINK").map(|s| s.parse::<PathBuf>().unwrap()) {
  415. _ = tmp_file.persist(path);
  416. }
  417. Ok(manifest)
  418. }
  419. fn env_vars(&self) -> Result<Vec<(&str, String)>> {
  420. let mut env_vars = vec![];
  421. if self.build.platform() == Platform::Android {
  422. let ndk = self
  423. .krate
  424. .android_ndk()
  425. .context("Could not autodetect android linker")?;
  426. let arch = self.build.target_args.arch();
  427. let linker = arch.android_linker(&ndk);
  428. let min_sdk_version = arch.android_min_sdk_version();
  429. let ar_path = arch.android_ar_path(&ndk);
  430. let target_cc = arch.target_cc(&ndk);
  431. let target_cxx = arch.target_cxx(&ndk);
  432. let java_home = arch.java_home();
  433. tracing::debug!(
  434. r#"Using android:
  435. min_sdk_version: {min_sdk_version}
  436. linker: {linker:?}
  437. ar_path: {ar_path:?}
  438. target_cc: {target_cc:?}
  439. target_cxx: {target_cxx:?}
  440. java_home: {java_home:?}
  441. "#
  442. );
  443. env_vars.push(("ANDROID_NATIVE_API_LEVEL", min_sdk_version.to_string()));
  444. env_vars.push(("TARGET_AR", ar_path.display().to_string()));
  445. env_vars.push(("TARGET_CC", target_cc.display().to_string()));
  446. env_vars.push(("TARGET_CXX", target_cxx.display().to_string()));
  447. env_vars.push(("ANDROID_NDK_ROOT", ndk.display().to_string()));
  448. // attempt to set java_home to the android studio java home if it exists.
  449. // https://stackoverflow.com/questions/71381050/java-home-is-set-to-an-invalid-directory-android-studio-flutter
  450. // attempt to set java_home to the android studio java home if it exists and java_home was not already set
  451. if let Some(java_home) = java_home {
  452. tracing::debug!("Setting JAVA_HOME to {java_home:?}");
  453. env_vars.push(("JAVA_HOME", java_home.display().to_string()));
  454. }
  455. env_vars.push(("WRY_ANDROID_PACKAGE", "dev.dioxus.main".to_string()));
  456. env_vars.push(("WRY_ANDROID_LIBRARY", "dioxusmain".to_string()));
  457. env_vars.push((
  458. "WRY_ANDROID_KOTLIN_FILES_OUT_DIR",
  459. self.wry_android_kotlin_files_out_dir()
  460. .display()
  461. .to_string(),
  462. ));
  463. env_vars.push(("RUSTFLAGS", self.android_rust_flags()))
  464. // todo(jon): the guide for openssl recommends extending the path to include the tools dir
  465. // in practice I couldn't get this to work, but this might eventually become useful.
  466. //
  467. // https://github.com/openssl/openssl/blob/master/NOTES-ANDROID.md#configuration
  468. //
  469. // They recommend a configuration like this:
  470. //
  471. // // export ANDROID_NDK_ROOT=/home/whoever/Android/android-sdk/ndk/20.0.5594570
  472. // PATH=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$ANDROID_NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin:$PATH
  473. // ./Configure android-arm64 -D__ANDROID_API__=29
  474. // make
  475. //
  476. // let tools_dir = arch.android_tools_dir(&ndk);
  477. // let extended_path = format!(
  478. // "{}:{}",
  479. // tools_dir.display(),
  480. // std::env::var("PATH").unwrap_or_default()
  481. // );
  482. // env_vars.push(("PATH", extended_path));
  483. };
  484. // If this is a release build, bake the base path and title
  485. // into the binary with env vars
  486. if self.build.release {
  487. if let Some(base_path) = &self.krate.config.web.app.base_path {
  488. env_vars.push((ASSET_ROOT_ENV, base_path.clone()));
  489. }
  490. env_vars.push((APP_TITLE_ENV, self.krate.config.web.app.title.clone()));
  491. }
  492. Ok(env_vars)
  493. }
  494. /// We only really currently care about:
  495. ///
  496. /// - app dir (.app, .exe, .apk, etc)
  497. /// - assets dir
  498. /// - exe dir (.exe, .app, .apk, etc)
  499. /// - extra scaffolding
  500. ///
  501. /// It's not guaranteed that they're different from any other folder
  502. fn prepare_build_dir(&self) -> Result<()> {
  503. use once_cell::sync::OnceCell;
  504. use std::fs::{create_dir_all, remove_dir_all};
  505. static INITIALIZED: OnceCell<Result<()>> = OnceCell::new();
  506. let success = INITIALIZED.get_or_init(|| {
  507. _ = remove_dir_all(self.exe_dir());
  508. create_dir_all(self.root_dir())?;
  509. create_dir_all(self.exe_dir())?;
  510. create_dir_all(self.asset_dir())?;
  511. tracing::debug!("Initialized Root dir: {:?}", self.root_dir());
  512. tracing::debug!("Initialized Exe dir: {:?}", self.exe_dir());
  513. tracing::debug!("Initialized Asset dir: {:?}", self.asset_dir());
  514. // we could download the templates from somewhere (github?) but after having banged my head against
  515. // cargo-mobile2 for ages, I give up with that. We're literally just going to hardcode the templates
  516. // by writing them here.
  517. if let Platform::Android = self.build.platform() {
  518. self.build_android_app_dir()?;
  519. }
  520. Ok(())
  521. });
  522. if let Err(e) = success.as_ref() {
  523. return Err(format!("Failed to initialize build directory: {e}").into());
  524. }
  525. Ok(())
  526. }
  527. /// The directory in which we'll put the main exe
  528. ///
  529. /// Mac, Android, Web are a little weird
  530. /// - mac wants to be in Contents/MacOS
  531. /// - android wants to be in jniLibs/arm64-v8a (or others, depending on the platform / architecture)
  532. /// - web wants to be in wasm (which... we don't really need to, we could just drop the wasm into public and it would work)
  533. ///
  534. /// I think all others are just in the root folder
  535. ///
  536. /// todo(jon): investigate if we need to put .wasm in `wasm`. It kinda leaks implementation details, which ideally we don't want to do.
  537. pub fn exe_dir(&self) -> PathBuf {
  538. match self.build.platform() {
  539. Platform::MacOS => self.root_dir().join("Contents").join("MacOS"),
  540. Platform::Web => self.root_dir().join("wasm"),
  541. // Android has a whole build structure to it
  542. Platform::Android => self
  543. .root_dir()
  544. .join("app")
  545. .join("src")
  546. .join("main")
  547. .join("jniLibs")
  548. .join(self.build.target_args.arch().android_jnilib()),
  549. // these are all the same, I think?
  550. Platform::Windows
  551. | Platform::Linux
  552. | Platform::Ios
  553. | Platform::Server
  554. | Platform::Liveview => self.root_dir(),
  555. }
  556. }
  557. /// returns the path to root build folder. This will be our working directory for the build.
  558. ///
  559. /// we only add an extension to the folders where it sorta matters that it's named with the extension.
  560. /// for example, on mac, the `.app` indicates we can `open` it and it pulls in icons, dylibs, etc.
  561. ///
  562. /// for our simulator-based platforms, this is less important since they need to be zipped up anyways
  563. /// to run in the simulator.
  564. ///
  565. /// For windows/linux, it's also not important since we're just running the exe directly out of the folder
  566. ///
  567. /// The idea of this folder is that we can run our top-level build command against it and we'll get
  568. /// a final build output somewhere. Some platforms have basically no build command, and can simply
  569. /// be ran by executing the exe directly.
  570. pub(crate) fn root_dir(&self) -> PathBuf {
  571. let platform_dir = self.platform_dir();
  572. match self.build.platform() {
  573. Platform::Web => platform_dir.join("public"),
  574. Platform::Server => platform_dir.clone(), // ends up *next* to the public folder
  575. // These might not actually need to be called `.app` but it does let us run these with `open`
  576. Platform::MacOS => platform_dir.join(format!("{}.app", self.krate.bundled_app_name())),
  577. Platform::Ios => platform_dir.join(format!("{}.app", self.krate.bundled_app_name())),
  578. // in theory, these all could end up directly in the root dir
  579. Platform::Android => platform_dir.join("app"), // .apk (after bundling)
  580. Platform::Linux => platform_dir.join("app"), // .appimage (after bundling)
  581. Platform::Windows => platform_dir.join("app"), // .exe (after bundling)
  582. Platform::Liveview => platform_dir.join("app"), // .exe (after bundling)
  583. }
  584. }
  585. pub(crate) fn platform_dir(&self) -> PathBuf {
  586. self.krate
  587. .build_dir(self.build.platform(), self.build.release)
  588. }
  589. pub fn asset_dir(&self) -> PathBuf {
  590. match self.build.platform() {
  591. Platform::MacOS => self
  592. .root_dir()
  593. .join("Contents")
  594. .join("Resources")
  595. .join("assets"),
  596. Platform::Android => self
  597. .root_dir()
  598. .join("app")
  599. .join("src")
  600. .join("main")
  601. .join("assets"),
  602. // everyone else is soooo normal, just app/assets :)
  603. Platform::Web
  604. | Platform::Ios
  605. | Platform::Windows
  606. | Platform::Linux
  607. | Platform::Server
  608. | Platform::Liveview => self.root_dir().join("assets"),
  609. }
  610. }
  611. pub fn platform_exe_name(&self) -> String {
  612. match self.build.platform() {
  613. Platform::MacOS => self.krate.executable_name().to_string(),
  614. Platform::Ios => self.krate.executable_name().to_string(),
  615. Platform::Server => self.krate.executable_name().to_string(),
  616. Platform::Liveview => self.krate.executable_name().to_string(),
  617. Platform::Windows => format!("{}.exe", self.krate.executable_name()),
  618. // from the apk spec, the root exe is a shared library
  619. // we include the user's rust code as a shared library with a fixed namespacea
  620. Platform::Android => "libdioxusmain.so".to_string(),
  621. Platform::Web => unimplemented!("there's no main exe on web"), // this will be wrong, I think, but not important?
  622. // todo: maybe this should be called AppRun?
  623. Platform::Linux => self.krate.executable_name().to_string(),
  624. }
  625. }
  626. fn build_android_app_dir(&self) -> Result<()> {
  627. use std::fs::{create_dir_all, write};
  628. let root = self.root_dir();
  629. // gradle
  630. let wrapper = root.join("gradle").join("wrapper");
  631. create_dir_all(&wrapper)?;
  632. tracing::debug!("Initialized Gradle wrapper: {:?}", wrapper);
  633. // app
  634. let app = root.join("app");
  635. let app_main = app.join("src").join("main");
  636. let app_kotlin = app_main.join("kotlin");
  637. let app_jnilibs = app_main.join("jniLibs");
  638. let app_assets = app_main.join("assets");
  639. let app_kotlin_out = self.wry_android_kotlin_files_out_dir();
  640. create_dir_all(&app)?;
  641. create_dir_all(&app_main)?;
  642. create_dir_all(&app_kotlin)?;
  643. create_dir_all(&app_jnilibs)?;
  644. create_dir_all(&app_assets)?;
  645. create_dir_all(&app_kotlin_out)?;
  646. tracing::debug!("Initialized app: {:?}", app);
  647. tracing::debug!("Initialized app/src: {:?}", app_main);
  648. tracing::debug!("Initialized app/src/kotlin: {:?}", app_kotlin);
  649. tracing::debug!("Initialized app/src/jniLibs: {:?}", app_jnilibs);
  650. tracing::debug!("Initialized app/src/assets: {:?}", app_assets);
  651. tracing::debug!("Initialized app/src/kotlin/main: {:?}", app_kotlin_out);
  652. // handlerbars
  653. let hbs = handlebars::Handlebars::new();
  654. #[derive(serde::Serialize)]
  655. struct HbsTypes {
  656. application_id: String,
  657. app_name: String,
  658. }
  659. let hbs_data = HbsTypes {
  660. application_id: self.krate.full_mobile_app_name(),
  661. app_name: self.krate.bundled_app_name(),
  662. };
  663. // Top-level gradle config
  664. write(
  665. root.join("build.gradle.kts"),
  666. include_bytes!("../../assets/android/gen/build.gradle.kts"),
  667. )?;
  668. write(
  669. root.join("gradle.properties"),
  670. include_bytes!("../../assets/android/gen/gradle.properties"),
  671. )?;
  672. write(
  673. root.join("gradlew"),
  674. include_bytes!("../../assets/android/gen/gradlew"),
  675. )?;
  676. write(
  677. root.join("gradlew.bat"),
  678. include_bytes!("../../assets/android/gen/gradlew.bat"),
  679. )?;
  680. write(
  681. root.join("settings.gradle"),
  682. include_bytes!("../../assets/android/gen/settings.gradle"),
  683. )?;
  684. // Then the wrapper and its properties
  685. write(
  686. wrapper.join("gradle-wrapper.properties"),
  687. include_bytes!("../../assets/android/gen/gradle/wrapper/gradle-wrapper.properties"),
  688. )?;
  689. write(
  690. wrapper.join("gradle-wrapper.jar"),
  691. include_bytes!("../../assets/android/gen/gradle/wrapper/gradle-wrapper.jar"),
  692. )?;
  693. // Now the app directory
  694. write(
  695. app.join("build.gradle.kts"),
  696. hbs.render_template(
  697. include_str!("../../assets/android/gen/app/build.gradle.kts.hbs"),
  698. &hbs_data,
  699. )?,
  700. )?;
  701. write(
  702. app.join("proguard-rules.pro"),
  703. include_bytes!("../../assets/android/gen/app/proguard-rules.pro"),
  704. )?;
  705. write(
  706. app.join("src").join("main").join("AndroidManifest.xml"),
  707. hbs.render_template(
  708. include_str!("../../assets/android/gen/app/src/main/AndroidManifest.xml.hbs"),
  709. &hbs_data,
  710. )?,
  711. )?;
  712. // Write the main activity manually since tao dropped support for it
  713. write(
  714. self.wry_android_kotlin_files_out_dir()
  715. .join("MainActivity.kt"),
  716. hbs.render_template(
  717. include_str!("../../assets/android/MainActivity.kt.hbs"),
  718. &hbs_data,
  719. )?,
  720. )?;
  721. // Write the res folder
  722. let res = app_main.join("res");
  723. create_dir_all(&res)?;
  724. create_dir_all(res.join("values"))?;
  725. write(
  726. res.join("values").join("strings.xml"),
  727. hbs.render_template(
  728. include_str!("../../assets/android/gen/app/src/main/res/values/strings.xml.hbs"),
  729. &hbs_data,
  730. )?,
  731. )?;
  732. write(
  733. res.join("values").join("colors.xml"),
  734. include_bytes!("../../assets/android/gen/app/src/main/res/values/colors.xml"),
  735. )?;
  736. write(
  737. res.join("values").join("styles.xml"),
  738. include_bytes!("../../assets/android/gen/app/src/main/res/values/styles.xml"),
  739. )?;
  740. create_dir_all(res.join("drawable"))?;
  741. write(
  742. res.join("drawable").join("ic_launcher_background.xml"),
  743. include_bytes!(
  744. "../../assets/android/gen/app/src/main/res/drawable/ic_launcher_background.xml"
  745. ),
  746. )?;
  747. create_dir_all(res.join("drawable-v24"))?;
  748. write(
  749. res.join("drawable-v24").join("ic_launcher_foreground.xml"),
  750. include_bytes!(
  751. "../../assets/android/gen/app/src/main/res/drawable-v24/ic_launcher_foreground.xml"
  752. ),
  753. )?;
  754. create_dir_all(res.join("mipmap-anydpi-v26"))?;
  755. write(
  756. res.join("mipmap-anydpi-v26").join("ic_launcher.xml"),
  757. include_bytes!(
  758. "../../assets/android/gen/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml"
  759. ),
  760. )?;
  761. create_dir_all(res.join("mipmap-hdpi"))?;
  762. write(
  763. res.join("mipmap-hdpi").join("ic_launcher.webp"),
  764. include_bytes!(
  765. "../../assets/android/gen/app/src/main/res/mipmap-hdpi/ic_launcher.webp"
  766. ),
  767. )?;
  768. create_dir_all(res.join("mipmap-mdpi"))?;
  769. write(
  770. res.join("mipmap-mdpi").join("ic_launcher.webp"),
  771. include_bytes!(
  772. "../../assets/android/gen/app/src/main/res/mipmap-mdpi/ic_launcher.webp"
  773. ),
  774. )?;
  775. create_dir_all(res.join("mipmap-xhdpi"))?;
  776. write(
  777. res.join("mipmap-xhdpi").join("ic_launcher.webp"),
  778. include_bytes!(
  779. "../../assets/android/gen/app/src/main/res/mipmap-xhdpi/ic_launcher.webp"
  780. ),
  781. )?;
  782. create_dir_all(res.join("mipmap-xxhdpi"))?;
  783. write(
  784. res.join("mipmap-xxhdpi").join("ic_launcher.webp"),
  785. include_bytes!(
  786. "../../assets/android/gen/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp"
  787. ),
  788. )?;
  789. create_dir_all(res.join("mipmap-xxxhdpi"))?;
  790. write(
  791. res.join("mipmap-xxxhdpi").join("ic_launcher.webp"),
  792. include_bytes!(
  793. "../../assets/android/gen/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp"
  794. ),
  795. )?;
  796. Ok(())
  797. }
  798. pub(crate) fn wry_android_kotlin_files_out_dir(&self) -> PathBuf {
  799. let mut kotlin_dir = self
  800. .root_dir()
  801. .join("app")
  802. .join("src")
  803. .join("main")
  804. .join("kotlin");
  805. for segment in "dev.dioxus.main".split('.') {
  806. kotlin_dir = kotlin_dir.join(segment);
  807. }
  808. tracing::debug!("app_kotlin_out: {:?}", kotlin_dir);
  809. kotlin_dir
  810. }
  811. }