Skip to content

Commit e7d59cd

Browse files
EvanBaconclaude
andauthored
Add XCConfig parsing support (#41)
Adds a new xcconfig module for parsing and manipulating Xcode configuration files (.xcconfig). Features include: - Single-pass parser for xcconfig format - #include and #include? directive support with file resolution - Conditional settings ([sdk=*], [arch=*], [config=*]) - Variable expansion and $(inherited) support - Build settings flattening with optional platform filtering - Integration with XCBuildConfiguration for baseConfigurationReference Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bd6455d commit e7d59cd

14 files changed

Lines changed: 1068 additions & 1 deletion

README.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,84 @@ Workspace file references use location specifiers:
277277
- `container:path` - Absolute container reference (rare)
278278
- `absolute:path` - Absolute file path
279279

280+
## XCConfig Support
281+
282+
Parse and manipulate Xcode configuration files (`.xcconfig`). These files define build settings that can be shared across targets and configurations.
283+
284+
### Low-level API
285+
286+
```ts
287+
import * as xcconfig from "@bacons/xcode/xcconfig";
288+
import fs from "fs";
289+
290+
// Parse an xcconfig string
291+
const config = xcconfig.parse(`
292+
#include "Base.xcconfig"
293+
PRODUCT_NAME = MyApp
294+
OTHER_LDFLAGS[sdk=iphoneos*] = -framework UIKit
295+
`);
296+
297+
// Parse from file (resolves #include directives)
298+
const config = xcconfig.parseFile("/path/to/Project.xcconfig");
299+
300+
// Flatten build settings (merges includes, applies conditions)
301+
const allSettings = xcconfig.flattenBuildSettings(config);
302+
303+
// Filter by platform conditions
304+
const iosSettings = xcconfig.flattenBuildSettings(config, {
305+
sdk: "iphoneos",
306+
arch: "arm64",
307+
config: "Release",
308+
});
309+
310+
// Serialize back to xcconfig format
311+
const output = xcconfig.build(config);
312+
fs.writeFileSync("/path/to/Project.xcconfig", output);
313+
```
314+
315+
### Conditional Settings
316+
317+
XCConfig supports conditional settings based on SDK, architecture, and configuration:
318+
319+
```
320+
// SDK-specific settings
321+
OTHER_LDFLAGS[sdk=iphoneos*] = -framework UIKit
322+
OTHER_LDFLAGS[sdk=macosx*] = -framework AppKit
323+
324+
// Architecture-specific settings
325+
ARCHS[arch=arm64] = arm64
326+
ARCHS[arch=x86_64] = x86_64
327+
328+
// Configuration-specific settings
329+
GCC_OPTIMIZATION_LEVEL[config=Debug] = 0
330+
GCC_OPTIMIZATION_LEVEL[config=Release] = s
331+
332+
// Combined conditions
333+
LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64] = /usr/lib/arm64
334+
```
335+
336+
### Include Directives
337+
338+
```
339+
// Required include - throws if file not found
340+
#include "Base.xcconfig"
341+
342+
// Optional include - silently ignored if file not found
343+
#include? "Optional.xcconfig"
344+
```
345+
346+
### Variable Expansion
347+
348+
XCConfig supports variable references and the `$(inherited)` keyword:
349+
350+
```
351+
// Reference other settings
352+
PRODUCT_BUNDLE_IDENTIFIER = $(BUNDLE_ID_PREFIX).$(PRODUCT_NAME:lower)
353+
354+
// Inherit from included files
355+
OTHER_LDFLAGS = $(inherited) -framework UIKit
356+
```
357+
280358
## Solution
281359

282360
- Uses a hand-optimized single-pass parser that is 11x faster than the legacy `xcode` package (which uses PEG.js).
@@ -307,7 +385,7 @@ We support the following types: `Object`, `Array`, `Data`, `String`. Notably, we
307385
- [ ] Create robust xcode projects from scratch.
308386
- [ ] Skills.
309387
- [ ] Import from other tools.
310-
- [ ] **XCConfig** Parsing: `.xcconfig` file parsing with `#include` support and build settings flattening.
388+
- [x] **XCConfig** Parsing: `.xcconfig` file parsing with `#include` support and build settings flattening.
311389
- [ ] **XCSharedData**: Shared project data directory (schemes, breakpoints, workspace settings).
312390
- [ ] **XCSchemeManagement**: Scheme ordering, visibility, and management plist. Controls which schemes appear and in what order in Xcode.
313391
- [ ] **XCUserData**: User-specific data (breakpoints, UI state). Useful for tooling that manages user preferences.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
"./workspace": {
3333
"types": "./build/workspace/index.d.ts",
3434
"default": "./build/workspace/index.js"
35+
},
36+
"./xcconfig": {
37+
"types": "./build/xcconfig/index.d.ts",
38+
"default": "./build/xcconfig/index.js"
3539
}
3640
},
3741
"files": [

src/api/XCBuildConfiguration.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type { SansIsa } from "./utils/util.types";
1212
import type { XcodeProject } from "./XcodeProject";
1313
import type { PBXFileReference } from "./PBXFileReference";
1414
import { resolveXcodeBuildSetting } from "./utils/resolveBuildSettings";
15+
import * as xcconfig from "../xcconfig";
16+
import type { XCConfig, XCConfigFlattenOptions } from "../xcconfig";
1517

1618
const debug = require("debug")(
1719
"xcode:XCBuildConfiguration"
@@ -170,6 +172,62 @@ export class XCBuildConfiguration extends AbstractObject<XCBuildConfigurationMod
170172
baseConfigurationReference: String,
171173
};
172174
}
175+
176+
/**
177+
* Get the absolute file path of the base configuration xcconfig file.
178+
* @returns The absolute path or null if no base configuration is set.
179+
*/
180+
getBaseConfigurationFilePath(): string | null {
181+
const fileRef = this.props.baseConfigurationReference;
182+
if (!fileRef) return null;
183+
184+
const filePath = fileRef.props.path;
185+
if (!filePath) return null;
186+
187+
const root = this.getXcodeProject().getProjectRoot();
188+
189+
// Handle different source tree types
190+
const sourceTree = fileRef.props.sourceTree;
191+
if (sourceTree === "<group>" || sourceTree === "SOURCE_ROOT") {
192+
return path.join(root, filePath);
193+
}
194+
if (sourceTree === "<absolute>" || path.isAbsolute(filePath)) {
195+
return filePath;
196+
}
197+
198+
return path.join(root, filePath);
199+
}
200+
201+
/**
202+
* Parse and return the base configuration xcconfig file.
203+
* @returns Parsed XCConfig or null if no base configuration is set or file doesn't exist.
204+
*/
205+
getBaseConfiguration(): XCConfig | null {
206+
const filePath = this.getBaseConfigurationFilePath();
207+
if (!filePath || !fs.existsSync(filePath)) return null;
208+
209+
try {
210+
return xcconfig.parseFile(filePath);
211+
} catch (error) {
212+
debug("Failed to parse base configuration: %s", error);
213+
return null;
214+
}
215+
}
216+
217+
/**
218+
* Get flattened build settings from the base configuration xcconfig file.
219+
* Settings are merged from included files with current file taking precedence.
220+
*
221+
* @param options - Optional filtering by sdk/arch/config
222+
* @returns Flattened key-value map of build settings, or empty object if no base config.
223+
*/
224+
getBaseConfigurationSettings(
225+
options?: XCConfigFlattenOptions
226+
): Record<string, string> {
227+
const config = this.getBaseConfiguration();
228+
if (!config) return {};
229+
return xcconfig.flattenBuildSettings(config, options);
230+
}
173231
}
174232

175233
// https://opensource.apple.com/source/pb_makefiles/pb_makefiles-1005/platform-variables.make.auto.html
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Base xcconfig - root settings
2+
BASE_SETTING = base_value
3+
OVERRIDDEN_SETTING = from_base
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Children xcconfig - includes Parent
2+
#include "Parent.xcconfig"
3+
4+
CHILD_SETTING = child_value
5+
OVERRIDDEN_SETTING = from_child
6+
INHERITED_SETTING = $(inherited) child_part
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Parent xcconfig - includes Base
2+
#include "Base.xcconfig"
3+
4+
PARENT_SETTING = parent_value
5+
OVERRIDDEN_SETTING = from_parent
6+
INHERITED_SETTING = parent_part
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Conditional settings based on SDK, architecture, and config
2+
PRODUCT_NAME = MyApp
3+
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
4+
5+
// SDK-specific settings
6+
OTHER_LDFLAGS[sdk=iphoneos*] = -framework UIKit
7+
OTHER_LDFLAGS[sdk=iphonesimulator*] = -framework UIKit
8+
OTHER_LDFLAGS[sdk=macosx*] = -framework AppKit
9+
10+
// Architecture-specific settings
11+
ARCHS[arch=arm64] = arm64
12+
ARCHS[arch=x86_64] = x86_64
13+
14+
// Combined conditions
15+
LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64] = $(inherited) /usr/lib/arm64
16+
LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64] = $(inherited) /usr/lib/x86_64
17+
18+
// Config-specific settings
19+
DEBUG_INFORMATION_FORMAT[config=Debug] = dwarf
20+
DEBUG_INFORMATION_FORMAT[config=Release] = dwarf-with-dsym
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Optional include test
2+
#include? "NonExistent.xcconfig"
3+
#include "Base.xcconfig"
4+
5+
LOCAL_SETTING = local_value
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Simple xcconfig file
2+
PRODUCT_NAME = MyApp
3+
PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp
4+
SWIFT_VERSION = 5.0

0 commit comments

Comments
 (0)