Skip to content

Commit

Permalink
Add an install.ps1 script for Windows. (#93)
Browse files Browse the repository at this point in the history
The `install.sh` script can still be used with Git bash, but hopefully
this native PowerShell script is a bit more friendly for Windows users.

Fixes #91
  • Loading branch information
jsirois authored Sep 9, 2024
1 parent c3ef8ac commit 69238b3
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 56 deletions.
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,21 @@ a result. The "thin" varieties have the CPython 3.12 distribution gouged out and
result. In its place a [`ptex`](https://github.com/a-scie/ptex) binary is included that fills in the
CPython 3.12 distribution by fetching it when the "thin" `science` binary is first run.

You can install the latest `science` release using the `install.sh` script like so:

```
$ curl --proto '=https' --tlsv1.2 -LSsf https://raw.githubusercontent.com/a-scie/lift/main/install.sh | bash
...
$ science -V
```
You can install the latest `science` "fat" binary release using a convenience install script
like so:

+ Linux and macOS:
```
$ curl --proto '=https' --tlsv1.2 -LSsf https://raw.githubusercontent.com/a-scie/lift/main/install.sh | bash
...
$ science -V
```
+ Windows PowerShell:
```
> irm https://raw.githubusercontent.com/a-scie/lift/main/install.ps1 | iex
...
> science -V
```

The high level documentation is currently thin! The command line help is pretty decent though; so
try there 1st starting with just running `science` with no arguments.
Expand Down
188 changes: 188 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#!/usr/bin/env pwsh
# Copyright 2024 Science project contributors.
# Licensed under the Apache License, Version 2.0 (see LICENSE).

<#
.SYNOPSIS
Installs the `science.exe` executable.
.DESCRIPTION
Downloads and installs the latest version of the science executable by default.
The download is first verified against the published checksum before being installed.
The install process will add the science executable to your user PATH environment variable
if needed as well as to the current shell session PATH for immediate use.
.PARAMETER Help
Display this help message.
.PARAMETER BinDir
The directory to install the science binary in.
.PARAMETER NoModifyPath
Do not automatically add -BinDir to the PATH.
.PARAMETER Version
The version of the science binary to install, the latest version by default.
The available versions can be seen at:
https://github.com/a-scie/lift/releases
.PARAMETER Debug
Enable debug logging.
.INPUTS
None
.OUTPUTS
The path of the installed science executable.
.LINK
Docs https://science.scie.app
.LINK
Chat https://scie.app/discord
.LINK
Source https://github.com/a-scie/lift
#>

param (
[Alias('h')]
[switch]$Help,

[Alias('d')]
[string]$BinDir = (
# N.B.: PowerShell>=6 supports varargs, but to retain compatibility with older PowerShell, we
# just Join-Path twice.
# See: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/join-path?view=powershell-7.4#-additionalchildpath
Join-Path (
Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Programs'
) 'Science'
),

[switch]$NoModifyPath,

[Alias('V')]
[string]$Version = 'latest/download',

[switch]$Debug
)

$ErrorActionPreference = 'Stop'
$DebugPreference = if ($Debug) { 'Continue' } else { 'SilentlyContinue' }

$ExitActions = @()

function AtExit(
[ScriptBlock]$Action,

[Parameter(ValueFromRemainingArguments=$true)]
[array]$ArgList = @()
) {
$script:ExitActions += @{ Action = $Action; Args = $ArgList }
}

function RunWithAtExit([ScriptBlock]$Action) {
try {
& $Action
} finally {
foreach ($exitAction in $script:ExitActions) {
& $exitAction.Action $exitAction.Args
}
}
}

function Green($Message) {
Write-Host $Message -ForegroundColor Green
}

function Die($Message) {
Write-Host $Message -ForegroundColor Red
exit 1
}

function TemporaryDirectory {
$Tmp = [IO.Path]::GetTempPath()
$Unique = (New-Guid).ToString('N')
$TempDir = New-Item -ItemType Directory -Path (Join-Path $Tmp "science-install.$Unique")
AtExit { param($Dir) Remove-Item -Recurse -Force $Dir; Write-Debug "Removed $Dir" } $TempDir
$TempDir
}

function Fetch([string]$Url, [string]$DestFile) {
$IrmArgs = @{ Uri = $Url; OutFile = $DestFile }

# Support for protocol pinning has not always been available.
# See: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-restmethod?view=powershell-7.4#-sslprotocol
if ($Host.Version -ge [Version]::new(7, 1)) {
$IrmArgs["SslProtocol"] = "Tls12,Tls13"
} elseif ($Host.Version.Major -ge 6) {
$IrmArgs["SslProtocol"] = "Tls12"
}
Invoke-RestMethod @IrmArgs
}

function InstallFromUrl([string]$Url) {
$Sha256Url = "$Url.sha256"

$Workdir = TemporaryDirectory
$ScienceExeFile = Join-Path $Workdir 'science.exe'
$Sha256File = Join-Path $Workdir 'science.exe.sha256'

Fetch -Url $Url -DestFile $ScienceExeFile
Fetch -Url $Sha256Url -DestFile $Sha256File
Green 'Download completed successfully'

$ExpectedHash = ((Get-Content $Sha256File).Trim().ToLower() -Split '\s+',2)[0]
$ActualHash = (Get-FileHash $ScienceExeFile -Algorithm SHA256).Hash.ToLower()
if ($ActualHash -eq $ExpectedHash) {
Green "Download matched it's expected sha256 fingerprint, proceeding"
} else {
Die "Download from $Url did not match the fingerprint at $Sha256Url"
}

if (!(Test-Path $BinDir)) {
New-Item $BinDir -ItemType Directory | Out-Null
}
Move-Item $ScienceExeFile $BinDir -Force
Join-Path $BinDir 'science.exe'
}

function Main {
if ($Help) {
Get-Help -Detailed $PSCommandPath
exit 0
}

$Version = switch ($Version) {
'latest/download' { 'latest/download' }
default { "download/v$Version" }
}

$Arch = switch -Wildcard ((Get-CimInstance Win32_operatingsystem).OSArchitecture) {
'arm*' { 'aarch64' }
default { 'x86_64' }
}

$DownloadURL = "https://github.com/a-scie/lift/releases/$Version/science-fat-windows-$Arch.exe"

Green "Download URL is: $DownloadURL"
$ScienceExe = InstallFromUrl -Url $DownloadURL

$User = [EnvironmentVariableTarget]::User
$Path = [Environment]::GetEnvironmentVariable('Path', $User)
if (!(";$Path;".ToLower() -like "*;$BinDir;*".ToLower())) {
if ($NoModifyPath) {
Write-Warning @"
$BinDir is not detected on `$PATH
You'll either need to invoke $ScienceExe explicitly or else add $BinDir to your PATH.
"@
} else {
[Environment]::SetEnvironmentVariable('Path', "$Path;$BinDir", $User)
$Env:Path += ";$BinDir"
}
}
}

RunWithAtExit {
Main
}
26 changes: 11 additions & 15 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ function gc() {
if (($# > 0)); then
_GC+=("$@")
else
# Check if $_GC has members to avoid "unbound variable" warnings if gc w/ arguments is never called.
# Check if $_GC has members to avoid "unbound variable" warnings if gc w/ arguments is never
# called.
if ! [ ${#_GC[@]} -eq 0 ]; then
rm -rf "${_GC[@]}"
fi
Expand Down Expand Up @@ -76,7 +77,7 @@ function determine_arch() {
amd64*) echo "x86_64" ;;
arm64*) echo "aarch64" ;;
aarch64*) echo "aarch64" ;;
*) die "unknown arch: ${read_arch}" ;;
*) die "unknown arch: ${read_arch}" ;;
esac
}

Expand All @@ -89,7 +90,8 @@ function fetch() {
local dest
dest="${dest_dir}/$(basename "${url}")"

# N.B. Curl is included on Windows 10+: https://devblogs.microsoft.com/commandline/tar-and-curl-come-to-windows/
# N.B. Curl is included on Windows 10+:
# https://devblogs.microsoft.com/commandline/tar-and-curl-come-to-windows/
curl --proto '=https' --tlsv1.2 -SfL --progress-bar -o "${dest}" "${url}"
}

Expand Down Expand Up @@ -154,9 +156,6 @@ Installs the \`science\` binary.
-d | --bin-dir:
The directory to install the science binary in, "~/.local/bin" by default.
-b | --base-name:
The name to use for the science binary, "science" by default.
-V | --version:
The version of the science binary to install, the latest version by default.
The available versions can be seen at:
Expand All @@ -166,7 +165,6 @@ __EOF__
}

INSTALL_PREFIX="${HOME}/.local/bin"
INSTALL_FILE="science"
VERSION="latest/download"

# Parse arguments.
Expand All @@ -180,10 +178,6 @@ while (($# > 0)); do
INSTALL_PREFIX="$2"
shift
;;
--base-name | -b)
INSTALL_FILE="$2"
shift
;;
--version | -V)
VERSION="download/v${2}"
shift
Expand All @@ -198,15 +192,17 @@ done

ARCH="$(determine_arch)"
DIRSEP=$([[ "${OS}" == "windows" ]] && echo "\\" || echo "/")
INSTALL_DEST="${INSTALL_PREFIX}${DIRSEP}${INSTALL_FILE}"
DL_EXT=$([[ "${OS}" == "windows" ]] && echo ".exe" || echo "")
DL_URL="https://github.com/a-scie/lift/releases/${VERSION}/science-fat-${OS}-${ARCH}${DL_EXT}"
EXE_EXT=$([[ "${OS}" == "windows" ]] && echo ".exe" || echo "")

INSTALL_DEST="${INSTALL_PREFIX}${DIRSEP}science${EXE_EXT}"
DL_URL="https://github.com/a-scie/lift/releases/${VERSION}/science-fat-${OS}-${ARCH}${EXE_EXT}"

green "Download URL is: ${DL_URL}"
install_from_url "${DL_URL}" "${INSTALL_DEST}"

# Warn if the install prefix is not on $PATH.
if ! [[ ":$PATH:" == *":${INSTALL_PREFIX}:"* ]]; then
warn "WARNING: ${INSTALL_PREFIX} is not detected on \$PATH"
warn "You'll either need to invoke ${INSTALL_DEST} explicitly or else add ${INSTALL_PREFIX} to your shell's PATH."
warn "You'll either need to invoke ${INSTALL_DEST} explicitly or else add ${INSTALL_PREFIX} \
to your shell's PATH."
fi
49 changes: 15 additions & 34 deletions tests/test_installer.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Copyright 2024 Science project contributors.
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import shutil
import subprocess
from pathlib import Path, PurePath
from pathlib import Path

import pytest
from _pytest.tmpdir import TempPathFactory
Expand All @@ -13,40 +12,22 @@

@pytest.fixture(scope="module")
def installer(build_root: Path) -> list:
installer = build_root / "install.sh"
if IS_WINDOWS:
# TODO(John Sirois): Get rid of all this shenanigans and write an install.ps1 instead:
# https://github.com/a-scie/lift/issues/91

# Given a git for Windows install at C:\Program Files\Git, we will find the git executable
# at one of these two locations:
# + Running under cmd or pwsh, etc.: C:\Program Files\Git\cmd\git.EXE
# + Running under git bash: C:\Program Files\Git\mingw64\bin\git.EXE
# We expect the msys2 root to be at the git for Windows install root, which is
# C:\Program Files\Git in this case.
assert (git := shutil.which("git")) is not None, "This test requires Git bash on Windows."
msys2_root = PurePath(git).parent.parent
if "mingw64" == msys2_root.name:
msys2_root = msys2_root.parent

assert (bash := shutil.which("bash", path=msys2_root / "usr" / "bin")) is not None, (
f"The git executable at {git} does not appear to have msys2 root at the expected path "
f"of {msys2_root}."
)
return [bash, installer]
return ["pwsh", build_root / "install.ps1", "-NoModifyPath"]
else:
return [installer]
return [build_root / "install.sh"]


def run_captured(cmd: list):
return subprocess.run(cmd, capture_output=True, text=True)


def test_installer_help(installer: list):
"""Validates -h|--help in the installer."""
for tested_flag in ("-h", "--help"):
"""Validates help in the installer."""
long_help = "-Help" if IS_WINDOWS else "--help"
for tested_flag in ("-h", long_help):
assert (result := run_captured(installer + [tested_flag])).returncode == 0
assert "--help" in result.stdout, "Expected '--help' in tool output"
assert long_help in result.stdout, f"Expected '{long_help}' in tool output"


def test_installer_fetch_latest(tmp_path_factory: TempPathFactory, installer: list):
Expand All @@ -55,7 +36,9 @@ def test_installer_fetch_latest(tmp_path_factory: TempPathFactory, installer: li
bin_dir = test_dir / "bin"

assert (result := run_captured(installer + ["-d", bin_dir])).returncode == 0
assert "success" in result.stderr, "Expected 'success' in tool stderr logging"
assert (
"success" in result.stdout if IS_WINDOWS else result.stderr
), "Expected 'success' in tool stderr logging"

assert (result := run_captured([bin_dir / "science", "-V"])).returncode == 0
assert result.stdout.strip(), "Expected version output in tool stdout"
Expand All @@ -66,18 +49,16 @@ def test_installer_fetch_argtest(tmp_path_factory: TempPathFactory, installer: l
test_dir = tmp_path_factory.mktemp("install-test")
test_ver = "0.7.0"
bin_dir = test_dir / "bin"
bin_file = f"science{test_ver}"

assert (
result := run_captured(installer + ["-V", test_ver, "-b", bin_file, "-d", bin_dir])
).returncode == 0
assert "success" in result.stderr, "Expected 'success' in tool stderr logging"
assert (result := run_captured(installer + ["-V", test_ver, "-d", bin_dir])).returncode == 0
output = result.stdout if IS_WINDOWS else result.stderr
assert "success" in output, "Expected 'success' in tool stderr logging"

# Ensure missing $PATH entry warning (assumes our temp dir by nature is not on $PATH).
assert "is not detected on $PATH" in result.stderr, "Expected missing $PATH entry warning"
assert "is not detected on $PATH" in output, "Expected missing $PATH entry warning"

# Check expected versioned binary exists.
assert (result := run_captured([bin_dir / bin_file, "-V"])).returncode == 0
assert (result := run_captured([bin_dir / "science", "-V"])).returncode == 0
assert (
result.stdout.strip() == test_ver
), f"Expected version output in tool stdout to be {test_ver}"

0 comments on commit 69238b3

Please sign in to comment.