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    ensure_out_dir, AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsResult,
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::acl::{ACL_MANIFESTS_FILE_NAME, CAPABILITIES_FILE_NAME};
22use tauri_utils::{
23  acl::capability::{Capability, CapabilityFile},
24  acl::manifest::Manifest,
25  acl::resolved::Resolved,
26  assets::AssetKey,
27  config::{CapabilityEntry, Config, FrontendDist, PatternKind},
28  html::{inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef},
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: &NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) {
48  if let Ok(inline_script_elements) = document.select("script:not(empty)") {
49    let mut scripts = Vec::new();
50    for inline_script_el in inline_script_elements {
51      let script = inline_script_el.as_node().text_contents();
52      let mut hasher = Sha256::new();
53      hasher.update(&script);
54      let hash = hasher.finalize();
55      scripts.push(format!(
56        "'sha256-{}'",
57        base64::engine::general_purpose::STANDARD.encode(hash)
58      ));
59    }
60    csp_hashes
61      .inline_scripts
62      .entry(key.clone().into())
63      .or_default()
64      .append(&mut scripts);
65  }
66}
67
68fn map_core_assets(
69  options: &AssetOptions,
70) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> {
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_html(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_html_node(&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<()> {
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_html(String::from_utf8_lossy(input).into_owned());
110
111      // this is appended, so no need to reverse order it
112      tauri_utils::html::inject_codegen_isolation_script(&isolation_html);
113
114      // temporary workaround for windows not loading assets
115      tauri_utils::html::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 = isolation_html.to_string().as_bytes().to_vec()
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      let icon_path = find_icon(
215        &config,
216        &config_parent,
217        |i| i.ends_with(".ico"),
218        "icons/icon.ico",
219      );
220      if icon_path.exists() {
221        let icon = CachedIcon::new(&root, &icon_path)?;
222        quote!(::std::option::Option::Some(#icon))
223      } else {
224        let icon_path = find_icon(
225          &config,
226          &config_parent,
227          |i| i.ends_with(".png"),
228          "icons/icon.png",
229        );
230        let icon = CachedIcon::new(&root, &icon_path)?;
231        quote!(::std::option::Option::Some(#icon))
232      }
233    } else {
234      // handle default window icons for Unix targets
235      let icon_path = find_icon(
236        &config,
237        &config_parent,
238        |i| i.ends_with(".png"),
239        "icons/icon.png",
240      );
241      let icon = CachedIcon::new(&root, &icon_path)?;
242      quote!(::std::option::Option::Some(#icon))
243    }
244  };
245
246  let app_icon = if target == Target::MacOS && dev {
247    let mut icon_path = find_icon(
248      &config,
249      &config_parent,
250      |i| i.ends_with(".icns"),
251      "icons/icon.png",
252    );
253    if !icon_path.exists() {
254      icon_path = find_icon(
255        &config,
256        &config_parent,
257        |i| i.ends_with(".png"),
258        "icons/icon.png",
259      );
260    }
261
262    let icon = CachedIcon::new_raw(&root, &icon_path)?;
263    quote!(::std::option::Option::Some(#icon.to_vec()))
264  } else {
265    quote!(::std::option::Option::None)
266  };
267
268  let package_name = if let Some(product_name) = &config.product_name {
269    quote!(#product_name.to_string())
270  } else {
271    quote!(env!("CARGO_PKG_NAME").to_string())
272  };
273  let package_version = if let Some(version) = &config.version {
274    semver::Version::from_str(version)?;
275    quote!(#version.to_string())
276  } else {
277    quote!(env!("CARGO_PKG_VERSION").to_string())
278  };
279  let package_info = quote!(
280    #root::PackageInfo {
281      name: #package_name,
282      version: #package_version.parse().unwrap(),
283      authors: env!("CARGO_PKG_AUTHORS"),
284      description: env!("CARGO_PKG_DESCRIPTION"),
285      crate_name: env!("CARGO_PKG_NAME"),
286    }
287  );
288
289  let with_tray_icon_code = if target.is_desktop() {
290    if let Some(tray) = &config.app.tray_icon {
291      let tray_icon_icon_path = config_parent.join(&tray.icon_path);
292      let icon = CachedIcon::new(&root, &tray_icon_icon_path)?;
293      quote!(context.set_tray_icon(::std::option::Option::Some(#icon));)
294    } else {
295      quote!()
296    }
297  } else {
298    quote!()
299  };
300
301  #[cfg(target_os = "macos")]
302  let maybe_embed_plist_block = if target == Target::MacOS && dev && !running_tests {
303    let info_plist_path = config_parent.join("Info.plist");
304    let mut info_plist = if info_plist_path.exists() {
305      plist::Value::from_file(&info_plist_path)
306        .unwrap_or_else(|e| panic!("failed to read plist {}: {}", info_plist_path.display(), e))
307    } else {
308      plist::Value::Dictionary(Default::default())
309    };
310
311    if let Some(plist) = info_plist.as_dictionary_mut() {
312      if let Some(product_name) = &config.product_name {
313        plist.insert("CFBundleName".into(), product_name.clone().into());
314      }
315      if let Some(version) = &config.version {
316        plist.insert("CFBundleShortVersionString".into(), version.clone().into());
317        plist.insert("CFBundleVersion".into(), version.clone().into());
318      }
319    }
320
321    let mut plist_contents = std::io::BufWriter::new(Vec::new());
322    info_plist
323      .to_writer_xml(&mut plist_contents)
324      .expect("failed to serialize plist");
325    let plist_contents =
326      String::from_utf8_lossy(&plist_contents.into_inner().unwrap()).into_owned();
327
328    let plist = crate::Cached::try_from(plist_contents)?;
329    quote!({
330      tauri::embed_plist::embed_info_plist!(#plist);
331    })
332  } else {
333    quote!()
334  };
335  #[cfg(not(target_os = "macos"))]
336  let maybe_embed_plist_block = quote!();
337
338  let pattern = match &options.pattern {
339    PatternKind::Brownfield => quote!(#root::Pattern::Brownfield),
340    #[cfg(not(feature = "isolation"))]
341    PatternKind::Isolation { dir: _ } => {
342      quote!(#root::Pattern::Brownfield)
343    }
344    #[cfg(feature = "isolation")]
345    PatternKind::Isolation { dir } => {
346      let dir = config_parent.join(dir);
347      if !dir.exists() {
348        panic!("The isolation application path is set to `{dir:?}` but it does not exist")
349      }
350
351      let mut sets_isolation_hook = false;
352
353      let key = uuid::Uuid::new_v4().to_string();
354      let map_isolation = map_isolation(&options, dir.clone());
355      let assets = EmbeddedAssets::new(dir, &options, |key, path, input, csp_hashes| {
356        // we check if `__TAURI_ISOLATION_HOOK__` exists in the isolation code
357        // before modifying the files since we inject our own `__TAURI_ISOLATION_HOOK__` reference in HTML files
358        if String::from_utf8_lossy(input).contains("__TAURI_ISOLATION_HOOK__") {
359          sets_isolation_hook = true;
360        }
361        map_isolation(key, path, input, csp_hashes)
362      })?;
363
364      if !sets_isolation_hook {
365        panic!("The isolation application does not contain a file setting the `window.__TAURI_ISOLATION_HOOK__` value.");
366      }
367
368      let schema = options.isolation_schema;
369
370      quote!(#root::Pattern::Isolation {
371        assets: ::std::sync::Arc::new(#assets),
372        schema: #schema.into(),
373        key: #key.into(),
374        crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
375      })
376    }
377  };
378
379  let acl_file_path = out_dir.join(ACL_MANIFESTS_FILE_NAME);
380  let acl: BTreeMap<String, Manifest> = if acl_file_path.exists() {
381    let acl_file =
382      std::fs::read_to_string(acl_file_path).expect("failed to read plugin manifest map");
383    serde_json::from_str(&acl_file).expect("failed to parse plugin manifest map")
384  } else {
385    Default::default()
386  };
387
388  let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
389  let mut capabilities_from_files: BTreeMap<String, Capability> = if capabilities_file_path.exists()
390  {
391    let capabilities_file =
392      std::fs::read_to_string(capabilities_file_path).expect("failed to read capabilities");
393    serde_json::from_str(&capabilities_file).expect("failed to parse capabilities")
394  } else {
395    Default::default()
396  };
397
398  let mut capabilities = if config.app.security.capabilities.is_empty() {
399    capabilities_from_files
400  } else {
401    let mut capabilities = BTreeMap::new();
402    for capability_entry in &config.app.security.capabilities {
403      match capability_entry {
404        CapabilityEntry::Inlined(capability) => {
405          capabilities.insert(capability.identifier.clone(), capability.clone());
406        }
407        CapabilityEntry::Reference(id) => {
408          let capability = capabilities_from_files
409            .remove(id)
410            .unwrap_or_else(|| panic!("capability with identifier {id} not found"));
411          capabilities.insert(id.clone(), capability);
412        }
413      }
414    }
415    capabilities
416  };
417
418  let acl_tokens = map_lit(
419    quote! { ::std::collections::BTreeMap },
420    &acl,
421    str_lit,
422    identity,
423  );
424
425  if let Some(paths) = additional_capabilities {
426    for path in paths {
427      let capability = CapabilityFile::load(&path)
428        .unwrap_or_else(|e| panic!("failed to read capability {}: {e}", path.display()));
429      match capability {
430        CapabilityFile::Capability(c) => {
431          capabilities.insert(c.identifier.clone(), c);
432        }
433        CapabilityFile::List(capabilities_list)
434        | CapabilityFile::NamedList {
435          capabilities: capabilities_list,
436        } => {
437          capabilities.extend(
438            capabilities_list
439              .into_iter()
440              .map(|c| (c.identifier.clone(), c)),
441          );
442        }
443      }
444    }
445  }
446
447  let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
448  let runtime_authority = quote!(#root::ipc::RuntimeAuthority::new(#acl_tokens, #resolved));
449
450  let plugin_global_api_scripts = if config.app.with_global_tauri {
451    if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) {
452      let scripts = scripts.into_iter().map(|s| quote!(#s));
453      quote!(::std::option::Option::Some(&[#(#scripts),*]))
454    } else {
455      quote!(::std::option::Option::None)
456    }
457  } else {
458    quote!(::std::option::Option::None)
459  };
460
461  let maybe_config_parent_setter = if dev {
462    let config_parent = config_parent.to_string_lossy();
463    quote!({
464      context.with_config_parent(#config_parent);
465    })
466  } else {
467    quote!()
468  };
469
470  let context = quote!({
471    #maybe_embed_plist_block
472
473    #[allow(unused_mut, clippy::let_and_return)]
474    let mut context = #root::Context::new(
475      #config,
476      ::std::boxed::Box::new(#assets),
477      #default_window_icon,
478      #app_icon,
479      #package_info,
480      #pattern,
481      #runtime_authority,
482      #plugin_global_api_scripts
483    );
484
485    #with_tray_icon_code
486    #maybe_config_parent_setter
487
488    context
489  });
490
491  Ok(quote!({
492    let thread = ::std::thread::Builder::new()
493      .name(String::from("generated tauri context creation"))
494      .stack_size(8 * 1024 * 1024)
495      .spawn(|| #context)
496      .expect("unable to create thread with 8MiB stack");
497
498    match thread.join() {
499      Ok(context) => context,
500      Err(_) => {
501        eprintln!("the generated Tauri `Context` panicked during creation");
502        ::std::process::exit(101);
503      }
504    }
505  }))
506}
507
508fn find_icon(
509  config: &Config,
510  config_parent: &Path,
511  predicate: impl Fn(&&String) -> bool,
512  default: &str,
513) -> PathBuf {
514  let icon_path = config
515    .bundle
516    .icon
517    .iter()
518    .find(predicate)
519    .map(AsRef::as_ref)
520    .unwrap_or(default);
521  config_parent.join(icon_path)
522}