wix.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. use super::common;
  2. use super::path_utils::{copy, Options};
  3. use super::settings::Settings;
  4. use handlebars::{to_json, Handlebars};
  5. use lazy_static::lazy_static;
  6. use regex::Regex;
  7. use sha2::Digest;
  8. use std::collections::BTreeMap;
  9. use std::fs::{create_dir_all, remove_dir_all, write, File};
  10. use std::io::{BufRead, BufReader, Cursor, Read, Write};
  11. use std::path::{Path, PathBuf};
  12. use std::process::{Command, Stdio};
  13. use uuid::Uuid;
  14. use zip::ZipArchive;
  15. // URLS for the WIX toolchain. Can be used for crossplatform compilation.
  16. pub const WIX_URL: &str =
  17. "https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip";
  18. pub const WIX_SHA256: &str = "37f0a533b0978a454efb5dc3bd3598becf9660aaf4287e55bf68ca6b527d051d";
  19. // For Cross Platform Complilation.
  20. // const VC_REDIST_X86_URL: &str =
  21. // "https://download.visualstudio.microsoft.com/download/pr/c8edbb87-c7ec-4500-a461-71e8912d25e9/99ba493d660597490cbb8b3211d2cae4/vc_redist.x86.exe";
  22. // const VC_REDIST_X86_SHA256: &str =
  23. // "3a43e8a55a3f3e4b73d01872c16d47a19dd825756784f4580187309e7d1fcb74";
  24. // const VC_REDIST_X64_URL: &str =
  25. // "https://download.visualstudio.microsoft.com/download/pr/9e04d214-5a9d-4515-9960-3d71398d98c3/1e1e62ab57bbb4bf5199e8ce88f040be/vc_redist.x64.exe";
  26. // const VC_REDIST_X64_SHA256: &str =
  27. // "d6cd2445f68815fe02489fafe0127819e44851e26dfbe702612bc0d223cbbc2b";
  28. // A v4 UUID that was generated specifically for cargo-bundle, to be used as a
  29. // namespace for generating v5 UUIDs from bundle identifier strings.
  30. const UUID_NAMESPACE: [u8; 16] = [
  31. 0xfd, 0x85, 0x95, 0xa8, 0x17, 0xa3, 0x47, 0x4e, 0xa6, 0x16, 0x76, 0x14, 0x8d, 0xfa, 0x0c, 0x7b,
  32. ];
  33. // setup for the main.wxs template file using handlebars. Dynamically changes the template on compilation based on the application metadata.
  34. lazy_static! {
  35. static ref HANDLEBARS: Handlebars = {
  36. let mut handlebars = Handlebars::new();
  37. handlebars
  38. .register_template_string("main.wxs", include_str!("templates/main.wxs"))
  39. .or_else(|e| Err(e.to_string()))
  40. .expect("Failed to setup handlebar template");
  41. handlebars
  42. };
  43. }
  44. type ResourceMap = BTreeMap<String, ResourceDirectory>;
  45. #[derive(Serialize)]
  46. struct ExternalBinary {
  47. guid: String,
  48. id: String,
  49. path: String,
  50. }
  51. #[derive(Serialize, Clone)]
  52. struct ResourceFile {
  53. guid: String,
  54. id: String,
  55. path: String,
  56. }
  57. #[derive(Serialize)]
  58. struct ResourceDirectory {
  59. name: String,
  60. files: Vec<ResourceFile>,
  61. directories: Vec<ResourceDirectory>,
  62. }
  63. impl ResourceDirectory {
  64. fn add_file(&mut self, file: ResourceFile) {
  65. self.files.push(file);
  66. }
  67. // generates the wix XML string to bundle this directory resources recursively
  68. fn get_wix_data(self) -> crate::Result<(String, Vec<String>)> {
  69. let mut files = String::from("");
  70. let mut file_ids = Vec::new();
  71. for file in self.files {
  72. file_ids.push(file.id.clone());
  73. files.push_str(
  74. format!(
  75. r#"<Component Id="{id}" Guid="{guid}" Win64="$(var.Win64)" KeyPath="yes"><File Id="PathFile_{id}" Source="{path}" /></Component>"#,
  76. id = file.id,
  77. guid = file.guid,
  78. path = file.path
  79. ).as_str()
  80. );
  81. }
  82. let mut directories = String::from("");
  83. for directory in self.directories {
  84. let (wix_string, ids) = directory.get_wix_data()?;
  85. for id in ids {
  86. file_ids.push(id)
  87. }
  88. directories.push_str(wix_string.as_str());
  89. }
  90. let wix_string = format!(
  91. r#"<Directory Id="{name}" Name="{name}">{contents}</Directory>"#,
  92. name = self.name,
  93. contents = format!("{}{}", files, directories)
  94. );
  95. Ok((wix_string, file_ids))
  96. }
  97. }
  98. fn copy_icons(settings: &Settings) -> crate::Result<PathBuf> {
  99. let base_dir = settings.binary_path();
  100. let base_dir = base_dir.parent().expect("Failed to get dir");
  101. let resource_dir = base_dir.join("resources");
  102. let mut image_path = PathBuf::from(settings.project_out_directory());
  103. // pop off till in tauri_src dir
  104. image_path.pop();
  105. image_path.pop();
  106. // get icon dir and icon file.
  107. let image_path = image_path.join("icons");
  108. let opts = super::path_utils::Options::default();
  109. copy(
  110. image_path,
  111. &resource_dir,
  112. &Options {
  113. copy_files: true,
  114. overwrite: true,
  115. ..opts
  116. },
  117. )
  118. .or_else(|e| Err(e.to_string()))?;
  119. Ok(resource_dir)
  120. }
  121. // Function used to download Wix and VC_REDIST. Checks SHA256 to verify the download.
  122. fn download_and_verify(url: &str, hash: &str) -> crate::Result<Vec<u8>> {
  123. common::print_info(format!("Downloading {}", url).as_str())?;
  124. let response = attohttpc::get(url).send().or_else(|e| Err(e.to_string()))?;
  125. let data: Vec<u8> = response.bytes().or_else(|e| Err(e.to_string()))?;
  126. common::print_info("validating hash")?;
  127. let mut hasher = sha2::Sha256::new();
  128. hasher.input(&data);
  129. let url_hash = hasher.result().to_vec();
  130. let expected_hash = hex::decode(hash).or_else(|e| Err(e.to_string()))?;
  131. if expected_hash == url_hash {
  132. Ok(data)
  133. } else {
  134. Err(crate::Error::from("hash mismatch of downloaded file"))
  135. }
  136. }
  137. fn app_installer_dir(settings: &Settings) -> crate::Result<PathBuf> {
  138. let arch = match settings.binary_arch() {
  139. "x86_64" => "x86",
  140. "x64" => "x64",
  141. target => {
  142. return Err(crate::Error::from(format!(
  143. "Unsupported architecture: {}",
  144. target
  145. )))
  146. }
  147. };
  148. Ok(settings.project_out_directory().to_path_buf().join(format!(
  149. "{}.{}.msi",
  150. settings.bundle_name(),
  151. arch
  152. )))
  153. }
  154. // Extracts the zips from Wix and VC_REDIST into a useable path.
  155. fn extract_zip(data: &Vec<u8>, path: &Path) -> crate::Result<()> {
  156. let cursor = Cursor::new(data);
  157. let mut zipa = ZipArchive::new(cursor).or_else(|e| Err(e.to_string()))?;
  158. for i in 0..zipa.len() {
  159. let mut file = zipa.by_index(i).or_else(|e| Err(e.to_string()))?;
  160. let dest_path = path.join(file.name());
  161. let parent = dest_path.parent().expect("Failed to get parent");
  162. if !parent.exists() {
  163. create_dir_all(parent).or_else(|e| Err(e.to_string()))?;
  164. }
  165. let mut buff: Vec<u8> = Vec::new();
  166. file
  167. .read_to_end(&mut buff)
  168. .or_else(|e| Err(e.to_string()))?;
  169. let mut fileout = File::create(dest_path).expect("Failed to open file");
  170. fileout.write_all(&buff).or_else(|e| Err(e.to_string()))?;
  171. }
  172. Ok(())
  173. }
  174. // Generates the UUID for the Wix template.
  175. fn generate_package_guid(settings: &Settings) -> Uuid {
  176. generate_guid(settings.bundle_identifier().as_bytes())
  177. }
  178. fn generate_guid(key: &[u8]) -> Uuid {
  179. let namespace = Uuid::from_bytes(UUID_NAMESPACE);
  180. Uuid::new_v5(&namespace, key)
  181. }
  182. // Specifically goes and gets Wix and verifies the download via Sha256
  183. pub fn get_and_extract_wix(path: &Path) -> crate::Result<()> {
  184. common::print_info("Verifying wix package")?;
  185. let data = download_and_verify(WIX_URL, WIX_SHA256)?;
  186. common::print_info("extracting WIX")?;
  187. extract_zip(&data, path)
  188. }
  189. // For if bundler needs DLL files.
  190. // fn run_heat_exe(
  191. // wix_toolset_path: &Path,
  192. // build_path: &Path,
  193. // harvest_dir: &Path,
  194. // platform: &str,
  195. // ) -> Result<(), String> {
  196. // let mut args = vec!["dir"];
  197. // let harvest_str = harvest_dir.display().to_string();
  198. // args.push(&harvest_str);
  199. // args.push("-platform");
  200. // args.push(platform);
  201. // args.push("-cg");
  202. // args.push("AppFiles");
  203. // args.push("-dr");
  204. // args.push("APPLICATIONFOLDER");
  205. // args.push("-gg");
  206. // args.push("-srd");
  207. // args.push("-out");
  208. // args.push("appdir.wxs");
  209. // args.push("-var");
  210. // args.push("var.SourceDir");
  211. // let heat_exe = wix_toolset_path.join("heat.exe");
  212. // let mut cmd = Command::new(&heat_exe)
  213. // .args(&args)
  214. // .stdout(Stdio::piped())
  215. // .current_dir(build_path)
  216. // .spawn()
  217. // .expect("error running heat.exe");
  218. // {
  219. // let stdout = cmd.stdout.as_mut().unwrap();
  220. // let reader = BufReader::new(stdout);
  221. // for line in reader.lines() {
  222. // info!(logger, "{}", line.unwrap());
  223. // }
  224. // }
  225. // let status = cmd.wait().unwrap();
  226. // if status.success() {
  227. // Ok(())
  228. // } else {
  229. // Err("error running heat.exe".to_string())
  230. // }
  231. // }
  232. // Runs the Candle.exe executable for Wix. Candle parses the wxs file and generates the code for building the installer.
  233. fn run_candle(
  234. settings: &Settings,
  235. wix_toolset_path: &Path,
  236. build_path: &Path,
  237. wxs_file_name: &str,
  238. ) -> crate::Result<()> {
  239. let arch = match settings.binary_arch() {
  240. "x86_64" => "x64",
  241. "x86" => "x86",
  242. target => {
  243. return Err(crate::Error::from(format!(
  244. "unsupported target: {}",
  245. target
  246. )))
  247. }
  248. };
  249. let args = vec![
  250. "-arch".to_string(),
  251. arch.to_string(),
  252. wxs_file_name.to_string(),
  253. format!("-dSourceDir={}", settings.binary_path().display()),
  254. ];
  255. let candle_exe = wix_toolset_path.join("candle.exe");
  256. common::print_info(format!("running candle for {}", wxs_file_name).as_str())?;
  257. let mut cmd = Command::new(&candle_exe)
  258. .args(&args)
  259. .stdout(Stdio::piped())
  260. .current_dir(build_path)
  261. .spawn()
  262. .expect("error running candle.exe");
  263. {
  264. let stdout = cmd.stdout.as_mut().expect("Failed to get stdout handle");
  265. let reader = BufReader::new(stdout);
  266. for line in reader.lines() {
  267. common::print_info(line.expect("Failed to get line").as_str())?;
  268. }
  269. }
  270. let status = cmd.wait()?;
  271. if status.success() {
  272. Ok(())
  273. } else {
  274. Err(crate::Error::from("error running candle.exe"))
  275. }
  276. }
  277. // Runs the Light.exe file. Light takes the generated code from Candle and produces an MSI Installer.
  278. fn run_light(
  279. wix_toolset_path: &Path,
  280. build_path: &Path,
  281. wixobjs: &[&str],
  282. output_path: &Path,
  283. ) -> crate::Result<PathBuf> {
  284. let light_exe = wix_toolset_path.join("light.exe");
  285. let mut args: Vec<String> = vec!["-o".to_string(), output_path.display().to_string()];
  286. for p in wixobjs {
  287. args.push(p.to_string());
  288. }
  289. common::print_info(format!("running light to produce {}", output_path.display()).as_str())?;
  290. let mut cmd = Command::new(&light_exe)
  291. .args(&args)
  292. .stdout(Stdio::piped())
  293. .current_dir(build_path)
  294. .spawn()
  295. .expect("error running light.exe");
  296. {
  297. let stdout = cmd.stdout.as_mut().expect("Failed to get stdout handle");
  298. let reader = BufReader::new(stdout);
  299. for line in reader.lines() {
  300. common::print_info(line.expect("Failed to get line").as_str())?;
  301. }
  302. }
  303. let status = cmd.wait()?;
  304. if status.success() {
  305. Ok(output_path.to_path_buf())
  306. } else {
  307. Err(crate::Error::from("error running light.exe"))
  308. }
  309. }
  310. // fn get_icon_data() -> crate::Result<()> {
  311. // Ok(())
  312. // }
  313. // Entry point for bundling and creating the MSI installer. For now the only supported platform is Windows x64.
  314. pub fn build_wix_app_installer(
  315. settings: &Settings,
  316. wix_toolset_path: &Path,
  317. ) -> crate::Result<PathBuf> {
  318. let arch = match settings.binary_arch() {
  319. "x86_64" => "x64",
  320. "x86" => "x86",
  321. target => {
  322. return Err(crate::Error::from(format!(
  323. "unsupported target: {}",
  324. target
  325. )))
  326. }
  327. };
  328. // common::print_warning("Only x64 supported")?;
  329. // target only supports x64.
  330. common::print_info(format!("Target: {}", arch).as_str())?;
  331. let output_path = settings.project_out_directory().join("wix").join(arch);
  332. let mut data = BTreeMap::new();
  333. data.insert("product_name", to_json(settings.bundle_name()));
  334. data.insert("version", to_json(settings.version_string()));
  335. let manufacturer = settings.bundle_identifier().to_string();
  336. data.insert("manufacturer", to_json(manufacturer.as_str()));
  337. let upgrade_code = Uuid::new_v5(
  338. &Uuid::NAMESPACE_DNS,
  339. format!("{}.app.x64", &settings.binary_name()).as_bytes(),
  340. )
  341. .to_string();
  342. data.insert("upgrade_code", to_json(&upgrade_code.as_str()));
  343. let path_guid = generate_package_guid(settings).to_string();
  344. data.insert("path_component_guid", to_json(&path_guid.as_str()));
  345. let shortcut_guid = generate_package_guid(settings).to_string();
  346. data.insert("shortcut_guid", to_json(&shortcut_guid.as_str()));
  347. let app_exe_name = settings.binary_name().to_string();
  348. data.insert("app_exe_name", to_json(&app_exe_name));
  349. let external_binaries = generate_external_binary_data(&settings)?;
  350. let external_binaries_json = to_json(&external_binaries);
  351. data.insert("external_binaries", external_binaries_json);
  352. let resources = generate_resource_data(&settings)?;
  353. let mut resources_wix_string = String::from("");
  354. let mut files_ids = Vec::new();
  355. for (_, dir) in resources {
  356. let (wix_string, ids) = dir.get_wix_data()?;
  357. resources_wix_string.push_str(wix_string.as_str());
  358. for id in ids {
  359. files_ids.push(id);
  360. }
  361. }
  362. data.insert("resources", to_json(resources_wix_string));
  363. data.insert("resource_file_ids", to_json(files_ids));
  364. let app_exe_source = settings.binary_path().display().to_string();
  365. data.insert("app_exe_source", to_json(&app_exe_source));
  366. // copy icons from icons folder to resource folder near msi
  367. let image_path = copy_icons(&settings)?;
  368. let path = image_path.join("icon.ico").display().to_string();
  369. data.insert("icon_path", to_json(path.as_str()));
  370. let temp = HANDLEBARS
  371. .render("main.wxs", &data)
  372. .or_else(|e| Err(e.to_string()))?;
  373. if output_path.exists() {
  374. remove_dir_all(&output_path).or_else(|e| Err(e.to_string()))?;
  375. }
  376. create_dir_all(&output_path).or_else(|e| Err(e.to_string()))?;
  377. let main_wxs_path = output_path.join("main.wxs");
  378. write(&main_wxs_path, temp).or_else(|e| Err(e.to_string()))?;
  379. let input_basenames = vec!["main"];
  380. for basename in &input_basenames {
  381. let wxs = format!("{}.wxs", basename);
  382. run_candle(settings, &wix_toolset_path, &output_path, &wxs)?;
  383. }
  384. let wixobjs = vec!["main.wixobj"];
  385. let target = run_light(
  386. &wix_toolset_path,
  387. &output_path,
  388. &wixobjs,
  389. &app_installer_dir(settings)?,
  390. )?;
  391. Ok(target)
  392. }
  393. fn generate_external_binary_data(settings: &Settings) -> crate::Result<Vec<ExternalBinary>> {
  394. let mut external_binaries = Vec::new();
  395. let regex = Regex::new(r"[^\w\d\.]")?;
  396. let cwd = std::env::current_dir()?;
  397. for src in settings.external_binaries() {
  398. let src = src?;
  399. let filename = src
  400. .file_name()
  401. .expect("failed to extract external binary filename")
  402. .to_os_string()
  403. .into_string()
  404. .expect("failed to convert external binary filename to string");
  405. let guid = generate_guid(filename.as_bytes()).to_string();
  406. external_binaries.push(ExternalBinary {
  407. guid: guid,
  408. path: cwd
  409. .join(src)
  410. .into_os_string()
  411. .into_string()
  412. .expect("failed to read external binary path"),
  413. id: regex.replace_all(&filename, "").to_string(),
  414. });
  415. }
  416. Ok(external_binaries)
  417. }
  418. // generates the data required for the resource bundling on wix
  419. fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
  420. let mut resources = ResourceMap::new();
  421. let regex = Regex::new(r"[^\w\d\.]")?;
  422. let cwd = std::env::current_dir()?;
  423. for src in settings.resource_files() {
  424. let src = src?;
  425. let filename = src
  426. .file_name()
  427. .expect("failed to extract resource filename")
  428. .to_os_string()
  429. .into_string()
  430. .expect("failed to convert resource filename to string");
  431. let resource_path = cwd
  432. .join(src.clone())
  433. .into_os_string()
  434. .into_string()
  435. .expect("failed to read resource path");
  436. let resource_entry = ResourceFile {
  437. guid: generate_guid(filename.as_bytes()).to_string(),
  438. path: resource_path,
  439. id: regex.replace_all(&filename, "").to_string(),
  440. };
  441. // split the resource path directories
  442. let mut directories = src
  443. .components()
  444. .filter(|component| {
  445. let comp = component.as_os_str();
  446. comp != "." && comp != ".."
  447. })
  448. .collect::<Vec<_>>();
  449. directories.truncate(directories.len() - 1);
  450. // transform the directory structure to a chained vec structure
  451. for directory in directories {
  452. let directory_name = directory
  453. .as_os_str()
  454. .to_os_string()
  455. .into_string()
  456. .expect("failed to read resource folder name");
  457. // if the directory is already on the map
  458. if resources.contains_key(&directory_name) {
  459. let directory_entry = &mut resources
  460. .get_mut(&directory_name)
  461. .expect("Unable to handle resources");
  462. if directory_entry.name == directory_name {
  463. // the directory entry is the root of the chain
  464. directory_entry.add_file(resource_entry.clone());
  465. } else {
  466. let index = directory_entry
  467. .directories
  468. .iter()
  469. .position(|f| f.name == directory_name);
  470. if index.is_some() {
  471. // the directory entry is already a part of the chain
  472. let dir = directory_entry
  473. .directories
  474. .get_mut(index.expect("Unable to get index"))
  475. .expect("Unable to get directory");
  476. dir.add_file(resource_entry.clone());
  477. } else {
  478. // push it to the chain
  479. directory_entry.directories.push(ResourceDirectory {
  480. name: directory_name.clone(),
  481. directories: vec![],
  482. files: vec![resource_entry.clone()],
  483. });
  484. }
  485. }
  486. } else {
  487. resources.insert(
  488. directory_name.clone(),
  489. ResourceDirectory {
  490. name: directory_name.clone(),
  491. directories: vec![],
  492. files: vec![resource_entry.clone()],
  493. },
  494. );
  495. }
  496. }
  497. }
  498. Ok(resources)
  499. }