Skip to content

Commit 8a45b05

Browse files
authored
feat: added Android SDK support
1 parent 5a8cba5 commit 8a45b05

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+108282
-98726
lines changed

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"image": "mcr.microsoft.com/devcontainers/universal:2",
2+
"image": "mcr.microsoft.com/devcontainers/universal:5-noble",
33
"customizations": {
44
"vscode": {
55
"extensions": [

.github/workflows/main.yml

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,14 +345,35 @@ jobs:
345345
uses: ./
346346
with:
347347
check-latest: ${{ needs.ci.outputs.check_latest }}
348-
sdks: static-linux;wasm
348+
sdks: static-linux;wasm;android
349349
dry-run: true
350350

351+
- name: Generate SDK install command
352+
id: gen-sdk-install
353+
uses: actions/github-script@v8
354+
with:
355+
script: |
356+
const sdks = JSON.parse(process.env.SWIFT_SDK_SNAPSHOTS);
357+
const commands = sdks.map(sdk => {
358+
const url = new URL(`https://download.swift.org/${sdk.branch}/${sdk.platform}/${sdk.dir}/${sdk.download}`).href;
359+
const args = ['swift', 'sdk', 'install', url];
360+
if (sdk.checksum) {
361+
args.push('--checksum', sdk.checksum);
362+
}
363+
return args.join(' ');
364+
});
365+
core.setOutput('command', commands.join(' && '));
366+
env:
367+
SWIFT_SDK_SNAPSHOTS: ${{ steps.setup-swift.outputs.sdks }}
368+
351369
- name: Verify Swift version
352370
uses: addnab/docker-run-action@v3
353371
with:
354372
image: swift:${{ fromJSON(steps.setup-swift.outputs.toolchain).docker }}
355-
run: swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1
373+
shell: bash
374+
run: |
375+
swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1
376+
${{ steps.gen-sdk-install.outputs.command }}
356377
357378
e2e-test:
358379
name: End-to-end test latest Swift on ${{ matrix.os }}
@@ -400,7 +421,7 @@ jobs:
400421
with: {
401422
'swift-version': 'latest',
402423
'check-latest': '${{ needs.ci.outputs.check_latest }}',
403-
'sdks': '${{ runner.os }}' != 'Windows' ? 'static-linux;wasm' : ''
424+
'sdks': '${{ runner.os }}' != 'Windows' ? 'static-linux;wasm;android' : ''
404425
}
405426
}
406427
]
@@ -431,14 +452,22 @@ jobs:
431452

432453
- name: Verify Swift SDKs
433454
if: runner.os != 'Windows'
434-
run: swift sdk list | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_static-linux || exit 1
455+
run: |
456+
SWIFT_SDK_LIST=$(swift sdk list)
457+
echo "$SWIFT_SDK_LIST" | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_static-linux || exit 1
458+
echo "$SWIFT_SDK_LIST" | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_wasm || exit 1
459+
echo "$SWIFT_SDK_LIST" | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_android || exit 1
435460
436461
- name: Test Swift package
437462
run: |
438463
swift package init --type library --name SetupLib
439464
swift build --build-tests
440465
swift test
441466
467+
- name: Test Swift package for Android
468+
if: runner.os != 'Windows'
469+
run: swift build --swift-sdk aarch64-unknown-linux-android28 --static-swift-stdlib
470+
442471
pages:
443472
name: Publish metadata to GitHub Pages
444473
if: |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Install additional SDKs as part of toolchain setup, i.e. install static Linux SD
4949
```yml
5050
- uses: SwiftyLab/setup-swift@latest
5151
with:
52-
sdks: static-linux;wasm
52+
sdks: static-linux;wasm;android
5353
```
5454

5555
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:

__mocks__/https.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ClientRequest, IncomingMessage, IncomingHttpHeaders} from 'http'
22

3-
let urls: (string | URL)[] = []
3+
const urls: (string | URL)[] = []
44
let content: Content
55

66
export interface Content {
@@ -18,12 +18,14 @@ export function get(
1818
callback: ((res: IncomingMessage) => void) | undefined
1919
) {
2020
urls.push(url)
21-
let res = {
21+
const res = {
2222
statusCode: content.statusCode,
2323
url: url,
2424
headers: content.headers,
2525
data: content.data,
26+
/* eslint-disable @typescript-eslint/no-explicit-any */
2627
on: (event: string, listener: (...args: any[]) => void) => {
28+
/* eslint-enable @typescript-eslint/no-explicit-any */
2729
if (event === 'data') {
2830
listener(content.data)
2931
} else if (event === 'end') {

__tests__/installer/linux.test.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {coerce as parseSemVer} from 'semver'
1111
import {LinuxToolchainInstaller} from '../../src/installer/linux'
1212
import {ToolchainVersion} from '../../src/version'
1313
import {Platform} from '../../src/platform'
14+
import {describe, expect, it, jest, beforeEach, afterEach} from '@jest/globals'
1415

1516
jest.mock('getos')
1617

@@ -94,7 +95,7 @@ describe('linux toolchain installation verification', () => {
9495
const actionCacheSpy = jest.spyOn(cache, 'saveCache')
9596
actionCacheSpy.mockResolvedValue(1)
9697
jest.spyOn(exec, 'exec').mockResolvedValue(0)
97-
await installer.install(arch)
98+
await installer.install(arch, false)
9899
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
99100
for (const spy of [
100101
downloadSpy,
@@ -129,7 +130,7 @@ describe('linux toolchain installation verification', () => {
129130
const toolCacheSpy = jest.spyOn(toolCache, 'cacheDir')
130131
const actionCacheSpy = jest.spyOn(cache, 'saveCache')
131132
toolCacheSpy.mockResolvedValue(cached)
132-
await installer.install('aarch64')
133+
await installer.install('aarch64', false)
133134
const toolCacheKey = `${toolchain.dir}-${toolchain.platform}`
134135
const tmpDir = process.env.RUNNER_TEMP || os.tmpdir()
135136
const restore = path.join(tmpDir, 'setup-swift', toolCacheKey)
@@ -153,7 +154,7 @@ describe('linux toolchain installation verification', () => {
153154
const extractSpy = jest.spyOn(toolCache, 'extractTar')
154155
const toolCacheSpy = jest.spyOn(toolCache, 'cacheDir')
155156
const actionCacheSpy = jest.spyOn(cache, 'saveCache')
156-
await installer.install('aarch64')
157+
await installer.install('aarch64', false)
157158
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
158159
for (const spy of [downloadSpy, extractSpy, toolCacheSpy, actionCacheSpy]) {
159160
expect(spy).not.toHaveBeenCalled()
@@ -210,4 +211,34 @@ describe('linux toolchain installation verification', () => {
210211
expect(installer.data.dir).toBe(name)
211212
expect(installer.data.branch).toBe('swiftwasm')
212213
})
214+
215+
it('tests SDK installation', async () => {
216+
setos({os: 'linux', dist: 'Ubuntu', release: '22.04'})
217+
jest.spyOn(os, 'arch').mockReturnValue('x64')
218+
const cVer = ToolchainVersion.create('6.3.0', false, [
219+
'static-linux',
220+
'wasm',
221+
'android'
222+
])
223+
const download = path.resolve('tool', 'download', 'path')
224+
const extracted = path.resolve('tool', 'extracted', 'path')
225+
const cached = path.resolve('tool', 'cached', 'path')
226+
jest.spyOn(core, 'getBooleanInput').mockReturnValue(true)
227+
jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined)
228+
jest.spyOn(toolCache, 'find').mockReturnValue('')
229+
jest.spyOn(fs, 'cp').mockResolvedValue()
230+
jest.spyOn(fs, 'access').mockResolvedValue()
231+
jest.spyOn(toolCache, 'downloadTool').mockResolvedValue(download)
232+
jest.spyOn(toolCache, 'extractTar').mockResolvedValue(extracted)
233+
jest.spyOn(toolCache, 'extractZip').mockResolvedValue(extracted)
234+
jest.spyOn(toolCache, 'cacheDir').mockResolvedValue(cached)
235+
jest.spyOn(exec, 'exec').mockResolvedValue(0)
236+
jest.spyOn(cache, 'saveCache').mockResolvedValue(1)
237+
const {installer} = await Platform.install(cVer)
238+
expect(installer.data.preventCaching).toBe(false)
239+
expect(installer.data.platform).toBe('ubuntu2204')
240+
expect(installer.data.download).toBe('swift-6.3-RELEASE-ubuntu22.04.tar.gz')
241+
expect(installer.data.dir).toBe('swift-6.3-RELEASE')
242+
expect(installer.data.branch).toBe('swift-6.3-release')
243+
})
213244
})

__tests__/installer/package_manager.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as exec from '@actions/exec'
22
import {PackageManager} from '../../src/installer/package_manager'
3+
import {describe, expect, it, jest} from '@jest/globals'
34

45
describe('package manager setup validation', () => {
56
it('tests package manager running correct commands', async () => {
@@ -10,7 +11,7 @@ describe('package manager setup validation', () => {
1011
expect(manager.name).toBe('apt-get')
1112
expect(manager.installationCommands).toBe(installationCommands)
1213
await manager.install()
13-
await expect(execSpy).toHaveBeenCalledTimes(2)
14+
expect(execSpy).toHaveBeenCalledTimes(2)
1415
const calls = execSpy.mock.calls
1516
expect(calls[0]).toStrictEqual(['sudo', ['apt-get', 'update']])
1617
expect(calls[1]).toStrictEqual(['sudo', [...installationCommands, '-y']])

__tests__/installer/sdk.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import * as core from '@actions/core'
2+
import * as exec from '@actions/exec'
3+
import {SdkToolchainInstaller} from '../../src/installer/sdk'
4+
import {describe, expect, it, jest, beforeEach, afterEach} from '@jest/globals'
5+
6+
describe('SDK toolchain installation', () => {
7+
const sdkSnapshot = {
8+
name: 'Swift SDK for Android',
9+
date: new Date('2024-03-30 10:28:49.000000000 -05:00'),
10+
download: 'swift-6.0-android-sdk.tar.gz',
11+
checksum: 'abc123',
12+
dir: 'swift-6.0-RELEASE',
13+
platform: 'android',
14+
branch: 'swift-6.0-release',
15+
preventCaching: false
16+
}
17+
18+
beforeEach(() => {
19+
jest.useFakeTimers()
20+
})
21+
22+
afterEach(() => {
23+
jest.restoreAllMocks()
24+
jest.useRealTimers()
25+
})
26+
27+
it('tests install succeeds on first attempt', async () => {
28+
const installer = new SdkToolchainInstaller(sdkSnapshot)
29+
const execSpy = jest.spyOn(exec, 'exec').mockResolvedValue(0)
30+
31+
await installer.install('x86_64', false)
32+
33+
expect(execSpy).toHaveBeenCalledTimes(1)
34+
expect(execSpy).toHaveBeenCalledWith('swift', [
35+
'sdk',
36+
'install',
37+
'https://download.swift.org/swift-6.0-release/android/swift-6.0-RELEASE/swift-6.0-android-sdk.tar.gz',
38+
'--checksum',
39+
'abc123'
40+
])
41+
})
42+
43+
it('tests install without checksum', async () => {
44+
const snapshotWithoutChecksum = {...sdkSnapshot, checksum: undefined}
45+
const installer = new SdkToolchainInstaller(snapshotWithoutChecksum)
46+
const execSpy = jest.spyOn(exec, 'exec').mockResolvedValue(0)
47+
48+
await installer.install('aarch64', false)
49+
50+
expect(execSpy).toHaveBeenCalledTimes(1)
51+
expect(execSpy).toHaveBeenCalledWith('swift', [
52+
'sdk',
53+
'install',
54+
'https://download.swift.org/swift-6.0-release/android/swift-6.0-RELEASE/swift-6.0-android-sdk.tar.gz'
55+
])
56+
})
57+
58+
it('tests install retries on failure and succeeds on second attempt', async () => {
59+
const installer = new SdkToolchainInstaller(sdkSnapshot)
60+
const execSpy = jest
61+
.spyOn(exec, 'exec')
62+
.mockRejectedValueOnce(new Error('Network error'))
63+
.mockResolvedValueOnce(0)
64+
const infoSpy = jest.spyOn(core, 'info').mockReturnValue()
65+
66+
const installPromise = installer.install('x86_64', false)
67+
68+
// Fast-forward through first retry delay (1000ms)
69+
await jest.advanceTimersByTimeAsync(1000)
70+
71+
await installPromise
72+
73+
expect(execSpy).toHaveBeenCalledTimes(2)
74+
expect(infoSpy).toHaveBeenCalledWith('Waiting 1000ms before retrying')
75+
})
76+
77+
it('tests install retries on failure and succeeds on third attempt', async () => {
78+
const installer = new SdkToolchainInstaller(sdkSnapshot)
79+
const execSpy = jest
80+
.spyOn(exec, 'exec')
81+
.mockRejectedValueOnce(new Error('Network error'))
82+
.mockRejectedValueOnce(new Error('Timeout'))
83+
.mockResolvedValueOnce(0)
84+
const infoSpy = jest.spyOn(core, 'info').mockReturnValue()
85+
86+
const installPromise = installer.install('x86_64', false)
87+
88+
// Fast-forward through first retry delay (1000ms)
89+
await jest.advanceTimersByTimeAsync(1000)
90+
// Fast-forward through second retry delay (2000ms)
91+
await jest.advanceTimersByTimeAsync(2000)
92+
93+
await installPromise
94+
95+
expect(execSpy).toHaveBeenCalledTimes(3)
96+
expect(infoSpy).toHaveBeenCalledWith('Waiting 1000ms before retrying')
97+
expect(infoSpy).toHaveBeenCalledWith('Waiting 2000ms before retrying')
98+
})
99+
100+
it('tests install throws after three failed attempts', async () => {
101+
jest.useRealTimers()
102+
const installer = new SdkToolchainInstaller(sdkSnapshot)
103+
const error = new Error('Persistent network error')
104+
const execSpy = jest.spyOn(exec, 'exec').mockRejectedValue(error)
105+
// Mock setTimeout to resolve immediately for faster test execution
106+
jest.spyOn(global, 'setTimeout').mockImplementation(callback => {
107+
callback()
108+
return 0 as unknown as NodeJS.Timeout
109+
})
110+
111+
await expect(installer.install('x86_64', false)).rejects.toThrow(
112+
'Persistent network error'
113+
)
114+
expect(execSpy).toHaveBeenCalledTimes(3)
115+
})
116+
117+
it('tests install with custom base URL', async () => {
118+
const customSnapshot = {
119+
...sdkSnapshot,
120+
baseUrl: new URL('https://custom.swift.org/downloads/')
121+
}
122+
const installer = new SdkToolchainInstaller(customSnapshot)
123+
const execSpy = jest.spyOn(exec, 'exec').mockResolvedValue(0)
124+
125+
await installer.install('x86_64', false)
126+
127+
expect(execSpy).toHaveBeenCalledTimes(1)
128+
expect(execSpy).toHaveBeenCalledWith('swift', [
129+
'sdk',
130+
'install',
131+
'https://custom.swift.org/downloads/swift-6.0-android-sdk.tar.gz',
132+
'--checksum',
133+
'abc123'
134+
])
135+
})
136+
})

__tests__/installer/windows.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import os from 'os'
1010
import {coerce as parseSemVer} from 'semver'
1111
import {WindowsToolchainInstaller} from '../../src/installer/windows'
1212
import {VisualStudio} from '../../src/utils/visual_studio'
13+
import {describe, expect, it, jest, beforeEach, afterEach} from '@jest/globals'
1314

1415
jest.mock('https')
1516

@@ -747,7 +748,7 @@ describe('windows toolchain installation verification', () => {
747748
})
748749
const mkdirSpy = jest
749750
.spyOn(fs, 'mkdir')
750-
.mockImplementation(path => Promise.resolve(path.toString()))
751+
.mockImplementation(async path => Promise.resolve(path.toString()))
751752
jest.spyOn(fs, 'copyFile').mockResolvedValue()
752753
const writeFileSpy = jest.spyOn(fs, 'writeFile').mockResolvedValue()
753754
const toolPath = path.join(
@@ -873,7 +874,7 @@ describe('windows toolchain installation verification', () => {
873874
stdout: vsEnvs.join(os.EOL),
874875
stderr: ''
875876
})
876-
await installer.install('x86_64')
877+
await installer.install('x86_64', false)
877878
expect(setupSpy).toHaveBeenCalled()
878879
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
879880
expect(process.env.PATH?.includes(swiftDev)).toBeTruthy()

0 commit comments

Comments
 (0)