init.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use super::{get_app, Target};
  5. use crate::{
  6. helpers::{config::get as get_tauri_config, template::JsonMap},
  7. interface::{AppInterface, Interface},
  8. Result,
  9. };
  10. use cargo_mobile2::{
  11. android::{
  12. config::Config as AndroidConfig, env::Env as AndroidEnv, target::Target as AndroidTarget,
  13. },
  14. config::app::App,
  15. dot_cargo,
  16. reserved_names::KOTLIN_ONLY_KEYWORDS,
  17. target::TargetTrait as _,
  18. util::{
  19. self,
  20. cli::{Report, TextWrapper},
  21. },
  22. };
  23. use handlebars::{
  24. Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, RenderErrorReason,
  25. };
  26. use serde::Serialize;
  27. use std::{env::var_os, path::PathBuf};
  28. pub fn command(
  29. target: Target,
  30. ci: bool,
  31. reinstall_deps: bool,
  32. skip_targets_install: bool,
  33. ) -> Result<()> {
  34. let wrapper = TextWrapper::default();
  35. let tauri_init_config = TauriInitConfig {
  36. #[cfg(target_os = "macos")]
  37. ios: {
  38. let (keychain, provisioning_profile) = super::ios::signing_from_env()?;
  39. super::ios::init_config(keychain.as_ref(), provisioning_profile.as_ref())?
  40. },
  41. };
  42. exec(
  43. target,
  44. &wrapper,
  45. &tauri_init_config,
  46. ci,
  47. reinstall_deps,
  48. skip_targets_install,
  49. )
  50. .map_err(|e| anyhow::anyhow!("{:#}", e))?;
  51. Ok(())
  52. }
  53. pub fn configure_cargo(
  54. app: &App,
  55. android: Option<(&mut AndroidEnv, &AndroidConfig)>,
  56. ) -> Result<()> {
  57. if let Some((env, config)) = android {
  58. for target in AndroidTarget::all().values() {
  59. let config = target.generate_cargo_config(config, env)?;
  60. let target_var_name = target.triple.replace('-', "_").to_uppercase();
  61. if let Some(linker) = config.linker {
  62. env.base.insert_env_var(
  63. format!("CARGO_TARGET_{target_var_name}_LINKER"),
  64. linker.into(),
  65. );
  66. }
  67. env.base.insert_env_var(
  68. format!("CARGO_TARGET_{target_var_name}_RUSTFLAGS"),
  69. config.rustflags.join(" ").into(),
  70. );
  71. }
  72. }
  73. let mut dot_cargo = dot_cargo::DotCargo::load(app)?;
  74. // Mysteriously, builds that don't specify `--target` seem to fight over
  75. // the build cache with builds that use `--target`! This means that
  76. // alternating between i.e. `cargo run` and `cargo apple run` would
  77. // result in clean builds being made each time you switched... which is
  78. // pretty nightmarish. Specifying `build.target` in `.cargo/config`
  79. // fortunately has the same effect as specifying `--target`, so now we can
  80. // `cargo run` with peace of mind!
  81. //
  82. // This behavior could be explained here:
  83. // https://doc.rust-lang.org/cargo/reference/config.html#buildrustflags
  84. dot_cargo.set_default_target(util::host_target_triple()?);
  85. dot_cargo.write(app).map_err(Into::into)
  86. }
  87. #[cfg(target_os = "macos")]
  88. #[derive(Serialize)]
  89. pub enum CodeSignStyle {
  90. Manual,
  91. Automatic,
  92. }
  93. #[cfg(target_os = "macos")]
  94. #[derive(Serialize)]
  95. #[serde(rename_all = "kebab-case")]
  96. pub struct IosInitConfig {
  97. pub code_sign_style: CodeSignStyle,
  98. pub code_sign_identity: Option<String>,
  99. pub team_id: Option<String>,
  100. pub provisioning_profile_uuid: Option<String>,
  101. }
  102. #[derive(Serialize)]
  103. pub struct TauriInitConfig {
  104. #[cfg(target_os = "macos")]
  105. ios: IosInitConfig,
  106. }
  107. pub fn exec(
  108. target: Target,
  109. wrapper: &TextWrapper,
  110. tauri_init_config: &TauriInitConfig,
  111. #[allow(unused_variables)] non_interactive: bool,
  112. #[allow(unused_variables)] reinstall_deps: bool,
  113. skip_targets_install: bool,
  114. ) -> Result<App> {
  115. let tauri_config = get_tauri_config(target.platform_target(), None)?;
  116. let tauri_config_guard = tauri_config.lock().unwrap();
  117. let tauri_config_ = tauri_config_guard.as_ref().unwrap();
  118. let app = get_app(tauri_config_, &AppInterface::new(tauri_config_, None)?);
  119. let (handlebars, mut map) = handlebars(&app);
  120. map.insert("tauri", tauri_init_config);
  121. let mut args = std::env::args_os();
  122. let (binary, mut build_args) = args
  123. .next()
  124. .map(|bin| {
  125. let bin_path = PathBuf::from(&bin);
  126. let mut build_args = vec!["tauri"];
  127. if let Some(bin_stem) = bin_path.file_stem() {
  128. let r = regex::Regex::new("(nodejs|node)\\-?([1-9]*)*$").unwrap();
  129. if r.is_match(&bin_stem.to_string_lossy()) {
  130. if var_os("PNPM_PACKAGE_NAME").is_some() {
  131. return ("pnpm".into(), build_args);
  132. } else if let Some(npm_execpath) = var_os("npm_execpath") {
  133. let manager_stem = PathBuf::from(&npm_execpath)
  134. .file_stem()
  135. .unwrap()
  136. .to_os_string();
  137. let is_npm = manager_stem == "npm-cli";
  138. let binary = if is_npm {
  139. "npm".into()
  140. } else if manager_stem == "npx-cli" {
  141. "npx".into()
  142. } else {
  143. manager_stem
  144. };
  145. if is_npm {
  146. build_args.insert(0, "run");
  147. build_args.insert(1, "--");
  148. }
  149. return (binary, build_args);
  150. }
  151. } else if !cfg!(debug_assertions) && bin_stem == "cargo-tauri" {
  152. return (std::ffi::OsString::from("cargo"), build_args);
  153. }
  154. }
  155. (bin, build_args)
  156. })
  157. .unwrap_or_else(|| (std::ffi::OsString::from("cargo"), vec!["tauri"]));
  158. build_args.push(target.command_name());
  159. build_args.push(target.ide_build_script_name());
  160. map.insert("tauri-binary", binary.to_string_lossy());
  161. map.insert("tauri-binary-args", &build_args);
  162. map.insert("tauri-binary-args-str", build_args.join(" "));
  163. let app = match target {
  164. // Generate Android Studio project
  165. Target::Android => match AndroidEnv::new() {
  166. Ok(_env) => {
  167. let (config, metadata) =
  168. super::android::get_config(&app, tauri_config_, None, &Default::default());
  169. map.insert("android", &config);
  170. super::android::project::gen(
  171. &config,
  172. &metadata,
  173. (handlebars, map),
  174. wrapper,
  175. skip_targets_install,
  176. )?;
  177. app
  178. }
  179. Err(err) => {
  180. if err.sdk_or_ndk_issue() {
  181. Report::action_request(
  182. " to initialize Android environment; Android support won't be usable until you fix the issue below and re-run `tauri android init`!",
  183. err,
  184. )
  185. .print(wrapper);
  186. app
  187. } else {
  188. return Err(err.into());
  189. }
  190. }
  191. },
  192. #[cfg(target_os = "macos")]
  193. // Generate Xcode project
  194. Target::Ios => {
  195. let (config, metadata) =
  196. super::ios::get_config(&app, tauri_config_, None, &Default::default());
  197. map.insert("apple", &config);
  198. super::ios::project::gen(
  199. &config,
  200. &metadata,
  201. (handlebars, map),
  202. wrapper,
  203. non_interactive,
  204. reinstall_deps,
  205. skip_targets_install,
  206. )?;
  207. app
  208. }
  209. };
  210. Report::victory(
  211. "Project generated successfully!",
  212. "Make cool apps! 🌻 🐕 🎉",
  213. )
  214. .print(wrapper);
  215. Ok(app)
  216. }
  217. fn handlebars(app: &App) -> (Handlebars<'static>, JsonMap) {
  218. let mut h = Handlebars::new();
  219. h.register_escape_fn(handlebars::no_escape);
  220. h.register_helper("html-escape", Box::new(html_escape));
  221. h.register_helper("join", Box::new(join));
  222. h.register_helper("quote-and-join", Box::new(quote_and_join));
  223. h.register_helper(
  224. "quote-and-join-colon-prefix",
  225. Box::new(quote_and_join_colon_prefix),
  226. );
  227. h.register_helper("snake-case", Box::new(snake_case));
  228. h.register_helper("reverse-domain", Box::new(reverse_domain));
  229. h.register_helper(
  230. "reverse-domain-snake-case",
  231. Box::new(reverse_domain_snake_case),
  232. );
  233. h.register_helper("escape-kotlin-keyword", Box::new(escape_kotlin_keyword));
  234. // don't mix these up or very bad things will happen to all of us
  235. h.register_helper("prefix-path", Box::new(prefix_path));
  236. h.register_helper("unprefix-path", Box::new(unprefix_path));
  237. let mut map = JsonMap::default();
  238. map.insert("app", app);
  239. (h, map)
  240. }
  241. fn get_str<'a>(helper: &'a Helper) -> &'a str {
  242. helper
  243. .param(0)
  244. .and_then(|v| v.value().as_str())
  245. .unwrap_or("")
  246. }
  247. fn get_str_array(helper: &Helper, formatter: impl Fn(&str) -> String) -> Option<Vec<String>> {
  248. helper.param(0).and_then(|v| {
  249. v.value().as_array().and_then(|arr| {
  250. arr
  251. .iter()
  252. .map(|val| {
  253. val.as_str().map(
  254. #[allow(clippy::redundant_closure)]
  255. |s| formatter(s),
  256. )
  257. })
  258. .collect()
  259. })
  260. })
  261. }
  262. fn html_escape(
  263. helper: &Helper,
  264. _: &Handlebars,
  265. _ctx: &Context,
  266. _: &mut RenderContext,
  267. out: &mut dyn Output,
  268. ) -> HelperResult {
  269. out
  270. .write(&handlebars::html_escape(get_str(helper)))
  271. .map_err(Into::into)
  272. }
  273. fn join(
  274. helper: &Helper,
  275. _: &Handlebars,
  276. _: &Context,
  277. _: &mut RenderContext,
  278. out: &mut dyn Output,
  279. ) -> HelperResult {
  280. out
  281. .write(
  282. &get_str_array(helper, |s| s.to_string())
  283. .ok_or_else(|| {
  284. RenderErrorReason::ParamTypeMismatchForName("join", "0".to_owned(), "array".to_owned())
  285. })?
  286. .join(", "),
  287. )
  288. .map_err(Into::into)
  289. }
  290. fn quote_and_join(
  291. helper: &Helper,
  292. _: &Handlebars,
  293. _: &Context,
  294. _: &mut RenderContext,
  295. out: &mut dyn Output,
  296. ) -> HelperResult {
  297. out
  298. .write(
  299. &get_str_array(helper, |s| format!("{s:?}"))
  300. .ok_or_else(|| {
  301. RenderErrorReason::ParamTypeMismatchForName(
  302. "quote-and-join",
  303. "0".to_owned(),
  304. "array".to_owned(),
  305. )
  306. })?
  307. .join(", "),
  308. )
  309. .map_err(Into::into)
  310. }
  311. fn quote_and_join_colon_prefix(
  312. helper: &Helper,
  313. _: &Handlebars,
  314. _: &Context,
  315. _: &mut RenderContext,
  316. out: &mut dyn Output,
  317. ) -> HelperResult {
  318. out
  319. .write(
  320. &get_str_array(helper, |s| format!("{:?}", format!(":{s}")))
  321. .ok_or_else(|| {
  322. RenderErrorReason::ParamTypeMismatchForName(
  323. "quote-and-join-colon-prefix",
  324. "0".to_owned(),
  325. "array".to_owned(),
  326. )
  327. })?
  328. .join(", "),
  329. )
  330. .map_err(Into::into)
  331. }
  332. fn snake_case(
  333. helper: &Helper,
  334. _: &Handlebars,
  335. _: &Context,
  336. _: &mut RenderContext,
  337. out: &mut dyn Output,
  338. ) -> HelperResult {
  339. use heck::ToSnekCase as _;
  340. out
  341. .write(&get_str(helper).to_snek_case())
  342. .map_err(Into::into)
  343. }
  344. fn reverse_domain(
  345. helper: &Helper,
  346. _: &Handlebars,
  347. _: &Context,
  348. _: &mut RenderContext,
  349. out: &mut dyn Output,
  350. ) -> HelperResult {
  351. out
  352. .write(&util::reverse_domain(get_str(helper)))
  353. .map_err(Into::into)
  354. }
  355. fn reverse_domain_snake_case(
  356. helper: &Helper,
  357. _: &Handlebars,
  358. _: &Context,
  359. _: &mut RenderContext,
  360. out: &mut dyn Output,
  361. ) -> HelperResult {
  362. use heck::ToSnekCase as _;
  363. out
  364. .write(&util::reverse_domain(get_str(helper)).to_snek_case())
  365. .map_err(Into::into)
  366. }
  367. fn escape_kotlin_keyword(
  368. helper: &Helper,
  369. _: &Handlebars,
  370. _: &Context,
  371. _: &mut RenderContext,
  372. out: &mut dyn Output,
  373. ) -> HelperResult {
  374. let escaped_result = get_str(helper)
  375. .split('.')
  376. .map(|s| {
  377. if KOTLIN_ONLY_KEYWORDS.contains(&s) {
  378. format!("`{}`", s)
  379. } else {
  380. s.to_string()
  381. }
  382. })
  383. .collect::<Vec<_>>()
  384. .join(".");
  385. out.write(&escaped_result).map_err(Into::into)
  386. }
  387. fn app_root(ctx: &Context) -> Result<&str, RenderError> {
  388. let app_root = ctx
  389. .data()
  390. .get("app")
  391. .ok_or_else(|| RenderErrorReason::Other("`app` missing from template data.".to_owned()))?
  392. .get("root-dir")
  393. .ok_or_else(|| {
  394. RenderErrorReason::Other("`app.root-dir` missing from template data.".to_owned())
  395. })?;
  396. app_root.as_str().ok_or_else(|| {
  397. RenderErrorReason::Other("`app.root-dir` contained invalid UTF-8.".to_owned()).into()
  398. })
  399. }
  400. fn prefix_path(
  401. helper: &Helper,
  402. _: &Handlebars,
  403. ctx: &Context,
  404. _: &mut RenderContext,
  405. out: &mut dyn Output,
  406. ) -> HelperResult {
  407. out
  408. .write(
  409. util::prefix_path(app_root(ctx)?, get_str(helper))
  410. .to_str()
  411. .ok_or_else(|| {
  412. RenderErrorReason::Other(
  413. "Either the `app.root-dir` or the specified path contained invalid UTF-8.".to_owned(),
  414. )
  415. })?,
  416. )
  417. .map_err(Into::into)
  418. }
  419. fn unprefix_path(
  420. helper: &Helper,
  421. _: &Handlebars,
  422. ctx: &Context,
  423. _: &mut RenderContext,
  424. out: &mut dyn Output,
  425. ) -> HelperResult {
  426. out
  427. .write(
  428. util::unprefix_path(app_root(ctx)?, get_str(helper))
  429. .map_err(|_| {
  430. RenderErrorReason::Other(
  431. "Attempted to unprefix a path that wasn't in the app root dir.".to_owned(),
  432. )
  433. })?
  434. .to_str()
  435. .ok_or_else(|| {
  436. RenderErrorReason::Other(
  437. "Either the `app.root-dir` or the specified path contained invalid UTF-8.".to_owned(),
  438. )
  439. })?,
  440. )
  441. .map_err(Into::into)
  442. }