Skip to content

Commit d28b887

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

44 files changed

Lines changed: 108202 additions & 98663 deletions

Some content is hidden

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

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

__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: 1 addition & 0 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

__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')
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')
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')
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')
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')).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')
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: 2 additions & 1 deletion
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(

__tests__/installer/windows/modules.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as path from 'path'
22
import {promises as fs} from 'fs'
3+
import * as https from 'https'
34
// @ts-ignore
45
import {__setContent as setContent} from 'https'
5-
import * as core from '@actions/core'
66
import {updateSdkModules} from '../../../src/installer/windows/modules'
7+
import {describe, expect, it, jest, beforeEach, afterEach} from '@jest/globals'
78

89
jest.mock('https')
910

@@ -102,10 +103,12 @@ describe('windows modules SDK update', () => {
102103
// Mock setTimeout to avoid actual delays
103104
const setTimeoutSpy = jest
104105
.spyOn(global, 'setTimeout')
106+
/* eslint-disable @typescript-eslint/no-explicit-any */
105107
.mockImplementation((callback: any) => {
106108
callback()
107109
return {} as any
108110
})
111+
/* eslint-enable @typescript-eslint/no-explicit-any */
109112

110113
await expect(updateSdkModules(mockSdkRoot)).rejects.toThrow(
111114
"Request Failed Status Code: '404'"
@@ -119,8 +122,10 @@ describe('windows modules SDK update', () => {
119122

120123
// Mock https.get to fail twice then succeed
121124
jest
122-
.spyOn(require('https'), 'get')
125+
.spyOn(https, 'get')
126+
/* eslint-disable @typescript-eslint/no-explicit-any */
123127
.mockImplementation((url: any, callback: any) => {
128+
/* eslint-enable @typescript-eslint/no-explicit-any */
124129
attemptCount++
125130

126131
if (attemptCount <= 2) {
@@ -129,7 +134,9 @@ describe('windows modules SDK update', () => {
129134
statusCode: 500,
130135
url: url,
131136
headers: {'content-type': 'text/plain'},
137+
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
132138
on: (event: string, listener: Function) => {
139+
/* eslint-enable @typescript-eslint/no-unsafe-function-type */
133140
if (event === 'data') {
134141
listener('Internal Server Error')
135142
} else if (event === 'end') {
@@ -146,7 +153,9 @@ describe('windows modules SDK update', () => {
146153
statusCode: 200,
147154
url: url,
148155
headers: {'content-type': 'text/plain'},
156+
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
149157
on: (event: string, listener: Function) => {
158+
/* eslint-enable @typescript-eslint/no-unsafe-function-type */
150159
if (event === 'data') {
151160
listener(moduleContent)
152161
} else if (event === 'end') {
@@ -159,7 +168,7 @@ describe('windows modules SDK update', () => {
159168
callback(res)
160169
}
161170

162-
return {} as any
171+
return {} as any // eslint-disable-line @typescript-eslint/no-explicit-any
163172
})
164173

165174
jest.spyOn(fs, 'access').mockResolvedValue()
@@ -169,10 +178,12 @@ describe('windows modules SDK update', () => {
169178
// Mock setTimeout to avoid actual delays
170179
const setTimeoutSpy = jest
171180
.spyOn(global, 'setTimeout')
181+
/* eslint-disable @typescript-eslint/no-explicit-any */
172182
.mockImplementation((callback: any) => {
173183
callback()
174184
return {} as any
175185
})
186+
/* eslint-enable @typescript-eslint/no-explicit-any */
176187

177188
await updateSdkModules(mockSdkRoot)
178189

@@ -199,10 +210,12 @@ describe('windows modules SDK update', () => {
199210
// Mock setTimeout to avoid actual delays but still track calls
200211
const setTimeoutSpy = jest
201212
.spyOn(global, 'setTimeout')
213+
/* eslint-disable @typescript-eslint/no-explicit-any */
202214
.mockImplementation((callback: any) => {
203215
callback()
204216
return {} as any
205217
})
218+
/* eslint-enable @typescript-eslint/no-explicit-any */
206219

207220
await expect(updateSdkModules(mockSdkRoot)).rejects.toThrow()
208221

@@ -217,10 +230,12 @@ describe('windows modules SDK update', () => {
217230
// Mock setTimeout to avoid actual delays but still track calls
218231
const setTimeoutSpy = jest
219232
.spyOn(global, 'setTimeout')
233+
/* eslint-disable @typescript-eslint/no-explicit-any */
220234
.mockImplementation((callback: any) => {
221235
callback()
222236
return {} as any
223237
})
238+
/* eslint-enable @typescript-eslint/no-explicit-any */
224239

225240
await expect(updateSdkModules(mockSdkRoot)).rejects.toThrow(
226241
'Swift command not found in PATH'

0 commit comments

Comments
 (0)