Skip to content

Commit 2431824

Browse files
gfraiteurclaude
andcommitted
Make DockerBuild.ps1 cross-platform (Windows, Linux, macOS support).
Platform detection: - Add $IsWindows and $IsUnix variables for platform detection - Support Windows, Linux, and macOS hosts Path handling: - BuildAgentPath: defaults to 'C:\BuildAgent' (Windows) or '/build-agent' (Unix) - Container user profile: 'C:\Users\ContainerAdministrator' (Windows) or '/root' (Unix) - NuGet cache: uses $env:USERPROFILE (Windows) or $env:HOME (Unix) - PostSharp.Engineering data: LOCALAPPDATA (Windows) or ~/.local/share (Unix) - PowerShell executable: 'C:\Program Files\PowerShell\7\pwsh.exe' (Windows) or '/usr/bin/pwsh' (Unix) Drive letter mapping: - Windows: Handle non-C: drives by mounting X:\foo to C:\X\foo and using subst - Unix: No drive letter mapping needed (all paths under single root) - Unix: Use case-sensitive path deduplication Docker configuration: - Docker --isolation flag only used on Windows - Use platform-specific path separators: ';' (Windows) or ':' (Unix) Dockerfile mountpoints code: - Windows containers: PowerShell with ';' separator - Unix containers: bash/sh with ':' separator - Auto-generates appropriate code based on host platform Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 56e8911 commit 2431824

File tree

2 files changed

+340
-148
lines changed

2 files changed

+340
-148
lines changed

DockerBuild.ps1

Lines changed: 170 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ param(
1313
[switch]$NoMcp, # Do not start the MCP approval server (for -Claude mode).
1414
[switch]$Update, # Update timestamp to invalidate Docker cache and force Claude/plugin updates (Claude mode only).
1515
[string]$ImageName, # Image name (defaults to a name based on the directory).
16-
[string]$BuildAgentPath = $(if ($env:TEAMCITY_JRE) { Split-Path $env:TEAMCITY_JRE -Parent } else { 'C:\BuildAgent' }),
16+
[string]$BuildAgentPath, # Path to build agent directory (defaults based on platform).
1717
[switch]$LoadEnvFromKeyVault, # Forces loading environment variables form the key vault.
1818
[switch]$StartVsmon, # Enable the remote debugger.
1919
[string]$Script = 'Build.ps1', # The build script to be executed inside Docker.
@@ -35,6 +35,33 @@ $EnvironmentVariables = 'AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AZ_IDENTITY_USE
3535
$ErrorActionPreference = "Stop"
3636
$dockerContextDirectory = "$EngPath/docker-context"
3737

38+
# Detect platform (use built-in variables if available, fallback for older PowerShell)
39+
if ($null -eq $IsWindows)
40+
{
41+
$IsWindows = [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT
42+
}
43+
$IsUnix = -not $IsWindows # Covers both Linux and macOS
44+
45+
# Docker isolation is Windows-only
46+
$isolationArg = if ($IsWindows) { "$isolationArg" } else { "" }
47+
48+
# Set BuildAgentPath default based on platform
49+
if ([string]::IsNullOrEmpty($BuildAgentPath))
50+
{
51+
if ($env:TEAMCITY_JRE)
52+
{
53+
$BuildAgentPath = Split-Path $env:TEAMCITY_JRE -Parent
54+
}
55+
elseif ($IsUnix)
56+
{
57+
$BuildAgentPath = '/build-agent'
58+
}
59+
else
60+
{
61+
$BuildAgentPath = 'C:\BuildAgent'
62+
}
63+
}
64+
3865
Set-Location $PSScriptRoot
3966

4067
if ($env:IS_TEAMCITY_AGENT)
@@ -336,7 +363,14 @@ function Get-TimestampFile
336363
[switch]$Update
337364
)
338365

339-
$timestampDir = Join-Path $env:LOCALAPPDATA "PostSharp.Engineering"
366+
$timestampDir = if ($IsUnix)
367+
{
368+
Join-Path $env:HOME ".local/share/PostSharp.Engineering"
369+
}
370+
else
371+
{
372+
Join-Path $env:LOCALAPPDATA "PostSharp.Engineering"
373+
}
340374
$timestampFile = Join-Path $timestampDir "update.timestamp"
341375

342376
# Ensure directory exists
@@ -497,8 +531,8 @@ if (-not (Test-Path $dockerContextDirectory))
497531
}
498532

499533

500-
# Container user profile (matches actual Windows user in container)
501-
$containerUserProfile = "C:\Users\ContainerAdministrator"
534+
# Container user profile (matches actual user in container)
535+
$containerUserProfile = if ($IsUnix) { "/root" } else { "C:\Users\ContainerAdministrator" }
502536

503537
# Prepare volume mappings (stored as mapping strings, "-v" flags added later)
504538
$VolumeMappings = @("${SourceDirName}:${SourceDirName}")
@@ -521,7 +555,14 @@ if (-not $NoNuGetCache)
521555
$nugetCacheDir = $env:NUGET_PACKAGES
522556
if ( [string]::IsNullOrEmpty($nugetCacheDir))
523557
{
524-
$nugetCacheDir = Join-Path $env:USERPROFILE ".nuget\packages"
558+
if ($IsUnix)
559+
{
560+
$nugetCacheDir = Join-Path $env:HOME ".nuget/packages"
561+
}
562+
else
563+
{
564+
$nugetCacheDir = Join-Path $env:USERPROFILE ".nuget\packages"
565+
}
525566
}
526567

527568
Write-Host "NuGet cache directory: $nugetCacheDir" -ForegroundColor Cyan
@@ -537,13 +578,28 @@ if (-not $NoNuGetCache)
537578
}
538579

539580
# Mount PostSharp.Engineering data directory (for version counters)
540-
$hostEngineeringDataDir = Join-Path $env:LOCALAPPDATA "PostSharp.Engineering"
581+
$hostEngineeringDataDir = if ($IsUnix)
582+
{
583+
Join-Path $env:HOME ".local/share/PostSharp.Engineering"
584+
}
585+
else
586+
{
587+
Join-Path $env:LOCALAPPDATA "PostSharp.Engineering"
588+
}
589+
541590
if (-not (Test-Path $hostEngineeringDataDir))
542591
{
543592
New-Item -ItemType Directory -Force -Path $hostEngineeringDataDir | Out-Null
544593
}
545594

546-
$containerEngineeringDataDir = Join-Path $containerUserProfile "AppData\Local\PostSharp.Engineering"
595+
$containerEngineeringDataDir = if ($IsUnix)
596+
{
597+
Join-Path $containerUserProfile ".local/share/PostSharp.Engineering"
598+
}
599+
else
600+
{
601+
Join-Path $containerUserProfile "AppData\Local\PostSharp.Engineering"
602+
}
547603
$VolumeMappings += "${hostEngineeringDataDir}:${containerEngineeringDataDir}"
548604
$MountPoints += $containerEngineeringDataDir
549605

@@ -650,80 +706,95 @@ elseif (-not $env:IS_TEAMCITY_AGENT)
650706
exit 1
651707
}
652708

653-
# Handle non-C: drive letters for Docker (Windows containers only have C: by default)
654-
# We mount X:\foo to C:\X\foo in the container, then use subst to create the X: drive
655-
$driveLetters = @{ }
709+
# Handle path transformations (platform-specific)
710+
$substCommandsInline = ""
656711

657-
function Get-ContainerPath($hostPath)
712+
if ($IsWindows)
658713
{
659-
if ($hostPath -match '^([A-Za-z]):(.*)$')
714+
# Handle non-C: drive letters for Docker (Windows containers only have C: by default)
715+
# We mount X:\foo to C:\X\foo in the container, then use subst to create the X: drive
716+
$driveLetters = @{ }
717+
718+
function Get-ContainerPath($hostPath)
660719
{
661-
$driveLetter = $Matches[1].ToUpper()
662-
$pathWithoutDrive = $Matches[2]
663-
if ($driveLetter -ne 'C')
720+
if ($hostPath -match '^([A-Za-z]):(.*)$')
664721
{
665-
$driveLetters[$driveLetter] = $true
666-
return "C:\$driveLetter$pathWithoutDrive"
722+
$driveLetter = $Matches[1].ToUpper()
723+
$pathWithoutDrive = $Matches[2]
724+
if ($driveLetter -ne 'C')
725+
{
726+
$driveLetters[$driveLetter] = $true
727+
return "C:\$driveLetter$pathWithoutDrive"
728+
}
667729
}
730+
return $hostPath
668731
}
669-
return $hostPath
670-
}
671732

672-
# Transform all volume mappings to use container paths
673-
$transformedVolumeMappings = @()
674-
foreach ($mapping in $VolumeMappings)
675-
{
676-
# Parse volume mapping: hostPath:containerPath[:options]
677-
if ($mapping -match '^([A-Za-z]:\\[^:]*):([A-Za-z]:\\[^:]*)(:.+)?$')
733+
# Transform all volume mappings to use container paths
734+
$transformedVolumeMappings = @()
735+
foreach ($mapping in $VolumeMappings)
678736
{
679-
$hostPath = $Matches[1]
680-
$containerPath = $Matches[2]
681-
$options = $Matches[3]
682-
$newContainerPath = Get-ContainerPath $containerPath
683-
$transformedVolumeMappings += "${hostPath}:${newContainerPath}${options}"
684-
}
685-
else
686-
{
687-
$transformedVolumeMappings += $mapping
737+
# Parse volume mapping: hostPath:containerPath[:options]
738+
if ($mapping -match '^([A-Za-z]:\\[^:]*):([A-Za-z]:\\[^:]*)(:.+)?$')
739+
{
740+
$hostPath = $Matches[1]
741+
$containerPath = $Matches[2]
742+
$options = $Matches[3]
743+
$newContainerPath = Get-ContainerPath $containerPath
744+
$transformedVolumeMappings += "${hostPath}:${newContainerPath}${options}"
745+
}
746+
else
747+
{
748+
$transformedVolumeMappings += $mapping
749+
}
688750
}
689-
}
690-
$VolumeMappings = $transformedVolumeMappings
751+
$VolumeMappings = $transformedVolumeMappings
691752

692-
# Transform MountPoints, GitDirectories, and SourceDirName for the container
693-
$MountPoints = $MountPoints | ForEach-Object { Get-ContainerPath $_ }
694-
$GitDirectories = $GitDirectories | ForEach-Object { Get-ContainerPath $_ }
695-
$ContainerSourceDir = Get-ContainerPath $SourceDirName
753+
# Transform MountPoints, GitDirectories, and SourceDirName for the container
754+
$MountPoints = $MountPoints | ForEach-Object { Get-ContainerPath $_ }
755+
$GitDirectories = $GitDirectories | ForEach-Object { Get-ContainerPath $_ }
756+
$ContainerSourceDir = Get-ContainerPath $SourceDirName
696757

697-
# Add both the unmapped (C:\X\...) and mapped (X:\...) paths to GitDirectories for safe.directory
698-
# Git may resolve paths differently depending on how it's invoked
699-
$expandedGitDirectories = @()
700-
foreach ($dir in $GitDirectories)
701-
{
702-
$expandedGitDirectories += $dir
703-
# If path is C:\<letter>\... (unmapped subst path), also add <letter>:\... (mapped path)
704-
if ($dir -match '^C:\\([A-Za-z])\\(.*)$')
758+
# Add both the unmapped (C:\X\...) and mapped (X:\...) paths to GitDirectories for safe.directory
759+
# Git may resolve paths differently depending on how it's invoked
760+
$expandedGitDirectories = @()
761+
foreach ($dir in $GitDirectories)
705762
{
706-
$letter = $Matches[1].ToUpper()
707-
$rest = $Matches[2]
708-
$expandedGitDirectories += "${letter}:\$rest"
763+
$expandedGitDirectories += $dir
764+
# If path is C:\<letter>\... (unmapped subst path), also add <letter>:\... (mapped path)
765+
if ($dir -match '^C:\\([A-Za-z])\\(.*)$')
766+
{
767+
$letter = $Matches[1].ToUpper()
768+
$rest = $Matches[2]
769+
$expandedGitDirectories += "${letter}:\$rest"
770+
}
709771
}
710-
}
711-
$GitDirectories = $expandedGitDirectories
772+
$GitDirectories = $expandedGitDirectories
712773

713-
# Deduplicate again after transformations and expansions (case-insensitive for Windows paths)
714-
$VolumeMappings = $VolumeMappings | Group-Object { $_.ToLower() } | ForEach-Object { $_.Group[0] }
715-
$MountPoints = $MountPoints | Group-Object { $_.ToLower() } | ForEach-Object { $_.Group[0] }
716-
$GitDirectories = $GitDirectories | Group-Object { "$_".ToLower() } | ForEach-Object { $_.Group[0] }
774+
# Deduplicate again after transformations and expansions (case-insensitive for Windows paths)
775+
$VolumeMappings = $VolumeMappings | Group-Object { $_.ToLower() } | ForEach-Object { $_.Group[0] }
776+
$MountPoints = $MountPoints | Group-Object { $_.ToLower() } | ForEach-Object { $_.Group[0] }
777+
$GitDirectories = $GitDirectories | Group-Object { "$_".ToLower() } | ForEach-Object { $_.Group[0] }
717778

718-
# Build subst commands string for inline execution in docker run
719-
$substCommandsInline = ""
720-
foreach ($letter in $driveLetters.Keys | Sort-Object)
721-
{
722-
$substCommandsInline += "C:\Windows\System32\subst.exe ${letter}: C:\$letter; "
779+
# Build subst commands string for inline execution in docker run
780+
foreach ($letter in $driveLetters.Keys | Sort-Object)
781+
{
782+
$substCommandsInline += "C:\Windows\System32\subst.exe ${letter}: C:\$letter; "
783+
}
784+
if ($driveLetters.Keys.Count -gt 0)
785+
{
786+
Write-Host "Drive letter mappings for container: $( $driveLetters.Keys -join ', ' )" -ForegroundColor Cyan
787+
}
723788
}
724-
if ($driveLetters.Count -gt 0)
789+
else
725790
{
726-
Write-Host "Drive letter mappings for container: $( $driveLetters.Keys -join ', ' )" -ForegroundColor Cyan
791+
# Unix (Linux/macOS): No drive letter mapping needed, paths remain as-is
792+
$ContainerSourceDir = $SourceDirName
793+
794+
# Deduplicate (case-sensitive for Unix paths)
795+
$VolumeMappings = $VolumeMappings | Sort-Object -Unique
796+
$MountPoints = $MountPoints | Sort-Object -Unique
797+
$GitDirectories = $GitDirectories | Sort-Object -Unique
727798
}
728799

729800
# Create Init.g.ps1 with git configuration (safe.directory and user identity)
@@ -758,6 +829,7 @@ $( ($GitDirectories | ForEach-Object { " '$_'" }) -join ",`n" )
758829
759830
foreach (`$dir in `$gitDirectories) {
760831
if (`$dir) {
832+
# Normalize path: convert backslashes to forward slashes, add trailing slash
761833
`$normalizedDir = (`$dir -replace '\\\\', '/').TrimEnd('/') + '/'
762834
git config --global --add safe.directory `$normalizedDir
763835
}
@@ -780,8 +852,10 @@ if ($Claude -and $timestampFile)
780852
Write-Host "Copied timestamp file to docker context" -ForegroundColor Cyan
781853
}
782854

783-
$mountPointsAsString = $MountPoints -Join ";"
784-
$gitDirectoriesAsString = $GitDirectories -Join ";"
855+
# Path separator depends on platform (and container OS)
856+
$pathSeparator = if ($IsUnix) { ":" } else { ";" }
857+
$mountPointsAsString = $MountPoints -Join $pathSeparator
858+
$gitDirectoriesAsString = $GitDirectories -Join $pathSeparator
785859

786860
Write-Host "Volume mappings: " @VolumeMappings -ForegroundColor Gray
787861
Write-Host "Mount points: " $mountPointsAsString -ForegroundColor Gray
@@ -829,8 +903,11 @@ if (-not $NoBuildImage -and -not $existingContainerId)
829903
{
830904
Write-Host "Dockerfile does not have mountpoints creation code. Appending mountpoints setup." -ForegroundColor Yellow
831905

832-
# Append hardcoded mountpoints creation code
833-
$mountpointsCode = @"
906+
# Append hardcoded mountpoints creation code (platform-specific)
907+
if ($IsWindows)
908+
{
909+
# Windows container (PowerShell)
910+
$mountpointsCode = @"
834911
835912
# Create directories for mountpoints
836913
ARG MOUNTPOINTS
@@ -844,6 +921,25 @@ RUN if (`$env:MOUNTPOINTS) { ``
844921
} ``
845922
}
846923
"@
924+
}
925+
else
926+
{
927+
# Unix container (bash/sh)
928+
$mountpointsCode = @"
929+
930+
# Create directories for mountpoints
931+
ARG MOUNTPOINTS
932+
RUN if [ -n "`$MOUNTPOINTS" ]; then \
933+
IFS=':' read -ra mounts <<< "`$MOUNTPOINTS"; \
934+
for dir in "`${mounts[@]}"; do \
935+
if [ -n "`$dir" ]; then \
936+
echo "Creating directory `$dir."; \
937+
mkdir -p "`$dir"; \
938+
fi; \
939+
done; \
940+
fi
941+
"@
942+
}
847943
$dockerfileContent += $mountpointsCode
848944
Write-Host "Appended mountpoints creation code" -ForegroundColor Cyan
849945
}
@@ -1057,7 +1153,7 @@ if (-not $BuildImage)
10571153
}
10581154

10591155
$dockerArgsAsString = $dockerArgs -join " "
1060-
$pwshPath = 'C:\Program Files\PowerShell\7\pwsh.exe'
1156+
$pwshPath = if ($IsUnix) { '/usr/bin/pwsh' } else { 'C:\Program Files\PowerShell\7\pwsh.exe' }
10611157

10621158
# Environment variables to pass to container
10631159
$envArgs = @()
@@ -1072,8 +1168,8 @@ if (-not $BuildImage)
10721168
{
10731169
# Start new container with docker run
10741170
$envArgsAsString = ($envArgs -join " ")
1075-
Write-Host "Executing: docker run --rm --memory=$Memory --cpus=$Cpus --isolation=$Isolation $dockerArgsAsString $VolumeMappingsAsString $envArgsAsString -w $ContainerSourceDir $ImageTag `"$pwshPath`" -Command `"$inlineScript`"" -ForegroundColor Cyan
1076-
docker run --rm --memory=$Memory --cpus=$Cpus --isolation=$Isolation $dockerArgs @volumeArgs @envArgs -w $ContainerSourceDir $ImageTag $pwshPath -Command $inlineScript
1171+
Write-Host "Executing: docker run --rm --memory=$Memory --cpus=$Cpus $isolationArg $dockerArgsAsString $VolumeMappingsAsString $envArgsAsString -w $ContainerSourceDir $ImageTag `"$pwshPath`" -Command `"$inlineScript`"" -ForegroundColor Cyan
1172+
docker run --rm --memory=$Memory --cpus=$Cpus $isolationArg $dockerArgs @volumeArgs @envArgs -w $ContainerSourceDir $ImageTag $pwshPath -Command $inlineScript
10771173
$dockerExitCode = $LASTEXITCODE
10781174
}
10791175
finally
@@ -1195,7 +1291,7 @@ if (-not $BuildImage)
11951291
$scriptInvocation = "& `"$scriptFullPath`""
11961292
$inlineScript = "${substCommandsInline}${initCall}cd '$SourceDirName'; $scriptInvocation $buildArgsString; $pwshExitCommand"
11971293

1198-
$pwshPath = 'C:\Program Files\PowerShell\7\pwsh.exe'
1294+
$pwshPath = if ($IsUnix) { '/usr/bin/pwsh' } else { 'C:\Program Files\PowerShell\7\pwsh.exe' }
11991295

12001296
# Build docker command arguments
12011297
if ($existingContainerId)
@@ -1208,8 +1304,8 @@ if (-not $BuildImage)
12081304
else
12091305
{
12101306
# Start new container with docker run
1211-
Write-Host "Executing: ``docker run --rm --memory=$Memory --cpus=$Cpus --isolation=$Isolation $dockerArgsAsString $VolumeMappingsAsString -w $ContainerSourceDir $ImageTag `"$pwshPath`" $pwshArgs -Command `"$inlineScript`"" -ForegroundColor Cyan
1212-
docker run --rm --memory=$Memory --cpus=$Cpus --isolation=$Isolation $dockerArgs @volumeArgs -w $ContainerSourceDir $ImageTag $pwshPath $pwshArgs -Command $inlineScript
1307+
Write-Host "Executing: ``docker run --rm --memory=$Memory --cpus=$Cpus $isolationArg $dockerArgsAsString $VolumeMappingsAsString -w $ContainerSourceDir $ImageTag `"$pwshPath`" $pwshArgs -Command `"$inlineScript`"" -ForegroundColor Cyan
1308+
docker run --rm --memory=$Memory --cpus=$Cpus $isolationArg $dockerArgs @volumeArgs -w $ContainerSourceDir $ImageTag $pwshPath $pwshArgs -Command $inlineScript
12131309
}
12141310

12151311
if ($LASTEXITCODE -ne 0)

0 commit comments

Comments
 (0)