Skip to content

[tool] Run a config-only build before Xcode analyze #9075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions script/tool/lib/src/common/flutter_command_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:platform/platform.dart';

import 'process_runner.dart';
import 'repository_package.dart';

/// Runs the appropriate `flutter build --config-only` command for the given
/// target platform and build mode, to ensure that all of the native build files
/// are present for that mode.
///
/// If [streamOutput] is false, output will only be printed if the command
/// fails.
Future<bool> runConfigOnlyBuild(
RepositoryPackage package,
ProcessRunner processRunner,
Platform platform,
FlutterPlatform targetPlatform, {
bool buildDebug = false,
List<String> extraArgs = const <String>[],
}) async {
final String flutterCommand = platform.isWindows ? 'flutter.bat' : 'flutter';

final String target = switch (targetPlatform) {
FlutterPlatform.android => 'apk',
FlutterPlatform.ios => 'ios',
FlutterPlatform.linux => 'linux',
FlutterPlatform.macos => 'macos',
FlutterPlatform.web => 'web',
FlutterPlatform.windows => 'windows',
};

final int exitCode = await processRunner.runAndStream(
flutterCommand,
<String>[
'build',
target,
if (buildDebug) '--debug',
'--config-only',
...extraArgs,
],
workingDir: package.directory);
return exitCode == 0;
}
30 changes: 15 additions & 15 deletions script/tool/lib/src/fetch_deps_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'common/core.dart';
import 'common/flutter_command_utils.dart';
import 'common/gradle.dart';
import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
Expand Down Expand Up @@ -179,12 +180,9 @@ class FetchDepsCommand extends PackageLoopingCommand {
processRunner: processRunner, platform: platform);

if (!gradleProject.isConfigured()) {
final int exitCode = await processRunner.runAndStream(
flutterCommand,
<String>['build', 'apk', '--config-only'],
workingDir: example.directory,
);
if (exitCode != 0) {
final bool buildSuccess = await runConfigOnlyBuild(
example, processRunner, platform, FlutterPlatform.android);
if (!buildSuccess) {
printError('Unable to configure Gradle project.');
return PackageResult.fail(<String>['Unable to configure Gradle.']);
}
Expand All @@ -203,23 +201,25 @@ class FetchDepsCommand extends PackageLoopingCommand {
}

Future<PackageResult> _fetchDarwinDeps(
RepositoryPackage package, final String platform) async {
if (!pluginSupportsPlatform(platform, package,
RepositoryPackage package, final String platformString) async {
if (!pluginSupportsPlatform(platformString, package,
requiredMode: PlatformSupport.inline)) {
// Convert from the flag (lower case ios/macos) to the actual name.
final String displayPlatform = platform.replaceFirst('os', 'OS');
final String displayPlatform = platformString.replaceFirst('os', 'OS');
return PackageResult.skip(
'Package does not have native $displayPlatform dependencies.');
}

for (final RepositoryPackage example in package.getExamples()) {
// Create the necessary native build files, which will run pub get and pod install if needed.
final int exitCode = await processRunner.runAndStream(
flutterCommand,
<String>['build', platform, '--config-only'],
workingDir: example.directory,
);
if (exitCode != 0) {
final bool buildSuccess = await runConfigOnlyBuild(
example,
processRunner,
platform,
platformString == platformIOS
? FlutterPlatform.ios
: FlutterPlatform.macos);
if (!buildSuccess) {
printError('Unable to prepare native project files.');
return PackageResult.fail(<String>['Unable to configure project.']);
}
Expand Down
31 changes: 14 additions & 17 deletions script/tool/lib/src/firebase_test_lab_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:file/file.dart';
import 'package:uuid/uuid.dart';

import 'common/core.dart';
import 'common/flutter_command_utils.dart';
import 'common/gradle.dart';
import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
Expand Down Expand Up @@ -182,7 +183,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand {
// Ensures that gradle wrapper exists
final GradleProject project = GradleProject(example,
processRunner: processRunner, platform: platform);
if (!await _ensureGradleWrapperExists(project)) {
if (!await _ensureGradleWrapperExists(example, project)) {
return PackageResult.fail(<String>['Unable to build example apk']);
}

Expand Down Expand Up @@ -245,26 +246,22 @@ class FirebaseTestLabCommand extends PackageLoopingCommand {
/// Flutter build to generate it.
///
/// Returns true if either gradlew was already present, or the build succeeds.
Future<bool> _ensureGradleWrapperExists(GradleProject project) async {
Future<bool> _ensureGradleWrapperExists(
RepositoryPackage package, GradleProject project) async {
// Unconditionally re-run build with --debug --config-only, to ensure that
// the project is in a debug state even if it was previously configured.
print('Running flutter build apk...');
final String experiment = getStringArg(kEnableExperiment);
final int exitCode = await processRunner.runAndStream(
flutterCommand,
<String>[
'build',
'apk',
'--debug',
'--config-only',
if (experiment.isNotEmpty) '--enable-experiment=$experiment',
],
workingDir: project.androidDirectory);

if (exitCode != 0) {
return false;
}
return true;
return runConfigOnlyBuild(
package,
processRunner,
platform,
FlutterPlatform.android,
buildDebug: true,
extraArgs: <String>[
if (experiment.isNotEmpty) '--enable-experiment=$experiment',
],
);
}

/// Runs [test] from [example] as a Firebase Test Lab test, returning true if
Expand Down
10 changes: 4 additions & 6 deletions script/tool/lib/src/lint_android_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'common/core.dart';
import 'common/flutter_command_utils.dart';
import 'common/gradle.dart';
import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
Expand Down Expand Up @@ -41,12 +42,9 @@ class LintAndroidCommand extends PackageLoopingCommand {
processRunner: processRunner, platform: platform);

if (!project.isConfigured()) {
final int exitCode = await processRunner.runAndStream(
flutterCommand,
<String>['build', 'apk', '--config-only'],
workingDir: example.directory,
);
if (exitCode != 0) {
final bool buildSuccess = await runConfigOnlyBuild(
example, processRunner, platform, FlutterPlatform.android);
if (!buildSuccess) {
printError('Unable to configure Gradle project.');
return PackageResult.fail(<String>['Unable to configure Gradle.']);
}
Expand Down
12 changes: 7 additions & 5 deletions script/tool/lib/src/native_test_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:meta/meta.dart';

import 'common/cmake.dart';
import 'common/core.dart';
import 'common/flutter_command_utils.dart';
import 'common/gradle.dart';
import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
Expand Down Expand Up @@ -330,12 +331,13 @@ this command.
platform: platform,
);
if (!project.isConfigured()) {
final int exitCode = await processRunner.runAndStream(
flutterCommand,
<String>['build', 'apk', '--config-only'],
workingDir: example.directory,
final bool buildSuccess = await runConfigOnlyBuild(
example,
processRunner,
platform,
FlutterPlatform.android,
);
if (exitCode != 0) {
if (!buildSuccess) {
printError('Unable to configure Gradle project.');
failed = true;
continue;
Expand Down
53 changes: 37 additions & 16 deletions script/tool/lib/src/xcode_analyze_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'common/core.dart';
import 'common/flutter_command_utils.dart';
import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
import 'common/plugin_utils.dart';
Expand Down Expand Up @@ -74,19 +75,21 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand {

final List<String> failures = <String>[];
if (testIOS &&
!await _analyzePlugin(package, 'iOS', extraFlags: <String>[
'-destination',
'generic/platform=iOS Simulator',
if (minIOSVersion.isNotEmpty)
'IPHONEOS_DEPLOYMENT_TARGET=$minIOSVersion',
])) {
!await _analyzePlugin(package, FlutterPlatform.ios,
extraFlags: <String>[
'-destination',
'generic/platform=iOS Simulator',
if (minIOSVersion.isNotEmpty)
'IPHONEOS_DEPLOYMENT_TARGET=$minIOSVersion',
])) {
failures.add('iOS');
}
if (testMacOS &&
!await _analyzePlugin(package, 'macOS', extraFlags: <String>[
if (minMacOSVersion.isNotEmpty)
'MACOSX_DEPLOYMENT_TARGET=$minMacOSVersion',
])) {
!await _analyzePlugin(package, FlutterPlatform.macos,
extraFlags: <String>[
if (minMacOSVersion.isNotEmpty)
'MACOSX_DEPLOYMENT_TARGET=$minMacOSVersion',
])) {
failures.add('macOS');
}

Expand All @@ -101,22 +104,40 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand {
/// Analyzes [plugin] for [targetPlatform], returning true if it passed analysis.
Future<bool> _analyzePlugin(
RepositoryPackage plugin,
String targetPlatform, {
FlutterPlatform targetPlatform, {
List<String> extraFlags = const <String>[],
}) async {
final String platformString =
targetPlatform == FlutterPlatform.ios ? 'iOS' : 'macOS';
bool passing = true;
for (final RepositoryPackage example in plugin.getExamples()) {
// Unconditionally re-run build with --debug --config-only, to ensure that
// the project is in a debug state even if it was previously configured.
print('Running flutter build --config-only...');
final bool buildSuccess = await runConfigOnlyBuild(
example,
processRunner,
platform,
targetPlatform,
buildDebug: true,
);
if (!buildSuccess) {
printError('Unable to prepare native project files.');
passing = false;
continue;
}

// Running tests and static analyzer.
final String examplePath = getRelativePosixPath(example.directory,
from: plugin.directory.parent);
print('Running $targetPlatform tests and analyzer for $examplePath...');
print('Running $platformString tests and analyzer for $examplePath...');
final int exitCode = await _xcode.runXcodeBuild(
example.directory,
targetPlatform,
platformString,
// Clean before analyzing to remove cached swiftmodules from previous
// runs, which can cause conflicts.
actions: <String>['clean', 'analyze'],
workspace: '${targetPlatform.toLowerCase()}/Runner.xcworkspace',
workspace: '${platformString.toLowerCase()}/Runner.xcworkspace',
scheme: 'Runner',
configuration: 'Debug',
hostPlatform: platform,
Expand All @@ -126,9 +147,9 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand {
],
);
if (exitCode == 0) {
printSuccess('$examplePath ($targetPlatform) passed analysis.');
printSuccess('$examplePath ($platformString) passed analysis.');
} else {
printError('$examplePath ($targetPlatform) failed analysis.');
printError('$examplePath ($platformString) failed analysis.');
passing = false;
}
}
Expand Down
30 changes: 5 additions & 25 deletions script/tool/test/firebase_test_lab_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,7 @@ public class MainActivityTest {
ProcessCall(
'flutter',
const <String>['build', 'apk', '--debug', '--config-only'],
plugin1
.getExamples()
.first
.platformDirectory(FlutterPlatform.android)
.path),
plugin1.getExamples().first.directory.path),
ProcessCall(
'gcloud',
'auth activate-service-account --key-file=/path/to/key'
Expand All @@ -196,11 +192,7 @@ public class MainActivityTest {
ProcessCall(
'flutter',
const <String>['build', 'apk', '--debug', '--config-only'],
plugin2
.getExamples()
.first
.platformDirectory(FlutterPlatform.android)
.path),
plugin2.getExamples().first.directory.path),
ProcessCall(
'/packages/plugin2/example/android/gradlew',
'app:assembleAndroidTest -Pverbose=true'.split(' '),
Expand Down Expand Up @@ -264,11 +256,7 @@ public class MainActivityTest {
ProcessCall(
'flutter',
const <String>['build', 'apk', '--debug', '--config-only'],
plugin
.getExamples()
.first
.platformDirectory(FlutterPlatform.android)
.path),
plugin.getExamples().first.directory.path),
ProcessCall(
'/packages/plugin/example/android/gradlew',
'app:assembleAndroidTest -Pverbose=true'.split(' '),
Expand Down Expand Up @@ -694,11 +682,7 @@ class MainActivityTest {
ProcessCall(
'flutter',
'build apk --debug --config-only'.split(' '),
plugin
.getExamples()
.first
.platformDirectory(FlutterPlatform.android)
.path,
plugin.getExamples().first.directory.path,
),
ProcessCall(
'/packages/plugin/example/android/gradlew',
Expand Down Expand Up @@ -878,11 +862,7 @@ class MainActivityTest {
'--config-only',
'--enable-experiment=exp1'
],
plugin
.getExamples()
.first
.platformDirectory(FlutterPlatform.android)
.path),
plugin.getExamples().first.directory.path),
ProcessCall(
'/packages/plugin/example/android/gradlew',
'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1'
Expand Down
Loading