Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"image": "mcr.microsoft.com/devcontainers/universal:5-noble",
"customizations": {
"vscode": {
"extensions": [
Expand Down
37 changes: 33 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -345,14 +345,35 @@ jobs:
uses: ./
with:
check-latest: ${{ needs.ci.outputs.check_latest }}
sdks: static-linux;wasm
sdks: static-linux;wasm;android
dry-run: true

- name: Generate SDK install command
id: gen-sdk-install
uses: actions/github-script@v8
with:
script: |
const sdks = JSON.parse(process.env.SWIFT_SDK_SNAPSHOTS);
const commands = sdks.map(sdk => {
const url = new URL(`https://download.swift.org/${sdk.branch}/${sdk.platform}/${sdk.dir}/${sdk.download}`).href;
const args = ['swift', 'sdk', 'install', url];
if (sdk.checksum) {
args.push('--checksum', sdk.checksum);
}
return args.join(' ');
});
core.setOutput('command', commands.join(' && '));
env:
SWIFT_SDK_SNAPSHOTS: ${{ steps.setup-swift.outputs.sdks }}

- name: Verify Swift version
uses: addnab/docker-run-action@v3
with:
image: swift:${{ fromJSON(steps.setup-swift.outputs.toolchain).docker }}
run: swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1
shell: bash
run: |
swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1
${{ steps.gen-sdk-install.outputs.command }}

e2e-test:
name: End-to-end test latest Swift on ${{ matrix.os }}
Expand Down Expand Up @@ -400,7 +421,7 @@ jobs:
with: {
'swift-version': 'latest',
'check-latest': '${{ needs.ci.outputs.check_latest }}',
'sdks': '${{ runner.os }}' != 'Windows' ? 'static-linux;wasm' : ''
'sdks': '${{ runner.os }}' != 'Windows' ? 'static-linux;wasm;android' : ''
}
}
]
Expand Down Expand Up @@ -431,14 +452,22 @@ jobs:

- name: Verify Swift SDKs
if: runner.os != 'Windows'
run: swift sdk list | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_static-linux || exit 1
run: |
SWIFT_SDK_LIST=$(swift sdk list)
echo "$SWIFT_SDK_LIST" | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_static-linux || exit 1
echo "$SWIFT_SDK_LIST" | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_wasm || exit 1
echo "$SWIFT_SDK_LIST" | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_android || exit 1

- name: Test Swift package
run: |
swift package init --type library --name SetupLib
swift build --build-tests
swift test

- name: Test Swift package for Android
if: runner.os != 'Windows'
run: swift build --swift-sdk aarch64-unknown-linux-android28 --static-swift-stdlib

pages:
name: Publish metadata to GitHub Pages
if: |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Install additional SDKs as part of toolchain setup, i.e. install static Linux SD
```yml
- uses: SwiftyLab/setup-swift@latest
with:
sdks: static-linux;wasm
sdks: static-linux;wasm;android
```

After the environment is configured you can run swift and xcode commands using the standard [`run`](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsrun) step:
Expand Down
6 changes: 4 additions & 2 deletions __mocks__/https.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ClientRequest, IncomingMessage, IncomingHttpHeaders} from 'http'

let urls: (string | URL)[] = []
const urls: (string | URL)[] = []
let content: Content

export interface Content {
Expand All @@ -18,12 +18,14 @@ export function get(
callback: ((res: IncomingMessage) => void) | undefined
) {
urls.push(url)
let res = {
const res = {
statusCode: content.statusCode,
url: url,
headers: content.headers,
data: content.data,
/* eslint-disable @typescript-eslint/no-explicit-any */
on: (event: string, listener: (...args: any[]) => void) => {
/* eslint-enable @typescript-eslint/no-explicit-any */
if (event === 'data') {
listener(content.data)
} else if (event === 'end') {
Expand Down
37 changes: 34 additions & 3 deletions __tests__/installer/linux.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {coerce as parseSemVer} from 'semver'
import {LinuxToolchainInstaller} from '../../src/installer/linux'
import {ToolchainVersion} from '../../src/version'
import {Platform} from '../../src/platform'
import {describe, expect, it, jest, beforeEach, afterEach} from '@jest/globals'

jest.mock('getos')

Expand Down Expand Up @@ -94,7 +95,7 @@ describe('linux toolchain installation verification', () => {
const actionCacheSpy = jest.spyOn(cache, 'saveCache')
actionCacheSpy.mockResolvedValue(1)
jest.spyOn(exec, 'exec').mockResolvedValue(0)
await installer.install(arch)
await installer.install(arch, false)
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
for (const spy of [
downloadSpy,
Expand Down Expand Up @@ -129,7 +130,7 @@ describe('linux toolchain installation verification', () => {
const toolCacheSpy = jest.spyOn(toolCache, 'cacheDir')
const actionCacheSpy = jest.spyOn(cache, 'saveCache')
toolCacheSpy.mockResolvedValue(cached)
await installer.install('aarch64')
await installer.install('aarch64', false)
const toolCacheKey = `${toolchain.dir}-${toolchain.platform}`
const tmpDir = process.env.RUNNER_TEMP || os.tmpdir()
const restore = path.join(tmpDir, 'setup-swift', toolCacheKey)
Expand All @@ -153,7 +154,7 @@ describe('linux toolchain installation verification', () => {
const extractSpy = jest.spyOn(toolCache, 'extractTar')
const toolCacheSpy = jest.spyOn(toolCache, 'cacheDir')
const actionCacheSpy = jest.spyOn(cache, 'saveCache')
await installer.install('aarch64')
await installer.install('aarch64', false)
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
for (const spy of [downloadSpy, extractSpy, toolCacheSpy, actionCacheSpy]) {
expect(spy).not.toHaveBeenCalled()
Expand Down Expand Up @@ -210,4 +211,34 @@ describe('linux toolchain installation verification', () => {
expect(installer.data.dir).toBe(name)
expect(installer.data.branch).toBe('swiftwasm')
})

it('tests SDK installation', async () => {
setos({os: 'linux', dist: 'Ubuntu', release: '22.04'})
jest.spyOn(os, 'arch').mockReturnValue('x64')
const cVer = ToolchainVersion.create('6.3.0', false, [
'static-linux',
'wasm',
'android'
])
const download = path.resolve('tool', 'download', 'path')
const extracted = path.resolve('tool', 'extracted', 'path')
const cached = path.resolve('tool', 'cached', 'path')
jest.spyOn(core, 'getBooleanInput').mockReturnValue(true)
jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined)
jest.spyOn(toolCache, 'find').mockReturnValue('')
jest.spyOn(fs, 'cp').mockResolvedValue()
jest.spyOn(fs, 'access').mockResolvedValue()
jest.spyOn(toolCache, 'downloadTool').mockResolvedValue(download)
jest.spyOn(toolCache, 'extractTar').mockResolvedValue(extracted)
jest.spyOn(toolCache, 'extractZip').mockResolvedValue(extracted)
jest.spyOn(toolCache, 'cacheDir').mockResolvedValue(cached)
jest.spyOn(exec, 'exec').mockResolvedValue(0)
jest.spyOn(cache, 'saveCache').mockResolvedValue(1)
const {installer} = await Platform.install(cVer)
expect(installer.data.preventCaching).toBe(false)
expect(installer.data.platform).toBe('ubuntu2204')
expect(installer.data.download).toBe('swift-6.3-RELEASE-ubuntu22.04.tar.gz')
expect(installer.data.dir).toBe('swift-6.3-RELEASE')
expect(installer.data.branch).toBe('swift-6.3-release')
})
})
3 changes: 2 additions & 1 deletion __tests__/installer/package_manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as exec from '@actions/exec'
import {PackageManager} from '../../src/installer/package_manager'
import {describe, expect, it, jest} from '@jest/globals'

describe('package manager setup validation', () => {
it('tests package manager running correct commands', async () => {
Expand All @@ -10,7 +11,7 @@ describe('package manager setup validation', () => {
expect(manager.name).toBe('apt-get')
expect(manager.installationCommands).toBe(installationCommands)
await manager.install()
await expect(execSpy).toHaveBeenCalledTimes(2)
expect(execSpy).toHaveBeenCalledTimes(2)
const calls = execSpy.mock.calls
expect(calls[0]).toStrictEqual(['sudo', ['apt-get', 'update']])
expect(calls[1]).toStrictEqual(['sudo', [...installationCommands, '-y']])
Expand Down
136 changes: 136 additions & 0 deletions __tests__/installer/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import {SdkToolchainInstaller} from '../../src/installer/sdk'
import {describe, expect, it, jest, beforeEach, afterEach} from '@jest/globals'

describe('SDK toolchain installation', () => {
const sdkSnapshot = {
name: 'Swift SDK for Android',
date: new Date('2024-03-30 10:28:49.000000000 -05:00'),
download: 'swift-6.0-android-sdk.tar.gz',
checksum: 'abc123',
dir: 'swift-6.0-RELEASE',
platform: 'android',
branch: 'swift-6.0-release',
preventCaching: false
}

beforeEach(() => {
jest.useFakeTimers()
})

afterEach(() => {
jest.restoreAllMocks()
jest.useRealTimers()
})

it('tests install succeeds on first attempt', async () => {
const installer = new SdkToolchainInstaller(sdkSnapshot)
const execSpy = jest.spyOn(exec, 'exec').mockResolvedValue(0)

await installer.install('x86_64', false)

expect(execSpy).toHaveBeenCalledTimes(1)
expect(execSpy).toHaveBeenCalledWith('swift', [
'sdk',
'install',
'https://download.swift.org/swift-6.0-release/android/swift-6.0-RELEASE/swift-6.0-android-sdk.tar.gz',
'--checksum',
'abc123'
])
})

it('tests install without checksum', async () => {
const snapshotWithoutChecksum = {...sdkSnapshot, checksum: undefined}
const installer = new SdkToolchainInstaller(snapshotWithoutChecksum)
const execSpy = jest.spyOn(exec, 'exec').mockResolvedValue(0)

await installer.install('aarch64', false)

expect(execSpy).toHaveBeenCalledTimes(1)
expect(execSpy).toHaveBeenCalledWith('swift', [
'sdk',
'install',
'https://download.swift.org/swift-6.0-release/android/swift-6.0-RELEASE/swift-6.0-android-sdk.tar.gz'
])
})

it('tests install retries on failure and succeeds on second attempt', async () => {
const installer = new SdkToolchainInstaller(sdkSnapshot)
const execSpy = jest
.spyOn(exec, 'exec')
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce(0)
const infoSpy = jest.spyOn(core, 'info').mockReturnValue()

const installPromise = installer.install('x86_64', false)

// Fast-forward through first retry delay (1000ms)
await jest.advanceTimersByTimeAsync(1000)

await installPromise

expect(execSpy).toHaveBeenCalledTimes(2)
expect(infoSpy).toHaveBeenCalledWith('Waiting 1000ms before retrying')
})

it('tests install retries on failure and succeeds on third attempt', async () => {
const installer = new SdkToolchainInstaller(sdkSnapshot)
const execSpy = jest
.spyOn(exec, 'exec')
.mockRejectedValueOnce(new Error('Network error'))
.mockRejectedValueOnce(new Error('Timeout'))
.mockResolvedValueOnce(0)
const infoSpy = jest.spyOn(core, 'info').mockReturnValue()

const installPromise = installer.install('x86_64', false)

// Fast-forward through first retry delay (1000ms)
await jest.advanceTimersByTimeAsync(1000)
// Fast-forward through second retry delay (2000ms)
await jest.advanceTimersByTimeAsync(2000)

await installPromise

expect(execSpy).toHaveBeenCalledTimes(3)
expect(infoSpy).toHaveBeenCalledWith('Waiting 1000ms before retrying')
expect(infoSpy).toHaveBeenCalledWith('Waiting 2000ms before retrying')
})

it('tests install throws after three failed attempts', async () => {
jest.useRealTimers()
const installer = new SdkToolchainInstaller(sdkSnapshot)
const error = new Error('Persistent network error')
const execSpy = jest.spyOn(exec, 'exec').mockRejectedValue(error)
// Mock setTimeout to resolve immediately for faster test execution
jest.spyOn(global, 'setTimeout').mockImplementation(callback => {
callback()
return 0 as unknown as NodeJS.Timeout
})

await expect(installer.install('x86_64', false)).rejects.toThrow(
'Persistent network error'
)
expect(execSpy).toHaveBeenCalledTimes(3)
})

it('tests install with custom base URL', async () => {
const customSnapshot = {
...sdkSnapshot,
baseUrl: new URL('https://custom.swift.org/downloads/')
}
const installer = new SdkToolchainInstaller(customSnapshot)
const execSpy = jest.spyOn(exec, 'exec').mockResolvedValue(0)

await installer.install('x86_64', false)

expect(execSpy).toHaveBeenCalledTimes(1)
expect(execSpy).toHaveBeenCalledWith('swift', [
'sdk',
'install',
'https://custom.swift.org/downloads/swift-6.0-android-sdk.tar.gz',
'--checksum',
'abc123'
])
})
})
5 changes: 3 additions & 2 deletions __tests__/installer/windows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import os from 'os'
import {coerce as parseSemVer} from 'semver'
import {WindowsToolchainInstaller} from '../../src/installer/windows'
import {VisualStudio} from '../../src/utils/visual_studio'
import {describe, expect, it, jest, beforeEach, afterEach} from '@jest/globals'

jest.mock('https')

Expand Down Expand Up @@ -747,7 +748,7 @@ describe('windows toolchain installation verification', () => {
})
const mkdirSpy = jest
.spyOn(fs, 'mkdir')
.mockImplementation(path => Promise.resolve(path.toString()))
.mockImplementation(async path => Promise.resolve(path.toString()))
jest.spyOn(fs, 'copyFile').mockResolvedValue()
const writeFileSpy = jest.spyOn(fs, 'writeFile').mockResolvedValue()
const toolPath = path.join(
Expand Down Expand Up @@ -873,7 +874,7 @@ describe('windows toolchain installation verification', () => {
stdout: vsEnvs.join(os.EOL),
stderr: ''
})
await installer.install('x86_64')
await installer.install('x86_64', false)
expect(setupSpy).toHaveBeenCalled()
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
expect(process.env.PATH?.includes(swiftDev)).toBeTruthy()
Expand Down
Loading
Loading