| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as path; |
| |
| import 'task_result.dart'; |
| import 'utils.dart'; |
| |
| final String platformLineSep = Platform.isWindows ? '\r\n' : '\n'; |
| |
| final List<String> flutterAssets = <String>[ |
| 'assets/flutter_assets/AssetManifest.json', |
| 'assets/flutter_assets/NOTICES.Z', |
| 'assets/flutter_assets/fonts/MaterialIcons-Regular.otf', |
| 'assets/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf', |
| ]; |
| |
| final List<String> debugAssets = <String>[ |
| 'assets/flutter_assets/isolate_snapshot_data', |
| 'assets/flutter_assets/kernel_blob.bin', |
| 'assets/flutter_assets/vm_snapshot_data', |
| ]; |
| |
| final List<String> baseApkFiles = <String> [ |
| 'classes.dex', |
| 'AndroidManifest.xml', |
| ]; |
| |
| /// Runs the given [testFunction] on a freshly generated Flutter project. |
| Future<void> runProjectTest(Future<void> Function(FlutterProject project) testFunction) async { |
| final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_plugin_test.'); |
| final FlutterProject project = await FlutterProject.create(tempDir, 'hello'); |
| |
| try { |
| await testFunction(project); |
| } finally { |
| rmTree(tempDir); |
| } |
| } |
| |
| /// Runs the given [testFunction] on a freshly generated Flutter plugin project. |
| Future<void> runPluginProjectTest(Future<void> Function(FlutterPluginProject pluginProject) testFunction) async { |
| final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_plugin_test.'); |
| final FlutterPluginProject pluginProject = await FlutterPluginProject.create(tempDir, 'aaa'); |
| |
| try { |
| await testFunction(pluginProject); |
| } finally { |
| rmTree(tempDir); |
| } |
| } |
| |
| /// Runs the given [testFunction] on a freshly generated Flutter module project. |
| Future<void> runModuleProjectTest(Future<void> Function(FlutterModuleProject moduleProject) testFunction) async { |
| final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_module_test.'); |
| final FlutterModuleProject moduleProject = await FlutterModuleProject.create(tempDir, 'hello_module'); |
| |
| try { |
| await testFunction(moduleProject); |
| } finally { |
| rmTree(tempDir); |
| } |
| } |
| |
| /// Returns the list of files inside an Android Package Kit. |
| Future<Iterable<String>> getFilesInApk(String apk) async { |
| if (!File(apk).existsSync()) { |
| throw TaskResult.failure( |
| 'Gradle did not produce an output artifact file at: $apk'); |
| } |
| final String files = await _evalApkAnalyzer( |
| <String>[ |
| 'files', |
| 'list', |
| apk, |
| ] |
| ); |
| return files.split('\n').map((String file) => file.substring(1).trim()); |
| } |
| /// Returns the list of files inside an Android App Bundle. |
| Future<Iterable<String>> getFilesInAppBundle(String bundle) { |
| return getFilesInApk(bundle); |
| } |
| |
| /// Returns the list of files inside an Android Archive. |
| Future<Iterable<String>> getFilesInAar(String aar) { |
| return getFilesInApk(aar); |
| } |
| |
| TaskResult failure(String message, ProcessResult result) { |
| print('Unexpected process result:'); |
| print('Exit code: ${result.exitCode}'); |
| print('Std out :\n${result.stdout}'); |
| print('Std err :\n${result.stderr}'); |
| return TaskResult.failure(message); |
| } |
| |
| bool hasMultipleOccurrences(String text, Pattern pattern) { |
| return text.indexOf(pattern) != text.lastIndexOf(pattern); |
| } |
| |
| /// The Android home directory. |
| String get _androidHome { |
| final String? androidHome = Platform.environment['ANDROID_HOME'] ?? |
| Platform.environment['ANDROID_SDK_ROOT']; |
| if (androidHome == null || androidHome.isEmpty) { |
| throw Exception('Environment variable `ANDROID_SDK_ROOT` is not set.'); |
| } |
| return androidHome; |
| } |
| |
| /// Executes an APK analyzer subcommand. |
| Future<String> _evalApkAnalyzer( |
| List<String> args, { |
| bool printStdout = false, |
| String? workingDirectory, |
| }) async { |
| final String? javaHome = await findJavaHome(); |
| if (javaHome == null || javaHome.isEmpty) { |
| throw Exception('No JAVA_HOME set.'); |
| } |
| final String apkAnalyzer = path |
| .join(_androidHome, 'cmdline-tools', 'latest', 'bin', Platform.isWindows ? 'apkanalyzer.bat' : 'apkanalyzer'); |
| if (canRun(apkAnalyzer)) { |
| return eval( |
| apkAnalyzer, |
| args, |
| printStdout: printStdout, |
| workingDirectory: workingDirectory, |
| environment: <String, String>{ |
| 'JAVA_HOME': javaHome, |
| }, |
| ); |
| } |
| |
| final String javaBinary = path.join(javaHome, 'bin', 'java'); |
| assert(canRun(javaBinary)); |
| final String androidTools = path.join(_androidHome, 'tools'); |
| final String libs = path.join(androidTools, 'lib'); |
| assert(Directory(libs).existsSync()); |
| |
| final String classSeparator = Platform.isWindows ? ';' : ':'; |
| return eval( |
| javaBinary, |
| <String>[ |
| '-Dcom.android.sdklib.toolsdir=$androidTools', |
| '-classpath', |
| '.$classSeparator$libs${Platform.pathSeparator}*', |
| 'com.android.tools.apk.analyzer.ApkAnalyzerCli', |
| ...args, |
| ], |
| printStdout: printStdout, |
| workingDirectory: workingDirectory, |
| ); |
| } |
| |
| /// Utility class to analyze the content inside an APK using the APK analyzer. |
| class ApkExtractor { |
| ApkExtractor(this.apkFile); |
| |
| /// The APK. |
| final File apkFile; |
| |
| bool _extracted = false; |
| |
| Set<String> _classes = const <String>{}; |
| Set<String> _methods = const <String>{}; |
| |
| Future<void> _extractDex() async { |
| if (_extracted) { |
| return; |
| } |
| final String packages = await _evalApkAnalyzer( |
| <String>[ |
| 'dex', |
| 'packages', |
| apkFile.path, |
| ], |
| ); |
| final List<String> lines = packages.split('\n'); |
| _classes = Set<String>.from( |
| lines.where((String line) => line.startsWith('C')) |
| .map<String>((String line) => line.split('\t').last), |
| ); |
| assert(_classes.isNotEmpty); |
| _methods = Set<String>.from( |
| lines.where((String line) => line.startsWith('M')) |
| .map<String>((String line) => line.split('\t').last) |
| ); |
| assert(_methods.isNotEmpty); |
| _extracted = true; |
| } |
| |
| /// Returns true if the APK contains a given class. |
| Future<bool> containsClass(String className) async { |
| await _extractDex(); |
| return _classes.contains(className); |
| } |
| |
| /// Returns true if the APK contains a given method. |
| /// For example: io.flutter.plugins.googlemaps.GoogleMapController void onFlutterViewAttached(android.view.View) |
| Future<bool> containsMethod(String methodName) async { |
| await _extractDex(); |
| return _methods.contains(methodName); |
| } |
| } |
| |
| /// Gets the content of the `AndroidManifest.xml`. |
| Future<String> getAndroidManifest(String apk) async { |
| return _evalApkAnalyzer( |
| <String>[ |
| 'manifest', |
| 'print', |
| apk, |
| ], |
| workingDirectory: _androidHome, |
| ); |
| } |
| |
| /// Checks that the classes are contained in the APK, throws otherwise. |
| Future<void> checkApkContainsClasses(File apk, List<String> classes) async { |
| final ApkExtractor extractor = ApkExtractor(apk); |
| for (final String className in classes) { |
| if (!(await extractor.containsClass(className))) { |
| throw Exception("APK doesn't contain class `$className`."); |
| } |
| } |
| } |
| |
| /// Checks that the methods are defined in the APK, throws otherwise. |
| Future<void> checkApkContainsMethods(File apk, List<String> methods) async { |
| final ApkExtractor extractor = ApkExtractor(apk); |
| for (final String method in methods) { |
| if (!(await extractor.containsMethod(method))) { |
| throw Exception("APK doesn't contain method `$method`."); |
| } |
| } |
| } |
| |
| class FlutterProject { |
| FlutterProject(this.parent, this.name); |
| |
| final Directory parent; |
| final String name; |
| |
| static Future<FlutterProject> create(Directory directory, String name) async { |
| await inDirectory(directory, () async { |
| await flutter('create', options: <String>['--template=app', name]); |
| }); |
| return FlutterProject(directory, name); |
| } |
| |
| String get rootPath => path.join(parent.path, name); |
| String get androidPath => path.join(rootPath, 'android'); |
| String get iosPath => path.join(rootPath, 'ios'); |
| |
| Future<void> addCustomBuildType(String name, {required String initWith}) async { |
| final File buildScript = File( |
| path.join(androidPath, 'app', 'build.gradle'), |
| ); |
| |
| buildScript.openWrite(mode: FileMode.append).write(''' |
| |
| android { |
| buildTypes { |
| $name { |
| initWith $initWith |
| } |
| } |
| } |
| '''); |
| } |
| |
| /// Adds a plugin to the pubspec. |
| /// In pubspec, each dependency is expressed as key, value pair joined by a colon `:`. |
| /// such as `plugin_a`:`^0.0.1` or `plugin_a`:`\npath: /some/path`. |
| void addPlugin(String plugin, { String value = '' }) { |
| final File pubspec = File(path.join(rootPath, 'pubspec.yaml')); |
| String content = pubspec.readAsStringSync(); |
| content = content.replaceFirst( |
| '${platformLineSep}dependencies:$platformLineSep', |
| '${platformLineSep}dependencies:$platformLineSep $plugin: $value$platformLineSep', |
| ); |
| pubspec.writeAsStringSync(content, flush: true); |
| } |
| |
| Future<void> getPackages() async { |
| await inDirectory(Directory(rootPath), () async { |
| await flutter('pub', options: <String>['get']); |
| }); |
| } |
| |
| Future<void> addProductFlavors(Iterable<String> flavors) async { |
| final File buildScript = File( |
| path.join(androidPath, 'app', 'build.gradle'), |
| ); |
| |
| final String flavorConfig = flavors.map((String name) { |
| return ''' |
| $name { |
| applicationIdSuffix ".$name" |
| versionNameSuffix "-$name" |
| } |
| '''; |
| }).join('\n'); |
| |
| buildScript.openWrite(mode: FileMode.append).write(''' |
| android { |
| flavorDimensions "mode" |
| productFlavors { |
| $flavorConfig |
| } |
| } |
| '''); |
| } |
| |
| Future<void> introduceError() async { |
| final File buildScript = File( |
| path.join(androidPath, 'app', 'build.gradle'), |
| ); |
| await buildScript.writeAsString((await buildScript.readAsString()).replaceAll('buildTypes', 'builTypes')); |
| } |
| |
| Future<void> introducePubspecError() async { |
| final File pubspec = File( |
| path.join(parent.path, 'hello', 'pubspec.yaml') |
| ); |
| final String contents = pubspec.readAsStringSync(); |
| final String newContents = contents.replaceFirst('${platformLineSep}flutter:$platformLineSep', ''' |
| |
| flutter: |
| assets: |
| - lib/gallery/example_code.dart |
| |
| '''); |
| pubspec.writeAsStringSync(newContents); |
| } |
| |
| Future<void> runGradleTask(String task, {List<String>? options}) async { |
| return _runGradleTask(workingDirectory: androidPath, task: task, options: options); |
| } |
| |
| Future<ProcessResult> resultOfGradleTask(String task, {List<String>? options}) { |
| return _resultOfGradleTask(workingDirectory: androidPath, task: task, options: options); |
| } |
| |
| Future<ProcessResult> resultOfFlutterCommand(String command, List<String> options) { |
| return Process.run( |
| path.join(flutterDirectory.path, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'), |
| <String>[command, ...options], |
| workingDirectory: rootPath, |
| ); |
| } |
| } |
| |
| class FlutterPluginProject { |
| FlutterPluginProject(this.parent, this.name); |
| |
| final Directory parent; |
| final String name; |
| |
| static Future<FlutterPluginProject> create(Directory directory, String name) async { |
| await inDirectory(directory, () async { |
| await flutter('create', options: <String>['--template=plugin', '--platforms=ios,android', name]); |
| }); |
| return FlutterPluginProject(directory, name); |
| } |
| |
| String get rootPath => path.join(parent.path, name); |
| String get examplePath => path.join(rootPath, 'example'); |
| String get exampleAndroidPath => path.join(examplePath, 'android'); |
| String get debugApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk', 'app-debug.apk'); |
| String get releaseApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk', 'app-release.apk'); |
| String get releaseArmApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk','app-armeabi-v7a-release.apk'); |
| String get releaseArm64ApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk', 'app-arm64-v8a-release.apk'); |
| String get releaseBundlePath => path.join(examplePath, 'build', 'app', 'outputs', 'bundle', 'release', 'app.aab'); |
| } |
| |
| class FlutterModuleProject { |
| FlutterModuleProject(this.parent, this.name); |
| |
| final Directory parent; |
| final String name; |
| |
| static Future<FlutterModuleProject> create(Directory directory, String name) async { |
| await inDirectory(directory, () async { |
| await flutter('create', options: <String>['--template=module', name]); |
| }); |
| return FlutterModuleProject(directory, name); |
| } |
| |
| String get rootPath => path.join(parent.path, name); |
| } |
| |
| Future<void> _runGradleTask({ |
| required String workingDirectory, |
| required String task, |
| List<String>? options, |
| }) async { |
| final ProcessResult result = await _resultOfGradleTask( |
| workingDirectory: workingDirectory, |
| task: task, |
| options: options); |
| if (result.exitCode != 0) { |
| print('stdout:'); |
| print(result.stdout); |
| print('stderr:'); |
| print(result.stderr); |
| } |
| if (result.exitCode != 0) |
| throw 'Gradle exited with error'; |
| } |
| |
| Future<ProcessResult> _resultOfGradleTask({ |
| required String workingDirectory, |
| required String task, |
| List<String>? options, |
| }) async { |
| section('Find Java'); |
| final String? javaHome = await findJavaHome(); |
| |
| if (javaHome == null) |
| throw TaskResult.failure('Could not find Java'); |
| |
| print('\nUsing JAVA_HOME=$javaHome'); |
| |
| final List<String> args = <String>[ |
| 'app:$task', |
| ...?options, |
| ]; |
| final String gradle = path.join(workingDirectory, Platform.isWindows ? 'gradlew.bat' : './gradlew'); |
| print('┌── $gradle'); |
| print(File(path.join(workingDirectory, gradle)).readAsLinesSync().map((String line) => '| $line').join('\n')); |
| print('└─────────────────────────────────────────────────────────────────────────────────────'); |
| print( |
| 'Running Gradle:\n' |
| ' Executable: $gradle\n' |
| ' Arguments: ${args.join(' ')}\n' |
| ' Working directory: $workingDirectory\n' |
| ' JAVA_HOME: $javaHome\n' |
| ); |
| return Process.run( |
| gradle, |
| args, |
| workingDirectory: workingDirectory, |
| environment: <String, String>{ 'JAVA_HOME': javaHome }, |
| ); |
| } |
| |
| /// Returns [null] if target matches [expectedTarget], otherwise returns an error message. |
| String? validateSnapshotDependency(FlutterProject project, String expectedTarget) { |
| final File snapshotBlob = File( |
| path.join(project.rootPath, 'build', 'app', 'intermediates', |
| 'flutter', 'debug', 'flutter_build.d')); |
| |
| assert(snapshotBlob.existsSync()); |
| final String contentSnapshot = snapshotBlob.readAsStringSync(); |
| return contentSnapshot.contains('$expectedTarget ') |
| ? null : 'Dependency file should have $expectedTarget as target. Instead found $contentSnapshot'; |
| } |