Skip to content

Commit 8822077

Browse files
authored
fix: fixed macOS action cache architecture mismatch (#317)
1 parent 7378759 commit 8822077

File tree

7 files changed

+145
-95
lines changed

7 files changed

+145
-95
lines changed

.github/workflows/main.yml

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,22 +157,25 @@ jobs:
157157
development: [false, true]
158158
include:
159159
- os: macos-latest
160-
swift: '5.0.0'
160+
swift: '5.0.0' # oldest
161161
development: false
162162
- os: macos-13
163-
swift: '5.9'
163+
swift: 'latest' # action caching architecture mismatch
164+
development: false
165+
- os: macos-13
166+
swift: '5.9' # Xcode toolchain inclusion optimization
164167
development: false
165168
- os: windows-latest
166-
swift: '5.9'
169+
swift: '5.9' # 2nd installation approach
167170
development: false
168171
- os: ubuntu-latest
169-
swift: '5.3.0'
172+
swift: '5.3.0' # oldest
170173
development: false
171174
- os: windows-latest
172-
swift: '5.3'
175+
swift: '5.3' # 1st installation approach
173176
development: false
174177
- os: ubuntu-22.04
175-
swift: ${{ fromJSON(vars.SETUPSWIFT_CUSTOM_TOOLCHAINS).ubuntu2204 }}
178+
swift: ${{ fromJSON(vars.SETUPSWIFT_CUSTOM_TOOLCHAINS).ubuntu2204 }} # custom toolchain
176179
development: true
177180

178181
steps:
@@ -223,8 +226,10 @@ jobs:
223226
with:
224227
script: |
225228
const os = require('os');
229+
const path = require('path');
226230
const fs = require('fs/promises');
227231
const toolCache = require('@actions/tool-cache');
232+
const {exec: actionExec} = require('@actions/exec');
228233
let arch = '';
229234
switch (os.arch()) {
230235
case 'x64':
@@ -242,22 +247,25 @@ jobs:
242247
core.debug(`Toolcache not set`);
243248
return;
244249
}
245-
const tools = toolCache.findAllVersions(key, arch);
246-
if (!tools.length) {
247-
core.debug(`No tools found for "${key}" with arch ${arch}`);
250+
const versions = toolCache.findAllVersions(key, arch);
251+
if (!versions.length) {
252+
core.debug(`No versions found for "${key}" with arch ${arch}`);
248253
return;
249254
} else {
250-
core.debug(`Found tools "${tools.join(', ')}" for "${key}" with arch ${arch}`);
255+
core.debug(`Found versions "${versions.join(', ')}" for "${key}" with arch ${arch}`);
251256
}
252-
const tool = tools[0];
257+
const tool = toolCache.find(key, versions[0], arch).trim();
253258
await fs.access(tool);
254-
return { key: key, tool: tool };
259+
const tmpDir = process.env.RUNNER_TEMP || os.tmpdir();
260+
const zip = path.join(tmpDir, `${key}.zip`);
261+
await actionExec('zip', ['-r', zip, tool, '-x', '*.DS_Store']);
262+
return { key: key, tool: zip };
255263
256264
- name: Upload cached installation as artifact
257265
if: always() && steps.get-tool.outputs.result != ''
258266
uses: actions/upload-artifact@v4
259267
with:
260-
name: ${{ fromJson(steps.get-tool.outputs.result).key }}-tool
268+
name: ${{ fromJson(steps.get-tool.outputs.result).key }}-${{ matrix.os }}-tool
261269
path: ${{ fromJson(steps.get-tool.outputs.result).tool }}
262270

263271
dry-run:

__tests__/installer/linux.test.ts

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -71,30 +71,48 @@ describe('linux toolchain installation verification', () => {
7171
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
7272
})
7373

74-
it('tests installation with download', async () => {
75-
const installer = new LinuxToolchainInstaller(toolchain)
76-
const download = path.resolve('tool', 'download', 'path')
77-
const extracted = path.resolve('tool', 'extracted', 'path')
78-
const cached = path.resolve('tool', 'cached', 'path')
79-
const swiftPath = path.join(cached, 'usr', 'bin')
80-
jest.spyOn(core, 'getBooleanInput').mockReturnValue(true)
81-
jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined)
82-
jest.spyOn(cache, 'saveCache').mockResolvedValue(1)
83-
jest.spyOn(toolCache, 'find').mockReturnValue('')
84-
jest.spyOn(fs, 'cp').mockResolvedValue()
85-
const downloadSpy = jest.spyOn(toolCache, 'downloadTool')
86-
downloadSpy.mockResolvedValue(download)
87-
const extractSpy = jest.spyOn(toolCache, 'extractTar')
88-
extractSpy.mockResolvedValue(extracted)
89-
const cacheSpy = jest.spyOn(toolCache, 'cacheDir')
90-
cacheSpy.mockResolvedValue(cached)
91-
jest.spyOn(exec, 'exec').mockResolvedValue(0)
92-
await installer.install()
93-
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
94-
for (const spy of [downloadSpy, extractSpy, cacheSpy]) {
95-
expect(spy).toHaveBeenCalled()
74+
it.each(['aarch64', 'x86_64'])(
75+
'tests installation with download for arch %s',
76+
async arch => {
77+
const installer = new LinuxToolchainInstaller(toolchain)
78+
const download = path.resolve('tool', 'download', 'path')
79+
const extracted = path.resolve('tool', 'extracted', 'path')
80+
const cached = path.resolve('tool', 'cached', 'path')
81+
const swiftPath = path.join(cached, 'usr', 'bin')
82+
jest.spyOn(core, 'getBooleanInput').mockReturnValue(true)
83+
jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined)
84+
jest.spyOn(toolCache, 'find').mockReturnValue('')
85+
jest.spyOn(fs, 'cp').mockResolvedValue()
86+
const downloadSpy = jest.spyOn(toolCache, 'downloadTool')
87+
downloadSpy.mockResolvedValue(download)
88+
const extractSpy = jest.spyOn(toolCache, 'extractTar')
89+
extractSpy.mockResolvedValue(extracted)
90+
const toolCacheSpy = jest.spyOn(toolCache, 'cacheDir')
91+
toolCacheSpy.mockResolvedValue(cached)
92+
const actionCacheSpy = jest.spyOn(cache, 'saveCache')
93+
actionCacheSpy.mockResolvedValue(1)
94+
jest.spyOn(exec, 'exec').mockResolvedValue(0)
95+
await installer.install(arch)
96+
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
97+
for (const spy of [
98+
downloadSpy,
99+
extractSpy,
100+
toolCacheSpy,
101+
actionCacheSpy
102+
]) {
103+
expect(spy).toHaveBeenCalled()
104+
}
105+
const toolCacheKey = `${toolchain.dir}-${toolchain.platform}`
106+
const actionCacheKey = `${toolCacheKey}-${arch}`
107+
const toolDir = path.basename(toolchain.download, '.tar.gz')
108+
const cachedTool = path.join(extracted, toolDir)
109+
expect(toolCacheSpy.mock.calls[0]?.[0]).toBe(cachedTool)
110+
expect(toolCacheSpy.mock.calls[0]?.[1]).toBe(toolCacheKey)
111+
expect(toolCacheSpy.mock.calls[0]?.[2]).toBe('5.8.0')
112+
expect(toolCacheSpy.mock.calls[0]?.[3]).toBe(arch)
113+
expect(actionCacheSpy.mock.calls[0]?.[1]).toBe(actionCacheKey)
96114
}
97-
})
115+
)
98116

99117
it('tests installation with cache', async () => {
100118
const installer = new LinuxToolchainInstaller(toolchain)
@@ -107,7 +125,7 @@ describe('linux toolchain installation verification', () => {
107125
jest.spyOn(exec, 'exec').mockResolvedValue(0)
108126
const downloadSpy = jest.spyOn(toolCache, 'downloadTool')
109127
const extractSpy = jest.spyOn(toolCache, 'extractTar')
110-
await installer.install()
128+
await installer.install('aarch64')
111129
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
112130
for (const spy of [downloadSpy, extractSpy]) {
113131
expect(spy).not.toHaveBeenCalled()

__tests__/installer/windows.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ describe('windows toolchain installation verification', () => {
255255
stdout: vsEnvs.join(os.EOL),
256256
stderr: ''
257257
})
258-
await installer.install()
258+
await installer.install('x86_64')
259259
expect(setupSpy).toHaveBeenCalled()
260260
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
261261
expect(process.env.PATH?.includes(swiftDev)).toBeTruthy()

__tests__/installer/xcode.test.ts

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ describe('macOS toolchain installation verification', () => {
4646
const installationNeededSpy = jest.spyOn(installer, 'isInstallationNeeded')
4747
const downloadSpy = jest.spyOn(toolCache, 'downloadTool')
4848
const extractSpy = jest.spyOn(toolCache, 'extractXar')
49-
await installer.install()
49+
await installer.install('x86_64')
50+
await installer.install('aarch64')
5051
for (const spy of [downloadSpy, extractSpy]) {
5152
expect(spy).not.toHaveBeenCalled()
5253
}
@@ -96,39 +97,58 @@ describe('macOS toolchain installation verification', () => {
9697
expect(process.env.TOOLCHAINS).toBe(identifier)
9798
})
9899

99-
it('tests installation with download', async () => {
100-
const installer = new XcodeToolchainInstaller(toolchain)
101-
const download = path.resolve('tool', 'download', 'path')
102-
const extracted = path.resolve('tool', 'extracted', 'path')
103-
const deployed = path.resolve('tool', 'deployed', 'path')
104-
const cached = path.resolve('tool', 'cached', 'path')
105-
const swiftPath = path.join(cached, 'usr', 'bin')
106-
const identifier = 'org.swift.581202305171a'
107-
jest.spyOn(installer, 'isInstallationNeeded').mockResolvedValue(true)
108-
jest.spyOn(core, 'getBooleanInput').mockReturnValue(true)
109-
jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined)
110-
jest.spyOn(cache, 'saveCache').mockResolvedValue(1)
111-
jest.spyOn(toolCache, 'find').mockReturnValue('')
112-
jest.spyOn(exec, 'exec').mockResolvedValue(0)
113-
jest.spyOn(fs, 'cp').mockResolvedValue()
114-
const downloadSpy = jest.spyOn(toolCache, 'downloadTool')
115-
downloadSpy.mockResolvedValue(download)
116-
const extractSpy = jest.spyOn(toolCache, 'extractXar')
117-
extractSpy.mockResolvedValue(extracted)
118-
const deploySpy = jest.spyOn(toolCache, 'extractTar')
119-
deploySpy.mockResolvedValue(deployed)
120-
const cacheSpy = jest.spyOn(toolCache, 'cacheDir')
121-
cacheSpy.mockResolvedValue(cached)
122-
jest.spyOn(fs, 'access').mockResolvedValue()
123-
jest.spyOn(fs, 'readFile').mockResolvedValue('')
124-
jest.spyOn(plist, 'parse').mockReturnValue({CFBundleIdentifier: identifier})
125-
await installer.install()
126-
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
127-
expect(process.env.TOOLCHAINS).toBe(identifier)
128-
for (const spy of [downloadSpy, extractSpy, deploySpy, cacheSpy]) {
129-
expect(spy).toHaveBeenCalled()
100+
it.each(['aarch64', 'x86_64'])(
101+
'tests installation with download for %s',
102+
async arch => {
103+
const installer = new XcodeToolchainInstaller(toolchain)
104+
const download = path.resolve('tool', 'download', 'path')
105+
const extracted = path.resolve('tool', 'extracted', 'path')
106+
const deployed = path.resolve('tool', 'deployed', 'path')
107+
const cached = path.resolve('tool', 'cached', 'path')
108+
const swiftPath = path.join(cached, 'usr', 'bin')
109+
const identifier = 'org.swift.581202305171a'
110+
jest.spyOn(installer, 'isInstallationNeeded').mockResolvedValue(true)
111+
jest.spyOn(core, 'getBooleanInput').mockReturnValue(true)
112+
jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined)
113+
jest.spyOn(toolCache, 'find').mockReturnValue('')
114+
jest.spyOn(exec, 'exec').mockResolvedValue(0)
115+
jest.spyOn(fs, 'cp').mockResolvedValue()
116+
const downloadSpy = jest.spyOn(toolCache, 'downloadTool')
117+
downloadSpy.mockResolvedValue(download)
118+
const extractSpy = jest.spyOn(toolCache, 'extractXar')
119+
extractSpy.mockResolvedValue(extracted)
120+
const deploySpy = jest.spyOn(toolCache, 'extractTar')
121+
deploySpy.mockResolvedValue(deployed)
122+
const toolCacheSpy = jest.spyOn(toolCache, 'cacheDir')
123+
toolCacheSpy.mockResolvedValue(cached)
124+
const actionCacheSpy = jest.spyOn(cache, 'saveCache')
125+
actionCacheSpy.mockResolvedValue(1)
126+
jest.spyOn(fs, 'access').mockResolvedValue()
127+
jest.spyOn(fs, 'readFile').mockResolvedValue('')
128+
jest
129+
.spyOn(plist, 'parse')
130+
.mockReturnValue({CFBundleIdentifier: identifier})
131+
await installer.install(arch)
132+
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
133+
expect(process.env.TOOLCHAINS).toBe(identifier)
134+
for (const spy of [
135+
downloadSpy,
136+
extractSpy,
137+
deploySpy,
138+
toolCacheSpy,
139+
actionCacheSpy
140+
]) {
141+
expect(spy).toHaveBeenCalled()
142+
}
143+
const toolCacheKey = `${toolchain.dir}-${toolchain.platform}`
144+
const actionCacheKey = `${toolCacheKey}-${arch}`
145+
expect(toolCacheSpy.mock.calls[0]?.[0]).toBe(deployed)
146+
expect(toolCacheSpy.mock.calls[0]?.[1]).toBe(toolCacheKey)
147+
expect(toolCacheSpy.mock.calls[0]?.[2]).toBe('5.8.1')
148+
expect(toolCacheSpy.mock.calls[0]?.[3]).toBe(arch)
149+
expect(actionCacheSpy.mock.calls[0]?.[1]).toBe(actionCacheKey)
130150
}
131-
})
151+
)
132152

133153
it('tests installation with cache', async () => {
134154
const installer = new XcodeToolchainInstaller(toolchain)
@@ -152,7 +172,7 @@ describe('macOS toolchain installation verification', () => {
152172
jest.spyOn(fs, 'access').mockResolvedValue()
153173
jest.spyOn(fs, 'readFile').mockResolvedValue('')
154174
jest.spyOn(plist, 'parse').mockReturnValue({CFBundleIdentifier: identifier})
155-
await installer.install()
175+
await installer.install('aarch64')
156176
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
157177
expect(process.env.TOOLCHAINS).toBe(identifier)
158178
for (const spy of [downloadSpy, extractSpy, deploySpy]) {
@@ -170,7 +190,7 @@ describe('macOS toolchain installation verification', () => {
170190
stdout: `swift-driver version: 1.75.2 Apple Swift version 5.8.1 (swiftlang-5.8.0.124.5 clang-1403.0.22.11.100)\nTarget: arm64-apple-macosx13.0`,
171191
stderr: ''
172192
})
173-
await expect(installer.install()).resolves
193+
await expect(installer.install('aarch64')).resolves
174194
expect(process.env.DEVELOPER_DIR).toBe(toolchain.xcodePath)
175195
})
176196

dist/index.js

Lines changed: 11 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/installer/base.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,26 @@ export abstract class ToolchainInstaller<Snapshot extends ToolchainSnapshot> {
3939
}
4040
}
4141

42-
async install(arch?: string) {
43-
const key = `${this.data.dir}-${this.data.platform}`
42+
async install(arch: string) {
43+
const toolCacheKey = `${this.data.dir}-${this.data.platform}`
44+
const actionCacheKey = arch ? `${toolCacheKey}-${arch}` : toolCacheKey
4445
const version = this.version?.raw
4546
let tool: string | undefined
4647
let cacheHit = false
4748
if (version) {
4849
core.debug(
49-
`Finding tool with key: "${key}", version: "${version}" and arch: "${arch}" in tool cache`
50+
`Finding tool with key: "${toolCacheKey}", version: "${version}" and arch: "${arch}" in tool cache`
5051
)
51-
tool = toolCache.find(key, version, arch).trim()
52+
tool = toolCache.find(toolCacheKey, version, arch).trim()
5253
}
5354

5455
const tmpDir = process.env.RUNNER_TEMP || os.tmpdir()
55-
const restore = path.join(tmpDir, 'setup-swift', key)
56+
const restore = path.join(tmpDir, 'setup-swift', toolCacheKey)
5657
if (!tool?.length) {
57-
if (await cache.restoreCache([restore], key)) {
58-
core.debug(`Restored snapshot at "${restore}" from key "${key}"`)
58+
if (await cache.restoreCache([restore], actionCacheKey)) {
59+
core.debug(
60+
`Restored snapshot at "${restore}" from key "${actionCacheKey}"`
61+
)
5962
tool = restore
6063
cacheHit = true
6164
} else {
@@ -70,9 +73,9 @@ export abstract class ToolchainInstaller<Snapshot extends ToolchainSnapshot> {
7073
}
7174

7275
if (tool && version) {
73-
tool = await toolCache.cacheDir(tool, key, version, arch)
76+
tool = await toolCache.cacheDir(tool, toolCacheKey, version, arch)
7477
if (core.isDebug()) {
75-
core.exportVariable('SWIFT_SETUP_TOOL_KEY', key)
78+
core.exportVariable('SWIFT_SETUP_TOOL_KEY', toolCacheKey)
7679
}
7780
core.debug(`Added to tool cache at "${tool}"`)
7881
}
@@ -83,8 +86,8 @@ export abstract class ToolchainInstaller<Snapshot extends ToolchainSnapshot> {
8386
!this.data.preventCaching
8487
) {
8588
await fs.cp(tool, restore, {recursive: true})
86-
await cache.saveCache([restore], key)
87-
core.debug(`Saved to cache with key "${key}"`)
89+
await cache.saveCache([restore], actionCacheKey)
90+
core.debug(`Saved to cache with key "${actionCacheKey}"`)
8891
}
8992
await this.add(tool)
9093
}

0 commit comments

Comments
 (0)