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