Skip to content

Commit 541955d

Browse files
committed
feat: strip framework binaries to avoid duplicate symbol errors
1 parent ae21134 commit 541955d

5 files changed

Lines changed: 171 additions & 10 deletions

File tree

apps/RNApp/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2848,7 +2848,7 @@ SPEC CHECKSUMS:
28482848
ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654
28492849
RNScreens: d6413aeb1878cdafd3c721e2c5218faf5d5d3b13
28502850
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
2851-
Yoga: 46ff53afcbeda2bae19c85b65e17487c3e3984dd
2851+
Yoga: 526f25666395d30c297d53154398ffd249eaf9e1
28522852

28532853
PODFILE CHECKSUM: 7c116a16dd0744063c8c6293dbfc638c9d447c19
28542854

apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
79BD1EEA2EEBFB76003AA29F /* BrownfieldLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1414
79F35E8C2EEC1D4500E64860 /* BrownfieldLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F35E8A2EEC1D4500E64860 /* BrownfieldLib.swift */; };
1515
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
16-
A19F59C8902E0F7A03F2A8C6 /* Pods_RNApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6CBC8DE61C44A9AB569A3CA /* Pods_RNApp.framework */; };
17-
BFB326E698C6BD239F8AEAE9 /* Pods_RNApp_BrownfieldLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64481842743F90E340AD414A /* Pods_RNApp_BrownfieldLib.framework */; };
16+
8A98A64BB43C0122DC814AAC /* Pods_RNApp_BrownfieldLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68771E7510668353F394A5EB /* Pods_RNApp_BrownfieldLib.framework */; };
17+
93ED5C277B3EACB98380C031 /* Pods_RNApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79473D6C91983A1A5D79CA66 /* Pods_RNApp.framework */; };
1818
C66C2A65406C527E9529D08F /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
1919
/* End PBXBuildFile section */
2020

@@ -49,13 +49,13 @@
4949
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = RNApp/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
5050
3B4392A12AC88292D35C810B /* Pods-RNApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp.debug.xcconfig"; path = "Target Support Files/Pods-RNApp/Pods-RNApp.debug.xcconfig"; sourceTree = "<group>"; };
5151
5709B34CF0A7D63546082F79 /* Pods-RNApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp.release.xcconfig"; path = "Target Support Files/Pods-RNApp/Pods-RNApp.release.xcconfig"; sourceTree = "<group>"; };
52-
64481842743F90E340AD414A /* Pods_RNApp_BrownfieldLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp_BrownfieldLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
52+
68771E7510668353F394A5EB /* Pods_RNApp_BrownfieldLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp_BrownfieldLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5353
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = RNApp/AppDelegate.swift; sourceTree = "<group>"; };
54+
79473D6C91983A1A5D79CA66 /* Pods_RNApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5455
79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BrownfieldLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5556
79F35E8A2EEC1D4500E64860 /* BrownfieldLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrownfieldLib.swift; sourceTree = "<group>"; };
5657
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = RNApp/LaunchScreen.storyboard; sourceTree = "<group>"; };
5758
8A02E03D9F74B585B0A8F7F7 /* Pods-RNApp-BrownfieldLib.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp-BrownfieldLib.debug.xcconfig"; path = "Target Support Files/Pods-RNApp-BrownfieldLib/Pods-RNApp-BrownfieldLib.debug.xcconfig"; sourceTree = "<group>"; };
58-
B6CBC8DE61C44A9AB569A3CA /* Pods_RNApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5959
D8C030F60E402FD6CFBB3904 /* Pods-RNApp-BrownfieldLib.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp-BrownfieldLib.release.xcconfig"; path = "Target Support Files/Pods-RNApp-BrownfieldLib/Pods-RNApp-BrownfieldLib.release.xcconfig"; sourceTree = "<group>"; };
6060
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
6161
/* End PBXFileReference section */
@@ -66,15 +66,15 @@
6666
buildActionMask = 2147483647;
6767
files = (
6868
79BD1EE92EEBFB76003AA29F /* BrownfieldLib.framework in Frameworks */,
69-
A19F59C8902E0F7A03F2A8C6 /* Pods_RNApp.framework in Frameworks */,
69+
93ED5C277B3EACB98380C031 /* Pods_RNApp.framework in Frameworks */,
7070
);
7171
runOnlyForDeploymentPostprocessing = 0;
7272
};
7373
79BD1EE02EEBFB76003AA29F /* Frameworks */ = {
7474
isa = PBXFrameworksBuildPhase;
7575
buildActionMask = 2147483647;
7676
files = (
77-
BFB326E698C6BD239F8AEAE9 /* Pods_RNApp_BrownfieldLib.framework in Frameworks */,
77+
8A98A64BB43C0122DC814AAC /* Pods_RNApp_BrownfieldLib.framework in Frameworks */,
7878
);
7979
runOnlyForDeploymentPostprocessing = 0;
8080
};
@@ -97,8 +97,8 @@
9797
isa = PBXGroup;
9898
children = (
9999
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
100-
B6CBC8DE61C44A9AB569A3CA /* Pods_RNApp.framework */,
101-
64481842743F90E340AD414A /* Pods_RNApp_BrownfieldLib.framework */,
100+
79473D6C91983A1A5D79CA66 /* Pods_RNApp.framework */,
101+
68771E7510668353F394A5EB /* Pods_RNApp_BrownfieldLib.framework */,
102102
);
103103
name = Frameworks;
104104
sourceTree = "<group>";

packages/cli/src/brownfield/commands/packageIos.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { Command } from 'commander';
1717

1818
import { isBrownieInstalled } from '../../brownie/config.js';
1919
import { runCodegen } from '../../brownie/commands/codegen.js';
20-
import { getProjectInfo } from '../utils/index.js';
20+
import { getProjectInfo, stripFrameworkBinary } from '../utils/index.js';
2121
import {
2222
actionRunner,
2323
curryOptions,
@@ -101,6 +101,11 @@ export const packageIosCommand = curryOptions(
101101
outputPath: brownieOutputPath,
102102
});
103103

104+
// Strip the binary from Brownie.xcframework to make it interface-only.
105+
// This avoids duplicate symbols when consumer apps embed both BrownfieldLib
106+
// (which contains Brownie symbols) and Brownie.xcframework.
107+
await stripFrameworkBinary(brownieOutputPath);
108+
104109
logger.success(
105110
`Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`
106111
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './paths.js';
22
export * from './rn-cli.js';
3+
export * from './stripFrameworkBinary.js';
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { execSync } from 'node:child_process';
5+
import { logger } from '@rock-js/tools';
6+
7+
interface SliceConfig {
8+
target: string;
9+
/** Additional targets for fat binaries */
10+
additionalTargets?: string[];
11+
}
12+
13+
const SLICE_CONFIGS: Record<string, SliceConfig> = {
14+
'ios-arm64': {
15+
target: 'arm64-apple-ios15.0',
16+
},
17+
'ios-arm64_x86_64-simulator': {
18+
target: 'arm64-apple-ios15.0-simulator',
19+
additionalTargets: ['x86_64-apple-ios15.0-simulator'],
20+
},
21+
};
22+
23+
/**
24+
* Creates an empty static library for the given target.
25+
*/
26+
function createEmptyStaticLib(target: string): string {
27+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'framework-strip-'));
28+
const tempObj = path.join(tempDir, 'empty.o');
29+
const tempLib = path.join(tempDir, 'empty.a');
30+
31+
// Create empty object file
32+
execSync(
33+
`echo "" | xcrun clang -x c -c - -o "${tempObj}" -target ${target}`,
34+
{
35+
stdio: 'pipe',
36+
}
37+
);
38+
39+
// Create static library from object file
40+
execSync(`xcrun ar rcs "${tempLib}" "${tempObj}"`, {
41+
stdio: 'pipe',
42+
});
43+
44+
// Cleanup object file
45+
fs.unlinkSync(tempObj);
46+
47+
return tempLib;
48+
}
49+
50+
/**
51+
* Creates a fat static library combining multiple architectures.
52+
*/
53+
function createFatStaticLib(targets: string[]): string {
54+
const libs = targets.map((target) => createEmptyStaticLib(target));
55+
56+
// Create output in a separate temp directory
57+
const outputDir = fs.mkdtempSync(
58+
path.join(os.tmpdir(), 'framework-strip-fat-')
59+
);
60+
const outputLib = path.join(outputDir, 'fat.a');
61+
62+
execSync(
63+
`xcrun lipo -create ${libs.map((l) => `"${l}"`).join(' ')} -output "${outputLib}"`,
64+
{
65+
stdio: 'pipe',
66+
}
67+
);
68+
69+
// Cleanup individual libs and their directories
70+
libs.forEach((lib) => {
71+
fs.unlinkSync(lib);
72+
fs.rmdirSync(path.dirname(lib));
73+
});
74+
75+
return outputLib;
76+
}
77+
78+
/**
79+
* Strips the binary from an xcframework, keeping only Swift module interfaces.
80+
* This creates an "interface-only" framework where consumers can import the module
81+
* but the actual symbols must come from another framework (e.g., BrownfieldLib).
82+
*
83+
* @param xcframeworkPath - Path to the .xcframework directory
84+
*/
85+
export async function stripFrameworkBinary(
86+
xcframeworkPath: string
87+
): Promise<void> {
88+
if (!fs.existsSync(xcframeworkPath)) {
89+
throw new Error(`XCFramework not found at: ${xcframeworkPath}`);
90+
}
91+
92+
const frameworkName = path.basename(xcframeworkPath, '.xcframework');
93+
94+
logger.info(
95+
`Stripping binary from ${frameworkName}.xcframework (interface-only)...`
96+
);
97+
98+
const slices = fs.readdirSync(xcframeworkPath).filter((entry) => {
99+
const fullPath = path.join(xcframeworkPath, entry);
100+
return fs.statSync(fullPath).isDirectory() && entry.startsWith('ios-');
101+
});
102+
103+
for (const sliceName of slices) {
104+
const frameworkDir = path.join(
105+
xcframeworkPath,
106+
sliceName,
107+
`${frameworkName}.framework`
108+
);
109+
const binaryPath = path.join(frameworkDir, frameworkName);
110+
111+
if (!fs.existsSync(binaryPath)) {
112+
logger.warn(`No binary found at ${binaryPath}, skipping`);
113+
continue;
114+
}
115+
116+
const config = SLICE_CONFIGS[sliceName];
117+
if (!config) {
118+
logger.warn(`Unknown slice type: ${sliceName}, skipping`);
119+
continue;
120+
}
121+
122+
const originalSize = fs.statSync(binaryPath).size;
123+
124+
let emptyLib: string;
125+
if (config.additionalTargets) {
126+
// Create fat library for multiple architectures
127+
emptyLib = createFatStaticLib([
128+
config.target,
129+
...config.additionalTargets,
130+
]);
131+
} else {
132+
// Create single-arch library
133+
emptyLib = createEmptyStaticLib(config.target);
134+
}
135+
136+
// Replace original binary with empty stub
137+
fs.copyFileSync(emptyLib, binaryPath);
138+
fs.unlinkSync(emptyLib);
139+
// Clean up temp directory
140+
fs.rmdirSync(path.dirname(emptyLib));
141+
142+
const newSize = fs.statSync(binaryPath).size;
143+
logger.debug(
144+
` ${sliceName}: ${formatBytes(originalSize)} -> ${formatBytes(newSize)}`
145+
);
146+
}
147+
148+
logger.success(`${frameworkName}.xcframework is now interface-only`);
149+
}
150+
151+
function formatBytes(bytes: number): string {
152+
if (bytes < 1024) return `${bytes} B`;
153+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
154+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
155+
}

0 commit comments

Comments
 (0)