Skip to main content

tauri_codegen/
context.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::collections::BTreeMap;
6use std::convert::identity;
7use std::path::{Path, PathBuf};
8use std::{ffi::OsStr, str::FromStr};
9
10use crate::{
11  embedded_assets::{
12    AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsResult, ensure_out_dir,
13  },
14  image::CachedIcon,
15};
16use base64::Engine;
17use proc_macro2::TokenStream;
18use quote::quote;
19use sha2::{Digest, Sha256};
20use syn::Expr;
21use tauri_utils::{
22  acl::{
23    ACL_MANIFESTS_FILE_NAME, CAPABILITIES_FILE_NAME, get_capabilities, manifest::Manifest,
24    resolved::Resolved,
25  },
26  assets::AssetKey,
27  config::{Config, FrontendDist, PatternKind},
28  html2::{Document, inject_nonce_token, parse_doc, serialize_doc},
29  platform::Target,
30  tokens::{map_lit, str_lit},
31};
32
33/// Necessary data needed by [`context_codegen`] to generate code for a Tauri application context.
34pub struct ContextData {
35  pub dev: bool,
36  pub config: Config,
37  pub config_parent: PathBuf,
38  pub root: TokenStream,
39  /// Additional capabilities to include.
40  pub capabilities: Option<Vec<PathBuf>>,
41  /// The custom assets implementation
42  pub assets: Option<Expr>,
43  /// Skip runtime-only types generation for tests (e.g. embed-plist usage).
44  pub test: bool,
45}
46
47fn inject_script_hashes(document: &Document, key: &AssetKey, csp_hashes: &mut CspHashes) {
48  let script_elements = document.select("script:not(:empty)");
49
50  let scripts = script_elements
51    .iter()
52    .map(|element| {
53      let script = tauri_utils::html2::normalize_script_for_csp(element.text().as_bytes());
54      let script_hash = Sha256::digest(script);
55      let hash_base64 = base64::engine::general_purpose::STANDARD.encode(script_hash);
56
57      format!("'sha256-{hash_base64}'")
58    })
59    .collect::<Vec<_>>();
60
61  csp_hashes
62    .inline_scripts
63    .entry(key.clone().into())
64    .or_default()
65    .extend(scripts);
66}
67
68fn map_core_assets(
69  options: &AssetOptions,
70) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> + use<> {
71  let csp = options.csp;
72  let dangerous_disable_asset_csp_modification =
73    options.dangerous_disable_asset_csp_modification.clone();
74  move |key, path, input, csp_hashes| {
75    if path.extension() == Some(OsStr::new("html")) {
76      #[allow(clippy::collapsible_if)]
77      if csp {
78        let document = parse_doc(String::from_utf8_lossy(input).into_owned());
79
80        inject_nonce_token(&document, &dangerous_disable_asset_csp_modification);
81
82        if dangerous_disable_asset_csp_modification.can_modify("script-src") {
83          inject_script_hashes(&document, key, csp_hashes);
84        }
85
86        *input = serialize_doc(&document);
87      }
88    }
89    Ok(())
90  }
91}
92
93#[cfg(feature = "isolation")]
94fn map_isolation(
95  _options: &AssetOptions,
96  dir: PathBuf,
97) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> + use<> {
98  // create the csp for the isolation iframe styling now, to make the runtime less complex
99  let mut hasher = Sha256::new();
100  hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
101  let hash = hasher.finalize();
102  let iframe_style_csp_hash = format!(
103    "'sha256-{}'",
104    base64::engine::general_purpose::STANDARD.encode(hash)
105  );
106
107  move |key, path, input, csp_hashes| {
108    if path.extension() == Some(OsStr::new("html")) {
109      let isolation_html = parse_doc(String::from_utf8_lossy(input).into_owned());
110
111      // this is appended, so no need to reverse order it
112      tauri_utils::html2::inject_codegen_isolation_script(&isolation_html);
113
114      // temporary workaround for windows not loading assets
115      tauri_utils::html2::inline_isolation(&isolation_html, &dir);
116
117      inject_nonce_token(
118        &isolation_html,
119        &tauri_utils::config::DisabledCspModificationKind::Flag(false),
120      );
121
122      inject_script_hashes(&isolation_html, key, csp_hashes);
123
124      csp_hashes.styles.push(iframe_style_csp_hash.clone());
125
126      *input = serialize_doc(&isolation_html)
127    }
128
129    Ok(())
130  }
131}
132
133/// Build a `tauri::Context` for including in application code.
134pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
135  let ContextData {
136    dev,
137    config,
138    config_parent,
139    root,
140    capabilities: additional_capabilities,
141    assets,
142    test,
143  } = data;
144
145  #[allow(unused_variables)]
146  let running_tests = test;
147
148  let target = std::env::var("TAURI_ENV_TARGET_TRIPLE")
149    .as_deref()
150    .map(Target::from_triple)
151    .unwrap_or_else(|_| Target::current());
152
153  let mut options = AssetOptions::new(config.app.security.pattern.clone())
154    .freeze_prototype(config.app.security.freeze_prototype)
155    .dangerous_disable_asset_csp_modification(
156      config
157        .app
158        .security
159        .dangerous_disable_asset_csp_modification
160        .clone(),
161    );
162  let csp = if dev {
163    config
164      .app
165      .security
166      .dev_csp
167      .as_ref()
168      .or(config.app.security.csp.as_ref())
169  } else {
170    config.app.security.csp.as_ref()
171  };
172  if csp.is_some() {
173    options = options.with_csp();
174  }
175
176  let assets = if let Some(assets) = assets {
177    quote!(#assets)
178  } else if dev && config.build.dev_url.is_some() {
179    let assets = EmbeddedAssets::default();
180    quote!(#assets)
181  } else {
182    let assets = match &config.build.frontend_dist {
183      Some(url) => match url {
184        FrontendDist::Url(_url) => Default::default(),
185        FrontendDist::Directory(path) => {
186          let assets_path = config_parent.join(path);
187          if !assets_path.exists() {
188            panic!(
189              "The `frontendDist` configuration is set to `{path:?}` but this path doesn't exist"
190            )
191          }
192          EmbeddedAssets::new(assets_path, &options, map_core_assets(&options))?
193        }
194        FrontendDist::Files(files) => EmbeddedAssets::new(
195          files
196            .iter()
197            .map(|p| config_parent.join(p))
198            .collect::<Vec<_>>(),
199          &options,
200          map_core_assets(&options),
201        )?,
202        _ => unimplemented!(),
203      },
204      None => Default::default(),
205    };
206    quote!(#assets)
207  };
208
209  let out_dir = ensure_out_dir()?;
210
211  let default_window_icon = {
212    if target == Target::Windows {
213      // handle default window icons for Windows targets
214      quote!(#root::image::default_window_icon_from_app_icon_resource())
215    } else {
216      // handle default window icons for Unix targets
217      let icon_path = find_icon(
218        &config,
219        &config_parent,
220        |i| i.ends_with(".png"),
221        "icons/icon.png",
222      );
223      let icon = CachedIcon::new(&root, &icon_path)?;
224      quote!(::std::option::Option::Some(#icon))
225    }
226  };
227
228  let app_icon = if target == Target::MacOS && dev {
229    let mut icon_path = find_icon(
230      &config,
231      &config_parent,
232      |i| i.ends_with(".icns"),
233      "icons/icon.png",
234    );
235    if !icon_path.exists() {
236      icon_path = find_icon(
237        &config,
238        &config_parent,
239        |i| i.ends_with(".png"),
240        "icons/icon.png",
241      );
242    }
243
244    let icon = CachedIcon::new_raw(&root, &icon_path)?;
245    quote!(::std::option::Option::Some(#icon.to_vec()))
246  } else {
247    quote!(::std::option::Option::None)
248  };
249
250  let package_name = if let Some(product_name) = &config.product_name {
251    quote!(#product_name.to_string())
252  } else {
253    quote!(env!("CARGO_PKG_NAME").to_string())
254  };
255  let package_version = if let Some(version) = &config.version {
256    semver::Version::from_str(version)?;
257    quote!(#version.to_string())
258  } else {
259    quote!(env!("CARGO_PKG_VERSION").to_string())
260  };
261  let package_info = quote!(
262    #root::PackageInfo {
263      name: #package_name,
264      version: #package_version.parse().unwrap(),
265      authors: env!("CARGO_PKG_AUTHORS"),
266      description: env!("CARGO_PKG_DESCRIPTION"),
267      crate_name: env!("CARGO_PKG_NAME"),
268    }
269  );
270
271  let with_tray_icon_code = if target.is_desktop() {
272    if let Some(tray) = &config.app.tray_icon {
273      let tray_icon_icon_path = config_parent.join(&tray.icon_path);
274      let icon = CachedIcon::new(&root, &tray_icon_icon_path)?;
275      quote!(context.set_tray_icon(::std::option::Option::Some(#icon));)
276    } else {
277      quote!()
278    }
279  } else {
280    quote!()
281  };
282
283  #[cfg(target_os = "macos")]
284  let maybe_embed_plist_block = if target == Target::MacOS && dev && !running_tests {
285    let info_plist_path = config_parent.join("Info.plist");
286    let mut info_plist = if info_plist_path.exists() {
287      plist::Value::from_file(&info_plist_path)
288        .unwrap_or_else(|e| panic!("failed to read plist {}: {}", info_plist_path.display(), e))
289    } else {
290      plist::Value::Dictionary(Default::default())
291    };
292
293    if let Some(plist) = info_plist.as_dictionary_mut() {
294      if let Some(bundle_name) = config
295        .bundle
296        .macos
297        .bundle_name
298        .as_ref()
299        .or(config.product_name.as_ref())
300      {
301        plist.insert("CFBundleName".into(), bundle_name.as_str().into());
302      }
303
304      if let Some(version) = &config.version {
305        let bundle_version = &config.bundle.macos.bundle_version;
306        plist.insert("CFBundleShortVersionString".into(), version.clone().into());
307        plist.insert(
308          "CFBundleVersion".into(),
309          bundle_version
310            .clone()
311            .unwrap_or_else(|| version.clone())
312            .into(),
313        );
314      }
315    }
316
317    let mut plist_contents = std::io::BufWriter::new(Vec::new());
318    info_plist
319      .to_writer_xml(&mut plist_contents)
320      .expect("failed to serialize plist");
321    let plist_contents =
322      String::from_utf8_lossy(&plist_contents.into_inner().unwrap()).into_owned();
323
324    let plist = crate::Cached::try_from(plist_contents)?;
325    quote!({
326      tauri::embed_plist::embed_info_plist!(#plist);
327    })
328  } else {
329    quote!()
330  };
331  #[cfg(not(target_os = "macos"))]
332  let maybe_embed_plist_block = quote!();
333
334  let pattern = match &options.pattern {
335    PatternKind::Brownfield => quote!(#root::Pattern::Brownfield),
336    #[cfg(not(feature = "isolation"))]
337    PatternKind::Isolation { dir: _ } => {
338      quote!(#root::Pattern::Brownfield)
339    }
340    #[cfg(feature = "isolation")]
341    PatternKind::Isolation { dir } => {
342      let dir = config_parent.join(dir);
343      if !dir.exists() {
344        panic!("The isolation application path is set to `{dir:?}` but it does not exist")
345      }
346
347      let mut sets_isolation_hook = false;
348
349      let key = uuid::Uuid::new_v4().to_string();
350      let map_isolation = map_isolation(&options, dir.clone());
351      let assets = EmbeddedAssets::new(dir, &options, |key, path, input, csp_hashes| {
352        // we check if `__TAURI_ISOLATION_HOOK__` exists in the isolation code
353        // before modifying the files since we inject our own `__TAURI_ISOLATION_HOOK__` reference in HTML files
354        if String::from_utf8_lossy(input).contains("__TAURI_ISOLATION_HOOK__") {
355          sets_isolation_hook = true;
356        }
357        map_isolation(key, path, input, csp_hashes)
358      })?;
359
360      if !sets_isolation_hook {
361        panic!(
362          "The isolation application does not contain a file setting the `window.__TAURI_ISOLATION_HOOK__` value."
363        );
364      }
365
366      let schema = options.isolation_schema.clone();
367
368      quote!(#root::Pattern::Isolation {
369        assets: ::std::sync::Arc::new(#assets),
370        schema: #schema.into(),
371        key: #key.into(),
372        crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
373      })
374    }
375  };
376
377  let acl_file_path = out_dir.join(ACL_MANIFESTS_FILE_NAME);
378  let acl: BTreeMap<String, Manifest> = if acl_file_path.exists() {
379    let acl_file =
380      std::fs::read_to_string(acl_file_path).expect("failed to read plugin manifest map");
381    serde_json::from_str(&acl_file).expect("failed to parse plugin manifest map")
382  } else {
383    Default::default()
384  };
385
386  let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
387  let capabilities_from_files = if capabilities_file_path.exists() {
388    let capabilities_json =
389      std::fs::read_to_string(&capabilities_file_path).expect("failed to read capabilities");
390    serde_json::from_str(&capabilities_json).expect("failed to parse capabilities")
391  } else {
392    Default::default()
393  };
394  let capabilities = get_capabilities(
395    &config,
396    capabilities_from_files,
397    additional_capabilities.as_deref(),
398  )
399  .unwrap();
400
401  let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
402
403  let acl_tokens = map_lit(
404    quote! { ::std::collections::BTreeMap },
405    &acl,
406    str_lit,
407    identity,
408  );
409
410  let runtime_authority = quote!(#root::runtime_authority!(#acl_tokens, #resolved));
411
412  let plugin_global_api_scripts = if config.app.with_global_tauri {
413    if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) {
414      let scripts = scripts.into_iter().map(|s| quote!(#s));
415      quote!(::std::option::Option::Some(&[#(#scripts),*]))
416    } else {
417      quote!(::std::option::Option::None)
418    }
419  } else {
420    quote!(::std::option::Option::None)
421  };
422
423  let maybe_config_parent_setter = if dev {
424    let config_parent = config_parent.to_string_lossy();
425    quote!({
426      context.with_config_parent(#config_parent);
427    })
428  } else {
429    quote!()
430  };
431
432  let context = quote!({
433    #maybe_embed_plist_block
434
435    #[allow(unused_mut, clippy::let_and_return)]
436    let mut context = #root::Context::new(
437      #config,
438      ::std::boxed::Box::new(assets),
439      #default_window_icon,
440      #app_icon,
441      #package_info,
442      #pattern,
443      #runtime_authority,
444      #plugin_global_api_scripts
445    );
446
447    #with_tray_icon_code
448    #maybe_config_parent_setter
449
450    context
451  });
452
453  // Wrapping in a function to make rust analyzer faster,
454  // see https://github.com/tauri-apps/tauri/pull/14457
455  // We take the assets as an argument so when the caller provides custom `assets` the closure
456  // does not capture from the caller's scope ("can't capture dynamic environment in a fn item").
457  let output = quote!({
458    fn inner<R: #root::Runtime, A: #root::Assets<R> + 'static>(assets: A) -> #root::Context<R> {
459      let thread = ::std::thread::Builder::new()
460        .name(String::from("generated tauri context creation"))
461        .stack_size(8 * 1024 * 1024)
462        .spawn(move || #context)
463        .expect("unable to create thread with 8MiB stack");
464
465      match thread.join() {
466        Ok(context) => context,
467        Err(_) => {
468          eprintln!("the generated Tauri `Context` panicked during creation");
469          ::std::process::exit(101);
470        }
471      }
472    }
473    inner(#assets)
474  });
475
476  Ok(output)
477}
478
479fn find_icon(
480  config: &Config,
481  config_parent: &Path,
482  predicate: impl Fn(&&String) -> bool,
483  default: &str,
484) -> PathBuf {
485  let icon_path = config
486    .bundle
487    .icon
488    .iter()
489    .find(predicate)
490    .map(AsRef::as_ref)
491    .unwrap_or(default);
492  config_parent.join(icon_path)
493}