Skip to content
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

feat: windows installer #104

Merged
merged 1 commit into from
Dec 15, 2020
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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions installers/binstall/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ version = "0.1.0"
authors = ["Apollo Developers <[email protected]>"]
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"
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"
78 changes: 78 additions & 0 deletions installers/binstall/scripts/windows/install.ps1
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions installers/binstall/scripts/windows/local-install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cargo build --workspace
..\..\..\..\..\target\debug\rover.exe install
166 changes: 165 additions & 1 deletion installers/binstall/src/system/windows.rs
Original file line number Diff line number Diff line change
@@ -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<Option<String>, 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<String> {
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::<u16>(), 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<String> {
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", &reg_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<u8> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
let v: Vec<u16> = OsStr::new(s).encode_wide().chain(Some(0)).collect();
unsafe { std::slice::from_raw_parts(v.as_ptr().cast::<u8>(), 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")
);
}
}