mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
WIP MSI
bat scripting to work around Rust CLI argument parsing on windows
This commit is contained in:
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -2319,15 +2319,6 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waitpid-any"
|
||||
version = "0.3.0"
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
pub fn install(pkg: &mut BundleZip, install_to: (PathBuf, bool), start_args: Option<Vec<&str>>) -> Result<()> {
|
||||
pub fn install(pkg: &mut BundleZip, install_to: (PathBuf, bool), is_bootstrap_install: bool, start_args: Option<Vec<&str>>) -> Result<()> {
|
||||
// find and parse nuspec
|
||||
info!("Reading package manifest...");
|
||||
let app = pkg.read_manifest()?;
|
||||
@@ -80,7 +80,7 @@ pub fn install(pkg: &mut BundleZip, install_to: (PathBuf, bool), start_args: Opt
|
||||
|
||||
let mut root_path_renamed = String::new();
|
||||
// does the target directory exist and have files? (eg. already installed)
|
||||
if !shared::is_dir_empty(&root_path) {
|
||||
if !shared::is_dir_empty(&root_path) && !is_bootstrap_install {
|
||||
// the target directory is not empty, and not dead
|
||||
if !dialogs::show_overwrite_repair_dialog(&app, &root_path, root_is_default) {
|
||||
// user cancelled overwrite prompt
|
||||
@@ -104,9 +104,11 @@ pub fn install(pkg: &mut BundleZip, install_to: (PathBuf, bool), start_args: Opt
|
||||
})?;
|
||||
}
|
||||
|
||||
info!("Preparing and cleaning installation directory...");
|
||||
remove_dir_all::ensure_empty_dir(&root_path)?;
|
||||
|
||||
if !is_bootstrap_install {
|
||||
info!("Preparing and cleaning installation directory...");
|
||||
remove_dir_all::ensure_empty_dir(&root_path)?;
|
||||
}
|
||||
|
||||
info!("Acquiring lock...");
|
||||
let paths = create_config_from_root_dir(&root_path);
|
||||
let locator = VelopackLocator::new_with_manifest(paths, app);
|
||||
@@ -122,7 +124,7 @@ pub fn install(pkg: &mut BundleZip, install_to: (PathBuf, bool), start_args: Opt
|
||||
windows::splash::show_splash_dialog(locator.get_manifest_title(), splash_bytes)
|
||||
};
|
||||
|
||||
let install_result = install_impl(pkg, &locator, &tx, start_args);
|
||||
let install_result = install_impl(pkg, &locator, &tx, is_bootstrap_install, start_args);
|
||||
let _ = tx.send(windows::splash::MSG_CLOSE);
|
||||
|
||||
if install_result.is_ok() {
|
||||
@@ -149,6 +151,7 @@ fn install_impl(
|
||||
pkg: &mut BundleZip,
|
||||
locator: &VelopackLocator,
|
||||
tx: &std::sync::mpsc::Sender<i16>,
|
||||
is_bootstrap_install: bool,
|
||||
start_args: Option<Vec<&str>>,
|
||||
) -> Result<()> {
|
||||
info!("Starting installation!");
|
||||
@@ -195,7 +198,11 @@ fn install_impl(
|
||||
}
|
||||
|
||||
let _ = tx.send(100);
|
||||
windows::registry::write_uninstall_entry(&locator)?;
|
||||
if is_bootstrap_install {
|
||||
info!("Skipping uninstall entry creation.");
|
||||
} else {
|
||||
windows::registry::write_uninstall_entry(&locator)?;
|
||||
}
|
||||
|
||||
if !dialogs::get_silent() {
|
||||
info!("Starting app...");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
//#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[macro_use]
|
||||
@@ -67,6 +67,7 @@ fn main_inner() -> Result<()> {
|
||||
.arg(arg!(-v --verbose "Print debug messages to console"))
|
||||
.arg(arg!(-l --log <FILE> "Enable file logging and set location").required(false).value_parser(value_parser!(PathBuf)))
|
||||
.arg(arg!(-t --installto <DIR> "Installation directory to install the application").required(false).value_parser(value_parser!(PathBuf)))
|
||||
.arg(arg!(-b --bootstrap "Just apply install files, do not write uninstall registry keys"))
|
||||
.arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..));
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
@@ -114,6 +115,7 @@ fn run_inner(arg_config: Command) -> Result<()> {
|
||||
let debug = matches.get_one::<PathBuf>("debug");
|
||||
let install_to = matches.get_one::<PathBuf>("installto");
|
||||
let exe_args: Option<Vec<&str>> = matches.get_many::<String>("EXE_ARGS").map(|v| v.map(|f| f.as_str()).collect());
|
||||
let is_bootstrap_install = matches.get_flag("bootstrap");
|
||||
|
||||
info!("Starting Velopack Setup ({})", env!("NGBV_VERSION"));
|
||||
info!(" Location: {:?}", env::current_exe()?);
|
||||
@@ -123,6 +125,7 @@ fn run_inner(arg_config: Command) -> Result<()> {
|
||||
info!(" Install To: {:?}", install_to);
|
||||
if cfg!(debug_assertions) {
|
||||
info!(" Debug: {:?}", debug);
|
||||
info!(" Bootstrap: {:?}", is_bootstrap_install);
|
||||
}
|
||||
|
||||
// change working directory to the containing directory of the exe
|
||||
@@ -225,6 +228,6 @@ fn run_inner(arg_config: Command) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
commands::install(&mut bundle, (root_path, root_is_default), exe_args)?;
|
||||
commands::install(&mut bundle, (root_path, root_is_default), is_bootstrap_install, exe_args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[macro_use]
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
|
||||
let formatted_date = format!("{}{:02}{:02}", now.year(), now.month(), now.day());
|
||||
|
||||
let uninstall_cmd = format!("\"{}\" --uninstall", updater_path);
|
||||
let uninstall_quiet = format!("\"{}\" --uninstall --silent", updater_path);
|
||||
let uninstall_quiet: String = format!("\"{}\" --uninstall --silent", updater_path);
|
||||
|
||||
let reg_uninstall =
|
||||
w::HKEY::CURRENT_USER.RegCreateKeyEx(UNINSTALL_REGISTRY_KEY, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
|
||||
|
||||
@@ -11,6 +11,7 @@ use winsafe::{self as w, co};
|
||||
use velopack::bundle::load_bundle_from_file;
|
||||
use velopack::locator::{auto_locate_app_manifest, LocationContext};
|
||||
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
pub fn test_install_apply_uninstall() {
|
||||
|
||||
@@ -21,7 +21,9 @@ namespace Velopack
|
||||
/// <summary> An application installer archive. </summary>
|
||||
Installer = 4,
|
||||
/// <summary> A Windows Installer package (.msi) for the deployment tool.</summary>
|
||||
MsiDeploymentTool = 5
|
||||
MsiDeploymentTool = 5,
|
||||
/// <summary> A Windows Installer package (.msi) that bootstraps the installer.</summary>
|
||||
Msi = 6,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -97,7 +97,7 @@ fn get_known_folder(rfid: *const GUID) -> Result<String, Error> {
|
||||
#[cfg(target_os = "windows")]
|
||||
fn pwstr_to_string(input: PWSTR) -> Result<String, Error> {
|
||||
unsafe {
|
||||
let hstring = input.to_hstring()?;
|
||||
let hstring = input.to_hstring();
|
||||
let string = hstring.to_string_lossy();
|
||||
Ok(string.trim_end_matches('\0').to_string())
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ public class PackTask : MSBuildAsyncTask
|
||||
|
||||
public string? Compression { get; set; }
|
||||
|
||||
public bool BuildMsiDeploymentTool { get; set; }
|
||||
public bool BuildMsi { get; set; }
|
||||
|
||||
public string? MsiVersionOverride { get; set; }
|
||||
|
||||
@@ -41,7 +41,7 @@ public static class DefaultName
|
||||
}
|
||||
|
||||
|
||||
public static string GetSuggestedMsiName(string id, string channel, RuntimeOs os)
|
||||
public static string GetSuggestedMsiDeploymentToolName(string id, string channel, RuntimeOs os)
|
||||
{
|
||||
var suffix = GetUniqueAssetSuffix(channel);
|
||||
if (os == RuntimeOs.Windows)
|
||||
@@ -50,6 +50,15 @@ public static class DefaultName
|
||||
throw new PlatformNotSupportedException("Platform not supported.");
|
||||
}
|
||||
|
||||
public static string GetSuggestedMsiName(string id, string channel, RuntimeOs os)
|
||||
{
|
||||
var suffix = GetUniqueAssetSuffix(channel);
|
||||
if (os == RuntimeOs.Windows)
|
||||
return $"{id}{suffix}.msi";
|
||||
else
|
||||
throw new PlatformNotSupportedException("Platform not supported.");
|
||||
}
|
||||
|
||||
private static string GetUniqueAssetSuffix(string channel)
|
||||
{
|
||||
return "-" + channel;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Globalization;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NuGet.Versioning;
|
||||
using Velopack.Compression;
|
||||
using Velopack.Core;
|
||||
@@ -220,10 +220,16 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
Log.Debug($"Setup bundle created '{Path.GetFileName(targetSetupExe)}'.");
|
||||
setupExeProgress(100);
|
||||
|
||||
if (Options.BuildMsiDeploymentTool && VelopackRuntimeInfo.IsWindows) {
|
||||
var msiName = DefaultName.GetSuggestedMsiDeploymentToolName(Options.PackId, Options.Channel, TargetOs);
|
||||
var msiPath = createAsset(msiName, VelopackAssetType.MsiDeploymentTool);
|
||||
CompileWixTemplateToMsiDeploymentTool(msiProgress, targetSetupExe, msiPath);
|
||||
}
|
||||
|
||||
if (Options.BuildMsi && VelopackRuntimeInfo.IsWindows) {
|
||||
var msiName = DefaultName.GetSuggestedMsiName(Options.PackId, Options.Channel, TargetOs);
|
||||
var msiPath = createAsset(msiName, VelopackAssetType.MsiDeploymentTool);
|
||||
CompileWixTemplateToMsi(msiProgress, targetSetupExe, msiPath);
|
||||
var msiPath = createAsset(msiName, VelopackAssetType.Msi);
|
||||
CompileWixTemplateToMsi(msiProgress, releasePkg, packDir, msiPath);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
@@ -261,7 +267,6 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
return dict;
|
||||
}
|
||||
|
||||
|
||||
private void CreateExecutableStubForExe(string exeToCopy, string targetStubPath)
|
||||
{
|
||||
if (!File.Exists(exeToCopy)) {
|
||||
@@ -345,6 +350,126 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private void CompileWixTemplateToMsi(Action<int> progress,
|
||||
string releasePkg, string setupExePath, string msiFilePath)
|
||||
{
|
||||
bool packageAs64Bit =
|
||||
Options.TargetRuntime.Architecture is not RuntimeCpu.x86;
|
||||
|
||||
Log.Info($"Compiling msi installer tool in {(packageAs64Bit ? "64-bit" : "32-bit")} mode");
|
||||
|
||||
var outputDirectory = Path.GetDirectoryName(releasePkg);
|
||||
var culture = CultureInfo.GetCultureInfo("en-US").TextInfo.ANSICodePage;
|
||||
|
||||
// WiX Identifiers may contain ASCII characters A-Z, a-z, digits, underscores (_), or
|
||||
// periods(.). Every identifier must begin with either a letter or an underscore.
|
||||
var wixId = Regex.Replace(Options.PackId, @"[^\w\.]", "_");
|
||||
if (char.GetUnicodeCategory(wixId[0]) == UnicodeCategory.DecimalDigitNumber)
|
||||
wixId = "_" + wixId;
|
||||
|
||||
var msiVersion = Options.MsiVersionOverride;
|
||||
if (string.IsNullOrWhiteSpace(msiVersion)) {
|
||||
var parsedVersion = SemanticVersion.Parse(Options.PackVersion);
|
||||
msiVersion = $"{parsedVersion.Major}.{parsedVersion.Minor}.{parsedVersion.Patch}.0";
|
||||
}
|
||||
|
||||
static string SanitizeDirectoryString(string name)
|
||||
=> name;//TODO
|
||||
|
||||
string wixVersion = "5.0.2";
|
||||
|
||||
string wixProjectFile = $"""
|
||||
<Project Sdk="WixToolset.Sdk/{wixVersion}">
|
||||
<PropertyGroup>
|
||||
<InstallerPlatform>{(packageAs64Bit ? "x64" : "x86")}</InstallerPlatform>
|
||||
<TargetFileName>{Path.GetFileName(msiFilePath)}</TargetFileName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
//Scope can be perMachine or perUser or perUserOrMachine, https://docs.firegiant.com/wix/schema/wxs/packagescopetype/
|
||||
//TODO: It is recommended to use ID rather than UpgradeCode. But this should be a namespaced id. This could probably just be our wixId above
|
||||
string wixPackage = $"""
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Package Name="{GetEffectiveTitle()}"
|
||||
Manufacturer="{GetEffectiveAuthors()}"
|
||||
Version="{msiVersion}"
|
||||
Codepage="{culture}"
|
||||
Language="1033"
|
||||
Scope="perMachine"
|
||||
UpgradeCode="{GuidUtil.CreateGuidFromHash($"{Options.PackId}:UpgradeCode")}"
|
||||
>
|
||||
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
|
||||
<StandardDirectory Id="{(packageAs64Bit ? "ProgramFiles64Folder" : "ProgramFiles6432Folder")}">
|
||||
<Directory Id="INSTALLFOLDER" Name="{SanitizeDirectoryString(GetEffectiveAuthors())}">
|
||||
<Directory Name="current" />
|
||||
<Directory Id="PACKAGES_DIR" Name="packages" />
|
||||
</Directory>
|
||||
</StandardDirectory>
|
||||
|
||||
<File Id="SetupExe" Source="{setupExePath}" KeyPath="yes" />
|
||||
<File Id="InstallBat" Source="install.bat" KeyPath="yes" />
|
||||
|
||||
<!--
|
||||
Impersonate="no" is required for the script to run with elevation.
|
||||
This then breaks user checks such as attempting to find the user's Desktop location for shortcuts.
|
||||
https://docs.firegiant.com/wix/schema/wxs/customaction/
|
||||
-->
|
||||
<CustomAction Id="RunSetup"
|
||||
Directory="INSTALLFOLDER"
|
||||
ExeCommand="[INSTALLFOLDER]install.bat"
|
||||
Impersonate="yes"
|
||||
TerminalServerAware="yes"
|
||||
Execute="deferred"
|
||||
Return="check" />
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<!-- https://docs.firegiant.com/wix3/xsd/wix/installexecutesequence/-->
|
||||
<Custom Action="RunSetup" After="InstallFiles" />
|
||||
</InstallExecuteSequence>
|
||||
|
||||
</Package>
|
||||
</Wix>
|
||||
""";
|
||||
//<Files Include="{packDir}\**" />
|
||||
string installBatchScript = $"""
|
||||
@REM @echo off
|
||||
|
||||
set CURRENT_DIR=%~dp0:~0,-1%
|
||||
set SETUP_EXE="%CURRENT_DIR%\{Path.GetFileName(setupExePath)}"
|
||||
|
||||
%SETUP_EXE% -s --bootstrap -t "%CURRENT_DIR%"
|
||||
""";
|
||||
//File.Copy(Path.Combine(packDir, "Squirrel.exe"), Path.Combine(dir.FullName, "Update.exe"), true);
|
||||
|
||||
|
||||
var wixproj = Path.Combine(outputDirectory, wixId + ".wixproj");
|
||||
var wxs = Path.Combine(outputDirectory, wixId + ".wxs");
|
||||
try {
|
||||
File.WriteAllText(Path.Combine(outputDirectory, "install.bat"), installBatchScript, Encoding.UTF8);
|
||||
File.WriteAllText(wixproj, wixProjectFile, Encoding.UTF8);
|
||||
File.WriteAllText(wxs, wixPackage, Encoding.UTF8);
|
||||
|
||||
//TODO: Allow for some level of customization
|
||||
|
||||
progress(30);
|
||||
|
||||
// NB: Assuming dotnet is installed
|
||||
Log.Info("Compiling WiX Template (dotnet build)");
|
||||
var buildCommand = $"dotnet build -c Release \"{wixproj}\" -o \"{Path.GetDirectoryName(msiFilePath)}\"";
|
||||
//Debugger.Launch();
|
||||
_ = Exe.RunHostedCommand(buildCommand);
|
||||
|
||||
progress(90);
|
||||
|
||||
} finally {
|
||||
IoUtil.DeleteFileOrDirectoryHard(wixproj, throwOnFailure: false);
|
||||
IoUtil.DeleteFileOrDirectoryHard(wxs, throwOnFailure: false);
|
||||
}
|
||||
progress(100);
|
||||
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private void CompileWixTemplateToMsiDeploymentTool(Action<int> progress,
|
||||
string setupExePath, string msiFilePath)
|
||||
{
|
||||
bool packageAs64Bit =
|
||||
@@ -391,23 +516,23 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
"ProgramFilesFolder" => packageAs64Bit ? "ProgramFiles64Folder" : "ProgramFilesFolder",
|
||||
"Win64YesNo" => packageAs64Bit ? "yes" : "no",
|
||||
"SetupName" => setupName,
|
||||
_ when key.StartsWith("IdAsGuid") => GuidUtil.CreateGuidFromHash($"{Options.PackId}:{key.Substring(8)}").ToString(),
|
||||
_ when key.StartsWith("IdAsGuid", StringComparison.OrdinalIgnoreCase) => GuidUtil.CreateGuidFromHash($"{Options.PackId}:{key.Substring("IdAsGuid".Length)}").ToString(),
|
||||
_ => match.Value,
|
||||
};
|
||||
});
|
||||
|
||||
File.WriteAllText(wxsFile, templateResult, Encoding.UTF8);
|
||||
|
||||
// Candle reprocesses and compiles WiX source files into object files (.wixobj).
|
||||
// Candle preprocesses and compiles WiX source files into object files (.wixobj).
|
||||
Log.Info("Compiling WiX Template (candle.exe)");
|
||||
var candleCommand = $"{HelperFile.WixCandlePath} -nologo -ext WixNetFxExtension -out \"{objFile}\" \"{wxsFile}\"";
|
||||
var candleCommand = $"{HelperFile.WixCandlePath} -nologo -out \"{objFile}\" \"{wxsFile}\"";
|
||||
_ = Exe.RunHostedCommand(candleCommand);
|
||||
|
||||
progress(45);
|
||||
|
||||
// Light links and binds one or more .wixobj files and creates a Windows Installer database (.msi or .msm).
|
||||
Log.Info("Linking WiX Template (light.exe)");
|
||||
var lightCommand = $"{HelperFile.WixLightPath} -ext WixNetFxExtension -spdb -sval -out \"{msiFilePath}\" \"{objFile}\"";
|
||||
var lightCommand = $"{HelperFile.WixLightPath} -spdb -sval -out \"{msiFilePath}\" \"{objFile}\"";
|
||||
_ = Exe.RunHostedCommand(lightCommand);
|
||||
|
||||
progress(90);
|
||||
|
||||
@@ -25,6 +25,7 @@ public class WindowsPackOptions : WindowsReleasifyOptions, INugetPackCommand, IP
|
||||
public string Shortcuts { get; set; }
|
||||
|
||||
public bool BuildMsi { get; set; }
|
||||
public bool BuildMsiDeploymentTool { get; set; }
|
||||
|
||||
public string MsiVersionOverride { get; set; }
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public class WindowsPackCommand : PackCommand
|
||||
public string Shortcuts { get; private set; }
|
||||
|
||||
public bool BuildMsi { get; private set; }
|
||||
public bool BuildMsiDeploymentTool { get; private set; }
|
||||
|
||||
public string MsiVersionOverride { get; private set; }
|
||||
|
||||
@@ -74,15 +75,20 @@ public class WindowsPackCommand : PackCommand
|
||||
|
||||
this.AreMutuallyExclusive(signTemplate, signParams, azTrustedSign);
|
||||
|
||||
AddOption<bool>((v) => BuildMsi = v, "--msiDeploymentTool")
|
||||
AddOption<bool>((v) => BuildMsiDeploymentTool = v, "--msiDeploymentTool")
|
||||
.SetDescription("Compile a .msi machine-wide deployment tool.")
|
||||
.SetHidden();
|
||||
;//.SetHidden();
|
||||
|
||||
AddOption<string>((v) => MsiVersionOverride = v, "--msiDeploymentToolVersion")
|
||||
.SetDescription("Override the product version for the generated msi.")
|
||||
.SetArgumentHelpName("VERSION")
|
||||
.SetHidden()
|
||||
//.SetHidden()
|
||||
.MustBeValidMsiVersion();
|
||||
|
||||
AddOption<bool>((v) => BuildMsi = v, "--msi")
|
||||
.SetDescription("Compile a .msi machine-wide bootstrap package.")
|
||||
;//.SetHidden();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
6
vendor/wix/template.wxs
vendored
6
vendor/wix/template.wxs
vendored
@@ -5,12 +5,6 @@
|
||||
<MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A later version of this product is already installed. Setup will now exit."/>
|
||||
<Media Id="1" Cabinet="contents.cab" EmbedCab="yes" CompressionLevel="high"/>
|
||||
|
||||
<PropertyRef Id="NETFRAMEWORK45" />
|
||||
|
||||
<Condition Message="This application requires .NET Framework 4.5 or higher. Please install the latest .NET Framework then run this installer again.">
|
||||
<![CDATA[Installed OR NETFRAMEWORK45]]>
|
||||
</Condition>
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="{{ProgramFilesFolder}}">
|
||||
<Directory Id="APPLICATIONROOTDIRECTORY" Name="{{Title}} Deployment Tool" />
|
||||
|
||||
Reference in New Issue
Block a user