Skip to content

Commit 2f708eb

Browse files
authored
[CP] Wait for CONFIGURATION_BUILD_DIR to update when debugging with Xcode (flutter#135609)
Original PR: flutter#135444
1 parent ead4559 commit 2f708eb

File tree

6 files changed

+215
-33
lines changed

6 files changed

+215
-33
lines changed

packages/flutter_tools/bin/xcode_debug.js

Lines changed: 152 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ class CommandArguments {
6161

6262
this.xcodePath = this.validatedStringArgument('--xcode-path', parsedArguments['--xcode-path']);
6363
this.projectPath = this.validatedStringArgument('--project-path', parsedArguments['--project-path']);
64+
this.projectName = this.validatedStringArgument('--project-name', parsedArguments['--project-name']);
65+
this.expectedConfigurationBuildDir = this.validatedStringArgument(
66+
'--expected-configuration-build-dir',
67+
parsedArguments['--expected-configuration-build-dir'],
68+
);
6469
this.workspacePath = this.validatedStringArgument('--workspace-path', parsedArguments['--workspace-path']);
6570
this.targetDestinationId = this.validatedStringArgument('--device-id', parsedArguments['--device-id']);
6671
this.targetSchemeName = this.validatedStringArgument('--scheme', parsedArguments['--scheme']);
@@ -92,42 +97,76 @@ class CommandArguments {
9297
}
9398

9499
/**
95-
* Validates the flag is allowed for the current command.
100+
* Returns map of commands to map of allowed arguments. For each command, if
101+
* an argument flag is a key, than that flag is allowed for that command. If
102+
* the value for the key is true, then it is required for the command.
96103
*
97-
* @param {!string} flag
98-
* @param {?string} value
99-
* @returns {!bool}
100-
* @throws Will throw an error if the flag is not allowed for the current
101-
* command and the value is not null, undefined, or empty.
104+
* @returns {!string} Map of commands to allowed and optionally required
105+
* arguments.
102106
*/
103-
isArgumentAllowed(flag, value) {
104-
const allowedArguments = {
105-
'common': {
107+
argumentSettings() {
108+
return {
109+
'check-workspace-opened': {
106110
'--xcode-path': true,
107111
'--project-path': true,
108112
'--workspace-path': true,
109-
'--verbose': true,
113+
'--verbose': false,
110114
},
111-
'check-workspace-opened': {},
112115
'debug': {
116+
'--xcode-path': true,
117+
'--project-path': true,
118+
'--workspace-path': true,
119+
'--project-name': true,
120+
'--expected-configuration-build-dir': false,
113121
'--device-id': true,
114122
'--scheme': true,
115123
'--skip-building': true,
116124
'--launch-args': true,
125+
'--verbose': false,
117126
},
118127
'stop': {
128+
'--xcode-path': true,
129+
'--project-path': true,
130+
'--workspace-path': true,
119131
'--close-window': true,
120132
'--prompt-to-save': true,
133+
'--verbose': false,
121134
},
122-
}
135+
};
136+
}
123137

124-
const isAllowed = allowedArguments['common'][flag] === true || allowedArguments[this.command][flag] === true;
138+
/**
139+
* Validates the flag is allowed for the current command.
140+
*
141+
* @param {!string} flag
142+
* @param {?string} value
143+
* @returns {!bool}
144+
* @throws Will throw an error if the flag is not allowed for the current
145+
* command and the value is not null, undefined, or empty.
146+
*/
147+
isArgumentAllowed(flag, value) {
148+
const isAllowed = this.argumentSettings()[this.command].hasOwnProperty(flag);
125149
if (isAllowed === false && (value != null && value !== '')) {
126150
throw `The flag ${flag} is not allowed for the command ${this.command}.`;
127151
}
128152
return isAllowed;
129153
}
130154

155+
/**
156+
* Validates required flag has a value.
157+
*
158+
* @param {!string} flag
159+
* @param {?string} value
160+
* @throws Will throw an error if the flag is required for the current
161+
* command and the value is not null, undefined, or empty.
162+
*/
163+
validateRequiredArgument(flag, value) {
164+
const isRequired = this.argumentSettings()[this.command][flag] === true;
165+
if (isRequired === true && (value == null || value === '')) {
166+
throw `Missing value for ${flag}`;
167+
}
168+
}
169+
131170
/**
132171
* Parses the command line arguments into an object.
133172
*
@@ -182,9 +221,7 @@ class CommandArguments {
182221
if (this.isArgumentAllowed(flag, value) === false) {
183222
return null;
184223
}
185-
if (value == null || value === '') {
186-
throw `Missing value for ${flag}`;
187-
}
224+
this.validateRequiredArgument(flag, value);
188225
return value;
189226
}
190227

@@ -226,9 +263,7 @@ class CommandArguments {
226263
if (this.isArgumentAllowed(flag, value) === false) {
227264
return null;
228265
}
229-
if (value == null || value === '') {
230-
throw `Missing value for ${flag}`;
231-
}
266+
this.validateRequiredArgument(flag, value);
232267
try {
233268
return JSON.parse(value);
234269
} catch (e) {
@@ -347,6 +382,15 @@ function debugApp(xcode, args) {
347382
return new FunctionResult(null, destinationResult.error)
348383
}
349384

385+
// If expectedConfigurationBuildDir is available, ensure that it matches the
386+
// build settings.
387+
if (args.expectedConfigurationBuildDir != null && args.expectedConfigurationBuildDir !== '') {
388+
const updateResult = waitForConfigurationBuildDirToUpdate(targetWorkspace, args);
389+
if (updateResult.error != null) {
390+
return new FunctionResult(null, updateResult.error);
391+
}
392+
}
393+
350394
try {
351395
// Documentation from the Xcode Script Editor dictionary indicates that the
352396
// `debug` function has a parameter called `runDestinationSpecifier` which
@@ -528,3 +572,92 @@ function stopApp(xcode, args) {
528572
}
529573
return new FunctionResult(null, null);
530574
}
575+
576+
/**
577+
* Gets resolved build setting for CONFIGURATION_BUILD_DIR and waits until its
578+
* value matches the `--expected-configuration-build-dir` argument. Waits up to
579+
* 2 minutes.
580+
*
581+
* @param {!WorkspaceDocument} targetWorkspace A `WorkspaceDocument` (Xcode Mac
582+
* Scripting class).
583+
* @param {!CommandArguments} args
584+
* @returns {!FunctionResult} Always returns null as the `result`.
585+
*/
586+
function waitForConfigurationBuildDirToUpdate(targetWorkspace, args) {
587+
// Get the project
588+
let project;
589+
try {
590+
project = targetWorkspace.projects().find(x => x.name() == args.projectName);
591+
} catch (e) {
592+
return new FunctionResult(null, `Failed to find project ${args.projectName}: ${e}`);
593+
}
594+
if (project == null) {
595+
return new FunctionResult(null, `Failed to find project ${args.projectName}.`);
596+
}
597+
598+
// Get the target
599+
let target;
600+
try {
601+
// The target is probably named the same as the project, but if not, just use the first.
602+
const targets = project.targets();
603+
target = targets.find(x => x.name() == args.projectName);
604+
if (target == null && targets.length > 0) {
605+
target = targets[0];
606+
if (args.verbose) {
607+
console.log(`Failed to find target named ${args.projectName}, picking first target: ${target.name()}.`);
608+
}
609+
}
610+
} catch (e) {
611+
return new FunctionResult(null, `Failed to find target: ${e}`);
612+
}
613+
if (target == null) {
614+
return new FunctionResult(null, `Failed to find target.`);
615+
}
616+
617+
try {
618+
// Use the first build configuration (Debug). Any should do since they all
619+
// include Generated.xcconfig.
620+
const buildConfig = target.buildConfigurations()[0];
621+
const buildSettings = buildConfig.resolvedBuildSettings().reverse();
622+
623+
// CONFIGURATION_BUILD_DIR is often at (reverse) index 225 for Xcode
624+
// projects, so check there first. If it's not there, search the build
625+
// settings (which can be a little slow).
626+
const defaultIndex = 225;
627+
let configurationBuildDirSettings;
628+
if (buildSettings[defaultIndex] != null && buildSettings[defaultIndex].name() === 'CONFIGURATION_BUILD_DIR') {
629+
configurationBuildDirSettings = buildSettings[defaultIndex];
630+
} else {
631+
configurationBuildDirSettings = buildSettings.find(x => x.name() === 'CONFIGURATION_BUILD_DIR');
632+
}
633+
634+
if (configurationBuildDirSettings == null) {
635+
// This should not happen, even if it's not set by Flutter, there should
636+
// always be a resolved build setting for CONFIGURATION_BUILD_DIR.
637+
return new FunctionResult(null, `Unable to find CONFIGURATION_BUILD_DIR.`);
638+
}
639+
640+
// Wait up to 2 minutes for the CONFIGURATION_BUILD_DIR to update to the
641+
// expected value.
642+
const checkFrequencyInSeconds = 0.5;
643+
const maxWaitInSeconds = 2 * 60; // 2 minutes
644+
const verboseLogInterval = 10 * (1 / checkFrequencyInSeconds);
645+
const iterations = maxWaitInSeconds * (1 / checkFrequencyInSeconds);
646+
for (let i = 0; i < iterations; i++) {
647+
const verbose = args.verbose && i % verboseLogInterval === 0;
648+
649+
const configurationBuildDir = configurationBuildDirSettings.value();
650+
if (configurationBuildDir === args.expectedConfigurationBuildDir) {
651+
console.log(`CONFIGURATION_BUILD_DIR: ${configurationBuildDir}`);
652+
return new FunctionResult(null, null);
653+
}
654+
if (verbose) {
655+
console.log(`Current CONFIGURATION_BUILD_DIR: ${configurationBuildDir} while expecting ${args.expectedConfigurationBuildDir}`);
656+
}
657+
delay(checkFrequencyInSeconds);
658+
}
659+
return new FunctionResult(null, 'Timed out waiting for CONFIGURATION_BUILD_DIR to update.');
660+
} catch (e) {
661+
return new FunctionResult(null, `Failed to get CONFIGURATION_BUILD_DIR: ${e}`);
662+
}
663+
}

packages/flutter_tools/lib/src/ios/devices.dart

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,18 @@ class IOSDevice extends Device {
721721
return LaunchResult.failed();
722722
} finally {
723723
startAppStatus.stop();
724+
725+
if ((isCoreDevice || forceXcodeDebugWorkflow) && debuggingOptions.debuggingEnabled && package is BuildableIOSApp) {
726+
// When debugging via Xcode, after the app launches, reset the Generated
727+
// settings to not include the custom configuration build directory.
728+
// This is to prevent confusion if the project is later ran via Xcode
729+
// rather than the Flutter CLI.
730+
await updateGeneratedXcodeProperties(
731+
project: FlutterProject.current(),
732+
buildInfo: debuggingOptions.buildInfo,
733+
targetOverride: mainPath,
734+
);
735+
}
724736
}
725737
}
726738

@@ -818,6 +830,8 @@ class IOSDevice extends Device {
818830
scheme: scheme,
819831
xcodeProject: project.xcodeProject,
820832
xcodeWorkspace: project.xcodeWorkspace!,
833+
hostAppProjectName: project.hostAppProjectName,
834+
expectedConfigurationBuildDir: bundle.parent.absolute.path,
821835
verboseLogging: _logger.isVerbose,
822836
);
823837
} else {
@@ -839,18 +853,6 @@ class IOSDevice extends Device {
839853
shutdownHooks.addShutdownHook(() => _xcodeDebug.exit(force: true));
840854
}
841855

842-
if (package is BuildableIOSApp) {
843-
// After automating Xcode, reset the Generated settings to not include
844-
// the custom configuration build directory. This is to prevent
845-
// confusion if the project is later ran via Xcode rather than the
846-
// Flutter CLI.
847-
await updateGeneratedXcodeProperties(
848-
project: flutterProject,
849-
buildInfo: debuggingOptions.buildInfo,
850-
targetOverride: mainPath,
851-
);
852-
}
853-
854856
return debugSuccess;
855857
}
856858
}

packages/flutter_tools/lib/src/ios/xcode_debug.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ class XcodeDebug {
8585
project.xcodeProject.path,
8686
'--workspace-path',
8787
project.xcodeWorkspace.path,
88+
'--project-name',
89+
project.hostAppProjectName,
90+
if (project.expectedConfigurationBuildDir != null)
91+
...<String>[
92+
'--expected-configuration-build-dir',
93+
project.expectedConfigurationBuildDir!,
94+
],
8895
'--device-id',
8996
deviceId,
9097
'--scheme',
@@ -310,6 +317,7 @@ class XcodeDebug {
310317
_xcode.xcodeAppPath,
311318
'-g', // Do not bring the application to the foreground.
312319
'-j', // Launches the app hidden.
320+
'-F', // Open "fresh", without restoring windows.
313321
xcodeWorkspace.path
314322
],
315323
throwOnError: true,
@@ -396,6 +404,7 @@ class XcodeDebug {
396404

397405
return XcodeDebugProject(
398406
scheme: 'Runner',
407+
hostAppProjectName: 'Runner',
399408
xcodeProject: tempXcodeProject.childDirectory('Runner.xcodeproj'),
400409
xcodeWorkspace: tempXcodeProject.childDirectory('Runner.xcworkspace'),
401410
isTemporaryProject: true,
@@ -470,13 +479,17 @@ class XcodeDebugProject {
470479
required this.scheme,
471480
required this.xcodeWorkspace,
472481
required this.xcodeProject,
482+
required this.hostAppProjectName,
483+
this.expectedConfigurationBuildDir,
473484
this.isTemporaryProject = false,
474485
this.verboseLogging = false,
475486
});
476487

477488
final String scheme;
478489
final Directory xcodeWorkspace;
479490
final Directory xcodeProject;
491+
final String hostAppProjectName;
492+
final String? expectedConfigurationBuildDir;
480493
final bool isTemporaryProject;
481494

482495
/// When [verboseLogging] is true, the xcode_debug.js script will log

packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ void main() {
472472
scheme: 'Runner',
473473
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
474474
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
475+
hostAppProjectName: 'Runner',
475476
),
476477
expectedDeviceId: '123',
477478
expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -534,6 +535,8 @@ void main() {
534535
scheme: 'Runner',
535536
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
536537
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
538+
hostAppProjectName: 'Runner',
539+
expectedConfigurationBuildDir: '/build/ios/iphoneos',
537540
),
538541
expectedDeviceId: '123',
539542
expectedLaunchArguments: <String>['--enable-dart-profiling'],

packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,7 @@ void main() {
625625
scheme: 'Runner',
626626
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
627627
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
628+
hostAppProjectName: 'Runner',
628629
),
629630
expectedDeviceId: '123',
630631
expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -669,6 +670,7 @@ void main() {
669670
scheme: 'Runner',
670671
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
671672
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
673+
hostAppProjectName: 'Runner',
672674
),
673675
expectedDeviceId: '123',
674676
expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -729,6 +731,7 @@ void main() {
729731
scheme: 'Runner',
730732
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
731733
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
734+
hostAppProjectName: 'Runner',
732735
),
733736
expectedDeviceId: '123',
734737
expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -781,6 +784,7 @@ void main() {
781784
scheme: 'Runner',
782785
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
783786
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
787+
hostAppProjectName: 'Runner',
784788
),
785789
expectedDeviceId: '123',
786790
expectedLaunchArguments: <String>['--enable-dart-profiling'],

0 commit comments

Comments
 (0)