99 [switch ]$NoClean , # Does not clean up.
1010 [switch ]$NoNuGetCache , # Does not mount the host nuget cache in the container.
1111 [switch ]$KeepEnv , # Does not override the env.g.json file.
12- [switch ]$Claude , # Run Claude CLI instead of Build.ps1.
13- [string ]$ClaudePrompt , # Optional prompt for Claude (non-interactive mode).
12+ [switch ]$Claude , # Run Claude CLI instead of Build.ps1. Use -Claude for interactive, -Claude "prompt" for non-interactive.
1413 [string ]$ImageName , # Image name (defaults to a name based on the directory).
1514 [string ]$BuildAgentPath = ' C:\BuildAgent' ,
1615 [switch ]$LoadEnvFromKeyVault , # Forces loading environment variables form the key vault.
1716 [switch ]$StartVsmon , # Enable the remote debugger.
1817 [string ]$Script = ' Build.ps1' , # The build script to be executed inside Docker.
1918 [Parameter (ValueFromRemainingArguments )]
20- [string []]$BuildArgs # Arguments passed to `Build.ps1` within the container.
19+ [string []]$BuildArgs # Arguments passed to `Build.ps1` within the container (or Claude prompt if -Claude is specified) .
2120)
2221
2322# ###
@@ -235,9 +234,9 @@ if (-not (Test-Path $dockerContextDirectory))
235234
236235
237236# Prepare volume mappings
238- $VolumeMappings = @ (" -v" , " ${SourceDirName} :${SourceDirName} " )
239- $MountPoints = @ ($SourceDirName , " c:\packages" )
240- $GitDirectories = @ ($SourceDirName )
237+ $VolumeMappings = @ (" -v" , " ${SourceDirName} :${SourceDirName} " )
238+ $MountPoints = @ ($SourceDirName , " c:\packages" )
239+ $GitDirectories = @ ($SourceDirName )
241240
242241# Define static Git system directory for mapping. This used by Teamcity as an LFS parent repo.
243242$gitSystemDir = " $BuildAgentPath \system\git"
@@ -321,6 +320,68 @@ if (Test-Path $dockerMountsScript)
321320 . $dockerMountsScript
322321}
323322
323+ # Handle non-C: drive letters for Docker (Windows containers only have C: by default)
324+ # We mount X:\foo to C:\X\foo in the container, then use subst to create the X: drive
325+ $driveLetters = @ {}
326+
327+ function Get-ContainerPath ($hostPath )
328+ {
329+ if ($hostPath -match ' ^([A-Za-z]):(.*)$' )
330+ {
331+ $driveLetter = $Matches [1 ].ToUpper()
332+ $pathWithoutDrive = $Matches [2 ]
333+ if ($driveLetter -ne ' C' )
334+ {
335+ $driveLetters [$driveLetter ] = $true
336+ return " C:\$driveLetter$pathWithoutDrive "
337+ }
338+ }
339+ return $hostPath
340+ }
341+
342+ # Transform all volume mappings to use container paths
343+ $transformedVolumeMappings = @ ()
344+ for ($i = 0 ; $i -lt $VolumeMappings.Count ; $i += 2 )
345+ {
346+ $flag = $VolumeMappings [$i ]
347+ $mapping = $VolumeMappings [$i + 1 ]
348+
349+ # Parse volume mapping: hostPath:containerPath[:options]
350+ if ($mapping -match ' ^([A-Za-z]:\\[^:]*):([A-Za-z]:\\[^:]*)(:.+)?$' )
351+ {
352+ $hostPath = $Matches [1 ]
353+ $containerPath = $Matches [2 ]
354+ $options = $Matches [3 ]
355+ $newContainerPath = Get-ContainerPath $containerPath
356+ $transformedVolumeMappings += @ ($flag , " ${hostPath} :${newContainerPath}${options} " )
357+ }
358+ else
359+ {
360+ $transformedVolumeMappings += @ ($flag , $mapping )
361+ }
362+ }
363+ $VolumeMappings = $transformedVolumeMappings
364+
365+ # Transform MountPoints, GitDirectories, and SourceDirName for the container
366+ $MountPoints = $MountPoints | ForEach-Object { Get-ContainerPath $_ }
367+ $GitDirectories = $GitDirectories | ForEach-Object { Get-ContainerPath $_ }
368+ $ContainerSourceDir = Get-ContainerPath $SourceDirName
369+
370+ # Build subst commands string for inline execution in docker run
371+ $substCommandsInline = " "
372+ foreach ($letter in $driveLetters.Keys | Sort-Object )
373+ {
374+ $substCommandsInline += " C:\Windows\System32\subst.exe ${letter} : C:\$letter ; "
375+ }
376+ if ($driveLetters.Count -gt 0 )
377+ {
378+ Write-Host " Drive letter mappings for container: $ ( $driveLetters.Keys -join ' , ' ) " - ForegroundColor Cyan
379+ }
380+
381+ # Create empty Init.g.ps1 for Dockerfile COPY (required by Dockerfile but not used for drive mapping)
382+ $initScript = Join-Path $dockerContextDirectory " Init.g.ps1"
383+ " # Drive mappings are handled inline in docker run command" | Set-Content - Path $initScript - Encoding UTF8
384+
324385$mountPointsAsString = $MountPoints -Join " ;"
325386$gitDirectoriesAsString = $GitDirectories -Join " ;"
326387
@@ -337,35 +398,35 @@ docker ps -q --filter "ancestor=$ImageTag" | ForEach-Object {
337398# Building the image.
338399if (-not $NoBuildImage )
339400{
340- Write-Host " Building the base image with tag: $ImageTag " - ForegroundColor Green
341- Get-Content - Raw Dockerfile | docker build - t $ImageTag -- build-arg GITDIRS= " $gitDirectoriesAsString " -- build-arg MOUNTPOINTS= " $mountPointsAsString " -f - $dockerContextDirectory
342- if ($LASTEXITCODE -ne 0 )
343- {
344- Write-Host " Docker build failed with exit code $LASTEXITCODE " - ForegroundColor Red
345- exit $LASTEXITCODE
346- }
347-
348- # Build Claude image if requested
349401 if ($Claude )
350402 {
351- $ClaudeImageTag = " $ImageTag -claude"
352- Write-Host " Building the Claude image with tag: $ClaudeImageTag " - ForegroundColor Green
403+ # Build Claude image directly from standalone Dockerfile.claude
404+ $ImageTag = " $ImageTag -claude"
405+ Write-Host " Building the Claude image with tag: $ImageTag " - ForegroundColor Green
353406
354407 if (-not (Test-Path " Dockerfile.claude" ))
355408 {
356- Write-Error " Dockerfile.claude not found. Make sure generate-scripts was run with a NodeJs component ."
409+ Write-Error " Dockerfile.claude not found. Make sure generate-scripts was run with Claude support ."
357410 exit 1
358411 }
359412
360- Get-Content - Raw Dockerfile.claude | docker build - t $ClaudeImageTag -- build-arg BASE_IMAGE = " $ImageTag " -f - $dockerContextDirectory
413+ Get-Content - Raw Dockerfile.claude | docker build - t $ImageTag -- build-arg GITDIRS = " $gitDirectoriesAsString " -- build-arg MOUNTPOINTS = " $mountPointsAsString " -f - $dockerContextDirectory
361414 if ($LASTEXITCODE -ne 0 )
362415 {
363416 Write-Host " Docker build (Claude) failed with exit code $LASTEXITCODE " - ForegroundColor Red
364417 exit $LASTEXITCODE
365418 }
366-
367- # Use Claude image for the run
368- $ImageTag = $ClaudeImageTag
419+ }
420+ else
421+ {
422+ # Build base image
423+ Write-Host " Building the base image with tag: $ImageTag " - ForegroundColor Green
424+ Get-Content - Raw Dockerfile | docker build - t $ImageTag -- build-arg GITDIRS= " $gitDirectoriesAsString " -- build-arg MOUNTPOINTS= " $mountPointsAsString " -f - $dockerContextDirectory
425+ if ($LASTEXITCODE -ne 0 )
426+ {
427+ Write-Host " Docker build failed with exit code $LASTEXITCODE " - ForegroundColor Red
428+ exit $LASTEXITCODE
429+ }
369430 }
370431}
371432else
@@ -383,7 +444,87 @@ else
383444# Run the build within the container
384445if (-not $BuildImage )
385446{
447+ if ($Claude )
448+ {
449+ # Run Claude mode
450+ Write-Host " Running Claude in the container." - ForegroundColor Green
386451
452+ # Add Claude-specific volume mounts for auth and settings
453+ $hostUserProfile = $env: USERPROFILE
454+ $containerUserProfile = " C:\Users\ContainerUser"
455+
456+ # Mount .claude directory (settings and credentials)
457+ if (Test-Path " $hostUserProfile \.claude" )
458+ {
459+ $VolumeMappings += @ (" -v" , " ${hostUserProfile} \.claude:${containerUserProfile} \.claude" )
460+ }
461+
462+ # Copy .claude.json to docker-context (cannot mount files on Windows Docker)
463+ # Also fix installMethod to match container's npm installation
464+ $claudeJsonSource = " $hostUserProfile \.claude.json"
465+ $claudeJsonDest = Join-Path $dockerContextDirectory " claude.json"
466+ $copyClaudeJsonScript = " "
467+ if (Test-Path $claudeJsonSource )
468+ {
469+ $claudeConfig = Get-Content $claudeJsonSource - Raw | ConvertFrom-Json
470+ # Change installMethod to npm since that's how Claude is installed in container
471+ if ($claudeConfig.installMethod )
472+ {
473+ $claudeConfig.installMethod = " npm"
474+ }
475+ $claudeConfig | ConvertTo-Json - Depth 10 | Set-Content $claudeJsonDest - Encoding UTF8
476+ # Will copy from mounted source dir to user profile in container
477+ $copyClaudeJsonScript = " Copy-Item '$ContainerSourceDir \eng\docker-context\claude.json' '$containerUserProfile \.claude.json' -Force; "
478+ }
479+
480+ # Mount .cache\claude (cache)
481+ if (Test-Path " $hostUserProfile \.cache\claude" )
482+ {
483+ $VolumeMappings += @ (" -v" , " ${hostUserProfile} \.cache\claude:${containerUserProfile} \.cache\claude" )
484+ }
485+
486+ $VolumeMappingsAsString = $VolumeMappings -join " "
487+
488+ # Extract Claude prompt from remaining arguments if present
489+ # Usage: -Claude for interactive, -Claude "prompt" for non-interactive
490+ $ClaudePrompt = $null
491+ if ($BuildArgs -and $BuildArgs.Count -gt 0 -and $BuildArgs [0 ] -and -not $BuildArgs [0 ].StartsWith(' -' ))
492+ {
493+ $ClaudePrompt = $BuildArgs [0 ]
494+ }
495+
496+ # Build inline script: subst drives, copy claude.json, cd to source, run Claude
497+ if ($ClaudePrompt )
498+ {
499+ # Non-interactive mode with prompt - no -it flags
500+ $dockerArgs = @ ()
501+ $inlineScript = " ${substCommandsInline}${copyClaudeJsonScript} cd '$SourceDirName '; & .\eng\RunClaude.g.ps1 -Prompt `" $ClaudePrompt `" "
502+ }
503+ else
504+ {
505+ # Interactive mode - requires TTY
506+ $dockerArgs = @ (" -it" )
507+ $inlineScript = " ${substCommandsInline}${copyClaudeJsonScript} cd '$SourceDirName '; & .\eng\RunClaude.g.ps1"
508+ }
509+
510+ $dockerArgsAsString = $dockerArgs -join " "
511+ $pwshPath = ' C:\Program Files\PowerShell\7\pwsh.exe'
512+
513+ # Set HOME/USERPROFILE so Claude finds its config in the mounted location
514+ $envArgs = @ (" -e" , " HOME=$containerUserProfile " , " -e" , " USERPROFILE=$containerUserProfile " )
515+
516+ Write-Host " Executing: `` docker run --rm --memory=12g $dockerArgsAsString $VolumeMappingsAsString -e HOME=$containerUserProfile -e USERPROFILE=$containerUserProfile -w $ContainerSourceDir $ImageTag `" $pwshPath `" -Command `" $inlineScript `" " - ForegroundColor Cyan
517+ docker run -- rm -- memory= 12g $dockerArgs @VolumeMappings @envArgs - w $ContainerSourceDir $ImageTag $pwshPath - Command $inlineScript
518+
519+ if ($LASTEXITCODE -ne 0 )
520+ {
521+ Write-Host " Docker run (Claude) failed with exit code $LASTEXITCODE " - ForegroundColor Red
522+ exit $LASTEXITCODE
523+ }
524+ }
525+ else
526+ {
527+ # Run standard build mode
387528 # Delete now and not in the container because it's much faster and lock error messages are more relevant.
388529 Write-Host " Building the product in the container." - ForegroundColor Green
389530
@@ -404,23 +545,26 @@ if (-not $BuildImage)
404545 {
405546 $pwshArgs = " -NonInteractive"
406547 $dockerArgs = @ ()
407- $pwshExitCommand = " exit `$ LASTEXITCODE`;"
548+ $pwshExitCommand = " exit `$ LASTEXITCODE`;"
408549 }
409550
410551 $buildArgsString = $BuildArgs -join " "
411552 $VolumeMappingsAsString = $VolumeMappings -join " "
412553 $dockerArgsAsString = $dockerArgs -join " "
413554
555+ # Build inline script: subst drives, cd to source, run build
556+ $inlineScript = " ${substCommandsInline} cd '$SourceDirName '; & .\$Script $buildArgsString ; $pwshExitCommand "
414557
415- Write-Host " Executing: `` docker run --rm --memory=12g $dockerArgsAsString $VolumeMappingsAsString -w $SourceDirName $ImageTag pwsh $pwshArgs -Command `" & .\$Script $buildArgsString `; $pwshExitCommand `" " - ForegroundColor Cyan
558+ $pwshPath = ' C:\Program Files\PowerShell\7\pwsh.exe'
559+ Write-Host " Executing: `` docker run --rm --memory=12g $dockerArgsAsString $VolumeMappingsAsString -w $ContainerSourceDir $ImageTag `" $pwshPath `" $pwshArgs -Command `" $inlineScript `" " - ForegroundColor Cyan
416560
417- docker run -- rm -- memory= 12g $dockerArgs @VolumeMappings - w $SourceDirName @dockerArgs $ImageTag pwsh $pwshArgs - Command " & .\ $Script $buildArgsString `; $pwshExitCommand ; "
561+ docker run -- rm -- memory= 12g $dockerArgs @VolumeMappings - w $ContainerSourceDir $ImageTag $pwshPath $pwshArgs - Command $inlineScript
418562 if ($LASTEXITCODE -ne 0 )
419563 {
420564 Write-Host " Docker run (build) failed with exit code $LASTEXITCODE " - ForegroundColor Red
421565 exit $LASTEXITCODE
422566 }
423-
567+ }
424568}
425569else
426570{
0 commit comments