// 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. // @dart = 2.8 import 'package:archive/archive.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_studio.dart'; import 'package:flutter_tools/src/android/gradle.dart'; import 'package:flutter_tools/src/android/gradle_errors.dart'; import 'package:flutter_tools/src/android/gradle_utils.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:mockito/mockito.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/mocks.dart'; void main() { Cache.flutterRoot = getFlutterRoot(); group('build artifacts', () { FileSystem fileSystem; setUp(() { fileSystem = MemoryFileSystem.test(); }); testWithoutContext('getApkDirectory in app projects', () { final FlutterProject project = MockFlutterProject(); final AndroidProject androidProject = MockAndroidProject(); when(project.android).thenReturn(androidProject); when(project.isModule).thenReturn(false); when(androidProject.buildDirectory).thenReturn(fileSystem.directory('foo')); expect( getApkDirectory(project).path, equals(fileSystem.path.join('foo', 'app', 'outputs', 'flutter-apk')), ); }); testWithoutContext('getApkDirectory in module projects', () { final FlutterProject project = MockFlutterProject(); final AndroidProject androidProject = MockAndroidProject(); when(project.android).thenReturn(androidProject); when(project.isModule).thenReturn(true); when(androidProject.buildDirectory).thenReturn(fileSystem.directory('foo')); expect( getApkDirectory(project).path, equals(fileSystem.path.join('foo', 'host', 'outputs', 'apk')), ); }); testWithoutContext('getBundleDirectory in app projects', () { final FlutterProject project = MockFlutterProject(); final AndroidProject androidProject = MockAndroidProject(); when(project.android).thenReturn(androidProject); when(project.isModule).thenReturn(false); when(androidProject.buildDirectory).thenReturn(fileSystem.directory('foo')); expect( getBundleDirectory(project).path, equals(fileSystem.path.join('foo', 'app', 'outputs', 'bundle')), ); }); testWithoutContext('getBundleDirectory in module projects', () { final FlutterProject project = MockFlutterProject(); final AndroidProject androidProject = MockAndroidProject(); when(project.android).thenReturn(androidProject); when(project.isModule).thenReturn(true); when(androidProject.buildDirectory).thenReturn(fileSystem.directory('foo')); expect( getBundleDirectory(project).path, equals(fileSystem.path.join('foo', 'host', 'outputs', 'bundle')), ); }); testWithoutContext('getRepoDirectory', () { expect( getRepoDirectory(fileSystem.directory('foo')).path, equals(fileSystem.path.join('foo','outputs', 'repo')), ); }); }); group('gradle tasks', () { testWithoutContext('assemble release', () { expect( getAssembleTaskFor(const BuildInfo(BuildMode.release, null, treeShakeIcons: false)), equals('assembleRelease'), ); expect( getAssembleTaskFor(const BuildInfo(BuildMode.release, 'flavorFoo', treeShakeIcons: false)), equals('assembleFlavorFooRelease'), ); }); testWithoutContext('assemble debug', () { expect( getAssembleTaskFor(BuildInfo.debug), equals('assembleDebug'), ); expect( getAssembleTaskFor(const BuildInfo(BuildMode.debug, 'flavorFoo', treeShakeIcons: false)), equals('assembleFlavorFooDebug'), ); }); testWithoutContext('assemble profile', () { expect( getAssembleTaskFor(const BuildInfo(BuildMode.profile, null, treeShakeIcons: false)), equals('assembleProfile'), ); expect( getAssembleTaskFor(const BuildInfo(BuildMode.profile, 'flavorFoo', treeShakeIcons: false)), equals('assembleFlavorFooProfile'), ); }); }); group('findBundleFile', () { FileSystem fileSystem; TestUsage testUsage; setUp(() { fileSystem = MemoryFileSystem.test(); testUsage = TestUsage(); }); testWithoutContext('Finds app bundle when flavor contains underscores in release mode', () { final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app.aab')); }); testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in release mode', () { final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_Bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app.aab')); }); testWithoutContext("Finds app bundle when flavor doesn't contain underscores in release mode", () { final FlutterProject project = generateFakeAppBundle('fooRelease', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooRelease', 'app.aab')); }); testWithoutContext("Finds app bundle when flavor doesn't contain underscores but contains uppercase letters in release mode", () { final FlutterProject project = generateFakeAppBundle('fooaRelease', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'fooA', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooaRelease', 'app.aab')); }); testWithoutContext('Finds app bundle when no flavor is used in release mode', () { final FlutterProject project = generateFakeAppBundle('release', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null, treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app.aab')); }); testWithoutContext('Finds app bundle when flavor contains underscores in debug mode', () { final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barDebug', 'app.aab')); }); testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in debug mode', () { final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_Bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barDebug', 'app.aab')); }); testWithoutContext("Finds app bundle when flavor doesn't contain underscores in debug mode", () { final FlutterProject project = generateFakeAppBundle('fooDebug', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooDebug', 'app.aab')); }); testWithoutContext("Finds app bundle when flavor doesn't contain underscores but contains uppercase letters in debug mode", () { final FlutterProject project = generateFakeAppBundle('fooaDebug', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'fooA', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooaDebug', 'app.aab')); }); testWithoutContext('Finds app bundle when no flavor is used in debug mode', () { final FlutterProject project = generateFakeAppBundle('debug', 'app.aab', fileSystem); final File bundle = findBundleFile(project, BuildInfo.debug); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app.aab')); }); testWithoutContext('Finds app bundle when flavor contains underscores in profile mode', () { final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app.aab')); }); testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in profile mode', () { final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_Bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app.aab')); }); testWithoutContext("Finds app bundle when flavor doesn't contain underscores in profile mode", () { final FlutterProject project = generateFakeAppBundle('fooProfile', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooProfile', 'app.aab')); }); testWithoutContext("Finds app bundle when flavor doesn't contain underscores but contains uppercase letters in profile mode", () { final FlutterProject project = generateFakeAppBundle('fooaProfile', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'fooA', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooaProfile', 'app.aab')); }); testWithoutContext('Finds app bundle when no flavor is used in profile mode', () { final FlutterProject project = generateFakeAppBundle('profile', 'app.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null, treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app.aab')); }); testWithoutContext('Finds app bundle in release mode - Gradle 3.5', () { final FlutterProject project = generateFakeAppBundle('release', 'app-release.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null, treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app-release.aab')); }); testWithoutContext('Finds app bundle in profile mode - Gradle 3.5', () { final FlutterProject project = generateFakeAppBundle('profile', 'app-profile.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null, treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app-profile.aab')); }); testWithoutContext('Finds app bundle in debug mode - Gradle 3.5', () { final FlutterProject project = generateFakeAppBundle('debug', 'app-debug.aab', fileSystem); final File bundle = findBundleFile(project, BuildInfo.debug); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app-debug.aab')); }); testWithoutContext('Finds app bundle when flavor contains underscores in release mode - Gradle 3.5', () { final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app-foo_bar-release.aab')); }); testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in release mode - Gradle 3.5', () { final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_Bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app-foo_bar-release.aab')); }); testWithoutContext('Finds app bundle when flavor contains underscores in profile mode - Gradle 3.5', () { final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app-foo_bar-profile.aab')); }); testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in debug mode - Gradle 3.5', () { final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab', fileSystem); final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_Bar', treeShakeIcons: false)); expect(bundle, isNotNull); expect(bundle.path, fileSystem.path.join('irrelevant','app', 'outputs', 'bundle', 'foo_barDebug', 'app-foo_bar-debug.aab')); }); testUsingContext('aab not found', () { final FlutterProject project = FlutterProject.current(); expect( () { findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar', treeShakeIcons: false)); }, throwsToolExit( message: "Gradle build failed to produce an .aab file. It's likely that this file " "was generated under ${project.android.buildDirectory.path}, but the tool couldn't find it." ) ); expect(testUsage.events, contains( const TestUsageEvent( 'build', 'unspecified', label: 'gradle-expected-file-not-found', parameters: <String, String> { 'cd37': 'androidGradlePluginVersion: 6.7, fileExtension: .aab', }, ), )); }, overrides: <Type, Generator>{ FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), Usage: () => testUsage, }); }); group('listApkPaths', () { testWithoutContext('Finds APK without flavor in release', () { final Iterable<String> apks = listApkPaths( const AndroidBuildInfo(BuildInfo(BuildMode.release, '', treeShakeIcons: false)), ); expect(apks, <String>['app-release.apk']); }); testWithoutContext('Finds APK with flavor in release mode', () { final Iterable<String> apks = listApkPaths( const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1', treeShakeIcons: false)), ); expect(apks, <String>['app-flavor1-release.apk']); }); testWithoutContext('Finds APK with flavor in release mode', () { final Iterable<String> apks = listApkPaths( const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavorA', treeShakeIcons: false)), ); expect(apks, <String>['app-flavora-release.apk']); }); testWithoutContext('Finds APK with flavor in release mode - AGP v3', () { final Iterable<String> apks = listApkPaths( const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1', treeShakeIcons: false)), ); expect(apks, <String>['app-flavor1-release.apk']); }); testWithoutContext('Finds APK with split-per-abi', () { final Iterable<String> apks = listApkPaths( const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1', treeShakeIcons: false), splitPerAbi: true), ); expect(apks, unorderedEquals(<String>[ 'app-armeabi-v7a-flavor1-release.apk', 'app-arm64-v8a-flavor1-release.apk', 'app-x86_64-flavor1-release.apk', ])); }); testWithoutContext('Finds APK with split-per-abi when flavor contains uppercase letters', () { final Iterable<String> apks = listApkPaths( const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavorA', treeShakeIcons: false), splitPerAbi: true), ); expect(apks, unorderedEquals(<String>[ 'app-armeabi-v7a-flavora-release.apk', 'app-arm64-v8a-flavora-release.apk', 'app-x86_64-flavora-release.apk', ])); }); }); group('gradle build', () { testUsingContext('do not crash if there is no Android SDK', () async { expect(() { updateLocalProperties(project: FlutterProject.current()); }, throwsToolExit( message: '$warningMark No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.', )); }, overrides: <Type, Generator>{ AndroidSdk: () => null, }); test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () { final List<String> nonMatchingLines = <String>[ ':app:preBuild UP-TO-DATE', 'BUILD SUCCESSFUL in 0s', 'Generic plugin AndroidX text', '', ]; final List<String> matchingLines = <String>[ '*********************************************************************************************************************************', "WARNING: This version of image_picker will break your Android build if it or its dependencies aren't compatible with AndroidX.", 'See https://goo.gl/CP92wY for more information on the problem and how to fix it.', 'This warning prints for all Android build failures. The real root cause of the error may be unrelated.', ]; for (final String m in nonMatchingLines) { expect(androidXPluginWarningRegex.hasMatch(m), isFalse); } for (final String m in matchingLines) { expect(androidXPluginWarningRegex.hasMatch(m), isTrue); } }); }); group('Config files', () { Directory tempDir; setUp(() { tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_settings_aar_test.'); }); testUsingContext('create settings_aar.gradle when current settings.gradle loads plugins', () { const String currentSettingsGradle = r''' include ':app' def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() def plugins = new Properties() def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') if (pluginsFile.exists()) { pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } } plugins.each { name, path -> def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() if (pluginDirectory.exists()) { include ":$name" project(":$name").projectDir = pluginDirectory } } '''; const String settingsAarFile = ''' include ':app' '''; tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle); final String toolGradlePath = globals.fs.path.join( globals.fs.path.absolute(Cache.flutterRoot), 'packages', 'flutter_tools', 'gradle'); globals.fs.directory(toolGradlePath).createSync(recursive: true); globals.fs.file(globals.fs.path.join(toolGradlePath, 'settings.gradle.legacy_versions')) .writeAsStringSync(currentSettingsGradle); globals.fs.file(globals.fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl')) .writeAsStringSync(settingsAarFile); createSettingsAarGradle(tempDir); expect(testLogger.statusText, contains('created successfully')); expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue); }, overrides: <Type, Generator>{ FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext("create settings_aar.gradle when current settings.gradle doesn't load plugins", () { const String currentSettingsGradle = ''' include ':app' '''; const String settingsAarFile = ''' include ':app' '''; tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle); final String toolGradlePath = globals.fs.path.join( globals.fs.path.absolute(Cache.flutterRoot), 'packages', 'flutter_tools', 'gradle'); globals.fs.directory(toolGradlePath).createSync(recursive: true); globals.fs.file(globals.fs.path.join(toolGradlePath, 'settings.gradle.legacy_versions')) .writeAsStringSync(currentSettingsGradle); globals.fs.file(globals.fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl')) .writeAsStringSync(settingsAarFile); createSettingsAarGradle(tempDir); expect(testLogger.statusText, contains('created successfully')); expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue); }, overrides: <Type, Generator>{ FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); }); group('Gradle local.properties', () { MockLocalEngineArtifacts mockArtifacts; MockProcessManager mockProcessManager; FakePlatform android; FileSystem fs; setUp(() { fs = MemoryFileSystem.test(); mockArtifacts = MockLocalEngineArtifacts(); mockProcessManager = MockProcessManager(); android = fakePlatform('android'); }); void testUsingAndroidContext(String description, dynamic testMethod()) { testUsingContext(description, testMethod, overrides: <Type, Generator>{ Artifacts: () => mockArtifacts, Platform: () => android, FileSystem: () => fs, ProcessManager: () => mockProcessManager, }); } String propertyFor(String key, File file) { final Iterable<String> result = file.readAsLinesSync() .where((String line) => line.startsWith('$key=')) .map((String line) => line.split('=')[1]); return result.isEmpty ? null : result.first; } Future<void> checkBuildVersion({ String manifest, BuildInfo buildInfo, String expectedBuildName, String expectedBuildNumber, }) async { when(mockArtifacts.getArtifactPath( Artifact.flutterFramework, platform: TargetPlatform.android_arm, mode: anyNamed('mode'), environmentType: anyNamed('environmentType'), )).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(globals.fs.path.join('out', 'android_arm')); final File manifestFile = globals.fs.file('path/to/project/pubspec.yaml'); manifestFile.createSync(recursive: true); manifestFile.writeAsStringSync(manifest); updateLocalProperties( project: FlutterProject.fromPath('path/to/project'), buildInfo: buildInfo, requireAndroidSdk: false, ); final File localPropertiesFile = globals.fs.file('path/to/project/android/local.properties'); expect(propertyFor('flutter.versionName', localPropertiesFile), expectedBuildName); expect(propertyFor('flutter.versionCode', localPropertiesFile), expectedBuildNumber); } testUsingAndroidContext('extract build name and number from pubspec.yaml', () async { const String manifest = ''' name: test version: 1.0.0+1 dependencies: flutter: sdk: flutter flutter: '''; const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false); await checkBuildVersion( manifest: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.0', expectedBuildNumber: '1', ); }); testUsingAndroidContext('extract build name from pubspec.yaml', () async { const String manifest = ''' name: test version: 1.0.0 dependencies: flutter: sdk: flutter flutter: '''; const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false); await checkBuildVersion( manifest: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.0', expectedBuildNumber: null, ); }); testUsingAndroidContext('allow build info to override build name', () async { const String manifest = ''' name: test version: 1.0.0+1 dependencies: flutter: sdk: flutter flutter: '''; const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false); await checkBuildVersion( manifest: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.2', expectedBuildNumber: '1', ); }); testUsingAndroidContext('allow build info to override build number', () async { const String manifest = ''' name: test version: 1.0.0+1 dependencies: flutter: sdk: flutter flutter: '''; const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3', treeShakeIcons: false); await checkBuildVersion( manifest: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.0', expectedBuildNumber: '3', ); }); testUsingAndroidContext('allow build info to override build name and number', () async { const String manifest = ''' name: test version: 1.0.0+1 dependencies: flutter: sdk: flutter flutter: '''; const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false); await checkBuildVersion( manifest: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.2', expectedBuildNumber: '3', ); }); testUsingAndroidContext('allow build info to override build name and set number', () async { const String manifest = ''' name: test version: 1.0.0 dependencies: flutter: sdk: flutter flutter: '''; const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false); await checkBuildVersion( manifest: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.2', expectedBuildNumber: '3', ); }); testUsingAndroidContext('allow build info to set build name and number', () async { const String manifest = ''' name: test dependencies: flutter: sdk: flutter flutter: '''; const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false); await checkBuildVersion( manifest: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.2', expectedBuildNumber: '3', ); }); testUsingAndroidContext('allow build info to unset build name and number', () async { const String manifest = ''' name: test dependencies: flutter: sdk: flutter flutter: '''; await checkBuildVersion( manifest: manifest, buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null, treeShakeIcons: false), expectedBuildName: null, expectedBuildNumber: null, ); await checkBuildVersion( manifest: manifest, buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false), expectedBuildName: '1.0.2', expectedBuildNumber: '3', ); await checkBuildVersion( manifest: manifest, buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.3', buildNumber: '4', treeShakeIcons: false), expectedBuildName: '1.0.3', expectedBuildNumber: '4', ); // Values don't get unset. await checkBuildVersion( manifest: manifest, buildInfo: null, expectedBuildName: '1.0.3', expectedBuildNumber: '4', ); // Values get unset. await checkBuildVersion( manifest: manifest, buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null, treeShakeIcons: false), expectedBuildName: null, expectedBuildNumber: null, ); }); }); group('gradle version', () { testWithoutContext('should be compatible with the Android plugin version', () { // Granular versions. expect(getGradleVersionFor('1.0.0'), '2.3'); expect(getGradleVersionFor('1.0.1'), '2.3'); expect(getGradleVersionFor('1.0.2'), '2.3'); expect(getGradleVersionFor('1.0.4'), '2.3'); expect(getGradleVersionFor('1.0.8'), '2.3'); expect(getGradleVersionFor('1.1.0'), '2.3'); expect(getGradleVersionFor('1.1.2'), '2.3'); expect(getGradleVersionFor('1.1.2'), '2.3'); expect(getGradleVersionFor('1.1.3'), '2.3'); // Version Ranges. expect(getGradleVersionFor('1.2.0'), '2.9'); expect(getGradleVersionFor('1.3.1'), '2.9'); expect(getGradleVersionFor('1.5.0'), '2.2.1'); expect(getGradleVersionFor('2.0.0'), '2.13'); expect(getGradleVersionFor('2.1.2'), '2.13'); expect(getGradleVersionFor('2.1.3'), '2.14.1'); expect(getGradleVersionFor('2.2.3'), '2.14.1'); expect(getGradleVersionFor('2.3.0'), '3.3'); expect(getGradleVersionFor('3.0.0'), '4.1'); expect(getGradleVersionFor('3.1.0'), '4.4'); expect(getGradleVersionFor('3.2.0'), '4.6'); expect(getGradleVersionFor('3.2.1'), '4.6'); expect(getGradleVersionFor('3.3.0'), '4.10.2'); expect(getGradleVersionFor('3.3.2'), '4.10.2'); expect(getGradleVersionFor('3.4.0'), '5.6.2'); expect(getGradleVersionFor('3.5.0'), '5.6.2'); expect(getGradleVersionFor('4.0.0'), '6.7'); expect(getGradleVersionFor('4.1.0'), '6.7'); }); testWithoutContext('throws on unsupported versions', () { expect(() => getGradleVersionFor('3.6.0'), throwsA(predicate<Exception>((Exception e) => e is ToolExit))); }); }); group('isAppUsingAndroidX', () { FileSystem fs; setUp(() { fs = MemoryFileSystem.test(); }); testUsingContext('returns true when the project is using AndroidX', () async { final Directory androidDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_android.'); androidDirectory .childFile('gradle.properties') .writeAsStringSync('android.useAndroidX=true'); expect(isAppUsingAndroidX(androidDirectory), isTrue); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('returns false when the project is not using AndroidX', () async { final Directory androidDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_android.'); androidDirectory .childFile('gradle.properties') .writeAsStringSync('android.useAndroidX=false'); expect(isAppUsingAndroidX(androidDirectory), isFalse); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('returns false when gradle.properties does not exist', () async { final Directory androidDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_android.'); expect(isAppUsingAndroidX(androidDirectory), isFalse); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); }); group('buildPluginsAsAar', () { FileSystem fs; FakeProcessManager fakeProcessManager; MockAndroidSdk mockAndroidSdk; setUp(() { fs = MemoryFileSystem.test(); fakeProcessManager = FakeProcessManager.list(<FakeCommand>[]); mockAndroidSdk = MockAndroidSdk(); when(mockAndroidSdk.directory).thenReturn('irrelevant'); }); testUsingContext('calls gradle', () async { final Directory androidDirectory = globals.fs.directory('android.'); androidDirectory.createSync(); androidDirectory .childFile('pubspec.yaml') .writeAsStringSync('name: irrelevant'); final Directory plugin1 = globals.fs.directory('plugin1.'); plugin1 ..createSync() ..childFile('pubspec.yaml') .writeAsStringSync(''' name: irrelevant flutter: plugin: androidPackage: irrelevant '''); plugin1 .childDirectory('android') .childFile('build.gradle') .createSync(recursive: true); final Directory plugin2 = globals.fs.directory('plugin2.'); plugin2 ..createSync() ..childFile('pubspec.yaml') .writeAsStringSync(''' name: irrelevant flutter: plugin: androidPackage: irrelevant '''); plugin2 .childDirectory('android') .childFile('build.gradle') .createSync(recursive: true); androidDirectory .childFile('.flutter-plugins') .writeAsStringSync(''' plugin1=${plugin1.path} plugin2=${plugin2.path} '''); final Directory buildDirectory = androidDirectory .childDirectory('build'); buildDirectory .childDirectory('outputs') .childDirectory('repo') .createSync(recursive: true); final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot); final String initScript = globals.fs.path.join( flutterRoot, 'packages', 'flutter_tools', 'gradle', 'aar_init_script.gradle', ); fakeProcessManager ..addCommand(FakeCommand( command: <String>[ 'gradlew', '-I=$initScript', '-Pflutter-root=$flutterRoot', '-Poutput-dir=${buildDirectory.path}', '-Pis-plugin=true', '-PbuildNumber=1.0', '-q', '-Pdart-obfuscation=false', '-Ptrack-widget-creation=false', '-Ptree-shake-icons=true', '-Ptarget-platform=android-arm,android-arm64,android-x64', 'assembleAarRelease', ], workingDirectory: plugin1.childDirectory('android').path, )) ..addCommand(FakeCommand( command: <String>[ 'gradlew', '-I=$initScript', '-Pflutter-root=$flutterRoot', '-Poutput-dir=${buildDirectory.path}', '-Pis-plugin=true', '-PbuildNumber=1.0', '-q', '-Pdart-obfuscation=false', '-Ptrack-widget-creation=false', '-Ptree-shake-icons=true', '-Ptarget-platform=android-arm,android-arm64,android-x64', 'assembleAarRelease', ], workingDirectory: plugin2.childDirectory('android').path, )); await buildPluginsAsAar( FlutterProject.fromPath(androidDirectory.path), const AndroidBuildInfo(BuildInfo( BuildMode.release, '', treeShakeIcons: true, dartObfuscation: true, buildNumber: '2.0' )), buildDirectory: buildDirectory, ); expect(fakeProcessManager.hasRemainingExpectations, isFalse); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, FileSystem: () => fs, ProcessManager: () => fakeProcessManager, GradleUtils: () => FakeGradleUtils(), }); testUsingContext('skips plugin without a android/build.gradle file', () async { final Directory androidDirectory = globals.fs.directory('android.'); androidDirectory.createSync(); androidDirectory .childFile('pubspec.yaml') .writeAsStringSync('name: irrelevant'); final Directory plugin1 = globals.fs.directory('plugin1.'); plugin1 ..createSync() ..childFile('pubspec.yaml') .writeAsStringSync(''' name: irrelevant flutter: plugin: androidPackage: irrelevant '''); androidDirectory .childFile('.flutter-plugins') .writeAsStringSync(''' plugin1=${plugin1.path} '''); // Create an empty android directory. // https://github.com/flutter/flutter/issues/46898 plugin1.childDirectory('android').createSync(); final Directory buildDirectory = androidDirectory.childDirectory('build'); buildDirectory .childDirectory('outputs') .childDirectory('repo') .createSync(recursive: true); await buildPluginsAsAar( FlutterProject.fromPath(androidDirectory.path), const AndroidBuildInfo(BuildInfo.release), buildDirectory: buildDirectory, ); expect(fakeProcessManager.hasRemainingExpectations, isFalse); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, FileSystem: () => fs, ProcessManager: () => fakeProcessManager, GradleUtils: () => FakeGradleUtils(), }); }); group('gradle build', () { TestUsage testUsage; MockAndroidSdk mockAndroidSdk; MockAndroidStudio mockAndroidStudio; MockLocalEngineArtifacts mockArtifacts; MockProcessManager mockProcessManager; FakePlatform android; FileSystem fileSystem; FileSystemUtils fileSystemUtils; Cache cache; setUp(() { testUsage = TestUsage(); fileSystem = MemoryFileSystem.test(); fileSystemUtils = MockFileSystemUtils(); mockAndroidSdk = MockAndroidSdk(); mockAndroidStudio = MockAndroidStudio(); mockArtifacts = MockLocalEngineArtifacts(); mockProcessManager = MockProcessManager(); android = fakePlatform('android'); when(mockAndroidSdk.directory).thenReturn('irrelevant'); final Directory rootDirectory = fileSystem.currentDirectory; cache = Cache.test( rootOverride: rootDirectory, fileSystem: fileSystem, ); final Directory gradleWrapperDirectory = rootDirectory .childDirectory('bin') .childDirectory('cache') .childDirectory('artifacts') .childDirectory('gradle_wrapper'); gradleWrapperDirectory.createSync(recursive: true); gradleWrapperDirectory .childFile('gradlew') .writeAsStringSync('irrelevant'); gradleWrapperDirectory .childDirectory('gradle') .childDirectory('wrapper') .createSync(recursive: true); gradleWrapperDirectory .childDirectory('gradle') .childDirectory('wrapper') .childFile('gradle-wrapper.jar') .writeAsStringSync('irrelevant'); }); testUsingContext('recognizes common errors - tool exit', () async { final Process process = createMockProcess( exitCode: 1, stdout: 'irrelevant\nSome gradle message\nirrelevant', ); when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenAnswer((_) => Future<Process>.value(process)); fileSystem.directory('android') .childFile('build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); bool handlerCalled = false; await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: <GradleHandledError>[ GradleHandledError( test: (String line) { return line.contains('Some gradle message'); }, handler: ({ String line, FlutterProject project, bool usesAndroidX, bool shouldBuildPluginAsAar, }) async { handlerCalled = true; return GradleBuildStatus.exit; }, eventLabel: 'random-event-label', ), ], ); }, throwsToolExit( message: 'Gradle task assembleRelease failed with exit code 1' )); expect(handlerCalled, isTrue); expect(testUsage.events, contains( const TestUsageEvent( 'build', 'unspecified', label: 'gradle-random-event-label-failure', parameters: <String, String>{}, ), )); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, Usage: () => testUsage, }); testUsingContext('recognizes common errors - retry build', () async { when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenAnswer((_) { final Process process = createMockProcess( exitCode: 1, stdout: 'irrelevant\nSome gradle message\nirrelevant', ); return Future<Process>.value(process); }); fileSystem.directory('android') .childFile('build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); int testFnCalled = 0; await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: <GradleHandledError>[ GradleHandledError( test: (String line) { if (line.contains('Some gradle message')) { testFnCalled++; return true; } return false; }, handler: ({ String line, FlutterProject project, bool usesAndroidX, bool shouldBuildPluginAsAar, }) async { return GradleBuildStatus.retry; }, eventLabel: 'random-event-label', ), ], ); }, throwsToolExit( message: 'Gradle task assembleRelease failed with exit code 1' )); expect(testFnCalled, equals(2)); expect(testUsage.events, contains( const TestUsageEvent( 'build', 'unspecified', label: 'gradle-random-event-label-failure', parameters: <String, String>{}, ), )); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, Usage: () => testUsage, }); testUsingContext('recognizes process exceptions - tool exit', () async { when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenThrow(const ProcessException('', <String>[], 'Some gradle message')); fileSystem.directory('android') .childFile('build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); bool handlerCalled = false; await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: <GradleHandledError>[ GradleHandledError( test: (String line) { return line.contains('Some gradle message'); }, handler: ({ String line, FlutterProject project, bool usesAndroidX, bool shouldBuildPluginAsAar, }) async { handlerCalled = true; return GradleBuildStatus.exit; }, eventLabel: 'random-event-label', ), ], ); }, throwsToolExit( message: 'Gradle task assembleRelease failed with exit code 1' )); expect(handlerCalled, isTrue); expect(testUsage.events, contains( const TestUsageEvent( 'build', 'unspecified', label: 'gradle-random-event-label-failure', parameters: <String, String>{}, ), )); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, Usage: () => testUsage, }); testUsingContext('rethrows unrecognized ProcessException', () async { when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenThrow(const ProcessException('', <String>[], 'Unrecognized')); fileSystem.directory('android') .childFile('build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: const <GradleHandledError>[], ); }, throwsA(isA<ProcessException>())); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, }); testUsingContext('logs success event after a successful retry', () async { int testFnCalled = 0; when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenAnswer((_) { Process process; if (testFnCalled == 0) { process = createMockProcess( exitCode: 1, stdout: 'irrelevant\nSome gradle message\nirrelevant', ); } else { process = createMockProcess( exitCode: 0, stdout: 'irrelevant', ); } testFnCalled++; return Future<Process>.value(process); }); fileSystem.directory('android') .childFile('build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); fileSystem.directory('build') .childDirectory('app') .childDirectory('outputs') .childDirectory('flutter-apk') .childFile('app-release.apk') .createSync(recursive: true); await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: <GradleHandledError>[ GradleHandledError( test: (String line) { return line.contains('Some gradle message'); }, handler: ({ String line, FlutterProject project, bool usesAndroidX, bool shouldBuildPluginAsAar, }) async { return GradleBuildStatus.retry; }, eventLabel: 'random-event-label', ), ], ); expect(testUsage.events, contains( const TestUsageEvent( 'build', 'unspecified', label: 'gradle-random-event-label-success', parameters: <String, String>{}, ), )); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, Cache: () => cache, FileSystem: () => fileSystem, Platform: () => android, ProcessManager: () => mockProcessManager, Usage: () => testUsage, }); testUsingContext('performs code size analysis and sends analytics', () async { when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenAnswer((_) { return Future<Process>.value(createMockProcess( exitCode: 0, stdout: 'irrelevant', )); }); fileSystem.directory('android') .childFile('build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); final Archive archive = Archive() ..addFile(ArchiveFile('AndroidManifest.xml', 100, List<int>.filled(100, 0))) ..addFile(ArchiveFile('META-INF/CERT.RSA', 10, List<int>.filled(10, 0))) ..addFile(ArchiveFile('META-INF/CERT.SF', 10, List<int>.filled(10, 0))) ..addFile(ArchiveFile('lib/arm64-v8a/libapp.so', 50, List<int>.filled(50, 0))) ..addFile(ArchiveFile('lib/arm64-v8a/libflutter.so', 50, List<int>.filled(50, 0))); fileSystem.directory('build') .childDirectory('app') .childDirectory('outputs') .childDirectory('flutter-apk') .childFile('app-release.apk') ..createSync(recursive: true) ..writeAsBytesSync(ZipEncoder().encode(archive)); fileSystem.file('foo/snapshot.arm64-v8a.json') ..createSync(recursive: true) ..writeAsStringSync(r''' [ { "l": "dart:_internal", "c": "SubListIterable", "n": "[Optimized] skip", "s": 2400 } ]'''); fileSystem.file('foo/trace.arm64-v8a.json') ..createSync(recursive: true) ..writeAsStringSync('{}'); await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, codeSizeDirectory: 'foo', ), targetArchs: <AndroidArch>[AndroidArch.arm64_v8a], ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: <GradleHandledError>[], ); expect(testUsage.events, contains( const TestUsageEvent( 'code-size-analysis', 'apk', ), )); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, Cache: () => cache, FileSystem: () => fileSystem, Platform: () => android, ProcessManager: () => mockProcessManager, Usage: () => testUsage, }); testUsingContext('recognizes common errors - retry build with AAR plugins', () async { when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenAnswer((_) { final Process process = createMockProcess( exitCode: 1, stdout: 'irrelevant\nSome gradle message\nirrelevant', ); return Future<Process>.value(process); }); fileSystem.directory('android') .childFile('build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); int testFnCalled = 0; bool builtPluginAsAar = false; await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: <GradleHandledError>[ GradleHandledError( test: (String line) { if (line.contains('Some gradle message')) { testFnCalled++; return true; } return false; }, handler: ({ String line, FlutterProject project, bool usesAndroidX, bool shouldBuildPluginAsAar, }) async { if (testFnCalled == 2) { builtPluginAsAar = shouldBuildPluginAsAar; } return GradleBuildStatus.retryWithAarPlugins; }, eventLabel: 'random-event-label', ), ], ); }, throwsToolExit( message: 'Gradle task assembleRelease failed with exit code 1' )); expect(testFnCalled, equals(2)); expect(builtPluginAsAar, isTrue); expect(testUsage.events, contains( const TestUsageEvent( 'build', 'unspecified', label: 'gradle-random-event-label-failure', parameters: <String, String>{}, ), )); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, Usage: () => testUsage, }); testUsingContext('indicates that an APK has been built successfully', () async { fileSystem.directory('android') .childFile('build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); fileSystem.directory('build') .childDirectory('app') .childDirectory('outputs') .childDirectory('flutter-apk') .childFile('app-release.apk') .createSync(recursive: true); await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: const <GradleHandledError>[], ); expect( testLogger.statusText, contains('Built build/app/outputs/flutter-apk/app-release.apk (0.0MB)'), ); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, Cache: () => cache, FileSystem: () => fileSystem, Platform: () => android, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext("doesn't indicate how to consume an AAR when printHowToConsumeAaar is false", () async { final File manifestFile = fileSystem.file('pubspec.yaml'); manifestFile.createSync(recursive: true); manifestFile.writeAsStringSync(''' flutter: module: androidPackage: com.example.test ''' ); fileSystem.file('.android/gradlew').createSync(recursive: true); fileSystem.file('.android/gradle.properties') .writeAsStringSync('irrelevant'); fileSystem.file('.android/build.gradle') .createSync(recursive: true); // Let any process start. Assert after. when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); fileSystem.directory('build/outputs/repo').createSync(recursive: true); await buildGradleAar( androidBuildInfo: const AndroidBuildInfo(BuildInfo(BuildMode.release, null, treeShakeIcons: false)), project: FlutterProject.current(), outputDirectory: fileSystem.directory('build/'), target: '', buildNumber: '1.0', ); expect( testLogger.statusText, contains('Built build/outputs/repo'), ); expect( testLogger.statusText.contains('Consuming the Module'), isFalse, ); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, }); testUsingContext('build apk uses selected local engine,the engine abi is arm', () async { when(mockArtifacts.getArtifactPath( Artifact.flutterFramework, platform: TargetPlatform.android_arm, mode: anyNamed('mode'), environmentType: anyNamed('environmentType'), )).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_arm')); fileSystem.file('out/android_arm/flutter_embedding_release.pom') ..createSync(recursive: true) ..writeAsStringSync(''' <?xml version="1.0" encoding="UTF-8"?> <project> <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version> <dependencies> </dependencies> </project> '''); fileSystem.file('out/android_arm/armeabi_v7a_release.pom').createSync(recursive: true); fileSystem.file('out/android_arm/armeabi_v7a_release.jar').createSync(recursive: true); fileSystem.file('out/android_arm/armeabi_v7a_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('out/android_arm/flutter_embedding_release.jar').createSync(recursive: true); fileSystem.file('out/android_arm/flutter_embedding_release.pom').createSync(recursive: true); fileSystem.file('out/android_arm/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('android/gradlew').createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.file('android/build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); // Let any process start. Assert after. when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenAnswer((_) { return Future<Process>.value( createMockProcess( exitCode: 1, ) ); }); await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: const <GradleHandledError>[], ); }, throwsToolExit()); final List<String> actualGradlewCall = verify( mockProcessManager.start( captureAny, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory') ), ).captured.last as List<String>; expect(actualGradlewCall, contains('/android/gradlew')); expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_arm')); expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0')); expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release')); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => mockArtifacts, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, }); testUsingContext( 'build apk uses selected local engine,the engine abi is arm64', () async { when(mockArtifacts.getArtifactPath( Artifact.flutterFramework, platform: anyNamed('platform'), mode: anyNamed('mode'), environmentType: anyNamed('environmentType'), )).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_arm64')); fileSystem.file('out/android_arm64/flutter_embedding_release.pom') ..createSync(recursive: true) ..writeAsStringSync(''' <?xml version="1.0" encoding="UTF-8"?> <project> <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version> <dependencies> </dependencies> </project> '''); fileSystem.file('out/android_arm64/arm64_v8a_release.pom').createSync(recursive: true); fileSystem.file('out/android_arm64/arm64_v8a_release.jar').createSync(recursive: true); fileSystem.file('out/android_arm64/arm64_v8a_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('out/android_arm64/flutter_embedding_release.jar').createSync(recursive: true); fileSystem.file('out/android_arm64/flutter_embedding_release.pom').createSync(recursive: true); fileSystem.file('out/android_arm64/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('android/gradlew').createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.file('android/build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); // Let any process start. Assert after. when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenAnswer((_) { return Future<Process>.value( createMockProcess( exitCode: 1, ) ); }); await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: const <GradleHandledError>[], ); }, throwsToolExit()); final List<String> actualGradlewCall = verify( mockProcessManager.start( captureAny, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory') ), ).captured.last as List<String>; expect(actualGradlewCall, contains('/android/gradlew')); expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_arm64')); expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0')); expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release')); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => mockArtifacts, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, }); testUsingContext( 'build apk uses selected local engine,the engine abi is x86', () async { when(mockArtifacts.getArtifactPath( Artifact.flutterFramework, platform: anyNamed('platform'), mode: anyNamed('mode'), environmentType: anyNamed('environmentType'), )).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_x86')); fileSystem.file('out/android_x86/flutter_embedding_release.pom') ..createSync(recursive: true) ..writeAsStringSync(''' <?xml version="1.0" encoding="UTF-8"?> <project> <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version> <dependencies> </dependencies> </project> '''); fileSystem.file('out/android_x86/x86_release.pom').createSync(recursive: true); fileSystem.file('out/android_x86/x86_release.jar').createSync(recursive: true); fileSystem.file('out/android_x86/x86_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('out/android_x86/flutter_embedding_release.jar').createSync(recursive: true); fileSystem.file('out/android_x86/flutter_embedding_release.pom').createSync(recursive: true); fileSystem.file('out/android_x86/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('android/gradlew').createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.file('android/build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); // Let any process start. Assert after. when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenAnswer((_) { return Future<Process>.value( createMockProcess( exitCode: 1, ) ); }); await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: const <GradleHandledError>[], ); }, throwsToolExit()); final List<String> actualGradlewCall = verify( mockProcessManager.start( captureAny, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory') ), ).captured.last as List<String>; expect(actualGradlewCall, contains('/android/gradlew')); expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_x86')); expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0')); expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release')); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => mockArtifacts, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, }); testUsingContext( 'build apk uses selected local engine,the engine abi is x64', () async { when(mockArtifacts.getArtifactPath( Artifact.flutterFramework, platform: anyNamed('platform'), mode: anyNamed('mode'), environmentType: anyNamed('environmentType'), )).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_x64')); fileSystem.file('out/android_x64/flutter_embedding_release.pom') ..createSync(recursive: true) ..writeAsStringSync(''' <?xml version="1.0" encoding="UTF-8"?> <project> <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version> <dependencies> </dependencies> </project> '''); fileSystem.file('out/android_x64/x86_64_release.pom').createSync(recursive: true); fileSystem.file('out/android_x64/x86_64_release.jar').createSync(recursive: true); fileSystem.file('out/android_x64/x86_64_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('out/android_x64/flutter_embedding_release.jar').createSync(recursive: true); fileSystem.file('out/android_x64/flutter_embedding_release.pom').createSync(recursive: true); fileSystem.file('out/android_x64/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('android/gradlew').createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.file('android/build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); // Let any process start. Assert after. when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) .thenAnswer((_) { return Future<Process>.value( createMockProcess( exitCode: 1, ) ); }); await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: const <GradleHandledError>[], ); }, throwsToolExit()); final List<String> actualGradlewCall = verify( mockProcessManager.start( captureAny, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory') ), ).captured.last as List<String>; expect(actualGradlewCall, contains('/android/gradlew')); expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_x64')); expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0')); expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release')); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => mockArtifacts, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => mockProcessManager, }); testUsingContext('honors --no-android-gradle-daemon setting', () async { (globals.processManager as FakeProcessManager).addCommand( const FakeCommand(command: <String>[ '/android/gradlew', '-q', '--no-daemon', '-Ptarget-platform=android-arm,android-arm64,android-x64', '-Ptarget=lib/main.dart', '-Pdart-obfuscation=false', '-Ptrack-widget-creation=false', '-Ptree-shake-icons=false', 'assembleRelease' ], )); fileSystem.file('android/gradlew').createSync(recursive: true); fileSystem.directory('android') .childFile('gradle.properties') .createSync(recursive: true); fileSystem.file('android/build.gradle') .createSync(recursive: true); fileSystem.directory('android') .childDirectory('app') .childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); await expectLater(() async { await buildGradleApp( project: FlutterProject.current(), androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, null, treeShakeIcons: false, androidGradleDaemon: false, ), ), target: 'lib/main.dart', isBuildingBundle: false, localGradleErrors: const <GradleHandledError>[], ); }, throwsToolExit()); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => Artifacts.test(), Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.list(<FakeCommand>[]), }); testUsingContext('build aar uses selected local engine,the engine abi is arm', () async { when(mockArtifacts.getArtifactPath( Artifact.flutterFramework, platform: TargetPlatform.android_arm, mode: anyNamed('mode'), environmentType: anyNamed('environmentType'), )).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_arm')); fileSystem.file('out/android_arm/flutter_embedding_release.pom') ..createSync(recursive: true) ..writeAsStringSync(''' <?xml version="1.0" encoding="UTF-8"?> <project> <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version> <dependencies> </dependencies> </project> '''); fileSystem.file('out/android_arm/armeabi_v7a_release.pom').createSync(recursive: true); fileSystem.file('out/android_arm/armeabi_v7a_release.jar').createSync(recursive: true); fileSystem.file('out/android_arm/armeabi_v7a_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('out/android_arm/flutter_embedding_release.jar').createSync(recursive: true); fileSystem.file('out/android_arm/flutter_embedding_release.pom').createSync(recursive: true); fileSystem.file('out/android_arm/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true); final File manifestFile = fileSystem.file('pubspec.yaml'); manifestFile.createSync(recursive: true); manifestFile.writeAsStringSync(''' flutter: module: androidPackage: com.example.test ''' ); fileSystem.directory('.android/gradle') .createSync(recursive: true); fileSystem.directory('.android/gradle/wrapper') .createSync(recursive: true); fileSystem.file('.android/gradlew').createSync(recursive: true); fileSystem.file('.android/gradle.properties') .writeAsStringSync('irrelevant'); fileSystem.file('.android/build.gradle') .createSync(recursive: true); // Let any process start. Assert after. when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); fileSystem.directory('build/outputs/repo').createSync(recursive: true); when(fileSystemUtils.copyDirectorySync(any, any)).thenReturn(null); await buildGradleAar( androidBuildInfo: const AndroidBuildInfo(BuildInfo(BuildMode.release, null, treeShakeIcons: false)), project: FlutterProject.current(), outputDirectory: fileSystem.directory('build/'), target: '', buildNumber: '2.0', ); final List<String> actualGradlewCall = verify( mockProcessManager.run( captureAny, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), ), ).captured.last as List<String>; expect(actualGradlewCall, contains('/.android/gradlew')); expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_arm')); expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0')); expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release')); expect(actualGradlewCall, contains('-PbuildNumber=2.0')); // Verify the local engine repo is copied into the generated Maven repo. final List<dynamic> copyDirectoryArguments = verify( fileSystemUtils.copyDirectorySync(captureAny, captureAny) ).captured; expect(copyDirectoryArguments.length, 2); expect((copyDirectoryArguments.first as Directory).path, '/.tmp_rand0/flutter_tool_local_engine_repo.rand0'); expect((copyDirectoryArguments.last as Directory).path, 'build/outputs/repo'); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => mockArtifacts, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, FileSystemUtils: () => fileSystemUtils, ProcessManager: () => mockProcessManager, }); testUsingContext( 'build aar uses selected local engine,the engine abi is arm64', () async { when(mockArtifacts.getArtifactPath( Artifact.flutterFramework, platform: anyNamed('platform'), mode: anyNamed('mode'), environmentType: anyNamed('environmentType'), )).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_arm64')); fileSystem.file('out/android_arm64/flutter_embedding_release.pom') ..createSync(recursive: true) ..writeAsStringSync(''' <?xml version="1.0" encoding="UTF-8"?> <project> <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version> <dependencies> </dependencies> </project> '''); fileSystem.file('out/android_arm64/arm64_v8a_release.pom').createSync(recursive: true); fileSystem.file('out/android_arm64/arm64_v8a_release.jar').createSync(recursive: true); fileSystem.file('out/android_arm64/arm64_v8a_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('out/android_arm64/flutter_embedding_release.jar').createSync(recursive: true); fileSystem.file('out/android_arm64/flutter_embedding_release.pom').createSync(recursive: true); fileSystem.file('out/android_arm64/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true); final File manifestFile = fileSystem.file('pubspec.yaml'); manifestFile.createSync(recursive: true); manifestFile.writeAsStringSync(''' flutter: module: androidPackage: com.example.test ''' ); fileSystem.directory('.android/gradle') .createSync(recursive: true); fileSystem.directory('.android/gradle/wrapper') .createSync(recursive: true); fileSystem.file('.android/gradlew').createSync(recursive: true); fileSystem.file('.android/gradle.properties') .writeAsStringSync('irrelevant'); fileSystem.file('.android/build.gradle') .createSync(recursive: true); // Let any process start. Assert after. when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); fileSystem.directory('build/outputs/repo').createSync(recursive: true); when(fileSystemUtils.copyDirectorySync(any, any)).thenReturn(null); await buildGradleAar( androidBuildInfo: const AndroidBuildInfo( BuildInfo(BuildMode.release, null, treeShakeIcons: false)), project: FlutterProject.current(), outputDirectory: fileSystem.directory('build/'), target: '', buildNumber: '2.0', ); final List<String> actualGradlewCall = verify( mockProcessManager.run( captureAny, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), ), ).captured.last as List<String>; expect(actualGradlewCall, contains('/.android/gradlew')); expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_arm64')); expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0')); expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release')); expect(actualGradlewCall, contains('-PbuildNumber=2.0')); // Verify the local engine repo is copied into the generated Maven repo. final List<dynamic> copyDirectoryArguments = verify( fileSystemUtils.copyDirectorySync(captureAny, captureAny) ).captured; expect(copyDirectoryArguments.length, 2); expect((copyDirectoryArguments.first as Directory).path, '/.tmp_rand0/flutter_tool_local_engine_repo.rand0'); expect((copyDirectoryArguments.last as Directory).path, 'build/outputs/repo'); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => mockArtifacts, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, FileSystemUtils: () => fileSystemUtils, ProcessManager: () => mockProcessManager, }); testUsingContext( 'build aar uses selected local engine,the engine abi is x86', () async { when(mockArtifacts.getArtifactPath( Artifact.flutterFramework, platform: anyNamed('platform'), mode: anyNamed('mode'), environmentType: anyNamed('environmentType'), )).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_x86')); fileSystem.file('out/android_x86/flutter_embedding_release.pom') ..createSync(recursive: true) ..writeAsStringSync(''' <?xml version="1.0" encoding="UTF-8"?> <project> <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version> <dependencies> </dependencies> </project> '''); fileSystem.file('out/android_x86/x86_release.pom').createSync(recursive: true); fileSystem.file('out/android_x86/x86_release.jar').createSync(recursive: true); fileSystem.file('out/android_x86/x86_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('out/android_x86/flutter_embedding_release.jar').createSync(recursive: true); fileSystem.file('out/android_x86/flutter_embedding_release.pom').createSync(recursive: true); fileSystem.file('out/android_x86/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true); final File manifestFile = fileSystem.file('pubspec.yaml'); manifestFile.createSync(recursive: true); manifestFile.writeAsStringSync(''' flutter: module: androidPackage: com.example.test ''' ); fileSystem.directory('.android/gradle') .createSync(recursive: true); fileSystem.directory('.android/gradle/wrapper') .createSync(recursive: true); fileSystem.file('.android/gradlew').createSync(recursive: true); fileSystem.file('.android/gradle.properties') .writeAsStringSync('irrelevant'); fileSystem.file('.android/build.gradle') .createSync(recursive: true); // Let any process start. Assert after. when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); fileSystem.directory('build/outputs/repo').createSync(recursive: true); when(fileSystemUtils.copyDirectorySync(any, any)).thenReturn(null); await buildGradleAar( androidBuildInfo: const AndroidBuildInfo( BuildInfo(BuildMode.release, null, treeShakeIcons: false)), project: FlutterProject.current(), outputDirectory: fileSystem.directory('build/'), target: '', buildNumber: '2.0', ); final List<String> actualGradlewCall = verify( mockProcessManager.run( captureAny, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), ), ).captured.last as List<String>; expect(actualGradlewCall, contains('/.android/gradlew')); expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_x86')); expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0')); expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release')); expect(actualGradlewCall, contains('-PbuildNumber=2.0')); // Verify the local engine repo is copied into the generated Maven repo. final List<dynamic> copyDirectoryArguments = verify( fileSystemUtils.copyDirectorySync(captureAny, captureAny) ).captured; expect(copyDirectoryArguments.length, 2); expect((copyDirectoryArguments.first as Directory).path, '/.tmp_rand0/flutter_tool_local_engine_repo.rand0'); expect((copyDirectoryArguments.last as Directory).path, 'build/outputs/repo'); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => mockArtifacts, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, FileSystemUtils: () => fileSystemUtils, ProcessManager: () => mockProcessManager, }); testUsingContext( 'build aar uses selected local engine,the engine abi is x64', () async { when(mockArtifacts.getArtifactPath( Artifact.flutterFramework, platform: anyNamed('platform'), mode: anyNamed('mode'), environmentType: anyNamed('environmentType'), )).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_x64')); fileSystem.file('out/android_x64/flutter_embedding_release.pom') ..createSync(recursive: true) ..writeAsStringSync(''' <?xml version="1.0" encoding="UTF-8"?> <project> <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version> <dependencies> </dependencies> </project> '''); fileSystem.file('out/android_x64/x86_64_release.pom').createSync(recursive: true); fileSystem.file('out/android_x64/x86_64_release.jar').createSync(recursive: true); fileSystem.file('out/android_x64/x86_64_release.maven-metadata.xml').createSync(recursive: true); fileSystem.file('out/android_x64/flutter_embedding_release.jar').createSync(recursive: true); fileSystem.file('out/android_x64/flutter_embedding_release.pom').createSync(recursive: true); fileSystem.file('out/android_x64/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true); final File manifestFile = fileSystem.file('pubspec.yaml'); manifestFile.createSync(recursive: true); manifestFile.writeAsStringSync(''' flutter: module: androidPackage: com.example.test ''' ); fileSystem.directory('.android/gradle') .createSync(recursive: true); fileSystem.directory('.android/gradle/wrapper') .createSync(recursive: true); fileSystem.file('.android/gradlew').createSync(recursive: true); fileSystem.file('.android/gradle.properties') .writeAsStringSync('irrelevant'); fileSystem.file('.android/build.gradle') .createSync(recursive: true); // Let any process start. Assert after. when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); fileSystem.directory('build/outputs/repo').createSync(recursive: true); when(fileSystemUtils.copyDirectorySync(any, any)).thenReturn(null); await buildGradleAar( androidBuildInfo: const AndroidBuildInfo( BuildInfo(BuildMode.release, null, treeShakeIcons: false)), project: FlutterProject.current(), outputDirectory: fileSystem.directory('build/'), target: '', buildNumber: '2.0', ); final List<String> actualGradlewCall = verify( mockProcessManager.run( captureAny, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), ), ).captured.last as List<String>; expect(actualGradlewCall, contains('/.android/gradlew')); expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_x64')); expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0')); expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release')); expect(actualGradlewCall, contains('-PbuildNumber=2.0')); // Verify the local engine repo is copied into the generated Maven repo. final List<dynamic> copyDirectoryArguments = verify( fileSystemUtils.copyDirectorySync(captureAny, captureAny) ).captured; expect(copyDirectoryArguments.length, 2); expect((copyDirectoryArguments.first as Directory).path, '/.tmp_rand0/flutter_tool_local_engine_repo.rand0'); expect((copyDirectoryArguments.last as Directory).path, 'build/outputs/repo'); }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => mockArtifacts, Cache: () => cache, Platform: () => android, FileSystem: () => fileSystem, FileSystemUtils: () => fileSystemUtils, ProcessManager: () => mockProcessManager, }); }); group('printHowToConsumeAar', () { BufferLogger logger; FileSystem fileSystem; setUp(() { logger = BufferLogger.test(); fileSystem = MemoryFileSystem.test(); }); testWithoutContext('stdout contains release, debug and profile', () async { printHowToConsumeAar( buildModes: const <String>{'release', 'debug', 'profile'}, androidPackage: 'com.mycompany', repoDirectory: fileSystem.directory('build/'), buildNumber: '2.2', logger: logger, fileSystem: fileSystem, ); expect( logger.statusText, contains( '\n' 'Consuming the Module\n' ' 1. Open <host>/app/build.gradle\n' ' 2. Ensure you have the repositories configured, otherwise add them:\n' '\n' ' String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"\n' ' repositories {\n' ' maven {\n' " url 'build/'\n" ' }\n' ' maven {\n' ' url "\$storageUrl/download.flutter.io"\n' ' }\n' ' }\n' '\n' ' 3. Make the host app depend on the Flutter module:\n' '\n' ' dependencies {\n' " releaseImplementation 'com.mycompany:flutter_release:2.2'\n" " debugImplementation 'com.mycompany:flutter_debug:2.2'\n" " profileImplementation 'com.mycompany:flutter_profile:2.2'\n" ' }\n' '\n' '\n' ' 4. Add the `profile` build type:\n' '\n' ' android {\n' ' buildTypes {\n' ' profile {\n' ' initWith debug\n' ' }\n' ' }\n' ' }\n' '\n' 'To learn more, visit https://flutter.dev/go/build-aar\n' ) ); }); testWithoutContext('stdout contains release', () async { printHowToConsumeAar( buildModes: const <String>{'release'}, androidPackage: 'com.mycompany', repoDirectory: fileSystem.directory('build/'), logger: logger, fileSystem: fileSystem, ); expect( logger.statusText, contains( '\n' 'Consuming the Module\n' ' 1. Open <host>/app/build.gradle\n' ' 2. Ensure you have the repositories configured, otherwise add them:\n' '\n' ' String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"\n' ' repositories {\n' ' maven {\n' " url 'build/'\n" ' }\n' ' maven {\n' ' url "\$storageUrl/download.flutter.io"\n' ' }\n' ' }\n' '\n' ' 3. Make the host app depend on the Flutter module:\n' '\n' ' dependencies {\n' " releaseImplementation 'com.mycompany:flutter_release:1.0'\n" ' }\n' '\n' 'To learn more, visit https://flutter.dev/go/build-aar\n' ) ); }); testWithoutContext('stdout contains debug', () async { printHowToConsumeAar( buildModes: const <String>{'debug'}, androidPackage: 'com.mycompany', repoDirectory: fileSystem.directory('build/'), logger: logger, fileSystem: fileSystem, ); expect( logger.statusText, contains( '\n' 'Consuming the Module\n' ' 1. Open <host>/app/build.gradle\n' ' 2. Ensure you have the repositories configured, otherwise add them:\n' '\n' ' String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"\n' ' repositories {\n' ' maven {\n' " url 'build/'\n" ' }\n' ' maven {\n' ' url "\$storageUrl/download.flutter.io"\n' ' }\n' ' }\n' '\n' ' 3. Make the host app depend on the Flutter module:\n' '\n' ' dependencies {\n' " debugImplementation 'com.mycompany:flutter_debug:1.0'\n" ' }\n' '\n' 'To learn more, visit https://flutter.dev/go/build-aar\n' ) ); }); testWithoutContext('stdout contains profile', () async { printHowToConsumeAar( buildModes: const <String>{'profile'}, androidPackage: 'com.mycompany', repoDirectory: fileSystem.directory('build/'), buildNumber: '1.0', logger: logger, fileSystem: fileSystem, ); expect( logger.statusText, contains( '\n' 'Consuming the Module\n' ' 1. Open <host>/app/build.gradle\n' ' 2. Ensure you have the repositories configured, otherwise add them:\n' '\n' ' String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"\n' ' repositories {\n' ' maven {\n' " url 'build/'\n" ' }\n' ' maven {\n' ' url "\$storageUrl/download.flutter.io"\n' ' }\n' ' }\n' '\n' ' 3. Make the host app depend on the Flutter module:\n' '\n' ' dependencies {\n' " profileImplementation 'com.mycompany:flutter_profile:1.0'\n" ' }\n' '\n' '\n' ' 4. Add the `profile` build type:\n' '\n' ' android {\n' ' buildTypes {\n' ' profile {\n' ' initWith debug\n' ' }\n' ' }\n' ' }\n' '\n' 'To learn more, visit https://flutter.dev/go/build-aar\n' ) ); }); }); test('Current settings.gradle is in our legacy settings.gradle file set', () { // If this test fails, you probably edited templates/app/android.tmpl. // That's fine, but you now need to add a copy of that file to gradle/settings.gradle.legacy_versions, separated // from the previous versions by a line that just says ";EOF". final File templateSettingsDotGradle = globals.fs.file(globals.fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'app', 'android.tmpl', 'settings.gradle')); final File legacySettingsDotGradleFiles = globals.fs.file(globals.fs.path.join(Cache.flutterRoot, 'packages','flutter_tools', 'gradle', 'settings.gradle.legacy_versions')); expect( legacySettingsDotGradleFiles.readAsStringSync().split(';EOF').map<String>((String body) => body.trim()), contains(templateSettingsDotGradle.readAsStringSync().trim()), ); }, skip: true); // TODO(jonahwilliams): This is an integration test and should be moved to the integration shard. } /// Generates a fake app bundle at the location [directoryName]/[fileName]. FlutterProject generateFakeAppBundle(String directoryName, String fileName, FileSystem fileSystem) { final FlutterProject project = MockFlutterProject(); final AndroidProject androidProject = MockAndroidProject(); when(project.isModule).thenReturn(false); when(project.android).thenReturn(androidProject); when(androidProject.buildDirectory).thenReturn(fileSystem.directory('irrelevant')); final Directory bundleDirectory = getBundleDirectory(project); bundleDirectory .childDirectory(directoryName) .createSync(recursive: true); bundleDirectory .childDirectory(directoryName) .childFile(fileName) .createSync(); return project; } FakePlatform fakePlatform(String name) { return FakePlatform( environment: <String, String>{'HOME': '/path/to/home'}, operatingSystem: name, stdoutSupportsAnsi: false, ); } class FakeGradleUtils extends GradleUtils { @override String getExecutable(FlutterProject project) { return 'gradlew'; } } class MockAndroidSdk extends Mock implements AndroidSdk {} class MockAndroidProject extends Mock implements AndroidProject {} class MockAndroidStudio extends Mock implements AndroidStudio {} class MockFileSystemUtils extends Mock implements FileSystemUtils {} class MockFlutterProject extends Mock implements FlutterProject {} class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {} class MockProcessManager extends Mock implements ProcessManager {}