Skip to content

Commit d2cee55

Browse files
feat: windows installer (#104)
1 parent c59b9c2 commit d2cee55

File tree

7 files changed

+253
-3
lines changed

7 files changed

+253
-3
lines changed

Cargo.lock

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

installers/binstall/Cargo.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ version = "0.1.0"
44
authors = ["Apollo Developers <[email protected]>"]
55
edition = "2018"
66

7-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8-
97
[dependencies]
108
atty = "0.2"
119
directories-next = "2.0"
1210
thiserror = "1.0"
1311
tracing = "0.1"
1412
which = "4.0"
1513

14+
[target.'cfg(target_os = "windows")'.dependencies]
15+
cc = "1.0"
16+
winapi = "0.3"
17+
winreg = "0.7"
18+
1619
[dev-dependencies]
1720
assert_fs = "1.0"
1821
serial_test = "0.5"
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
function Install-Binary() {
2+
$old_erroractionpreference = $ErrorActionPreference
3+
$ErrorActionPreference = 'stop'
4+
5+
$version = "0.0.1-rc.0"
6+
7+
Initialize-Environment
8+
9+
$exe = Download($version)
10+
11+
Invoke-Installer($exe)
12+
13+
$ErrorActionPreference = $old_erroractionpreference
14+
}
15+
16+
function Download($version) {
17+
$url = "https://github.com/apollographql/rover/releases/download/v$version/rover-v$version-x86_64-pc-windows-msvc.tar.gz"
18+
"Downloading Rover from $url" | Out-Host
19+
$tmp = New-Temp-Dir
20+
$dir_path = "$tmp\rover.tar.gz"
21+
$wc = New-Object Net.Webclient
22+
$wc.downloadFile($url, $dir_path)
23+
tar -xkf $dir_path -C "$tmp"
24+
return "$tmp"
25+
}
26+
27+
function Invoke-Installer($tmp) {
28+
$exe = "$tmp\dist\rover.exe"
29+
& "$exe" "install"
30+
Remove-Item "$tmp" -Recurse -Force
31+
}
32+
33+
function Initialize-Environment() {
34+
If (($PSVersionTable.PSVersion.Major) -lt 5) {
35+
Write-Error "PowerShell 5 or later is required to install Rover."
36+
Write-Error "Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/setup/installing-windows-powershell"
37+
break
38+
}
39+
40+
# show notification to change execution policy:
41+
$allowedExecutionPolicy = @('Unrestricted', 'RemoteSigned', 'ByPass')
42+
If ((Get-ExecutionPolicy).ToString() -notin $allowedExecutionPolicy) {
43+
Write-Error "PowerShell requires an execution policy in [$($allowedExecutionPolicy -join ", ")] to run Rover."
44+
Write-Error "For example, to set the execution policy to 'RemoteSigned' please run :"
45+
Write-Error "'Set-ExecutionPolicy RemoteSigned -scope CurrentUser'"
46+
break
47+
}
48+
49+
# GitHub requires TLS 1.2
50+
If ([System.Enum]::GetNames([System.Net.SecurityProtocolType]) -notcontains 'Tls12') {
51+
Write-Error "Installing Rover requires at least .NET Framework 4.5"
52+
Write-Error "Please download and install it first:"
53+
Write-Error "https://www.microsoft.com/net/download"
54+
break
55+
}
56+
57+
If (-Not (Get-Command 'curl')) {
58+
Write-Error "The curl command is not installed on this machine. Please install curl before installing Rover"
59+
# don't abort if invoked with iex that would close the PS session
60+
If ($myinvocation.mycommand.commandtype -eq 'Script') { return } else { exit 1 }
61+
}
62+
63+
If (-Not (Get-Command 'tar')) {
64+
Write-Error "The tar command is not installed on this machine. Please install curl before installing Rover"
65+
# don't abort if invoked with iex that would close the PS session
66+
If ($myinvocation.mycommand.commandtype -eq 'Script') { return } else { exit 1 }
67+
}
68+
}
69+
70+
function New-Temp-Dir() {
71+
[CmdletBinding(SupportsShouldProcess)]
72+
param()
73+
$parent = [System.IO.Path]::GetTempPath()
74+
[string] $name = [System.Guid]::NewGuid()
75+
New-Item -ItemType Directory -Path (Join-Path $parent $name)
76+
}
77+
78+
Install-Binary
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cargo build --workspace
2+
..\..\..\..\..\target\debug\rover.exe install
+165-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,169 @@
1+
// most of this code is taken wholesale from this:
2+
// https://github.com/rust-lang/rustup/blob/master/src/cli/self_update/windows.rs
13
use crate::{Installer, InstallerError};
24

5+
use std::io;
6+
7+
/// Adds the downloaded binary in Installer to a Windows PATH
38
pub fn add_binary_to_path(installer: &Installer) -> Result<(), InstallerError> {
4-
unimplemented!()
9+
let windows_path = get_windows_path_var()?;
10+
let bin_path = installer
11+
.get_bin_dir_path()?
12+
.to_str()
13+
.ok_or(InstallerError::PathNotUnicode)?
14+
.to_string();
15+
if let Some(old_path) = windows_path {
16+
if let Some(new_path) = add_to_path(&old_path, &bin_path) {
17+
apply_new_path(&new_path)?;
18+
}
19+
}
20+
21+
Ok(())
22+
}
23+
24+
// ---------------------------------------------
25+
// https://en.wikipedia.org/wiki/Here_be_dragons
26+
// ---------------------------------------------
27+
// nothing is sacred beyond this point.
28+
29+
// Get the windows PATH variable out of the registry as a String. If
30+
// this returns None then the PATH variable is not unicode and we
31+
// should not mess with it.
32+
fn get_windows_path_var() -> Result<Option<String>, InstallerError> {
33+
use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};
34+
use winreg::RegKey;
35+
36+
let root = RegKey::predef(HKEY_CURRENT_USER);
37+
let environment = root.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;
38+
39+
let reg_value = environment.get_raw_value("PATH");
40+
match reg_value {
41+
Ok(val) => {
42+
if let Some(s) = string_from_winreg_value(&val) {
43+
Ok(Some(s))
44+
} else {
45+
tracing::warn!("the registry key HKEY_CURRENT_USER\\Environment\\PATH does not contain valid Unicode. \
46+
Not modifying the PATH variable");
47+
Ok(None)
48+
}
49+
}
50+
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(Some(String::new())),
51+
Err(e) => Err(e)?,
52+
}
53+
}
54+
55+
// This is used to decode the value of HKCU\Environment\PATH. If that
56+
// key is not unicode (or not REG_SZ | REG_EXPAND_SZ) then this
57+
// returns None. The winreg library itself does a lossy unicode
58+
// conversion.
59+
fn string_from_winreg_value(val: &winreg::RegValue) -> Option<String> {
60+
use std::slice;
61+
use winreg::enums::RegType;
62+
63+
match val.vtype {
64+
RegType::REG_SZ | RegType::REG_EXPAND_SZ => {
65+
// Copied from winreg
66+
let words = unsafe {
67+
#[allow(clippy::cast_ptr_alignment)]
68+
slice::from_raw_parts(val.bytes.as_ptr().cast::<u16>(), val.bytes.len() / 2)
69+
};
70+
String::from_utf16(words).ok().map(|mut s| {
71+
while s.ends_with('\u{0}') {
72+
s.pop();
73+
}
74+
s
75+
})
76+
}
77+
_ => None,
78+
}
79+
}
80+
81+
// Returns None if the existing old_path does not need changing, otherwise
82+
// prepends the path_str to old_path, handling empty old_path appropriately.
83+
fn add_to_path(old_path: &str, path_str: &str) -> Option<String> {
84+
if old_path.is_empty() {
85+
Some(path_str.to_string())
86+
} else if old_path.contains(path_str) {
87+
None
88+
} else {
89+
let mut new_path = path_str.to_string();
90+
new_path.push_str(";");
91+
new_path.push_str(&old_path);
92+
Some(new_path)
93+
}
94+
}
95+
96+
fn apply_new_path(new_path: &str) -> Result<(), InstallerError> {
97+
use std::ptr;
98+
use winapi::shared::minwindef::*;
99+
use winapi::um::winuser::{
100+
SendMessageTimeoutA, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE,
101+
};
102+
use winreg::enums::{RegType, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};
103+
use winreg::{RegKey, RegValue};
104+
105+
let root = RegKey::predef(HKEY_CURRENT_USER);
106+
let environment = root.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;
107+
108+
if new_path.is_empty() {
109+
environment.delete_value("PATH")?;
110+
} else {
111+
let reg_value = RegValue {
112+
bytes: string_to_winreg_bytes(new_path),
113+
vtype: RegType::REG_EXPAND_SZ,
114+
};
115+
environment.set_raw_value("PATH", &reg_value)?;
116+
}
117+
118+
// Tell other processes to update their environment
119+
unsafe {
120+
SendMessageTimeoutA(
121+
HWND_BROADCAST,
122+
WM_SETTINGCHANGE,
123+
0 as WPARAM,
124+
"Environment\0".as_ptr() as LPARAM,
125+
SMTO_ABORTIFHUNG,
126+
5000,
127+
ptr::null_mut(),
128+
);
129+
}
130+
131+
Ok(())
132+
}
133+
134+
fn string_to_winreg_bytes(s: &str) -> Vec<u8> {
135+
use std::ffi::OsStr;
136+
use std::os::windows::ffi::OsStrExt;
137+
let v: Vec<u16> = OsStr::new(s).encode_wide().chain(Some(0)).collect();
138+
unsafe { std::slice::from_raw_parts(v.as_ptr().cast::<u8>(), v.len() * 2).to_vec() }
139+
}
140+
141+
#[cfg(test)]
142+
mod tests {
143+
#[test]
144+
fn windows_install_does_not_add_path_twice() {
145+
assert_eq!(
146+
None,
147+
super::add_to_path(
148+
r"c:\users\example\.mybinary\bin;foo",
149+
r"c:\users\example\.mybinary\bin"
150+
)
151+
);
152+
}
153+
154+
#[test]
155+
fn windows_install_does_add_path() {
156+
assert_eq!(
157+
Some(r"c:\users\example\.mybinary\bin;foo".to_string()),
158+
super::add_to_path("foo", r"c:\users\example\.mybinary\bin")
159+
);
160+
}
161+
162+
#[test]
163+
fn windows_install_does_add_path_no_double_semicolon() {
164+
assert_eq!(
165+
Some(r"c:\users\example\.mybinary\bin;foo;bar;".to_string()),
166+
super::add_to_path("foo;bar;", r"c:\users\example\.mybinary\bin")
167+
);
168+
}
5169
}

0 commit comments

Comments
 (0)