diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bb99481c..02e7d47b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -157,22 +157,25 @@ jobs: development: [false, true] include: - os: macos-latest - swift: '5.0.0' + swift: '5.0.0' # oldest development: false - os: macos-13 - swift: '5.9' + swift: 'latest' # action caching architecture mismatch + development: false + - os: macos-13 + swift: '5.9' # Xcode toolchain inclusion optimization development: false - os: windows-latest - swift: '5.9' + swift: '5.9' # 2nd installation approach development: false - os: ubuntu-latest - swift: '5.3.0' + swift: '5.3.0' # oldest development: false - os: windows-latest - swift: '5.3' + swift: '5.3' # 1st installation approach development: false - os: ubuntu-22.04 - swift: ${{ fromJSON(vars.SETUPSWIFT_CUSTOM_TOOLCHAINS).ubuntu2204 }} + swift: ${{ fromJSON(vars.SETUPSWIFT_CUSTOM_TOOLCHAINS).ubuntu2204 }} # custom toolchain development: true steps: @@ -223,8 +226,10 @@ jobs: with: script: | const os = require('os'); + const path = require('path'); const fs = require('fs/promises'); const toolCache = require('@actions/tool-cache'); + const {exec: actionExec} = require('@actions/exec'); let arch = ''; switch (os.arch()) { case 'x64': @@ -242,22 +247,25 @@ jobs: core.debug(`Toolcache not set`); return; } - const tools = toolCache.findAllVersions(key, arch); - if (!tools.length) { - core.debug(`No tools found for "${key}" with arch ${arch}`); + const versions = toolCache.findAllVersions(key, arch); + if (!versions.length) { + core.debug(`No versions found for "${key}" with arch ${arch}`); return; } else { - core.debug(`Found tools "${tools.join(', ')}" for "${key}" with arch ${arch}`); + core.debug(`Found versions "${versions.join(', ')}" for "${key}" with arch ${arch}`); } - const tool = tools[0]; + const tool = toolCache.find(key, versions[0], arch).trim(); await fs.access(tool); - return { key: key, tool: tool }; + const tmpDir = process.env.RUNNER_TEMP || os.tmpdir(); + const zip = path.join(tmpDir, `${key}.zip`); + await actionExec('zip', ['-r', zip, tool, '-x', '*.DS_Store']); + return { key: key, tool: zip }; - name: Upload cached installation as artifact if: always() && steps.get-tool.outputs.result != '' uses: actions/upload-artifact@v4 with: - name: ${{ fromJson(steps.get-tool.outputs.result).key }}-tool + name: ${{ fromJson(steps.get-tool.outputs.result).key }}-${{ matrix.os }}-tool path: ${{ fromJson(steps.get-tool.outputs.result).tool }} dry-run: diff --git a/__tests__/installer/linux.test.ts b/__tests__/installer/linux.test.ts index cb07e2c2..18671e5f 100644 --- a/__tests__/installer/linux.test.ts +++ b/__tests__/installer/linux.test.ts @@ -71,30 +71,48 @@ describe('linux toolchain installation verification', () => { expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() }) - it('tests installation with download', async () => { - const installer = new LinuxToolchainInstaller(toolchain) - const download = path.resolve('tool', 'download', 'path') - const extracted = path.resolve('tool', 'extracted', 'path') - const cached = path.resolve('tool', 'cached', 'path') - const swiftPath = path.join(cached, 'usr', 'bin') - jest.spyOn(core, 'getBooleanInput').mockReturnValue(true) - jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined) - jest.spyOn(cache, 'saveCache').mockResolvedValue(1) - jest.spyOn(toolCache, 'find').mockReturnValue('') - jest.spyOn(fs, 'cp').mockResolvedValue() - const downloadSpy = jest.spyOn(toolCache, 'downloadTool') - downloadSpy.mockResolvedValue(download) - const extractSpy = jest.spyOn(toolCache, 'extractTar') - extractSpy.mockResolvedValue(extracted) - const cacheSpy = jest.spyOn(toolCache, 'cacheDir') - cacheSpy.mockResolvedValue(cached) - jest.spyOn(exec, 'exec').mockResolvedValue(0) - await installer.install() - expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() - for (const spy of [downloadSpy, extractSpy, cacheSpy]) { - expect(spy).toHaveBeenCalled() + it.each(['aarch64', 'x86_64'])( + 'tests installation with download for arch %s', + async arch => { + const installer = new LinuxToolchainInstaller(toolchain) + const download = path.resolve('tool', 'download', 'path') + const extracted = path.resolve('tool', 'extracted', 'path') + const cached = path.resolve('tool', 'cached', 'path') + const swiftPath = path.join(cached, 'usr', 'bin') + jest.spyOn(core, 'getBooleanInput').mockReturnValue(true) + jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined) + jest.spyOn(toolCache, 'find').mockReturnValue('') + jest.spyOn(fs, 'cp').mockResolvedValue() + const downloadSpy = jest.spyOn(toolCache, 'downloadTool') + downloadSpy.mockResolvedValue(download) + const extractSpy = jest.spyOn(toolCache, 'extractTar') + extractSpy.mockResolvedValue(extracted) + const toolCacheSpy = jest.spyOn(toolCache, 'cacheDir') + toolCacheSpy.mockResolvedValue(cached) + const actionCacheSpy = jest.spyOn(cache, 'saveCache') + actionCacheSpy.mockResolvedValue(1) + jest.spyOn(exec, 'exec').mockResolvedValue(0) + await installer.install(arch) + expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() + for (const spy of [ + downloadSpy, + extractSpy, + toolCacheSpy, + actionCacheSpy + ]) { + expect(spy).toHaveBeenCalled() + } + const toolCacheKey = `${toolchain.dir}-${toolchain.platform}` + const actionCacheKey = `${toolCacheKey}-${arch}` + const toolDir = path.basename(toolchain.download, '.tar.gz') + const cachedTool = path.join(extracted, toolDir) + expect(toolCacheSpy.mock.calls[0]?.[0]).toBe(cachedTool) + expect(toolCacheSpy.mock.calls[0]?.[1]).toBe(toolCacheKey) + expect(toolCacheSpy.mock.calls[0]?.[2]).toBe('5.8.0') + expect(toolCacheSpy.mock.calls[0]?.[3]).toBe(arch) + expect(actionCacheSpy.mock.calls[0]?.[1]).toBe(actionCacheKey) } - }) + ) it('tests installation with cache', async () => { const installer = new LinuxToolchainInstaller(toolchain) @@ -107,7 +125,7 @@ describe('linux toolchain installation verification', () => { jest.spyOn(exec, 'exec').mockResolvedValue(0) const downloadSpy = jest.spyOn(toolCache, 'downloadTool') const extractSpy = jest.spyOn(toolCache, 'extractTar') - await installer.install() + await installer.install('aarch64') expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() for (const spy of [downloadSpy, extractSpy]) { expect(spy).not.toHaveBeenCalled() diff --git a/__tests__/installer/windows.test.ts b/__tests__/installer/windows.test.ts index fd71c659..1b8f636d 100644 --- a/__tests__/installer/windows.test.ts +++ b/__tests__/installer/windows.test.ts @@ -255,7 +255,7 @@ describe('windows toolchain installation verification', () => { stdout: vsEnvs.join(os.EOL), stderr: '' }) - await installer.install() + await installer.install('x86_64') expect(setupSpy).toHaveBeenCalled() expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() expect(process.env.PATH?.includes(swiftDev)).toBeTruthy() diff --git a/__tests__/installer/xcode.test.ts b/__tests__/installer/xcode.test.ts index 885f9842..feddd2fe 100644 --- a/__tests__/installer/xcode.test.ts +++ b/__tests__/installer/xcode.test.ts @@ -46,7 +46,8 @@ describe('macOS toolchain installation verification', () => { const installationNeededSpy = jest.spyOn(installer, 'isInstallationNeeded') const downloadSpy = jest.spyOn(toolCache, 'downloadTool') const extractSpy = jest.spyOn(toolCache, 'extractXar') - await installer.install() + await installer.install('x86_64') + await installer.install('aarch64') for (const spy of [downloadSpy, extractSpy]) { expect(spy).not.toHaveBeenCalled() } @@ -96,39 +97,58 @@ describe('macOS toolchain installation verification', () => { expect(process.env.TOOLCHAINS).toBe(identifier) }) - it('tests installation with download', async () => { - const installer = new XcodeToolchainInstaller(toolchain) - const download = path.resolve('tool', 'download', 'path') - const extracted = path.resolve('tool', 'extracted', 'path') - const deployed = path.resolve('tool', 'deployed', 'path') - const cached = path.resolve('tool', 'cached', 'path') - const swiftPath = path.join(cached, 'usr', 'bin') - const identifier = 'org.swift.581202305171a' - jest.spyOn(installer, 'isInstallationNeeded').mockResolvedValue(true) - jest.spyOn(core, 'getBooleanInput').mockReturnValue(true) - jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined) - jest.spyOn(cache, 'saveCache').mockResolvedValue(1) - jest.spyOn(toolCache, 'find').mockReturnValue('') - jest.spyOn(exec, 'exec').mockResolvedValue(0) - jest.spyOn(fs, 'cp').mockResolvedValue() - const downloadSpy = jest.spyOn(toolCache, 'downloadTool') - downloadSpy.mockResolvedValue(download) - const extractSpy = jest.spyOn(toolCache, 'extractXar') - extractSpy.mockResolvedValue(extracted) - const deploySpy = jest.spyOn(toolCache, 'extractTar') - deploySpy.mockResolvedValue(deployed) - const cacheSpy = jest.spyOn(toolCache, 'cacheDir') - cacheSpy.mockResolvedValue(cached) - jest.spyOn(fs, 'access').mockResolvedValue() - jest.spyOn(fs, 'readFile').mockResolvedValue('') - jest.spyOn(plist, 'parse').mockReturnValue({CFBundleIdentifier: identifier}) - await installer.install() - expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() - expect(process.env.TOOLCHAINS).toBe(identifier) - for (const spy of [downloadSpy, extractSpy, deploySpy, cacheSpy]) { - expect(spy).toHaveBeenCalled() + it.each(['aarch64', 'x86_64'])( + 'tests installation with download for %s', + async arch => { + const installer = new XcodeToolchainInstaller(toolchain) + const download = path.resolve('tool', 'download', 'path') + const extracted = path.resolve('tool', 'extracted', 'path') + const deployed = path.resolve('tool', 'deployed', 'path') + const cached = path.resolve('tool', 'cached', 'path') + const swiftPath = path.join(cached, 'usr', 'bin') + const identifier = 'org.swift.581202305171a' + jest.spyOn(installer, 'isInstallationNeeded').mockResolvedValue(true) + jest.spyOn(core, 'getBooleanInput').mockReturnValue(true) + jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined) + jest.spyOn(toolCache, 'find').mockReturnValue('') + jest.spyOn(exec, 'exec').mockResolvedValue(0) + jest.spyOn(fs, 'cp').mockResolvedValue() + const downloadSpy = jest.spyOn(toolCache, 'downloadTool') + downloadSpy.mockResolvedValue(download) + const extractSpy = jest.spyOn(toolCache, 'extractXar') + extractSpy.mockResolvedValue(extracted) + const deploySpy = jest.spyOn(toolCache, 'extractTar') + deploySpy.mockResolvedValue(deployed) + const toolCacheSpy = jest.spyOn(toolCache, 'cacheDir') + toolCacheSpy.mockResolvedValue(cached) + const actionCacheSpy = jest.spyOn(cache, 'saveCache') + actionCacheSpy.mockResolvedValue(1) + jest.spyOn(fs, 'access').mockResolvedValue() + jest.spyOn(fs, 'readFile').mockResolvedValue('') + jest + .spyOn(plist, 'parse') + .mockReturnValue({CFBundleIdentifier: identifier}) + await installer.install(arch) + expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() + expect(process.env.TOOLCHAINS).toBe(identifier) + for (const spy of [ + downloadSpy, + extractSpy, + deploySpy, + toolCacheSpy, + actionCacheSpy + ]) { + expect(spy).toHaveBeenCalled() + } + const toolCacheKey = `${toolchain.dir}-${toolchain.platform}` + const actionCacheKey = `${toolCacheKey}-${arch}` + expect(toolCacheSpy.mock.calls[0]?.[0]).toBe(deployed) + expect(toolCacheSpy.mock.calls[0]?.[1]).toBe(toolCacheKey) + expect(toolCacheSpy.mock.calls[0]?.[2]).toBe('5.8.1') + expect(toolCacheSpy.mock.calls[0]?.[3]).toBe(arch) + expect(actionCacheSpy.mock.calls[0]?.[1]).toBe(actionCacheKey) } - }) + ) it('tests installation with cache', async () => { const installer = new XcodeToolchainInstaller(toolchain) @@ -152,7 +172,7 @@ describe('macOS toolchain installation verification', () => { jest.spyOn(fs, 'access').mockResolvedValue() jest.spyOn(fs, 'readFile').mockResolvedValue('') jest.spyOn(plist, 'parse').mockReturnValue({CFBundleIdentifier: identifier}) - await installer.install() + await installer.install('aarch64') expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() expect(process.env.TOOLCHAINS).toBe(identifier) for (const spy of [downloadSpy, extractSpy, deploySpy]) { @@ -170,7 +190,7 @@ describe('macOS toolchain installation verification', () => { 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`, stderr: '' }) - await expect(installer.install()).resolves + await expect(installer.install('aarch64')).resolves expect(process.env.DEVELOPER_DIR).toBe(toolchain.xcodePath) }) diff --git a/dist/index.js b/dist/index.js index 11c7e07f..aeccfbe2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -113,19 +113,20 @@ class ToolchainInstaller { install(arch) { return __awaiter(this, void 0, void 0, function* () { var _a; - const key = `${this.data.dir}-${this.data.platform}`; + const toolCacheKey = `${this.data.dir}-${this.data.platform}`; + const actionCacheKey = arch ? `${toolCacheKey}-${arch}` : toolCacheKey; const version = (_a = this.version) === null || _a === void 0 ? void 0 : _a.raw; let tool; let cacheHit = false; if (version) { - core.debug(`Finding tool with key: "${key}", version: "${version}" and arch: "${arch}" in tool cache`); - tool = toolCache.find(key, version, arch).trim(); + core.debug(`Finding tool with key: "${toolCacheKey}", version: "${version}" and arch: "${arch}" in tool cache`); + tool = toolCache.find(toolCacheKey, version, arch).trim(); } const tmpDir = process.env.RUNNER_TEMP || os.tmpdir(); - const restore = path.join(tmpDir, 'setup-swift', key); + const restore = path.join(tmpDir, 'setup-swift', toolCacheKey); if (!(tool === null || tool === void 0 ? void 0 : tool.length)) { - if (yield cache.restoreCache([restore], key)) { - core.debug(`Restored snapshot at "${restore}" from key "${key}"`); + if (yield cache.restoreCache([restore], actionCacheKey)) { + core.debug(`Restored snapshot at "${restore}" from key "${actionCacheKey}"`); tool = restore; cacheHit = true; } @@ -141,9 +142,9 @@ class ToolchainInstaller { cacheHit = true; } if (tool && version) { - tool = yield toolCache.cacheDir(tool, key, version, arch); + tool = yield toolCache.cacheDir(tool, toolCacheKey, version, arch); if (core.isDebug()) { - core.exportVariable('SWIFT_SETUP_TOOL_KEY', key); + core.exportVariable('SWIFT_SETUP_TOOL_KEY', toolCacheKey); } core.debug(`Added to tool cache at "${tool}"`); } @@ -152,8 +153,8 @@ class ToolchainInstaller { !cacheHit && !this.data.preventCaching) { yield fs_1.promises.cp(tool, restore, { recursive: true }); - yield cache.saveCache([restore], key); - core.debug(`Saved to cache with key "${key}"`); + yield cache.saveCache([restore], actionCacheKey); + core.debug(`Saved to cache with key "${actionCacheKey}"`); } yield this.add(tool); }); diff --git a/src/installer/base.ts b/src/installer/base.ts index cabf60a7..5dcbb794 100644 --- a/src/installer/base.ts +++ b/src/installer/base.ts @@ -39,23 +39,26 @@ export abstract class ToolchainInstaller { } } - async install(arch?: string) { - const key = `${this.data.dir}-${this.data.platform}` + async install(arch: string) { + const toolCacheKey = `${this.data.dir}-${this.data.platform}` + const actionCacheKey = arch ? `${toolCacheKey}-${arch}` : toolCacheKey const version = this.version?.raw let tool: string | undefined let cacheHit = false if (version) { core.debug( - `Finding tool with key: "${key}", version: "${version}" and arch: "${arch}" in tool cache` + `Finding tool with key: "${toolCacheKey}", version: "${version}" and arch: "${arch}" in tool cache` ) - tool = toolCache.find(key, version, arch).trim() + tool = toolCache.find(toolCacheKey, version, arch).trim() } const tmpDir = process.env.RUNNER_TEMP || os.tmpdir() - const restore = path.join(tmpDir, 'setup-swift', key) + const restore = path.join(tmpDir, 'setup-swift', toolCacheKey) if (!tool?.length) { - if (await cache.restoreCache([restore], key)) { - core.debug(`Restored snapshot at "${restore}" from key "${key}"`) + if (await cache.restoreCache([restore], actionCacheKey)) { + core.debug( + `Restored snapshot at "${restore}" from key "${actionCacheKey}"` + ) tool = restore cacheHit = true } else { @@ -70,9 +73,9 @@ export abstract class ToolchainInstaller { } if (tool && version) { - tool = await toolCache.cacheDir(tool, key, version, arch) + tool = await toolCache.cacheDir(tool, toolCacheKey, version, arch) if (core.isDebug()) { - core.exportVariable('SWIFT_SETUP_TOOL_KEY', key) + core.exportVariable('SWIFT_SETUP_TOOL_KEY', toolCacheKey) } core.debug(`Added to tool cache at "${tool}"`) } @@ -83,8 +86,8 @@ export abstract class ToolchainInstaller { !this.data.preventCaching ) { await fs.cp(tool, restore, {recursive: true}) - await cache.saveCache([restore], key) - core.debug(`Saved to cache with key "${key}"`) + await cache.saveCache([restore], actionCacheKey) + core.debug(`Saved to cache with key "${actionCacheKey}"`) } await this.add(tool) } diff --git a/src/installer/xcode.ts b/src/installer/xcode.ts index 7e2026cb..1b769743 100644 --- a/src/installer/xcode.ts +++ b/src/installer/xcode.ts @@ -41,7 +41,7 @@ export class XcodeToolchainInstaller extends ToolchainInstaller