apk_utils.dart 14.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8
// 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;

9 10
import 'task_result.dart';
import 'utils.dart';
11 12 13

final String platformLineSep = Platform.isWindows ? '\r\n' : '\n';

14 15
final List<String> flutterAssets = <String>[
  'assets/flutter_assets/AssetManifest.json',
16
  'assets/flutter_assets/NOTICES.Z',
17
  'assets/flutter_assets/fonts/MaterialIcons-Regular.otf',
18 19 20
  'assets/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf',
];

21 22 23 24 25 26 27 28 29 30 31
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',
];

32
/// Runs the given [testFunction] on a freshly generated Flutter project.
33
Future<void> runProjectTest(Future<void> Function(FlutterProject project) testFunction) async {
34 35 36 37 38 39
  final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_plugin_test.');
  final FlutterProject project = await FlutterProject.create(tempDir, 'hello');

  try {
    await testFunction(project);
  } finally {
40
    rmTree(tempDir);
41 42 43 44
  }
}

/// Runs the given [testFunction] on a freshly generated Flutter plugin project.
45
Future<void> runPluginProjectTest(Future<void> Function(FlutterPluginProject pluginProject) testFunction) async {
46 47 48 49 50 51 52 53 54 55
  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);
  }
}

56
/// Runs the given [testFunction] on a freshly generated Flutter module project.
57
Future<void> runModuleProjectTest(Future<void> Function(FlutterModuleProject moduleProject) testFunction) async {
58 59 60 61 62 63 64 65 66 67
  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);
  }
}

68
/// Returns the list of files inside an Android Package Kit.
69
Future<Iterable<String>> getFilesInApk(String apk) async {
70
  if (!File(apk).existsSync()) {
71 72
    throw TaskResult.failure(
        'Gradle did not produce an output artifact file at: $apk');
73 74 75 76 77 78 79
  }
  final String files = await _evalApkAnalyzer(
    <String>[
      'files',
      'list',
      apk,
    ]
80
  );
81
  return files.split('\n').map((String file) => file.substring(1).trim());
82
}
83
/// Returns the list of files inside an Android App Bundle.
84 85 86 87
Future<Iterable<String>> getFilesInAppBundle(String bundle) {
  return getFilesInApk(bundle);
}

88 89 90 91 92
/// Returns the list of files inside an Android Archive.
Future<Iterable<String>> getFilesInAar(String aar) {
  return getFilesInApk(aar);
}

93 94 95 96 97 98 99 100 101 102 103 104
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);
}

105 106
/// The Android home directory.
String get _androidHome {
107
  final String? androidHome = Platform.environment['ANDROID_HOME'] ??
108 109
      Platform.environment['ANDROID_SDK_ROOT'];
  if (androidHome == null || androidHome.isEmpty) {
110
    throw Exception('Environment variable `ANDROID_SDK_ROOT` is not set.');
111 112 113 114
  }
  return androidHome;
}

115 116 117
/// Executes an APK analyzer subcommand.
Future<String> _evalApkAnalyzer(
  List<String> args, {
118
  bool printStdout = false,
119
  String? workingDirectory,
120
}) async {
121
  final String? javaHome = await findJavaHome();
122 123 124 125
  if (javaHome == null || javaHome.isEmpty) {
    throw Exception('No JAVA_HOME set.');
  }
  final String apkAnalyzer = path
126 127 128 129 130 131 132 133 134 135 136 137 138
     .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,
       },
     );
   }

139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
  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.
161 162 163 164 165 166 167 168
class ApkExtractor {
  ApkExtractor(this.apkFile);

  /// The APK.
  final File apkFile;

  bool _extracted = false;

169
  Set<String> _classes = const <String>{};
170
  Set<String> _methods = const <String>{};
171

172
  Future<void> _extractDex() async {
173 174 175
    if (_extracted) {
      return;
    }
176 177 178 179 180 181 182
    final String packages = await _evalApkAnalyzer(
      <String>[
        'dex',
        'packages',
        apkFile.path,
      ],
    );
183
    final List<String> lines = packages.split('\n');
184
    _classes = Set<String>.from(
185 186
      lines.where((String line) => line.startsWith('C'))
           .map<String>((String line) => line.split('\t').last),
187 188
    );
    assert(_classes.isNotEmpty);
189 190 191 192 193
    _methods = Set<String>.from(
      lines.where((String line) => line.startsWith('M'))
           .map<String>((String line) => line.split('\t').last)
    );
    assert(_methods.isNotEmpty);
194 195 196 197 198
    _extracted = true;
  }

  /// Returns true if the APK contains a given class.
  Future<bool> containsClass(String className) async {
199 200
    await _extractDex();
    return _classes.contains(className);
201
  }
202 203 204 205 206 207 208

  /// 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);
  }
209 210
}

211
/// Gets the content of the `AndroidManifest.xml`.
212
Future<String> getAndroidManifest(String apk) async {
213
  return _evalApkAnalyzer(
214 215 216 217 218 219 220
    <String>[
      'manifest',
      'print',
      apk,
    ],
    workingDirectory: _androidHome,
  );
221 222
}

223
/// Checks that the classes are contained in the APK, throws otherwise.
224 225
Future<void> checkApkContainsClasses(File apk, List<String> classes) async {
  final ApkExtractor extractor = ApkExtractor(apk);
226
  for (final String className in classes) {
227
    if (!(await extractor.containsClass(className))) {
228
      throw Exception("APK doesn't contain class `$className`.");
229 230
    }
  }
231 232 233 234 235 236 237 238 239 240
}

/// 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`.");
    }
  }
241 242
}

243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
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');
258
  String get iosPath => path.join(rootPath, 'ios');
259

260
  Future<void> addCustomBuildType(String name, {required String initWith}) async {
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
    final File buildScript = File(
      path.join(androidPath, 'app', 'build.gradle'),
    );

    buildScript.openWrite(mode: FileMode.append).write('''

android {
    buildTypes {
        $name {
            initWith $initWith
        }
    }
}
    ''');
  }

277 278 279
  /// 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`.
280
  void addPlugin(String plugin, { String value = '' }) {
281
    final File pubspec = File(path.join(rootPath, 'pubspec.yaml'));
282
    String content = pubspec.readAsStringSync();
283
    content = content.replaceFirst(
284
      '${platformLineSep}dependencies:$platformLineSep',
285
      '${platformLineSep}dependencies:$platformLineSep  $plugin: $value$platformLineSep',
286
    );
287
    pubspec.writeAsStringSync(content, flush: true);
288 289 290 291 292 293 294 295
  }

  Future<void> getPackages() async {
    await inDirectory(Directory(rootPath), () async {
      await flutter('pub', options: <String>['get']);
    });
  }

296
  Future<void> addProductFlavors(Iterable<String> flavors) async {
297 298 299 300
    final File buildScript = File(
      path.join(androidPath, 'app', 'build.gradle'),
    );

301 302 303 304 305 306 307 308
    final String flavorConfig = flavors.map((String name) {
      return '''
$name {
    applicationIdSuffix ".$name"
    versionNameSuffix "-$name"
}
      ''';
    }).join('\n');
309

310
    buildScript.openWrite(mode: FileMode.append).write('''
311 312 313
android {
    flavorDimensions "mode"
    productFlavors {
314
        $flavorConfig
315 316 317 318 319 320 321 322 323 324 325 326
    }
}
    ''');
  }

  Future<void> introduceError() async {
    final File buildScript = File(
      path.join(androidPath, 'app', 'build.gradle'),
    );
    await buildScript.writeAsString((await buildScript.readAsString()).replaceAll('buildTypes', 'builTypes'));
  }

327 328 329 330 331
  Future<void> introducePubspecError() async {
    final File pubspec = File(
      path.join(parent.path, 'hello', 'pubspec.yaml')
    );
    final String contents = pubspec.readAsStringSync();
332 333
    final String newContents = contents.replaceFirst('${platformLineSep}flutter:$platformLineSep', '''

334 335 336 337 338 339 340 341
flutter:
  assets:
    - lib/gallery/example_code.dart

''');
    pubspec.writeAsStringSync(newContents);
  }

342
  Future<void> runGradleTask(String task, {List<String>? options}) async {
343 344 345
    return _runGradleTask(workingDirectory: androidPath, task: task, options: options);
  }

346
  Future<ProcessResult> resultOfGradleTask(String task, {List<String>? options}) {
347 348 349 350 351
    return _resultOfGradleTask(workingDirectory: androidPath, task: task, options: options);
  }

  Future<ProcessResult> resultOfFlutterCommand(String command, List<String> options) {
    return Process.run(
352
      path.join(flutterDirectory.path, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'),
353
      <String>[command, ...options],
354 355 356 357 358 359 360 361 362 363 364 365 366
      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 {
367
      await flutter('create', options: <String>['--template=plugin', '--platforms=ios,android', name]);
368 369 370 371 372 373 374
    });
    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');
375 376 377 378
  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');
379 380 381
  String get releaseBundlePath => path.join(examplePath, 'build', 'app', 'outputs', 'bundle', 'release', 'app.aab');
}

382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
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);
}

398 399 400 401 402
Future<void> _runGradleTask({
  required String workingDirectory,
  required String task,
  List<String>? options,
}) async {
403 404 405 406 407 408 409 410 411 412 413 414 415 416
  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';
}

417 418 419 420 421
Future<ProcessResult> _resultOfGradleTask({
  required String workingDirectory,
  required String task,
  List<String>? options,
}) async {
422
  section('Find Java');
423
  final String? javaHome = await findJavaHome();
424 425 426 427 428 429

  if (javaHome == null)
    throw TaskResult.failure('Could not find Java');

  print('\nUsing JAVA_HOME=$javaHome');

430 431 432 433
  final List<String> args = <String>[
    'app:$task',
    ...?options,
  ];
434 435
  final String gradle = path.join(workingDirectory, Platform.isWindows ? 'gradlew.bat' : './gradlew');
  print('┌── $gradle');
436
  print(File(path.join(workingDirectory, gradle)).readAsLinesSync().map((String line) => '| $line').join('\n'));
437 438 439 440 441 442 443 444
  print('└─────────────────────────────────────────────────────────────────────────────────────');
  print(
    'Running Gradle:\n'
    '  Executable: $gradle\n'
    '  Arguments: ${args.join(' ')}\n'
    '  Working directory: $workingDirectory\n'
    '  JAVA_HOME: $javaHome\n'
  );
445 446 447 448 449 450 451 452 453
  return Process.run(
    gradle,
    args,
    workingDirectory: workingDirectory,
    environment: <String, String>{ 'JAVA_HOME': javaHome },
  );
}

/// Returns [null] if target matches [expectedTarget], otherwise returns an error message.
454
String? validateSnapshotDependency(FlutterProject project, String expectedTarget) {
455
  final File snapshotBlob = File(
456
      path.join(project.rootPath, 'build', 'app', 'intermediates',
457
          'flutter', 'debug', 'flutter_build.d'));
458 459 460 461 462

  assert(snapshotBlob.existsSync());
  final String contentSnapshot = snapshotBlob.readAsStringSync();
  return contentSnapshot.contains('$expectedTarget ')
    ? null : 'Dependency file should have $expectedTarget as target. Instead found $contentSnapshot';
463
}