tauri-plugin 2.7.0

Build script and runtime Tauri plugin definitions
Documentation
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::{
  collections::BTreeMap,
  path::{Path, PathBuf},
};

use anyhow::Result;
use tauri_utils::acl::{self, Error};

pub mod mobile;

use serde::de::DeserializeOwned;

use std::{env, io::Cursor};

const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];

/// Reads the configuration of the plugin with the given name from the environment.
///
/// Tauri applications configure plugins through the `plugins > $plugin-name` object of their
/// `tauri.conf.json`. The Tauri CLI forwards that object to the build script of the plugin
/// through the `TAURI_<PLUGIN_NAME>_PLUGIN_CONFIG` environment variable (uppercased, with `-`
/// replaced by `_`), so plugins can generate platform-specific files from it - for example
/// adding a usage description to the iOS `Info.plist` with `mobile::update_info_plist`
/// (macOS hosts only).
///
/// Returns `None` when the variable is not set, which is the case when the crate is built
/// without the Tauri CLI (e.g. a plain `cargo build`), so the build script must always be able
/// to run without a configuration.
///
/// Also emits a `cargo:rerun-if-env-changed` instruction for the variable.
///
/// # Examples
///
/// ```rust,no_run
/// // usually a type deriving `serde::Deserialize`
/// let config = tauri_plugin::plugin_config::<serde_json::Value>("my-plugin");
/// let timeout = config
///   .and_then(|c| c.get("timeout").and_then(|t| t.as_u64()))
///   .unwrap_or(30);
/// ```
///
/// # Panics
///
/// Panics if the value of the environment variable is not a valid JSON representation of `T`,
/// which means the plugin configuration on the app's `tauri.conf.json` does not match the
/// expected format.
pub fn plugin_config<T: DeserializeOwned>(name: &str) -> Option<T> {
  let config_env_var_name = format!(
    "TAURI_{}_PLUGIN_CONFIG",
    name.to_uppercase().replace('-', "_")
  );
  if let Ok(config_str) = env::var(&config_env_var_name) {
    println!("cargo:rerun-if-env-changed={config_env_var_name}");
    serde_json::from_reader(Cursor::new(config_str))
      .map(Some)
      .expect("failed to parse configuration")
  } else {
    None
  }
}

/// Builder of the Tauri plugin build script.
///
/// It must be executed from the `build.rs` of every Tauri plugin crate - see
/// [`Self::try_build`] for everything it does.
///
/// # Examples
///
/// ```rust,no_run
/// const COMMANDS: &[&str] = &["ping", "execute"];
///
/// tauri_plugin::Builder::new(COMMANDS)
///   .android_path("android")
///   .ios_path("ios")
///   .build();
/// ```
pub struct Builder<'a> {
  commands: &'a [&'static str],
  global_scope_schema: Option<schemars::schema::RootSchema>,
  global_api_script_path: Option<PathBuf>,
  android_path: Option<PathBuf>,
  ios_path: Option<PathBuf>,
}

impl<'a> Builder<'a> {
  /// Creates a new builder for a plugin exposing the given commands.
  ///
  /// The command names must be written in snake_case, matching the name of the Rust functions
  /// annotated with `#[tauri::command]`. An `allow-$command` and a `deny-$command` permission
  /// is generated for each of them in the `permissions/autogenerated/commands` directory,
  /// where `$command` is the command name with `_` replaced by `-`.
  ///
  /// Note that the default permission of the plugin is **not** autogenerated:
  /// it must be defined in a `permissions/default.toml` (or `.json`) file.
  pub fn new(commands: &'a [&'static str]) -> Self {
    Self {
      commands,
      global_scope_schema: None,
      global_api_script_path: None,
      android_path: None,
      ios_path: None,
    }
  }

  /// Sets the global scope JSON schema.
  pub fn global_scope_schema(mut self, schema: schemars::schema::RootSchema) -> Self {
    self.global_scope_schema.replace(schema);
    self
  }

  /// Sets the path to the script that is injected in the webview when the `withGlobalTauri` configuration is set to true.
  ///
  /// This is usually an IIFE that injects the plugin API JavaScript bindings to `window.__TAURI__`.
  pub fn global_api_script_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
    self.global_api_script_path.replace(path.into());
    self
  }

  /// Sets the Android project path.
  pub fn android_path<P: Into<PathBuf>>(mut self, android_path: P) -> Self {
    self.android_path.replace(android_path.into());
    self
  }

  /// Sets the iOS project path.
  pub fn ios_path<P: Into<PathBuf>>(mut self, ios_path: P) -> Self {
    self.ios_path.replace(ios_path.into());
    self
  }

  /// [`Self::try_build`] but will exit automatically if an error is found.
  pub fn build(self) {
    if let Err(error) = self.try_build() {
      println!("{}: {error:#}", env!("CARGO_PKG_NAME"));
      std::process::exit(1);
    }
  }

  /// Ensure this crate is properly configured to be a Tauri plugin.
  ///
  /// # Errors
  ///
  /// Errors will occur if environmental variables expected to be set inside of [build scripts]
  /// are not found, or if the crate violates Tauri plugin conventions.
  pub fn try_build(self) -> Result<()> {
    // convention: plugin names should not use underscores
    let name = build_var("CARGO_PKG_NAME")?;
    if name.contains('_') {
      anyhow::bail!("plugin names cannot contain underscores");
    }
    if RESERVED_PLUGIN_NAMES.contains(&name.as_str()) {
      anyhow::bail!("plugin name `{name}` is reserved");
    }

    let out_dir = PathBuf::from(build_var("OUT_DIR")?);

    // requirement: links MUST be set and MUST match the name
    let _links = std::env::var("CARGO_MANIFEST_LINKS").map_err(|_| Error::LinksMissing)?;

    let autogenerated = Path::new("permissions").join(acl::build::AUTOGENERATED_FOLDER_NAME);
    std::fs::create_dir_all(&autogenerated).expect("unable to create permissions dir");

    let commands_dir = autogenerated.join("commands");
    if !self.commands.is_empty() {
      acl::build::autogenerate_command_permissions(&commands_dir, self.commands, "", true);
    }

    println!("cargo:rerun-if-changed=permissions");
    let permissions =
      acl::build::define_permissions("./permissions/**/*.*", &name, &out_dir, |_| true)?;

    if permissions.is_empty() {
      let _ = std::fs::remove_file(format!(
        "./permissions/{}/{}",
        acl::PERMISSION_SCHEMAS_FOLDER_NAME,
        acl::PERMISSION_SCHEMA_FILE_NAME
      ));
      let _ = std::fs::remove_file(autogenerated.join(acl::build::PERMISSION_DOCS_FILE_NAME));
    } else {
      acl::schema::generate_permissions_schema(&permissions, "./permissions")?;
      acl::build::generate_docs(
        &permissions,
        &autogenerated,
        name.strip_prefix("tauri-plugin-").unwrap_or(&name),
      )?;
    }

    let mut permissions_map = BTreeMap::new();
    permissions_map.insert(name.clone(), permissions);
    tauri_utils::acl::build::generate_allowed_commands(&out_dir, None, permissions_map)?;

    if let Some(global_scope_schema) = self.global_scope_schema {
      acl::build::define_global_scope_schema(global_scope_schema, &name, &out_dir)?;
    }

    if let Some(path) = self.global_api_script_path {
      tauri_utils::plugin::define_global_api_script_path(&path);
    }

    mobile::setup(self.android_path, self.ios_path)?;

    Ok(())
  }
}

fn cfg_alias(alias: &str, has_feature: bool) {
  println!("cargo:rustc-check-cfg=cfg({alias})");
  if has_feature {
    println!("cargo:rustc-cfg={alias}");
  }
}

/// Grab an env var that is expected to be set inside of build scripts.
fn build_var(key: &'static str) -> Result<String, Error> {
  std::env::var(key).map_err(|_| Error::BuildVar(key))
}