Skip to content
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
12 changes: 12 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: docker run --rm --network none -v "$PWD:/src:ro" python:3.12-slim python /src/tests/installer.py

windows-installer:
name: Windows installer
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Windows PowerShell 5.1
shell: powershell
run: ./tests/windows_installer.ps1
- name: PowerShell 7
shell: pwsh
run: ./tests/windows_installer.ps1
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ Please run `envx config migrate` to migrate your config file to the new format.
curl -fsSL get.envx.sh | bash
```

For windows users:
Download the binary from [this page](https://github.com/envx-project/cli/releases/latest), then you can run that binary as an application.
```powershell
# Windows (PowerShell, no administrator rights needed)
powershell -c "irm https://raw.githubusercontent.com/envx-project/cli/main/install.ps1 | iex"
```

For more detailed instructions, see [windows installation](https://github.com/envx-project/cli/blob/main/windows-installation.md)

Expand Down Expand Up @@ -75,7 +77,7 @@ Commands:
shell Open a subshell with envx variables available
unlink Unlink the current project
unset Unset (delete) an environment variable
update Attempt to self-update envx using the installation script. Fails on Windows
update Self-update envx using the installation script
upload If your key is not in the database, use this command to upload it
variables Get all environment variables for the current configured directory
version Fancy, pretty-printed version information
Expand Down
125 changes: 125 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Install a published envx release on Windows.
#
# powershell -c "irm https://raw.githubusercontent.com/envx-project/cli/main/install.ps1 | iex"
#
# Installs per user (no administrator rights) and adds the folder to the user PATH.
# Environment: ENVX_VERSION, ENVX_INSTALL_DIR, ENVX_PLATFORM (msvc or gnu),
# ENVX_BASE_URL, ENVX_NO_MODIFY_PATH=1, ENVX_UNINSTALL=1.
# Compatible with Windows PowerShell 5.1 and PowerShell 7.

& {
$ErrorActionPreference = 'Stop'
# Invoke-WebRequest renders progress very slowly on Windows PowerShell 5.1.
$ProgressPreference = 'SilentlyContinue'
# Windows PowerShell 5.1 may default to TLS 1.0.
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

function Fail($message) { throw "envx: $message" }
$onWindows = $PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows

$installDir = $env:ENVX_INSTALL_DIR
if (-not $installDir) {
if (-not $env:LOCALAPPDATA) { Fail 'LOCALAPPDATA is not set; set ENVX_INSTALL_DIR' }
$installDir = Join-Path $env:LOCALAPPDATA 'Programs\envx'
}
$exe = Join-Path $installDir 'envx.exe'

function Update-UserPath([bool]$add) {
if (-not $onWindows -or $env:ENVX_NO_MODIFY_PATH -eq '1') { return }
$current = [Environment]::GetEnvironmentVariable('Path', 'User')
$entries = @(if ($current) { $current -split ';' | Where-Object { $_ } })
$present = $entries | Where-Object { $_.TrimEnd('\') -ieq $installDir.TrimEnd('\') }
if ($add -and -not $present) {
[Environment]::SetEnvironmentVariable('Path', (($entries + $installDir) -join ';'), 'User')
$env:Path = "$env:Path;$installDir"
Write-Host "Added $installDir to your user PATH. Restart other terminals to pick it up."
} elseif (-not $add -and $present) {
$kept = $entries | Where-Object { $_.TrimEnd('\') -ine $installDir.TrimEnd('\') }
[Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), 'User')
}
}

if ($env:ENVX_UNINSTALL -eq '1') {
if (Test-Path -LiteralPath $exe) { Remove-Item -LiteralPath $exe -Force }
Remove-Item -LiteralPath "$exe.old" -Force -ErrorAction SilentlyContinue
Update-UserPath $false
Write-Host 'Removed envx'
return
}

$baseUrl = if ($env:ENVX_BASE_URL) { $env:ENVX_BASE_URL.TrimEnd('/') } else { 'https://github.com/envx-project/cli/releases' }
if (-not $baseUrl.StartsWith('https://')) { Fail 'Release downloads require an HTTPS base URL' }

$arch = $env:PROCESSOR_ARCHITECTURE
if ($arch -and $arch -notin @('AMD64', 'ARM64')) {
# ARM64 Windows runs the x86_64 build under emulation; 32-bit has no build.
Fail "No published build for $arch Windows; see https://github.com/envx-project/cli/releases"
}
$platform = if ($env:ENVX_PLATFORM) { $env:ENVX_PLATFORM } else { 'msvc' }
if ($platform -notin @('msvc', 'gnu')) { Fail 'ENVX_PLATFORM must be msvc or gnu' }
$target = "x86_64-pc-windows-$platform"

$version = $env:ENVX_VERSION
if (-not $version) {
# /latest redirects to /tag/vX.Y.Z; read the tag without following it.
$request = [Net.HttpWebRequest]::Create("$baseUrl/latest")
$request.Method = 'HEAD'
$request.AllowAutoRedirect = $false
$response = $request.GetResponse()
try { $location = $response.Headers['Location'] } finally { $response.Close() }
if (-not $location) { Fail 'Could not determine the latest release' }
$version = ($location -split '/')[-1]
}
$version = $version -replace '^v', ''
if ($version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$') { Fail "Invalid release version: $version" }

$asset = "envx-$version-$target.zip"
$url = "$baseUrl/download/v$version/$asset"
$work = Join-Path ([IO.Path]::GetTempPath()) ("envx-install-" + [Guid]::NewGuid())
New-Item -ItemType Directory -Path $work | Out-Null
try {
$archive = Join-Path $work $asset
try { Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $archive } catch { Fail "Release download failed: $url" }

# Older published releases predate checksum sidecars. Never silently
# downgrade verification for a new release or on a mismatch.
$checksum = Join-Path $work 'checksum'
$haveChecksum = $true
try { Invoke-WebRequest -UseBasicParsing -Uri "$url.sha256" -OutFile $checksum } catch { $haveChecksum = $false }
if ($haveChecksum) {
$expected = ((Get-Content -LiteralPath $checksum -TotalCount 1) -split '\s+')[0]
if ($expected -notmatch '^[0-9a-fA-F]{64}$') { Fail 'Invalid SHA256 checksum' }
$actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash
if ($actual -ine $expected) { Fail 'Release checksum mismatch; installation aborted' }
} else {
$parts = $version -split '\.'
if ([int]$parts[0] -lt 2 -or ([int]$parts[0] -eq 2 -and [int]$parts[1] -le 13)) {
Write-Warning "Legacy release $version has no SHA256 sidecar; relying on HTTPS transport."
} else {
Fail 'Release checksum unavailable; installation aborted'
}
}

# Extract only envx.exe; never unpack archive paths into the install folder.
Add-Type -AssemblyName System.IO.Compression.FileSystem
$staged = Join-Path $work 'envx.exe'
$zip = [IO.Compression.ZipFile]::OpenRead($archive)
try {
$entry = $zip.Entries | Where-Object { $_.FullName -eq 'envx.exe' } | Select-Object -First 1
if (-not $entry -or $entry.Length -eq 0) { Fail 'Release archive contains no executable' }
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $staged, $true)
} finally { $zip.Dispose() }

New-Item -ItemType Directory -Path $installDir -Force | Out-Null
# A running envx.exe cannot be overwritten, but it can be renamed aside.
Remove-Item -LiteralPath "$exe.old" -Force -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $exe) { Move-Item -LiteralPath $exe -Destination "$exe.old" -Force }
Move-Item -LiteralPath $staged -Destination $exe -Force
Remove-Item -LiteralPath "$exe.old" -Force -ErrorAction SilentlyContinue
} finally {
Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue
}

Write-Host "Installed envx $version to $exe"
Update-UserPath $true
}
8 changes: 7 additions & 1 deletion skills/envx/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ Verify capability quickly: `envx whoami` (prints your key fingerprint + uuid) an
curl -fsSL https://get.envx.sh | sh # installs to /usr/local/bin (needs sudo)
```

No-sudo / locked-down machines: grab the release tarball for your target from the [`envx-project/cli`](https://github.com/envx-project/cli/releases) GitHub releases (e.g. `envx-2.13.0-aarch64-apple-darwin.tar.gz`), extract, and drop the `envx` binary somewhere on `PATH` such as `~/.local/bin`. Self-update later with `envx update` (re-runs the install script; fails on Windows).
Windows (PowerShell; per-user, no admin; adds itself to the user `PATH`):

```powershell
powershell -c "irm https://raw.githubusercontent.com/envx-project/cli/main/install.ps1 | iex"
```

No-sudo Unix machines: `curl -fsSL get.envx.sh | sh -s -- --bin-dir ~/.local/bin`. Self-update later with `envx update` (re-runs the install script on every platform).

Confirm: `envx --version` → `envx 2.13.0`.

Expand Down
50 changes: 39 additions & 11 deletions src/commands/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,15 @@ use crate::utils::config::Config;

use super::*;

/// Attempt to self-update envx using the installation script. Fails on Windows.
/// Self-update envx using the installation script
#[derive(Parser)]
pub struct Args {}

#[cfg(not(target_os = "windows"))]
const INSTALLER_URL: &str = "https://get.envx.sh";
#[cfg(target_os = "windows")]
pub async fn command(_args: Args, _config: &mut Config) -> Result<()> {
use anyhow::bail;

eprintln!("Self-update is not supported on Windows");
eprintln!("Read the installation instructions at https://github.com/envx-project/cli/blob/main/windows-installation.md");

bail!("Self-update is not supported on Windows")
}
const INSTALLER_URL: &str =
"https://raw.githubusercontent.com/envx-project/cli/main/install.ps1";

#[derive(Default, serde::Serialize, serde::Deserialize)]
pub struct UpdateCheck {
Expand Down Expand Up @@ -66,7 +62,6 @@ pub async fn check_update(force: bool) -> anyhow::Result<String> {
Ok(latest_version.to_string())
}

#[cfg(not(target_os = "windows"))]
pub async fn command(_args: Args, _config: &mut Config) -> Result<()> {
let latest_version = check_update(true).await?;

Expand All @@ -92,7 +87,7 @@ pub async fn command(_args: Args, _config: &mut Config) -> Result<()> {
.connect_timeout(std::time::Duration::from_secs(3))
.timeout(std::time::Duration::from_secs(15))
.build()?
.get("https://get.envx.sh")
.get(INSTALLER_URL)
.send()
.await?
.error_for_status()?
Expand Down Expand Up @@ -125,6 +120,39 @@ async fn run_installer(script: &str) -> Result<()> {
Ok(())
}

/// Reinstall into the folder of the running executable. The installer renames
/// the in-use envx.exe aside, which Windows allows, before placing the update.
#[cfg(target_os = "windows")]
async fn run_installer(script: &str) -> Result<()> {
let exe = std::env::current_exe()?;
let dir = exe.parent().context("Executable has no parent folder")?;
let path = std::env::temp_dir()
.join(format!("envx-install-{}.ps1", uuid::Uuid::new_v4()));
std::fs::write(&path, script)?;
let status = tokio::process::Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
])
.arg(&path)
.env("ENVX_INSTALL_DIR", dir)
.env("ENVX_NO_MODIFY_PATH", "1")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.await;
let _ = std::fs::remove_file(&path);
let status = status?;
if !status.success() {
bail!("Update command failed with status: {}", status);
}
println!("Command executed successfully.");
Ok(())
}

#[cfg(all(test, not(target_os = "windows")))]
mod tests {
use super::*;
Expand Down
22 changes: 22 additions & 0 deletions tests/windows_installer.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Exercise the documented `irm ... | iex` path against the latest published release.
$ErrorActionPreference = 'Stop'
$env:ENVX_INSTALL_DIR = Join-Path ([IO.Path]::GetTempPath()) ("envx-bin-" + [Guid]::NewGuid())
$script = Get-Content -Raw (Join-Path $PSScriptRoot '..\install.ps1')
function UserPath { [Environment]::GetEnvironmentVariable('Path', 'User') }

$script | Invoke-Expression
& (Join-Path $env:ENVX_INSTALL_DIR 'envx.exe') --version
if ($LASTEXITCODE -ne 0) { throw 'installed envx did not run' }
if ((UserPath) -notlike "*$env:ENVX_INSTALL_DIR*") { throw 'user PATH not updated' }

# Reinstall while the old binary exists; the entry must not be duplicated.
$script | Invoke-Expression
if (Test-Path (Join-Path $env:ENVX_INSTALL_DIR 'envx.exe.old')) { throw 'stale .old left behind' }
if (((UserPath) -split ';' | Where-Object { $_ -eq $env:ENVX_INSTALL_DIR }).Count -ne 1) { throw 'PATH entry duplicated' }

$env:ENVX_UNINSTALL = '1'
$script | Invoke-Expression
$env:ENVX_UNINSTALL = ''
if (Test-Path (Join-Path $env:ENVX_INSTALL_DIR 'envx.exe')) { throw 'still installed' }
if ((UserPath) -like "*$env:ENVX_INSTALL_DIR*") { throw 'user PATH not cleaned' }
'PASS: Windows installer install, reinstall, uninstall'
53 changes: 34 additions & 19 deletions windows-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,44 @@

## Installation

1. Download the latest release from [here](https://github.com/envx-project/cli/releases/latest)
- Choose the `x86_64-pc-windows-msvc.zip` file
- ![image](./assets/releases.png)
2. Unzip the file
3. Create a new folder called `envx` in your `C:\` directory
4. Copy the `envx.exe` file from the unzipped folder to the `envx` folder
5. Add the `envx` folder to your PATH environment variable (instructions [here](https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/))
Run this in PowerShell or Command Prompt:

```powershell
powershell -c "irm https://raw.githubusercontent.com/envx-project/cli/main/install.ps1 | iex"
```

The script downloads the latest release, verifies its SHA256 checksum, installs
`envx.exe` to `%LOCALAPPDATA%\Programs\envx` (no administrator rights needed),
and adds that folder to your user `PATH`. Open a new terminal afterwards.

Update later with `envx update`.

Environment variables customise the install:

## Portable Installation
| Variable | Effect |
| --- | --- |
| `ENVX_VERSION` | Install a specific release, e.g. `2.16.0` |
| `ENVX_INSTALL_DIR` | Install somewhere other than `%LOCALAPPDATA%\Programs\envx` |
| `ENVX_PLATFORM` | `msvc` (default) or `gnu` build |
| `ENVX_NO_MODIFY_PATH` | Set to `1` to leave `PATH` unchanged |
| `ENVX_UNINSTALL` | Set to `1` to remove envx and its `PATH` entry |

1. Download the latest release from [here](https://github.com/envx-project/cli/releases/latest)
- Choose the `x86_64-pc-windows-msvc.zip` file
For example, to uninstall:

```powershell
$env:ENVX_UNINSTALL = '1'; irm https://raw.githubusercontent.com/envx-project/cli/main/install.ps1 | iex
```

## Manual installation

1. Download `x86_64-pc-windows-msvc.zip` from the [latest release](https://github.com/envx-project/cli/releases/latest)
- ![image](./assets/releases.png)
2. Unzip the file
3. Open a terminal and navigate to the unzipped folder
4. Run `envx.exe` to start the program
2. Unzip it and move `envx.exe` to a folder of your choice
3. Add that folder to your `PATH` environment variable

## Troubleshooting

### Error: The term 'envx' is not recognized as the name of a cmdlet, function, script file, or operable program

If you get this error, you probably tried to run the program from the global scope when you followed the portable installation instructions.
To fix this, you can either:
### The term 'envx' is not recognized as the name of a cmdlet, function, script file, or operable program

- Run the program from the local scope by navigating to the `envx` folder and running `envx.exe`
- Add the `envx` folder to your PATH environment variable (instructions [here](https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/))
The terminal was opened before `PATH` changed. Open a new terminal. If it still
fails, check that the install folder is listed in your user `PATH`.
Loading