Skip to content

More work on MSI installer #1234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 67 additions & 17 deletions src/rustup-win-installer/msi/rustup.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,24 @@
<Product Id="*" Name="rustup" Language="1033" Version="$(env.CFG_VER_MAJOR).$(env.CFG_VER_MINOR).$(env.CFG_VER_PATCH).0" Manufacturer="The Rust Project Developers" UpgradeCode="09acbb1c-7123-44ac-b2a9-4a04b28ced11">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perUser" />

<Condition Message="The Rust toolchain is only supported on Windows 7, Windows Server 2008 R2, or higher.">
<![CDATA[Installed OR (VersionNT >= 601)]]>
</Condition>

<!-- TODO: How to configure updates? `AllowDowngrades` automatically removes previously installed versions, no matter what version they have -->
<MajorUpgrade AllowDowngrades="yes" />

<!-- Specifies a single cab file to be embedded in the installer's .msi. -->
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />

<Feature Id="ProductFeature" Title="rustup" Level="1">
<ComponentRef Id="CompleteInstallation" />
<Feature Id="Rustup" Title="rustup" Absent="disallow" Display="1" AllowAdvertise="no">
<ComponentRef Id="RustupExe" />
<ComponentRef Id="RegisterProductCode" />
<ComponentRef Id="RegisterInstallDir" />
</Feature>
<!-- TODO: Add UI to actually let the user disable this Feature -->
<Feature Id="Path" Title="Add to PATH" Absent="allow" Description="Add Rust to PATH environment variable" Display="2" AllowAdvertise="no">
<ComponentRef Id="ModifyPathEnv" />
</Feature>

<!-- Set some metadata that will appear in the "Installed Programs" list -->
Expand All @@ -35,8 +45,6 @@
<UIRef Id="CustomUI" />
<WixVariable Id="WixUIDialogBmp" Value="dialogbg.bmp" />
<WixVariable Id="WixUIBannerBmp" Value="banner.bmp" />
<!-- TODO: Include/generate license file -->
<!--<WixVariable Id="WixUILicenseRtf" Value="LICENSE.rtf" />-->

<Directory Id="TARGETDIR" Name="SourceDir">
<!-- `INSTALLLOCATION` will be set by custom action -->
Expand All @@ -46,36 +54,78 @@
</Directory>

<DirectoryRef Id="INSTALLLOCATION_BINARY">
<Component Id="CompleteInstallation" Guid="df2ab9f7-7888-465c-98dd-bb58cbca68f7">
<Component Id="RustupExe" Guid="df2ab9f7-7888-465c-98dd-bb58cbca68f7">
<!-- Install the main rustup.exe binary -->
<File Source="$(var.RustupExe)" Name="rustup.exe" KeyPath="yes"/>
</Component>
<Component Id="RegisterInstallDir" Guid="*">
<!-- Remember the installation directory in the registry -->
<RegistryKey Root="HKCU" Key="Software\rustup">
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLLOCATION]" KeyPath="yes"/>
</RegistryKey>
</Component>
<Component Id="RegisterProductCode" Guid="*">
<!-- Write the product code to the registry, so we can use it to run the uninstaller -->
<RegistryKey Root="HKCU" Key="Software\rustup">
<RegistryValue Name="InstalledProductCode" Type="string" Value="[ProductCode]" KeyPath="yes" />
</RegistryKey>
<!-- Install the main rustup.exe binary -->
<File Source="$(var.RustupExe)" Name="rustup.exe"/>
<!-- Append to PATH environment variable -->
<Environment Id="PATH" Name="PATH" Value="[INSTALLLOCATION_BINARY]" Permanent="no" Part="first" Action="set" System="no" />
</Component>
<Component Id="ModifyPathEnv" Guid="*">
<!-- Prepend to PATH environment variable -->
<RegistryValue Root="HKCU" Key="Software\rustup" Name="PathEnv" Type="string" Value="1" KeyPath="yes" />
<Environment Id="PathEnv" Name="PATH" Value="[INSTALLLOCATION_BINARY]" Permanent="no" Part="first" Action="set" System="no" />
</Component>
</DirectoryRef>

<!-- Read installation path from the registry. This is required for uninstall and upgrade
and should only find something if rustup has already been installed via MSI. -->
<Property Id="INSTALLLOCATION_REGISTRY">
<RegistrySearch Id="SearchRegistryInstallLocation" Root="HKCU" Key="Software\rustup" Name="InstallDir" Type="raw" />
</Property>

<!-- Read environment variables `CARGO_HOME` and `RUSTUP_HOME`, because they won't be visible in custom actions later -->
<SetProperty Action="ReadCargoHome" Id="CARGO_HOME" Value="[%CARGO_HOME]" Before="AppSearch" Sequence="first" />
<SetProperty Action="SetCargoHomeOverride" Id="CARGO_HOME_OR_DEFAULT" Value="[CARGO_HOME]" After="ReadCargoHome" Sequence="first">CARGO_HOME</SetProperty>
<SetProperty Action="SetCargoHomeDefault" Id="CARGO_HOME_OR_DEFAULT" Value="[%USERPROFILE]\.cargo" After="ReadCargoHome" Sequence="first">NOT CARGO_HOME</SetProperty>

<SetProperty Action="ReadRustupHome" Id="RUSTUP_HOME" Value="[%RUSTUP_HOME]" Before="AppSearch" Sequence="first" />
<SetProperty Action="SetRustupHomeOverride" Id="RUSTUP_HOME_OR_DEFAULT" Value="[RUSTUP_HOME]" After="ReadRustupHome" Sequence="first">RUSTUP_HOME</SetProperty>
<SetProperty Action="SetRustupHomeDefault" Id="RUSTUP_HOME_OR_DEFAULT" Value="[%USERPROFILE]\.rustup" After="ReadRustupHome" Sequence="first">NOT RUSTUP_HOME</SetProperty>

<!-- Looks for `rustup.exe` in `%USERPROFILE%\.cargo\bin` or `%CARGO_HOME%\bin` respectively -->
<Property Id="RUSTUP_EXE_EXISTS">
<DirectorySearch Id="ExistingRustupExeSearch" Path="[CARGO_HOME_OR_DEFAULT]\bin" Depth="0">
<FileSearch Name="rustup.exe" />
</DirectorySearch>
</Property>

<!-- Looks for `%USERPROFILE%\.rustup` or `%RUSTUP_HOME%` respectively -->
<Property Id="RUSTUP_DIRECTORY_EXISTS">
<DirectorySearch Id="ExistingRustupDirSearch" Path="[RUSTUP_HOME_OR_DEFAULT]" Depth="0" AssignToProperty="yes" />
</Property>

<!-- This property will be set whenever a previous installation of rustup was found, even if it was not installed via MSI -->
<SetProperty Id="RUSTUP_EXISTS" Value="1" After="AppSearch" Sequence="first">RUSTUP_EXE_EXISTS AND RUSTUP_DIRECTORY_EXISTS</SetProperty>

<!-- Propagate correct value to INSTALLLOCATION directory -->
<SetDirectory Action="AssignInstallLocationExisting" Id="INSTALLLOCATION" Value="[INSTALLLOCATION_REGISTRY]" Sequence="first">INSTALLLOCATION_REGISTRY</SetDirectory>
<SetDirectory Action="AssignInstallLocationNew" Id="INSTALLLOCATION" Value="[CARGO_HOME_OR_DEFAULT]" Sequence="first">NOT INSTALLLOCATION_REGISTRY</SetDirectory>

<!-- Register the DLL containing the custom actions as an embedded binary -->
<Binary Id="RustupCustomActionDll" SourceFile="$(var.RustupCustomActionDll)"/>
<!-- Use a type 51 custom action to send options to deferred custom action `RustupInstall`
(can use arbitrary value that encodes all necessary properties and will be parsed from Rust) -->
<CustomAction Id="SetInstallOptions" Property="RustupInstall" Value="... we can pass arbitrary options here ..." />
<CustomAction Id="RustupSetInstallLocation" BinaryKey="RustupCustomActionDll" DllEntry="RustupSetInstallLocation" Execute="immediate" Return="check" Impersonate="yes"/>
<!-- Propagate the value of `RustupInstallLocation` (set by custom action) to `INSTALLLOCATION` -->
<CustomAction Id="AssignInstallLocation" Directory="INSTALLLOCATION" Value="[RustupInstallLocation]"/>
(can use arbitrary string that encodes all necessary properties and will be parsed from Rust) -->
<CustomAction Id="SetInstallOptions" Property="RustupInstall" Value="INSTALLLOCATION=[INSTALLLOCATION];RUSTUP_HOME=[RUSTUP_HOME_OR_DEFAULT];RUSTUP_EXISTS=[RUSTUP_EXISTS]" />
<CustomAction Id="SetUninstallOptions" Property="RustupUninstall" Value="INSTALLLOCATION=[INSTALLLOCATION];RUSTUP_HOME=[RUSTUP_HOME_OR_DEFAULT];RUSTUP_EXISTS=[RUSTUP_EXISTS]" />
<CustomAction Id="RustupInstall" BinaryKey="RustupCustomActionDll" DllEntry="RustupInstall" Execute="deferred" Return="check" Impersonate="yes"/>
<CustomAction Id="RustupUninstall" BinaryKey="RustupCustomActionDll" DllEntry="RustupUninstall" Execute="deferred" Return="check" Impersonate="yes"/>

<!-- Schedule our custom actions -->
<InstallExecuteSequence>
<DisableRollback Before="InstallInitialize"/>
<Custom Action="RustupSetInstallLocation" After="CostFinalize"/>
<Custom Action="AssignInstallLocation" After="RustupSetInstallLocation"/>
<Custom Action="SetInstallOptions" Before="InstallInitialize">NOT Installed</Custom>
<Custom Action="RustupInstall" After="InstallFiles">NOT Installed</Custom>
<!-- Run RustupUninstall only on true uninstall, not on upgrade -->
<Custom Action="SetUninstallOptions" Before="InstallInitialize">Installed AND (NOT UPGRADINGPRODUCTCODE)</Custom>
<Custom Action="RustupUninstall" After="RemoveFiles">Installed AND (NOT UPGRADINGPRODUCTCODE)</Custom>
</InstallExecuteSequence>

Expand Down
25 changes: 25 additions & 0 deletions src/rustup-win-installer/msi/test-install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This script can be used for manually testing the MSI installer. It is not used for AppVeyor CI.

$env:RUSTFLAGS="-Zunstable-options -Ctarget-feature=+crt-static"

pushd ..\..\..
# Build rustup.exe
cargo build --release --target i686-pc-windows-msvc --features msi-installed
popd
if($LastExitCode -ne 0) { exit $LastExitCode }
pushd ..
# Build the CA library
cargo build --release --target i686-pc-windows-msvc
popd
if($LastExitCode -ne 0) { exit $LastExitCode }
# Build the MSI
.\build.ps1 -Target i686-pc-windows-msvc
if($LastExitCode -ne 0) { exit $LastExitCode }
# Run the MSI with logging
$OLD_CARGO_HOME = $env:CARGO_HOME
$OLD_RUSTUP_HOME = $env:RUSTUP_HOME
$env:CARGO_HOME = "$env:USERPROFILE\.cargo-test"
$env:RUSTUP_HOME = "$env:USERPROFILE\.rustup-test"
Start-Process msiexec -ArgumentList "/i target\rustup.msi /L*V target\Install.log" -Wait
$env:CARGO_HOME = $OLD_CARGO_HOME
$env:RUSTUP_HOME = $OLD_RUSTUP_HOME
10 changes: 10 additions & 0 deletions src/rustup-win-installer/msi/test-uninstall.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Uninstall currently installed version of rustup. Does the same thing as `rustup self uninstall`.

$key = 'HKCU:\SOFTWARE\rustup'
$productCode = (Get-ItemProperty -Path $key -Name InstalledProductCode).InstalledProductCode

# No need to set CARGO_HOME, because the installation directory is stored in the registry
$OLD_RUSTUP_HOME = $env:RUSTUP_HOME
$env:RUSTUP_HOME = "$env:USERPROFILE\.rustup-test"
msiexec /x "$productCode" /L*V "target\Uninstall.log"
$env:RUSTUP_HOME = $OLD_RUSTUP_HOME
46 changes: 19 additions & 27 deletions src/rustup-win-installer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ extern crate rustup;

use std::ffi::CString;
use std::path::PathBuf;
use std::collections::HashMap;
use ::winapi::{HRESULT, PCSTR, UINT, LPCWSTR, LPWSTR, LPVOID};

pub type MSIHANDLE = u32;
Expand All @@ -17,31 +18,20 @@ pub const LOGMSG_STANDARD: i32 = 2;
static TOOLS: &'static [&'static str]
= &["rustc", "rustdoc", "cargo", "rust-lldb", "rust-gdb", "rls"];

#[no_mangle]
/// This is run as an `immediate` action early in the install sequence
pub unsafe extern "system" fn RustupSetInstallLocation(hInstall: MSIHANDLE) -> UINT {
// TODO: error handling (get rid of unwrap)
let name = CString::new("RustupSetInstallLocation").unwrap();
let hr = WcaInitialize(hInstall, name.as_ptr());
//let path = ::rustup::utils::cargo_home().unwrap();
let path = PathBuf::from(::std::env::var_os("USERPROFILE").unwrap()).join(".rustup-test");
set_property("RustupInstallLocation", path.to_str().unwrap());
WcaFinalize(hr)
}

#[no_mangle]
/// This is be run as a `deferred` action after `InstallFiles` on install and upgrade
pub unsafe extern "system" fn RustupInstall(hInstall: MSIHANDLE) -> UINT {
let name = CString::new("RustupInstall").unwrap();
let hr = WcaInitialize(hInstall, name.as_ptr());
// For deferred custom actions, all data must be passed through the `CustomActionData` property
let custom_action_data = get_property("CustomActionData");
// TODO: use rustup_utils::cargo_home() or pass through CustomActionData
let path = PathBuf::from(::std::env::var_os("USERPROFILE").unwrap()).join(".rustup-test");
let parsed_ca_data = parse_custom_action_data(&custom_action_data);
let path = PathBuf::from(parsed_ca_data.get("INSTALLLOCATION").unwrap());
let bin_path = path.join("bin");
let rustup_path = bin_path.join("rustup.exe");
let exe_installed = rustup_path.exists();
log(&format!("Hello World from RustupInstall, confirming that rustup.exe has been installed: {}! CustomActionData: {}", exe_installed, custom_action_data));
log(&format!("Parsed CA data: {:?}", parsed_ca_data));
for tool in TOOLS {
let ref tool_path = bin_path.join(&format!("{}.exe", tool));
::rustup::utils::hardlink_file(&rustup_path, tool_path);
Expand All @@ -57,15 +47,26 @@ pub unsafe extern "system" fn RustupUninstall(hInstall: MSIHANDLE) -> UINT {
let hr = WcaInitialize(hInstall, name.as_ptr());
// For deferred custom actions, all data must be passed through the `CustomActionData` property
let custom_action_data = get_property("CustomActionData");
// TODO: use rustup_utils::cargo_home() or pass through CustomActionData
let path = PathBuf::from(::std::env::var_os("USERPROFILE").unwrap()).join(".rustup-test");
let parsed_ca_data = parse_custom_action_data(&custom_action_data);
let path = PathBuf::from(parsed_ca_data.get("INSTALLLOCATION").unwrap());
let exe_deleted = !path.join("bin").join("rustup.exe").exists();
log(&format!("Hello World from RustupUninstall, confirming that rustup.exe has been deleted: {}! CustomActionData: {}", exe_deleted, custom_action_data));
// TODO: Remove .cargo and .rustup
::rustup::utils::remove_dir("rustup-test", &path, &|_| {});
log(&format!("Parsed CA data: {:?}", parsed_ca_data));
::rustup::utils::remove_dir("cargo_home", &path, &|_| {});
// TODO: also remove RUSTUP_HOME
//::rustup::utils::remove_dir("rustup_home", &rustup_home, &|_| {});
WcaFinalize(hr)
}

fn parse_custom_action_data(ca_data: &str) -> HashMap<&str, &str> {
let mut map = HashMap::new();
for v in ca_data.split(";") {
let idx = v.find('=').unwrap();
map.insert(&v[..idx], &v[(idx + 1)..]);
}
map
}

// wrapper for WcaGetProperty (TODO: error handling)
fn get_property(name: &str) -> String {
let encoded_name = to_wide_chars(name);
Expand All @@ -76,14 +77,6 @@ fn get_property(name: &str) -> String {
result
}

// wrapper for WcaSetProperty
fn set_property(name: &str, value: &str) -> HRESULT {
let encoded_name = to_wide_chars(name);
let encoded_value = to_wide_chars(value);
unsafe { WcaSetProperty(encoded_name.as_ptr(), encoded_value.as_ptr()) }
}


fn log(message: &str) {
let msg = CString::new(message).unwrap();
unsafe { WcaLog(LOGMSG_STANDARD, msg.as_ptr()) }
Expand All @@ -109,7 +102,6 @@ extern "system" {
fn WcaInitialize(hInstall: MSIHANDLE, szCustomActionLogName: PCSTR) -> HRESULT;
fn WcaFinalize(iReturnValue: HRESULT) -> UINT;
fn WcaGetProperty(wzProperty: LPCWSTR, ppwzData: *mut LPWSTR) -> HRESULT; // see documentation for MsiGetProperty
fn WcaSetProperty(wzPropertyName: LPCWSTR, wzPropertyValue: LPCWSTR) -> HRESULT;
fn StrFree(p: LPVOID) -> HRESULT;
}

Expand Down