Skip to main content

tauri_plugin/build/
mobile.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Mobile-specific build utilities.
6
7use std::{
8  fs::{copy, create_dir, create_dir_all, remove_dir_all},
9  path::{Path, PathBuf},
10};
11
12use anyhow::{Context, Result};
13
14use super::{build_var, cfg_alias};
15
16/// Patches the entitlements file of the iOS project of the application that depends on this
17/// plugin, so the plugin can request the capabilities its native code needs
18/// (push notifications, app groups, HealthKit, iCloud...).
19///
20/// It edits `<project>/<app-name>_iOS/<app-name>_iOS.entitlements` of the Xcode project
21/// generated by `tauri ios init`, reading its path from the `TAURI_IOS_PROJECT_PATH` and
22/// `TAURI_IOS_APP_NAME` environment variables set by the Tauri CLI. It is a no-op when they are
23/// not set (a plain `cargo build`, or a desktop build) or when the file does not exist, and the
24/// file is only written when the closure actually changed it.
25///
26/// Must be called from the plugin `build.rs`, ideally guarded by a
27/// [`plugin_config`](super::plugin_config) value so the app can opt in to the entitlement.
28/// Only available on macOS hosts, where the iOS project can be built.
29///
30/// # Examples
31///
32/// ```rust,no_run
33/// tauri_plugin::mobile::update_entitlements(|entitlements| {
34///   entitlements.insert("aps-environment".into(), "development".into());
35/// })
36/// .expect("failed to update entitlements");
37/// ```
38#[cfg(target_os = "macos")]
39pub fn update_entitlements<F: FnOnce(&mut plist::Dictionary)>(f: F) -> Result<()> {
40  if let (Some(project_path), Ok(app_name)) = (
41    std::env::var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from),
42    std::env::var("TAURI_IOS_APP_NAME"),
43  ) {
44    update_plist_file(
45      project_path
46        .join(format!("{app_name}_iOS"))
47        .join(format!("{app_name}_iOS.entitlements")),
48      f,
49    )?;
50  }
51
52  Ok(())
53}
54
55/// Patches the `Info.plist` of the iOS project of the application that depends on this plugin,
56/// which is where iOS expects the usage descriptions, URL schemes and background modes a
57/// plugin needs to be declared.
58///
59/// It edits `<project>/<app-name>_iOS/Info.plist` of the Xcode project generated by
60/// `tauri ios init`, reading its path from the `TAURI_IOS_PROJECT_PATH` and
61/// `TAURI_IOS_APP_NAME` environment variables set by the Tauri CLI. It is a no-op when they are
62/// not set (a plain `cargo build`, or a desktop build) or when the file does not exist, and the
63/// file is only written when the closure actually changed it.
64///
65/// Must be called from the plugin `build.rs`. Values that are specific to the application, such
66/// as the text shown on a permission prompt, should be read from the plugin configuration with
67/// [`plugin_config`](super::plugin_config) instead of hardcoded.
68/// Only available on macOS hosts, where the iOS project can be built.
69///
70/// # Examples
71///
72/// ```rust,no_run
73/// tauri_plugin::mobile::update_info_plist(|plist| {
74///   plist.insert(
75///     "NSCameraUsageDescription".into(),
76///     "This app needs the camera to scan QR codes".into(),
77///   );
78/// })
79/// .expect("failed to update Info.plist");
80/// ```
81#[cfg(target_os = "macos")]
82pub fn update_info_plist<F: FnOnce(&mut plist::Dictionary)>(f: F) -> Result<()> {
83  if let (Some(project_path), Ok(app_name)) = (
84    std::env::var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from),
85    std::env::var("TAURI_IOS_APP_NAME"),
86  ) {
87    update_plist_file(
88      project_path
89        .join(format!("{app_name}_iOS"))
90        .join("Info.plist"),
91      f,
92    )?;
93  }
94
95  Ok(())
96}
97
98/// Updates the Android manifest by inserting XML content into a specified parent tag.
99pub fn update_android_manifest(block_identifier: &str, parent: &str, insert: String) -> Result<()> {
100  tauri_utils::build::update_android_manifest(block_identifier, parent, insert)
101}
102
103pub(crate) fn setup(
104  android_path: Option<PathBuf>,
105  #[allow(unused_variables)] ios_path: Option<PathBuf>,
106) -> Result<()> {
107  let target_os = build_var("CARGO_CFG_TARGET_OS")?;
108  let mobile = target_os == "android" || target_os == "ios";
109  cfg_alias("mobile", mobile);
110  cfg_alias("desktop", !mobile);
111
112  // docs.rs mounts the crate sources read-only, so the native projects can't get their `.tauri` directory,
113  // and the documentation doesn't need them anyway
114  if std::env::var_os("DOCS_RS").is_some() {
115    return Ok(());
116  }
117
118  match target_os.as_str() {
119    "android" => {
120      if let Some(path) = android_path {
121        let manifest_dir = build_var("CARGO_MANIFEST_DIR").map(PathBuf::from)?;
122        let source = manifest_dir.join(path);
123
124        let tauri_library_path = std::env::var("DEP_TAURI_ANDROID_LIBRARY_PATH")
125            .expect("missing `DEP_TAURI_ANDROID_LIBRARY_PATH` environment variable. Make sure `tauri` is a dependency of the plugin.");
126        println!("cargo:rerun-if-env-changed=DEP_TAURI_ANDROID_LIBRARY_PATH");
127
128        create_dir_all(source.join(".tauri")).context("failed to create .tauri directory")?;
129        copy_folder(
130          Path::new(&tauri_library_path),
131          &source.join(".tauri").join("tauri-api"),
132          &[],
133        )
134        .context("failed to copy tauri-api to the plugin project")?;
135
136        println!("cargo:android_library_path={}", source.display());
137      }
138    }
139    #[cfg(target_os = "macos")]
140    "ios" => {
141      if let Some(path) = ios_path {
142        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
143          .map(PathBuf::from)
144          .unwrap();
145        let tauri_library_path = std::env::var("DEP_TAURI_IOS_LIBRARY_PATH")
146            .expect("missing `DEP_TAURI_IOS_LIBRARY_PATH` environment variable. Make sure `tauri` is a dependency of the plugin.");
147
148        let tauri_dep_path = path.parent().unwrap().join(".tauri");
149        create_dir_all(&tauri_dep_path).context("failed to create .tauri directory")?;
150        copy_folder(
151          Path::new(&tauri_library_path),
152          &tauri_dep_path.join("tauri-api"),
153          &[".build", "Package.resolved", "Tests"],
154        )
155        .context("failed to copy tauri-api to the plugin project")?;
156        tauri_utils::build::link_apple_library(
157          &std::env::var("CARGO_PKG_NAME").unwrap(),
158          manifest_dir.join(path),
159        );
160      }
161    }
162    _ => (),
163  }
164
165  Ok(())
166}
167
168fn copy_folder(source: &Path, target: &Path, ignore_paths: &[&str]) -> Result<()> {
169  let _ = remove_dir_all(target);
170
171  for entry in walkdir::WalkDir::new(source) {
172    let entry = entry?;
173    let rel_path = entry.path().strip_prefix(source)?;
174    let rel_path_str = rel_path.to_string_lossy();
175    if ignore_paths
176      .iter()
177      .any(|path| rel_path_str.starts_with(path))
178    {
179      continue;
180    }
181    let dest_path = target.join(rel_path);
182
183    if entry.file_type().is_dir() {
184      create_dir(&dest_path)
185        .with_context(|| format!("failed to create directory {}", dest_path.display()))?;
186    } else {
187      copy(entry.path(), &dest_path).with_context(|| {
188        format!(
189          "failed to copy {} to {}",
190          entry.path().display(),
191          dest_path.display()
192        )
193      })?;
194      println!("cargo:rerun-if-changed={}", entry.path().display());
195    }
196  }
197
198  Ok(())
199}
200
201#[cfg(target_os = "macos")]
202fn update_plist_file<P: AsRef<Path>, F: FnOnce(&mut plist::Dictionary)>(
203  path: P,
204  f: F,
205) -> Result<()> {
206  use std::io::Cursor;
207
208  let path = path.as_ref();
209  if path.exists() {
210    let plist_str = std::fs::read_to_string(path)?;
211    let mut plist = plist::Value::from_reader(Cursor::new(&plist_str))?;
212    if let Some(dict) = plist.as_dictionary_mut() {
213      f(dict);
214      let mut plist_buf = Vec::new();
215      let writer = Cursor::new(&mut plist_buf);
216      plist::to_writer_xml(writer, &plist)?;
217      let new_plist_str = String::from_utf8(plist_buf)?;
218      if new_plist_str != plist_str {
219        std::fs::write(path, new_plist_str)?;
220      }
221    }
222  }
223
224  Ok(())
225}
226
227#[cfg(test)]
228mod tests {
229  #[test]
230  fn update_android_manifest() {
231    use tauri_utils::build::update_android_manifest;
232
233    // This test would require setting up the environment, so we just verify it compiles
234    // The actual implementation is tested in tauri-utils
235    let _result = update_android_manifest("test", "activity", "<test></test>".to_string());
236  }
237}