Initial server source import
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidatePattern('^release-[A-Za-z0-9][A-Za-z0-9._-]{0,63}$')]
|
||||
[string]$ReleaseName = (Get-Date -Format "'release-'yyyyMMdd-HHmmss"),
|
||||
[string]$WslDistro = 'Ubuntu-24.04',
|
||||
[string]$Python = 'python',
|
||||
[switch]$SkipTests
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ProjectRoot = $PSScriptRoot
|
||||
$DistRoot = Join-Path $ProjectRoot 'dist'
|
||||
$ReleaseRoot = Join-Path $DistRoot $ReleaseName
|
||||
$ReleaseId = $ReleaseName.Substring('release-'.Length)
|
||||
$ImageTag = 'truf-remote-worker:linux-x86_64'
|
||||
$DockerMode = $null
|
||||
|
||||
function Invoke-Checked {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Command,
|
||||
[Parameter(Mandatory = $true)][string[]]$Arguments,
|
||||
[Parameter(Mandatory = $true)][string]$Label
|
||||
)
|
||||
& $Command @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-DockerChecked {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string[]]$Arguments,
|
||||
[Parameter(Mandatory = $true)][string]$Label
|
||||
)
|
||||
if ($script:DockerMode -eq 'native') {
|
||||
Invoke-Checked -Command 'docker' -Arguments $Arguments -Label $Label
|
||||
return
|
||||
}
|
||||
$prefix = @(
|
||||
'-d', $script:WslDistro, '--', 'sudo', '-n', 'docker',
|
||||
'-H', 'unix:///var/run/docker.sock'
|
||||
)
|
||||
Invoke-Checked -Command 'wsl.exe' -Arguments ($prefix + $Arguments) -Label $Label
|
||||
}
|
||||
|
||||
function Get-DockerText {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string[]]$Arguments,
|
||||
[Parameter(Mandatory = $true)][string]$Label
|
||||
)
|
||||
if ($script:DockerMode -eq 'native') {
|
||||
$output = @(& docker @Arguments)
|
||||
}
|
||||
else {
|
||||
$prefix = @(
|
||||
'-d', $script:WslDistro, '--', 'sudo', '-n', 'docker',
|
||||
'-H', 'unix:///var/run/docker.sock'
|
||||
)
|
||||
$output = @(& wsl.exe @prefix @Arguments)
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
return ($output -join "`n")
|
||||
}
|
||||
|
||||
function Get-WslPath {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
$fullPath = [IO.Path]::GetFullPath($Path)
|
||||
if ($fullPath -notmatch '^(?<drive>[A-Za-z]):\\(?<tail>.*)$') {
|
||||
throw "Only absolute Windows drive paths can be translated for WSL: $Path"
|
||||
}
|
||||
$drive = $Matches['drive'].ToLowerInvariant()
|
||||
$tail = $Matches['tail'].Replace('\', '/')
|
||||
return "/mnt/$drive/$tail"
|
||||
}
|
||||
|
||||
function Get-Sha256 {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Write-AsciiLines {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][string[]]$Lines
|
||||
)
|
||||
[IO.File]::WriteAllText(
|
||||
$Path,
|
||||
(($Lines -join "`n") + "`n"),
|
||||
[Text.Encoding]::ASCII
|
||||
)
|
||||
}
|
||||
|
||||
function Write-Sha256Manifest {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][object[]]$Files
|
||||
)
|
||||
$lines = foreach ($file in $Files) {
|
||||
if ($file -is [string]) {
|
||||
$source = $file
|
||||
$name = [IO.Path]::GetFileName($file)
|
||||
}
|
||||
else {
|
||||
$source = [string]$file.Path
|
||||
$name = [string]$file.Name
|
||||
}
|
||||
"$(Get-Sha256 -Path $source) $name"
|
||||
}
|
||||
Write-AsciiLines -Path $Path -Lines $lines
|
||||
}
|
||||
|
||||
function Compress-Gzip {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$InputPath,
|
||||
[Parameter(Mandatory = $true)][string]$OutputPath
|
||||
)
|
||||
$source = [IO.File]::OpenRead($InputPath)
|
||||
try {
|
||||
$destination = [IO.File]::Open(
|
||||
$OutputPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write,
|
||||
[IO.FileShare]::None
|
||||
)
|
||||
try {
|
||||
$gzip = New-Object IO.Compression.GZipStream(
|
||||
$destination, [IO.Compression.CompressionLevel]::Fastest, $true
|
||||
)
|
||||
try {
|
||||
$source.CopyTo($gzip)
|
||||
}
|
||||
finally {
|
||||
$gzip.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$destination.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$source.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function New-StoredZip {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][object[]]$Files
|
||||
)
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$stream = [IO.File]::Open(
|
||||
$Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write,
|
||||
[IO.FileShare]::None
|
||||
)
|
||||
try {
|
||||
$zip = New-Object IO.Compression.ZipArchive(
|
||||
$stream, [IO.Compression.ZipArchiveMode]::Create, $false
|
||||
)
|
||||
try {
|
||||
foreach ($file in $Files) {
|
||||
$entry = $zip.CreateEntry(
|
||||
[string]$file.Name,
|
||||
[IO.Compression.CompressionLevel]::NoCompression
|
||||
)
|
||||
$entry.LastWriteTime = [DateTimeOffset]::Parse(
|
||||
'1980-01-01T00:00:00Z'
|
||||
)
|
||||
$input = [IO.File]::OpenRead([string]$file.Path)
|
||||
try {
|
||||
$output = $entry.Open()
|
||||
try {
|
||||
$input.CopyTo($output)
|
||||
}
|
||||
finally {
|
||||
$output.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$input.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$zip.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $DistRoot -PathType Container)) {
|
||||
throw "Distribution directory is missing: $DistRoot"
|
||||
}
|
||||
if (Test-Path -LiteralPath $ReleaseRoot) {
|
||||
throw "Release destination already exists: $ReleaseRoot"
|
||||
}
|
||||
if (-not (Get-Command $Python -ErrorAction SilentlyContinue)) {
|
||||
throw "Python command is unavailable: $Python"
|
||||
}
|
||||
|
||||
$nativeDocker = Get-Command docker -ErrorAction SilentlyContinue
|
||||
if ($null -ne $nativeDocker) {
|
||||
& docker version --format '{{.Server.Version}}' *> $null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$DockerMode = 'native'
|
||||
}
|
||||
}
|
||||
if ($null -eq $DockerMode) {
|
||||
if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
|
||||
throw 'Neither native Docker nor WSL is available'
|
||||
}
|
||||
& wsl.exe -d $WslDistro -- sudo -n docker -H unix:///var/run/docker.sock `
|
||||
version --format '{{.Server.Version}}' *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker is unavailable in WSL distribution $WslDistro"
|
||||
}
|
||||
$DockerMode = 'wsl'
|
||||
}
|
||||
|
||||
Write-Host "Release: $ReleaseRoot"
|
||||
Write-Host "Docker: $DockerMode"
|
||||
|
||||
if (-not $SkipTests) {
|
||||
Invoke-Checked -Command $Python -Arguments @(
|
||||
'-B', '-m', 'pytest', 'tests/test_worker_package.py', '-q'
|
||||
) -Label 'Worker package tests'
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $ReleaseRoot | Out-Null
|
||||
|
||||
$windowsRoot = Join-Path $ReleaseRoot 'truf-worker-windows-x86_64'
|
||||
$windowsZip = Join-Path $ReleaseRoot 'truf-worker-windows-x86_64.zip'
|
||||
$cacheRoot = Join-Path $ProjectRoot 'build\worker-cache'
|
||||
Invoke-Checked -Command $Python -Arguments @(
|
||||
'-B', 'app/worker_package_builder.py', 'windows',
|
||||
'--project-root', $ProjectRoot,
|
||||
'--output', $windowsRoot,
|
||||
'--archive', $windowsZip,
|
||||
'--cache', $cacheRoot
|
||||
) -Label 'Windows worker package build'
|
||||
|
||||
$windowsManifest = Join-Path $windowsRoot 'worker-package.json'
|
||||
$windowsManifestSnapshot = Join-Path $ReleaseRoot (
|
||||
"windows-worker-package-$ReleaseId.json"
|
||||
)
|
||||
Copy-Item -LiteralPath $windowsManifest -Destination $windowsManifestSnapshot
|
||||
|
||||
$buildContext = if ($DockerMode -eq 'wsl') {
|
||||
Get-WslPath -Path $ProjectRoot
|
||||
}
|
||||
else {
|
||||
$ProjectRoot
|
||||
}
|
||||
Invoke-DockerChecked -Arguments @(
|
||||
'build', '--provenance=false', '--target', 'worker',
|
||||
'--tag', $ImageTag, $buildContext
|
||||
) -Label 'Linux worker image build'
|
||||
|
||||
$linuxManifestSnapshot = Join-Path $ReleaseRoot (
|
||||
"linux-worker-package-$ReleaseId.json"
|
||||
)
|
||||
$linuxManifestText = Get-DockerText -Arguments @(
|
||||
'run', '--rm', '--network', 'none', '--entrypoint', '/bin/cat',
|
||||
$ImageTag, '/opt/truf-worker/worker-package.json'
|
||||
) -Label 'Linux worker manifest read'
|
||||
$null = $linuxManifestText | ConvertFrom-Json
|
||||
[IO.File]::WriteAllText(
|
||||
$linuxManifestSnapshot,
|
||||
($linuxManifestText.TrimEnd() + "`n"),
|
||||
(New-Object Text.UTF8Encoding($false))
|
||||
)
|
||||
|
||||
$linuxNativeArchive = Join-Path $ReleaseRoot (
|
||||
'truf-worker-native-linux-x86_64.tar.gz'
|
||||
)
|
||||
$releaseMount = if ($DockerMode -eq 'wsl') {
|
||||
Get-WslPath -Path $ReleaseRoot
|
||||
}
|
||||
else {
|
||||
$ReleaseRoot
|
||||
}
|
||||
Invoke-DockerChecked -Arguments @(
|
||||
'run', '--rm', '--network', 'none', '--user', '0:0',
|
||||
'--entrypoint', '/bin/sh',
|
||||
'--mount', "type=bind,source=$releaseMount,target=/release",
|
||||
$ImageTag, '-c',
|
||||
'set -eu; tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner --transform=s,^truf-worker,truf-worker-linux-x86_64, -C /opt -czf /release/truf-worker-native-linux-x86_64.tar.gz truf-worker'
|
||||
) -Label 'Native Linux worker package export'
|
||||
if (-not (Test-Path -LiteralPath $linuxNativeArchive -PathType Leaf)) {
|
||||
throw "Native Linux worker archive is missing: $linuxNativeArchive"
|
||||
}
|
||||
|
||||
$linuxTar = Join-Path $ReleaseRoot 'truf-worker-linux-x86_64.tar'
|
||||
$linuxArchive = "$linuxTar.gz"
|
||||
if ($DockerMode -eq 'wsl') {
|
||||
$linuxTarDockerPath = Get-WslPath -Path $linuxTar
|
||||
}
|
||||
else {
|
||||
$linuxTarDockerPath = $linuxTar
|
||||
}
|
||||
Invoke-DockerChecked -Arguments @(
|
||||
'image', 'save', '--output', $linuxTarDockerPath, $ImageTag
|
||||
) -Label 'Linux worker image export'
|
||||
if ($DockerMode -eq 'wsl') {
|
||||
$linuxTarWsl = Get-WslPath -Path $linuxTar
|
||||
Invoke-Checked -Command 'wsl.exe' -Arguments @(
|
||||
'-d', $WslDistro, '--', 'gzip', '-1', '-n', '-f', $linuxTarWsl
|
||||
) -Label 'Linux worker image compression'
|
||||
}
|
||||
else {
|
||||
Compress-Gzip -InputPath $linuxTar -OutputPath $linuxArchive
|
||||
Remove-Item -LiteralPath $linuxTar
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $linuxArchive -PathType Leaf)) {
|
||||
throw "Linux worker archive is missing: $linuxArchive"
|
||||
}
|
||||
|
||||
$compose = Join-Path $ReleaseRoot 'compose.yaml'
|
||||
$readme = Join-Path $ReleaseRoot 'README_RU.md'
|
||||
$workerctlShell = Join-Path $ReleaseRoot 'workerctl.sh'
|
||||
$workerctlPowerShell = Join-Path $ReleaseRoot 'workerctl.ps1'
|
||||
Copy-Item -LiteralPath (Join-Path $ProjectRoot 'deploy\worker\compose.yaml') `
|
||||
-Destination $compose
|
||||
$readmeText = [IO.File]::ReadAllText(
|
||||
(Join-Path $ProjectRoot 'docs\remote-worker-quickstart-ru.md')
|
||||
).Replace(
|
||||
'](remote-worker-cheatsheet-', '](cheatsheets/remote-worker-cheatsheet-'
|
||||
)
|
||||
[IO.File]::WriteAllText(
|
||||
$readme, $readmeText, (New-Object Text.UTF8Encoding($false))
|
||||
)
|
||||
Copy-Item -LiteralPath (Join-Path $ProjectRoot 'deploy\worker\workerctl.sh') `
|
||||
-Destination $workerctlShell
|
||||
Copy-Item -LiteralPath (Join-Path $ProjectRoot 'deploy\worker\workerctl.ps1') `
|
||||
-Destination $workerctlPowerShell
|
||||
|
||||
$cheatsheetRoot = Join-Path $ReleaseRoot 'cheatsheets'
|
||||
New-Item -ItemType Directory -Path $cheatsheetRoot | Out-Null
|
||||
$cheatsheetEntries = @(
|
||||
foreach ($name in @(
|
||||
'remote-worker-cheatsheet-windows-ru.md',
|
||||
'remote-worker-cheatsheet-linux-ru.md',
|
||||
'remote-worker-cheatsheet-docker-ru.md'
|
||||
)) {
|
||||
$destination = Join-Path $cheatsheetRoot $name
|
||||
Copy-Item -LiteralPath (Join-Path $ProjectRoot "docs\$name") `
|
||||
-Destination $destination
|
||||
[PSCustomObject]@{
|
||||
Path = $destination
|
||||
Name = "cheatsheets/$name"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$dockerSums = Join-Path $ReleaseRoot 'DOCKER-SHA256SUMS.txt'
|
||||
$dockerFiles = @(
|
||||
[PSCustomObject]@{ Path = $linuxArchive; Name = [IO.Path]::GetFileName($linuxArchive) },
|
||||
[PSCustomObject]@{ Path = $compose; Name = 'compose.yaml' },
|
||||
[PSCustomObject]@{ Path = $readme; Name = 'README_RU.md' },
|
||||
[PSCustomObject]@{ Path = $workerctlShell; Name = 'workerctl.sh' },
|
||||
[PSCustomObject]@{ Path = $workerctlPowerShell; Name = 'workerctl.ps1' }
|
||||
)
|
||||
$dockerFiles += $cheatsheetEntries
|
||||
Write-Sha256Manifest -Path $dockerSums -Files $dockerFiles
|
||||
$dockerBundle = Join-Path $ReleaseRoot 'truf-worker-docker-x86_64-bundle.zip'
|
||||
$dockerBundleFiles = @($dockerFiles)
|
||||
$dockerBundleFiles += [PSCustomObject]@{
|
||||
Path = $dockerSums
|
||||
Name = 'SHA256SUMS.txt'
|
||||
}
|
||||
New-StoredZip -Path $dockerBundle -Files $dockerBundleFiles
|
||||
|
||||
$releaseSums = Join-Path $ReleaseRoot 'SHA256SUMS.txt'
|
||||
$releaseFiles = @(
|
||||
$windowsZip,
|
||||
"$windowsZip.json",
|
||||
$windowsManifestSnapshot,
|
||||
$linuxNativeArchive,
|
||||
$linuxArchive,
|
||||
$linuxManifestSnapshot,
|
||||
$compose,
|
||||
$readme,
|
||||
$workerctlShell,
|
||||
$workerctlPowerShell,
|
||||
$dockerSums,
|
||||
$dockerBundle
|
||||
)
|
||||
$releaseFiles += $cheatsheetEntries
|
||||
Write-Sha256Manifest -Path $releaseSums -Files $releaseFiles
|
||||
|
||||
Write-Host "Complete release created: $ReleaseRoot"
|
||||
Write-Host "Checksums: $releaseSums"
|
||||
Reference in New Issue
Block a user