diff --git a/Cargo.lock b/Cargo.lock index 019b77f892..971abff363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,32 +1,9 @@ [root] -name = "rustup" +name = "rustup-win-installer" version = "0.5.0" dependencies = [ - "clap 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "download 0.3.0", - "error-chain 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)", - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "markdown 0.1.2 (git+https://github.com/Diggsey/markdown.rs.git)", - "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.73 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", - "rustup-dist 0.5.0", - "rustup-mock 0.5.0", - "rustup-utils 0.5.0", - "scopeguard 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 2.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", - "toml 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "user32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustup 0.5.0", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winreg 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -562,6 +539,37 @@ dependencies = [ "webpki 0.1.0 (git+https://github.com/ctz/webpki)", ] +[[package]] +name = "rustup" +version = "0.5.0" +dependencies = [ + "clap 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "download 0.3.0", + "error-chain 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "markdown 0.1.2 (git+https://github.com/Diggsey/markdown.rs.git)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.73 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "rustup-dist 0.5.0", + "rustup-mock 0.5.0", + "rustup-utils 0.5.0", + "scopeguard 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 2.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "user32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winreg 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustup-dist" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index cb7f60c987..99c1ec8113 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ rustls-backend = ["download/rustls-backend"] # Include in the default set to disable self-update and uninstall. no-self-update = [] +# Used to change behavior of self-update and uninstall if installed via MSI +msi-installed = [] + [dependencies] rustup-dist = { path = "src/rustup-dist", version = "0.5.0" } rustup-utils = { path = "src/rustup-utils", version = "0.5.0" } @@ -59,6 +62,9 @@ kernel32-sys = "0.2.1" rustup-mock = { path = "src/rustup-mock", version = "0.5.0" } lazy_static = "0.1.15" +[workspace] +members = ["src/ca-loader", "src/download", "src/rustup-dist", "src/rustup-mock", "src/rustup-utils", "src/rustup-win-installer"] + [lib] name = "rustup" path = "src/rustup/lib.rs" diff --git a/appveyor.yml b/appveyor.yml index 58d7302d6e..ab36081d18 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,7 @@ environment: matrix: + - TARGET: i686-pc-windows-msvc + BUILD_MSI: 1 - TARGET: i686-pc-windows-gnu MINGW_URL: https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/4.9.2/threads-win32/dwarf/i686-4.9.2-release-win32-dwarf-rt_v4-rev4.7z/download MINGW_ARCHIVE: i686-4.9.2-release-win32-dwarf-rt_v4-rev4.7z @@ -34,6 +36,10 @@ install: - if defined MINGW_ARCHIVE curl -L --retry 4 "%MINGW_URL%" -o "%MINGW_ARCHIVE%" - if defined MINGW_ARCHIVE 7z x -y "%MINGW_ARCHIVE%" > nul - if defined MINGW_ARCHIVE set PATH=%CD%\%MINGW_DIR%\bin;C:\msys64\usr\bin;%PATH% + + # set cargo features for MSI if requested (otherwise empty string) + - set FEATURES= + - if defined BUILD_MSI set FEATURES=--features msi-installed # let's see what we got - where gcc rustc cargo @@ -44,9 +50,18 @@ install: build: false test_script: - - cargo build --release --target %TARGET% - - cargo test --release -p rustup-dist --target %TARGET% - - cargo test --release --target %TARGET% + - cargo build --release --target %TARGET% %FEATURES% + - cargo test --release -p rustup-dist --target %TARGET% %FEATURES% + - cargo test --release --target %TARGET% %FEATURES% + - ps: | + if($env:BUILD_MSI) { + cd src\rustup-win-installer + cargo build --release --target $env:TARGET + cd msi + .\build.ps1 -Target $env:TARGET + cd ..\..\.. + if($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } + } notifications: - provider: Webhook diff --git a/ci/prepare-deploy-appveyor.ps1 b/ci/prepare-deploy-appveyor.ps1 index 743fbb3760..b2dabd3372 100644 --- a/ci/prepare-deploy-appveyor.ps1 +++ b/ci/prepare-deploy-appveyor.ps1 @@ -6,6 +6,11 @@ if ($env:APPVEYOR_REPO_BRANCH -eq "auto") { exit 0 } +# Don't do anything for MSI (yet) +if ($env:BUILD_MSI) { + exit 0 +} + # Copy rustup-init to rustup-setup for backwards compatibility cp target\${env:TARGET}\release\rustup-init.exe target\${env:TARGET}release\rustup-setup.exe diff --git a/src/rustup-cli/self_update.rs b/src/rustup-cli/self_update.rs index 0049072713..1f06abcdd0 100644 --- a/src/rustup-cli/self_update.rs +++ b/src/rustup-cli/self_update.rs @@ -571,6 +571,19 @@ pub fn uninstall(no_prompt: bool) -> Result<()> { err!("you should probably use your system package manager to uninstall rustup"); process::exit(1); } + + if cfg!(feature = "msi-installed") { + // Get the product code of the MSI installer from the registry + // and spawn `msiexec /x`, then exit immediately + let product_code = try!(get_msi_product_code()); + try!(Command::new("msiexec") + .arg("/x") + .arg(product_code) + .spawn() + .chain_err(|| ErrorKind::WindowsUninstallMadness)); + process::exit(0); + } + let ref cargo_home = try!(utils::cargo_home()); if !cargo_home.join(&format!("bin/rustup{}", EXE_SUFFIX)).exists() { @@ -647,6 +660,36 @@ pub fn uninstall(no_prompt: bool) -> Result<()> { process::exit(0); } +#[cfg(not(feature = "msi-installed"))] +fn get_msi_product_code() -> Result { + unreachable!() +} + +#[cfg(feature = "msi-installed")] +fn get_msi_product_code() -> Result { + use winreg::RegKey; + use winapi::*; + + let root = RegKey::predef(HKEY_CURRENT_USER); + let environment = root.open_subkey_with_flags("SOFTWARE\\rustup", KEY_READ); + + match environment { + Ok(env) => { + match env.get_value("InstalledProductCode") { + Ok(val) => { + Ok(val) + } + Err(e) => { + Err(e).chain_err(|| ErrorKind::WindowsUninstallMadness) + } + } + } + Err(e) => { + Err(e).chain_err(|| ErrorKind::WindowsUninstallMadness) + } + } +} + #[cfg(unix)] fn delete_rustup_and_cargo_home() -> Result<()> { let ref cargo_home = try!(utils::cargo_home()); diff --git a/src/rustup-win-installer/Cargo.toml b/src/rustup-win-installer/Cargo.toml new file mode 100644 index 0000000000..2518ce2538 --- /dev/null +++ b/src/rustup-win-installer/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rustup-win-installer" +version = "0.5.0" +authors = ["Patrick Reisert"] +build = "build.rs" + +[lib] +name = "rustup_msi" +crate-type = ["cdylib"] + +[dependencies] +winapi = "0.2" +rustup = { path = "../../", version = "0.5.0" } diff --git a/src/rustup-win-installer/build.rs b/src/rustup-win-installer/build.rs new file mode 100644 index 0000000000..57da43c97b --- /dev/null +++ b/src/rustup-win-installer/build.rs @@ -0,0 +1,13 @@ +use std::env; + +fn main() { + println!("cargo:rustc-link-lib=static=wcautil"); + println!("cargo:rustc-link-lib=static=dutil"); + println!("cargo:rustc-link-lib=dylib=msi"); + println!("cargo:rustc-link-lib=dylib=user32"); + println!("cargo:rustc-link-lib=dylib=mincore"); + + let wix_path = env::var("WIX").unwrap(); + // x86 target is hard-coded because we only build an x86 installer (works just fine on x64) + println!("cargo:rustc-link-search=native={}SDK\\VS2015\\lib\\x86", wix_path); +} \ No newline at end of file diff --git a/src/rustup-win-installer/msi/banner.bmp b/src/rustup-win-installer/msi/banner.bmp new file mode 100644 index 0000000000..b5459a797d Binary files /dev/null and b/src/rustup-win-installer/msi/banner.bmp differ diff --git a/src/rustup-win-installer/msi/banner.xcf b/src/rustup-win-installer/msi/banner.xcf new file mode 100644 index 0000000000..53296518ee Binary files /dev/null and b/src/rustup-win-installer/msi/banner.xcf differ diff --git a/src/rustup-win-installer/msi/build.ps1 b/src/rustup-win-installer/msi/build.ps1 new file mode 100644 index 0000000000..bf45d1fb17 --- /dev/null +++ b/src/rustup-win-installer/msi/build.ps1 @@ -0,0 +1,20 @@ +param( + [Parameter(Mandatory=$true)] + [string] $Target +) + +$manifest = cargo read-manifest --manifest-path ..\..\..\Cargo.toml | ConvertFrom-Json +$version = $manifest.version.Split(".") +$env:CFG_VER_MAJOR = $version[0] +$env:CFG_VER_MINOR = $version[1] +$env:CFG_VER_PATCH = $version[2] + +foreach($file in Get-ChildItem *.wxs) { + $in = $file.Name + $out = $($file.Name.Replace(".wxs",".wixobj")) + &"$($env:WIX)bin\candle.exe" -nologo -arch x86 "-dTARGET=$Target" -ext WixUIExtension -ext WixUtilExtension -out "target\$out" $in + if ($LASTEXITCODE -ne 0) { exit 1 } +} + +# ICE57 wrongly complains about per-machine data in per-user install, because it doesn't know that INSTALLLOCATION is in per-user directory +&"$($env:WIX)\bin\light.exe" -nologo -ext WixUIExtension -ext WixUtilExtension -out "target\rustup.msi" -sice:ICE57 $(Get-ChildItem target\*.wixobj) diff --git a/src/rustup-win-installer/msi/dialogbg.bmp b/src/rustup-win-installer/msi/dialogbg.bmp new file mode 100644 index 0000000000..7e4674a4fe Binary files /dev/null and b/src/rustup-win-installer/msi/dialogbg.bmp differ diff --git a/src/rustup-win-installer/msi/dialogbg.xcf b/src/rustup-win-installer/msi/dialogbg.xcf new file mode 100644 index 0000000000..49ca4e0c33 Binary files /dev/null and b/src/rustup-win-installer/msi/dialogbg.xcf differ diff --git a/src/rustup-win-installer/msi/rust-logo.ico b/src/rustup-win-installer/msi/rust-logo.ico new file mode 100644 index 0000000000..a58225d5ac Binary files /dev/null and b/src/rustup-win-installer/msi/rust-logo.ico differ diff --git a/src/rustup-win-installer/msi/rustup.wxs b/src/rustup-win-installer/msi/rustup.wxs new file mode 100644 index 0000000000..d9d116d456 --- /dev/null +++ b/src/rustup-win-installer/msi/rustup.wxs @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NOT Installed + NOT Installed + + Installed AND (NOT UPGRADINGPRODUCTCODE) + + + + + + \ No newline at end of file diff --git a/src/rustup-win-installer/msi/ui.wxs b/src/rustup-win-installer/msi/ui.wxs new file mode 100644 index 0000000000..7a875b14bb --- /dev/null +++ b/src/rustup-win-installer/msi/ui.wxs @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + 1 + + 1 + + 1 + Installed AND NOT PATCH + + 1 + + 1 + 1 + 1 + + + + \ No newline at end of file diff --git a/src/rustup-win-installer/src/lib.rs b/src/rustup-win-installer/src/lib.rs new file mode 100644 index 0000000000..b27c220652 --- /dev/null +++ b/src/rustup-win-installer/src/lib.rs @@ -0,0 +1,118 @@ +#![allow(non_snake_case)] + +extern crate winapi; +extern crate rustup; + +use std::ffi::CString; +use std::path::PathBuf; +use ::winapi::{HRESULT, PCSTR, UINT, LPCWSTR, LPWSTR, LPVOID}; + +pub type MSIHANDLE = u32; + +pub const LOGMSG_TRACEONLY: i32 = 0; +pub const LOGMSG_VERBOSE: i32 = 1; +pub const LOGMSG_STANDARD: i32 = 2; + +// TODO: share this with self_update.rs +static TOOLS: &'static [&'static str] + = &["rustc", "rustdoc", "cargo", "rust-lldb", "rust-gdb"]; + +#[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 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)); + for tool in TOOLS { + let ref tool_path = bin_path.join(&format!("{}.exe", tool)); + ::rustup::utils::hardlink_file(&rustup_path, tool_path); + } + // TODO: install default toolchain and report progress to UI + WcaFinalize(hr) +} + +#[no_mangle] +/// This is be run as a `deferred` action after `RemoveFiles` on uninstall (not on upgrade!) +pub unsafe extern "system" fn RustupUninstall(hInstall: MSIHANDLE) -> UINT { + let name = CString::new("RustupUninstall").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 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 .multirust + ::rustup::utils::remove_dir("rustup-test", &path, &|_| {}); + WcaFinalize(hr) +} + +// wrapper for WcaGetProperty (TODO: error handling) +fn get_property(name: &str) -> String { + let encoded_name = to_wide_chars(name); + let mut result_ptr = std::ptr::null_mut(); + unsafe { WcaGetProperty(encoded_name.as_ptr(), &mut result_ptr) }; + let result = from_wide_ptr(result_ptr); + unsafe { StrFree(result_ptr as LPVOID) }; + 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()) } +} +fn from_wide_ptr(ptr: *const u16) -> String { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + unsafe { + assert!(!ptr.is_null()); + let len = (0..std::isize::MAX).position(|i| *ptr.offset(i) == 0).unwrap(); + let slice = std::slice::from_raw_parts(ptr, len); + OsString::from_wide(slice).to_string_lossy().into_owned() + } +} + +fn to_wide_chars(s: &str) -> Vec { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + OsStr::new(s).encode_wide().chain(Some(0).into_iter()).collect::>() +} + +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; +} + +extern "cdecl" { + fn WcaLog(llv: i32, fmt: PCSTR); +} \ No newline at end of file diff --git a/tests/cli-self-upd.rs b/tests/cli-self-upd.rs index cd715187e1..faa868361b 100644 --- a/tests/cli-self-upd.rs +++ b/tests/cli-self-upd.rs @@ -1,5 +1,9 @@ //! Testing self install, uninstall and update +// Disable these tests for MSI-based installation. +// The `self update` and `self uninstall` commands just call `msiexec`. +#![cfg(not(feature = "msi-installed"))] + extern crate rustup_mock; extern crate rustup_utils; #[macro_use]