diff --git a/Cargo.lock b/Cargo.lock index af415c064..c4986eed1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,11 +124,14 @@ version = "0.1.0" dependencies = [ "assert_fs", "atty", + "cc", "directories-next", "serial_test", "thiserror", "tracing", "which", + "winapi 0.3.9", + "winreg", ] [[package]] diff --git a/installers/binstall/Cargo.toml b/installers/binstall/Cargo.toml index ac397ee92..027d29a05 100644 --- a/installers/binstall/Cargo.toml +++ b/installers/binstall/Cargo.toml @@ -4,8 +4,6 @@ version = "0.1.0" authors = ["Apollo Developers "] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] atty = "0.2" directories-next = "2.0" @@ -13,6 +11,11 @@ thiserror = "1.0" tracing = "0.1" which = "4.0" +[target.'cfg(target_os = "windows")'.dependencies] +cc = "1.0" +winapi = "0.3" +winreg = "0.7" + [dev-dependencies] assert_fs = "1.0" serial_test = "0.5" \ No newline at end of file diff --git a/installers/binstall/install.sh b/installers/binstall/scripts/nix/install.sh similarity index 100% rename from installers/binstall/install.sh rename to installers/binstall/scripts/nix/install.sh diff --git a/installers/binstall/local-install.sh b/installers/binstall/scripts/nix/local-install.sh similarity index 100% rename from installers/binstall/local-install.sh rename to installers/binstall/scripts/nix/local-install.sh diff --git a/installers/binstall/scripts/windows/install.ps1 b/installers/binstall/scripts/windows/install.ps1 new file mode 100644 index 000000000..83a87580d --- /dev/null +++ b/installers/binstall/scripts/windows/install.ps1 @@ -0,0 +1,78 @@ +function Install-Binary() { + $old_erroractionpreference = $ErrorActionPreference + $ErrorActionPreference = 'stop' + + $version = "0.0.1-rc.0" + + Initialize-Environment + + $exe = Download($version) + + Invoke-Installer($exe) + + $ErrorActionPreference = $old_erroractionpreference +} + +function Download($version) { + $url = "https://github.com/apollographql/rover/releases/download/v$version/rover-v$version-x86_64-pc-windows-msvc.tar.gz" + "Downloading Rover from $url" | Out-Host + $tmp = New-Temp-Dir + $dir_path = "$tmp\rover.tar.gz" + $wc = New-Object Net.Webclient + $wc.downloadFile($url, $dir_path) + tar -xkf $dir_path -C "$tmp" + return "$tmp" +} + +function Invoke-Installer($tmp) { + $exe = "$tmp\dist\rover.exe" + & "$exe" "install" + Remove-Item "$tmp" -Recurse -Force +} + +function Initialize-Environment() { + If (($PSVersionTable.PSVersion.Major) -lt 5) { + Write-Error "PowerShell 5 or later is required to install Rover." + Write-Error "Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/setup/installing-windows-powershell" + break + } + + # show notification to change execution policy: + $allowedExecutionPolicy = @('Unrestricted', 'RemoteSigned', 'ByPass') + If ((Get-ExecutionPolicy).ToString() -notin $allowedExecutionPolicy) { + Write-Error "PowerShell requires an execution policy in [$($allowedExecutionPolicy -join ", ")] to run Rover." + Write-Error "For example, to set the execution policy to 'RemoteSigned' please run :" + Write-Error "'Set-ExecutionPolicy RemoteSigned -scope CurrentUser'" + break + } + + # GitHub requires TLS 1.2 + If ([System.Enum]::GetNames([System.Net.SecurityProtocolType]) -notcontains 'Tls12') { + Write-Error "Installing Rover requires at least .NET Framework 4.5" + Write-Error "Please download and install it first:" + Write-Error "https://www.microsoft.com/net/download" + break + } + + If (-Not (Get-Command 'curl')) { + Write-Error "The curl command is not installed on this machine. Please install curl before installing Rover" + # don't abort if invoked with iex that would close the PS session + If ($myinvocation.mycommand.commandtype -eq 'Script') { return } else { exit 1 } + } + + If (-Not (Get-Command 'tar')) { + Write-Error "The tar command is not installed on this machine. Please install curl before installing Rover" + # don't abort if invoked with iex that would close the PS session + If ($myinvocation.mycommand.commandtype -eq 'Script') { return } else { exit 1 } + } +} + +function New-Temp-Dir() { + [CmdletBinding(SupportsShouldProcess)] + param() + $parent = [System.IO.Path]::GetTempPath() + [string] $name = [System.Guid]::NewGuid() + New-Item -ItemType Directory -Path (Join-Path $parent $name) +} + +Install-Binary diff --git a/installers/binstall/scripts/windows/local-install.ps1 b/installers/binstall/scripts/windows/local-install.ps1 new file mode 100644 index 000000000..c8e4d6027 --- /dev/null +++ b/installers/binstall/scripts/windows/local-install.ps1 @@ -0,0 +1,2 @@ +cargo build --workspace +..\..\..\..\..\target\debug\rover.exe install \ No newline at end of file diff --git a/installers/binstall/src/system/windows.rs b/installers/binstall/src/system/windows.rs index fb3870a8c..dcf60ce0c 100644 --- a/installers/binstall/src/system/windows.rs +++ b/installers/binstall/src/system/windows.rs @@ -1,5 +1,169 @@ +// most of this code is taken wholesale from this: +// https://github.com/rust-lang/rustup/blob/master/src/cli/self_update/windows.rs use crate::{Installer, InstallerError}; +use std::io; + +/// Adds the downloaded binary in Installer to a Windows PATH pub fn add_binary_to_path(installer: &Installer) -> Result<(), InstallerError> { - unimplemented!() + let windows_path = get_windows_path_var()?; + let bin_path = installer + .get_bin_dir_path()? + .to_str() + .ok_or(InstallerError::PathNotUnicode)? + .to_string(); + if let Some(old_path) = windows_path { + if let Some(new_path) = add_to_path(&old_path, &bin_path) { + apply_new_path(&new_path)?; + } + } + + Ok(()) +} + +// --------------------------------------------- +// https://en.wikipedia.org/wiki/Here_be_dragons +// --------------------------------------------- +// nothing is sacred beyond this point. + +// Get the windows PATH variable out of the registry as a String. If +// this returns None then the PATH variable is not unicode and we +// should not mess with it. +fn get_windows_path_var() -> Result, InstallerError> { + use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE}; + use winreg::RegKey; + + let root = RegKey::predef(HKEY_CURRENT_USER); + let environment = root.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?; + + let reg_value = environment.get_raw_value("PATH"); + match reg_value { + Ok(val) => { + if let Some(s) = string_from_winreg_value(&val) { + Ok(Some(s)) + } else { + tracing::warn!("the registry key HKEY_CURRENT_USER\\Environment\\PATH does not contain valid Unicode. \ + Not modifying the PATH variable"); + Ok(None) + } + } + Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(Some(String::new())), + Err(e) => Err(e)?, + } +} + +// This is used to decode the value of HKCU\Environment\PATH. If that +// key is not unicode (or not REG_SZ | REG_EXPAND_SZ) then this +// returns None. The winreg library itself does a lossy unicode +// conversion. +fn string_from_winreg_value(val: &winreg::RegValue) -> Option { + use std::slice; + use winreg::enums::RegType; + + match val.vtype { + RegType::REG_SZ | RegType::REG_EXPAND_SZ => { + // Copied from winreg + let words = unsafe { + #[allow(clippy::cast_ptr_alignment)] + slice::from_raw_parts(val.bytes.as_ptr().cast::(), val.bytes.len() / 2) + }; + String::from_utf16(words).ok().map(|mut s| { + while s.ends_with('\u{0}') { + s.pop(); + } + s + }) + } + _ => None, + } +} + +// Returns None if the existing old_path does not need changing, otherwise +// prepends the path_str to old_path, handling empty old_path appropriately. +fn add_to_path(old_path: &str, path_str: &str) -> Option { + if old_path.is_empty() { + Some(path_str.to_string()) + } else if old_path.contains(path_str) { + None + } else { + let mut new_path = path_str.to_string(); + new_path.push_str(";"); + new_path.push_str(&old_path); + Some(new_path) + } +} + +fn apply_new_path(new_path: &str) -> Result<(), InstallerError> { + use std::ptr; + use winapi::shared::minwindef::*; + use winapi::um::winuser::{ + SendMessageTimeoutA, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE, + }; + use winreg::enums::{RegType, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE}; + use winreg::{RegKey, RegValue}; + + let root = RegKey::predef(HKEY_CURRENT_USER); + let environment = root.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?; + + if new_path.is_empty() { + environment.delete_value("PATH")?; + } else { + let reg_value = RegValue { + bytes: string_to_winreg_bytes(new_path), + vtype: RegType::REG_EXPAND_SZ, + }; + environment.set_raw_value("PATH", ®_value)?; + } + + // Tell other processes to update their environment + unsafe { + SendMessageTimeoutA( + HWND_BROADCAST, + WM_SETTINGCHANGE, + 0 as WPARAM, + "Environment\0".as_ptr() as LPARAM, + SMTO_ABORTIFHUNG, + 5000, + ptr::null_mut(), + ); + } + + Ok(()) +} + +fn string_to_winreg_bytes(s: &str) -> Vec { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + let v: Vec = OsStr::new(s).encode_wide().chain(Some(0)).collect(); + unsafe { std::slice::from_raw_parts(v.as_ptr().cast::(), v.len() * 2).to_vec() } +} + +#[cfg(test)] +mod tests { + #[test] + fn windows_install_does_not_add_path_twice() { + assert_eq!( + None, + super::add_to_path( + r"c:\users\example\.mybinary\bin;foo", + r"c:\users\example\.mybinary\bin" + ) + ); + } + + #[test] + fn windows_install_does_add_path() { + assert_eq!( + Some(r"c:\users\example\.mybinary\bin;foo".to_string()), + super::add_to_path("foo", r"c:\users\example\.mybinary\bin") + ); + } + + #[test] + fn windows_install_does_add_path_no_double_semicolon() { + assert_eq!( + Some(r"c:\users\example\.mybinary\bin;foo;bar;".to_string()), + super::add_to_path("foo;bar;", r"c:\users\example\.mybinary\bin") + ); + } }