Skip to content

Commit 6d6b81c

Browse files
authored
refactor(apple): implement simple pbxproj parser (#2442)
1 parent 9993425 commit 6d6b81c

File tree

5 files changed

+157
-65
lines changed

5 files changed

+157
-65
lines changed

ios/app.mjs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ import {
2121
import { generateInfoPlist } from "./infoPlist.mjs";
2222
import { generateLocalizations, getProductName } from "./localizations.mjs";
2323
import { generatePrivacyManifest } from "./privacyManifest.mjs";
24-
import { isObject, isString, projectPath, resolveResources } from "./utils.mjs";
24+
import {
25+
assertObject,
26+
isObject,
27+
isString,
28+
projectPath,
29+
resolveResources,
30+
} from "./utils.mjs";
2531
import {
2632
PRODUCT_DISPLAY_NAME,
2733
PRODUCT_VERSION,
@@ -30,6 +36,7 @@ import {
3036
applySwiftFlags,
3137
applyUserHeaderSearchPaths,
3238
configureBuildSchemes,
39+
openXcodeProject,
3340
overrideBuildSettings,
3441
} from "./xcode.mjs";
3542

@@ -272,9 +279,57 @@ export function generateProject(
272279
return project;
273280
}
274281

282+
/**
283+
* @param {string} projectRoot
284+
* @param {string} targetPlatform
285+
* @param {JSONObject} options
286+
* @returns {ProjectConfiguration}
287+
*/
288+
export function makeProject(projectRoot, targetPlatform, options, fs = nodefs) {
289+
const project = generateProject(projectRoot, targetPlatform, options, fs);
290+
291+
/** @type {Record<string, Record<string, string | string[]>>} */
292+
const mods = {
293+
ReactTestApp: project.buildSettings,
294+
ReactTestAppTests: project.testsBuildSettings,
295+
ReactTestAppUITests: project.uitestsBuildSettings,
296+
};
297+
298+
const pbxproj = openXcodeProject(project.xcodeprojPath, fs);
299+
for (const target of pbxproj.targets) {
300+
const { name: targetName } = target;
301+
if (typeof targetName !== "string" || !(targetName in mods)) {
302+
continue;
303+
}
304+
305+
const targetBuildSettings = Object.entries(mods[targetName]);
306+
for (const config of target.buildConfigurations) {
307+
const { buildSettings } = config;
308+
assertObject(buildSettings, "target.buildConfigurations[].buildSettings");
309+
310+
for (const [setting, value] of targetBuildSettings) {
311+
if (Array.isArray(value)) {
312+
const origValue = buildSettings[setting] ?? ["$(inherited)"];
313+
if (Array.isArray(origValue)) {
314+
origValue.push(...value);
315+
buildSettings[setting] = origValue;
316+
} else {
317+
buildSettings[setting] = [origValue, ...value].join(" ");
318+
}
319+
} else {
320+
buildSettings[setting] = value;
321+
}
322+
}
323+
}
324+
}
325+
pbxproj.save();
326+
327+
return project;
328+
}
329+
275330
if (isMain(import.meta.url)) {
276331
const [, , projectRoot, platform, options] = process.argv;
277332
const user = typeof options === "string" ? JSON.parse(options) : {};
278-
const project = generateProject(projectRoot, platform, user);
333+
const project = makeProject(projectRoot, platform, user);
279334
console.log(JSON.stringify(project, undefined, 2));
280335
}

ios/test_app.rb

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -127,35 +127,6 @@ def make_project!(project_root, target_platform, options)
127127
build_settings = project['buildSettings']
128128

129129
app_project = Xcodeproj::Project.open(xcodeproj_path)
130-
app_project.native_targets.each do |target|
131-
case target.name
132-
when 'ReactTestApp'
133-
target.build_configurations.each do |config|
134-
build_settings.each do |setting, value|
135-
if value.is_a? Array
136-
arr = config.build_settings[setting] || ['$(inherited)']
137-
value.each { |v| arr << v }
138-
config.build_settings[setting] = arr
139-
else
140-
config.build_settings[setting] = value
141-
end
142-
end
143-
end
144-
when 'ReactTestAppTests'
145-
target.build_configurations.each do |config|
146-
project['testsBuildSettings'].each do |setting, value|
147-
config.build_settings[setting] = value
148-
end
149-
end
150-
when 'ReactTestAppUITests'
151-
target.build_configurations.each do |config|
152-
project['uitestsBuildSettings'].each do |setting, value|
153-
config.build_settings[setting] = value
154-
end
155-
end
156-
end
157-
end
158-
app_project.save
159130

160131
config = app_project.build_configurations[0]
161132
{

ios/utils.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,39 @@ export function isString(value) {
2525
return typeof value === "string";
2626
}
2727

28+
/**
29+
* @param {JSONValue} value
30+
* @param {string} key
31+
* @returns {asserts value is JSONValue[]}
32+
*/
33+
export function assertArray(value, key) {
34+
if (!Array.isArray(value)) {
35+
throw new Error(`Expected '${key}' to be an array`);
36+
}
37+
}
38+
39+
/**
40+
* @param {JSONValue} value
41+
* @param {string} key
42+
* @returns {asserts value is JSONObject}
43+
*/
44+
export function assertObject(value, key) {
45+
if (!isObject(value)) {
46+
throw new Error(`Expected '${key}' to be an object`);
47+
}
48+
}
49+
50+
/**
51+
* @param {JSONValue} value
52+
* @param {string} key
53+
* @returns {asserts value is string}
54+
*/
55+
export function assertUniqueId(value, key) {
56+
if (typeof value !== "string") {
57+
throw new Error(`Expected '${key}' to be a unique id string`);
58+
}
59+
}
60+
2861
/**
2962
* @param {string} filename
3063
* @returns {JSONObject}

ios/xcode.mjs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ import { XMLBuilder, XMLParser } from "fast-xml-parser";
33
import * as nodefs from "node:fs";
44
import * as path from "node:path";
55
import { findFile, readTextFile, v } from "../scripts/helpers.js";
6-
import { isObject, isString } from "./utils.mjs";
6+
import {
7+
assertArray,
8+
assertObject,
9+
assertUniqueId,
10+
isObject,
11+
isString,
12+
jsonFromPlist,
13+
plistFromJSON,
14+
} from "./utils.mjs";
715

816
/**
917
* @import {
@@ -216,6 +224,56 @@ export function configureBuildSchemes(
216224
}
217225
}
218226

227+
/**
228+
* @param {string} xcodeproj
229+
*/
230+
export function openXcodeProject(xcodeproj, fs = nodefs) {
231+
const projectPath = path.join(xcodeproj, "project.pbxproj");
232+
const pbxproj = jsonFromPlist(projectPath);
233+
assertObject(pbxproj.objects, "pbxproj.objects");
234+
assertUniqueId(pbxproj.rootObject, "pbxproj.rootObject");
235+
236+
const { objects } = pbxproj;
237+
const project = objects[pbxproj.rootObject];
238+
assertObject(project, pbxproj.rootObject);
239+
240+
const { targets } = project;
241+
assertArray(targets, "rootObject.targets");
242+
243+
return {
244+
save() {
245+
fs.writeFileSync(projectPath, plistFromJSON(pbxproj, projectPath));
246+
},
247+
get targets() {
248+
return targets.map((target, index) => {
249+
assertUniqueId(target, `rootObject.targets[${index}]`);
250+
251+
const product = objects[target];
252+
assertObject(product, target);
253+
254+
const { buildConfigurationList } = product;
255+
assertUniqueId(buildConfigurationList, "buildConfigurationList");
256+
assertObject(objects[buildConfigurationList], buildConfigurationList);
257+
258+
const { buildConfigurations } = objects[buildConfigurationList];
259+
assertArray(buildConfigurations, "buildConfigurations");
260+
261+
/** @type {{ buildConfigurations: JSONObject[]; [key: string]: JSONValue; }} */
262+
const targets = {
263+
...product,
264+
buildConfigurations: buildConfigurations.map((config) => {
265+
assertUniqueId(config, `buildConfigurations[${config}]`);
266+
const buildConfiguration = objects[config];
267+
assertObject(buildConfiguration, config);
268+
return buildConfiguration;
269+
}),
270+
};
271+
return targets;
272+
});
273+
},
274+
};
275+
}
276+
219277
/**
220278
* @param {JSONObject} buildSettings
221279
* @param {JSONObject} overrides

test/ios/xcode.test.ts

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import * as path from "node:path";
1010
import { afterEach, beforeEach, describe, it } from "node:test";
1111
import { fileURLToPath, URL } from "node:url";
12-
import { isObject, jsonFromPlist } from "../../ios/utils.mjs";
12+
import { isObject } from "../../ios/utils.mjs";
1313
import {
1414
applyBuildSettings as applyBuildSettingsActual,
1515
applyPreprocessorDefinitions,
@@ -20,6 +20,7 @@ import {
2020
configureBuildSchemes as configureBuildSchemesActual,
2121
DEVELOPMENT_TEAM,
2222
GCC_PREPROCESSOR_DEFINITIONS,
23+
openXcodeProject,
2324
OTHER_SWIFT_FLAGS,
2425
overrideBuildSettings,
2526
PRODUCT_BUILD_NUMBER,
@@ -471,43 +472,17 @@ describe("macos/ReactTestApp.xcodeproj", macosOnly, () => {
471472
// targeting macOS. Unlike when targeting iOS, the warnings are treated as
472473
// errors.
473474
it("does not specify development team", () => {
474-
const xcodeproj = jsonFromPlist(
475-
fileURLToPath(
476-
new URL(
477-
"../../macos/ReactTestApp.xcodeproj/project.pbxproj",
478-
import.meta.url
479-
)
480-
)
475+
const xcodeproj = new URL(
476+
"../../macos/ReactTestApp.xcodeproj",
477+
import.meta.url
481478
);
482-
483-
const { objects } = xcodeproj;
484-
485-
ok(isObject(objects));
486-
ok(typeof xcodeproj.rootObject === "string");
487-
488-
const rootObject = objects[xcodeproj.rootObject];
489-
490-
ok(isObject(rootObject));
491-
ok(Array.isArray(rootObject.targets));
492-
ok(typeof rootObject.targets[0] === "string");
493-
494-
const appTarget = objects[rootObject.targets[0]];
479+
const project = openXcodeProject(fileURLToPath(xcodeproj));
480+
const appTarget = project.targets[0];
495481

496482
ok(isObject(appTarget));
497483
equal(appTarget.name, "ReactTestApp");
498-
ok(typeof appTarget.buildConfigurationList === "string");
499-
500-
const buildConfigurationList = objects[appTarget.buildConfigurationList];
501-
502-
ok(isObject(buildConfigurationList));
503-
ok(Array.isArray(buildConfigurationList.buildConfigurations));
504-
505-
for (const config of buildConfigurationList.buildConfigurations) {
506-
ok(typeof config === "string");
507-
508-
const buildConfiguration: JSONValue = objects[config];
509484

510-
ok(isObject(buildConfiguration));
485+
for (const buildConfiguration of appTarget.buildConfigurations) {
511486
ok(isObject(buildConfiguration.buildSettings));
512487
equal(buildConfiguration.buildSettings[CODE_SIGN_IDENTITY], "-");
513488
equal(buildConfiguration.buildSettings[DEVELOPMENT_TEAM], undefined);

0 commit comments

Comments
 (0)