mirror of https://github.com/tauri-apps/tauri
Merge branch 'tensor-programming-msi-experimental' into dev
This commit is contained in:
commit
8adb667229
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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")?;
|
||||
let wix_path = PathBuf::from("./WixTools");
|
||||
|
||||
// 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")?;
|
||||
|
||||
// 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()?;
|
||||
let msi_path = wix::build_wix_app_installer(&settings, &wix_path)?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue