• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:convert';
7import 'dart:io';
8
9import 'package:path/path.dart' as path;
10import 'package:flutter_devicelab/framework/framework.dart';
11import 'package:flutter_devicelab/framework/utils.dart';
12
13/// Runs the given [testFunction] on a freshly generated Flutter project.
14Future<void> runProjectTest(Future<void> testFunction(FlutterProject project)) async {
15  final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_plugin_test.');
16  final FlutterProject project = await FlutterProject.create(tempDir, 'hello');
17
18  try {
19    await testFunction(project);
20  } finally {
21    rmTree(tempDir);
22  }
23}
24
25/// Runs the given [testFunction] on a freshly generated Flutter plugin project.
26Future<void> runPluginProjectTest(Future<void> testFunction(FlutterPluginProject pluginProject)) async {
27  final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_plugin_test.');
28  final FlutterPluginProject pluginProject = await FlutterPluginProject.create(tempDir, 'aaa');
29
30  try {
31    await testFunction(pluginProject);
32  } finally {
33    rmTree(tempDir);
34  }
35}
36
37/// Returns the list of files inside an Android Package Kit.
38Future<Iterable<String>> getFilesInApk(String apk) async {
39  if (!File(apk).existsSync())
40    throw TaskResult.failure(
41        'Gradle did not produce an output artifact file at: $apk');
42
43  final Process unzip = await startProcess(
44    'unzip',
45    <String>['-v', apk],
46    isBot: false, // we just want to test the output, not have any debugging info
47  );
48  return unzip.stdout
49      .transform(utf8.decoder)
50      .transform(const LineSplitter())
51      .map((String line) => line.split(' ').last)
52      .toList();
53}
54/// Returns the list of files inside an Android App Bundle.
55Future<Iterable<String>> getFilesInAppBundle(String bundle) {
56  return getFilesInApk(bundle);
57}
58
59/// Returns the list of files inside an Android Archive.
60Future<Iterable<String>> getFilesInAar(String aar) {
61  return getFilesInApk(aar);
62}
63
64void checkItContains<T>(Iterable<T> values, Iterable<T> collection) {
65  for (T value in values) {
66    if (!collection.contains(value)) {
67      throw TaskResult.failure('Expected to find `$value` in `$collection`.');
68    }
69  }
70}
71
72void checkItDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
73  for (T value in values) {
74    if (collection.contains(value)) {
75      throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
76    }
77  }
78}
79
80TaskResult failure(String message, ProcessResult result) {
81  print('Unexpected process result:');
82  print('Exit code: ${result.exitCode}');
83  print('Std out  :\n${result.stdout}');
84  print('Std err  :\n${result.stderr}');
85  return TaskResult.failure(message);
86}
87
88bool hasMultipleOccurrences(String text, Pattern pattern) {
89  return text.indexOf(pattern) != text.lastIndexOf(pattern);
90}
91
92/// The Android home directory.
93String get _androidHome {
94  final String androidHome = Platform.environment['ANDROID_HOME'] ??
95      Platform.environment['ANDROID_SDK_ROOT'];
96  if (androidHome == null || androidHome.isEmpty) {
97    throw Exception('Unset env flag: `ANDROID_HOME` or `ANDROID_SDK_ROOT`.');
98  }
99  return androidHome;
100}
101
102/// Utility class to analyze the content inside an APK using dexdump,
103/// which is provided by the Android SDK.
104/// https://android.googlesource.com/platform/art/+/master/dexdump/dexdump.cc
105class ApkExtractor {
106  ApkExtractor(this.apkFile);
107
108  /// The APK.
109  final File apkFile;
110
111  bool _extracted = false;
112
113  Directory _outputDir;
114
115  Future<void> _extractApk() async {
116    if (_extracted) {
117      return;
118    }
119    _outputDir = apkFile.parent.createTempSync('apk');
120    if (Platform.isWindows) {
121      await eval('7za', <String>['x', apkFile.path], workingDirectory: _outputDir.path);
122    } else {
123      await eval('unzip', <String>[apkFile.path], workingDirectory: _outputDir.path);
124    }
125    _extracted = true;
126  }
127
128  /// Returns the full path to the [dexdump] tool.
129  Future<String> _findDexDump() async {
130    String dexdumps;
131    if (Platform.isWindows) {
132      dexdumps = await eval('dir', <String>['/s/b', 'dexdump.exe'],
133          workingDirectory: _androidHome);
134    } else {
135      dexdumps = await eval('find', <String>[_androidHome, '-name', 'dexdump']);
136    }
137    if (dexdumps.isEmpty) {
138      throw Exception('Couldn\'t find a dexdump executable.');
139    }
140    return dexdumps.split('\n').first;
141  }
142
143  // Removes any temporary directory.
144  void dispose() {
145    if (!_extracted) {
146      return;
147    }
148    rmTree(_outputDir);
149    _extracted = true;
150  }
151
152  /// Returns true if the APK contains a given class.
153  Future<bool> containsClass(String className) async {
154    await _extractApk();
155
156    final String dexDump = await _findDexDump();
157    final String classesDex = path.join(_outputDir.path, 'classes.dex');
158
159    if (!File(classesDex).existsSync()) {
160      throw Exception('Couldn\'t find classes.dex in the APK.');
161    }
162    final String classDescriptors = await eval(dexDump,
163        <String>[classesDex], printStdout: false);
164
165    if (classDescriptors.isEmpty) {
166      throw Exception('No descriptors found in classes.dex.');
167    }
168    return classDescriptors.contains(className.replaceAll('.', '/'));
169  }
170}
171
172/// Gets the content of the `AndroidManifest.xml`.
173Future<String> getAndroidManifest(String apk) {
174  final String apkAnalyzer = path.join(_androidHome, 'tools', 'bin', 'apkanalyzer');
175  return eval(apkAnalyzer, <String>['manifest', 'print', apk],
176      workingDirectory: _androidHome);
177}
178
179 /// Checks that the classes are contained in the APK, throws otherwise.
180Future<void> checkApkContainsClasses(File apk, List<String> classes) async {
181  final ApkExtractor extractor = ApkExtractor(apk);
182  for (String className in classes) {
183    if (!(await extractor.containsClass(className))) {
184      throw Exception('APK doesn\'t contain class `$className`.');
185    }
186  }
187  extractor.dispose();
188}
189
190class FlutterProject {
191  FlutterProject(this.parent, this.name);
192
193  final Directory parent;
194  final String name;
195
196  static Future<FlutterProject> create(Directory directory, String name) async {
197    await inDirectory(directory, () async {
198      await flutter('create', options: <String>['--template=app', name]);
199    });
200    return FlutterProject(directory, name);
201  }
202
203  String get rootPath => path.join(parent.path, name);
204  String get androidPath => path.join(rootPath, 'android');
205
206  Future<void> addCustomBuildType(String name, {String initWith}) async {
207    final File buildScript = File(
208      path.join(androidPath, 'app', 'build.gradle'),
209    );
210
211    buildScript.openWrite(mode: FileMode.append).write('''
212
213android {
214    buildTypes {
215        $name {
216            initWith $initWith
217        }
218    }
219}
220    ''');
221  }
222
223  Future<void> addGlobalBuildType(String name, {String initWith}) async {
224    final File buildScript = File(
225      path.join(androidPath, 'build.gradle'),
226    );
227
228    buildScript.openWrite(mode: FileMode.append).write('''
229subprojects {
230  afterEvaluate {
231    android {
232        buildTypes {
233            $name {
234                initWith $initWith
235            }
236        }
237    }
238  }
239}
240    ''');
241  }
242
243  Future<void> addPlugin(String plugin) async {
244    final File pubspec = File(path.join(rootPath, 'pubspec.yaml'));
245    String content = await pubspec.readAsString();
246    content = content.replaceFirst(
247      '\ndependencies:\n',
248      '\ndependencies:\n  $plugin:\n',
249    );
250    await pubspec.writeAsString(content, flush: true);
251  }
252
253  Future<void> getPackages() async {
254    await inDirectory(Directory(rootPath), () async {
255      await flutter('pub', options: <String>['get']);
256    });
257  }
258
259  Future<void> addProductFlavors(Iterable<String> flavors) async {
260    final File buildScript = File(
261      path.join(androidPath, 'app', 'build.gradle'),
262    );
263
264    final String flavorConfig = flavors.map((String name) {
265      return '''
266$name {
267    applicationIdSuffix ".$name"
268    versionNameSuffix "-$name"
269}
270      ''';
271    }).join('\n');
272
273    buildScript.openWrite(mode: FileMode.append).write('''
274android {
275    flavorDimensions "mode"
276    productFlavors {
277        $flavorConfig
278    }
279}
280    ''');
281  }
282
283  Future<void> introduceError() async {
284    final File buildScript = File(
285      path.join(androidPath, 'app', 'build.gradle'),
286    );
287    await buildScript.writeAsString((await buildScript.readAsString()).replaceAll('buildTypes', 'builTypes'));
288  }
289
290  Future<void> runGradleTask(String task, {List<String> options}) async {
291    return _runGradleTask(workingDirectory: androidPath, task: task, options: options);
292  }
293
294  Future<ProcessResult> resultOfGradleTask(String task, {List<String> options}) {
295    return _resultOfGradleTask(workingDirectory: androidPath, task: task, options: options);
296  }
297
298  Future<ProcessResult> resultOfFlutterCommand(String command, List<String> options) {
299    return Process.run(
300      path.join(flutterDirectory.path, 'bin', 'flutter'),
301      <String>[command, ...options],
302      workingDirectory: rootPath,
303    );
304  }
305}
306
307class FlutterPluginProject {
308  FlutterPluginProject(this.parent, this.name);
309
310  final Directory parent;
311  final String name;
312
313  static Future<FlutterPluginProject> create(Directory directory, String name) async {
314    await inDirectory(directory, () async {
315      await flutter('create', options: <String>['--template=plugin', name]);
316    });
317    return FlutterPluginProject(directory, name);
318  }
319
320  String get rootPath => path.join(parent.path, name);
321  String get examplePath => path.join(rootPath, 'example');
322  String get exampleAndroidPath => path.join(examplePath, 'android');
323  String get debugApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'debug', 'app-debug.apk');
324  String get releaseApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-release.apk');
325  String get releaseArmApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-armeabi-v7a-release.apk');
326  String get releaseArm64ApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-arm64-v8a-release.apk');
327  String get releaseBundlePath => path.join(examplePath, 'build', 'app', 'outputs', 'bundle', 'release', 'app.aab');
328
329  Future<void> runGradleTask(String task, {List<String> options}) async {
330    return _runGradleTask(workingDirectory: exampleAndroidPath, task: task, options: options);
331  }
332}
333
334Future<void> _runGradleTask({String workingDirectory, String task, List<String> options}) async {
335  final ProcessResult result = await _resultOfGradleTask(
336      workingDirectory: workingDirectory,
337      task: task,
338      options: options);
339  if (result.exitCode != 0) {
340    print('stdout:');
341    print(result.stdout);
342    print('stderr:');
343    print(result.stderr);
344  }
345  if (result.exitCode != 0)
346    throw 'Gradle exited with error';
347}
348
349Future<ProcessResult> _resultOfGradleTask({String workingDirectory, String task,
350    List<String> options}) async {
351  section('Find Java');
352  final String javaHome = await findJavaHome();
353
354  if (javaHome == null)
355    throw TaskResult.failure('Could not find Java');
356
357  print('\nUsing JAVA_HOME=$javaHome');
358
359  final List<String> args = <String>[
360    'app:$task',
361    ...?options,
362  ];
363  final String gradle = path.join(workingDirectory, Platform.isWindows ? 'gradlew.bat' : './gradlew');
364  print('┌── $gradle');
365  print('│ ' + File(path.join(workingDirectory, gradle)).readAsLinesSync().join('\n│ '));
366  print('└─────────────────────────────────────────────────────────────────────────────────────');
367  print(
368    'Running Gradle:\n'
369    '  Executable: $gradle\n'
370    '  Arguments: ${args.join(' ')}\n'
371    '  Working directory: $workingDirectory\n'
372    '  JAVA_HOME: $javaHome\n'
373    ''
374  );
375  return Process.run(
376    gradle,
377    args,
378    workingDirectory: workingDirectory,
379    environment: <String, String>{ 'JAVA_HOME': javaHome },
380  );
381}
382
383class _Dependencies {
384  _Dependencies(String depfilePath) {
385    final RegExp _separatorExpr = RegExp(r'([^\\]) ');
386    final RegExp _escapeExpr = RegExp(r'\\(.)');
387
388    // Depfile format:
389    // outfile1 outfile2 : file1.dart file2.dart file3.dart file\ 4.dart
390    final String contents = File(depfilePath).readAsStringSync();
391    final List<String> colonSeparated = contents.split(': ');
392    target = colonSeparated[0].trim();
393    dependencies = colonSeparated[1]
394        // Put every file on right-hand side on the separate line
395        .replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n')
396        .split('\n')
397        // Expand escape sequences, so that '\ ', for example,ß becomes ' '
398        .map<String>((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim())
399        .where((String path) => path.isNotEmpty)
400        .toSet();
401  }
402
403  String target;
404  Set<String> dependencies;
405}
406
407/// Returns [null] if target matches [expectedTarget], otherwise returns an error message.
408String validateSnapshotDependency(FlutterProject project, String expectedTarget) {
409  final _Dependencies deps = _Dependencies(
410      path.join(project.rootPath, 'build', 'app', 'intermediates',
411          'flutter', 'debug', 'android-arm', 'snapshot_blob.bin.d'));
412  return deps.target == expectedTarget ? null :
413    'Dependency file should have $expectedTarget as target. Instead has ${deps.target}';
414}
415