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+
3865Set-Location $PSScriptRoot
3966
4067if ($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+
541590if (-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
759830foreach (`$ 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
786860Write-Host " Volume mappings: " @VolumeMappings - ForegroundColor Gray
787861Write-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
836913ARG 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