Skip to main content

tauri_cli/mobile/
init.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use 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    // remove Windows-only extension
126    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    // Generate Android Studio project
138    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    // Generate Xcode project
154    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  // don't mix these up or very bad things will happen to all of us
194  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}