Avoid CMD and PowerShell with a Visual Studio Dockerfile with Bash support
==========================================================================

Automation for projects targeting different operating systems is sometimes written twice: once in a Unix shell scripting language (normally bash) for Linux, BSD, and macOS, and once in either cmd or PowerShell for Windows. Most of the logic in these scripts will be equivalent, and therefore duplicated. Duplicated code is more difficult and error-prone to maintain when making updates.

However, there are a handful of distributions of Bash for Windows, which let you write (nearly) identical scripts across all targeted operating systems. GitHub Actions makes this easy by making these distributions of Bash available on the Windows runners.

I’ve created a Dockerfile that can serve as a base image for building software on Windows using Bash, both within a derivative image and executing as a development container. It is derived from an example from Microsoft. It installs the specified version of the Visual Studio Build Tools, the Chocolatey package manager, Ccache, 7zip, and Git (which includes a distribution of Bash when installed with the /GitAndUnixToolsOnPath parameter). It also sets the entrypoint to ...\vcvars64.bat && which ensures any command executed as part of a container run will do so with the 64-bit build tools available, including cmake.exe and cl.exe.

windows-toolchain.Dockerfile

# escape=`

FROM mcr.microsoft.com/windows/servercore:ltsc2019

SHELL ["powershell"]
RUN New-ItemProperty `
  -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
  -Name "LongPathsEnabled" `
  -Value 1 `
  -PropertyType DWORD `
  -Force
ARG VS_VERSION=2019
ENV VS_VERSION=${VS_VERSION}
RUN `
[System.Net.ServicePointManager]::SecurityProtocol = `
  [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; `
Switch ($env:VS_VERSION) { `
    "2019" {$url_version = "16"} `
    "2022" {$url_version = "17"} `
    default {echo "Unsupported VS version $env:VS_VERSION"; EXIT 1} `
}; `
wget `
  -Uri https://aka.ms/vs/${url_version}/release/vs_buildtools.exe `
  -OutFile vs_buildtools.exe

SHELL ["cmd", "/S", "/C"]
ENV VS="C:\Program Files (x86)\Microsoft Visual Studio\${VS_VERSION}"
RUN `
(start /w vs_buildtools.exe --quiet --wait --norestart --nocache `
  --installPath "%VS%\%VS_VERSION%\BuildTools" `
  --add Microsoft.VisualStudio.Workload.VCTools `
  --includeRecommended `
  || IF "%ERRORLEVEL%"=="3010" EXIT 0) && `
del /q vs_buildtools.exe && `
mklink /d "%VS%\current" "%VS%\%VS_VERSION%" && `
if not exist "%VS%\%VS_VERSION%\BuildTools\Common7\Tools\VsDevCmd.bat" exit 1

ENTRYPOINT ["C:\\Program Files (x86)\\Microsoft Visual Studio\\current\\BuildTools\\VC\\Auxiliary\\Build\\vcvars64.bat", "&&"]

SHELL ["powershell"]
ENV chocolateyUseWindowsCompression false
RUN `
Set-ExecutionPolicy Bypass -Scope Process -Force; `
[System.Net.ServicePointManager]::SecurityProtocol = `
  [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; `
iex ((New-Object System.Net.WebClient).DownloadString(`
  'https://community.chocolatey.org/install.ps1'))
SHELL ["cmd", "/S", "/C"]
RUN `
call "%VS%\current\BuildTools\VC\Auxiliary\Build\vcvars64.bat" && `
powershell -command `
  "[Environment]::SetEnvironmentVariable('Path', $env:Path, [System.EnvironmentVariableTarget]::Machine)"
RUN `
choco feature disable --name showDownloadProgress && `
choco install -y --fail-on-error-output git -params '"/GitAndUnixToolsOnPath"' && `
choco install -y --fail-on-error-output 7zip && `
choco install -y --fail-on-error-output ccache

Derivative image

When used as a base image in a derivative Dockerfile, bash.exe is an available shell. Because Windows Dockerfiles do not support extended Buildkit syntax features like heredocs, instructions must be strung together with &&.

# escape=`

FROM windows-toolchain:latest
SHELL ["bash.exe", "-x", "-e", "-c"]
RUN "`
curl -OSL --ssl-no-revoke https://zlib.net/zlib1213.zip && `
7z.exe x zlib1213.zip && `
rm zlib1213.zip && `
cd zlib-1.2.13 && `
mkdir build && `
cd build && `
cmake -G Ninja -D BUILD_SHARED_LIBS=YES .. && `
cmake --build . --target INSTALL && `
cd ../.. && `
rm -rf zlib-1.2.13"

Build environment

When used as a build environment, you can pass Bash code to be executed in with a Bash heredoc (<<EOF).

wget https://zlib.net/zlib1213.zip
unzip zlib1213.zip
rm zlib1213.zip
cd zlib-1.2.13
mkdir build
docker run \
  --rm \
  --interactive \
  -v $(cygpath -ad .):'C:\work' \
  -w 'C:\work\build' \
  windows-toolchain:latest \
  bash -e -x <<EOF
cmake -G Ninja -D BUILD_SHARED_LIBS=YES ..
cmake --build . --target INSTALL
EOF
cd ..
rm -rf zlib-1.2.13

Aside from the setup code in the windows-toolchain Dockerfile, no cmd or powershell code is necessary to compile code using Visual Studio Build Tools. This allows automation to be written once in Bash, with minimal conditions for handling operating system differences.

· docker, code-duplication, windows-containers