Skip to content

Commit 364fdbc

Browse files
authored
feat: add support for generating xcframework from static library (#716)
* feat: add support to generate xcframework from static lib * chore: add changeset * fix: update mergeFrameworks * fix: lint and add comments * fix: failing tests
1 parent 464c191 commit 364fdbc

4 files changed

Lines changed: 494 additions & 23 deletions

File tree

.changeset/loud-pigs-laugh.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rock-js/platform-apple-helpers': minor
3+
---
4+
5+
support static library to xcframework

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,5 @@ Thumbs.db
4545

4646
vite.config.*.timestamp*
4747
vitest.config.*.timestamp*
48+
49+
.pnpm-store
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import type * as Fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5+
6+
vi.unmock('node:fs');
7+
8+
const fs = await vi.importActual<typeof Fs>('node:fs');
9+
10+
const runXcodebuildMock = vi.fn();
11+
12+
vi.mock('../runXcodebuild.js', () => ({
13+
runXcodebuild: runXcodebuildMock,
14+
}));
15+
16+
const { mergeFrameworks } = await import('../mergeFrameworks.js');
17+
18+
describe('mergeFrameworks', () => {
19+
let tempDir: string;
20+
21+
beforeEach(() => {
22+
vi.clearAllMocks();
23+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'merge-frameworks-test-'));
24+
runXcodebuildMock.mockReset();
25+
runXcodebuildMock.mockResolvedValue({ errorSummary: undefined });
26+
});
27+
28+
afterEach(() => {
29+
fs.rmSync(tempDir, { recursive: true, force: true });
30+
});
31+
32+
it('passes through existing framework inputs unchanged', async () => {
33+
const frameworkPath = path.join(tempDir, 'Brownie.framework');
34+
fs.mkdirSync(frameworkPath, { recursive: true });
35+
36+
await mergeFrameworks({
37+
frameworkPaths: [frameworkPath],
38+
outputPath: path.join(tempDir, 'Brownie.xcframework'),
39+
sourceDir: tempDir,
40+
});
41+
42+
expect(runXcodebuildMock).toHaveBeenCalledWith(
43+
[
44+
'-create-xcframework',
45+
'-framework',
46+
frameworkPath,
47+
'-output',
48+
path.join(tempDir, 'Brownie.xcframework'),
49+
],
50+
{ cwd: tempDir },
51+
);
52+
});
53+
54+
it('sanitizes unresolved umbrella imports in synthesized framework wrappers', async () => {
55+
const frameworkPath = path.join(tempDir, 'Products', 'ReactBrownfield.framework');
56+
const buildProductPath = path.dirname(frameworkPath);
57+
58+
fs.mkdirSync(buildProductPath, { recursive: true });
59+
fs.writeFileSync(path.join(buildProductPath, 'libReactBrownfield.a'), 'static-lib');
60+
fs.writeFileSync(
61+
path.join(buildProductPath, 'ReactBrownfield-umbrella.h'),
62+
`#import "ReactBrownfield-Swift.h"
63+
#import "BrownfieldDevLoadingViewBridge.h"
64+
#import <ReactBrownfield/ReactNativeBrownfieldModule.h>
65+
66+
FOUNDATION_EXPORT double ReactBrownfieldVersionNumber;
67+
FOUNDATION_EXPORT const unsigned char ReactBrownfieldVersionString[];
68+
`,
69+
);
70+
fs.mkdirSync(path.join(buildProductPath, 'Swift Compatibility Header'), {
71+
recursive: true,
72+
});
73+
fs.writeFileSync(
74+
path.join(
75+
buildProductPath,
76+
'Swift Compatibility Header',
77+
'ReactBrownfield-Swift.h',
78+
),
79+
'swift-header',
80+
);
81+
82+
runXcodebuildMock.mockImplementation(async (args: string[]) => {
83+
const generatedFrameworkPath = args[2];
84+
const umbrellaHeader = fs.readFileSync(
85+
path.join(
86+
generatedFrameworkPath,
87+
'Headers',
88+
'ReactBrownfield-umbrella.h',
89+
),
90+
'utf8',
91+
);
92+
93+
expect(umbrellaHeader).toContain('#import "ReactBrownfield-Swift.h"');
94+
expect(umbrellaHeader).not.toContain('BrownfieldDevLoadingViewBridge.h');
95+
expect(umbrellaHeader).not.toContain('ReactNativeBrownfieldModule.h');
96+
expect(umbrellaHeader).toContain(
97+
'FOUNDATION_EXPORT double ReactBrownfieldVersionNumber;',
98+
);
99+
100+
return { errorSummary: undefined };
101+
});
102+
103+
await mergeFrameworks({
104+
frameworkPaths: [frameworkPath],
105+
outputPath: path.join(tempDir, 'ReactBrownfield.xcframework'),
106+
sourceDir: tempDir,
107+
});
108+
});
109+
110+
it('generates a module map without a Swift submodule when no Swift header exists', async () => {
111+
const frameworkPath = path.join(tempDir, 'Products', 'Brownie.framework');
112+
const buildProductPath = path.dirname(frameworkPath);
113+
114+
fs.mkdirSync(buildProductPath, { recursive: true });
115+
fs.writeFileSync(path.join(buildProductPath, 'libBrownie.a'), 'static-lib');
116+
fs.writeFileSync(path.join(buildProductPath, 'Brownie-umbrella.h'), 'umbrella');
117+
118+
runXcodebuildMock.mockImplementation(async (args: string[]) => {
119+
const generatedFrameworkPath = args[2];
120+
const generatedModuleMapPath = path.join(
121+
generatedFrameworkPath,
122+
'Modules',
123+
'module.modulemap',
124+
);
125+
126+
expect(fs.readFileSync(generatedModuleMapPath, 'utf8')).not.toContain(
127+
'module Brownie.Swift',
128+
);
129+
130+
return { errorSummary: undefined };
131+
});
132+
133+
await mergeFrameworks({
134+
frameworkPaths: [frameworkPath],
135+
outputPath: path.join(tempDir, 'Brownie.xcframework'),
136+
sourceDir: tempDir,
137+
});
138+
});
139+
140+
});

0 commit comments

Comments
 (0)