1use super::{get_app, Target};
6use crate::{
7 helpers::app_paths::Dirs,
8 helpers::{config::get_config as get_tauri_config, template::JsonMap},
9 interface::AppInterface,
10 ConfigValue, Result,
11};
12use cargo_mobile2::{
13 config::app::App,
14 reserved_names::KOTLIN_ONLY_KEYWORDS,
15 util::{
16 self,
17 cli::{Report, TextWrapper},
18 },
19};
20use handlebars::{
21 Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, RenderErrorReason,
22};
23
24use std::{env::var_os, path::PathBuf};
25
26pub fn command(
27 target: Target,
28 ci: bool,
29 reinstall_deps: bool,
30 skip_targets_install: bool,
31 config: Vec<ConfigValue>,
32) -> Result<()> {
33 let dirs = crate::helpers::app_paths::resolve_dirs();
34 let wrapper = TextWrapper::default();
35
36 exec(
37 target,
38 &wrapper,
39 ci,
40 reinstall_deps,
41 skip_targets_install,
42 config,
43 dirs,
44 )?;
45 Ok(())
46}
47
48fn exec(
49 target: Target,
50 wrapper: &TextWrapper,
51 #[allow(unused_variables)] non_interactive: bool,
52 #[allow(unused_variables)] reinstall_deps: bool,
53 skip_targets_install: bool,
54 config: Vec<ConfigValue>,
55 dirs: Dirs,
56) -> Result<App> {
57 let tauri_config = get_tauri_config(
58 target.platform_target(),
59 &config.iter().map(|conf| &conf.0).collect::<Vec<_>>(),
60 dirs.tauri,
61 )?;
62
63 let app = get_app(
64 target,
65 &tauri_config,
66 &AppInterface::new(&tauri_config, None, dirs.tauri)?,
67 dirs.tauri,
68 );
69
70 let (handlebars, mut map) = handlebars(&app);
71
72 let mut args = std::env::args_os();
73
74 let (binary, mut build_args) = args
75 .next()
76 .map(|bin| {
77 let bin_path = PathBuf::from(&bin);
78 let mut build_args = vec!["tauri"];
79
80 if let Some(bin_stem) = bin_path.file_stem() {
81 let r = regex::Regex::new("(nodejs|node)\\-?([1-9]*)*$").unwrap();
82 if r.is_match(&bin_stem.to_string_lossy()) {
83 if var_os("PNPM_PACKAGE_NAME").is_some() {
84 return ("pnpm".into(), build_args);
85 } else if is_pnpm_dlx() {
86 return ("pnpm".into(), vec!["dlx", "@tauri-apps/cli"]);
87 } else if let Some(npm_execpath) = var_os("npm_execpath") {
88 let manager_stem = PathBuf::from(&npm_execpath)
89 .file_stem()
90 .unwrap()
91 .to_os_string();
92 let is_npm = manager_stem == "npm-cli";
93 let binary = if is_npm {
94 "npm".into()
95 } else if manager_stem == "npx-cli" {
96 "npx".into()
97 } else {
98 manager_stem
99 };
100
101 if is_npm {
102 build_args.insert(0, "run");
103 build_args.insert(1, "--");
104 }
105
106 return (binary, build_args);
107 }
108 } else if bin_stem == "deno" {
109 build_args.insert(0, "task");
110 return (std::ffi::OsString::from("deno"), build_args);
111 } else if !cfg!(debug_assertions) && bin_stem == "cargo-tauri" {
112 return (std::ffi::OsString::from("cargo"), build_args);
113 }
114 }
115
116 (bin, build_args)
117 })
118 .unwrap_or_else(|| (std::ffi::OsString::from("cargo"), vec!["tauri"]));
119
120 build_args.push(target.command_name());
121 build_args.push(target.ide_build_script_name());
122
123 let mut binary = binary.to_string_lossy().to_string();
124 if binary.ends_with(".exe") || binary.ends_with(".cmd") || binary.ends_with(".bat") {
125 binary.pop();
127 binary.pop();
128 binary.pop();
129 binary.pop();
130 }
131
132 map.insert("tauri-binary", binary);
133 map.insert("tauri-binary-args", &build_args);
134 map.insert("tauri-binary-args-str", build_args.join(" "));
135
136 let app = match target {
137 Target::Android => {
139 let _env = super::android::env(non_interactive)?;
140 let (config, metadata) =
141 super::android::get_config(&app, &tauri_config, &[], &Default::default());
142 map.insert("android", &config);
143 super::android::project::gen(
144 &config,
145 &metadata,
146 (handlebars, map),
147 wrapper,
148 skip_targets_install,
149 )?;
150 app
151 }
152 #[cfg(target_os = "macos")]
153 Target::Ios => {
155 let (config, metadata) =
156 super::ios::get_config(&app, &tauri_config, &[], &Default::default(), dirs.tauri)?;
157 map.insert("apple", &config);
158 super::ios::project::gen(
159 &tauri_config,
160 &config,
161 &metadata,
162 (handlebars, map),
163 wrapper,
164 non_interactive,
165 reinstall_deps,
166 skip_targets_install,
167 )?;
168 app
169 }
170 };
171
172 Report::victory(
173 "Project generated successfully!",
174 "Make cool apps! 🌻 🐕 🎉",
175 )
176 .print(wrapper);
177 Ok(app)
178}
179
180fn handlebars(app: &App) -> (Handlebars<'static>, JsonMap) {
181 let mut h = Handlebars::new();
182 h.register_escape_fn(handlebars::no_escape);
183
184 h.register_helper("html-escape", Box::new(html_escape));
185 h.register_helper("join", Box::new(join));
186 h.register_helper("quote-and-join", Box::new(quote_and_join));
187 h.register_helper(
188 "quote-and-join-colon-prefix",
189 Box::new(quote_and_join_colon_prefix),
190 );
191 h.register_helper("snake-case", Box::new(snake_case));
192 h.register_helper("escape-kotlin-keyword", Box::new(escape_kotlin_keyword));
193 h.register_helper("prefix-path", Box::new(prefix_path));
195 h.register_helper("unprefix-path", Box::new(unprefix_path));
196
197 let mut map = JsonMap::default();
198 map.insert("app", app);
199
200 (h, map)
201}
202
203fn get_str<'a>(helper: &'a Helper) -> &'a str {
204 helper
205 .param(0)
206 .and_then(|v| v.value().as_str())
207 .unwrap_or("")
208}
209
210fn get_str_array(helper: &Helper, formatter: impl Fn(&str) -> String) -> Option<Vec<String>> {
211 helper.param(0).and_then(|v| {
212 v.value()
213 .as_array()
214 .and_then(|arr| arr.iter().map(|val| val.as_str().map(&formatter)).collect())
215 })
216}
217
218fn html_escape(
219 helper: &Helper,
220 _: &Handlebars,
221 _ctx: &Context,
222 _: &mut RenderContext,
223 out: &mut dyn Output,
224) -> HelperResult {
225 out
226 .write(&handlebars::html_escape(get_str(helper)))
227 .map_err(Into::into)
228}
229
230fn join(
231 helper: &Helper,
232 _: &Handlebars,
233 _: &Context,
234 _: &mut RenderContext,
235 out: &mut dyn Output,
236) -> HelperResult {
237 out
238 .write(
239 &get_str_array(helper, |s| s.to_string())
240 .ok_or_else(|| {
241 RenderErrorReason::ParamTypeMismatchForName("join", "0".to_owned(), "array".to_owned())
242 })?
243 .join(", "),
244 )
245 .map_err(Into::into)
246}
247
248fn quote_and_join(
249 helper: &Helper,
250 _: &Handlebars,
251 _: &Context,
252 _: &mut RenderContext,
253 out: &mut dyn Output,
254) -> HelperResult {
255 out
256 .write(
257 &get_str_array(helper, |s| format!("{s:?}"))
258 .ok_or_else(|| {
259 RenderErrorReason::ParamTypeMismatchForName(
260 "quote-and-join",
261 "0".to_owned(),
262 "array".to_owned(),
263 )
264 })?
265 .join(", "),
266 )
267 .map_err(Into::into)
268}
269
270fn quote_and_join_colon_prefix(
271 helper: &Helper,
272 _: &Handlebars,
273 _: &Context,
274 _: &mut RenderContext,
275 out: &mut dyn Output,
276) -> HelperResult {
277 out
278 .write(
279 &get_str_array(helper, |s| format!("{:?}", format!(":{s}")))
280 .ok_or_else(|| {
281 RenderErrorReason::ParamTypeMismatchForName(
282 "quote-and-join-colon-prefix",
283 "0".to_owned(),
284 "array".to_owned(),
285 )
286 })?
287 .join(", "),
288 )
289 .map_err(Into::into)
290}
291
292fn snake_case(
293 helper: &Helper,
294 _: &Handlebars,
295 _: &Context,
296 _: &mut RenderContext,
297 out: &mut dyn Output,
298) -> HelperResult {
299 use heck::ToSnekCase as _;
300 out
301 .write(&get_str(helper).to_snek_case())
302 .map_err(Into::into)
303}
304
305fn escape_kotlin_keyword(
306 helper: &Helper,
307 _: &Handlebars,
308 _: &Context,
309 _: &mut RenderContext,
310 out: &mut dyn Output,
311) -> HelperResult {
312 let escaped_result = get_str(helper)
313 .split('.')
314 .map(|s| {
315 if KOTLIN_ONLY_KEYWORDS.contains(&s) {
316 format!("`{s}`")
317 } else {
318 s.to_string()
319 }
320 })
321 .collect::<Vec<_>>()
322 .join(".");
323
324 out.write(&escaped_result).map_err(Into::into)
325}
326
327fn app_root(ctx: &Context) -> std::result::Result<&str, RenderError> {
328 let app_root = ctx
329 .data()
330 .get("app")
331 .ok_or_else(|| RenderErrorReason::Other("`app` missing from template data.".to_owned()))?
332 .get("root-dir")
333 .ok_or_else(|| {
334 RenderErrorReason::Other("`app.root-dir` missing from template data.".to_owned())
335 })?;
336 app_root.as_str().ok_or_else(|| {
337 RenderErrorReason::Other("`app.root-dir` contained invalid UTF-8.".to_owned()).into()
338 })
339}
340
341fn prefix_path(
342 helper: &Helper,
343 _: &Handlebars,
344 ctx: &Context,
345 _: &mut RenderContext,
346 out: &mut dyn Output,
347) -> HelperResult {
348 out
349 .write(
350 util::prefix_path(app_root(ctx)?, get_str(helper))
351 .to_str()
352 .ok_or_else(|| {
353 RenderErrorReason::Other(
354 "Either the `app.root-dir` or the specified path contained invalid UTF-8.".to_owned(),
355 )
356 })?,
357 )
358 .map_err(Into::into)
359}
360
361fn unprefix_path(
362 helper: &Helper,
363 _: &Handlebars,
364 ctx: &Context,
365 _: &mut RenderContext,
366 out: &mut dyn Output,
367) -> HelperResult {
368 out
369 .write(
370 util::unprefix_path(app_root(ctx)?, get_str(helper))
371 .map_err(|_| {
372 RenderErrorReason::Other(
373 "Attempted to unprefix a path that wasn't in the app root dir.".to_owned(),
374 )
375 })?
376 .to_str()
377 .ok_or_else(|| {
378 RenderErrorReason::Other(
379 "Either the `app.root-dir` or the specified path contained invalid UTF-8.".to_owned(),
380 )
381 })?,
382 )
383 .map_err(Into::into)
384}
385
386fn is_pnpm_dlx() -> bool {
387 var_os("NODE_PATH")
388 .map(PathBuf::from)
389 .is_some_and(|node_path| {
390 let mut iter = node_path.components().peekable();
391 while let Some(c) = iter.next() {
392 if c.as_os_str() == "pnpm" && iter.peek().is_some_and(|c| c.as_os_str() == "dlx") {
393 return true;
394 }
395 }
396 false
397 })
398}