// 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_HOME` 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> setMinSdkVersion(int sdkVersion) async { final File buildScript = File( path.join(androidPath, 'app', 'build.gradle'), ); buildScript.openWrite(mode: FileMode.append).write(''' android { defaultConfig { minSdkVersion $sdkVersion } } '''); } 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'; }