Skip to main content

tauri_plugin/build/
mod.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 std::{
6  collections::BTreeMap,
7  path::{Path, PathBuf},
8};
9
10use anyhow::Result;
11use tauri_utils::acl::{self, Error};
12
13pub mod mobile;
14
15use serde::de::DeserializeOwned;
16
17use std::{env, io::Cursor};
18
19const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];
20
21/// Reads the configuration of the plugin with the given name from the environment.
22///
23/// Tauri applications configure plugins through the `plugins > $plugin-name` object of their
24/// `tauri.conf.json`. The Tauri CLI forwards that object to the build script of the plugin
25/// through the `TAURI_<PLUGIN_NAME>_PLUGIN_CONFIG` environment variable (uppercased, with `-`
26/// replaced by `_`), so plugins can generate platform-specific files from it - for example
27/// adding a usage description to the iOS `Info.plist` with `mobile::update_info_plist`
28/// (macOS hosts only).
29///
30/// Returns `None` when the variable is not set, which is the case when the crate is built
31/// without the Tauri CLI (e.g. a plain `cargo build`), so the build script must always be able
32/// to run without a configuration.
33///
34/// Also emits a `cargo:rerun-if-env-changed` instruction for the variable.
35///
36/// # Examples
37///
38/// ```rust,no_run
39/// // usually a type deriving `serde::Deserialize`
40/// let config = tauri_plugin::plugin_config::<serde_json::Value>("my-plugin");
41/// let timeout = config
42///   .and_then(|c| c.get("timeout").and_then(|t| t.as_u64()))
43///   .unwrap_or(30);
44/// ```
45///
46/// # Panics
47///
48/// Panics if the value of the environment variable is not a valid JSON representation of `T`,
49/// which means the plugin configuration on the app's `tauri.conf.json` does not match the
50/// expected format.
51pub fn plugin_config<T: DeserializeOwned>(name: &str) -> Option<T> {
52  let config_env_var_name = format!(
53    "TAURI_{}_PLUGIN_CONFIG",
54    name.to_uppercase().replace('-', "_")
55  );
56  if let Ok(config_str) = env::var(&config_env_var_name) {
57    println!("cargo:rerun-if-env-changed={config_env_var_name}");
58    serde_json::from_reader(Cursor::new(config_str))
59      .map(Some)
60      .expect("failed to parse configuration")
61  } else {
62    None
63  }
64}
65
66/// Builder of the Tauri plugin build script.
67///
68/// It must be executed from the `build.rs` of every Tauri plugin crate - see
69/// [`Self::try_build`] for everything it does.
70///
71/// # Examples
72///
73/// ```rust,no_run
74/// const COMMANDS: &[&str] = &["ping", "execute"];
75///
76/// tauri_plugin::Builder::new(COMMANDS)
77///   .android_path("android")
78///   .ios_path("ios")
79///   .build();
80/// ```
81pub struct Builder<'a> {
82  commands: &'a [&'static str],
83  global_scope_schema: Option<schemars::schema::RootSchema>,
84  global_api_script_path: Option<PathBuf>,
85  android_path: Option<PathBuf>,
86  ios_path: Option<PathBuf>,
87}
88
89impl<'a> Builder<'a> {
90  /// Creates a new builder for a plugin exposing the given commands.
91  ///
92  /// The command names must be written in snake_case, matching the name of the Rust functions
93  /// annotated with `#[tauri::command]`. An `allow-$command` and a `deny-$command` permission
94  /// is generated for each of them in the `permissions/autogenerated/commands` directory,
95  /// where `$command` is the command name with `_` replaced by `-`.
96  ///
97  /// Note that the default permission of the plugin is **not** autogenerated:
98  /// it must be defined in a `permissions/default.toml` (or `.json`) file.
99  pub fn new(commands: &'a [&'static str]) -> Self {
100    Self {
101      commands,
102      global_scope_schema: None,
103      global_api_script_path: None,
104      android_path: None,
105      ios_path: None,
106    }
107  }
108
109  /// Sets the global scope JSON schema.
110  pub fn global_scope_schema(mut self, schema: schemars::schema::RootSchema) -> Self {
111    self.global_scope_schema.replace(schema);
112    self
113  }
114
115  /// Sets the path to the script that is injected in the webview when the `withGlobalTauri` configuration is set to true.
116  ///
117  /// This is usually an IIFE that injects the plugin API JavaScript bindings to `window.__TAURI__`.
118  pub fn global_api_script_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
119    self.global_api_script_path.replace(path.into());
120    self
121  }
122
123  /// Sets the Android project path.
124  pub fn android_path<P: Into<PathBuf>>(mut self, android_path: P) -> Self {
125    self.android_path.replace(android_path.into());
126    self
127  }
128
129  /// Sets the iOS project path.
130  pub fn ios_path<P: Into<PathBuf>>(mut self, ios_path: P) -> Self {
131    self.ios_path.replace(ios_path.into());
132    self
133  }
134
135  /// [`Self::try_build`] but will exit automatically if an error is found.
136  pub fn build(self) {
137    if let Err(error) = self.try_build() {
138      println!("{}: {error:#}", env!("CARGO_PKG_NAME"));
139      std::process::exit(1);
140    }
141  }
142
143  /// Ensure this crate is properly configured to be a Tauri plugin.
144  ///
145  /// # Errors
146  ///
147  /// Errors will occur if environmental variables expected to be set inside of [build scripts]
148  /// are not found, or if the crate violates Tauri plugin conventions.
149  pub fn try_build(self) -> Result<()> {
150    // convention: plugin names should not use underscores
151    let name = build_var("CARGO_PKG_NAME")?;
152    if name.contains('_') {
153      anyhow::bail!("plugin names cannot contain underscores");
154    }
155    if RESERVED_PLUGIN_NAMES.contains(&name.as_str()) {
156      anyhow::bail!("plugin name `{name}` is reserved");
157    }
158
159    let out_dir = PathBuf::from(build_var("OUT_DIR")?);
160
161    // requirement: links MUST be set and MUST match the name
162    let _links = std::env::var("CARGO_MANIFEST_LINKS").map_err(|_| Error::LinksMissing)?;
163
164    let autogenerated = Path::new("permissions").join(acl::build::AUTOGENERATED_FOLDER_NAME);
165    std::fs::create_dir_all(&autogenerated).expect("unable to create permissions dir");
166
167    let commands_dir = autogenerated.join("commands");
168    if !self.commands.is_empty() {
169      acl::build::autogenerate_command_permissions(&commands_dir, self.commands, "", true);
170    }
171
172    println!("cargo:rerun-if-changed=permissions");
173    let permissions =
174      acl::build::define_permissions("./permissions/**/*.*", &name, &out_dir, |_| true)?;
175
176    if permissions.is_empty() {
177      let _ = std::fs::remove_file(format!(
178        "./permissions/{}/{}",
179        acl::PERMISSION_SCHEMAS_FOLDER_NAME,
180        acl::PERMISSION_SCHEMA_FILE_NAME
181      ));
182      let _ = std::fs::remove_file(autogenerated.join(acl::build::PERMISSION_DOCS_FILE_NAME));
183    } else {
184      acl::schema::generate_permissions_schema(&permissions, "./permissions")?;
185      acl::build::generate_docs(
186        &permissions,
187        &autogenerated,
188        name.strip_prefix("tauri-plugin-").unwrap_or(&name),
189      )?;
190    }
191
192    let mut permissions_map = BTreeMap::new();
193    permissions_map.insert(name.clone(), permissions);
194    tauri_utils::acl::build::generate_allowed_commands(&out_dir, None, permissions_map)?;
195
196    if let Some(global_scope_schema) = self.global_scope_schema {
197      acl::build::define_global_scope_schema(global_scope_schema, &name, &out_dir)?;
198    }
199
200    if let Some(path) = self.global_api_script_path {
201      tauri_utils::plugin::define_global_api_script_path(&path);
202    }
203
204    mobile::setup(self.android_path, self.ios_path)?;
205
206    Ok(())
207  }
208}
209
210fn cfg_alias(alias: &str, has_feature: bool) {
211  println!("cargo:rustc-check-cfg=cfg({alias})");
212  if has_feature {
213    println!("cargo:rustc-cfg={alias}");
214  }
215}
216
217/// Grab an env var that is expected to be set inside of build scripts.
218fn build_var(key: &'static str) -> Result<String, Error> {
219  std::env::var(key).map_err(|_| Error::BuildVar(key))
220}