builder.rs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. use crate::{
  2. config::{CrateConfig, ExecutableType},
  3. error::{Error, Result},
  4. tools::Tool,
  5. DioxusConfig,
  6. };
  7. use cargo_metadata::{diagnostic::Diagnostic, Message};
  8. use indicatif::{ProgressBar, ProgressStyle};
  9. use serde::Serialize;
  10. use std::{
  11. fs::{copy, create_dir_all, File},
  12. io::Read,
  13. panic,
  14. path::PathBuf,
  15. time::Duration,
  16. };
  17. use wasm_bindgen_cli_support::Bindgen;
  18. #[derive(Serialize, Debug, Clone)]
  19. pub struct BuildResult {
  20. pub warnings: Vec<Diagnostic>,
  21. pub elapsed_time: u128,
  22. }
  23. #[allow(unused)]
  24. pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
  25. // [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
  26. // [2] Generate the appropriate build folders
  27. // [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
  28. // [4] Wasm-opt the .wasm file with whatever optimizations need to be done
  29. // [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary
  30. // [6] Link up the html page to the wasm module
  31. let CrateConfig {
  32. out_dir,
  33. crate_dir,
  34. target_dir,
  35. asset_dir,
  36. executable,
  37. dioxus_config,
  38. ..
  39. } = config;
  40. // start to build the assets
  41. let ignore_files = build_assets(config)?;
  42. let t_start = std::time::Instant::now();
  43. // [1] Build the .wasm module
  44. log::info!("🚅 Running build command...");
  45. let cmd = subprocess::Exec::cmd("cargo");
  46. let cmd = cmd
  47. .cwd(crate_dir)
  48. .arg("build")
  49. .arg("--target")
  50. .arg("wasm32-unknown-unknown")
  51. .arg("--message-format=json")
  52. .arg("--quiet");
  53. let cmd = if config.release {
  54. cmd.arg("--release")
  55. } else {
  56. cmd
  57. };
  58. let cmd = if config.verbose {
  59. cmd.arg("--verbose")
  60. } else {
  61. cmd
  62. };
  63. let cmd = if config.custom_profile.is_some() {
  64. let custom_profile = config.custom_profile.as_ref().unwrap();
  65. cmd.arg("--profile").arg(custom_profile)
  66. } else {
  67. cmd
  68. };
  69. let cmd = if config.features.is_some() {
  70. let features_str = config.features.as_ref().unwrap().join(" ");
  71. cmd.arg("--features").arg(features_str)
  72. } else {
  73. cmd
  74. };
  75. let cmd = match executable {
  76. ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
  77. ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
  78. ExecutableType::Example(name) => cmd.arg("--example").arg(name),
  79. };
  80. let warning_messages = prettier_build(cmd)?;
  81. // [2] Establish the output directory structure
  82. let bindgen_outdir = out_dir.join("assets").join("dioxus");
  83. let build_profile = if config.custom_profile.is_some() {
  84. config.custom_profile.as_ref().unwrap()
  85. } else if config.release {
  86. "release"
  87. } else {
  88. "debug"
  89. };
  90. let input_path = match executable {
  91. ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
  92. .join(format!("wasm32-unknown-unknown/{}", build_profile))
  93. .join(format!("{}.wasm", name)),
  94. ExecutableType::Example(name) => target_dir
  95. .join(format!("wasm32-unknown-unknown/{}/examples", build_profile))
  96. .join(format!("{}.wasm", name)),
  97. };
  98. let bindgen_result = panic::catch_unwind(move || {
  99. // [3] Bindgen the final binary for use easy linking
  100. let mut bindgen_builder = Bindgen::new();
  101. bindgen_builder
  102. .input_path(input_path)
  103. .web(true)
  104. .unwrap()
  105. .debug(true)
  106. .demangle(true)
  107. .keep_debug(true)
  108. .remove_name_section(false)
  109. .remove_producers_section(false)
  110. .out_name(&dioxus_config.application.name)
  111. .generate(&bindgen_outdir)
  112. .unwrap();
  113. });
  114. if bindgen_result.is_err() {
  115. return Err(Error::BuildFailed("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.81` Bindgen crate.".to_string()));
  116. }
  117. // check binaryen:wasm-opt tool
  118. let dioxus_tools = dioxus_config.application.tools.clone();
  119. if dioxus_tools.contains_key("binaryen") {
  120. let info = dioxus_tools.get("binaryen").unwrap();
  121. let binaryen = crate::tools::Tool::Binaryen;
  122. if binaryen.is_installed() {
  123. if let Some(sub) = info.as_table() {
  124. if sub.contains_key("wasm_opt")
  125. && sub.get("wasm_opt").unwrap().as_bool().unwrap_or(false)
  126. {
  127. log::info!("Optimizing WASM size with wasm-opt...");
  128. let target_file = out_dir
  129. .join("assets")
  130. .join("dioxus")
  131. .join(format!("{}_bg.wasm", dioxus_config.application.name));
  132. if target_file.is_file() {
  133. let mut args = vec![
  134. target_file.to_str().unwrap(),
  135. "-o",
  136. target_file.to_str().unwrap(),
  137. ];
  138. if config.release {
  139. args.push("-Oz");
  140. }
  141. binaryen.call("wasm-opt", args)?;
  142. }
  143. }
  144. }
  145. } else {
  146. log::warn!(
  147. "Binaryen tool not found, you can use `dx tool add binaryen` to install it."
  148. );
  149. }
  150. }
  151. // [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
  152. if dioxus_tools.contains_key("tailwindcss") {
  153. let info = dioxus_tools.get("tailwindcss").unwrap();
  154. let tailwind = crate::tools::Tool::Tailwind;
  155. if tailwind.is_installed() {
  156. if let Some(sub) = info.as_table() {
  157. log::info!("Building Tailwind bundle CSS file...");
  158. let input_path = match sub.get("input") {
  159. Some(val) => val.as_str().unwrap(),
  160. None => "./public",
  161. };
  162. let config_path = match sub.get("config") {
  163. Some(val) => val.as_str().unwrap(),
  164. None => "./src/tailwind.config.js",
  165. };
  166. let mut args = vec![
  167. "-i",
  168. input_path,
  169. "-o",
  170. "dist/tailwind.css",
  171. "-c",
  172. config_path,
  173. ];
  174. if config.release {
  175. args.push("--minify");
  176. }
  177. tailwind.call("tailwindcss", args)?;
  178. }
  179. } else {
  180. log::warn!(
  181. "Tailwind tool not found, you can use `dx tool add tailwindcss` to install it."
  182. );
  183. }
  184. }
  185. // this code will copy all public file to the output dir
  186. let copy_options = fs_extra::dir::CopyOptions {
  187. overwrite: true,
  188. skip_exist: false,
  189. buffer_size: 64000,
  190. copy_inside: false,
  191. content_only: false,
  192. depth: 0,
  193. };
  194. if asset_dir.is_dir() {
  195. for entry in std::fs::read_dir(asset_dir)? {
  196. let path = entry?.path();
  197. if path.is_file() {
  198. std::fs::copy(&path, out_dir.join(path.file_name().unwrap()))?;
  199. } else {
  200. match fs_extra::dir::copy(&path, out_dir, &copy_options) {
  201. Ok(_) => {}
  202. Err(_e) => {
  203. log::warn!("Error copying dir: {}", _e);
  204. }
  205. }
  206. for ignore in &ignore_files {
  207. let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
  208. let ignore = config.out_dir.join(ignore);
  209. if ignore.is_file() {
  210. std::fs::remove_file(ignore)?;
  211. }
  212. }
  213. }
  214. }
  215. }
  216. Ok(BuildResult {
  217. warnings: warning_messages,
  218. elapsed_time: t_start.elapsed().as_millis(),
  219. })
  220. }
  221. pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResult> {
  222. log::info!("🚅 Running build [Desktop] command...");
  223. let t_start = std::time::Instant::now();
  224. let ignore_files = build_assets(config)?;
  225. let mut cmd = subprocess::Exec::cmd("cargo")
  226. .cwd(&config.crate_dir)
  227. .arg("build")
  228. .arg("--quiet")
  229. .arg("--message-format=json");
  230. if config.release {
  231. cmd = cmd.arg("--release");
  232. }
  233. if config.verbose {
  234. cmd = cmd.arg("--verbose");
  235. }
  236. if config.custom_profile.is_some() {
  237. let custom_profile = config.custom_profile.as_ref().unwrap();
  238. cmd = cmd.arg("--profile").arg(custom_profile);
  239. }
  240. if config.features.is_some() {
  241. let features_str = config.features.as_ref().unwrap().join(" ");
  242. cmd = cmd.arg("--features").arg(features_str);
  243. }
  244. let cmd = match &config.executable {
  245. crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
  246. crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
  247. crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
  248. };
  249. let warning_messages = prettier_build(cmd)?;
  250. let release_type = match config.release {
  251. true => "release",
  252. false => "debug",
  253. };
  254. let file_name: String;
  255. let mut res_path = match &config.executable {
  256. crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
  257. file_name = name.clone();
  258. config.target_dir.join(release_type).join(name)
  259. }
  260. crate::ExecutableType::Example(name) => {
  261. file_name = name.clone();
  262. config
  263. .target_dir
  264. .join(release_type)
  265. .join("examples")
  266. .join(name)
  267. }
  268. };
  269. let target_file = if cfg!(windows) {
  270. res_path.set_extension("exe");
  271. format!("{}.exe", &file_name)
  272. } else {
  273. file_name
  274. };
  275. if !config.out_dir.is_dir() {
  276. create_dir_all(&config.out_dir)?;
  277. }
  278. copy(res_path, config.out_dir.join(target_file))?;
  279. // this code will copy all public file to the output dir
  280. if config.asset_dir.is_dir() {
  281. let copy_options = fs_extra::dir::CopyOptions {
  282. overwrite: true,
  283. skip_exist: false,
  284. buffer_size: 64000,
  285. copy_inside: false,
  286. content_only: false,
  287. depth: 0,
  288. };
  289. for entry in std::fs::read_dir(&config.asset_dir)? {
  290. let path = entry?.path();
  291. if path.is_file() {
  292. std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
  293. } else {
  294. match fs_extra::dir::copy(&path, &config.out_dir, &copy_options) {
  295. Ok(_) => {}
  296. Err(e) => {
  297. log::warn!("Error copying dir: {}", e);
  298. }
  299. }
  300. for ignore in &ignore_files {
  301. let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
  302. let ignore = config.out_dir.join(ignore);
  303. if ignore.is_file() {
  304. std::fs::remove_file(ignore)?;
  305. }
  306. }
  307. }
  308. }
  309. }
  310. log::info!(
  311. "🚩 Build completed: [./{}]",
  312. config.dioxus_config.application.out_dir.clone().display()
  313. );
  314. println!("build desktop done");
  315. Ok(BuildResult {
  316. warnings: warning_messages,
  317. elapsed_time: t_start.elapsed().as_millis(),
  318. })
  319. }
  320. fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
  321. let mut warning_messages: Vec<Diagnostic> = vec![];
  322. let pb = ProgressBar::new_spinner();
  323. pb.enable_steady_tick(Duration::from_millis(200));
  324. pb.set_style(
  325. ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}")
  326. .unwrap()
  327. .tick_chars("/|\\- "),
  328. );
  329. pb.set_message("💼 Waiting to start build the project...");
  330. struct StopSpinOnDrop(ProgressBar);
  331. impl Drop for StopSpinOnDrop {
  332. fn drop(&mut self) {
  333. self.0.finish_and_clear();
  334. }
  335. }
  336. let stdout = cmd.detached().stream_stdout()?;
  337. let reader = std::io::BufReader::new(stdout);
  338. for message in cargo_metadata::Message::parse_stream(reader) {
  339. match message.unwrap() {
  340. Message::CompilerMessage(msg) => {
  341. let message = msg.message;
  342. match message.level {
  343. cargo_metadata::diagnostic::DiagnosticLevel::Error => {
  344. return {
  345. Err(anyhow::anyhow!(message
  346. .rendered
  347. .unwrap_or("Unknown".into())))
  348. };
  349. }
  350. cargo_metadata::diagnostic::DiagnosticLevel::Warning => {
  351. warning_messages.push(message.clone());
  352. }
  353. _ => {}
  354. }
  355. }
  356. Message::CompilerArtifact(artifact) => {
  357. pb.set_message(format!("⚙️ Compiling {} ", artifact.package_id));
  358. pb.tick();
  359. }
  360. Message::BuildScriptExecuted(script) => {
  361. let _package_id = script.package_id.to_string();
  362. }
  363. Message::BuildFinished(finished) => {
  364. if finished.success {
  365. log::info!("👑 Build done.");
  366. } else {
  367. std::process::exit(1);
  368. }
  369. }
  370. _ => (), // Unknown message
  371. }
  372. }
  373. Ok(warning_messages)
  374. }
  375. pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
  376. let crate_root = crate::cargo::crate_root().unwrap();
  377. let custom_html_file = crate_root.join("index.html");
  378. let mut html = if custom_html_file.is_file() {
  379. let mut buf = String::new();
  380. let mut file = File::open(custom_html_file).unwrap();
  381. if file.read_to_string(&mut buf).is_ok() {
  382. buf
  383. } else {
  384. String::from(include_str!("./assets/index.html"))
  385. }
  386. } else {
  387. String::from(include_str!("./assets/index.html"))
  388. };
  389. let resources = config.web.resource.clone();
  390. let mut style_list = resources.style.unwrap_or_default();
  391. let mut script_list = resources.script.unwrap_or_default();
  392. if serve {
  393. let mut dev_style = resouces.dev.style.clone();
  394. let mut dev_script = resouces.dev.script.clone();
  395. style_list.append(&mut dev_style);
  396. script_list.append(&mut dev_script);
  397. }
  398. let mut style_str = String::new();
  399. for style in style_list {
  400. style_str.push_str(&format!(
  401. "<link rel=\"stylesheet\" href=\"{}\">\n",
  402. &style.to_str().unwrap(),
  403. ))
  404. }
  405. if config.application.tools.clone().contains_key("tailwindcss") {
  406. style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n");
  407. }
  408. replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
  409. let mut script_str = String::new();
  410. for script in script_list {
  411. script_str.push_str(&format!(
  412. "<script src=\"{}\"></script>\n",
  413. &script.to_str().unwrap(),
  414. ))
  415. }
  416. replace_or_insert_before("{script_include}", &script_str, "</body", &mut html);
  417. if serve {
  418. html += &format!(
  419. "<script>{}</script>",
  420. include_str!("./assets/autoreload.js")
  421. );
  422. }
  423. let base_path = match &config.web.app.base_path {
  424. Some(path) => path,
  425. None => ".",
  426. };
  427. let app_name = &config.application.name;
  428. // Check if a script already exists
  429. if html.contains("{app_name}") && html.contains("{base_path}") {
  430. html = html.replace("{app_name}", app_name);
  431. html = html.replace("{base_path}", base_path);
  432. } else {
  433. // If not, insert the script
  434. html = html.replace(
  435. "</body",
  436. &format!(
  437. r#"<script type="module">
  438. import init from "/{base_path}/assets/dioxus/{app_name}.js";
  439. init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {{
  440. if (wasm.__wbindgen_start == undefined) {{
  441. wasm.main();
  442. }}
  443. }});
  444. </script>
  445. </body"#
  446. ),
  447. );
  448. }
  449. let title = config.web.app.title.clone();
  450. replace_or_insert_before("{app_title}", &title, "</title", &mut html);
  451. html
  452. }
  453. fn replace_or_insert_before(
  454. replace: &str,
  455. with: &str,
  456. or_insert_before: &str,
  457. content: &mut String,
  458. ) {
  459. if content.contains(replace) {
  460. *content = content.replace(replace, with);
  461. } else {
  462. *content = content.replace(or_insert_before, &format!("{}{}", with, or_insert_before));
  463. }
  464. }
  465. // this function will build some assets file
  466. // like sass tool resources
  467. // this function will return a array which file don't need copy to out_dir.
  468. fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
  469. let mut result = vec![];
  470. let dioxus_config = &config.dioxus_config;
  471. let dioxus_tools = dioxus_config.application.tools.clone();
  472. // check sass tool state
  473. let sass = Tool::Sass;
  474. if sass.is_installed() && dioxus_tools.contains_key("sass") {
  475. let sass_conf = dioxus_tools.get("sass").unwrap();
  476. if let Some(tab) = sass_conf.as_table() {
  477. let source_map = tab.contains_key("source_map");
  478. let source_map = if source_map && tab.get("source_map").unwrap().is_bool() {
  479. if tab.get("source_map").unwrap().as_bool().unwrap_or_default() {
  480. "--source-map"
  481. } else {
  482. "--no-source-map"
  483. }
  484. } else {
  485. "--source-map"
  486. };
  487. if tab.contains_key("input") {
  488. if tab.get("input").unwrap().is_str() {
  489. let file = tab.get("input").unwrap().as_str().unwrap().trim();
  490. if file == "*" {
  491. // if the sass open auto, we need auto-check the assets dir.
  492. let asset_dir = config.asset_dir.clone();
  493. if asset_dir.is_dir() {
  494. for entry in walkdir::WalkDir::new(&asset_dir)
  495. .into_iter()
  496. .filter_map(|e| e.ok())
  497. {
  498. let temp = entry.path();
  499. if temp.is_file() {
  500. let suffix = temp.extension();
  501. if suffix.is_none() {
  502. continue;
  503. }
  504. let suffix = suffix.unwrap().to_str().unwrap();
  505. if suffix == "scss" || suffix == "sass" {
  506. // if file suffix is `scss` / `sass` we need transform it.
  507. let out_file = format!(
  508. "{}.css",
  509. temp.file_stem().unwrap().to_str().unwrap()
  510. );
  511. let target_path = config
  512. .out_dir
  513. .join(
  514. temp.strip_prefix(&asset_dir)
  515. .unwrap()
  516. .parent()
  517. .unwrap(),
  518. )
  519. .join(out_file);
  520. let res = sass.call(
  521. "sass",
  522. vec![
  523. temp.to_str().unwrap(),
  524. target_path.to_str().unwrap(),
  525. source_map,
  526. ],
  527. );
  528. if res.is_ok() {
  529. result.push(temp.to_path_buf());
  530. }
  531. }
  532. }
  533. }
  534. }
  535. } else {
  536. // just transform one file.
  537. let relative_path = if &file[0..1] == "/" {
  538. &file[1..file.len()]
  539. } else {
  540. file
  541. };
  542. let path = config.asset_dir.join(relative_path);
  543. let out_file =
  544. format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
  545. let target_path = config
  546. .out_dir
  547. .join(PathBuf::from(relative_path).parent().unwrap())
  548. .join(out_file);
  549. if path.is_file() {
  550. let res = sass.call(
  551. "sass",
  552. vec![
  553. path.to_str().unwrap(),
  554. target_path.to_str().unwrap(),
  555. source_map,
  556. ],
  557. );
  558. if res.is_ok() {
  559. result.push(path);
  560. } else {
  561. log::error!("{:?}", res);
  562. }
  563. }
  564. }
  565. } else if tab.get("input").unwrap().is_array() {
  566. // check files list.
  567. let list = tab.get("input").unwrap().as_array().unwrap();
  568. for i in list {
  569. if i.is_str() {
  570. let path = i.as_str().unwrap();
  571. let relative_path = if &path[0..1] == "/" {
  572. &path[1..path.len()]
  573. } else {
  574. path
  575. };
  576. let path = config.asset_dir.join(relative_path);
  577. let out_file =
  578. format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
  579. let target_path = config
  580. .out_dir
  581. .join(PathBuf::from(relative_path).parent().unwrap())
  582. .join(out_file);
  583. if path.is_file() {
  584. let res = sass.call(
  585. "sass",
  586. vec![
  587. path.to_str().unwrap(),
  588. target_path.to_str().unwrap(),
  589. source_map,
  590. ],
  591. );
  592. if res.is_ok() {
  593. result.push(path);
  594. }
  595. }
  596. }
  597. }
  598. }
  599. }
  600. }
  601. }
  602. // SASS END
  603. Ok(result)
  604. }