<# Install farm on Windows. In PowerShell (no administrator needed): irm https://farm.kylesoft.com/install.ps1 | iex Puts farm in %USERPROFILE%\buildfarm (or $env:FARM_HOME) and adds its bin folder to your PATH. If there's no Python 3.8+ it uses a private copy of Python from python.org inside that folder, so nothing else on the PC changes. Git is installed with winget if it's missing (Windows may ask for permission). Running it again updates farm and keeps your machines and builds. #> $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $Version = '0.2.0' $Sha256 = '303d6181bf955b987665fa53ccc8f8cff3de19cd6a4e8371552f4bda2a320928' $Site = if ($env:FARM_SITE) { $env:FARM_SITE } else { 'https://farm.kylesoft.com' } # Where release files live (this site today; maybe a storage bucket later). $Downloads = if ($env:FARM_DOWNLOADS) { $env:FARM_DOWNLOADS } elseif ($env:FARM_SITE) { $env:FARM_SITE } else { 'https://farm.kylesoft.com' } $Dir = if ($env:FARM_HOME) { $env:FARM_HOME } else { Join-Path $HOME 'buildfarm' } # Python from python.org (the "embeddable" zip: a self-contained Python in one folder). $PyVersion = '3.13.16' $PyBuilds = @{ 'AMD64' = @{ File = "python-$PyVersion-embed-amd64.zip"; Sha256 = '97dae5274cc54867065e8d5a3226e48c35017ed332a0fdb0e27d5b5821961297' } 'ARM64' = @{ File = "python-$PyVersion-embed-arm64.zip"; Sha256 = '790d097697a2020477549a6764d89d22194c88d6e42caebcb2db63791e004c94' } } function Say($m) { Write-Host "==> $m" -ForegroundColor Cyan } function Note($m) { Write-Host " $m" } function Refresh-Path { $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User') } function Get-Checked($url, $out, $sha) { Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $out $got = (Get-FileHash -Algorithm SHA256 $out).Hash.ToLower() if ($got -ne $sha) { throw "$url doesn't match its checksum (got $got). Nothing was installed." } } function Test-Python($exe) { # The Microsoft Store 'python' placeholder exits with an error instead of running. try { & $exe -c 'import sys; sys.exit(sys.version_info < (3, 8))' 2>$null; return $LASTEXITCODE -eq 0 } catch { return $false } } if (Test-Path (Join-Path $Dir '.git')) { throw "$Dir is a git checkout of farm; update it with git pull instead" } # ------------------------------------------------------------------ what farm needs if (-not (Get-Command ssh -ErrorAction SilentlyContinue)) { throw "The OpenSSH client is missing. Add it in Settings > System > Optional features > OpenSSH Client, then run this again." } Refresh-Path if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Say 'Installing Git' if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { throw 'Git is missing and so is winget. Install Git from https://git-scm.com/download/win, then run this again.' } winget install --id Git.Git -e --silent --accept-source-agreements --accept-package-agreements --disable-interactivity Refresh-Path if (-not (Get-Command git -ErrorAction SilentlyContinue)) { throw 'Git did not install. Install it from https://git-scm.com/download/win, then run this again.' } } $tmp = Join-Path ([IO.Path]::GetTempPath()) ("farm-install-" + [guid]::NewGuid().ToString('N').Substring(0, 8)) New-Item -ItemType Directory -Force $tmp | Out-Null try { New-Item -ItemType Directory -Force $Dir | Out-Null $pyDir = Join-Path $Dir 'python' $usePrivate = Test-Path (Join-Path $pyDir 'python.exe') if (-not $usePrivate -and -not ((Get-Command python -ErrorAction SilentlyContinue) -and (Test-Python 'python'))) { $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } $pb = $PyBuilds[$arch] if (-not $pb) { throw "No Python download for $arch; install Python 3.8+ from python.org, then run this again." } Say "Getting a private Python $PyVersion for farm (from python.org, about 11 MB)" $zip = Join-Path $tmp $pb.File Get-Checked "https://www.python.org/ftp/python/$PyVersion/$($pb.File)" $zip $pb.Sha256 Expand-Archive -Path $zip -DestinationPath $pyDir -Force Note "Python is in $pyDir (only farm uses it)" } # ---------------------------------------------------------------- download and check $tarball = "farm-$Version.tar.gz" Say "Downloading farm $Version" Get-Checked "$Downloads/$tarball" (Join-Path $tmp $tarball) $Sha256 Note 'checksum ok' New-Item -ItemType Directory -Force (Join-Path $tmp 'x') | Out-Null tar.exe -xzf (Join-Path $tmp $tarball) -C (Join-Path $tmp 'x') if ($LASTEXITCODE -ne 0) { throw 'Could not unpack farm (tar failed).' } $src = Join-Path $tmp "x\farm-$Version" # ---------------------------------------------------------------- install Say "Installing into $Dir" # Replace farm's own files; hosts.toml, state, artifacts and work are yours and stay. foreach ($d in 'bin', 'lib', 'bootstrap', 'templates', 'join') { $target = Join-Path $Dir $d if (Test-Path $target) { Remove-Item -Recurse -Force $target } Copy-Item -Recurse (Join-Path $src $d) $target } foreach ($f in 'README.md', 'hosts.example.toml', 'LICENSE') { if (Test-Path (Join-Path $src $f)) { Copy-Item (Join-Path $src $f) $Dir -Force } } $bin = Join-Path $Dir 'bin' $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if (-not (($userPath -split ';') -contains $bin)) { [Environment]::SetEnvironmentVariable('Path', ($(if ($userPath) { "$userPath;" } else { '' }) + $bin), 'User') Note "added $bin to your PATH (new terminals pick it up)" } $env:Path = "$bin;$env:Path" & (Join-Path $bin 'farm.cmd') version | Out-Null if ($LASTEXITCODE -ne 0) { throw 'farm was copied but does not start; please report this.' } } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } Write-Host '' Say "farm $Version is installed." Write-Host '' Write-Host 'Next (in this window or a new one):' Write-Host ' farm setup check this computer and add a build machine' Write-Host ' farm ui or do everything from your browser'