Skip to content

Commit 09b0709

Browse files
committed
Deduplicate Docker volume mounts with writable-wins logic
Add Add-VolumeMount function that uses a dictionary to track volume mounts and applies "writable wins" logic when the same path is mounted multiple times. Also reorganize CLAUDE.md for clarity.
1 parent 8849f0d commit 09b0709

File tree

3 files changed

+111
-50
lines changed

3 files changed

+111
-50
lines changed

CLAUDE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ Build orchestration SDK for PostSharp/Metalama repositories.
77
88
## Discovering Plugin Skills
99

10-
The `postsharp-engineering` plugin provides skills and slash commands. To discover them:
10+
- The `postsharp-engineering` plugin provides skills and slash commands. To discover them:
1111

12-
Before any work in this repo, read these skills:
13-
- `$env:USERPROFILE\.claude\plugins\cache\postsharp-engineering\**\skills\*.md` - Engineering workflows (git, builds, CI/CD)
14-
- Never update DockerBuild.ps1, Dockerfile. Dockerfile.claude, eng/RunClaude.ps1. These files are generated by `Build.ps1`. Their source code is in the Resources directory.
1512
- *ALWAYS* Read which plug-ins and skills are available to you before doing ANY work, and confirm to the users that you have read these skills before any session.
1613

14+
15+
## Generated scripts
16+
- Never update DockerBuild.ps1, Dockerfile. Dockerfile.claude, eng/RunClaude.ps1. These files are generated by `Build.ps1`. Their source code is in the Resources directory.
17+
1718
## MCP Approval Server (Docker Environment)
1819

1920
When running inside a Docker container, you have access to the `host-approval` MCP server for executing privileged commands on the host machine. These commands require human approval before execution.

DockerBuild.ps1

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,33 @@ function Get-TimestampFile
319319
return $timestampFile
320320
}
321321

322+
# Dictionary to track volume mounts with "writable wins" logic
323+
$script:VolumeMountDict = @{}
324+
325+
function Add-VolumeMount {
326+
param(
327+
[Parameter(Mandatory=$true)]
328+
[string]$Path,
329+
[switch]$Writable
330+
)
331+
332+
$normalizedPath = $Path.TrimEnd('\', '/')
333+
$normalizedKey = $normalizedPath.ToLower()
334+
$isGitDirectory = Test-Path (Join-Path $normalizedPath ".git")
335+
336+
if ($script:VolumeMountDict.ContainsKey($normalizedKey)) {
337+
if ($Writable) {
338+
$script:VolumeMountDict[$normalizedKey].Writable = $true
339+
}
340+
} else {
341+
$script:VolumeMountDict[$normalizedKey] = @{
342+
HostPath = $normalizedPath
343+
Writable = [bool]$Writable
344+
IsGitDirectory = $isGitDirectory
345+
}
346+
}
347+
}
348+
322349
if ($env:RUNNING_IN_DOCKER)
323350
{
324351
Write-Error "Already running in Docker."
@@ -452,18 +479,20 @@ if (-not (Test-Path $dockerContextDirectory))
452479
# Container user profile (matches actual user in container)
453480
$containerUserProfile = if ($IsUnix) { "/root" } else { "C:\Users\ContainerAdministrator" }
454481

455-
# Prepare volume mappings (stored as mapping strings, "-v" flags added later)
456-
$VolumeMappings = @("${SourceDirName}:${SourceDirName}")
457-
$MountPoints = @($SourceDirName)
458-
$GitDirectories = @($SourceDirName)
482+
# Initialize arrays for special mounts (those with different host/container paths)
483+
$VolumeMappings = @()
484+
$MountPoints = @()
485+
$GitDirectories = @()
486+
487+
# Prepare volume mappings using the dictionary
488+
Add-VolumeMount -Path $SourceDirName -Writable
459489

460490
# Define static Git system directory for mapping. This used by Teamcity as an LFS parent repo.
461491
$gitSystemDir = "$BuildAgentPath\system\git"
462492

463493
if (Test-Path $gitSystemDir)
464494
{
465-
$VolumeMappings += "${gitSystemDir}:${gitSystemDir}:ro"
466-
$MountPoints += $gitSystemDir
495+
Add-VolumeMount -Path $gitSystemDir
467496
}
468497

469498
# Mount the host NuGet cache in the container.
@@ -491,8 +520,7 @@ if (-not $NoNuGetCache)
491520
}
492521

493522
# Mount to the same path in the container (will be transformed by Get-ContainerPath later)
494-
$VolumeMappings += "${nugetCacheDir}:${nugetCacheDir}"
495-
$MountPoints += $nugetCacheDir
523+
Add-VolumeMount -Path $nugetCacheDir -Writable
496524
}
497525

498526
# Mount PostSharp.Engineering data directory (for version counters)
@@ -555,9 +583,7 @@ if (Test-Path $sourceDependenciesDir)
555583
if (-not [string]::IsNullOrEmpty($targetPath) -and (Test-Path $targetPath))
556584
{
557585
Write-Host "Found symbolic link '$( $link.Name )' -> '$targetPath'" -ForegroundColor Cyan
558-
$VolumeMappings += "${targetPath}:${targetPath}:ro"
559-
$MountPoints += $targetPath
560-
$GitDirectories += $targetPath
586+
Add-VolumeMount -Path $targetPath
561587
}
562588
else
563589
{
@@ -587,9 +613,7 @@ if ($parentDir -and (Test-Path $parentDir) -and ($parentDirName -like "PostSharp
587613
{
588614
$siblingPath = $sibling.FullName
589615
Write-Host "Mounting product family sibling: $siblingPath" -ForegroundColor Cyan
590-
$VolumeMappings += "${siblingPath}:${siblingPath}:ro"
591-
$MountPoints += $siblingPath
592-
$GitDirectories += $siblingPath
616+
Add-VolumeMount -Path $siblingPath
593617
}
594618
}
595619

@@ -605,9 +629,7 @@ if ($grandparentDir -and (Test-Path $grandparentDir))
605629
{
606630
$engDirPath = $engDir.FullName
607631
Write-Host "Mounting engineering repo: $engDirPath" -ForegroundColor Cyan
608-
$VolumeMappings += "${engDirPath}:${engDirPath}:ro"
609-
$MountPoints += $engDirPath
610-
$GitDirectories += $engDirPath
632+
Add-VolumeMount -Path $engDirPath
611633
}
612634
}
613635

@@ -628,8 +650,6 @@ if ($Mount -and $Mount.Count -gt 0)
628650
# Trim trailing slashes
629651
$pattern = $pattern.TrimEnd('\', '/')
630652

631-
$mountOption = if ($isWritable) { "" } else { ":ro" }
632-
633653
# Check if pattern contains glob characters
634654
if ($pattern -match '\*')
635655
{
@@ -694,8 +714,7 @@ if ($Mount -and $Mount.Count -gt 0)
694714
$dirPath = $dir.FullName
695715
$rwStatus = if ($isWritable) { "writable" } else { "readonly" }
696716
Write-Host "Mounting from -Mount pattern '$pattern': $dirPath ($rwStatus)" -ForegroundColor Cyan
697-
$VolumeMappings += "${dirPath}:${dirPath}${mountOption}"
698-
$MountPoints += $dirPath
717+
Add-VolumeMount -Path $dirPath -Writable:$isWritable
699718
}
700719
}
701720
}
@@ -711,8 +730,7 @@ if ($Mount -and $Mount.Count -gt 0)
711730
{
712731
$rwStatus = if ($isWritable) { "writable" } else { "readonly" }
713732
Write-Host "Mounting from -Mount: $pattern ($rwStatus)" -ForegroundColor Cyan
714-
$VolumeMappings += "${pattern}:${pattern}${mountOption}"
715-
$MountPoints += $pattern
733+
Add-VolumeMount -Path $pattern -Writable:$isWritable
716734
}
717735
else
718736
{
@@ -722,6 +740,18 @@ if ($Mount -and $Mount.Count -gt 0)
722740
}
723741
}
724742

743+
# Convert dictionary entries to arrays (with "writable wins" deduplication already applied)
744+
# Sort by key for deterministic ordering to optimize Docker image layer reuse
745+
foreach ($key in $script:VolumeMountDict.Keys | Sort-Object) {
746+
$entry = $script:VolumeMountDict[$key]
747+
$mountOption = if ($entry.Writable) { "" } else { ":ro" }
748+
$VolumeMappings += "$($entry.HostPath):$($entry.HostPath)$mountOption"
749+
$MountPoints += $entry.HostPath
750+
if ($entry.IsGitDirectory) {
751+
$GitDirectories += $entry.HostPath
752+
}
753+
}
754+
725755
# Execute auto-generated DockerMounts.g.ps1 script to add more directory mounts.
726756
$dockerMountsScript = Join-Path $EngPath 'DockerMounts.g.ps1'
727757
if (Test-Path $dockerMountsScript)

src/PostSharp.Engineering.BuildTools/Resources/DockerBuild.ps1

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,33 @@ function Get-TimestampFile
319319
return $timestampFile
320320
}
321321

322+
# Dictionary to track volume mounts with "writable wins" logic
323+
$script:VolumeMountDict = @{}
324+
325+
function Add-VolumeMount {
326+
param(
327+
[Parameter(Mandatory=$true)]
328+
[string]$Path,
329+
[switch]$Writable
330+
)
331+
332+
$normalizedPath = $Path.TrimEnd('\', '/')
333+
$normalizedKey = $normalizedPath.ToLower()
334+
$isGitDirectory = Test-Path (Join-Path $normalizedPath ".git")
335+
336+
if ($script:VolumeMountDict.ContainsKey($normalizedKey)) {
337+
if ($Writable) {
338+
$script:VolumeMountDict[$normalizedKey].Writable = $true
339+
}
340+
} else {
341+
$script:VolumeMountDict[$normalizedKey] = @{
342+
HostPath = $normalizedPath
343+
Writable = [bool]$Writable
344+
IsGitDirectory = $isGitDirectory
345+
}
346+
}
347+
}
348+
322349
if ($env:RUNNING_IN_DOCKER)
323350
{
324351
Write-Error "Already running in Docker."
@@ -452,18 +479,20 @@ if (-not (Test-Path $dockerContextDirectory))
452479
# Container user profile (matches actual user in container)
453480
$containerUserProfile = if ($IsUnix) { "/root" } else { "C:\Users\ContainerAdministrator" }
454481

455-
# Prepare volume mappings (stored as mapping strings, "-v" flags added later)
456-
$VolumeMappings = @("${SourceDirName}:${SourceDirName}")
457-
$MountPoints = @($SourceDirName)
458-
$GitDirectories = @($SourceDirName)
482+
# Initialize arrays for special mounts (those with different host/container paths)
483+
$VolumeMappings = @()
484+
$MountPoints = @()
485+
$GitDirectories = @()
486+
487+
# Prepare volume mappings using the dictionary
488+
Add-VolumeMount -Path $SourceDirName -Writable
459489

460490
# Define static Git system directory for mapping. This used by Teamcity as an LFS parent repo.
461491
$gitSystemDir = "$BuildAgentPath\system\git"
462492

463493
if (Test-Path $gitSystemDir)
464494
{
465-
$VolumeMappings += "${gitSystemDir}:${gitSystemDir}:ro"
466-
$MountPoints += $gitSystemDir
495+
Add-VolumeMount -Path $gitSystemDir
467496
}
468497

469498
# Mount the host NuGet cache in the container.
@@ -491,8 +520,7 @@ if (-not $NoNuGetCache)
491520
}
492521

493522
# Mount to the same path in the container (will be transformed by Get-ContainerPath later)
494-
$VolumeMappings += "${nugetCacheDir}:${nugetCacheDir}"
495-
$MountPoints += $nugetCacheDir
523+
Add-VolumeMount -Path $nugetCacheDir -Writable
496524
}
497525

498526
# Mount PostSharp.Engineering data directory (for version counters)
@@ -555,9 +583,7 @@ if (Test-Path $sourceDependenciesDir)
555583
if (-not [string]::IsNullOrEmpty($targetPath) -and (Test-Path $targetPath))
556584
{
557585
Write-Host "Found symbolic link '$( $link.Name )' -> '$targetPath'" -ForegroundColor Cyan
558-
$VolumeMappings += "${targetPath}:${targetPath}:ro"
559-
$MountPoints += $targetPath
560-
$GitDirectories += $targetPath
586+
Add-VolumeMount -Path $targetPath
561587
}
562588
else
563589
{
@@ -587,9 +613,7 @@ if ($parentDir -and (Test-Path $parentDir) -and ($parentDirName -like "PostSharp
587613
{
588614
$siblingPath = $sibling.FullName
589615
Write-Host "Mounting product family sibling: $siblingPath" -ForegroundColor Cyan
590-
$VolumeMappings += "${siblingPath}:${siblingPath}:ro"
591-
$MountPoints += $siblingPath
592-
$GitDirectories += $siblingPath
616+
Add-VolumeMount -Path $siblingPath
593617
}
594618
}
595619

@@ -605,9 +629,7 @@ if ($grandparentDir -and (Test-Path $grandparentDir))
605629
{
606630
$engDirPath = $engDir.FullName
607631
Write-Host "Mounting engineering repo: $engDirPath" -ForegroundColor Cyan
608-
$VolumeMappings += "${engDirPath}:${engDirPath}:ro"
609-
$MountPoints += $engDirPath
610-
$GitDirectories += $engDirPath
632+
Add-VolumeMount -Path $engDirPath
611633
}
612634
}
613635

@@ -628,8 +650,6 @@ if ($Mount -and $Mount.Count -gt 0)
628650
# Trim trailing slashes
629651
$pattern = $pattern.TrimEnd('\', '/')
630652

631-
$mountOption = if ($isWritable) { "" } else { ":ro" }
632-
633653
# Check if pattern contains glob characters
634654
if ($pattern -match '\*')
635655
{
@@ -694,8 +714,7 @@ if ($Mount -and $Mount.Count -gt 0)
694714
$dirPath = $dir.FullName
695715
$rwStatus = if ($isWritable) { "writable" } else { "readonly" }
696716
Write-Host "Mounting from -Mount pattern '$pattern': $dirPath ($rwStatus)" -ForegroundColor Cyan
697-
$VolumeMappings += "${dirPath}:${dirPath}${mountOption}"
698-
$MountPoints += $dirPath
717+
Add-VolumeMount -Path $dirPath -Writable:$isWritable
699718
}
700719
}
701720
}
@@ -711,8 +730,7 @@ if ($Mount -and $Mount.Count -gt 0)
711730
{
712731
$rwStatus = if ($isWritable) { "writable" } else { "readonly" }
713732
Write-Host "Mounting from -Mount: $pattern ($rwStatus)" -ForegroundColor Cyan
714-
$VolumeMappings += "${pattern}:${pattern}${mountOption}"
715-
$MountPoints += $pattern
733+
Add-VolumeMount -Path $pattern -Writable:$isWritable
716734
}
717735
else
718736
{
@@ -722,6 +740,18 @@ if ($Mount -and $Mount.Count -gt 0)
722740
}
723741
}
724742

743+
# Convert dictionary entries to arrays (with "writable wins" deduplication already applied)
744+
# Sort by key for deterministic ordering to optimize Docker image layer reuse
745+
foreach ($key in $script:VolumeMountDict.Keys | Sort-Object) {
746+
$entry = $script:VolumeMountDict[$key]
747+
$mountOption = if ($entry.Writable) { "" } else { ":ro" }
748+
$VolumeMappings += "$($entry.HostPath):$($entry.HostPath)$mountOption"
749+
$MountPoints += $entry.HostPath
750+
if ($entry.IsGitDirectory) {
751+
$GitDirectories += $entry.HostPath
752+
}
753+
}
754+
725755
# Execute auto-generated DockerMounts.g.ps1 script to add more directory mounts.
726756
$dockerMountsScript = Join-Path $EngPath 'DockerMounts.g.ps1'
727757
if (Test-Path $dockerMountsScript)

0 commit comments

Comments
 (0)