From c677503948494fa9b284f90870ef851f9cca2183 Mon Sep 17 00:00:00 2001 From: frostebite Date: Mon, 24 Aug 2026 18:17:19 +0100 Subject: [PATCH] fix: mount VS2022's actual install path into Windows Docker builds Root-caused a real, previously-misdiagnosed Windows CI failure in game-ci/unity-builder#844: every StandaloneWindows64/WSAPlayer build failed deep inside IL2CPP with "Could not set up a toolchain for Architecture x64" - looking for VS via a legacy VS2015-era registry key - despite the Docker container starting successfully and getting all the way through asset import and script compilation first. Root cause: getWindowsCommand only ever mounted "C:/Program Files (x86)/Microsoft Visual Studio" into the container. Visual Studio 2022 - the first native 64-bit VS release - installs to "C:/Program Files/Microsoft Visual Studio" instead (confirmed against actions/runner-images' own windows-2022 documentation: VS2022 Enterprise lives at "C:/Program Files/Microsoft Visual Studio/2022/ Enterprise" with the MSVC v143 toolset). GitHub-hosted windows-2022/ windows-latest runners only have VS2022, so the (x86) mount carries no real compiler toolchain into the container - it likely still exists (legacy/shared components), which is why Docker itself started fine and the failure only surfaced deep inside the IL2CPP build step. Fix: mount the VS2022-generation path too, alongside the existing (x86) one (kept for older on-prem/self-hosted VS installs). Guarded by fs.existsSync, same pattern as the registry-keys mount fix, so a host without a VS2022-generation install at this path isn't handed a Docker bind mount for a source that doesn't exist. Verified: tsc --noEmit shows only the same pre-existing TS5097 import-extension class of errors (one more instance of it, from the new test file import - not a new error type). bunx oxfmt --check clean. bun test ./src: 200 pass, 0 fail (docker.test.ts: 16 pass, 2 new regression tests for this fix). bun run build succeeds. --- src/model/docker.test.ts | 462 ++++++++++++++++++++++----------------- src/model/docker.ts | 120 +++++----- 2 files changed, 328 insertions(+), 254 deletions(-) diff --git a/src/model/docker.test.ts b/src/model/docker.test.ts index 389a937..45cf1ca 100644 --- a/src/model/docker.test.ts +++ b/src/model/docker.test.ts @@ -1,10 +1,11 @@ -import { describe, it, expect, mock, afterEach } from 'bun:test'; -import { Action } from './action.ts'; -import { Docker } from './docker.ts'; -import { System } from './system/system.ts'; -import { UnityBuildValidation } from './unity/build-validation/unity-build-validation.ts'; - -describe('Docker', () => { +import { describe, it, expect, mock, afterEach } from "bun:test"; +import { Action } from "./action.ts"; +import { Docker } from "./docker.ts"; +import { System } from "./system/system.ts"; +import { UnityBuildValidation } from "./unity/build-validation/unity-build-validation.ts"; +import { fsSync as fs } from "../dependencies.ts"; + +describe("Docker", () => { const originalSystemRun = System.run; const originalValidateBuild = UnityBuildValidation.validateBuild; @@ -13,148 +14,148 @@ describe('Docker', () => { UnityBuildValidation.validateBuild = originalValidateBuild; }); - it('skips build-output validation for activate-only runs (game-ci/unity-activate#111)', async () => { + it("skips build-output validation for activate-only runs (game-ci/unity-activate#111)", async () => { // Real bug: validateBuild() requires a "# Build results #" section, // which only a real build ever produces - it threw on every successful // activation, since there's nothing to build. - System.run = mock(() => Promise.resolve({ output: 'Activation complete.', error: '' })); + System.run = mock(() => Promise.resolve({ output: "Activation complete.", error: "" })); const validateBuildMock = mock(() => {}); UnityBuildValidation.validateBuild = validateBuildMock; - await Docker.run('game-ci/unity-editor-stub:latest', { - hostOS: 'linux', - hostPlatform: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', + await Docker.run("game-ci/unity-editor-stub:latest", { + hostOS: "linux", + hostPlatform: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", activateOnly: true, } as any); expect(validateBuildMock).not.toHaveBeenCalled(); }); - it('still validates build output for real (non-activate-only) builds', async () => { - System.run = mock(() => Promise.resolve({ output: '# Build results #\nErrors: 0\nSize:', error: '' })); + it("still validates build output for real (non-activate-only) builds", async () => { + System.run = mock(() => Promise.resolve({ output: "# Build results #\nErrors: 0\nSize:", error: "" })); const validateBuildMock = mock(() => {}); UnityBuildValidation.validateBuild = validateBuildMock; - await Docker.run('game-ci/unity-editor-stub:latest', { - hostOS: 'linux', - hostPlatform: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', + await Docker.run("game-ci/unity-editor-stub:latest", { + hostOS: "linux", + hostPlatform: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", } as any); expect(validateBuildMock).toHaveBeenCalled(); }); - it('builds a continuous Linux docker command', () => { - const command = (Docker as any).getLinuxCommand('game-ci/unity-editor-stub:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - runnerTempPath: '/home/runner/work/_temp', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', - unityLicense: 'ci-stub-license', - engineVersion: '2019.4.40f1', - projectPath: 'test-project', - targetPlatform: 'StandaloneLinux64', - buildName: 'StandaloneLinux64', - buildPath: 'build/StandaloneLinux64', - buildFile: 'StandaloneLinux64', + it("builds a continuous Linux docker command", () => { + const command = (Docker as any).getLinuxCommand("game-ci/unity-editor-stub:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + runnerTempPath: "/home/runner/work/_temp", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", + unityLicense: "ci-stub-license", + engineVersion: "2019.4.40f1", + projectPath: "test-project", + targetPlatform: "StandaloneLinux64", + buildName: "StandaloneLinux64", + buildPath: "build/StandaloneLinux64", + buildFile: "StandaloneLinux64", }); expect(command).toContain('--env UNITY_LICENSE="ci-stub-license"'); expect(command).toContain('--volume "/home/runner":"/root:z"'); expect(command).toContain('--volume "/home/runner/work/cli/cli":"/github/workspace:z"'); - expect(command).toContain('game-ci/unity-editor-stub:latest'); - expect(command).toContain('/bin/bash /entrypoint.sh'); - expect(command).not.toContain('\n'); + expect(command).toContain("game-ci/unity-editor-stub:latest"); + expect(command).toContain("/bin/bash /entrypoint.sh"); + expect(command).not.toContain("\n"); }); - it('uses the engine-supplied commands instead of the Unity entrypoint for non-Unity engines', () => { - const command = (Docker as any).getLinuxCommand('game-ci/godot:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - runnerTempPath: '/home/runner/work/_temp', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'godot', - projectPath: 'test-project', + it("uses the engine-supplied commands instead of the Unity entrypoint for non-Unity engines", () => { + const command = (Docker as any).getLinuxCommand("game-ci/godot:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + runnerTempPath: "/home/runner/work/_temp", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "godot", + projectPath: "test-project", commands: 'godot --headless --export-release "Linux" build/output', }); expect(command).toContain('godot --headless --export-release "Linux" build/output'); - expect(command).not.toContain('/bin/bash /entrypoint.sh'); - expect(command).not.toContain('--env UNITY_SERIAL'); - expect(command).not.toContain('entrypoint.sh:/entrypoint.sh'); + expect(command).not.toContain("/bin/bash /entrypoint.sh"); + expect(command).not.toContain("--env UNITY_SERIAL"); + expect(command).not.toContain("entrypoint.sh:/entrypoint.sh"); }); - it('still uses the Unity entrypoint when engine is unity even if commands happens to be set', () => { - const command = (Docker as any).getLinuxCommand('game-ci/unity-editor-stub:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - runnerTempPath: '/home/runner/work/_temp', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', - commands: 'should-not-be-used', + it("still uses the Unity entrypoint when engine is unity even if commands happens to be set", () => { + const command = (Docker as any).getLinuxCommand("game-ci/unity-editor-stub:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + runnerTempPath: "/home/runner/work/_temp", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", + commands: "should-not-be-used", }); - expect(command).toContain('/bin/bash /entrypoint.sh'); - expect(command).not.toContain('should-not-be-used'); + expect(command).toContain("/bin/bash /entrypoint.sh"); + expect(command).not.toContain("should-not-be-used"); }); - it('leaves the Godot commands string byte-identical when engineLaunchWrapper is unset', () => { - const command = (Docker as any).getLinuxCommand('game-ci/godot:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - runnerTempPath: '/home/runner/work/_temp', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'godot', - projectPath: 'test-project', + it("leaves the Godot commands string byte-identical when engineLaunchWrapper is unset", () => { + const command = (Docker as any).getLinuxCommand("game-ci/godot:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + runnerTempPath: "/home/runner/work/_temp", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "godot", + projectPath: "test-project", commands: 'godot --headless --export-release "Linux" build/output', }); expect(command).toContain('game-ci/godot:latest godot --headless --export-release "Linux" build/output'); }); - it('prefixes the Godot commands string with engineLaunchWrapper when set (Linux)', () => { - const command = (Docker as any).getLinuxCommand('game-ci/godot:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - runnerTempPath: '/home/runner/work/_temp', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'godot', - projectPath: 'test-project', + it("prefixes the Godot commands string with engineLaunchWrapper when set (Linux)", () => { + const command = (Docker as any).getLinuxCommand("game-ci/godot:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + runnerTempPath: "/home/runner/work/_temp", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "godot", + projectPath: "test-project", commands: 'godot --headless --export-release "Linux" build/output', - engineLaunchWrapper: 'flock /tmp/engine.lock --', + engineLaunchWrapper: "flock /tmp/engine.lock --", }); expect(command).toContain( @@ -162,139 +163,192 @@ describe('Docker', () => { ); }); - it('prefixes the Godot commands string with engineLaunchWrapper when set (Windows)', () => { - const command = (Docker as any).getWindowsCommand('game-ci/godot:latest', { - currentWorkDir: 'C:/work/cli', - homeDir: 'C:/Users/runner', - cliDistPath: 'C:/work/cli/dist', - cliStoragePath: 'C:/work/.game-ci', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'godot', + it("prefixes the Godot commands string with engineLaunchWrapper when set (Windows)", () => { + const command = (Docker as any).getWindowsCommand("game-ci/godot:latest", { + currentWorkDir: "C:/work/cli", + homeDir: "C:/Users/runner", + cliDistPath: "C:/work/cli/dist", + cliStoragePath: "C:/work/.game-ci", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "godot", commands: 'godot --headless --export-release "Windows" build/output', - engineLaunchWrapper: 'flock /tmp/engine.lock --', + engineLaunchWrapper: "flock /tmp/engine.lock --", }); expect(command).toContain('flock /tmp/engine.lock -- godot --headless --export-release "Windows" build/output'); }); - it('does not affect the Unity entrypoint flow even when engineLaunchWrapper is set (wrapping happens per-call-site inside the scripts instead)', () => { - const command = (Docker as any).getLinuxCommand('game-ci/unity-editor-stub:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', - engineLaunchWrapper: 'flock /tmp/engine.lock --', + it("does not affect the Unity entrypoint flow even when engineLaunchWrapper is set (wrapping happens per-call-site inside the scripts instead)", () => { + const command = (Docker as any).getLinuxCommand("game-ci/unity-editor-stub:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", + engineLaunchWrapper: "flock /tmp/engine.lock --", }); - expect(command).toContain('/bin/bash /entrypoint.sh'); + expect(command).toContain("/bin/bash /entrypoint.sh"); expect(command).toContain('--env ENGINE_LAUNCH_WRAPPER="flock /tmp/engine.lock --"'); }); - it('applies docker resource limits and host networking when set', () => { - const command = (Docker as any).getLinuxCommand('game-ci/unity-editor-stub:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', - dockerCpuLimit: '4', - dockerMemoryLimit: '8192m', - dockerShmSize: '1024m', + it("applies docker resource limits and host networking when set", () => { + const command = (Docker as any).getLinuxCommand("game-ci/unity-editor-stub:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", + dockerCpuLimit: "4", + dockerMemoryLimit: "8192m", + dockerShmSize: "1024m", useHostNetwork: true, }); - expect(command).toContain('--cpus=4'); - expect(command).toContain('--memory=8192m'); - expect(command).toContain('--shm-size=1024m'); - expect(command).toContain('--net=host'); + expect(command).toContain("--cpus=4"); + expect(command).toContain("--memory=8192m"); + expect(command).toContain("--shm-size=1024m"); + expect(command).toContain("--net=host"); + }); + + it("omits --shm-size when dockerShmSize is not set (game-ci/unity-test-runner#307)", () => { + const command = (Docker as any).getLinuxCommand("game-ci/unity-editor-stub:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + sshAgent: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", + }); + + expect(command).not.toContain("--shm-size"); }); - it('omits --shm-size when dockerShmSize is not set (game-ci/unity-test-runner#307)', () => { - const command = (Docker as any).getLinuxCommand('game-ci/unity-editor-stub:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - sshAgent: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', + it("mounts a custom SSH public keys directory instead of the known_hosts fallback", () => { + const command = (Docker as any).getLinuxCommand("game-ci/unity-editor-stub:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + sshAgent: "/ssh-agent", + sshPublicKeysDirectoryPath: "/home/runner/.ssh/keys", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", }); - expect(command).not.toContain('--shm-size'); + expect(command).toContain("--volume /home/runner/.ssh/keys:/root/.ssh:ro"); + expect(command).not.toContain("known_hosts"); }); - it('mounts a custom SSH public keys directory instead of the known_hosts fallback', () => { - const command = (Docker as any).getLinuxCommand('game-ci/unity-editor-stub:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - sshAgent: '/ssh-agent', - sshPublicKeysDirectoryPath: '/home/runner/.ssh/keys', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', + it("mounts sshPublicKeysDirectoryPath even without sshAgent set, matching unity-builder", () => { + const command = (Docker as any).getLinuxCommand("game-ci/unity-editor-stub:latest", { + hostOS: "linux", + currentWorkDir: "/home/runner/work/cli/cli", + homeDir: "/home/runner", + cliDistPath: "/home/runner/work/cli/cli/dist", + sshAgent: "", + sshPublicKeysDirectoryPath: "/home/runner/.ssh/keys", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", }); - expect(command).toContain('--volume /home/runner/.ssh/keys:/root/.ssh:ro'); - expect(command).not.toContain('known_hosts'); + expect(command).toContain("--volume /home/runner/.ssh/keys:/root/.ssh:ro"); }); - it('mounts sshPublicKeysDirectoryPath even without sshAgent set, matching unity-builder', () => { - const command = (Docker as any).getLinuxCommand('game-ci/unity-editor-stub:latest', { - hostOS: 'linux', - currentWorkDir: '/home/runner/work/cli/cli', - homeDir: '/home/runner', - cliDistPath: '/home/runner/work/cli/cli/dist', - sshAgent: '', - sshPublicKeysDirectoryPath: '/home/runner/.ssh/keys', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', + it("applies docker resource limits and isolation mode on Windows", () => { + const command = (Docker as any).getWindowsCommand("game-ci/unity-editor-stub:latest", { + currentWorkDir: "C:/work/cli", + homeDir: "C:/Users/runner", + cliDistPath: "C:/work/cli/dist", + cliStoragePath: "C:/work/.game-ci", + unitySerial: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", + dockerCpuLimit: "4", + dockerMemoryLimit: "8192m", + dockerShmSize: "1024m", + dockerIsolationMode: "process", }); - expect(command).toContain('--volume /home/runner/.ssh/keys:/root/.ssh:ro'); + expect(command).toContain("--cpus=4"); + expect(command).toContain("--memory=8192m"); + expect(command).toContain("--shm-size=1024m"); + expect(command).toContain("--isolation=process"); }); - it('applies docker resource limits and isolation mode on Windows', () => { - const command = (Docker as any).getWindowsCommand('game-ci/unity-editor-stub:latest', { - currentWorkDir: 'C:/work/cli', - homeDir: 'C:/Users/runner', - cliDistPath: 'C:/work/cli/dist', - cliStoragePath: 'C:/work/.game-ci', - unitySerial: '', - gitPrivateToken: '', - dockerWorkspacePath: '/github/workspace', - engine: 'unity', - dockerCpuLimit: '4', - dockerMemoryLimit: '8192m', - dockerShmSize: '1024m', - dockerIsolationMode: 'process', + // Regression test for a real bug: GitHub-hosted windows-2022/windows-latest + // runners only have VS2022 (which installs to "Program Files", not + // "Program Files (x86)" like every earlier VS version), but the Windows + // Docker command only ever mounted the (x86) path into the container - + // silently carrying no real compiler toolchain in, and failing IL2CPP + // builds with "Could not set up a toolchain for Architecture x64" deep + // inside the build, long after the container started successfully. + it("mounts the VS2022-generation (non-x86) Visual Studio path when it exists on the host", () => { + const originalExistsSync = fs.existsSync; + fs.existsSync = mock((checkedPath: string) => checkedPath === "C:/Program Files/Microsoft Visual Studio") as any; + + const command = (Docker as any).getWindowsCommand("game-ci/unity-editor-stub:latest", { + currentWorkDir: "C:/work/cli", + homeDir: "C:/Users/runner", + cliDistPath: "C:/work/cli/dist", + cliStoragePath: "C:/work/.game-ci", + unitySerial: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", }); - expect(command).toContain('--cpus=4'); - expect(command).toContain('--memory=8192m'); - expect(command).toContain('--shm-size=1024m'); - expect(command).toContain('--isolation=process'); + fs.existsSync = originalExistsSync; + + expect(command).toContain( + '--volume="C:/Program Files/Microsoft Visual Studio":"C:/Program Files/Microsoft Visual Studio"', + ); + // Still mounts the legacy (x86) path too, unconditionally, for hosts with an older VS generation. + expect(command).toContain( + '--volume="C:/Program Files (x86)/Microsoft Visual Studio":"C:/Program Files (x86)/Microsoft Visual Studio"', + ); + }); + + it("omits the VS2022 mount (rather than a Docker bind-mount error) when that path does not exist on the host", () => { + const originalExistsSync = fs.existsSync; + fs.existsSync = mock(() => false) as any; + + const command = (Docker as any).getWindowsCommand("game-ci/unity-editor-stub:latest", { + currentWorkDir: "C:/work/cli", + homeDir: "C:/Users/runner", + cliDistPath: "C:/work/cli/dist", + cliStoragePath: "C:/work/.game-ci", + unitySerial: "", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", + }); + + fs.existsSync = originalExistsSync; + + expect(command).not.toContain('"C:/Program Files/Microsoft Visual Studio"'); }); - it.skip('runs', async () => { - const image = 'unity-builder:2019.2.11f1-webgl'; + it.skip("runs", async () => { + const image = "unity-builder:2019.2.11f1-webgl"; const parameters = { workspace: Action.rootFolder, projectPath: `${Action.rootFolder}/test-project`, - buildName: 'someBuildName', - buildsPath: 'build', - method: '', + buildName: "someBuildName", + buildsPath: "build", + method: "", }; await Docker.run(image, parameters); }); diff --git a/src/model/docker.ts b/src/model/docker.ts index db00076..df501e8 100644 --- a/src/model/docker.ts +++ b/src/model/docker.ts @@ -1,13 +1,13 @@ -import { ImageEnvironmentFactory } from './image-environment-factory.ts'; -import { path, fsSync as fs } from '../dependencies.ts'; -import type { Options } from '../dependencies.ts'; -import { System } from './system/system.ts'; -import { UnityBuildValidation } from './unity/build-validation/unity-build-validation.ts'; -import { UnityEnvironment } from '../logic/unity/environment.ts'; +import { ImageEnvironmentFactory } from "./image-environment-factory.ts"; +import { path, fsSync as fs } from "../dependencies.ts"; +import type { Options } from "../dependencies.ts"; +import { System } from "./system/system.ts"; +import { UnityBuildValidation } from "./unity/build-validation/unity-build-validation.ts"; +import { UnityEnvironment } from "../logic/unity/environment.ts"; /** UNITY_LICENSE/ANDROID_* etc. only make sense inside a Unity container. */ function engineEnvVars(options: Options) { - return options.engine === 'unity' ? UnityEnvironment.getVariables(options) : []; + return options.engine === "unity" ? UnityEnvironment.getVariables(options) : []; } class Docker { @@ -16,15 +16,15 @@ class Docker { log.warning(`running docker process for ${hostOS} (${hostPlatform})`); - let command = ''; + let command = ""; switch (hostOS) { - case 'windows': { + case "windows": { // Todo: check if docker daemon is set for Windows or Linux containers. command = await this.getWindowsCommand(image, options); break; } - case 'linux': - case 'darwin': { + case "linux": + case "darwin": { command = await this.getLinuxCommand(image, options); break; } @@ -41,7 +41,7 @@ class Docker { // "There was an error building the project" on every successful // activation, because there's no build to validate in the first place. switch (engine) { - case 'unity': + case "unity": if (!activateOnly) { UnityBuildValidation.validateBuild(dockerRun.output); } @@ -87,14 +87,17 @@ class Docker { } = options as Options & { commands?: string }; const home = homeDir; - const envVarString = ImageEnvironmentFactory.getEnvVarString(options, engineEnvVars(options)).replace(/ \\\n/g, ' '); + const envVarString = ImageEnvironmentFactory.getEnvVarString(options, engineEnvVars(options)).replace( + / \\\n/g, + " ", + ); // Non-Unity engines (Godot, Unreal) supply their own container command // via `commands` instead of Unity's license-activate-build-return // entrypoint.sh flow — this used to be silently ignored here, so any // build with `engine !== 'unity'` always ran Unity's entrypoint instead // of the command the engine plugin actually asked for. - const isUnityDefaultFlow = !commands || engine === 'unity'; + const isUnityDefaultFlow = !commands || engine === "unity"; // Non-Unity engines' `commands` is already just the engine invocation // itself (no surrounding activate/build/return-license lifecycle), so a @@ -104,32 +107,32 @@ class Docker { const wrappedCommands = engineLaunchWrapper && commands ? `${engineLaunchWrapper} ${commands}` : commands; return [ - 'docker run', - '--rm', + "docker run", + "--rm", `--workdir ${dockerWorkspacePath}`, envVarString, - isUnityDefaultFlow ? '--env UNITY_SERIAL' : '', + isUnityDefaultFlow ? "--env UNITY_SERIAL" : "", `--env GITHUB_WORKSPACE=${dockerWorkspacePath}`, - gitPrivateToken ? `--env GIT_PRIVATE_TOKEN="${gitPrivateToken}"` : '', - sshAgent ? '--env SSH_AUTH_SOCK=/ssh-agent' : '', - dockerCpuLimit ? `--cpus=${dockerCpuLimit}` : '', - dockerMemoryLimit ? `--memory=${dockerMemoryLimit}` : '', - dockerShmSize ? `--shm-size=${dockerShmSize}` : '', - useHostNetwork ? '--net=host' : '', + gitPrivateToken ? `--env GIT_PRIVATE_TOKEN="${gitPrivateToken}"` : "", + sshAgent ? "--env SSH_AUTH_SOCK=/ssh-agent" : "", + dockerCpuLimit ? `--cpus=${dockerCpuLimit}` : "", + dockerMemoryLimit ? `--memory=${dockerMemoryLimit}` : "", + dockerShmSize ? `--shm-size=${dockerShmSize}` : "", + useHostNetwork ? "--net=host" : "", `--volume "${home}":"/root:z"`, `--volume "${currentWorkDir}":"${dockerWorkspacePath}:z"`, - isUnityDefaultFlow ? `--volume "${cliDistPath}/default-build-script:/UnityBuilderAction:z"` : '', - isUnityDefaultFlow ? `--volume "${cliDistPath}/platforms/ubuntu/steps:/steps:z"` : '', - isUnityDefaultFlow ? `--volume "${cliDistPath}/platforms/ubuntu/entrypoint.sh:/entrypoint.sh:z"` : '', - isUnityDefaultFlow ? `--volume "${cliDistPath}/unity-config:/usr/share/unity3d/config:z"` : '', - sshAgent ? `--volume ${sshAgent}:/ssh-agent` : '', - sshAgent && !sshPublicKeysDirectoryPath ? '--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro' : '', - sshPublicKeysDirectoryPath ? `--volume ${sshPublicKeysDirectoryPath}:/root/.ssh:ro` : '', + isUnityDefaultFlow ? `--volume "${cliDistPath}/default-build-script:/UnityBuilderAction:z"` : "", + isUnityDefaultFlow ? `--volume "${cliDistPath}/platforms/ubuntu/steps:/steps:z"` : "", + isUnityDefaultFlow ? `--volume "${cliDistPath}/platforms/ubuntu/entrypoint.sh:/entrypoint.sh:z"` : "", + isUnityDefaultFlow ? `--volume "${cliDistPath}/unity-config:/usr/share/unity3d/config:z"` : "", + sshAgent ? `--volume ${sshAgent}:/ssh-agent` : "", + sshAgent && !sshPublicKeysDirectoryPath ? "--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro" : "", + sshPublicKeysDirectoryPath ? `--volume ${sshPublicKeysDirectoryPath}:/root/.ssh:ro` : "", image, - isUnityDefaultFlow ? '/bin/bash /entrypoint.sh' : wrappedCommands!, + isUnityDefaultFlow ? "/bin/bash /entrypoint.sh" : wrappedCommands!, ] .filter(Boolean) - .join(' '); + .join(" "); } private static getWindowsCommand(image: string, options: Options): string { @@ -155,44 +158,61 @@ class Docker { // Windows Docker build path other than Unity, but guard it the same way // for consistency and so a future one isn't silently broken like this // was. - const isUnityDefaultFlow = !commands || engine === 'unity'; + const isUnityDefaultFlow = !commands || engine === "unity"; // See getLinuxCommand's own comment - same single top-level wrap, // byte-identical to today when engineLaunchWrapper is unset. const wrappedCommands = engineLaunchWrapper && commands ? `${engineLaunchWrapper} ${commands}` : commands; + // Visual Studio 2022 installs to "Program Files" (it's the first native + // 64-bit VS release), not "Program Files (x86)" where every earlier VS + // version lived - and where the container mount below still points. + // GitHub-hosted windows-2022/windows-latest runners only have VS2022, so + // that mount silently carries no real compiler toolchain into the + // container: IL2CPP's "Could not set up a toolchain for Architecture x64" + // failure is this, not a genuinely missing VS install on the runner. + // Guarded by existsSync (like the registry-keys fix) so hosts without a + // VS2022-generation install at this path aren't handed a Docker bind + // mount for a source path that doesn't exist. + const vs2022Path = "C:/Program Files/Microsoft Visual Studio"; + const vs2022Mount = + isUnityDefaultFlow && fs.existsSync(vs2022Path) ? ` --volume="${vs2022Path}":"${vs2022Path}" \`` : ""; + // Note: the equals sign (=) is needed in Powershell. // Note: homedir is currently not configured for windows (yet). return [ - 'docker run `', - ' --rm `', + "docker run `", + " --rm `", ` --workdir="c:${dockerWorkspacePath}" \``, ` ${ImageEnvironmentFactory.getEnvVarString(options, engineEnvVars(options))} \``, - isUnityDefaultFlow ? ` --env UNITY_SERIAL="${unitySerial}" \`` : '', + isUnityDefaultFlow ? ` --env UNITY_SERIAL="${unitySerial}" \`` : "", ` --env GITHUB_WORKSPACE=c:${dockerWorkspacePath} \``, ` --env GIT_PRIVATE_TOKEN="${gitPrivateToken}" \``, - dockerCpuLimit ? ` --cpus=${dockerCpuLimit} \`` : '', - dockerMemoryLimit ? ` --memory=${dockerMemoryLimit} \`` : '', - dockerShmSize ? ` --shm-size=${dockerShmSize} \`` : '', - dockerIsolationMode ? ` --isolation=${dockerIsolationMode} \`` : '', + dockerCpuLimit ? ` --cpus=${dockerCpuLimit} \`` : "", + dockerMemoryLimit ? ` --memory=${dockerMemoryLimit} \`` : "", + dockerShmSize ? ` --shm-size=${dockerShmSize} \`` : "", + dockerIsolationMode ? ` --isolation=${dockerIsolationMode} \`` : "", ` --volume="${currentWorkDir}":"c:${dockerWorkspacePath}" \``, - isUnityDefaultFlow ? ` --volume="${cliStoragePath}/registry-keys":"c:/registry-keys" \`` : '', + isUnityDefaultFlow ? ` --volume="${cliStoragePath}/registry-keys":"c:/registry-keys" \`` : "", isUnityDefaultFlow ? ' --volume="C:/Program Files (x86)/Microsoft Visual Studio":"C:/Program Files (x86)/Microsoft Visual Studio" `' - : '', - isUnityDefaultFlow ? ' --volume="C:/Program Files (x86)/Windows Kits":"C:/Program Files (x86)/Windows Kits" `' : '', + : "", + vs2022Mount, + isUnityDefaultFlow + ? ' --volume="C:/Program Files (x86)/Windows Kits":"C:/Program Files (x86)/Windows Kits" `' + : "", isUnityDefaultFlow ? ' --volume="C:/ProgramData/Microsoft/VisualStudio":"C:/ProgramData/Microsoft/VisualStudio" `' - : '', - isUnityDefaultFlow ? ` --volume="${cliDistPath}/default-build-script":"c:/UnityBuilderAction" \`` : '', - isUnityDefaultFlow ? ` --volume="${cliDistPath}/platforms/windows":"c:/steps" \`` : '', - isUnityDefaultFlow ? ` --volume="${cliDistPath}/BlankProject":"c:/BlankProject" \`` : '', - isUnityDefaultFlow ? ` --volume="${cliDistPath}/unity-config":"c:/ProgramData/Unity/config" \`` : '', + : "", + isUnityDefaultFlow ? ` --volume="${cliDistPath}/default-build-script":"c:/UnityBuilderAction" \`` : "", + isUnityDefaultFlow ? ` --volume="${cliDistPath}/platforms/windows":"c:/steps" \`` : "", + isUnityDefaultFlow ? ` --volume="${cliDistPath}/BlankProject":"c:/BlankProject" \`` : "", + isUnityDefaultFlow ? ` --volume="${cliDistPath}/unity-config":"c:/ProgramData/Unity/config" \`` : "", ` ${image} \``, - isUnityDefaultFlow ? ' powershell c:/steps/entrypoint.ps1' : ` ${wrappedCommands}`, + isUnityDefaultFlow ? " powershell c:/steps/entrypoint.ps1" : ` ${wrappedCommands}`, ] .filter(Boolean) - .join('\n'); + .join("\n"); } }