Compare commits

..
Author SHA1 Message Date
frostebiteandClaude Opus 5 adaf7cd8f4 fix(docker): pass --shm-size so Unity 6.6+ builds work (backport)
Unity 6.6+ editors request 1GiB of shared memory and hard-fail with
"Insufficient shared memory available - if using Docker, please run the
container with --shm-size=1025M" against Docker's 64m default.

This action never passed --shm-size at all and exposed no input to work
around it, so affected users had no escape hatch whatsoever. Reported as
#840 (closed as not-planned) and again on Discord against Unity
6000.6.0f1.

Backport of the fix already merged into game-ci/cli (#239), which owns
this code going forward. Adds the dockerShmSize input, the
BuildParameters field, and the flag on both the Linux and Windows
commands, defaulting to 1025m - the value unity-test-runner has always
passed. --shm-size caps a tmpfs rather than preallocating memory, so the
default costs nothing for editors that don't need it. "0"/"none" opts
back out to Docker's own default.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-02 00:30:08 +01:00
10 changed files with 125 additions and 58 deletions
+9 -9
View File
@@ -128,6 +128,13 @@ inputs:
required: false
default: ''
description: 'Number of CPU cores to assign the docker container. Defaults to all available cores on all platforms.'
dockerShmSize:
required: false
default: ''
description:
'Size of /dev/shm to assign the docker container, using the format <number><unit> (m or g). Unity 6.6+ editors
request 1GiB of shared memory and fail with "Insufficient shared memory available" against Docker''s 64m
default. Defaults to 1025m; set to "0" or "none" to use Docker''s own default.'
dockerMemoryLimit:
required: false
default: ''
@@ -168,14 +175,6 @@ inputs:
default: ''
required: false
description: 'The Unity licensing server address to use for activating Unity.'
unityLicensingToolset:
default: ''
required: false
description:
'Optional toolset identifier for Unity floating-license servers that host multiple toolsets (e.g.
"LicenseServer_1234567890_3"). When set, written to services-config.json so the Licensing Client
requests entitlements from the named toolset instead of relying on the server-side default. Leave
empty to preserve previous behavior.'
dockerWorkspacePath:
default: '/github/workspace'
required: false
@@ -189,7 +188,8 @@ inputs:
linux64RemoveExecutableExtension:
default: 'false'
required: false
description: 'When building for StandaloneLinux64, remove the default file extension of `.x86_64`. Set to true to restore the extensionless behavior from v4.'
description:
'When building for StandaloneLinux64, remove the default file extension of `.x86_64`. Set to true to restore the extensionless behavior from v4.'
outputs:
volume:
Generated Vendored
+29 -12
View File
@@ -269,7 +269,6 @@ class BuildParameters {
customImage;
unitySerial;
unityLicensingServer;
unityLicensingToolset;
skipActivation;
runnerTempPath;
targetPlatform;
@@ -294,6 +293,7 @@ class BuildParameters {
androidSymbolType;
dockerCpuLimit;
dockerMemoryLimit;
dockerShmSize;
dockerIsolationMode;
containerRegistryRepository;
containerRegistryImageVersion;
@@ -359,7 +359,6 @@ class BuildParameters {
customImage: input_1.default.customImage,
unitySerial,
unityLicensingServer: input_1.default.unityLicensingServer,
unityLicensingToolset: input_1.default.unityLicensingToolset,
skipActivation: input_1.default.skipActivation,
runnerTempPath: input_1.default.runnerTempPath,
targetPlatform: input_1.default.targetPlatform,
@@ -391,6 +390,7 @@ class BuildParameters {
chownFilesTo: input_1.default.chownFilesTo,
dockerCpuLimit: input_1.default.dockerCpuLimit,
dockerMemoryLimit: input_1.default.dockerMemoryLimit,
dockerShmSize: input_1.default.dockerShmSize,
dockerIsolationMode: input_1.default.dockerIsolationMode,
containerRegistryRepository: input_1.default.containerRegistryRepository,
containerRegistryImageVersion: input_1.default.containerRegistryImageVersion,
@@ -517,6 +517,19 @@ const image_environment_factory_1 = __importDefault(__nccwpck_require__(25145));
const node_fs_1 = __nccwpck_require__(87561);
const node_path_1 = __importDefault(__nccwpck_require__(49411));
const exec_1 = __nccwpck_require__(71514);
/**
* Unity 6.6+ editors request 1GiB of shared memory and hard-fail with
* "Insufficient shared memory available" against Docker's 64m default
* (game-ci/unity-builder#840). BuildParameters defaults this to 1025m, the
* value unity-test-runner has always passed. '0'/'none' omits the flag so
* Docker's own default applies.
*/
function shmSizeFlag(dockerShmSize) {
const value = String(dockerShmSize ?? '').trim();
if (value === '' || value === '0' || value.toLowerCase() === 'none')
return '';
return `--shm-size=${value}`;
}
class Docker {
static async run(image, parameters, silent = false, overrideCommands = '', additionalVariables = [], options = {}, entrypointBash = false) {
let runCommand = '';
@@ -535,7 +548,7 @@ class Docker {
return await (0, exec_1.exec)(runCommand, undefined, options);
}
static getLinuxCommand(image, parameters, overrideCommands = '', additionalVariables = [], entrypointBash = false) {
const { workspace, actionFolder, useHostNetwork, runnerTempPath, sshAgent, sshPublicKeysDirectoryPath, gitPrivateToken, dockerWorkspacePath, dockerCpuLimit, dockerMemoryLimit, } = parameters;
const { workspace, actionFolder, useHostNetwork, runnerTempPath, sshAgent, sshPublicKeysDirectoryPath, gitPrivateToken, dockerWorkspacePath, dockerCpuLimit, dockerMemoryLimit, dockerShmSize, } = parameters;
const githubHome = node_path_1.default.join(runnerTempPath, '_github_home');
if (!(0, node_fs_1.existsSync)(githubHome))
(0, node_fs_1.mkdirSync)(githubHome);
@@ -563,6 +576,7 @@ class Docker {
--volume "${actionFolder}/BlankProject":"/BlankProject:z" \
--cpus=${dockerCpuLimit} \
--memory=${dockerMemoryLimit} \
${shmSizeFlag(dockerShmSize)} \
${sshAgent ? `--volume ${sshAgent}:/ssh-agent` : ''} \
${sshAgent && !sshPublicKeysDirectoryPath
? '--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro'
@@ -575,7 +589,7 @@ class Docker {
"${overrideCommands !== '' ? overrideCommands : `/entrypoint.sh`}"`;
}
static getWindowsCommand(image, parameters) {
const { workspace, actionFolder, runnerTempPath, gitPrivateToken, dockerWorkspacePath, dockerCpuLimit, dockerMemoryLimit, dockerIsolationMode, } = parameters;
const { workspace, actionFolder, runnerTempPath, gitPrivateToken, dockerWorkspacePath, dockerCpuLimit, dockerMemoryLimit, dockerShmSize, dockerIsolationMode, } = parameters;
const githubHome = node_path_1.default.join(runnerTempPath, '_github_home');
if (!(0, node_fs_1.existsSync)(githubHome))
(0, node_fs_1.mkdirSync)(githubHome);
@@ -599,6 +613,7 @@ class Docker {
--volume "${actionFolder}/BlankProject":"c:/BlankProject" \
--cpus=${dockerCpuLimit} \
--memory=${dockerMemoryLimit} \
${shmSizeFlag(dockerShmSize)} \
--isolation=${dockerIsolationMode} \
${image} \
powershell c:/steps/entrypoint.ps1`;
@@ -1247,9 +1262,6 @@ class Input {
static get unityLicensingServer() {
return Input.getInput('unityLicensingServer') ?? '';
}
static get unityLicensingToolset() {
return Input.getInput('unityLicensingToolset') ?? '';
}
static get buildMethod() {
return Input.getInput('buildMethod') ?? ''; // Processed in docker file
}
@@ -1337,6 +1349,16 @@ class Input {
static get dockerWorkspacePath() {
return Input.getInput('dockerWorkspacePath') ?? '/github/workspace';
}
/**
* Unity 6.6+ editors request 1GiB of shared memory and hard-fail with
* "Insufficient shared memory available" against Docker's 64m default
* (game-ci/unity-builder#840). unity-test-runner has always passed 1025m,
* so match it here rather than leaving builds broken by default. '0' or
* 'none' omits the flag and uses Docker's own default.
*/
static get dockerShmSize() {
return Input.getInput('dockerShmSize') ?? '1025m';
}
static get dockerCpuLimit() {
return Input.getInput('dockerCpuLimit') ?? node_os_1.default.cpus().length.toString();
}
@@ -1514,11 +1536,6 @@ class PlatformSetup {
}
let servicesConfig = node_fs_1.default.readFileSync(servicesConfigPathTemplate).toString();
servicesConfig = servicesConfig.replace('%URL%', buildParameters.unityLicensingServer);
if (buildParameters.unityLicensingToolset) {
const parsed = JSON.parse(servicesConfig);
parsed.toolset = buildParameters.unityLicensingToolset;
servicesConfig = JSON.stringify(parsed, undefined, 2);
}
node_fs_1.default.writeFileSync(servicesConfigPath, servicesConfig);
platform_setup_1.SetupAndroid.setup(buildParameters);
}
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -6
View File
@@ -71,12 +71,8 @@ if [[ "$RUN_AS_HOST_USER" == "true" ]]; then
# Don't stop on error when running our scripts as error handling is baked in
set +e
# Switch to the host user so we can create files with the correct ownership.
# Pass HOME/USER explicitly so the Unity Licensing Client (which writes to
# ~/.config/unity3d) resolves a real, writable home directory rather than
# falling back to /home/UNKNOWN when getpwuid() inside the container has no
# entry for the host UID. -p preserves the rest of the env from root.
su -p $USERNAME -c "HOME=/home/$USERNAME USER=$USERNAME LOGNAME=$USERNAME $SHELL -c 'source /steps/runsteps.sh'"
# Switch to the host user so we can create files with the correct ownership
su $USERNAME -c "$SHELL -c 'source /steps/runsteps.sh'"
else
echo "Running as root"
+2 -17
View File
@@ -127,18 +127,11 @@ describe('BuildParameters', () => {
${Platform.types.StandaloneLinux64} | ${''} | ${'n/a'} | ${true}
`(
'appends $expectedExtension for $targetPlatform with linux64RemoveExecutableExtension=$linux64RemoveExecutableExtension',
async ({
targetPlatform,
expectedExtension,
androidExportType,
linux64RemoveExecutableExtension,
}) => {
async ({ targetPlatform, expectedExtension, androidExportType, linux64RemoveExecutableExtension }) => {
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
vi.spyOn(Input, 'androidExportType', 'get').mockReturnValue(androidExportType);
vi.spyOn(Input, 'linux64RemoveExecutableExtension', 'get').mockReturnValue(
linux64RemoveExecutableExtension,
);
vi.spyOn(Input, 'linux64RemoveExecutableExtension', 'get').mockReturnValue(linux64RemoveExecutableExtension);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: `${targetPlatform}${expectedExtension}` }),
);
@@ -228,14 +221,6 @@ describe('BuildParameters', () => {
);
});
it('returns the unity licensing toolset', async () => {
const mockValue = 'LicenseServer_1234567890_3';
vi.spyOn(Input, 'unityLicensingToolset', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ unityLicensingToolset: mockValue }),
);
});
it('throws error when no unity license provider provided', async () => {
delete process.env.UNITY_LICENSE; // Need to delete this as it is set for every test currently
await expect(BuildParameters.create()).rejects.toThrowError();
+2 -2
View File
@@ -18,7 +18,6 @@ class BuildParameters {
public customImage!: string;
public unitySerial!: string;
public unityLicensingServer!: string;
public unityLicensingToolset!: string;
public skipActivation!: string;
public runnerTempPath!: string;
public targetPlatform!: string;
@@ -43,6 +42,7 @@ class BuildParameters {
public androidSymbolType!: string;
public dockerCpuLimit!: string;
public dockerMemoryLimit!: string;
public dockerShmSize!: string;
public dockerIsolationMode!: string;
public containerRegistryRepository!: string;
public containerRegistryImageVersion!: string;
@@ -137,7 +137,6 @@ class BuildParameters {
customImage: Input.customImage,
unitySerial,
unityLicensingServer: Input.unityLicensingServer,
unityLicensingToolset: Input.unityLicensingToolset,
skipActivation: Input.skipActivation,
runnerTempPath: Input.runnerTempPath,
targetPlatform: Input.targetPlatform,
@@ -169,6 +168,7 @@ class BuildParameters {
chownFilesTo: Input.chownFilesTo,
dockerCpuLimit: Input.dockerCpuLimit,
dockerMemoryLimit: Input.dockerMemoryLimit,
dockerShmSize: Input.dockerShmSize,
dockerIsolationMode: Input.dockerIsolationMode,
containerRegistryRepository: Input.containerRegistryRepository,
containerRegistryImageVersion: Input.containerRegistryImageVersion,
+51
View File
@@ -14,4 +14,55 @@ describe('Docker', () => {
};
await Docker.run(image, parameters);
});
// game-ci/unity-builder#840: Unity 6.6+ editors request 1GiB of shared
// memory and hard-fail against Docker's 64m default. This action never
// passed --shm-size at all, and exposed no input to work around it.
describe('--shm-size', () => {
const baseParameters = {
workspace: '/github/workspace',
actionFolder: '/action',
runnerTempPath: Action.rootFolder,
sshAgent: '',
sshPublicKeysDirectoryPath: '',
gitPrivateToken: '',
dockerWorkspacePath: '/github/workspace',
dockerCpuLimit: '4',
dockerMemoryLimit: '8192m',
dockerIsolationMode: 'default',
};
it('passes --shm-size on Linux when dockerShmSize is set', () => {
const command = Docker.getLinuxCommand('unityci/editor:latest', {
...baseParameters,
dockerShmSize: '1025m',
} as any);
expect(command).toContain('--shm-size=1025m');
});
it('passes --shm-size on Windows when dockerShmSize is set', () => {
const command = Docker.getWindowsCommand('unityci/editor:latest', {
...baseParameters,
dockerShmSize: '2g',
} as any);
expect(command).toContain('--shm-size=2g');
});
it('omits --shm-size when explicitly disabled with "0"', () => {
const command = Docker.getLinuxCommand('unityci/editor:latest', {
...baseParameters,
dockerShmSize: '0',
} as any);
expect(command).not.toContain('--shm-size');
});
it('omits --shm-size when unset, rather than emitting "--shm-size=undefined"', () => {
const command = Docker.getLinuxCommand('unityci/editor:latest', baseParameters as any);
expect(command).not.toContain('--shm-size');
});
});
});
+18
View File
@@ -4,6 +4,20 @@ import path from 'node:path';
import { ExecOptions, exec } from '@actions/exec';
import { DockerParameters, StringKeyValuePair } from './shared-types';
/**
* Unity 6.6+ editors request 1GiB of shared memory and hard-fail with
* "Insufficient shared memory available" against Docker's 64m default
* (game-ci/unity-builder#840). BuildParameters defaults this to 1025m, the
* value unity-test-runner has always passed. '0'/'none' omits the flag so
* Docker's own default applies.
*/
function shmSizeFlag(dockerShmSize?: string): string {
const value = String(dockerShmSize ?? '').trim();
if (value === '' || value === '0' || value.toLowerCase() === 'none') return '';
return `--shm-size=${value}`;
}
class Docker {
static async run(
image: string,
@@ -56,6 +70,7 @@ class Docker {
dockerWorkspacePath,
dockerCpuLimit,
dockerMemoryLimit,
dockerShmSize,
} = parameters;
const githubHome = path.join(runnerTempPath, '_github_home');
@@ -85,6 +100,7 @@ class Docker {
--volume "${actionFolder}/BlankProject":"/BlankProject:z" \
--cpus=${dockerCpuLimit} \
--memory=${dockerMemoryLimit} \
${shmSizeFlag(dockerShmSize)} \
${sshAgent ? `--volume ${sshAgent}:/ssh-agent` : ''} \
${
sshAgent && !sshPublicKeysDirectoryPath
@@ -108,6 +124,7 @@ class Docker {
dockerWorkspacePath,
dockerCpuLimit,
dockerMemoryLimit,
dockerShmSize,
dockerIsolationMode,
} = parameters;
@@ -134,6 +151,7 @@ class Docker {
--volume "${actionFolder}/BlankProject":"c:/BlankProject" \
--cpus=${dockerCpuLimit} \
--memory=${dockerMemoryLimit} \
${shmSizeFlag(dockerShmSize)} \
--isolation=${dockerIsolationMode} \
${image} \
powershell c:/steps/entrypoint.ps1`;
+11 -4
View File
@@ -122,10 +122,6 @@ class Input {
return Input.getInput('unityLicensingServer') ?? '';
}
static get unityLicensingToolset(): string {
return Input.getInput('unityLicensingToolset') ?? '';
}
static get buildMethod(): string {
return Input.getInput('buildMethod') ?? ''; // Processed in docker file
}
@@ -246,6 +242,17 @@ class Input {
return Input.getInput('dockerWorkspacePath') ?? '/github/workspace';
}
/**
* Unity 6.6+ editors request 1GiB of shared memory and hard-fail with
* "Insufficient shared memory available" against Docker's 64m default
* (game-ci/unity-builder#840). unity-test-runner has always passed 1025m,
* so match it here rather than leaving builds broken by default. '0' or
* 'none' omits the flag and uses Docker's own default.
*/
static get dockerShmSize(): string {
return Input.getInput('dockerShmSize') ?? '1025m';
}
static get dockerCpuLimit(): string {
return Input.getInput('dockerCpuLimit') ?? os.cpus().length.toString();
}
-7
View File
@@ -32,13 +32,6 @@ class PlatformSetup {
let servicesConfig = fs.readFileSync(servicesConfigPathTemplate).toString();
servicesConfig = servicesConfig.replace('%URL%', buildParameters.unityLicensingServer);
if (buildParameters.unityLicensingToolset) {
const parsed = JSON.parse(servicesConfig);
parsed.toolset = buildParameters.unityLicensingToolset;
servicesConfig = JSON.stringify(parsed, undefined, 2);
}
fs.writeFileSync(servicesConfigPath, servicesConfig);
SetupAndroid.setup(buildParameters);