@@ -14,7 +14,13 @@ import {
1414} from './utils' ;
1515import path from 'node:path' ;
1616import { 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' ;
1824import type { NoSessionProxy } from './no-session-proxy' ;
1925
2026const DEFAULT_SIGNING_ID = 'iPhone Developer' ;
@@ -42,7 +48,6 @@ const REAL_DEVICES_CONFIG_DOCS_LINK =
4248const xcodeLog = logger . getLogger ( 'Xcode' ) ;
4349
4450export 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 * B U I L D _ D I R \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+ }
0 commit comments