From f0f1bae93a9369c09f688760c740d98917e6b65f Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 14 Apr 2025 14:45:35 -0400 Subject: [PATCH] [tool] Run a config-only build before Xcode analyze Currently xcode-analyze relies on the native project files already having been generated. This is unreliably locally, and now is problematic on CI as well since Xcode builds now (as of https://github.com/flutter/flutter/pull/165916) must match the last build mode, so analysis will fail if the previous CI step built in release mode (as is currently the case). This adds a config-only build call in debug mode before analyzing. Since running a config-only build is a common operation in the tool, this extracts a helper to abstract the logic. Unblocks the flutter/flutter->flutter/packages roller. --- .../lib/src/common/flutter_command_utils.dart | 46 ++++++++++++ script/tool/lib/src/fetch_deps_command.dart | 30 ++++---- .../lib/src/firebase_test_lab_command.dart | 31 ++++---- script/tool/lib/src/lint_android_command.dart | 10 ++- script/tool/lib/src/native_test_command.dart | 12 ++-- .../tool/lib/src/xcode_analyze_command.dart | 53 +++++++++----- .../test/firebase_test_lab_command_test.dart | 30 ++------ .../tool/test/xcode_analyze_command_test.dart | 72 +++++++++++++++++++ 8 files changed, 200 insertions(+), 84 deletions(-) create mode 100644 script/tool/lib/src/common/flutter_command_utils.dart diff --git a/script/tool/lib/src/common/flutter_command_utils.dart b/script/tool/lib/src/common/flutter_command_utils.dart new file mode 100644 index 00000000000..4c8ff746147 --- /dev/null +++ b/script/tool/lib/src/common/flutter_command_utils.dart @@ -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 runConfigOnlyBuild( + RepositoryPackage package, + ProcessRunner processRunner, + Platform platform, + FlutterPlatform targetPlatform, { + bool buildDebug = false, + List extraArgs = const [], +}) 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, + [ + 'build', + target, + if (buildDebug) '--debug', + '--config-only', + ...extraArgs, + ], + workingDir: package.directory); + return exitCode == 0; +} diff --git a/script/tool/lib/src/fetch_deps_command.dart b/script/tool/lib/src/fetch_deps_command.dart index a6ac9b7a520..ac57b35085b 100644 --- a/script/tool/lib/src/fetch_deps_command.dart +++ b/script/tool/lib/src/fetch_deps_command.dart @@ -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'; @@ -179,12 +180,9 @@ class FetchDepsCommand extends PackageLoopingCommand { processRunner: processRunner, platform: platform); if (!gradleProject.isConfigured()) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - ['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(['Unable to configure Gradle.']); } @@ -203,23 +201,25 @@ class FetchDepsCommand extends PackageLoopingCommand { } Future _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, - ['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(['Unable to configure project.']); } diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 3fb8e14cf3d..445a5aec511 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -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'; @@ -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(['Unable to build example apk']); } @@ -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 _ensureGradleWrapperExists(GradleProject project) async { + Future _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, - [ - '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: [ + if (experiment.isNotEmpty) '--enable-experiment=$experiment', + ], + ); } /// Runs [test] from [example] as a Firebase Test Lab test, returning true if diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index b893d9b0315..e9d49b9b459 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -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'; @@ -41,12 +42,9 @@ class LintAndroidCommand extends PackageLoopingCommand { processRunner: processRunner, platform: platform); if (!project.isConfigured()) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - ['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(['Unable to configure Gradle.']); } diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index d5995a35cd2..6714b68f607 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -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'; @@ -330,12 +331,13 @@ this command. platform: platform, ); if (!project.isConfigured()) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - ['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; diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index 6d6bd489649..7629a4e7346 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -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'; @@ -74,19 +75,21 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { final List failures = []; if (testIOS && - !await _analyzePlugin(package, 'iOS', extraFlags: [ - '-destination', - 'generic/platform=iOS Simulator', - if (minIOSVersion.isNotEmpty) - 'IPHONEOS_DEPLOYMENT_TARGET=$minIOSVersion', - ])) { + !await _analyzePlugin(package, FlutterPlatform.ios, + extraFlags: [ + '-destination', + 'generic/platform=iOS Simulator', + if (minIOSVersion.isNotEmpty) + 'IPHONEOS_DEPLOYMENT_TARGET=$minIOSVersion', + ])) { failures.add('iOS'); } if (testMacOS && - !await _analyzePlugin(package, 'macOS', extraFlags: [ - if (minMacOSVersion.isNotEmpty) - 'MACOSX_DEPLOYMENT_TARGET=$minMacOSVersion', - ])) { + !await _analyzePlugin(package, FlutterPlatform.macos, + extraFlags: [ + if (minMacOSVersion.isNotEmpty) + 'MACOSX_DEPLOYMENT_TARGET=$minMacOSVersion', + ])) { failures.add('macOS'); } @@ -101,22 +104,40 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { /// Analyzes [plugin] for [targetPlatform], returning true if it passed analysis. Future _analyzePlugin( RepositoryPackage plugin, - String targetPlatform, { + FlutterPlatform targetPlatform, { List extraFlags = const [], }) 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: ['clean', 'analyze'], - workspace: '${targetPlatform.toLowerCase()}/Runner.xcworkspace', + workspace: '${platformString.toLowerCase()}/Runner.xcworkspace', scheme: 'Runner', configuration: 'Debug', hostPlatform: platform, @@ -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; } } diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 13eeaa494ed..f3d2b21e171 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -167,11 +167,7 @@ public class MainActivityTest { ProcessCall( 'flutter', const ['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' @@ -196,11 +192,7 @@ public class MainActivityTest { ProcessCall( 'flutter', const ['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(' '), @@ -264,11 +256,7 @@ public class MainActivityTest { ProcessCall( 'flutter', const ['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(' '), @@ -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', @@ -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' diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index a69ee23120d..7ffac396a59 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -105,6 +105,15 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + 'flutter', + const [ + 'build', + 'ios', + '--debug', + '--config-only', + ], + pluginExampleDirectory.path), ProcessCall( 'xcrun', const [ @@ -146,6 +155,15 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + 'flutter', + const [ + 'build', + 'ios', + '--debug', + '--config-only', + ], + pluginExampleDirectory.path), ProcessCall( 'xcrun', const [ @@ -245,6 +263,15 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + 'flutter', + const [ + 'build', + 'macos', + '--debug', + '--config-only', + ], + pluginExampleDirectory.path), ProcessCall( 'xcrun', const [ @@ -280,6 +307,15 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + 'flutter', + const [ + 'build', + 'macos', + '--debug', + '--config-only', + ], + pluginExampleDirectory.path), ProcessCall( 'xcrun', const [ @@ -353,6 +389,15 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + 'flutter', + const [ + 'build', + 'ios', + '--debug', + '--config-only', + ], + pluginExampleDirectory.path), ProcessCall( 'xcrun', const [ @@ -370,6 +415,15 @@ void main() { 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), + ProcessCall( + 'flutter', + const [ + 'build', + 'macos', + '--debug', + '--config-only', + ], + pluginExampleDirectory.path), ProcessCall( 'xcrun', const [ @@ -411,6 +465,15 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + 'flutter', + const [ + 'build', + 'macos', + '--debug', + '--config-only', + ], + pluginExampleDirectory.path), ProcessCall( 'xcrun', const [ @@ -451,6 +514,15 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + 'flutter', + const [ + 'build', + 'ios', + '--debug', + '--config-only', + ], + pluginExampleDirectory.path), ProcessCall( 'xcrun', const [