tauri_codegen/
lib.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//! - Embed, hash, and compress assets, including icons for the app as well as the tray icon.
6//! - Parse `tauri.conf.json` at compile time and generate the Config struct.
7
8#![doc(
9  html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
10  html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
11)]
12
13pub use self::context::{context_codegen, ContextData};
14use crate::embedded_assets::{ensure_out_dir, EmbeddedAssetsError};
15use proc_macro2::TokenStream;
16use quote::{quote, ToTokens, TokenStreamExt};
17use std::{
18  borrow::Cow,
19  fmt::{self, Write},
20  path::{Path, PathBuf},
21};
22pub use tauri_utils::config::{parse::ConfigError, Config};
23use tauri_utils::platform::Target;
24use tauri_utils::write_if_changed;
25
26mod context;
27pub mod embedded_assets;
28pub mod image;
29#[doc(hidden)]
30pub mod vendor;
31
32/// Represents all the errors that can happen while reading the config during codegen.
33#[derive(Debug, thiserror::Error)]
34#[non_exhaustive]
35pub enum CodegenConfigError {
36  #[error("unable to access current working directory: {0}")]
37  CurrentDir(std::io::Error),
38
39  // this error should be "impossible" because we use std::env::current_dir() - cover it anyways
40  #[error("Tauri config file has no parent, this shouldn't be possible. file an issue on https://github.com/tauri-apps/tauri - target {0}")]
41  Parent(PathBuf),
42
43  #[error("unable to parse inline JSON TAURI_CONFIG env var: {0}")]
44  FormatInline(serde_json::Error),
45
46  #[error(transparent)]
47  Json(#[from] serde_json::Error),
48
49  #[error("{0}")]
50  ConfigError(#[from] ConfigError),
51}
52
53/// Get the [`Config`] from the `TAURI_CONFIG` environmental variable, or read from the passed path.
54///
55/// If the passed path is relative, it should be relative to the current working directory of the
56/// compiling crate.
57pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError> {
58  let path = if path.is_relative() {
59    let cwd = std::env::current_dir().map_err(CodegenConfigError::CurrentDir)?;
60    Cow::Owned(cwd.join(path))
61  } else {
62    Cow::Borrowed(path)
63  };
64
65  // this should be impossible because of the use of `current_dir()` above, but handle it anyways
66  let parent = path
67    .parent()
68    .map(ToOwned::to_owned)
69    .ok_or_else(|| CodegenConfigError::Parent(path.into_owned()))?;
70
71  let target = std::env::var("TAURI_ENV_TARGET_TRIPLE")
72    .as_deref()
73    .map(Target::from_triple)
74    .unwrap_or_else(|_| Target::current());
75
76  // in the future we may want to find a way to not need the TAURI_CONFIG env var so that
77  // it is impossible for the content of two separate configs to get mixed up. The chances are
78  // already unlikely unless the developer goes out of their way to run the cli on a different
79  // project than the target crate.
80  let mut config =
81    serde_json::from_value(tauri_utils::config::parse::read_from(target, parent.clone())?.0)?;
82
83  if let Ok(env) = std::env::var("TAURI_CONFIG") {
84    let merge_config: serde_json::Value =
85      serde_json::from_str(&env).map_err(CodegenConfigError::FormatInline)?;
86    json_patch::merge(&mut config, &merge_config);
87  }
88
89  // Set working directory to where `tauri.config.json` is, so that relative paths in it are parsed correctly.
90  let old_cwd = std::env::current_dir().map_err(CodegenConfigError::CurrentDir)?;
91  std::env::set_current_dir(parent.clone()).map_err(CodegenConfigError::CurrentDir)?;
92
93  let config = serde_json::from_value(config)?;
94
95  // Reset working directory.
96  std::env::set_current_dir(old_cwd).map_err(CodegenConfigError::CurrentDir)?;
97
98  Ok((config, parent))
99}
100
101/// Create a blake3 checksum of the passed bytes.
102fn checksum(bytes: &[u8]) -> Result<String, fmt::Error> {
103  let mut hasher = vendor::blake3_reference::Hasher::default();
104  hasher.update(bytes);
105
106  let mut bytes = [0u8; 32];
107  hasher.finalize(&mut bytes);
108
109  let mut hex = String::with_capacity(2 * bytes.len());
110  for b in bytes {
111    write!(hex, "{b:02x}")?;
112  }
113  Ok(hex)
114}
115
116/// Cache the data to `$OUT_DIR`, only if it does not already exist.
117///
118/// Due to using a checksum as the filename, an existing file should be the exact same content
119/// as the data being checked.
120struct Cached {
121  checksum: String,
122}
123
124impl TryFrom<String> for Cached {
125  type Error = EmbeddedAssetsError;
126
127  fn try_from(value: String) -> Result<Self, Self::Error> {
128    Self::try_from(Vec::from(value))
129  }
130}
131
132impl TryFrom<Vec<u8>> for Cached {
133  type Error = EmbeddedAssetsError;
134
135  fn try_from(content: Vec<u8>) -> Result<Self, Self::Error> {
136    let checksum = checksum(content.as_ref()).map_err(EmbeddedAssetsError::Hex)?;
137    let path = ensure_out_dir()?.join(&checksum);
138
139    write_if_changed(&path, &content)
140      .map(|_| Self { checksum })
141      .map_err(|error| EmbeddedAssetsError::AssetWrite { path, error })
142  }
143}
144
145impl ToTokens for Cached {
146  fn to_tokens(&self, tokens: &mut TokenStream) {
147    let path = &self.checksum;
148    tokens.append_all(quote!(::std::concat!(::std::env!("OUT_DIR"), "/", #path)))
149  }
150}