mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
426 lines
14 KiB
Dart
426 lines
14 KiB
Dart
// Copyright (c) 2016 The Chromium 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:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:flutter_devicelab/framework/framework.dart';
|
|
import 'package:flutter_devicelab/framework/utils.dart';
|
|
|
|
final List<String> flutterAssets = <String>[
|
|
'assets/flutter_assets/AssetManifest.json',
|
|
'assets/flutter_assets/LICENSE',
|
|
'assets/flutter_assets/fonts/MaterialIcons-Regular.ttf',
|
|
'assets/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf',
|
|
];
|
|
|
|
/// Runs the given [testFunction] on a freshly generated Flutter project.
|
|
Future<void> runProjectTest(Future<void> testFunction(FlutterProject project)) 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> testFunction(FlutterPluginProject pluginProject)) 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);
|
|
}
|
|
}
|
|
|
|
/// 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 Process unzip = await startProcess(
|
|
'unzip',
|
|
<String>['-v', apk],
|
|
isBot: false, // we just want to test the output, not have any debugging info
|
|
);
|
|
return unzip.stdout
|
|
.transform(utf8.decoder)
|
|
.transform(const LineSplitter())
|
|
.map((String line) => line.split(' ').last)
|
|
.toList();
|
|
}
|
|
/// 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);
|
|
}
|
|
|
|
void checkItContains<T>(Iterable<T> values, Iterable<T> collection) {
|
|
for (T value in values) {
|
|
if (!collection.contains(value)) {
|
|
throw TaskResult.failure('Expected to find `$value` in `$collection`.');
|
|
}
|
|
}
|
|
}
|
|
|
|
void checkItDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
|
|
for (T value in values) {
|
|
if (collection.contains(value)) {
|
|
throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
|
|
}
|
|
}
|
|
}
|
|
|
|
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('Unset env flag: `ANDROID_HOME` or `ANDROID_SDK_ROOT`.');
|
|
}
|
|
return androidHome;
|
|
}
|
|
|
|
/// Utility class to analyze the content inside an APK using dexdump,
|
|
/// which is provided by the Android SDK.
|
|
/// https://android.googlesource.com/platform/art/+/master/dexdump/dexdump.cc
|
|
class ApkExtractor {
|
|
ApkExtractor(this.apkFile);
|
|
|
|
/// The APK.
|
|
final File apkFile;
|
|
|
|
bool _extracted = false;
|
|
|
|
Directory _outputDir;
|
|
|
|
Future<void> _extractApk() async {
|
|
if (_extracted) {
|
|
return;
|
|
}
|
|
_outputDir = apkFile.parent.createTempSync('apk');
|
|
if (Platform.isWindows) {
|
|
await eval('7za', <String>['x', apkFile.path], workingDirectory: _outputDir.path);
|
|
} else {
|
|
await eval('unzip', <String>[apkFile.path], workingDirectory: _outputDir.path);
|
|
}
|
|
_extracted = true;
|
|
}
|
|
|
|
/// Returns the full path to the [dexdump] tool.
|
|
Future<String> _findDexDump() async {
|
|
String dexdumps;
|
|
if (Platform.isWindows) {
|
|
dexdumps = await eval('dir', <String>['/s/b', 'dexdump.exe'],
|
|
workingDirectory: _androidHome);
|
|
} else {
|
|
dexdumps = await eval('find', <String>[_androidHome, '-name', 'dexdump']);
|
|
}
|
|
if (dexdumps.isEmpty) {
|
|
throw Exception('Couldn\'t find a dexdump executable.');
|
|
}
|
|
return dexdumps.split('\n').first;
|
|
}
|
|
|
|
// Removes any temporary directory.
|
|
void dispose() {
|
|
if (!_extracted) {
|
|
return;
|
|
}
|
|
rmTree(_outputDir);
|
|
_extracted = true;
|
|
}
|
|
|
|
/// Returns true if the APK contains a given class.
|
|
Future<bool> containsClass(String className) async {
|
|
await _extractApk();
|
|
|
|
final String dexDump = await _findDexDump();
|
|
final String classesDex = path.join(_outputDir.path, 'classes.dex');
|
|
|
|
if (!File(classesDex).existsSync()) {
|
|
throw Exception('Couldn\'t find classes.dex in the APK.');
|
|
}
|
|
final String classDescriptors = await eval(dexDump,
|
|
<String>[classesDex], printStdout: false);
|
|
|
|
if (classDescriptors.isEmpty) {
|
|
throw Exception('No descriptors found in classes.dex.');
|
|
}
|
|
return classDescriptors.contains(className.replaceAll('.', '/'));
|
|
}
|
|
}
|
|
|
|
/// Gets the content of the `AndroidManifest.xml`.
|
|
Future<String> getAndroidManifest(String apk) {
|
|
final String apkAnalyzer = path.join(_androidHome, 'tools', 'bin', 'apkanalyzer');
|
|
return eval(apkAnalyzer, <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 (String className in classes) {
|
|
if (!(await extractor.containsClass(className))) {
|
|
throw Exception('APK doesn\'t contain class `$className`.');
|
|
}
|
|
}
|
|
extractor.dispose();
|
|
}
|
|
|
|
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');
|
|
|
|
Future<void> addCustomBuildType(String name, {String initWith}) async {
|
|
final File buildScript = File(
|
|
path.join(androidPath, 'app', 'build.gradle'),
|
|
);
|
|
|
|
buildScript.openWrite(mode: FileMode.append).write('''
|
|
|
|
android {
|
|
buildTypes {
|
|
$name {
|
|
initWith $initWith
|
|
}
|
|
}
|
|
}
|
|
''');
|
|
}
|
|
|
|
Future<void> addGlobalBuildType(String name, {String initWith}) async {
|
|
final File buildScript = File(
|
|
path.join(androidPath, 'build.gradle'),
|
|
);
|
|
|
|
buildScript.openWrite(mode: FileMode.append).write('''
|
|
subprojects {
|
|
afterEvaluate {
|
|
android {
|
|
buildTypes {
|
|
$name {
|
|
initWith $initWith
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
''');
|
|
}
|
|
|
|
Future<void> addPlugin(String plugin) async {
|
|
final File pubspec = File(path.join(rootPath, 'pubspec.yaml'));
|
|
String content = await pubspec.readAsString();
|
|
content = content.replaceFirst(
|
|
'\ndependencies:\n',
|
|
'\ndependencies:\n $plugin:\n',
|
|
);
|
|
await pubspec.writeAsString(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> 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', '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', 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', 'apk', 'debug', 'app-debug.apk');
|
|
String get releaseApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-release.apk');
|
|
String get releaseArmApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-armeabi-v7a-release.apk');
|
|
String get releaseArm64ApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-arm64-v8a-release.apk');
|
|
String get releaseBundlePath => path.join(examplePath, 'build', 'app', 'outputs', 'bundle', 'release', 'app.aab');
|
|
|
|
Future<void> runGradleTask(String task, {List<String> options}) async {
|
|
return _runGradleTask(workingDirectory: exampleAndroidPath, task: task, options: options);
|
|
}
|
|
}
|
|
|
|
Future<void> _runGradleTask({String workingDirectory, 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({String workingDirectory, 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().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 },
|
|
);
|
|
}
|
|
|
|
class _Dependencies {
|
|
_Dependencies(String depfilePath) {
|
|
// Depfile format:
|
|
// outfile1 outfile2 : file1.dart file2.dart file3.dart file\ 4.dart
|
|
final String contents = File(depfilePath).readAsStringSync();
|
|
final List<String> colonSeparated = contents.split(':');
|
|
targets = _processList(colonSeparated[0].trim());
|
|
dependencies = _processList(colonSeparated[1].trim());
|
|
}
|
|
|
|
final RegExp _separatorExpr = RegExp(r'([^\\]) ');
|
|
final RegExp _escapeExpr = RegExp(r'\\(.)');
|
|
|
|
Set<String> _processList(String rawText) {
|
|
return rawText
|
|
// Put every file on right-hand side on the separate line
|
|
.replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n')
|
|
.split('\n')
|
|
// Expand escape sequences, so that '\ ', for example,ß becomes ' '
|
|
.map<String>((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim())
|
|
.where((String path) => path.isNotEmpty)
|
|
.toSet();
|
|
}
|
|
|
|
Set<String> targets;
|
|
Set<String> dependencies;
|
|
}
|
|
|
|
/// Returns [null] if target matches [expectedTarget], otherwise returns an error message.
|
|
String validateSnapshotDependency(FlutterProject project, String expectedTarget) {
|
|
final _Dependencies deps = _Dependencies(
|
|
path.join(project.rootPath, 'build', 'app', 'intermediates',
|
|
'flutter', 'debug', 'android-arm', 'snapshot_blob.bin.d'));
|
|
return deps.targets.any((String target) => target.contains(expectedTarget)) ? null :
|
|
'Dependency file should have $expectedTarget as target. Instead has ${deps.targets}';
|
|
}
|