Bladeren bron

Merge branch 'msi-experimental' of https://github.com/tensor-programming/proton into tensor-programming-msi-experimental

Daniel Thompson-Yvetot 6 jaren geleden
bovenliggende
commit
e550db5637

File diff suppressed because it is too large
+ 729 - 2
tools/rust/cargo-tauri-bundle/Cargo.lock


+ 8 - 0
tools/rust/cargo-tauri-bundle/Cargo.toml

@@ -31,6 +31,14 @@ toml = "0.4"
 uuid = { version = "0.5", features = ["v5"] }
 walkdir = "2"
 
+sha2 = "0.8"
+lazy_static = "1.3"
+handlebars = "1.1"
+reqwest = "0.9.19"
+hex = "0.3"
+zip = "0.5"
+
+
 [dev-dependencies]
 tempfile = "3"
 winit = "0.11"

+ 1 - 1
tools/rust/cargo-tauri-bundle/rustfmt.toml

@@ -6,7 +6,7 @@ use_small_heuristics = "Default"
 reorder_imports = true
 reorder_modules = true
 remove_nested_parens = true
-edition = "2015"
+edition = "2018"
 merge_derives = true
 use_try_shorthand = false
 use_field_init_shorthand = false

+ 1 - 0
tools/rust/cargo-tauri-bundle/src/bundle.rs

@@ -6,6 +6,7 @@ mod msi_bundle;
 mod osx_bundle;
 mod rpm_bundle;
 mod settings;
+mod wix;
 
 pub use self::common::{print_error, print_finished};
 pub use self::settings::{BuildArtifact, PackageType, Settings};

+ 19 - 0
tools/rust/cargo-tauri-bundle/src/bundle/common.rs

@@ -189,6 +189,25 @@ pub fn print_warning(message: &str) -> crate::Result<()> {
   }
 }
 
+/// Prints a Info message to stderr.
+pub fn print_info(message: &str) -> crate::Result<()> {
+  if let Some(mut output) = term::stderr() {
+    safe_term_attr(&mut output, term::Attr::Bold)?;
+    output.fg(term::color::GREEN)?;
+    write!(output, "info:")?;
+    output.reset()?;
+    write!(output, " {}\n", message)?;
+    output.flush()?;
+    Ok(())
+  } else {
+    let mut output = io::stderr();
+    write!(output, "info:")?;
+    write!(output, " {}\n", message)?;
+    output.flush()?;
+    Ok(())
+  }
+}
+
 /// Prints an error to stderr, in the same format that `cargo` uses.
 pub fn print_error(error: &crate::Error) -> crate::Result<()> {
   if let Some(mut output) = term::stderr() {

+ 9 - 608
tools/rust/cargo-tauri-bundle/src/bundle/msi_bundle.rs

@@ -1,620 +1,21 @@
 use super::common;
 use super::settings::Settings;
-use crate::ResultExt;
-use cab;
-use msi;
+use super::wix;
 use std;
-use std::collections::{BTreeMap, HashMap, HashSet};
-use std::ffi::OsStr;
-use std::fs;
-use std::io::{self, Write};
-use std::path::{Path, PathBuf};
-use uuid::Uuid;
-
-type Package = msi::Package<fs::File>;
-
-// Don't add more files to a cabinet folder that already has this many bytes:
-const CABINET_FOLDER_SIZE_LIMIT: u64 = 0x8000;
-// The maximum number of resource files we'll put in one cabinet:
-const CABINET_MAX_FILES: usize = 1000;
-// The maximum number of data bytes we'll put in one cabinet:
-const CABINET_MAX_SIZE: u64 = 0x1000_0000;
-
-// File table attribute indicating that a file is "vital":
-const FILE_ATTR_VITAL: u16 = 0x200;
-
-// The name of the installer package's sole Feature:
-const MAIN_FEATURE_NAME: &str = "MainFeature";
-
-// A v4 UUID that was generated specifically for cargo-bundle, to be used as a
-// namespace for generating v5 UUIDs from bundle identifier strings.
-const UUID_NAMESPACE: [u8; 16] = [
-  0xfd, 0x85, 0x95, 0xa8, 0x17, 0xa3, 0x47, 0x4e, 0xa6, 0x16, 0x76, 0x14, 0x8d, 0xfa, 0x0c, 0x7b,
-];
-
-// Info about a resource file (including the main executable) in the bundle.
-struct ResourceInfo {
-  // The path to the existing file that will be bundled as a resource.
-  source_path: PathBuf,
-  // Relative path from the install dir where this will be installed.
-  dest_path: PathBuf,
-  // The name of this resource file in the filesystem.
-  filename: String,
-  // The size of this resource file, in bytes.
-  size: u64,
-  // The database key for the Component that this resource is part of.
-  component_key: String,
-}
-
-// Info about a directory that needs to be created during installation.
-struct DirectoryInfo {
-  // The database key for this directory.
-  key: String,
-  // The database key for this directory's parent.
-  parent_key: String,
-  // The name of this directory in the filesystem.
-  name: String,
-  // List of files in this directory, not counting subdirectories.
-  files: Vec<String>,
-}
-
-// Info about a CAB archive within the installer package.
-struct CabinetInfo {
-  // The stream name for this cabinet.
-  name: String,
-  // The resource files that are in this cabinet.
-  resources: Vec<ResourceInfo>,
-}
+use std::path::PathBuf;
 
+// Runs all of the commands to build the MSI installer.
+// Returns a vector of PathBuf that shows where the MSI was created.
 pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
   common::print_warning("MSI bundle support is still experimental.")?;
 
-  let msi_name = format!("{}.msi", settings.bundle_name());
-  common::print_bundling(&msi_name)?;
-  let base_dir = settings.project_out_directory().join("bundle/msi");
-  let msi_path = base_dir.join(&msi_name);
-  let mut package =
-    new_empty_package(&msi_path).chain_err(|| "Failed to initialize MSI package")?;
-
-  // Generate package metadata:
-  let guid = generate_package_guid(settings);
-  set_summary_info(&mut package, guid, settings);
-  create_property_table(&mut package, guid, settings)
-    .chain_err(|| "Failed to generate Property table")?;
+  let wix_path = PathBuf::from("./WixTools");
 
-  // Copy resource files into package:
-  let mut resources =
-    collect_resource_info(settings).chain_err(|| "Failed to collect resource file information")?;
-  let directories = collect_directory_info(settings, &mut resources)
-    .chain_err(|| "Failed to collect resource directory information")?;
-  let cabinets = divide_resources_into_cabinets(resources);
-  generate_resource_cabinets(&mut package, &cabinets)
-    .chain_err(|| "Failed to generate resource cabinets")?;
-
-  // Set up installer database tables:
-  create_directory_table(&mut package, &directories)
-    .chain_err(|| "Failed to generate Directory table")?;
-  create_feature_table(&mut package, settings).chain_err(|| "Failed to generate Feature table")?;
-  create_component_table(&mut package, guid, &directories)
-    .chain_err(|| "Failed to generate Component table")?;
-  create_feature_components_table(&mut package, &directories)
-    .chain_err(|| "Failed to generate FeatureComponents table")?;
-  create_media_table(&mut package, &cabinets).chain_err(|| "Failed to generate Media table")?;
-  create_file_table(&mut package, &cabinets).chain_err(|| "Failed to generate File table")?;
-  // TODO: Create other needed tables.
-
-  // Create app icon:
-  package.create_table(
-    "Icon",
-    vec![
-      msi::Column::build("Name").primary_key().id_string(72),
-      msi::Column::build("Data").binary(),
-    ],
-  )?;
-  let icon_name = format!("{}.ico", settings.binary_name());
-  {
-    let stream_name = format!("Icon.{}", icon_name);
-    let mut stream = package.write_stream(&stream_name)?;
-    create_app_icon(&mut stream, settings)?;
+  if !wix_path.exists() {
+    wix::get_and_extract_wix(&wix_path)?;
   }
-  package.insert_rows(msi::Insert::into("Icon").row(vec![
-    msi::Value::Str(icon_name.clone()),
-    msi::Value::from("Name"),
-  ]))?;
 
-  package.flush()?;
-  Ok(vec![msi_path])
-}
-
-fn new_empty_package(msi_path: &Path) -> crate::Result<Package> {
-  if let Some(parent) = msi_path.parent() {
-    fs::create_dir_all(&parent).chain_err(|| format!("Failed to create directory {:?}", parent))?;
-  }
-  let msi_file = fs::OpenOptions::new()
-    .read(true)
-    .write(true)
-    .create(true)
-    .truncate(true)
-    .open(msi_path)
-    .chain_err(|| format!("Failed to create file {:?}", msi_path))?;
-  let package = msi::Package::create(msi::PackageType::Installer, msi_file)?;
-  Ok(package)
-}
-
-// Generates a GUID for the package, based on `settings.bundle_identifier()`.
-fn generate_package_guid(settings: &Settings) -> Uuid {
-  let namespace = Uuid::from_bytes(&UUID_NAMESPACE).unwrap();
-  Uuid::new_v5(&namespace, &settings.bundle_identifier())
-}
-
-// Populates the summary metadata for the package from the bundle settings.
-fn set_summary_info(package: &mut Package, package_guid: Uuid, settings: &Settings) {
-  let summary_info = package.summary_info_mut();
-  summary_info.set_creation_time_to_now();
-  summary_info.set_subject(settings.bundle_name().to_string());
-  summary_info.set_uuid(package_guid);
-  summary_info.set_comments(settings.short_description().to_string());
-  if let Some(authors) = settings.authors_comma_separated() {
-    summary_info.set_author(authors);
-  }
-  let creating_app = format!("cargo-bundle v{}", crate_version!());
-  summary_info.set_creating_application(creating_app);
-}
-
-// Creates and populates the `Property` database table for the package.
-fn create_property_table(
-  package: &mut Package,
-  package_guid: Uuid,
-  settings: &Settings,
-) -> crate::Result<()> {
-  let authors = settings.authors_comma_separated().unwrap_or(String::new());
-  package.create_table(
-    "Property",
-    vec![
-      msi::Column::build("Property").primary_key().id_string(72),
-      msi::Column::build("Value").text_string(0),
-    ],
-  )?;
-  package.insert_rows(
-    msi::Insert::into("Property")
-      .row(vec![
-        msi::Value::from("Manufacturer"),
-        msi::Value::Str(authors),
-      ])
-      .row(vec![
-        msi::Value::from("ProductCode"),
-        msi::Value::from(package_guid),
-      ])
-      .row(vec![
-        msi::Value::from("ProductLanguage"),
-        msi::Value::from(msi::Language::from_tag("en-US")),
-      ])
-      .row(vec![
-        msi::Value::from("ProductName"),
-        msi::Value::from(settings.bundle_name()),
-      ])
-      .row(vec![
-        msi::Value::from("ProductVersion"),
-        msi::Value::from(settings.version_string()),
-      ]),
-  )?;
-  Ok(())
-}
-
-// Returns a list of `ResourceInfo` structs for the binary executable and all
-// the resource files that should be included in the package.
-fn collect_resource_info(settings: &Settings) -> crate::Result<Vec<ResourceInfo>> {
-  let mut resources = Vec::<ResourceInfo>::new();
-  resources.push(ResourceInfo {
-    source_path: settings.binary_path().to_path_buf(),
-    dest_path: PathBuf::from(settings.binary_name()),
-    filename: settings.binary_name().to_string(),
-    size: settings.binary_path().metadata()?.len(),
-    component_key: String::new(),
-  });
-  let root_rsrc_dir = PathBuf::from("Resources");
-  for source_path in settings.resource_files() {
-    let source_path = source_path?;
-    let metadata = source_path.metadata()?;
-    let size = metadata.len();
-    let dest_path = root_rsrc_dir.join(common::resource_relpath(&source_path));
-    let filename = dest_path.file_name().unwrap().to_string_lossy().to_string();
-    let info = ResourceInfo {
-      source_path,
-      dest_path,
-      filename,
-      size,
-      component_key: String::new(),
-    };
-    resources.push(info);
-  }
-  Ok(resources)
-}
+  let msi_path = wix::build_wix_app_installer(&settings, &wix_path)?;
 
-// Based on the list of all resource files to be bundled, returns a list of
-// all the directories that need to be created during installation.  Also,
-// modifies each `ResourceInfo` object to populate its `component_key` field
-// with the database key of the Component that the resource will be associated
-// with.
-fn collect_directory_info(
-  settings: &Settings,
-  resources: &mut Vec<ResourceInfo>,
-) -> crate::Result<Vec<DirectoryInfo>> {
-  let mut dir_map = BTreeMap::<PathBuf, DirectoryInfo>::new();
-  let mut dir_index: i32 = 0;
-  dir_map.insert(
-    PathBuf::new(),
-    DirectoryInfo {
-      key: "INSTALLDIR".to_string(),
-      parent_key: "ProgramFilesFolder".to_string(),
-      name: settings.bundle_name().to_string(),
-      files: Vec::new(),
-    },
-  );
-  for resource in resources.iter_mut() {
-    let mut dir_key = "INSTALLDIR".to_string();
-    let mut dir_path = PathBuf::new();
-    for component in resource.dest_path.parent().unwrap().components() {
-      if let std::path::Component::Normal(name) = component {
-        dir_path.push(name);
-        if dir_map.contains_key(&dir_path) {
-          dir_key = dir_map.get(&dir_path).unwrap().key.clone();
-        } else {
-          let new_key = format!("RDIR{:04}", dir_index);
-          dir_map.insert(
-            dir_path.clone(),
-            DirectoryInfo {
-              key: new_key.clone(),
-              parent_key: dir_key.clone(),
-              name: name.to_string_lossy().to_string(),
-              files: Vec::new(),
-            },
-          );
-          dir_key = new_key;
-          dir_index += 1;
-        }
-      }
-    }
-    let directory = dir_map.get_mut(&dir_path).unwrap();
-    debug_assert_eq!(directory.key, dir_key);
-    directory.files.push(resource.filename.clone());
-    resource.component_key = dir_key.to_string();
-  }
-  Ok(dir_map.into_iter().map(|(_k, v)| v).collect())
-}
-
-// Divides up the list of resource into some number of cabinets, subject to a
-// few contraints: 1) no one cabinet will have two resources with the same
-// filename, 2) no one cabinet will have more than `CABINET_MAX_FILES` files
-// in it, and 3) no one cabinet will contain mroe than `CABINET_MAX_SIZE`
-// bytes of data (unless that cabinet consists of a single file that is
-// already bigger than that).
-fn divide_resources_into_cabinets(mut resources: Vec<ResourceInfo>) -> Vec<CabinetInfo> {
-  let mut cabinets = Vec::new();
-  while !resources.is_empty() {
-    let mut filenames = HashSet::<String>::new();
-    let mut total_size = 0;
-    let mut leftovers = Vec::<ResourceInfo>::new();
-    let mut cabinet = CabinetInfo {
-      name: format!("rsrc{:04}.cab", cabinets.len()),
-      resources: Vec::new(),
-    };
-    for resource in resources.into_iter() {
-      if cabinet.resources.len() >= CABINET_MAX_FILES
-        || (!cabinet.resources.is_empty() && total_size + resource.size > CABINET_MAX_SIZE)
-        || filenames.contains(&resource.filename)
-      {
-        leftovers.push(resource);
-      } else {
-        filenames.insert(resource.filename.clone());
-        total_size += resource.size;
-        cabinet.resources.push(resource);
-      }
-    }
-    cabinets.push(cabinet);
-    resources = leftovers;
-  }
-  cabinets
-}
-
-// Creates the CAB archives within the package that contain the binary
-// execuable and all the resource files.
-fn generate_resource_cabinets(
-  package: &mut Package,
-  cabinets: &[CabinetInfo],
-) -> crate::Result<()> {
-  for cabinet_info in cabinets.iter() {
-    let mut builder = cab::CabinetBuilder::new();
-    let mut file_map = HashMap::<String, &Path>::new();
-    let mut resource_index: usize = 0;
-    while resource_index < cabinet_info.resources.len() {
-      let folder = builder.add_folder(cab::CompressionType::MsZip);
-      let mut folder_size: u64 = 0;
-      while resource_index < cabinet_info.resources.len() && folder_size < CABINET_FOLDER_SIZE_LIMIT
-      {
-        let resource = &cabinet_info.resources[resource_index];
-        folder_size += resource.size;
-        folder.add_file(resource.filename.as_str());
-        debug_assert!(!file_map.contains_key(&resource.filename));
-        file_map.insert(resource.filename.clone(), &resource.source_path);
-        resource_index += 1;
-      }
-    }
-    let stream = package.write_stream(cabinet_info.name.as_str())?;
-    let mut cabinet_writer = builder.build(stream)?;
-    while let Some(mut file_writer) = cabinet_writer.next_file()? {
-      debug_assert!(file_map.contains_key(file_writer.file_name()));
-      let file_path = file_map.get(file_writer.file_name()).unwrap();
-      let mut file = fs::File::open(file_path)?;
-      io::copy(&mut file, &mut file_writer)?;
-    }
-    cabinet_writer.finish()?;
-  }
-  Ok(())
-}
-
-// Creates and populates the `Directory` database table for the package.
-fn create_directory_table(
-  package: &mut Package,
-  directories: &[DirectoryInfo],
-) -> crate::Result<()> {
-  package.create_table(
-    "Directory",
-    vec![
-      msi::Column::build("Directory").primary_key().id_string(72),
-      msi::Column::build("Directory_Parent")
-        .nullable()
-        .foreign_key("Directory", 1)
-        .id_string(72),
-      msi::Column::build("DefaultDir")
-        .category(msi::Category::DefaultDir)
-        .string(255),
-    ],
-  )?;
-  let mut rows = Vec::new();
-  for directory in directories.iter() {
-    rows.push(vec![
-      msi::Value::Str(directory.key.clone()),
-      msi::Value::Str(directory.parent_key.clone()),
-      msi::Value::Str(directory.name.clone()),
-    ]);
-  }
-  package.insert_rows(
-    msi::Insert::into("Directory")
-      .row(vec![
-        msi::Value::from("TARGETDIR"),
-        msi::Value::Null,
-        msi::Value::from("SourceDir"),
-      ])
-      .row(vec![
-        msi::Value::from("ProgramFilesFolder"),
-        msi::Value::from("TARGETDIR"),
-        msi::Value::from("."),
-      ])
-      .rows(rows),
-  )?;
-  Ok(())
-}
-
-// Creates and populates the `Feature` database table for the package.  The
-// package will have a single main feature that installs everything.
-fn create_feature_table(package: &mut Package, settings: &Settings) -> crate::Result<()> {
-  package.create_table(
-    "Feature",
-    vec![
-      msi::Column::build("Feature").primary_key().id_string(38),
-      msi::Column::build("Feature_Parent")
-        .nullable()
-        .foreign_key("Feature", 1)
-        .id_string(38),
-      msi::Column::build("Title").nullable().text_string(64),
-      msi::Column::build("Description")
-        .nullable()
-        .text_string(255),
-      msi::Column::build("Display")
-        .nullable()
-        .range(0, 0x7fff)
-        .int16(),
-      msi::Column::build("Level").range(0, 0x7fff).int16(),
-      msi::Column::build("Directory_")
-        .nullable()
-        .foreign_key("Directory", 1)
-        .id_string(72),
-      msi::Column::build("Attributes").int16(),
-    ],
-  )?;
-  package.insert_rows(msi::Insert::into("Feature").row(vec![
-    msi::Value::from(MAIN_FEATURE_NAME),
-    msi::Value::Null,
-    msi::Value::from(settings.bundle_name()),
-    msi::Value::Null,
-    msi::Value::Int(1),
-    msi::Value::Int(3),
-    msi::Value::from("INSTALLDIR"),
-    msi::Value::Int(0),
-  ]))?;
-  Ok(())
-}
-
-// Creates and populates the `Component` database table for the package.  One
-// component is created for each subdirectory under in the install dir.
-fn create_component_table(
-  package: &mut Package,
-  package_guid: Uuid,
-  directories: &[DirectoryInfo],
-) -> crate::Result<()> {
-  package.create_table(
-    "Component",
-    vec![
-      msi::Column::build("Component").primary_key().id_string(72),
-      msi::Column::build("ComponentId")
-        .nullable()
-        .category(msi::Category::Guid)
-        .string(38),
-      msi::Column::build("Directory_")
-        .nullable()
-        .foreign_key("Directory", 1)
-        .id_string(72),
-      msi::Column::build("Attributes").int16(),
-      msi::Column::build("Condition")
-        .nullable()
-        .category(msi::Category::Condition)
-        .string(255),
-      msi::Column::build("KeyPath").nullable().id_string(72),
-    ],
-  )?;
-  let mut rows = Vec::new();
-  for directory in directories.iter() {
-    if !directory.files.is_empty() {
-      let hash_input = directory.files.join("/");
-      rows.push(vec![
-        msi::Value::Str(directory.key.clone()),
-        msi::Value::from(Uuid::new_v5(&package_guid, &hash_input)),
-        msi::Value::Str(directory.key.clone()),
-        msi::Value::Int(0),
-        msi::Value::Null,
-        msi::Value::Str(directory.files[0].clone()),
-      ]);
-    }
-  }
-  package.insert_rows(msi::Insert::into("Component").rows(rows))?;
-  Ok(())
-}
-
-// Creates and populates the `FeatureComponents` database table for the
-// package.  All components are added to the package's single main feature.
-fn create_feature_components_table(
-  package: &mut Package,
-  directories: &[DirectoryInfo],
-) -> crate::Result<()> {
-  package.create_table(
-    "FeatureComponents",
-    vec![
-      msi::Column::build("Feature_")
-        .primary_key()
-        .foreign_key("Component", 1)
-        .id_string(38),
-      msi::Column::build("Component_")
-        .primary_key()
-        .foreign_key("Component", 1)
-        .id_string(72),
-    ],
-  )?;
-  let mut rows = Vec::new();
-  for directory in directories.iter() {
-    if !directory.files.is_empty() {
-      rows.push(vec![
-        msi::Value::from(MAIN_FEATURE_NAME),
-        msi::Value::Str(directory.key.clone()),
-      ]);
-    }
-  }
-  package.insert_rows(msi::Insert::into("FeatureComponents").rows(rows))?;
-  Ok(())
-}
-
-// Creates and populates the `Media` database table for the package, with one
-// entry for each CAB archive within the package.
-fn create_media_table(package: &mut Package, cabinets: &[CabinetInfo]) -> crate::Result<()> {
-  package.create_table(
-    "Media",
-    vec![
-      msi::Column::build("DiskId")
-        .primary_key()
-        .range(1, 0x7fff)
-        .int16(),
-      msi::Column::build("LastSequence").range(0, 0x7fff).int16(),
-      msi::Column::build("DiskPrompt").nullable().text_string(64),
-      msi::Column::build("Cabinet")
-        .nullable()
-        .category(msi::Category::Cabinet)
-        .string(255),
-      msi::Column::build("VolumeLabel").nullable().text_string(32),
-      msi::Column::build("Source")
-        .nullable()
-        .category(msi::Category::Property)
-        .string(32),
-    ],
-  )?;
-  let mut disk_id: i32 = 0;
-  let mut last_seq: i32 = 0;
-  let mut rows = Vec::new();
-  for cabinet in cabinets.iter() {
-    disk_id += 1;
-    last_seq += cabinet.resources.len() as i32;
-    rows.push(vec![
-      msi::Value::Int(disk_id),
-      msi::Value::Int(last_seq),
-      msi::Value::Null,
-      msi::Value::Str(format!("#{}", cabinet.name)),
-      msi::Value::Null,
-      msi::Value::Null,
-    ]);
-  }
-  package.insert_rows(msi::Insert::into("Media").rows(rows))?;
-  Ok(())
-}
-
-// Creates and populates the `File` database table for the package, with one
-// entry for each resource file to be installed (including the main
-// executable).
-fn create_file_table(package: &mut Package, cabinets: &[CabinetInfo]) -> crate::Result<()> {
-  package.create_table(
-    "File",
-    vec![
-      msi::Column::build("File").primary_key().id_string(72),
-      msi::Column::build("Component_")
-        .foreign_key("Component", 1)
-        .id_string(72),
-      msi::Column::build("FileName")
-        .category(msi::Category::Filename)
-        .string(255),
-      msi::Column::build("FileSize").range(0, 0x7fffffff).int32(),
-      msi::Column::build("Version")
-        .nullable()
-        .category(msi::Category::Version)
-        .string(72),
-      msi::Column::build("Language")
-        .nullable()
-        .category(msi::Category::Language)
-        .string(20),
-      msi::Column::build("Attributes")
-        .nullable()
-        .range(0, 0x7fff)
-        .int16(),
-      msi::Column::build("Sequence").range(1, 0x7fff).int16(),
-    ],
-  )?;
-  let mut rows = Vec::new();
-  let mut sequence: i32 = 1;
-  for cabinet in cabinets.iter() {
-    for resource in cabinet.resources.iter() {
-      rows.push(vec![
-        msi::Value::Str(format!("r{:04}", sequence)),
-        msi::Value::Str(resource.component_key.clone()),
-        msi::Value::Str(resource.filename.clone()),
-        msi::Value::Int(resource.size as i32),
-        msi::Value::Null,
-        msi::Value::Null,
-        msi::Value::from(FILE_ATTR_VITAL),
-        msi::Value::Int(sequence),
-      ]);
-      sequence += 1;
-    }
-  }
-  package.insert_rows(msi::Insert::into("File").rows(rows))?;
-  Ok(())
-}
-
-fn create_app_icon<W: Write>(writer: &mut W, settings: &Settings) -> crate::Result<()> {
-  // Prefer ICO files.
-  for icon_path in settings.icon_files() {
-    let icon_path = icon_path?;
-    if icon_path.extension() == Some(OsStr::new("ico")) {
-      io::copy(&mut fs::File::open(icon_path)?, writer)?;
-      return Ok(());
-    }
-  }
-  // TODO: Convert from other formats.
-  Ok(())
+  Ok(vec![msi_path])
 }

+ 6 - 1
tools/rust/cargo-tauri-bundle/src/bundle/settings.rs

@@ -196,6 +196,11 @@ impl Settings {
         name.clone(),
       ),
     };
+    let binary_name = if cfg!(windows) {
+      format!("{}.{}", &binary_name, "exe")
+    } else {
+      binary_name
+    };
     let binary_path = target_dir.join(&binary_name);
     Ok(Settings {
       package,
@@ -247,7 +252,7 @@ impl Settings {
           - Stop at the first one found.
           - If one is found before reaching "/" then this folder belongs to that parent workspace
   */
-  fn get_workspace_dir(current_dir: &PathBuf) -> PathBuf {
+  pub fn get_workspace_dir(current_dir: &PathBuf) -> PathBuf {
     let mut dir = current_dir.clone();
     while dir.pop() {
       let set = CargoSettings::load(&dir);

+ 69 - 0
tools/rust/cargo-tauri-bundle/src/bundle/templates/main.wxs

@@ -0,0 +1,69 @@
+<?if $(sys.BUILDARCH)="x86"?>
+    <?define Win64 = "no" ?>
+    <?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
+<?elseif $(sys.BUILDARCH)="x64"?>
+    <?define Win64 = "yes" ?>
+    <?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
+<?else?>
+    <?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
+<?endif?>
+
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
+
+    <Product
+            Id="*"
+            Name="{{{product_name}}}"
+            UpgradeCode="{{{upgrade_code}}}"
+            Language="1033"
+            Codepage="1252"
+            Manufacturer="{{{manufacturer}}}"
+            Version="{{{version}}}">
+
+        <Package Id="*"
+                 Keywords="Installer"
+                 InstallerVersion="200"
+                 Languages="1033"
+                 Compressed="yes"
+                 InstallScope="perMachine"
+                 SummaryCodepage="1252"/>
+
+        <Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
+        <Icon Id="ProductIcon" SourceFile="{{{icon_path}}}"/>
+        <Property Id="ARPPRODUCTICON" Value="ProductIcon"/>
+        <Property Id="ARPNOREPAIR" Value="1"/>
+        <Property Id="ARPNOMODIFY" Value="1"/>
+
+        <Directory Id="TARGETDIR" Name="SourceDir">
+            <Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
+                <Directory Id="APPLICATIONFOLDER" Name="{{{product_name}}}">
+                    <Component Id="Path" Guid="{{{path_component_guid}}}" Win64="$(var.Win64)" KeyPath="yes">
+                        <File Id="PathFile" Source="{{{app_exe_source}}}" />
+                    </Component>
+                </Directory>
+            </Directory>
+        </Directory>
+
+        <Feature
+                Id="MainProgram"
+                Title="Application"
+                Description="Installs the executable."
+                Level="1"
+                ConfigurableDirectory="APPLICATIONFOLDER"
+                AllowAdvertise="no"
+                Display="expand"
+                Absent="disallow">
+
+            <Feature
+                    Id="Environment"
+                    Title="PATH Environment Variable"
+                    Description="Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location."
+                    Level="1"
+                    Absent="allow">
+                <ComponentRef Id="Path"/>
+            </Feature>
+        </Feature>
+
+        <SetProperty Id="ARPINSTALLLOCATION" Value="[APPLICATIONFOLDER]" After="CostFinalize"/>
+
+    </Product>
+</Wix>

+ 371 - 0
tools/rust/cargo-tauri-bundle/src/bundle/wix.rs

@@ -0,0 +1,371 @@
+use super::common;
+use super::settings::Settings;
+use handlebars::Handlebars;
+use lazy_static::lazy_static;
+use sha2::Digest;
+
+use std::collections::BTreeMap;
+use std::fs::{create_dir_all, remove_dir_all, write, File};
+use std::io::{BufRead, BufReader, Cursor, Read, Write};
+use std::path::{Path, PathBuf};
+use std::process::{Command, Stdio};
+use uuid::Uuid;
+use zip::ZipArchive;
+
+// URLS for the WIX toolchain.  Can be used for crossplatform compilation.
+pub const WIX_URL: &str =
+  "https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip";
+pub const WIX_SHA256: &str = "37f0a533b0978a454efb5dc3bd3598becf9660aaf4287e55bf68ca6b527d051d";
+
+// For Cross Platform Complilation.
+
+// const VC_REDIST_X86_URL: &str =
+//     "https://download.visualstudio.microsoft.com/download/pr/c8edbb87-c7ec-4500-a461-71e8912d25e9/99ba493d660597490cbb8b3211d2cae4/vc_redist.x86.exe";
+
+// const VC_REDIST_X86_SHA256: &str =
+//   "3a43e8a55a3f3e4b73d01872c16d47a19dd825756784f4580187309e7d1fcb74";
+
+// const VC_REDIST_X64_URL: &str =
+//     "https://download.visualstudio.microsoft.com/download/pr/9e04d214-5a9d-4515-9960-3d71398d98c3/1e1e62ab57bbb4bf5199e8ce88f040be/vc_redist.x64.exe";
+
+// const VC_REDIST_X64_SHA256: &str =
+//   "d6cd2445f68815fe02489fafe0127819e44851e26dfbe702612bc0d223cbbc2b";
+
+// A v4 UUID that was generated specifically for cargo-bundle, to be used as a
+// namespace for generating v5 UUIDs from bundle identifier strings.
+const UUID_NAMESPACE: [u8; 16] = [
+  0xfd, 0x85, 0x95, 0xa8, 0x17, 0xa3, 0x47, 0x4e, 0xa6, 0x16, 0x76, 0x14, 0x8d, 0xfa, 0x0c, 0x7b,
+];
+
+// setup for the main.wxs template file using handlebars. Dynamically changes the template on compilation based on the application metadata.
+lazy_static! {
+  static ref HANDLEBARS: Handlebars = {
+    let mut handlebars = Handlebars::new();
+
+    handlebars
+      .register_template_string("main.wxs", include_str!("templates/main.wxs"))
+      .unwrap();
+    handlebars
+  };
+}
+
+// Function used to download Wix and VC_REDIST. Checks SHA256 to verify the download.
+fn download_and_verify(url: &str, hash: &str) -> crate::Result<Vec<u8>> {
+  common::print_info(format!("Downloading {}", url).as_str())?;
+
+  let mut response = reqwest::get(url).or_else(|e| Err(e.to_string()))?;
+
+  let mut data: Vec<u8> = Vec::new();
+
+  response
+    .read_to_end(&mut data)
+    .or_else(|e| Err(e.to_string()))?;
+
+  common::print_info("validating hash")?;
+
+  let mut hasher = sha2::Sha256::new();
+  hasher.input(&data);
+
+  let url_hash = hasher.result().to_vec();
+  let expected_hash = hex::decode(hash).or_else(|e| Err(e.to_string()))?;
+
+  if expected_hash == url_hash {
+    Ok(data)
+  } else {
+    Err(crate::Error::from("hash mismatch of downloaded file"))
+  }
+}
+
+fn app_installer_dir(settings: &Settings) -> crate::Result<PathBuf> {
+  let arch = match settings.binary_arch() {
+    "x86_64" => "x86",
+    "x64" => "x64",
+    target => {
+      return Err(crate::Error::from(format!(
+        "Unsupported architecture: {}",
+        target
+      )))
+    }
+  };
+
+  Ok(settings.project_out_directory().to_path_buf().join(format!(
+    "{}.{}.msi",
+    settings.bundle_name(),
+    arch
+  )))
+}
+
+// Extracts the zips from Wix and VC_REDIST into a useable path.
+fn extract_zip(data: &Vec<u8>, path: &Path) -> crate::Result<()> {
+  let cursor = Cursor::new(data);
+
+  let mut zipa = ZipArchive::new(cursor).or_else(|e| Err(e.to_string()))?;
+
+  for i in 0..zipa.len() {
+    let mut file = zipa.by_index(i).or_else(|e| Err(e.to_string()))?;
+    let dest_path = path.join(file.name());
+    let parent = dest_path.parent().unwrap();
+
+    if !parent.exists() {
+      create_dir_all(parent).or_else(|e| Err(e.to_string()))?;
+    }
+
+    let mut buff: Vec<u8> = Vec::new();
+    file
+      .read_to_end(&mut buff)
+      .or_else(|e| Err(e.to_string()))?;
+    let mut fileout = File::create(dest_path).unwrap();
+
+    fileout.write_all(&buff).or_else(|e| Err(e.to_string()))?;
+  }
+
+  Ok(())
+}
+
+// Generates the UUID for the Wix template.
+fn generate_package_guid(settings: &Settings) -> Uuid {
+  let namespace = Uuid::from_bytes(&UUID_NAMESPACE).unwrap();
+  Uuid::new_v5(&namespace, &settings.bundle_identifier())
+}
+
+// Specifically goes and gets Wix and verifies the download via Sha256
+
+pub fn get_and_extract_wix(path: &Path) -> crate::Result<()> {
+  common::print_info("Verifying wix package")?;
+
+  let data = download_and_verify(WIX_URL, WIX_SHA256)?;
+
+  common::print_info("extracting WIX")?;
+
+  extract_zip(&data, path)
+}
+
+// For if bundler needs DLL files.
+
+// fn run_heat_exe(
+//   wix_toolset_path: &Path,
+//   build_path: &Path,
+//   harvest_dir: &Path,
+//   platform: &str,
+// ) -> Result<(), String> {
+//   let mut args = vec!["dir"];
+
+//   let harvest_str = harvest_dir.display().to_string();
+
+//   args.push(&harvest_str);
+//   args.push("-platform");
+//   args.push(platform);
+//   args.push("-cg");
+//   args.push("AppFiles");
+//   args.push("-dr");
+//   args.push("APPLICATIONFOLDER");
+//   args.push("-gg");
+//   args.push("-srd");
+//   args.push("-out");
+//   args.push("appdir.wxs");
+//   args.push("-var");
+//   args.push("var.SourceDir");
+
+//   let heat_exe = wix_toolset_path.join("heat.exe");
+
+//   let mut cmd = Command::new(&heat_exe)
+//     .args(&args)
+//     .stdout(Stdio::piped())
+//     .current_dir(build_path)
+//     .spawn()
+//     .expect("error running heat.exe");
+
+//   {
+//     let stdout = cmd.stdout.as_mut().unwrap();
+//     let reader = BufReader::new(stdout);
+
+//     for line in reader.lines() {
+//       info!(logger, "{}", line.unwrap());
+//     }
+//   }
+
+//   let status = cmd.wait().unwrap();
+//   if status.success() {
+//     Ok(())
+//   } else {
+//     Err("error running heat.exe".to_string())
+//   }
+// }
+
+// Runs the Candle.exe executable for Wix.  Candle parses the wxs file and generates the code for building the installer.
+fn run_candle(
+  settings: &Settings,
+  wix_toolset_path: &Path,
+  build_path: &Path,
+  wxs_file_name: &str,
+) -> crate::Result<()> {
+  let arch = match settings.binary_arch() {
+    "x86_64" => "x64",
+    "x86" => "x86",
+    target => {
+      return Err(crate::Error::from(format!(
+        "unsupported target: {}",
+        target
+      )))
+    }
+  };
+
+  let args = vec![
+    "-arch".to_string(),
+    arch.to_string(),
+    wxs_file_name.to_string(),
+    format!("-dSourceDir={}", settings.binary_path().display()),
+  ];
+
+  let candle_exe = wix_toolset_path.join("candle.exe");
+  common::print_info(format!("running candle for {}", wxs_file_name).as_str())?;
+
+  let mut cmd = Command::new(&candle_exe)
+    .args(&args)
+    .stdout(Stdio::piped())
+    .current_dir(build_path)
+    .spawn()
+    .expect("error running candle.exe");
+  {
+    let stdout = cmd.stdout.as_mut().unwrap();
+    let reader = BufReader::new(stdout);
+
+    for line in reader.lines() {
+      common::print_info(line.unwrap().as_str())?;
+    }
+  }
+
+  let status = cmd.wait().unwrap();
+  if status.success() {
+    Ok(())
+  } else {
+    Err(crate::Error::from("error running candle.exe"))
+  }
+}
+
+// Runs the Light.exe file.  Light takes the generated code from Candle and produces an MSI Installer.
+fn run_light(
+  wix_toolset_path: &Path,
+  build_path: &Path,
+  wixobjs: &[&str],
+  output_path: &Path,
+) -> crate::Result<PathBuf> {
+  let light_exe = wix_toolset_path.join("light.exe");
+
+  let mut args: Vec<String> = vec!["-o".to_string(), output_path.display().to_string()];
+
+  for p in wixobjs {
+    args.push(p.to_string());
+  }
+
+  common::print_info(format!("running light to produce {}", output_path.display()).as_str())?;
+
+  let mut cmd = Command::new(&light_exe)
+    .args(&args)
+    .stdout(Stdio::piped())
+    .current_dir(build_path)
+    .spawn()
+    .expect("error running light.exe");
+  {
+    let stdout = cmd.stdout.as_mut().unwrap();
+    let reader = BufReader::new(stdout);
+
+    for line in reader.lines() {
+      common::print_info(line.unwrap().as_str())?;
+    }
+  }
+
+  let status = cmd.wait().unwrap();
+  if status.success() {
+    Ok(output_path.to_path_buf())
+  } else {
+    Err(crate::Error::from("error running light.exe"))
+  }
+}
+
+// fn get_icon_data() -> crate::Result<()> {
+//   Ok(())
+// }
+
+// Entry point for bundling and creating the MSI installer.  For now the only supported platform is Windows x64.
+pub fn build_wix_app_installer(
+  settings: &Settings,
+  wix_toolset_path: &Path,
+) -> crate::Result<PathBuf> {
+  let arch = match settings.binary_arch() {
+    "x86_64" => "x64",
+    "x86" => "x86",
+    target => {
+      return Err(crate::Error::from(format!(
+        "unsupported target: {}",
+        target
+      )))
+    }
+  };
+
+  // common::print_warning("Only x64 supported")?;
+  // target only supports x64.
+  common::print_info(format!("Target: {}", arch).as_str())?;
+
+  let output_path = settings.project_out_directory().join("wix").join(arch);
+
+  let mut data = BTreeMap::new();
+
+  data.insert("product_name", settings.bundle_name());
+  data.insert("version", settings.version_string());
+  let manufacturer = settings.bundle_identifier().to_string();
+  data.insert("manufacturer", manufacturer.as_str());
+  let upgrade_code = Uuid::new_v5(
+    &uuid::NAMESPACE_DNS,
+    format!("{}.app.x64", &settings.binary_name()).as_str(),
+  )
+  .to_string();
+
+  data.insert("upgrade_code", &upgrade_code.as_str());
+
+  let path_guid = generate_package_guid(settings).to_string();
+  data.insert("path_component_guid", &path_guid.as_str());
+
+  let app_exe_name = settings.binary_name().to_string();
+  data.insert("app_exe_name", &app_exe_name);
+
+  let app_exe_source = settings.binary_path().display().to_string();
+
+  data.insert("app_exe_source", &app_exe_source);
+
+  let image_path = PathBuf::from("../../../../icons/icon.ico")
+    .display()
+    .to_string();
+
+  data.insert("icon_path", &image_path);
+
+  let temp = HANDLEBARS
+    .render("main.wxs", &data)
+    .or_else(|e| Err(e.to_string()))?;
+
+  if output_path.exists() {
+    remove_dir_all(&output_path).or_else(|e| Err(e.to_string()))?;
+  }
+
+  create_dir_all(&output_path).or_else(|e| Err(e.to_string()))?;
+
+  let main_wxs_path = output_path.join("main.wxs");
+  write(&main_wxs_path, temp).or_else(|e| Err(e.to_string()))?;
+
+  let input_basenames = vec!["main"];
+
+  for basename in &input_basenames {
+    let wxs = format!("{}.wxs", basename);
+    run_candle(settings, &wix_toolset_path, &output_path, &wxs)?;
+  }
+
+  let wixobjs = vec!["main.wixobj"];
+  let target = run_light(
+    &wix_toolset_path,
+    &output_path,
+    &wixobjs,
+    &app_installer_dir(settings)?,
+  )?;
+
+  Ok(target)
+}

Some files were not shown because too many files changed in this diff