lib.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. #![cfg_attr(doc_cfg, feature(doc_cfg))]
  5. pub use anyhow::Result;
  6. use heck::AsShoutySnakeCase;
  7. use tauri_utils::resources::{external_binaries, resource_relpath, ResourcePaths};
  8. use std::path::{Path, PathBuf};
  9. #[cfg(feature = "codegen")]
  10. mod codegen;
  11. #[cfg(windows)]
  12. mod static_vcruntime;
  13. #[cfg(feature = "codegen")]
  14. #[cfg_attr(doc_cfg, doc(cfg(feature = "codegen")))]
  15. pub use codegen::context::CodegenContext;
  16. fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
  17. let from = from.as_ref();
  18. let to = to.as_ref();
  19. if !from.exists() {
  20. return Err(anyhow::anyhow!("{:?} does not exist", from));
  21. }
  22. if !from.is_file() {
  23. return Err(anyhow::anyhow!("{:?} is not a file", from));
  24. }
  25. let dest_dir = to.parent().expect("No data in parent");
  26. std::fs::create_dir_all(dest_dir)?;
  27. std::fs::copy(from, to)?;
  28. Ok(())
  29. }
  30. fn copy_binaries<'a>(
  31. binaries: ResourcePaths<'a>,
  32. target_triple: &str,
  33. path: &Path,
  34. package_name: Option<&String>,
  35. ) -> Result<()> {
  36. for src in binaries {
  37. let src = src?;
  38. println!("cargo:rerun-if-changed={}", src.display());
  39. let file_name = src
  40. .file_name()
  41. .expect("failed to extract external binary filename")
  42. .to_string_lossy()
  43. .replace(&format!("-{}", target_triple), "");
  44. if package_name.map_or(false, |n| n == &file_name) {
  45. return Err(anyhow::anyhow!(
  46. "Cannot define a sidecar with the same name as the Cargo package name `{}`. Please change the sidecar name in the filesystem and the Tauri configuration.",
  47. file_name
  48. ));
  49. }
  50. let dest = path.join(file_name);
  51. if dest.exists() {
  52. std::fs::remove_file(&dest).unwrap();
  53. }
  54. copy_file(&src, &dest)?;
  55. }
  56. Ok(())
  57. }
  58. /// Copies resources to a path.
  59. fn copy_resources(resources: ResourcePaths<'_>, path: &Path) -> Result<()> {
  60. for src in resources {
  61. let src = src?;
  62. println!("cargo:rerun-if-changed={}", src.display());
  63. let dest = path.join(resource_relpath(&src));
  64. copy_file(&src, &dest)?;
  65. }
  66. Ok(())
  67. }
  68. // checks if the given Cargo feature is enabled.
  69. fn has_feature(feature: &str) -> bool {
  70. // when a feature is enabled, Cargo sets the `CARGO_FEATURE_<name` env var to 1
  71. // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts
  72. std::env::var(format!("CARGO_FEATURE_{}", AsShoutySnakeCase(feature)))
  73. .map(|x| x == "1")
  74. .unwrap_or(false)
  75. }
  76. // creates a cfg alias if `has_feature` is true.
  77. // `alias` must be a snake case string.
  78. fn cfg_alias(alias: &str, has_feature: bool) {
  79. if has_feature {
  80. println!("cargo:rustc-cfg={}", alias);
  81. }
  82. }
  83. /// Attributes used on Windows.
  84. #[allow(dead_code)]
  85. #[derive(Debug, Default)]
  86. pub struct WindowsAttributes {
  87. window_icon_path: Option<PathBuf>,
  88. /// The path to the sdk location.
  89. ///
  90. /// For the GNU toolkit this has to be the path where MinGW put windres.exe and ar.exe.
  91. /// This could be something like: "C:\Program Files\mingw-w64\x86_64-5.3.0-win32-seh-rt_v4-rev0\mingw64\bin"
  92. ///
  93. /// For MSVC the Windows SDK has to be installed. It comes with the resource compiler rc.exe.
  94. /// This should be set to the root directory of the Windows SDK, e.g., "C:\Program Files (x86)\Windows Kits\10" or,
  95. /// if multiple 10 versions are installed, set it directly to the corret bin directory "C:\Program Files (x86)\Windows Kits\10\bin\10.0.14393.0\x64"
  96. ///
  97. /// If it is left unset, it will look up a path in the registry, i.e. HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots
  98. sdk_dir: Option<PathBuf>,
  99. }
  100. impl WindowsAttributes {
  101. /// Creates the default attribute set.
  102. pub fn new() -> Self {
  103. Self::default()
  104. }
  105. /// Sets the icon to use on the window. Currently only used on Windows.
  106. /// It must be in `ico` format. Defaults to `icons/icon.ico`.
  107. #[must_use]
  108. pub fn window_icon_path<P: AsRef<Path>>(mut self, window_icon_path: P) -> Self {
  109. self
  110. .window_icon_path
  111. .replace(window_icon_path.as_ref().into());
  112. self
  113. }
  114. /// Sets the sdk dir for windows. Currently only used on Windows. This must be a valid UTF-8
  115. /// path. Defaults to whatever the `winres` crate determines is best.
  116. #[must_use]
  117. pub fn sdk_dir<P: AsRef<Path>>(mut self, sdk_dir: P) -> Self {
  118. self.sdk_dir = Some(sdk_dir.as_ref().into());
  119. self
  120. }
  121. }
  122. /// The attributes used on the build.
  123. #[derive(Debug, Default)]
  124. pub struct Attributes {
  125. #[allow(dead_code)]
  126. windows_attributes: WindowsAttributes,
  127. }
  128. impl Attributes {
  129. /// Creates the default attribute set.
  130. pub fn new() -> Self {
  131. Self::default()
  132. }
  133. /// Sets the icon to use on the window. Currently only used on Windows.
  134. #[must_use]
  135. pub fn windows_attributes(mut self, windows_attributes: WindowsAttributes) -> Self {
  136. self.windows_attributes = windows_attributes;
  137. self
  138. }
  139. }
  140. /// Run all build time helpers for your Tauri Application.
  141. ///
  142. /// The current helpers include the following:
  143. /// * Generates a Windows Resource file when targeting Windows.
  144. ///
  145. /// # Platforms
  146. ///
  147. /// [`build()`] should be called inside of `build.rs` regardless of the platform:
  148. /// * New helpers may target more platforms in the future.
  149. /// * Platform specific code is handled by the helpers automatically.
  150. /// * A build script is required in order to activate some cargo environmental variables that are
  151. /// used when generating code and embedding assets - so [`build()`] may as well be called.
  152. ///
  153. /// In short, this is saying don't put the call to [`build()`] behind a `#[cfg(windows)]`.
  154. ///
  155. /// # Panics
  156. ///
  157. /// If any of the build time helpers fail, they will [`std::panic!`] with the related error message.
  158. /// This is typically desirable when running inside a build script; see [`try_build`] for no panics.
  159. pub fn build() {
  160. if let Err(error) = try_build(Attributes::default()) {
  161. let error = format!("{:#}", error);
  162. println!("{}", error);
  163. if error.starts_with("unknown field") {
  164. print!("found an unknown configuration field. This usually means that you are using a CLI version that is newer than `tauri-build` and is incompatible. ");
  165. println!(
  166. "Please try updating the Rust crates by running `cargo update` in the Tauri app folder."
  167. );
  168. }
  169. std::process::exit(1);
  170. }
  171. }
  172. /// Non-panicking [`build()`].
  173. #[allow(unused_variables)]
  174. pub fn try_build(attributes: Attributes) -> Result<()> {
  175. use anyhow::anyhow;
  176. use cargo_toml::{Dependency, Manifest};
  177. use tauri_utils::config::{Config, TauriConfig};
  178. println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
  179. println!("cargo:rerun-if-changed=tauri.conf.json");
  180. #[cfg(feature = "config-json5")]
  181. println!("cargo:rerun-if-changed=tauri.conf.json5");
  182. let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
  183. let mobile = target_os == "ios" || target_os == "android";
  184. cfg_alias("desktop", !mobile);
  185. cfg_alias("mobile", mobile);
  186. let mut config = serde_json::from_value(tauri_utils::config::parse::read_from(
  187. std::env::current_dir().unwrap(),
  188. )?)?;
  189. if let Ok(env) = std::env::var("TAURI_CONFIG") {
  190. let merge_config: serde_json::Value = serde_json::from_str(&env)?;
  191. json_patch::merge(&mut config, &merge_config);
  192. }
  193. let config: Config = serde_json::from_value(config)?;
  194. cfg_alias("dev", !has_feature("custom-protocol"));
  195. let mut manifest = Manifest::from_path("Cargo.toml")?;
  196. if let Some(tauri) = manifest.dependencies.remove("tauri") {
  197. let features = match tauri {
  198. Dependency::Simple(_) => Vec::new(),
  199. Dependency::Detailed(dep) => dep.features,
  200. };
  201. let all_cli_managed_features = TauriConfig::all_features();
  202. let diff = features_diff(
  203. &features
  204. .into_iter()
  205. .filter(|f| all_cli_managed_features.contains(&f.as_str()))
  206. .collect::<Vec<String>>(),
  207. &config
  208. .tauri
  209. .features()
  210. .into_iter()
  211. .map(|f| f.to_string())
  212. .collect::<Vec<String>>(),
  213. );
  214. let mut error_message = String::new();
  215. if !diff.remove.is_empty() {
  216. error_message.push_str("remove the `");
  217. error_message.push_str(&diff.remove.join(", "));
  218. error_message.push_str(if diff.remove.len() == 1 {
  219. "` feature"
  220. } else {
  221. "` features"
  222. });
  223. if !diff.add.is_empty() {
  224. error_message.push_str(" and ");
  225. }
  226. }
  227. if !diff.add.is_empty() {
  228. error_message.push_str("add the `");
  229. error_message.push_str(&diff.add.join(", "));
  230. error_message.push_str(if diff.add.len() == 1 {
  231. "` feature"
  232. } else {
  233. "` features"
  234. });
  235. }
  236. if !error_message.is_empty() {
  237. return Err(anyhow!("
  238. The `tauri` dependency features on the `Cargo.toml` file does not match the allowlist defined under `tauri.conf.json`.
  239. Please run `tauri dev` or `tauri build` or {}.
  240. ", error_message));
  241. }
  242. }
  243. let target_triple = std::env::var("TARGET").unwrap();
  244. let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
  245. // TODO: far from ideal, but there's no other way to get the target dir, see <https://github.com/rust-lang/cargo/issues/5457>
  246. let target_dir = out_dir
  247. .parent()
  248. .unwrap()
  249. .parent()
  250. .unwrap()
  251. .parent()
  252. .unwrap();
  253. if let Some(paths) = &config.tauri.bundle.external_bin {
  254. copy_binaries(
  255. ResourcePaths::new(external_binaries(paths, &target_triple).as_slice(), true),
  256. &target_triple,
  257. target_dir,
  258. manifest.package.as_ref().map(|p| &p.name),
  259. )?;
  260. }
  261. #[allow(unused_mut, clippy::redundant_clone)]
  262. let mut resources = config.tauri.bundle.resources.clone().unwrap_or_default();
  263. #[cfg(windows)]
  264. if let Some(fixed_webview2_runtime_path) = &config.tauri.bundle.windows.webview_fixed_runtime_path
  265. {
  266. resources.push(fixed_webview2_runtime_path.display().to_string());
  267. }
  268. copy_resources(ResourcePaths::new(resources.as_slice(), true), target_dir)?;
  269. #[cfg(target_os = "macos")]
  270. {
  271. if let Some(version) = config.tauri.bundle.macos.minimum_system_version {
  272. println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={}", version);
  273. }
  274. }
  275. #[cfg(windows)]
  276. {
  277. use anyhow::Context;
  278. use semver::Version;
  279. use winres::{VersionInfo, WindowsResource};
  280. fn find_icon<F: Fn(&&String) -> bool>(config: &Config, predicate: F, default: &str) -> PathBuf {
  281. let icon_path = config
  282. .tauri
  283. .bundle
  284. .icon
  285. .iter()
  286. .find(|i| predicate(i))
  287. .cloned()
  288. .unwrap_or_else(|| default.to_string());
  289. icon_path.into()
  290. }
  291. let window_icon_path = attributes
  292. .windows_attributes
  293. .window_icon_path
  294. .unwrap_or_else(|| find_icon(&config, |i| i.ends_with(".ico"), "icons/icon.ico"));
  295. if window_icon_path.exists() {
  296. let mut res = WindowsResource::new();
  297. if let Some(sdk_dir) = &attributes.windows_attributes.sdk_dir {
  298. if let Some(sdk_dir_str) = sdk_dir.to_str() {
  299. res.set_toolkit_path(sdk_dir_str);
  300. } else {
  301. return Err(anyhow!(
  302. "sdk_dir path is not valid; only UTF-8 characters are allowed"
  303. ));
  304. }
  305. }
  306. if let Some(version) = &config.package.version {
  307. if let Ok(v) = Version::parse(version) {
  308. let version = v.major << 48 | v.minor << 32 | v.patch << 16;
  309. res.set_version_info(VersionInfo::FILEVERSION, version);
  310. res.set_version_info(VersionInfo::PRODUCTVERSION, version);
  311. }
  312. res.set("FileVersion", version);
  313. res.set("ProductVersion", version);
  314. }
  315. if let Some(product_name) = &config.package.product_name {
  316. res.set("ProductName", product_name);
  317. res.set("FileDescription", product_name);
  318. }
  319. res.set_icon_with_id(&window_icon_path.display().to_string(), "32512");
  320. res.compile().with_context(|| {
  321. format!(
  322. "failed to compile `{}` into a Windows Resource file during tauri-build",
  323. window_icon_path.display()
  324. )
  325. })?;
  326. } else {
  327. return Err(anyhow!(format!(
  328. "`{}` not found; required for generating a Windows Resource file during tauri-build",
  329. window_icon_path.display()
  330. )));
  331. }
  332. let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();
  333. match target_env.as_str() {
  334. "gnu" => {
  335. let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() {
  336. "x86_64" => Some("x64"),
  337. "x86" => Some("x86"),
  338. "aarch64" => Some("arm64"),
  339. arch => None,
  340. };
  341. if let Some(target_arch) = target_arch {
  342. for entry in std::fs::read_dir(target_dir.join("build"))? {
  343. let path = entry?.path();
  344. let webview2_loader_path = path
  345. .join("out")
  346. .join(target_arch)
  347. .join("WebView2Loader.dll");
  348. if path.to_string_lossy().contains("webview2-com-sys") && webview2_loader_path.exists()
  349. {
  350. std::fs::copy(webview2_loader_path, target_dir.join("WebView2Loader.dll"))?;
  351. break;
  352. }
  353. }
  354. }
  355. }
  356. "msvc" => {
  357. if std::env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "true") {
  358. static_vcruntime::build();
  359. }
  360. }
  361. _ => (),
  362. }
  363. }
  364. Ok(())
  365. }
  366. #[derive(Debug, Default, PartialEq, Eq)]
  367. struct Diff {
  368. remove: Vec<String>,
  369. add: Vec<String>,
  370. }
  371. fn features_diff(current: &[String], expected: &[String]) -> Diff {
  372. let mut remove = Vec::new();
  373. let mut add = Vec::new();
  374. for feature in current {
  375. if !expected.contains(feature) {
  376. remove.push(feature.clone());
  377. }
  378. }
  379. for feature in expected {
  380. if !current.contains(feature) {
  381. add.push(feature.clone());
  382. }
  383. }
  384. Diff { remove, add }
  385. }
  386. #[cfg(test)]
  387. mod tests {
  388. use super::Diff;
  389. #[test]
  390. fn array_diff() {
  391. for (current, expected, result) in [
  392. (vec![], vec![], Default::default()),
  393. (
  394. vec!["a".into()],
  395. vec![],
  396. Diff {
  397. remove: vec!["a".into()],
  398. add: vec![],
  399. },
  400. ),
  401. (vec!["a".into()], vec!["a".into()], Default::default()),
  402. (
  403. vec!["a".into(), "b".into()],
  404. vec!["a".into()],
  405. Diff {
  406. remove: vec!["b".into()],
  407. add: vec![],
  408. },
  409. ),
  410. (
  411. vec!["a".into(), "b".into()],
  412. vec!["a".into(), "c".into()],
  413. Diff {
  414. remove: vec!["b".into()],
  415. add: vec!["c".into()],
  416. },
  417. ),
  418. ] {
  419. assert_eq!(super::features_diff(&current, &expected), result);
  420. }
  421. }
  422. }