diff --git a/packages/flutter_tools/lib/src/android/android_builder.dart b/packages/flutter_tools/lib/src/android/android_builder.dart index 0e9a9b8327a..caf48038613 100644 --- a/packages/flutter_tools/lib/src/android/android_builder.dart +++ b/packages/flutter_tools/lib/src/android/android_builder.dart @@ -27,6 +27,7 @@ abstract class AndroidBuilder { required FlutterProject project, required AndroidBuildInfo androidBuildInfo, required String target, + bool configOnly = false, }); /// Builds an App Bundle artifact. @@ -36,5 +37,6 @@ abstract class AndroidBuilder { required String target, bool validateDeferredComponents = true, bool deferredComponentsEnabled = false, + bool configOnly = false, }); } diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 012a1646339..b0426c95519 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -181,6 +181,7 @@ class AndroidGradleBuilder implements AndroidBuilder { required FlutterProject project, required AndroidBuildInfo androidBuildInfo, required String target, + bool configOnly = false, }) async { await buildGradleApp( project: project, @@ -188,6 +189,7 @@ class AndroidGradleBuilder implements AndroidBuilder { target: target, isBuildingBundle: false, localGradleErrors: gradleErrors, + configOnly: configOnly, ); } @@ -199,6 +201,7 @@ class AndroidGradleBuilder implements AndroidBuilder { required String target, bool validateDeferredComponents = true, bool deferredComponentsEnabled = false, + bool configOnly = false, }) async { await buildGradleApp( project: project, @@ -208,6 +211,7 @@ class AndroidGradleBuilder implements AndroidBuilder { localGradleErrors: gradleErrors, validateDeferredComponents: validateDeferredComponents, deferredComponentsEnabled: deferredComponentsEnabled, + configOnly: configOnly, ); } @@ -225,6 +229,7 @@ class AndroidGradleBuilder implements AndroidBuilder { required String target, required bool isBuildingBundle, required List localGradleErrors, + required bool configOnly, bool validateDeferredComponents = true, bool deferredComponentsEnabled = false, int retry = 0, @@ -249,8 +254,22 @@ class AndroidGradleBuilder implements AndroidBuilder { } // The default Gradle script reads the version name and number // from the local.properties file. - updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo); + updateLocalProperties( + project: project, buildInfo: androidBuildInfo.buildInfo); + final List command = [ + // This does more than get gradlewrapper. It creates the file, ensures it + // exists and verifies the file is executable. + _gradleUtils.getExecutable(project), + ]; + + // All automatically created files should exist. + if (configOnly) { + _logger.printStatus('Config complete.'); + return; + } + + // Assembly work starts here. final BuildInfo buildInfo = androidBuildInfo.buildInfo; final String assembleTask = isBuildingBundle ? getBundleTaskFor(buildInfo) @@ -260,9 +279,6 @@ class AndroidGradleBuilder implements AndroidBuilder { "Running Gradle task '$assembleTask'...", ); - final List command = [ - _gradleUtils.getExecutable(project), - ]; if (_logger.isVerbose) { command.add('--full-stacktrace'); command.add('--info'); @@ -428,6 +444,7 @@ class AndroidGradleBuilder implements AndroidBuilder { localGradleErrors: localGradleErrors, retry: retry, maxRetries: maxRetries, + configOnly: configOnly, ); final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success'; BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send(); diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart index 946d790485f..7402169814c 100644 --- a/packages/flutter_tools/lib/src/commands/build_apk.dart +++ b/packages/flutter_tools/lib/src/commands/build_apk.dart @@ -43,6 +43,9 @@ class BuildApkCommand extends BuildSubCommand { help: 'Whether to split the APKs per ABIs. ' 'To learn more, see: https://developer.android.com/studio/build/configure-apk-splits#configure-abi-split', ) + ..addFlag('config-only', + help: 'Generate build files used by flutter but ' + 'do not build any artifacts.') ..addMultiOption('target-platform', defaultsTo: ['android-arm', 'android-arm64', 'android-x64'], allowed: ['android-arm', 'android-arm64', 'android-x86', 'android-x64'], @@ -57,6 +60,8 @@ class BuildApkCommand extends BuildSubCommand { @override DeprecationBehavior get deprecationBehavior => boolArgDeprecated('ignore-deprecation') ? DeprecationBehavior.ignore : DeprecationBehavior.exit; + bool get configOnly => boolArg('config-only') ?? false; + @override Future> get requiredArtifacts async => { DevelopmentArtifact.androidGenSnapshot, @@ -112,6 +117,7 @@ class BuildApkCommand extends BuildSubCommand { project: FlutterProject.current(), target: targetFile, androidBuildInfo: androidBuildInfo, + configOnly: configOnly, ); return FlutterCommandResult.success(); } diff --git a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart index 1e358bcd6cc..54c4268e1e5 100644 --- a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart @@ -92,6 +92,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: [ GradleHandledError( test: (String line) { @@ -187,6 +188,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: [], ); expect(processManager, hasNoRemainingExpectations); @@ -256,6 +258,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: [ GradleHandledError( test: (String line) { @@ -350,6 +353,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: [ GradleHandledError( test: (String line) { @@ -441,6 +445,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: const [], ); }, throwsProcessException()); @@ -520,6 +525,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: [ GradleHandledError( test: (String line) { @@ -636,6 +642,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: [], ); @@ -704,6 +711,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: const [], ); @@ -963,6 +971,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: const [], ); }, throwsToolExit()); @@ -1039,6 +1048,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: const [], ); }, throwsToolExit()); @@ -1115,6 +1125,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: const [], ); }, throwsToolExit()); @@ -1192,6 +1203,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: const [], ); }, throwsToolExit()); @@ -1250,6 +1262,7 @@ void main() { ), target: 'lib/main.dart', isBuildingBundle: false, + configOnly: false, localGradleErrors: const [], ); }, throwsToolExit()); diff --git a/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart b/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart new file mode 100644 index 00000000000..55d793056b0 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart @@ -0,0 +1,65 @@ +// Copyright 2014 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:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; + +import '../src/common.dart'; +import 'test_utils.dart'; + +// Test that configOnly creates the gradlew file and does not assemble and app. +void main() { + late Directory tempDir; + late String flutterBin; + late Directory exampleAppDir; + + setUp(() async { + tempDir = createResolvedTempDirectorySync('flutter_build_test.'); + flutterBin = fileSystem.path.join( + getFlutterRoot(), + 'bin', + 'flutter', + ); + exampleAppDir = tempDir.childDirectory('bbb').childDirectory('example'); + + processManager.runSync([ + flutterBin, + ...getLocalEngineArguments(), + 'create', + '--template=plugin', + '--platforms=android', + 'bbb', + ], workingDirectory: tempDir.path); + }); + + tearDown(() async { + tryToDelete(tempDir); + }); + + test( + 'flutter build apk --config-only should create gradlew and not assemble', + () async { + final File gradleFile = fileSystem + .directory(exampleAppDir) + .childDirectory('android') + .childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew'); + // Ensure file is gone prior to configOnly running. + await gradleFile.delete(); + + final ProcessResult result = processManager.runSync([ + flutterBin, + ...getLocalEngineArguments(), + 'build', + 'apk', + '--target-platform=android-arm', + '--config-only', + ], workingDirectory: exampleAppDir.path); + + expect(gradleFile, exists); + expect(result.stdout, contains(RegExp(r'Config complete'))); + expect(result.stdout, isNot(contains(RegExp(r'Running Gradle task')))); + }, + ); +} diff --git a/packages/flutter_tools/test/src/android_common.dart b/packages/flutter_tools/test/src/android_common.dart index cab436d610f..6003432d9f1 100644 --- a/packages/flutter_tools/test/src/android_common.dart +++ b/packages/flutter_tools/test/src/android_common.dart @@ -24,6 +24,7 @@ class FakeAndroidBuilder implements AndroidBuilder { required FlutterProject project, required AndroidBuildInfo androidBuildInfo, required String target, + bool configOnly = false, }) async {} @override @@ -33,6 +34,7 @@ class FakeAndroidBuilder implements AndroidBuilder { required String target, bool validateDeferredComponents = true, bool deferredComponentsEnabled = false, + bool configOnly = false, }) async {} }