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(&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(bundle_name) = config
313        .bundle
314        .macos
315        .bundle_name
316        .as_ref()
317        .or(config.product_name.as_ref())
318      {
319        plist.insert("CFBundleName".into(), bundle_name.as_str().into());
320      }
321
322      if let Some(version) = &config.version {
323        let bundle_version = &config.bundle.macos.bundle_version;
324        plist.insert("CFBundleShortVersionString".into(), version.clone().into());
325        plist.insert(
326          "CFBundleVersion".into(),
327          bundle_version
328            .clone()
329            .unwrap_or_else(|| version.clone())
330            .into(),
331        );
332      }
333    }
334
335    let mut plist_contents = std::io::BufWriter::new(Vec::new());
336    info_plist
337      .to_writer_xml(&mut plist_contents)
338      .expect("failed to serialize plist");
339    let plist_contents =
340      String::from_utf8_lossy(&plist_contents.into_inner().unwrap()).into_owned();
341
342    let plist = crate::Cached::try_from(plist_contents)?;
343    quote!({
344      tauri::embed_plist::embed_info_plist!(#plist);
345    })
346  } else {
347    quote!()
348  };
349  #[cfg(not(target_os = "macos"))]
350  let maybe_embed_plist_block = quote!();
351
352  let pattern = match &options.pattern {
353    PatternKind::Brownfield => quote!(#root::Pattern::Brownfield),
354    #[cfg(not(feature = "isolation"))]
355    PatternKind::Isolation { dir: _ } => {
356      quote!(#root::Pattern::Brownfield)
357    }
358    #[cfg(feature = "isolation")]
359    PatternKind::Isolation { dir } => {
360      let dir = config_parent.join(dir);
361      if !dir.exists() {
362        panic!("The isolation application path is set to `{dir:?}` but it does not exist")
363      }
364
365      let mut sets_isolation_hook = false;
366
367      let key = uuid::Uuid::new_v4().to_string();
368      let map_isolation = map_isolation(&options, dir.clone());
369      let assets = EmbeddedAssets::new(dir, &options, |key, path, input, csp_hashes| {
370        // we check if `__TAURI_ISOLATION_HOOK__` exists in the isolation code
371        // before modifying the files since we inject our own `__TAURI_ISOLATION_HOOK__` reference in HTML files
372        if String::from_utf8_lossy(input).contains("__TAURI_ISOLATION_HOOK__") {
373          sets_isolation_hook = true;
374        }
375        map_isolation(key, path, input, csp_hashes)
376      })?;
377
378      if !sets_isolation_hook {
379        panic!("The isolation application does not contain a file setting the `window.__TAURI_ISOLATION_HOOK__` value.");
380      }
381
382      let schema = options.isolation_schema;
383
384      quote!(#root::Pattern::Isolation {
385        assets: ::std::sync::Arc::new(#assets),
386        schema: #schema.into(),
387        key: #key.into(),
388        crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
389      })
390    }
391  };
392
393  let acl_file_path = out_dir.join(ACL_MANIFESTS_FILE_NAME);
394  let acl: BTreeMap<String, Manifest> = if acl_file_path.exists() {
395    let acl_file =
396      std::fs::read_to_string(acl_file_path).expect("failed to read plugin manifest map");
397    serde_json::from_str(&acl_file).expect("failed to parse plugin manifest map")
398  } else {
399    Default::default()
400  };
401
402  let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
403  let capabilities_from_files = if capabilities_file_path.exists() {
404    let capabilities_json =
405      std::fs::read_to_string(&capabilities_file_path).expect("failed to read capabilities");
406    serde_json::from_str(&capabilities_json).expect("failed to parse capabilities")
407  } else {
408    Default::default()
409  };
410  let capabilities = get_capabilities(
411    &config,
412    capabilities_from_files,
413    additional_capabilities.as_deref(),
414  )
415  .unwrap();
416
417  let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
418
419  let acl_tokens = map_lit(
420    quote! { ::std::collections::BTreeMap },
421    &acl,
422    str_lit,
423    identity,
424  );
425
426  let runtime_authority = quote!(#root::runtime_authority!(#acl_tokens, #resolved));
427
428  let plugin_global_api_scripts = if config.app.with_global_tauri {
429    if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) {
430      let scripts = scripts.into_iter().map(|s| quote!(#s));
431      quote!(::std::option::Option::Some(&[#(#scripts),*]))
432    } else {
433      quote!(::std::option::Option::None)
434    }
435  } else {
436    quote!(::std::option::Option::None)
437  };
438
439  let maybe_config_parent_setter = if dev {
440    let config_parent = config_parent.to_string_lossy();
441    quote!({
442      context.with_config_parent(#config_parent);
443    })
444  } else {
445    quote!()
446  };
447
448  let context = quote!({
449    #maybe_embed_plist_block
450
451    #[allow(unused_mut, clippy::let_and_return)]
452    let mut context = #root::Context::new(
453      #config,
454      ::std::boxed::Box::new(#assets),
455      #default_window_icon,
456      #app_icon,
457      #package_info,
458      #pattern,
459      #runtime_authority,
460      #plugin_global_api_scripts
461    );
462
463    #with_tray_icon_code
464    #maybe_config_parent_setter
465
466    context
467  });
468
469  Ok(quote!({
470    let thread = ::std::thread::Builder::new()
471      .name(String::from("generated tauri context creation"))
472      .stack_size(8 * 1024 * 1024)
473      .spawn(|| #context)
474      .expect("unable to create thread with 8MiB stack");
475
476    match thread.join() {
477      Ok(context) => context,
478      Err(_) => {
479        eprintln!("the generated Tauri `Context` panicked during creation");
480        ::std::process::exit(101);
481      }
482    }
483  }))
484}
485
486fn find_icon(
487  config: &Config,
488  config_parent: &Path,
489  predicate: impl Fn(&&String) -> bool,
490  default: &str,
491) -> PathBuf {
492  let icon_path = config
493    .bundle
494    .icon
495    .iter()
496    .find(predicate)
497    .map(AsRef::as_ref)
498    .unwrap_or(default);
499  config_parent.join(icon_path)
500}