Skip to main content

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::{ContextData, context_codegen};
14use crate::embedded_assets::{EmbeddedAssetsError, ensure_out_dir};
15use proc_macro2::TokenStream;
16use quote::{ToTokens, TokenStreamExt, quote};
17use std::{
18  borrow::Cow,
19  fmt::{self, Write},
20  path::{Path, PathBuf},
21  time::{Duration, SystemTime},
22};
23pub use tauri_utils::config::{Config, parse::ConfigError};
24use tauri_utils::platform::Target;
25use tauri_utils::write_if_changed;
26
27mod context;
28pub mod embedded_assets;
29pub mod image;
30#[doc(hidden)]
31pub mod vendor;
32
33/// Represents all the errors that can happen while reading the config during codegen.
34#[derive(Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum CodegenConfigError {
37  #[error("unable to access current working directory: {0}")]
38  CurrentDir(std::io::Error),
39
40  // this error should be "impossible" because we use std::env::current_dir() - cover it anyways
41  #[error(
42    "Tauri config file has no parent, this shouldn't be possible. file an issue on https://github.com/tauri-apps/tauri - target {0}"
43  )]
44  Parent(PathBuf),
45
46  #[error("unable to parse inline JSON TAURI_CONFIG env var: {0}")]
47  FormatInline(serde_json::Error),
48
49  #[error(transparent)]
50  Json(#[from] serde_json::Error),
51
52  #[error("{0}")]
53  ConfigError(#[from] ConfigError),
54}
55
56/// Get the [`Config`] from the `TAURI_CONFIG` environmental variable, or read from the passed path.
57///
58/// If the passed path is relative, it should be relative to the current working directory of the
59/// compiling crate.
60pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError> {
61  let path = if path.is_relative() {
62    let cwd = std::env::current_dir().map_err(CodegenConfigError::CurrentDir)?;
63    Cow::Owned(cwd.join(path))
64  } else {
65    Cow::Borrowed(path)
66  };
67
68  // this should be impossible because of the use of `current_dir()` above, but handle it anyways
69  let parent = path
70    .parent()
71    .map(ToOwned::to_owned)
72    .ok_or_else(|| CodegenConfigError::Parent(path.into_owned()))?;
73
74  let target = std::env::var("TAURI_ENV_TARGET_TRIPLE")
75    .as_deref()
76    .map(Target::from_triple)
77    .unwrap_or_else(|_| Target::current());
78
79  // in the future we may want to find a way to not need the TAURI_CONFIG env var so that
80  // it is impossible for the content of two separate configs to get mixed up. The chances are
81  // already unlikely unless the developer goes out of their way to run the cli on a different
82  // project than the target crate.
83  let mut config =
84    serde_json::from_value(tauri_utils::config::parse::read_from(target, &parent)?.0)?;
85
86  if let Ok(env) = std::env::var("TAURI_CONFIG") {
87    let merge_config: serde_json::Value =
88      serde_json::from_str(&env).map_err(CodegenConfigError::FormatInline)?;
89    json_patch::merge(&mut config, &merge_config);
90  }
91
92  // Set working directory to where `tauri.config.json` is, so that relative paths in it are parsed correctly.
93  let old_cwd = std::env::current_dir().map_err(CodegenConfigError::CurrentDir)?;
94  std::env::set_current_dir(parent.clone()).map_err(CodegenConfigError::CurrentDir)?;
95
96  let config = serde_json::from_value(config)?;
97
98  // Reset working directory.
99  std::env::set_current_dir(old_cwd).map_err(CodegenConfigError::CurrentDir)?;
100
101  Ok((config, parent))
102}
103
104/// Create a blake3 checksum of the passed bytes.
105fn checksum(bytes: &[u8]) -> Result<String, fmt::Error> {
106  let mut hasher = vendor::blake3_reference::Hasher::default();
107  hasher.update(bytes);
108
109  let mut bytes = [0u8; 32];
110  hasher.finalize(&mut bytes);
111
112  let mut hex = String::with_capacity(2 * bytes.len());
113  for b in bytes {
114    write!(hex, "{b:02x}")?;
115  }
116  Ok(hex)
117}
118
119/// Set the modification time of a file written to `$OUT_DIR` to 2000-01-01 UTC.
120///
121/// The generated code embeds these files with `include_bytes!`, so Cargo checks them for changes
122/// like source files: it treats a file modified after the compilation started as changed, and the
123/// macro writes the file during the compilation. Without this, the next build recompiles the crate.
124/// Each file name is a checksum of the data the file was generated from, so the modification time
125/// isn't needed to tell versions apart. Unlike the Unix epoch, the date fits FAT and exFAT. Errors
126/// are ignored, since at worst the next build recompiles the crate as before.
127fn backdate(path: &Path) {
128  let _ = std::fs::File::options()
129    .write(true)
130    .open(path)
131    .and_then(|file| file.set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(946_684_800)));
132}
133
134/// Cache the data to `$OUT_DIR`, only if it does not already exist.
135///
136/// Due to using a checksum as the filename, an existing file should be the exact same content
137/// as the data being checked.
138struct Cached {
139  checksum: String,
140}
141
142impl TryFrom<String> for Cached {
143  type Error = EmbeddedAssetsError;
144
145  fn try_from(value: String) -> Result<Self, Self::Error> {
146    Self::try_from(Vec::from(value))
147  }
148}
149
150impl TryFrom<Vec<u8>> for Cached {
151  type Error = EmbeddedAssetsError;
152
153  fn try_from(content: Vec<u8>) -> Result<Self, Self::Error> {
154    let checksum = checksum(content.as_ref()).map_err(EmbeddedAssetsError::Hex)?;
155    let path = ensure_out_dir()?.join(&checksum);
156
157    write_if_changed(&path, &content)
158      .map(|_| {
159        backdate(&path);
160        Self { checksum }
161      })
162      .map_err(|error| EmbeddedAssetsError::AssetWrite { path, error })
163  }
164}
165
166impl ToTokens for Cached {
167  fn to_tokens(&self, tokens: &mut TokenStream) {
168    let path = &self.checksum;
169    tokens.append_all(quote!(::std::concat!(::std::env!("OUT_DIR"), "/", #path)))
170  }
171}
172
173#[cfg(test)]
174mod tests {
175  use crate::{
176    Cached,
177    embedded_assets::{AssetOptions, EmbeddedAssets},
178  };
179  use std::time::{Duration, SystemTime};
180  use tauri_utils::config::PatternKind;
181
182  #[test]
183  fn out_dir_files_are_backdated() {
184    let dir = std::env::temp_dir().join(format!("tauri-codegen-backdate-{}", std::process::id()));
185    let _ = std::fs::remove_dir_all(&dir);
186    let out_dir = dir.join("out");
187    let dist = dir.join("dist");
188    std::fs::create_dir_all(&out_dir).unwrap();
189    std::fs::create_dir_all(&dist).unwrap();
190    std::fs::write(dist.join("index.html"), "<html></html>").unwrap();
191    // SAFETY: no other thread in this test binary accesses the environment outside `std::env`;
192    // the other tests only process in-memory icons
193    unsafe { std::env::set_var("OUT_DIR", &out_dir) };
194
195    let cached = Cached::try_from(vec![1, 2, 3]).unwrap();
196    EmbeddedAssets::new(
197      dist,
198      &AssetOptions::new(PatternKind::Brownfield),
199      |_, _, _, _| Ok(()),
200    )
201    .unwrap();
202
203    let mut files = vec![out_dir.join(&cached.checksum)];
204    for entry in std::fs::read_dir(out_dir.join("tauri-codegen-assets")).unwrap() {
205      files.push(entry.unwrap().path());
206    }
207    let modified: Vec<_> = files
208      .iter()
209      .map(|file| std::fs::metadata(file).unwrap().modified().unwrap())
210      .collect();
211
212    // unset `OUT_DIR` and remove the test files before the assertions, so a failed one leaves
213    // nothing behind
214    // SAFETY: see above
215    unsafe { std::env::remove_var("OUT_DIR") };
216    std::fs::remove_dir_all(&dir).unwrap();
217
218    // 2000-01-01 UTC
219    let expected = SystemTime::UNIX_EPOCH + Duration::from_secs(946_684_800);
220    assert_eq!(files.len(), 2);
221    for (file, modified) in files.iter().zip(modified) {
222      assert_eq!(modified, expected, "{}", file.display());
223    }
224  }
225}