diff --git a/README.md b/README.md index c4ec9a7..eb83e19 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..7fb1626 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,172 @@ +#!/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 + +.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" +) + +$ErrorActionPreference = 'Stop' + +function Green { + [Parameter(Position=0)] + param ($Message) + + Write-Host $Message -ForegroundColor Green +} + +function Warn { + [Parameter(Position=0)] + param ($Message) + + Write-Host $Message -ForegroundColor Yellow +} + +function Die { + [Parameter(Position=0)] + param ($Message) + + Write-Host $Message -ForegroundColor Red + exit 1 +} + +function TemporaryDirectory { + $Tmp = [System.IO.Path]::GetTempPath() + $Unique = (New-Guid).ToString("N") + $TempDir = New-Item -ItemType Directory -Path (Join-Path $Tmp "$Unique") + trap { + Remove-Item $TempDir -Recurse + } + return $TempDir +} + +function Fetch { + param ( + [string]$Url, + [string]$DestFile + ) + + Invoke-RestMethod -Uri $Url -OutFile $DestFile +} + +function InstallFromUrl { + param ( + [string]$Url, + $DestDir + ) + + $Sha256Url = "$Url.sha256" + + $Workdir = TemporaryDirectory + $ScienceExeFile = "$Workdir\science.exe" + $Sha256File = "$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 $DestDir -ItemType Directory | Out-Null + } + Move-Item $ScienceExeFile $DestDir -Force +} + +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" +InstallFromUrl -Url $DownloadURL -DestDir $BinDir + +if (!(";$Path;".ToLower() -like "*;$BinDir;*".ToLower())) { + if ($NoModifyPath) { + Warn "WARNING: $BinDir is not detected on `$PATH" + Warn ( + "You'll either need to invoke $BinDir\science.exe explicitly or else add $BinDir to your " + + "PATH." + ) + } else { + $User = [System.EnvironmentVariableTarget]::User + $Path = [System.Environment]::GetEnvironmentVariable('Path', $User) + if (!(";$Path;".ToLower() -like "*;$BinDir;*".ToLower())) { + [System.Environment]::SetEnvironmentVariable('Path', "$Path;$BinDir", $User) + $Env:Path += ";$BinDir" + } + } +} diff --git a/install.sh b/install.sh index 6f7346d..cdea844 100755 --- a/install.sh +++ b/install.sh @@ -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 @@ -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 } @@ -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}" } @@ -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: @@ -166,7 +165,6 @@ __EOF__ } INSTALL_PREFIX="${HOME}/.local/bin" -INSTALL_FILE="science" VERSION="latest/download" # Parse arguments. @@ -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 @@ -198,9 +192,10 @@ 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}" @@ -208,5 +203,6 @@ 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 diff --git a/tests/test_installer.py b/tests/test_installer.py index be14058..8f9eef1 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -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 @@ -13,29 +12,10 @@ @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): @@ -43,10 +23,11 @@ def run_captured(cmd: list): 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): @@ -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" @@ -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}"