Skip to content

Commit 56b5f38

Browse files
feat: Add helper method to fetch build settings (#1139)
1 parent daad857 commit 56b5f38

4 files changed

Lines changed: 156 additions & 44 deletions

File tree

lib/types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,35 @@ export interface DeviceInfo {
115115
platformName: string;
116116
}
117117

118+
/** Xcode build setting key/value pairs from `xcodebuild -showBuildSettings -json`. */
119+
export type XcodeBuildSettings = Record<string, string>;
120+
121+
/** A single target entry returned by `xcodebuild -showBuildSettings -json`. */
122+
export interface XcodeShowBuildSettingsEntry {
123+
action: string;
124+
buildSettings: XcodeBuildSettings;
125+
target: string;
126+
}
127+
128+
export type WdaScheme =
129+
| 'WebDriverAgentRunner'
130+
| 'WebDriverAgentLib'
131+
| 'WebDriverAgentRunner_tvOS'
132+
| 'WebDriverAgentLib_tvOS';
133+
134+
export type WdaSdk = 'iphonesimulator' | 'iphoneos' | 'appletvsimulator' | 'appletvos';
135+
136+
export type WdaBuildConfiguration = 'Debug' | 'Release';
137+
138+
/** Options passed to {@link XcodeBuild.retrieveBuildSettings}. */
139+
export interface RetrieveBuildSettingsOptions {
140+
scheme?: WdaScheme;
141+
sdk?: WdaSdk;
142+
configuration?: WdaBuildConfiguration;
143+
/** `-destination` value (e.g. `id=<udid>` or a full destination specifier). */
144+
destination?: string;
145+
}
146+
118147
export interface XcodeBuildArgs {
119148
realDevice: boolean; // Required
120149
agentPath: string; // Required

lib/webdriveragent.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import {
2121
DEFAULT_TEST_BUNDLE_SUFFIX,
2222
} from './constants';
2323
import {strongbox} from '@appium/strongbox';
24-
import type {WebDriverAgentArgs, AppleDevice} from './types';
24+
import type {
25+
WebDriverAgentArgs,
26+
AppleDevice,
27+
XcodeBuildSettings,
28+
RetrieveBuildSettingsOptions,
29+
} from './types';
2530
import type {Simctl} from 'node-simctl';
2631
import type {Devicectl} from 'node-devicectl';
2732

@@ -51,13 +56,11 @@ export class WebDriverAgent {
5156
jwproxy?: JWProxy;
5257
proxyReqRes?: any;
5358
private readonly log: AppiumLogger;
54-
private readonly wdaBundlePath?: string;
5559
private readonly wdaLocalPort?: number;
5660
private readonly prebuildWDA?: boolean;
5761
private readonly wdaConnectionTimeout?: number;
5862
private readonly useXctestrunFile?: boolean;
5963
private readonly usePrebuiltWDA?: boolean;
60-
private readonly derivedDataPath?: string;
6164
private readonly mjpegServerPort?: number;
6265
private readonly wdaLaunchTimeout: number;
6366
private readonly usePreinstalledWDA?: boolean;
@@ -80,7 +83,6 @@ export class WebDriverAgent {
8083
this.iosSdkVersion = args.iosSdkVersion;
8184
this.host = args.host;
8285
this.isRealDevice = !!args.realDevice;
83-
this.wdaBundlePath = args.wdaBundlePath;
8486

8587
this.setWDAPaths(args.bootstrapPath, args.agentPath);
8688

@@ -102,7 +104,6 @@ export class WebDriverAgent {
102104

103105
this.useXctestrunFile = args.useXctestrunFile;
104106
this.usePrebuiltWDA = args.usePrebuiltWDA;
105-
this.derivedDataPath = args.derivedDataPath;
106107
this.mjpegServerPort = args.mjpegServerPort;
107108

108109
this.updatedWDABundleId = args.updatedWDABundleId;
@@ -396,7 +397,21 @@ export class WebDriverAgent {
396397
}
397398

398399
/**
399-
* Retrieves the Xcode derived data path for WebDriverAgent.
400+
* Retrieves Xcode build settings.
401+
* @param options - Optional scheme, SDK, configuration, or destination
402+
* @returns Build settings, or `undefined` if xcodebuild is skipped or settings cannot be determined
403+
*/
404+
async retrieveBuildSettings(
405+
options?: RetrieveBuildSettingsOptions,
406+
): Promise<XcodeBuildSettings | undefined> {
407+
if (this.canSkipXcodebuild) {
408+
return;
409+
}
410+
return await this.xcodebuild.retrieveBuildSettings(options);
411+
}
412+
413+
/**
414+
* @deprecated Use {@link retrieveBuildSettings} instead. Will be removed in a future release.
400415
* @returns The derived data path, or `undefined` if xcodebuild is skipped
401416
*/
402417
async retrieveDerivedDataPath(): Promise<string | undefined> {

lib/xcodebuild.ts

Lines changed: 104 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ import {
1414
} from './utils';
1515
import path from 'node:path';
1616
import {WDA_RUNNER_BUNDLE_ID} from './constants';
17-
import type {AppleDevice, XcodeBuildArgs} from './types';
17+
import type {
18+
AppleDevice,
19+
RetrieveBuildSettingsOptions,
20+
XcodeBuildArgs,
21+
XcodeBuildSettings,
22+
XcodeShowBuildSettingsEntry,
23+
} from './types';
1824
import type {NoSessionProxy} from './no-session-proxy';
1925

2026
const DEFAULT_SIGNING_ID = 'iPhone Developer';
@@ -42,7 +48,6 @@ const REAL_DEVICES_CONFIG_DOCS_LINK =
4248
const xcodeLog = logger.getLogger('Xcode');
4349

4450
export class XcodeBuild {
45-
xcodebuild?: SubProcess;
4651
readonly device: AppleDevice;
4752
readonly realDevice: boolean;
4853
readonly agentPath: string;
@@ -51,9 +56,9 @@ export class XcodeBuild {
5156
readonly platformName?: string;
5257
readonly iosSdkVersion?: string;
5358
readonly xcodeSigningId: string;
54-
usePrebuiltWDA?: boolean;
55-
derivedDataPath?: string;
56-
agentUrl?: string;
59+
private xcodebuild?: SubProcess;
60+
private usePrebuiltWDA?: boolean;
61+
private derivedDataPath?: string;
5762
private readonly log: AppiumLogger;
5863
private readonly showXcodeLog?: boolean;
5964
private readonly xcodeConfigFile?: string;
@@ -73,7 +78,10 @@ export class XcodeBuild {
7378
private readonly resultBundleVersion?: string;
7479
private _didBuildFail: boolean;
7580
private _didProcessExit: boolean;
76-
private _derivedDataPathPromise?: Promise<string | undefined>;
81+
private readonly _buildSettingsPromises = new Map<
82+
string,
83+
Promise<XcodeBuildSettings | undefined>
84+
>();
7785
private noSessionProxy?: NoSessionProxy;
7886
private xctestrunFilePath?: string;
7987

@@ -158,42 +166,42 @@ export class XcodeBuild {
158166
}
159167

160168
/**
161-
* Retrieves the Xcode derived data path for the build.
162-
* Uses cached value if available, otherwise queries xcodebuild for BUILD_DIR.
169+
* Retrieves Xcode build settings via `xcodebuild -showBuildSettings -json`.
170+
* @param options - Optional scheme, SDK, configuration, or destination
171+
* @returns Build settings for the `build` action, or `undefined` if they cannot be determined
172+
*/
173+
async retrieveBuildSettings(
174+
options?: RetrieveBuildSettingsOptions,
175+
): Promise<XcodeBuildSettings | undefined> {
176+
const cacheKey = buildSettingsCacheKey(options);
177+
let promise = this._buildSettingsPromises.get(cacheKey);
178+
if (!promise) {
179+
promise = this.fetchBuildSettings(options);
180+
this._buildSettingsPromises.set(cacheKey, promise);
181+
}
182+
return await promise;
183+
}
184+
185+
/**
163186
* @returns The derived data path, or `undefined` if it cannot be determined
164187
*/
165188
async retrieveDerivedDataPath(): Promise<string | undefined> {
166189
if (this.derivedDataPath) {
167190
return this.derivedDataPath;
168191
}
169192

170-
// avoid race conditions
171-
if (this._derivedDataPathPromise) {
172-
return await this._derivedDataPathPromise;
193+
const buildSettings = await this.retrieveBuildSettings();
194+
const buildDir = buildSettings?.BUILD_DIR;
195+
if (!buildDir) {
196+
this.log.warn('Cannot parse WDA BUILD_DIR from build settings');
197+
return;
173198
}
174199

175-
this._derivedDataPathPromise = (async () => {
176-
let stdout: string;
177-
try {
178-
({stdout} = await exec('xcodebuild', ['-project', this.agentPath, '-showBuildSettings']));
179-
} catch (err: any) {
180-
this.log.warn(`Cannot retrieve WDA build settings. Original error: ${err.message}`);
181-
return;
182-
}
183-
184-
const pattern = /^\s*BUILD_DIR\s+=\s+(\/.*)/m;
185-
const match = pattern.exec(stdout);
186-
if (!match) {
187-
this.log.warn(`Cannot parse WDA build dir from ${truncateString(stdout, 300)}`);
188-
return;
189-
}
190-
this.log.debug(`Parsed BUILD_DIR configuration value: '${match[1]}'`);
191-
// Derived data root is two levels higher over the build dir
192-
this.derivedDataPath = path.dirname(path.dirname(path.normalize(match[1])));
193-
this.log.debug(`Got derived data root: '${this.derivedDataPath}'`);
194-
return this.derivedDataPath;
195-
})();
196-
return await this._derivedDataPathPromise;
200+
this.log.debug(`Parsed BUILD_DIR configuration value: '${buildDir}'`);
201+
// Derived data root is two levels higher over the build dir
202+
this.derivedDataPath = path.dirname(path.dirname(path.normalize(buildDir)));
203+
this.log.debug(`Got derived data root: '${this.derivedDataPath}'`);
204+
return this.derivedDataPath;
197205
}
198206

199207
/**
@@ -297,6 +305,45 @@ export class XcodeBuild {
297305
await killProcess('xcodebuild', this.xcodebuild);
298306
}
299307

308+
private async fetchBuildSettings(
309+
options?: RetrieveBuildSettingsOptions,
310+
): Promise<XcodeBuildSettings | undefined> {
311+
const schemeLabel = options?.scheme ?? 'default';
312+
let stdout: string;
313+
try {
314+
({stdout} = await exec('xcodebuild', [
315+
'-project',
316+
this.agentPath,
317+
'-showBuildSettings',
318+
'-json',
319+
...buildSettingsArgsFromOptions(options),
320+
]));
321+
} catch (err: any) {
322+
this.log.warn(
323+
`Cannot retrieve WDA build settings for scheme '${schemeLabel}'. Original error: ${err.message}`,
324+
);
325+
return;
326+
}
327+
328+
let entries: XcodeShowBuildSettingsEntry[];
329+
try {
330+
entries = JSON.parse(stdout) as XcodeShowBuildSettingsEntry[];
331+
} catch (err: any) {
332+
this.log.warn(
333+
`Cannot parse WDA build settings for scheme '${schemeLabel}' from ${truncateString(stdout, 300)}. ` +
334+
`Original error: ${err.message}`,
335+
);
336+
return;
337+
}
338+
339+
const entry = entries.find(({action}) => action === 'build') ?? entries[0];
340+
if (!entry?.buildSettings) {
341+
this.log.warn(`Cannot find build settings for scheme '${schemeLabel}'`);
342+
return;
343+
}
344+
return entry.buildSettings;
345+
}
346+
300347
private getCommand(buildOnly: boolean = false): {cmd: string; args: string[]} {
301348
const cmd = 'xcodebuild';
302349
const args: string[] = [];
@@ -465,9 +512,6 @@ export class XcodeBuild {
465512
(noSessionProxy as any).timeout = 1000;
466513
try {
467514
currentStatus = (await noSessionProxy.command('/status', 'GET')) as StringRecord;
468-
if (currentStatus?.ios?.ip) {
469-
this.agentUrl = currentStatus.ios.ip as string;
470-
}
471515
this.log.debug(`WebDriverAgent information:`);
472516
this.log.debug(JSON.stringify(currentStatus, null, 2));
473517
} catch (err: any) {
@@ -498,3 +542,27 @@ export class XcodeBuild {
498542
return currentStatus;
499543
}
500544
}
545+
546+
function buildSettingsArgsFromOptions(options?: RetrieveBuildSettingsOptions): string[] {
547+
const args: string[] = [];
548+
if (!options) {
549+
return args;
550+
}
551+
if (options.scheme) {
552+
args.push('-scheme', options.scheme);
553+
}
554+
if (options.sdk) {
555+
args.push('-sdk', options.sdk);
556+
}
557+
if (options.configuration) {
558+
args.push('-configuration', options.configuration);
559+
}
560+
if (options.destination) {
561+
args.push('-destination', options.destination);
562+
}
563+
return args;
564+
}
565+
566+
function buildSettingsCacheKey(options?: RetrieveBuildSettingsOptions): string {
567+
return buildSettingsArgsFromOptions(options).join('\0');
568+
}

test/unit/webdriveragent-specs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ describe('WebDriverAgent', function () {
5151
expect(agent.bootstrapPath).to.eql(customBootstrapPath);
5252
expect(agent.agentPath).to.eql(customAgentPath);
5353
});
54-
it('should have custom derivedDataPath if specified', function () {
54+
it('should have custom derivedDataPath if specified', async function () {
5555
const agent = new WebDriverAgent({
5656
...fakeConstructorArgs,
5757
derivedDataPath: customDerivedDataPath,
5858
});
5959
if (agent.xcodebuild) {
60-
expect(agent.xcodebuild.derivedDataPath).to.eql(customDerivedDataPath);
60+
expect(await agent.retrieveDerivedDataPath()).to.eql(customDerivedDataPath);
6161
}
6262
});
6363
});

0 commit comments

Comments
 (0)