Skip to content

Commit f9ceb52

Browse files
committed
feat(windows/installer): add hardened PowerShell installer; update docs; relax runtime regex for CI
We ship a first‑party Windows installer that’s safe on PS 5.1 and PS 7+ and robust on GitHub runners. Installer: - Validates parameters; forces TLS 1.2; uses `UseBasicParsing` only on PS < 6. - Verifies archive SHA256 against `checksums.txt`. - Installs to a configurable dir, creates `cu.exe`, and updates PATH (User) without duplicates. - Supports `-Repo` for fork testing. Docs: - Replace the multi‑step PowerShell instructions with a one‑liner that works on PS 5.1 and PS 7+. Tests: - Allow “unknown” container runtime version in CI where the runner doesn’t report a version string. Signed-off-by: Guillaume de Rouville <guillaume@dagger.io>
1 parent 067b7a1 commit f9ceb52

3 files changed

Lines changed: 330 additions & 2 deletions

File tree

cmd/container-use/version_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestVersionCommand(t *testing.T) {
4646

4747
// Container runtime output should show one of the supported runtimes
4848
// This handles: "Docker 24.0.5", "Podman 4.3.1", "Docker 24.0.5 (daemon not running)", or "not found"
49-
assert.Regexp(t, `Container Runtime: ((Docker|Podman|nerdctl|finch) [\d\.]+(v[\d\.]+)?(\s+\(daemon not running\))?|not found)`, output)
49+
assert.Regexp(t, `Container Runtime: ((Docker|Podman|nerdctl|finch) ([\d\.]+(v[\d\.]+)?|unknown)(\s+\(daemon not running\))?|not found)`, output)
5050
},
5151
},
5252
{

docs/quickstart.mdx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,28 @@ Make sure you have [Docker](https://www.docker.com/get-started) and Git installe
2020

2121
</Tab>
2222

23-
<Tab title="Shell Script (All Platforms)">
23+
<Tab title="Windows">
24+
25+
<Note>Native Windows support requires Docker Desktop</Note>
26+
27+
<Tabs>
28+
<Tab title="PowerShell Script">
29+
```powershell
30+
$u='https://raw.githubusercontent.com/dagger/container-use/main/install.ps1';$d=Join-Path $env:TEMP 'install-container-use.ps1';$o=@{};if((Get-Command Invoke-WebRequest).Parameters.ContainsKey('UseBasicParsing')){$o.UseBasicParsing=$true};Invoke-WebRequest -Uri $u -OutFile $d @o;& $d -AddToPath
31+
```
32+
</Tab>
33+
34+
<Tab title="Chocolatey">
35+
```powershell
36+
# Available after first release with Windows support
37+
choco install container-use
38+
```
39+
</Tab>
40+
</Tabs>
41+
42+
</Tab>
43+
44+
<Tab title="Shell Script (Linux/macOS)">
2445

2546
```sh
2647
curl -fsSL https://raw.githubusercontent.com/dagger/container-use/main/install.sh | bash

install.ps1

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
#Requires -Version 5.0
2+
<#
3+
.Description
4+
Download and install container-use.
5+
6+
.PARAMETER Version
7+
Version of container-use to install (e.g., v0.4.0). Defaults to latest.
8+
Also supports a 7-40 char git commit or "latest".
9+
10+
.PARAMETER DownloadPath
11+
Temporary download location seed. The script will place artifacts in the same directory
12+
as this temp file. Defaults to a system temp file.
13+
14+
.PARAMETER InstallPath
15+
Installation directory. Defaults to $env:USERPROFILE\container-use
16+
17+
.PARAMETER AddToPath
18+
If set, add the installation directory to the user's PATH (no elevation required).
19+
20+
.PARAMETER Repo
21+
(Advanced) GitHub "owner/repo" to install from. Defaults to dagger/container-use.
22+
Useful for testing a fork without editing the script.
23+
24+
.EXAMPLE
25+
.\install.ps1
26+
Install latest version with default settings.
27+
28+
.EXAMPLE
29+
.\install.ps1 -InstallPath "C:\tools\container-use"
30+
Install to C:\tools\container-use.
31+
32+
.EXAMPLE
33+
.\install.ps1 -Version v0.4.0
34+
Install specified version v0.4.0.
35+
36+
.EXAMPLE
37+
.\install.ps1 -AddToPath
38+
Install and add to PATH.
39+
40+
.EXAMPLE
41+
# Install from a fork's releases for testing
42+
.\install.ps1 -Repo "grouville/container-use" -Version v0.3.5-test -AddToPath
43+
#>
44+
45+
Param (
46+
[Parameter(Mandatory = $false)]
47+
[ValidatePattern('^(latest|v?\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?|[a-f0-9]{7,40})$')]
48+
[string]$Version = "latest",
49+
50+
[Parameter(Mandatory = $false)]
51+
[ValidateNotNullOrEmpty()]
52+
[string]$DownloadPath = [System.IO.Path]::GetTempFileName(),
53+
54+
[Parameter(Mandatory = $false)]
55+
[ValidateNotNullOrEmpty()]
56+
[string]$InstallPath = "$env:USERPROFILE\container-use",
57+
58+
[Parameter(Mandatory = $false)]
59+
[switch]$AddToPath = $false,
60+
61+
[Parameter(Mandatory = $false)]
62+
[ValidatePattern('^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$')]
63+
[string]$Repo = "dagger/container-use"
64+
)
65+
66+
# ---------------------------------------------------------------------------------
67+
# Container Use Installation Utility for Windows
68+
# Hardened for PS 5.1 and PS 7+; secure downloads; robust PATH handling
69+
# ---------------------------------------------------------------------------------
70+
71+
$ErrorActionPreference = "Stop"
72+
73+
# Ensure TLS 1.2 (older Windows can default to TLS 1.0/1.1)
74+
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { }
75+
76+
# Configuration
77+
$REPO = $Repo
78+
$BINARY_NAME = "container-use"
79+
80+
# Conditionally supply -UseBasicParsing (PS < 6 supports it; PS 7+ removed it)
81+
function New-WebArgs {
82+
param([Parameter(Mandatory=$true)] [string]$Uri)
83+
$args = @{ Uri = $Uri }
84+
if ($PSVersionTable.PSVersion.Major -lt 6) { $args.UseBasicParsing = $true }
85+
return $args
86+
}
87+
88+
# Safely append to PATH (User by default), avoiding dupes and empty tails
89+
function Add-PathEntry {
90+
param(
91+
[Parameter(Mandatory=$true)] [string]$PathToAdd,
92+
[Parameter(Mandatory=$false)] [ValidateSet('User','Machine')] [string]$Scope = 'User'
93+
)
94+
$cur = [Environment]::GetEnvironmentVariable('Path', $Scope)
95+
if (-not $cur) { $cur = '' }
96+
$parts = @($cur -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + @()
97+
98+
if ($parts -notcontains $PathToAdd) {
99+
$newPath = ($parts + $PathToAdd) -join ';'
100+
[Environment]::SetEnvironmentVariable('Path', $newPath, $Scope)
101+
Write-Host "Added to $Scope PATH. Restart your terminal to pick it up." -ForegroundColor Green
102+
} else {
103+
Write-Host "Path already contains $PathToAdd" -ForegroundColor Green
104+
}
105+
}
106+
107+
function Get-ProcessorArchitecture {
108+
# Map to Go arch names we ship: amd64, arm64
109+
$arch = $env:PROCESSOR_ARCHITECTURE
110+
switch ($arch) {
111+
'AMD64' { return 'amd64' }
112+
'ARM64' { return 'arm64' }
113+
default { throw "Unsupported architecture: $arch (supported: AMD64, ARM64)" }
114+
}
115+
}
116+
117+
function Find-LatestVersion {
118+
try {
119+
$release = Invoke-RestMethod @((New-WebArgs "https://api.github.com/repos/$REPO/releases/latest")) -UserAgent "PowerShell"
120+
return $release.tag_name
121+
} catch {
122+
throw "Failed to fetch latest release: $_"
123+
}
124+
}
125+
126+
function Get-DownloadUrl {
127+
Param (
128+
[Parameter(Mandatory = $true)] [string]$Version,
129+
[Parameter(Mandatory = $true)] [string]$Arch
130+
)
131+
# GoReleaser uses the tag (with 'v') in archive names
132+
"https://github.com/$REPO/releases/download/$Version/container-use_${Version}_windows_${Arch}.zip"
133+
}
134+
135+
function Get-ChecksumUrl {
136+
Param ([Parameter(Mandatory = $true)] [string]$Version)
137+
"https://github.com/$REPO/releases/download/$Version/checksums.txt"
138+
}
139+
140+
function Get-Checksum {
141+
Param (
142+
[Parameter(Mandatory = $true)] [string]$Version,
143+
[Parameter(Mandatory = $true)] [string]$Arch
144+
)
145+
$checksumUrl = Get-ChecksumUrl -Version $Version
146+
$target = "container-use_${Version}_windows_${Arch}.zip"
147+
148+
try {
149+
$response = Invoke-RestMethod @((New-WebArgs $checksumUrl)) -UserAgent "PowerShell"
150+
$checksums = $response -split "`n"
151+
152+
foreach ($line in $checksums) {
153+
if ($line -match [regex]::Escape($target)) {
154+
return ($line -split ' ' | Select-Object -First 1)
155+
}
156+
}
157+
throw "Checksum not found for $target"
158+
} catch {
159+
throw "Failed to fetch or parse checksums: $_"
160+
}
161+
}
162+
163+
function Compare-Checksum {
164+
Param (
165+
[Parameter(Mandatory = $true)] [string]$FilePath,
166+
[Parameter(Mandatory = $true)] [string]$ExpectedChecksum
167+
)
168+
$hash = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash
169+
if ($hash.ToUpperInvariant() -ne $ExpectedChecksum.ToUpperInvariant()) {
170+
Remove-Item -Path $FilePath -Force
171+
throw "Checksum mismatch. Expected: $ExpectedChecksum, Got: $hash"
172+
}
173+
}
174+
175+
function Get-InstallPath {
176+
if (-not (Test-Path $InstallPath)) {
177+
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
178+
}
179+
return (Get-Item -Path $InstallPath).FullName
180+
}
181+
182+
function Test-Dependencies {
183+
Write-Host "Checking dependencies..." -ForegroundColor Blue
184+
185+
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
186+
Write-Host " Docker is required but not installed." -ForegroundColor Red
187+
Write-Host " Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/" -ForegroundColor Yellow
188+
return $false
189+
}
190+
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
191+
Write-Host " Git is required but not installed." -ForegroundColor Red
192+
Write-Host " Install Git: https://git-scm.com/download/win" -ForegroundColor Yellow
193+
return $false
194+
}
195+
196+
try { docker version *>$null } catch { Write-Host " Docker is installed but not responding." -ForegroundColor Red; return $false }
197+
try { git version *>$null } catch { Write-Host " Git is installed but not responding." -ForegroundColor Red; return $false }
198+
199+
Write-Host " Docker is installed" -ForegroundColor Green
200+
Write-Host " Git is installed" -ForegroundColor Green
201+
return $true
202+
}
203+
204+
function Install-ContainerUse {
205+
Write-Host ""
206+
Write-Host "Container Use Installer for Windows" -ForegroundColor Cyan
207+
Write-Host "===================================" -ForegroundColor Cyan
208+
Write-Host ""
209+
210+
if (-not (Test-Dependencies)) {
211+
throw "Missing required dependencies"
212+
}
213+
214+
$targetVersion = $Version
215+
if ($targetVersion -eq "latest") {
216+
Write-Host "Finding latest version..." -ForegroundColor Blue
217+
$targetVersion = Find-LatestVersion
218+
Write-Host "Latest version: $targetVersion" -ForegroundColor Green
219+
}
220+
221+
$arch = Get-ProcessorArchitecture
222+
Write-Host "Architecture: $arch" -ForegroundColor Blue
223+
224+
$downloadUrl = Get-DownloadUrl -Version $targetVersion -Arch $arch
225+
226+
$zipName = "container-use_${targetVersion}_windows_${arch}.zip"
227+
$zipPath = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($DownloadPath), $zipName)
228+
229+
Write-Host "Downloading from $downloadUrl..." -ForegroundColor Blue
230+
try {
231+
Invoke-WebRequest @((New-WebArgs $downloadUrl)) -OutFile $zipPath
232+
Write-Host "Downloaded successfully" -ForegroundColor Green
233+
} catch {
234+
throw "Failed to download: $_"
235+
}
236+
237+
Write-Host "Verifying checksum..." -ForegroundColor Blue
238+
try {
239+
$expectedChecksum = Get-Checksum -Version $targetVersion -Arch $arch
240+
Compare-Checksum -FilePath $zipPath -ExpectedChecksum $expectedChecksum
241+
Write-Host "Checksum verified" -ForegroundColor Green
242+
} catch {
243+
throw "Checksum verification failed: $_"
244+
}
245+
246+
$tempExtractPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "container-use-extract-$(Get-Random)")
247+
Write-Host "Extracting..." -ForegroundColor Blue
248+
try {
249+
Expand-Archive -Path $zipPath -DestinationPath $tempExtractPath -Force
250+
} catch {
251+
throw "Failed to extract: $_"
252+
} finally {
253+
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
254+
}
255+
256+
$installFullPath = Get-InstallPath
257+
258+
$exePath = Join-Path $tempExtractPath "container-use.exe"
259+
if (-not (Test-Path $exePath)) {
260+
throw "container-use.exe not found in archive"
261+
}
262+
263+
$destPath = Join-Path $installFullPath "container-use.exe"
264+
Copy-Item -Path $exePath -Destination $destPath -Force
265+
Write-Host "Installed to $destPath" -ForegroundColor Green
266+
267+
# Create cu.exe alias for convenience
268+
$cuPath = Join-Path $installFullPath "cu.exe"
269+
Copy-Item -Path $destPath -Destination $cuPath -Force
270+
Write-Host "Created cu.exe alias" -ForegroundColor Green
271+
272+
# Cleanup
273+
Remove-Item $tempExtractPath -Recurse -Force -ErrorAction SilentlyContinue
274+
275+
# PATH update on request
276+
if ($AddToPath) {
277+
Add-PathEntry -PathToAdd $installFullPath -Scope 'User'
278+
} else {
279+
Write-Host ""
280+
Write-Host "To add container-use to your PATH, run:" -ForegroundColor Yellow
281+
Write-Host " [Environment]::SetEnvironmentVariable('Path', `$env:Path + ';$installFullPath', [EnvironmentVariableTarget]::User)" -ForegroundColor White
282+
Write-Host "Or run this script again with -AddToPath" -ForegroundColor Yellow
283+
}
284+
285+
# Verify installation
286+
Write-Host ""
287+
Write-Host "Verifying installation..." -ForegroundColor Blue
288+
try {
289+
$versionOutput = (& $destPath version 2>&1 | Out-String).Trim()
290+
Write-Host "container-use is ready! Version: $versionOutput" -ForegroundColor Green
291+
} catch {
292+
Write-Host "container-use installed but couldn't verify version" -ForegroundColor Yellow
293+
}
294+
295+
Write-Host ""
296+
Write-Host "Installation complete!" -ForegroundColor Green
297+
Write-Host "Run 'container-use --help' to get started" -ForegroundColor Cyan
298+
}
299+
300+
# Main execution
301+
try {
302+
Install-ContainerUse
303+
} catch {
304+
Write-Host ""
305+
Write-Host "Installation failed: $_" -ForegroundColor Red
306+
exit 1
307+
}

0 commit comments

Comments
 (0)