import 'dart:math';
import 'dart:typed_data';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'package:xml/xml.dart';
import '../artifacts.dart';
import '../base/analyze_size.dart';
import '../base/common.dart';
import '../base/deferred_component.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/net.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/project_migrator.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
import '../context/android_context.dart';
import '../context/tool_context.dart';
import '../convert.dart';
import '../flutter_manifest.dart';
import '../globals.dart' as globals;
import '../project.dart';
import 'android_builder.dart';
import 'android_sdk.dart';
import 'android_studio.dart';
import 'gradle_errors.dart';
import 'gradle_utils.dart' as gradle;
import 'gradle_utils.dart';
import 'java.dart';
import 'migrations/android_studio_java_gradle_conflict_migration.dart';
import 'migrations/cmake_android_16k_pages_migration.dart';
import 'migrations/disable_built_in_kotlin_migration.dart';
import 'migrations/disable_new_dsl_migration.dart';
import 'migrations/min_sdk_version_migration.dart';
import 'migrations/multidex_removal_migration.dart';
import 'migrations/top_level_gradle_build_file_migration.dart';
final _kBuildVariantRegex = RegExp('^BuildVariant: (?<$_kBuildVariantRegexGroupName>.*)\$');
const _kBuildVariantRegexGroupName = 'variant';
const _kBuildVariantTaskName = 'printBuildVariants';
const _kSdkManagerPathProperty = 'flutter.sdkManagerPath';
const _kAndroidSdkRootProperty = 'flutter.androidSdkRoot';
const _kInstalledNdkVersionsProperty = 'flutter.installedNdkVersions';
@visibleForTesting
const failedToStripDebugSymbolsErrorMessage = r'''
Release app bundle failed to strip debug symbols from native libraries.
Please run flutter doctor and ensure that the Android toolchain does not
report any issues.
Otherwise, file an issue at https://github.com/flutter/flutter/issues.''';
typedef _OutputParser = void Function(String line);
String _getOutputAppLinkSettingsTaskFor(String buildVariant) {
return _taskForBuildVariant('output', buildVariant, 'AppLinkSettings');
}
Directory getApkDirectory(FlutterProject project) {
return project.isModule
? project.android.buildDirectory
.childDirectory('host')
.childDirectory('outputs')
.childDirectory('apk')
: project.android.buildDirectory
.childDirectory('app')
.childDirectory('outputs')
.childDirectory('flutter-apk');
}
@visibleForTesting
Directory getBundleDirectory(FlutterProject project) {
return project.isModule
? project.android.buildDirectory
.childDirectory('host')
.childDirectory('outputs')
.childDirectory('bundle')
: project.android.buildDirectory
.childDirectory('app')
.childDirectory('outputs')
.childDirectory('bundle');
}
@visibleForTesting
final apkAnalyzerBinaryName = globals.platform.isWindows ? 'apkanalyzer.bat' : 'apkanalyzer';
Directory getRepoDirectory(Directory buildDirectory) {
return buildDirectory.childDirectory('outputs').childDirectory('repo');
}
String _taskFor(String prefix, BuildInfo buildInfo) {
final String buildType = camelCase(buildInfo.modeName);
final String productFlavor = buildInfo.flavor ?? '';
return _taskForBuildVariant(prefix, '$productFlavor${sentenceCase(buildType)}');
}
String _taskForBuildVariant(String prefix, String buildVariant, [String suffix = '']) {
return '$prefix${sentenceCase(buildVariant)}$suffix';
}
@visibleForTesting
String getAssembleTaskFor(BuildInfo buildInfo) {
return _taskFor('assemble', buildInfo);
}
@visibleForTesting
String getBundleTaskFor(BuildInfo buildInfo) {
return _taskFor('bundle', buildInfo);
}
@visibleForTesting
String getAarTaskFor(BuildInfo buildInfo) {
return _taskFor('assembleAar', buildInfo);
}
Iterable<String> _apkFilesFor(AndroidBuildInfo androidBuildInfo) {
final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
final String productFlavor = androidBuildInfo.buildInfo.lowerCasedFlavor ?? '';
final flavorString = productFlavor.isEmpty ? '' : '-$productFlavor';
if (androidBuildInfo.splitPerAbi) {
return androidBuildInfo.targetArchs.map<String>((CpuArch arch) {
final String abi = arch.androidArchName;
return 'app$flavorString-$abi-$buildType.apk';
});
}
return <String>['app$flavorString-$buildType.apk'];
}
const kMaxRetryTime = Duration(seconds: 10);
class AndroidGradleBuilder implements AndroidBuilder {
AndroidGradleBuilder({
required this._analytics,
required AndroidStudio? androidStudio,
required this._artifacts,
required FileSystem fileSystem,
required GradleUtils gradleUtils,
required Java? java,
required Logger logger,
required Platform platform,
required ProcessManager processManager,
required this._toolContext,
AndroidSdk? androidSdk,
}) : _androidContext = AndroidContext(
androidSdkBuilder: () => androidSdk,
androidStudioBuilder: () => androidStudio,
gradleUtilsBuilder: () => gradleUtils,
javaBuilder: () => java,
),
_fileSystem = fileSystem,
_logger = logger,
_fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform),
_processUtils = ProcessUtils(logger: logger, processManager: processManager);
AndroidGradleBuilder.fromContexts({
required this._analytics,
required this._androidContext,
required ToolContext toolContext,
}) : _artifacts = toolContext.artifacts,
_fileSystem = toolContext.fs,
_logger = toolContext.logger,
_fileSystemUtils = toolContext.fileSystemUtils,
_processUtils = toolContext.processUtils,
_toolContext = toolContext;
final Analytics _analytics;
final AndroidContext _androidContext;
late final Java? _java = _androidContext.java;
final Logger _logger;
final FileSystem _fileSystem;
final Artifacts _artifacts;
late final GradleUtils _gradleUtils = _androidContext.gradleUtils;
late final AndroidStudio? _androidStudio = _androidContext.androidStudio;
late final AndroidSdk? _androidSdk = _androidContext.androidSdk;
final FileSystemUtils _fileSystemUtils;
final ProcessUtils _processUtils;
final ToolContext _toolContext;
@override
Future<void> buildAar({
required FlutterProject project,
required Set<AndroidBuildInfo> androidBuildInfo,
required String target,
required Future<void> Function(FlutterProject, {required bool releaseMode}) generateTooling,
String? outputDirectoryPath,
required String buildNumber,
}) async {
Directory outputDirectory = _fileSystem.directory(
outputDirectoryPath ?? project.android.buildDirectory,
);
if (project.isModule) {
outputDirectory = outputDirectory.childDirectory('host');
}
for (final androidBuildInfo in androidBuildInfo) {
await generateTooling(project, releaseMode: androidBuildInfo.buildInfo.isRelease);
await buildGradleAar(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
outputDirectory: outputDirectory,
buildNumber: buildNumber,
);
}
printHowToConsumeAar(
buildModes: androidBuildInfo.map<String>((AndroidBuildInfo androidBuildInfo) {
return androidBuildInfo.buildInfo.modeName;
}).toSet(),
androidPackage: project.manifest.androidPackage,
repoDirectory: getRepoDirectory(outputDirectory),
buildNumber: buildNumber,
logger: _logger,
fileSystem: _fileSystem,
);
}
@override
Future<void> buildApk({
required FlutterProject project,
required AndroidBuildInfo androidBuildInfo,
required String target,
bool configOnly = false,
}) async {
await buildGradleApp(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: false,
localGradleErrors: gradleErrors,
configOnly: configOnly,
maxRetries: 1,
);
}
@override
Future<void> buildAab({
required FlutterProject project,
required AndroidBuildInfo androidBuildInfo,
required String target,
bool validateDeferredComponents = true,
bool deferredComponentsEnabled = false,
bool configOnly = false,
}) async {
await buildGradleApp(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: true,
localGradleErrors: gradleErrors,
validateDeferredComponents: validateDeferredComponents,
deferredComponentsEnabled: deferredComponentsEnabled,
configOnly: configOnly,
maxRetries: 1,
);
}
Future<int> _runGradleTask(
String taskName, {
List<String> options = const <String>[],
required FlutterProject project,
required List<GradleHandledError> localGradleErrors,
required String gradleExecutablePath,
bool printOutput = true,
int retry = 0,
VoidCallback? preRunTask,
VoidCallback? postRunTask,
int? maxRetries,
_OutputParser? outputParser,
}) async {
final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot);
final String? agpVersion = gradle.getAgpVersion(
project.android.hostAppGradleRoot,
globals.logger,
);
if (usesAndroidX) {
_analytics.send(
Event.flutterBuildInfo(
label: 'app-using-android-x',
buildType: 'gradle',
settings: 'androidGradlePluginVersion: $agpVersion',
),
);
} else if (!usesAndroidX) {
_analytics.send(
Event.flutterBuildInfo(
label: 'app-not-using-android-x',
buildType: 'gradle',
settings: 'androidGradlePluginVersion: $agpVersion',
),
);
}
GradleHandledError? detectedGradleError;
String? detectedGradleErrorLine;
String? consumeLog(String line) {
if (outputParser != null) {
outputParser(line);
}
if (detectedGradleError != null && detectedGradleError != incompatibleKotlinVersionHandler) {
return line;
}
for (final gradleError in localGradleErrors) {
if (gradleError.test(line)) {
detectedGradleErrorLine = line;
detectedGradleError = gradleError;
break;
}
}
return printOutput ? line : null;
}
final Status status = _logger.startProgress("Running Gradle task '$taskName'...");
final command = <String>[
gradleExecutablePath,
...options,
taskName,
];
preRunTask?.call();
var exitCode = 1;
try {
exitCode = await _processUtils.stream(
command,
workingDirectory: project.android.hostAppGradleRoot.path,
allowReentrantFlutter: true,
environment: _java?.gradleEnvironment,
mapFunction: consumeLog,
);
} on ProcessException catch (exception) {
consumeLog(exception.toString());
if (detectedGradleError == null) {
rethrow;
}
} finally {
status.stop();
}
postRunTask?.call();
if (exitCode != 0) {
if (detectedGradleError == null) {
_analytics.send(
Event.flutterBuildInfo(label: 'gradle-unknown-failure', buildType: 'gradle'),
);
return exitCode;
}
final GradleBuildStatus status = await detectedGradleError!.handler(
gradleUtils: _gradleUtils,
java: _java,
line: detectedGradleErrorLine!,
project: project,
toolContext: _toolContext,
usesAndroidX: usesAndroidX,
);
if (maxRetries == null || retry < maxRetries) {
switch (status) {
case GradleBuildStatus.retry:
final int waitTime = min(
pow(2, min(retry, 7)).toInt() * 100,
kMaxRetryTime.inMilliseconds,
);
retry += 1;
_logger.printStatus('Retrying Gradle Build: #$retry, wait time: ${waitTime}ms');
await Future<void>.delayed(Duration(milliseconds: waitTime));
exitCode = await _runGradleTask(
taskName,
options: options,
preRunTask: preRunTask,
postRunTask: postRunTask,
localGradleErrors: localGradleErrors,
gradleExecutablePath: gradleExecutablePath,
printOutput: printOutput,
retry: retry,
project: project,
maxRetries: maxRetries,
);
if (exitCode == 0) {
final successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
_analytics.send(
Event.flutterBuildInfo(
label: successEventLabel,
buildType: 'gradle',
settings: 'androidGradlePluginVersion: $agpVersion',
),
);
return exitCode;
}
case GradleBuildStatus.exit:
}
}
final usageLabel = 'gradle-${detectedGradleError!.eventLabel}-failure';
_analytics.send(
Event.flutterBuildInfo(
label: usageLabel,
buildType: 'gradle',
settings: 'androidGradlePluginVersion: $agpVersion',
),
);
}
return exitCode;
}
Future<void> _checkJavaAndGradleCompatibility(
FlutterProject project,
AndroidBuildInfo androidBuildInfo,
) async {
if (!androidBuildInfo.buildInfo.androidGradleConfig.skipBuildDependencyValidation) {
final Version? javaVersionObj = _java?.version;
final String? javaVersion = javaVersionObj != null
? '${javaVersionObj.major}.${javaVersionObj.minor}.${javaVersionObj.patch}'
: null;
final String? gradleVersion = await getGradleVersionFromFile(
project.android.hostAppGradleRoot,
_logger,
);
if (javaVersion != null && gradleVersion != null) {
if (!gradle.validateJavaAndGradle(
_logger,
javaVersion: javaVersion,
gradleVersion: gradleVersion,
)) {
final JavaGradleCompat? compat = gradle.getValidGradleVersionRangeForJavaVersion(
_logger,
javaV: javaVersion,
);
final gradleRangeMax = compat != null && compat.gradleMax != null
? ' to ${compat.gradleMax}'
: '';
final gradleRangeCompatSuggestion = compat != null
? '${compat.gradleMin}$gradleRangeMax or newer'
: 'unknown';
final gradleRangeInfo =
'compatible Gradle versions for Java $javaVersion are $gradleRangeCompatSuggestion';
throwToolExit("""
Gradle build failed due to Java/Gradle incompatibility.
The Java version used for the build is $javaVersion, which is incompatible with Gradle $gradleVersion.
To fix this, you can either:
1. Upgrade your project's Gradle version (typically in gradle-wrapper.properties to a version matching the range: $gradleRangeInfo).
2. Use a different Java version for Flutter by running `flutter config --jdk-dir=<path>`.""");
}
}
}
}
Future<void> buildGradleApp({
required FlutterProject project,
required AndroidBuildInfo androidBuildInfo,
required String target,
required bool isBuildingBundle,
required List<GradleHandledError> localGradleErrors,
required bool configOnly,
bool validateDeferredComponents = true,
bool deferredComponentsEnabled = false,
int retry = 0,
@visibleForTesting int? maxRetries,
}) async {
if (!project.android.isSupportedVersion) {
_exitWithUnsupportedProjectMessage(_logger.terminal, _analytics);
}
final migrators = <ProjectMigrator>[
TopLevelGradleBuildFileMigration(project.android, _logger),
AndroidStudioJavaGradleConflictMigration(
_logger,
project: project.android,
androidStudio: _androidStudio,
java: globals.java,
),
MinSdkVersionMigration(project.android, _logger),
MultidexRemovalMigration(project.android, _logger),
CmakeAndroid16kPagesMigration(project.android, _logger),
DisableBuiltInKotlinMigration(project.android, _logger),
DisableNewDslMigration(project.android, _logger),
];
final migration = ProjectMigration(migrators);
await migration.run();
updateLocalProperties(
project: project,
analytics: _analytics,
androidSdk: _androidSdk,
buildInfo: androidBuildInfo.buildInfo,
fileSystemUtils: _fileSystemUtils,
logger: _logger,
);
final options = <String>[];
final String gradleExecutablePath = _gradleUtils.getExecutable(project);
if (configOnly) {
return;
}
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
final String assembleTask = isBuildingBundle
? getBundleTaskFor(buildInfo)
: getAssembleTaskFor(buildInfo);
if (_logger.isVerbose) {
options.add('--full-stacktrace');
options.add('--info');
options.add('-Pverbose=true');
} else {
options.add('-q');
}
if (!buildInfo.androidGradleConfig.gradleDaemon) {
options.add('--no-daemon');
}
if (buildInfo.androidGradleConfig.skipBuildDependencyValidation) {
options.add('-PskipDependencyChecks=true');
}
final LocalEngineInfo? localEngineInfo = _artifacts.localEngineInfo;
if (localEngineInfo != null) {
final Directory localEngineRepo = _getLocalEngineRepo(
engineOutPath: localEngineInfo.targetOutPath,
androidBuildInfo: androidBuildInfo,
fileSystem: _fileSystem,
);
_logger.printTrace(
'Using local engine: ${localEngineInfo.targetOutPath}\n'
'Local Maven repo: ${localEngineRepo.path}',
);
options.add('-Plocal-engine-repo=${localEngineRepo.path}');
options.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
options.add('-Plocal-engine-out=${localEngineInfo.targetOutPath}');
options.add('-Plocal-engine-host-out=${localEngineInfo.hostOutPath}');
options.add(
'-Ptarget-platform=${_getTargetPlatformByLocalEnginePath(localEngineInfo.targetOutPath)}',
);
} else if (androidBuildInfo.targetArchs.isNotEmpty) {
final String targetPlatforms = androidBuildInfo.targetArchs
.map((CpuArch e) => e.androidPlatformName)
.join(',');
options.add('-Ptarget-platform=$targetPlatforms');
}
if (androidBuildInfo.releaseManifestEngineShellArgs != null) {
final String base64JsonArgs = base64Encode(
utf8.encode(jsonEncode(androidBuildInfo.releaseManifestEngineShellArgs)),
);
options.add('-Pflutter.engineShellArgs=$base64JsonArgs');
}
options.add('-Ptarget=$target');
final baseApplicationName = project.android.getEmbeddingVersion() == AndroidEmbeddingVersion.v2
? 'android.app.Application'
: 'io.flutter.app.FlutterApplication';
options.add('-Pbase-application-name=$baseApplicationName');
final List<DeferredComponent>? deferredComponents = project.manifest.deferredComponents;
if (deferredComponents != null) {
if (deferredComponentsEnabled) {
options.add('-Pdeferred-components=true');
androidBuildInfo.buildInfo.dartDefines.add(
'validate-deferred-components=$validateDeferredComponents',
);
}
final componentNames = <String>[];
for (final DeferredComponent component in deferredComponents) {
componentNames.add(component.name);
}
if (componentNames.isNotEmpty) {
options.add('-Pdeferred-component-names=${componentNames.join(',')}');
_logger.printStatus(
'Shrinking has been disabled for this build due to deferred components. Shrinking is '
'not available for multi-apk applications. This limitation is expected to be removed '
'when Gradle plugin 4.2+ is available in Flutter.',
color: TerminalColor.yellow,
);
options.add('-Pshrink=false');
}
}
options.addAll(androidBuildInfo.toGradleConfig());
if (buildInfo.fileSystemRoots.isNotEmpty) {
options.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
}
if (buildInfo.fileSystemScheme != null) {
options.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
}
options.addAll(_getAndroidNdkProvisioningProperties());
late Stopwatch sw;
final int exitCode = await _runGradleTask(
assembleTask,
preRunTask: () {
sw = Stopwatch()..start();
},
postRunTask: () {
final Duration elapsedDuration = sw.elapsed;
_analytics.send(
Event.timing(
workflow: 'build',
variableName: 'gradle',
elapsedMilliseconds: elapsedDuration.inMilliseconds,
),
);
},
options: options,
project: project,
maxRetries: maxRetries,
localGradleErrors: localGradleErrors,
gradleExecutablePath: gradleExecutablePath,
);
if (exitCode != 0) {
await _checkJavaAndGradleCompatibility(project, androidBuildInfo);
throwToolExit(
'Gradle task $assembleTask failed with exit code $exitCode',
exitCode: exitCode,
);
}
if (isBuildingBundle) {
final File bundleFile = findBundleFile(project, buildInfo, _logger, _analytics);
if ((buildInfo.mode == BuildMode.release) &&
!(await _isAabStrippedOfDebugSymbols(
project,
bundleFile.path,
androidBuildInfo.targetArchs,
))) {
throwToolExit(failedToStripDebugSymbolsErrorMessage);
}
final appSize = (buildInfo.mode == BuildMode.debug)
? ''
: ' (${getSizeAsPlatformMB(bundleFile.lengthSync())})';
if (buildInfo.codeSizeDirectory != null) {
await _performCodeSizeAnalysis('aab', bundleFile, androidBuildInfo);
}
_logger.printStatus(
'${_logger.terminal.successMark} '
'Built ${_fileSystem.path.relative(bundleFile.path)}$appSize',
color: TerminalColor.green,
);
return;
}
final Iterable<String> apkFilesPaths = project.isModule
? findApkFilesModule(project, androidBuildInfo, _logger, _analytics)
: listApkPaths(androidBuildInfo);
final Directory apkDirectory = getApkDirectory(project);
for (final File apkFile in apkFilesPaths.map(apkDirectory.childFile)) {
if (!apkFile.existsSync()) {
_exitWithExpectedFileNotFound(
project: project,
fileExtension: '.apk',
logger: _logger,
analytics: _analytics,
);
}
final String filename = apkFile.basename;
_logger.printTrace('Calculate SHA1: $apkDirectory/$filename');
final File apkShaFile = apkDirectory.childFile('$filename.sha1');
apkShaFile.writeAsStringSync(calculateSha(apkFile));
final appSize = (buildInfo.mode == BuildMode.debug)
? ''
: ' (${getSizeAsPlatformMB(apkFile.lengthSync())})';
_logger.printStatus(
'${_logger.terminal.successMark} '
'Built ${_fileSystem.path.relative(apkFile.path)}$appSize',
color: TerminalColor.green,
);
if (buildInfo.codeSizeDirectory != null) {
await _performCodeSizeAnalysis('apk', apkFile, androidBuildInfo);
}
}
}
Future<bool> _isAabStrippedOfDebugSymbols(
FlutterProject project,
String aabPath,
Iterable<CpuArch> targetArchs,
) async {
if (_androidSdk == null) {
_logger.printTrace(
'Failed to find android sdk when checking final appbundle for debug symbols.',
);
return false;
}
final AndroidSdk androidSdk = _androidSdk;
if (!androidSdk.cmdlineToolsAvailable) {
_logger.printTrace(
'Failed to find cmdline-tools when checking final appbundle for debug symbols.',
);
return false;
}
final String? apkAnalyzerPath = androidSdk.getCmdlineToolsPath(apkAnalyzerBinaryName);
if (apkAnalyzerPath == null) {
_logger.printTrace(
'Failed to find apkanalyzer when checking final appbundle for debug symbols.',
);
return false;
}
final RunResult result = await _processUtils.run(
<String>[apkAnalyzerPath, 'files', 'list', aabPath],
workingDirectory: project.android.hostAppGradleRoot.path,
environment: _java?.environment,
);
if (result.exitCode != 0) {
_logger.printTrace(
'apkanalyzer finished with exit code 0 when checking final appbundle for debug symbols.\n'
'stderr was: ${result.stderr}\n'
'and stdout was: ${result.stdout}',
);
return false;
}
if (!(result.stdout.contains('libflutter.so.sym') ||
result.stdout.contains('libflutter.so.dbg'))) {
_logger.printTrace(
'libflutter.so.sym or libflutter.so.dbg not present when checking final appbundle for debug symbols.',
);
return false;
}
if (!(result.stdout.contains('libapp.so.sym') || result.stdout.contains('libapp.so.dbg'))) {
_logger.printTrace(
'libapp.so.sym or libapp.so.dbg not present when checking final appbundle for debug symbols.',
);
return false;
}
return true;
}
Future<void> _performCodeSizeAnalysis(
String kind,
File zipFile,
AndroidBuildInfo androidBuildInfo,
) async {
final sizeAnalyzer = SizeAnalyzer(
fileSystem: _fileSystem,
logger: _logger,
analytics: _analytics,
);
final String archName = androidBuildInfo.targetArchs.single.androidArchName;
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
final File aotSnapshot = _fileSystem
.directory(buildInfo.codeSizeDirectory)
.childFile('snapshot.$archName.json');
final File precompilerTrace = _fileSystem
.directory(buildInfo.codeSizeDirectory)
.childFile('trace.$archName.json');
final Map<String, Object?> output = await sizeAnalyzer.analyzeZipSizeAndAotSnapshot(
zipFile: zipFile,
aotSnapshot: aotSnapshot,
precompilerTrace: precompilerTrace,
kind: kind,
);
final File outputFile = _fileSystemUtils.getUniqueFile(
_fileSystem.directory(_fileSystemUtils.homeDirPath).childDirectory('.flutter-devtools'),
'$kind-code-size-analysis',
'json',
)..writeAsStringSync(jsonEncode(output));
_logger.printStatus(
'A summary of your ${kind.toUpperCase()} analysis can be found at: ${outputFile.path}',
);
_logger.printStatus(
'\nTo analyze your app size in Dart DevTools, run the following command:\n'
'dart devtools --appSizeBase=${outputFile.path}',
);
}
Future<void> buildGradleAar({
required FlutterProject project,
required AndroidBuildInfo androidBuildInfo,
required String target,
required Directory outputDirectory,
required String buildNumber,
}) async {
final FlutterManifest manifest = project.manifest;
if (!manifest.isModule) {
throwToolExit('AARs can only be built for module projects.');
}
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
final String aarTask = getAarTaskFor(buildInfo);
final Status status = _logger.startProgress("Running Gradle task '$aarTask'...");
final String flutterRoot = _fileSystem.path.absolute(Cache.flutterRoot!);
final String initScript = _fileSystem.path.join(
flutterRoot,
'packages',
'flutter_tools',
'gradle',
'aar_init_script.gradle',
);
final command = <String>[
_gradleUtils.getExecutable(project),
'-I=$initScript',
'-Pflutter-root=$flutterRoot',
'-Poutput-dir=${outputDirectory.path}',
'-Pis-plugin=${manifest.isPlugin}',
'-PbuildNumber=$buildNumber',
];
if (_logger.isVerbose) {
command.add('--full-stacktrace');
command.add('--info');
command.add('-Pverbose=true');
} else {
command.add('-q');
}
if (!buildInfo.androidGradleConfig.gradleDaemon) {
command.add('--no-daemon');
}
if (target.isNotEmpty) {
command.add('-Ptarget=$target');
}
command.addAll(androidBuildInfo.toGradleConfig());
command.addAll(_getAndroidNdkProvisioningProperties());
if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) {
_logger.printStatus(
'Dart obfuscation is not supported in ${buildInfo.mode.uppercaseFriendlyName}'
' mode, building as un-obfuscated.',
);
}
final LocalEngineInfo? localEngineInfo = _artifacts.localEngineInfo;
if (localEngineInfo != null) {
final Directory localEngineRepo = _getLocalEngineRepo(
engineOutPath: localEngineInfo.targetOutPath,
androidBuildInfo: androidBuildInfo,
fileSystem: _fileSystem,
);
_logger.printTrace(
'Using local engine: ${localEngineInfo.targetOutPath}\n'
'Local Maven repo: ${localEngineRepo.path}',
);
command.add('-Plocal-engine-repo=${localEngineRepo.path}');
command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
command.add('-Plocal-engine-out=${localEngineInfo.targetOutPath}');
command.add('-Plocal-engine-host-out=${localEngineInfo.hostOutPath}');
try {
copyDirectory(localEngineRepo, getRepoDirectory(outputDirectory));
} on FileSystemException catch (error, st) {
throwToolExit(
'Failed to copy the local engine ${localEngineRepo.path} repo '
'in ${outputDirectory.path}: $error, $st',
);
}
command.add(
'-Ptarget-platform=${_getTargetPlatformByLocalEnginePath(localEngineInfo.targetOutPath)}',
);
} else if (androidBuildInfo.targetArchs.isNotEmpty) {
final String targetPlatforms = androidBuildInfo.targetArchs
.map((CpuArch e) => e.androidPlatformName)
.join(',');
command.add('-Ptarget-platform=$targetPlatforms');
}
command.add(aarTask);
final sw = Stopwatch()..start();
RunResult result;
try {
result = await _processUtils.run(
command,
workingDirectory: project.android.hostAppGradleRoot.path,
allowReentrantFlutter: true,
environment: _java?.gradleEnvironment,
);
} finally {
status.stop();
}
final Duration elapsedDuration = sw.elapsed;
_analytics.send(
Event.timing(
workflow: 'build',
variableName: 'gradle-aar',
elapsedMilliseconds: elapsedDuration.inMilliseconds,
),
);
if (result.exitCode != 0) {
_logger.printStatus(result.stdout, wrap: false);
_logger.printError(result.stderr, wrap: false);
await _checkJavaAndGradleCompatibility(project, androidBuildInfo);
throwToolExit(
'Gradle task $aarTask failed with exit code ${result.exitCode}.',
exitCode: result.exitCode,
);
}
final Directory repoDirectory = getRepoDirectory(outputDirectory);
if (!repoDirectory.existsSync()) {
_logger.printStatus(result.stdout, wrap: false);
_logger.printError(result.stderr, wrap: false);
throwToolExit('Gradle task $aarTask failed to produce $repoDirectory.', exitCode: exitCode);
}
_logger.printStatus(
'${_logger.terminal.successMark} '
'Built ${_fileSystem.path.relative(repoDirectory.path)}',
color: TerminalColor.green,
);
}
@override
Future<List<String>> getBuildVariants({required FlutterProject project}) async {
late Stopwatch sw;
var exitCode = 1;
final results = <String>[];
try {
exitCode = await _runGradleTask(
_kBuildVariantTaskName,
preRunTask: () {
sw = Stopwatch()..start();
},
postRunTask: () {
final Duration elapsedDuration = sw.elapsed;
_analytics.send(
Event.timing(
workflow: 'print',
variableName: 'android build variants',
elapsedMilliseconds: elapsedDuration.inMilliseconds,
),
);
},
options: const <String>['-q'],
project: project,
localGradleErrors: gradleErrors,
gradleExecutablePath: _gradleUtils.getExecutable(project),
outputParser: (String line) {
if (_kBuildVariantRegex.firstMatch(line) case final RegExpMatch match) {
results.add(match.namedGroup(_kBuildVariantRegexGroupName)!);
}
},
);
} on Error catch (error) {
_logger.printError(error.toString());
}
if (exitCode != 0) {
return const <String>[];
}
return results;
}
@override
Future<String> outputsAppLinkSettings(
String buildVariant, {
required FlutterProject project,
}) async {
final String taskName = _getOutputAppLinkSettingsTaskFor(buildVariant);
final Directory directory = await project.buildDirectory
.childDirectory('deeplink_data')
.create(recursive: true);
final String outputPath = globals.fs.path.join(
directory.absolute.path,
'app-link-settings-$buildVariant.json',
);
late Stopwatch sw;
var exitCode = 1;
try {
exitCode = await _runGradleTask(
taskName,
preRunTask: () {
sw = Stopwatch()..start();
},
postRunTask: () {
final Duration elapsedDuration = sw.elapsed;
_analytics.send(
Event.timing(
workflow: 'outputs',
variableName: 'app link settings',
elapsedMilliseconds: elapsedDuration.inMilliseconds,
),
);
},
options: <String>['-q', '-PoutputPath=$outputPath'],
project: project,
localGradleErrors: gradleErrors,
gradleExecutablePath: _gradleUtils.getExecutable(project),
);
} on Error catch (error) {
_logger.printError(error.toString());
}
if (exitCode != 0) {
throwToolExit('Gradle task $taskName failed with exit code $exitCode');
}
return outputPath;
}
List<String> _getAndroidNdkProvisioningProperties() {
final AndroidSdk? androidSdk = _androidSdk;
if (androidSdk == null || !androidSdk.directory.existsSync()) {
return const <String>[];
}
final properties = <String>[
'-P$_kAndroidSdkRootProperty=${androidSdk.directory.path}',
'-P$_kInstalledNdkVersionsProperty=${_getInstalledNdkVersionsForGradle(androidSdk).join(',')}',
];
final String? sdkManagerPath = androidSdk.sdkManagerPath;
if (sdkManagerPath != null &&
androidSdk.cmdlineToolsAvailable &&
androidSdk.licensesAvailable) {
properties.add('-P$_kSdkManagerPathProperty=$sdkManagerPath');
}
return properties;
}
}
void printHowToConsumeAar({
required Set<String> buildModes,
String? androidPackage = 'unknown',
required Directory repoDirectory,
required Logger logger,
required FileSystem fileSystem,
String? buildNumber,
}) {
assert(buildModes.isNotEmpty);
buildNumber ??= '1.0';
logger.printStatus('\nConsuming the Module', emphasis: true);
logger.printStatus('''
1. Open ${fileSystem.path.join('<host>', 'app', 'build.gradle')}
2. Ensure you have the repositories configured, otherwise add them:
String storageUrl = System.env.$kFlutterStorageBaseUrl ?: "https://storage.googleapis.com"
repositories {
maven {
url '${repoDirectory.path}'
}
maven {
url "\$storageUrl/download.flutter.io"
}
}
3. Make the host app depend on the Flutter module:
dependencies {''');
for (final buildMode in buildModes) {
logger.printStatus("""
${buildMode}Implementation '$androidPackage:flutter_$buildMode:$buildNumber'""");
}
logger.printStatus('''
}
''');
if (buildModes.contains('profile')) {
logger.printStatus('''
4. Add the `profile` build type:
android {
buildTypes {
profile {
initWith debug
}
}
}
''');
}
logger.printStatus('To learn more, visit https://flutter.dev/to/integrate-android-archive');
}
@visibleForTesting
String calculateSha(File file) {
final RandomAccessFile openedFile = file.openSync();
try {
final sink = AccumulatorSink<Digest>();
final ByteConversionSink sha1Sink = sha1.startChunkedConversion(sink);
final buffer = Uint8List(64 * 1024);
while (true) {
final int bytesRead = openedFile.readIntoSync(buffer);
if (bytesRead == 0) {
break;
}
sha1Sink.add(Uint8List.sublistView(buffer, 0, bytesRead));
}
sha1Sink.close();
return sink.events.single.toString();
} finally {
openedFile.closeSync();
}
}
void _exitWithUnsupportedProjectMessage(Terminal terminal, Analytics analytics) {
analytics.send(
Event.flutterBuildInfo(
label: 'unsupported-project',
buildType: 'gradle',
error: 'gradle-plugin',
),
);
throwToolExit(
'${terminal.warningMark} Your app is using an unsupported Gradle project. '
'To fix this problem, create a new project by running `flutter create -t app <app-directory>` '
'and then move the dart code, assets and pubspec.yaml to the new project.',
);
}
bool isAppUsingAndroidX(Directory androidDirectory) {
final File properties = androidDirectory.childFile('gradle.properties');
if (!properties.existsSync()) {
return false;
}
bool? usesAndroidX;
final androidXRegExp = RegExp(r'^android\.useAndroidX(?:\s*[=:]\s*|\s+)(\S+)');
for (final String rawLine in properties.readAsLinesSync()) {
final String line = rawLine.trimLeft();
if (line.isEmpty || line.startsWith('#') || line.startsWith('!')) {
continue;
}
final RegExpMatch? match = androidXRegExp.firstMatch(line);
if (match == null) {
continue;
}
final String value = match.group(1)!.toLowerCase();
if (value == 'true') {
usesAndroidX = true;
} else if (value == 'false') {
usesAndroidX = false;
}
}
return usesAndroidX ?? false;
}
@visibleForTesting
Iterable<String> findApkFilesModule(
FlutterProject project,
AndroidBuildInfo androidBuildInfo,
Logger logger,
Analytics analytics,
) {
final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
final Directory apkDirectory = getApkDirectory(project);
final Iterable<File> apks = apkFileNames.expand<File>((String apkFileName) {
File apkFile = apkDirectory.childFile(apkFileName);
if (apkFile.existsSync()) {
return <File>[apkFile];
}
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
final String modeName = camelCase(buildInfo.modeName);
apkFile = apkDirectory.childDirectory(modeName).childFile(apkFileName);
if (apkFile.existsSync()) {
return <File>[apkFile];
}
final String? flavor = buildInfo.flavor;
if (flavor != null) {
apkFile = apkDirectory.childDirectory(flavor).childDirectory(modeName).childFile(apkFileName);
if (apkFile.existsSync()) {
return <File>[apkFile];
}
}
return const <File>[];
});
if (apks.isEmpty) {
_exitWithExpectedFileNotFound(
project: project,
fileExtension: '.apk',
logger: logger,
analytics: analytics,
);
}
return apks.map((File file) => file.path);
}
@visibleForTesting
Iterable<String> listApkPaths(AndroidBuildInfo androidBuildInfo) {
final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
final apkPartialName = <String>[
if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false)
androidBuildInfo.buildInfo.lowerCasedFlavor!,
'$buildType.apk',
];
if (androidBuildInfo.splitPerAbi) {
return <String>[
for (final CpuArch cpuArch in androidBuildInfo.targetArchs)
<String>['app', cpuArch.androidArchName, ...apkPartialName].join('-'),
];
}
return <String>[
<String>['app', ...apkPartialName].join('-'),
];
}
@visibleForTesting
File findBundleFile(
FlutterProject project,
BuildInfo buildInfo,
Logger logger,
Analytics analytics,
) {
final Directory bundleDir = getBundleDirectory(project);
if (!bundleDir.existsSync()) {
_exitWithExpectedFileNotFound(
project: project,
fileExtension: '.aab',
logger: logger,
analytics: analytics,
);
}
final Iterable<File> allBundleFiles = bundleDir.listSync(recursive: true).whereType<File>().where(
(File file) {
return file.path.endsWith('.aab');
},
);
for (final bundleFile in allBundleFiles) {
final String bundleParentDir = bundleFile.parent.basename.toLowerCase();
if (buildInfo.flavor != null) {
if (bundleParentDir.contains(
'${buildInfo.lowerCasedFlavor}${buildInfo.modeName.toLowerCase()}',
) &&
bundleFile.basename.endsWith('${buildInfo.modeName}.aab')) {
return bundleFile;
}
if (bundleParentDir.contains('${buildInfo.lowerCasedFlavor}${buildInfo.modeName}')) {
return bundleFile;
}
}
if (bundleParentDir == buildInfo.modeName &&
bundleFile.basename.endsWith('${buildInfo.modeName}.aab')) {
return bundleFile;
}
if (bundleParentDir == buildInfo.modeName && bundleFile.basename.endsWith('aab')) {
return bundleFile;
}
}
_exitWithExpectedFileNotFound(
project: project,
fileExtension: '.aab',
logger: logger,
analytics: analytics,
);
}
Never _exitWithExpectedFileNotFound({
required FlutterProject project,
required String fileExtension,
required Logger logger,
required Analytics analytics,
}) {
final String androidGradlePluginVersion = getGradleVersionForAndroidPlugin(
project.android.hostAppGradleRoot,
logger,
);
final gradleBuildSettings =
'androidGradlePluginVersion: $androidGradlePluginVersion, '
'fileExtension: $fileExtension';
analytics.send(
Event.flutterBuildInfo(
label: 'gradle-expected-file-not-found',
buildType: 'gradle',
settings: gradleBuildSettings,
),
);
throwToolExit(
'Gradle build failed to produce an $fileExtension file. '
"It's likely that this file was generated under ${project.android.buildDirectory.path}, "
"but the tool couldn't find it.",
);
}
void _createSymlink(String targetPath, String linkPath, FileSystem fileSystem) {
final File targetFile = fileSystem.file(targetPath);
if (!targetFile.existsSync()) {
throwToolExit("The file $targetPath wasn't found in the local engine out directory.");
}
final File linkFile = fileSystem.file(linkPath);
final Link symlink = linkFile.parent.childLink(linkFile.basename);
try {
symlink.createSync(targetPath, recursive: true);
} on FileSystemException catch (exception) {
throwToolExit('Failed to create the symlink $linkPath->$targetPath: $exception');
}
}
String _getLocalArtifactVersion(String pomPath, FileSystem fileSystem) {
final File pomFile = fileSystem.file(pomPath);
if (!pomFile.existsSync()) {
throwToolExit("The file $pomPath wasn't found in the local engine out directory.");
}
XmlDocument document;
try {
document = XmlDocument.parse(pomFile.readAsStringSync());
} on XmlException {
throwToolExit('Error parsing $pomPath. Please ensure that this is a valid XML document.');
} on FileSystemException {
throwToolExit(
'Error reading $pomPath. Please ensure that you have read permission to this '
'file and try again.',
);
}
final Iterable<XmlElement> project = document.findElements('project');
assert(project.isNotEmpty);
for (final XmlElement versionElement in document.findAllElements('version')) {
if (versionElement.parent == project.first) {
return versionElement.innerText;
}
}
throwToolExit('Error while parsing the <version> element from $pomPath');
}
Directory _getLocalEngineRepo({
required String engineOutPath,
required AndroidBuildInfo androidBuildInfo,
required FileSystem fileSystem,
}) {
final String abi = _getAbiByLocalEnginePath(engineOutPath);
final Directory localEngineRepo = fileSystem.systemTempDirectory.createTempSync(
'flutter_tool_local_engine_repo.',
);
final String buildMode = androidBuildInfo.buildInfo.modeName;
final String artifactVersion = _getLocalArtifactVersion(
fileSystem.path.join(engineOutPath, 'flutter_embedding_$buildMode.pom'),
fileSystem,
);
for (final artifact in const <String>['pom', 'jar']) {
_createSymlink(
fileSystem.path.join(engineOutPath, 'flutter_embedding_$buildMode.$artifact'),
fileSystem.path.join(
localEngineRepo.path,
'io',
'flutter',
'flutter_embedding_$buildMode',
artifactVersion,
'flutter_embedding_$buildMode-$artifactVersion.$artifact',
),
fileSystem,
);
_createSymlink(
fileSystem.path.join(engineOutPath, '${abi}_$buildMode.$artifact'),
fileSystem.path.join(
localEngineRepo.path,
'io',
'flutter',
'${abi}_$buildMode',
artifactVersion,
'${abi}_$buildMode-$artifactVersion.$artifact',
),
fileSystem,
);
}
for (final artifact in <String>['flutter_embedding_$buildMode', '${abi}_$buildMode']) {
_createSymlink(
fileSystem.path.join(engineOutPath, '$artifact.maven-metadata.xml'),
fileSystem.path.join(localEngineRepo.path, 'io', 'flutter', artifact, 'maven-metadata.xml'),
fileSystem,
);
}
return localEngineRepo;
}
String _getAbiByLocalEnginePath(String engineOutPath) {
var result = 'armeabi_v7a';
if (engineOutPath.contains('x64')) {
result = 'x86_64';
} else if (engineOutPath.contains('arm64')) {
result = 'arm64_v8a';
}
return result;
}
String _getTargetPlatformByLocalEnginePath(String engineOutPath) {
var result = 'android-arm';
if (engineOutPath.contains('x64')) {
result = 'android-x64';
} else if (engineOutPath.contains('arm64')) {
result = 'android-arm64';
}
return result;
}
List<String> _getInstalledNdkVersionsForGradle(AndroidSdk androidSdk) {
final Directory ndkDir = androidSdk.directory.childDirectory('ndk');
if (!ndkDir.existsSync()) {
return const <String>[];
}
final List<String> installedNdkVersions =
ndkDir
.listSync()
.whereType<Directory>()
.map((Directory dir) => dir.basename)
.where(
(String version) =>
ndkDir.childDirectory(version).childFile('source.properties').existsSync(),
)
.toList()
..sort();
return installedNdkVersions;
}