fix(windows): improve PowerShell installer and add clone script

- Fix return value leakage in install-core.ps1 (suppressed with $null)
- Fix ACL -bor compatibility across PowerShell versions
- Handle unsigned/lightweight git tags in GPG verification
- Skip GPG verification for branch builds (main)
- Add explicit GOOS=windows for Go build
- Detect Windows syscall build errors with helpful message
- Add clone-repos.ps1 as Windows alternative to `core setup`
- Update CLAUDE.md with Windows-specific setup instructions

Closes #56 workaround documented

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-02-01 01:17:10 +00:00
parent 621438a876
commit 9a52fd937a
3 changed files with 141 additions and 23 deletions

View file

@ -53,12 +53,38 @@ See `.core/docs/core-folder-spec.md` for the full specification that each packag
```bash ```bash
# macOS/Linux # macOS/Linux
git clone git@github.com:host-uk/core-devops.git && cd core-devops && make setup git clone git@github.com:host-uk/core-devops.git && cd core-devops && make setup
# Windows (PowerShell as Admin)
.\scripts\install-deps.ps1 && .\scripts\install-core.ps1 && core setup
``` ```
Environment variables for `install-core.sh`: ### Windows Setup
The `core` CLI is not yet available on Windows. Use these steps instead:
```powershell
# 1. Install dependencies (PowerShell as Admin)
.\scripts\install-deps.ps1
# 2. Authenticate GitHub CLI
gh auth login -h github.com -p https -s workflow,repo,read:org,read:project,project
# 3. Clone all repos
.\scripts\clone-repos.ps1
# 4. Enable PHP extensions (edit C:\tools\php84\php.ini, uncomment these):
# extension=curl
# extension=fileinfo
# extension=mbstring
# extension=openssl
# extension=pdo_sqlite
# 5. Install and test a package
cd packages/core-php
composer install
composer test
```
### Environment Variables (macOS/Linux)
For `install-core.sh`:
- `INSTALL_DIR` - Binary location (default: `~/.local/bin`) - `INSTALL_DIR` - Binary location (default: `~/.local/bin`)
- `BUILD_FROM_SOURCE` - `true`, `false`, or `auto` (default: `auto`, tries binary then builds) - `BUILD_FROM_SOURCE` - `true`, `false`, or `auto` (default: `auto`, tries binary then builds)
@ -140,6 +166,12 @@ Defined in `repos.yaml`:
- **"refusing to allow..." or "missing required scopes"** → `gh auth refresh -h github.com -s workflow,read:project,project` - **"refusing to allow..." or "missing required scopes"** → `gh auth refresh -h github.com -s workflow,read:project,project`
- **Clone failures**`ssh -T git@github.com` to verify SSH keys - **Clone failures**`ssh -T git@github.com` to verify SSH keys
### Windows-Specific
- **"openssl extension is required"** → Enable extensions in `C:\tools\php84\php.ini` (see setup above)
- **"composer: command not found"** → Use PowerShell, not Git Bash: `powershell -Command "composer install"`
- **core CLI not available** → Use `.\scripts\clone-repos.ps1` and work directly with composer
## This Repo's Scope ## This Repo's Scope
Don't add application code here. This repo only contains: Don't add application code here. This repo only contains:

48
scripts/clone-repos.ps1 Normal file
View file

@ -0,0 +1,48 @@
# Clone all Host UK repos (Windows alternative to `core setup`)
# Run: .\scripts\clone-repos.ps1
$ErrorActionPreference = "Stop"
$repos = @(
"core-php",
"core-tenant",
"core-admin",
"core-api",
"core-mcp",
"core-agentic",
"core-bio",
"core-social",
"core-analytics",
"core-notify",
"core-trust",
"core-support",
"core-commerce",
"core-content",
"core-tools",
"core-uptelligence",
"core-developer",
"core-template"
)
$packagesDir = Join-Path $PSScriptRoot "..\packages"
Write-Host "[INFO] Cloning repos to $packagesDir" -ForegroundColor Green
foreach ($repo in $repos) {
$repoPath = Join-Path $packagesDir $repo
if (Test-Path $repoPath) {
Write-Host "[SKIP] $repo already exists" -ForegroundColor Yellow
continue
}
Write-Host "[CLONE] $repo..." -ForegroundColor Cyan
gh repo clone "host-uk/$repo" $repoPath
if ($LASTEXITCODE -ne 0) {
Write-Host "[WARN] Failed to clone $repo" -ForegroundColor Yellow
}
}
Write-Host ""
Write-Host "[DONE] Repos cloned. Run 'composer install' in each package." -ForegroundColor Green

View file

@ -27,7 +27,7 @@ if ($PSVersionTable.PSVersion.Major -lt 4) {
} }
$Repo = "host-uk/core" $Repo = "host-uk/core"
$Version = "v0.1.0" # Pinned version - update when releasing new versions $Version = "main" # Build from main until stable Windows releases are available
$MinDiskSpaceMB = 100 # Minimum required disk space in MB $MinDiskSpaceMB = 100 # Minimum required disk space in MB
function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green } function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green }
@ -144,7 +144,7 @@ function New-SecureDirectory {
# Check parent directory for symlinks first # Check parent directory for symlinks first
$parent = Split-Path $Path -Parent $parent = Split-Path $Path -Parent
if ($parent -and (Test-Path $parent)) { if ($parent -and (Test-Path $parent)) {
Test-SecureDirectory -Path $parent $null = Test-SecureDirectory -Path $parent
} }
# Create directory # Create directory
@ -153,7 +153,7 @@ function New-SecureDirectory {
} }
# Immediately verify it's not a symlink (minimize TOCTOU window) # Immediately verify it's not a symlink (minimize TOCTOU window)
Test-SecureDirectory -Path $Path $null = Test-SecureDirectory -Path $Path
return $Path return $Path
} }
@ -181,10 +181,12 @@ function Set-SecureDirectoryAcl {
# Add full control for current user only # Add full control for current user only
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
# Pre-calculate flags to avoid -bor compatibility issues across PowerShell versions
$inheritFlags = [System.Security.AccessControl.InheritanceFlags]([int][System.Security.AccessControl.InheritanceFlags]::ContainerInherit + [int][System.Security.AccessControl.InheritanceFlags]::ObjectInherit)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
$currentUser, $currentUser,
[System.Security.AccessControl.FileSystemRights]::FullControl, [System.Security.AccessControl.FileSystemRights]::FullControl,
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit, $inheritFlags,
[System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.PropagationFlags]::None,
[System.Security.AccessControl.AccessControlType]::Allow [System.Security.AccessControl.AccessControlType]::Allow
) )
@ -221,8 +223,8 @@ function Download-Binary {
$tempExe = $null $tempExe = $null
try { try {
# Create and verify install directory # Create and verify install directory (suppress output to avoid polluting return value)
New-SecureDirectory -Path $InstallDir $null = New-SecureDirectory -Path $InstallDir
# Use a temp file in the same directory (same filesystem for atomic move) # Use a temp file in the same directory (same filesystem for atomic move)
$tempExe = Join-Path $InstallDir "core.exe.tmp.$([System.Guid]::NewGuid().ToString('N').Substring(0,8))" $tempExe = Join-Path $InstallDir "core.exe.tmp.$([System.Guid]::NewGuid().ToString('N').Substring(0,8))"
@ -252,7 +254,7 @@ function Download-Binary {
Test-FileHash -FilePath $tempExe -ExpectedHash $expectedHash Test-FileHash -FilePath $tempExe -ExpectedHash $expectedHash
# Re-verify directory hasn't been replaced with symlink (reduce TOCTOU window) # Re-verify directory hasn't been replaced with symlink (reduce TOCTOU window)
Test-SecureDirectory -Path $InstallDir $null = Test-SecureDirectory -Path $InstallDir
# Atomic move to final location (same filesystem) # Atomic move to final location (same filesystem)
$finalPath = Join-Path $InstallDir "core.exe" $finalPath = Join-Path $InstallDir "core.exe"
@ -297,13 +299,19 @@ function Test-GitTagSignature {
Push-Location $RepoPath Push-Location $RepoPath
try { try {
# Attempt to verify the tag signature # Attempt to verify the tag signature
$result = git tag -v $Tag 2>&1 # Use $ErrorActionPreference temporarily to prevent stderr from throwing
if ($LASTEXITCODE -eq 0) { $oldErrorAction = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$result = git tag -v $Tag 2>&1 | Out-String
$exitCode = $LASTEXITCODE
$ErrorActionPreference = $oldErrorAction
if ($exitCode -eq 0) {
Write-Info "GPG signature verified for tag $Tag" Write-Info "GPG signature verified for tag $Tag"
return $true return $true
} else { } else {
# Check if tag is unsigned vs signature invalid # Check if tag is unsigned vs signature invalid
if ($result -match "error: no signature found") { if ($result -match "no signature found" -or $result -match "cannot verify a non-tag") {
Write-Warn "Tag $Tag is not signed - continuing without signature verification" Write-Warn "Tag $Tag is not signed - continuing without signature verification"
return $true return $true
} else { } else {
@ -326,10 +334,10 @@ function Build-FromSource {
# Create secure temp directory with restrictive ACL # Create secure temp directory with restrictive ACL
$tmpdir = Join-Path ([System.IO.Path]::GetTempPath()) "core-build-$([System.Guid]::NewGuid().ToString('N'))" $tmpdir = Join-Path ([System.IO.Path]::GetTempPath()) "core-build-$([System.Guid]::NewGuid().ToString('N'))"
New-SecureDirectory -Path $tmpdir $null = New-SecureDirectory -Path $tmpdir
# ACL is REQUIRED for temp build directories (security critical) # ACL is REQUIRED for temp build directories (security critical)
Set-SecureDirectoryAcl -Path $tmpdir -Required $null = Set-SecureDirectoryAcl -Path $tmpdir -Required
try { try {
Write-Info "Cloning $Repo (version $Version)..." Write-Info "Cloning $Repo (version $Version)..."
@ -341,22 +349,52 @@ function Build-FromSource {
Write-Err "Failed to clone repository at version $Version" Write-Err "Failed to clone repository at version $Version"
} }
# Verify GPG signature on tag (if available) # Verify GPG signature on tag (if available, skip for branches)
Test-GitTagSignature -RepoPath $cloneDir -Tag $Version if ($Version -match "^v\d") {
$null = Test-GitTagSignature -RepoPath $cloneDir -Tag $Version
} else {
Write-Warn "Building from branch '$Version' - GPG verification skipped (only applies to tags)"
}
Write-Info "Building core CLI..." Write-Info "Building core CLI..."
Push-Location $cloneDir Push-Location $cloneDir
try { try {
go build -o core.exe . # Explicitly set GOOS/GOARCH to ensure Windows build
if ($LASTEXITCODE -ne 0) { $env:GOOS = "windows"
Write-Err "Go build failed" $env:GOARCH = "amd64"
$oldErrorAction = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$buildOutput = go build -o core.exe . 2>&1 | Out-String
$buildExitCode = $LASTEXITCODE
$ErrorActionPreference = $oldErrorAction
if ($buildExitCode -ne 0) {
# Check for Windows-specific build issues
if ($buildOutput -match "Setpgid|Getpgid|syscall\.Kill|undefined.*syscall") {
Write-Warn "Build failed: core CLI uses Unix-specific syscalls not available on Windows."
Write-Warn ""
Write-Warn "Options:"
Write-Warn " 1. Wait for pre-built Windows binaries in GitHub releases"
Write-Warn " 2. Use WSL (Windows Subsystem for Linux) for development"
Write-Warn " 3. Work directly with composer in packages/ (core CLI is optional)"
Write-Warn ""
Write-Warn "For PHP development, you can use composer directly:"
Write-Warn " cd packages/core-php && composer test"
Write-Host ""
# Don't use Write-Err here - exit gracefully
exit 0
} else {
Write-Host $buildOutput
Write-Err "Go build failed"
}
} }
} finally { } finally {
Pop-Location Pop-Location
} }
# Create and verify install directory # Create and verify install directory
New-SecureDirectory -Path $InstallDir $null = New-SecureDirectory -Path $InstallDir
# Move built binary to install location # Move built binary to install location
Move-Item (Join-Path $cloneDir "core.exe") (Join-Path $InstallDir "core.exe") -Force Move-Item (Join-Path $cloneDir "core.exe") (Join-Path $InstallDir "core.exe") -Force
@ -427,7 +465,7 @@ function Main {
Write-Info "Installing Core CLI (version $Version)..." Write-Info "Installing Core CLI (version $Version)..."
# Check disk space before starting # Check disk space before starting
Test-DiskSpace -Path $InstallDir $null = Test-DiskSpace -Path $InstallDir
# Try download first, fallback to build # Try download first, fallback to build
if (-not (Download-Binary)) { if (-not (Download-Binary)) {