Skip to content

Instantly share code, notes, and snippets.

@GaryQian
Created August 25, 2020 01:39
Show Gist options
  • Select an option

  • Save GaryQian/1b65918904e9879b76c01f553e41105c to your computer and use it in GitHub Desktop.

Select an option

Save GaryQian/1b65918904e9879b76c01f553e41105c to your computer and use it in GitHub Desktop.
// Copyright 2020 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:convert';
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../build_system/build_system.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../runner/flutter_command.dart';
class FeatureModule {
String name;
List<String> dartLibs;
List<String> assets;
FeatureModule({
this.name,
this.dartLibs,
this.assets,
});
}
void setupBundleGradle(Environment env, BuildResult result) {
print('setup Bundle Gradle');
Directory androidDir = env.projectDir.childDirectory('android');
List<String> loadingUnits = _getLoadingUnits(env);
compareAgainstGolden(loadingUnits, androidDir);
for (String loadingUnitName in loadingUnits) {
_setupFiles(androidDir, loadingUnitName, result, env);
}
createLoadingUnitGolden(loadingUnits, androidDir);
createBundleConfig(loadingUnits, env);
}
void moveSoToModuleLibs(Environment env, BuildResult result) {
print('moveSoToModuleLibs');
Directory androidDir = env.projectDir.childDirectory('android');
_parseBundleConfig(env);
List<String> loadingUnits = _getLoadingUnits(env);
for (String loadingUnitName in loadingUnits) {
_moveSoFiles(androidDir, loadingUnitName, result, env);
}
}
List<String> _getLoadingUnits(Environment env) {
List<String> loadingUnits = [];
List<FileSystemEntity> files = env.outputDir.listSync(recursive: true);
while (files.length != 0) {
if (files.last is File) {
File file = files.last;
String subPath = file.path;
if (!subPath.contains('manifest.json')) {
files.removeLast();
continue;
}
// Read gen_snapshot manifest
String fileString = file.readAsStringSync();
Map manifest = jsonDecode(fileString);
// Setup android source directory
print('FINDING MODULES ${file.path}');
for (Map loadingUnitMetadata in manifest['loadingUnits']) {
if (loadingUnitMetadata['id'] == 1) continue;
loadingUnits.add('module${loadingUnitMetadata['id'].toString()}');
}
break;
}
files.removeLast();
}
return loadingUnits;
}
List<FeatureModule> _parseBundleConfig(Environment env) {
File configFile = env.projectDir.childFile('bundle_config.yaml');
if (!configFile.existsSync()) {
print('No bundle_config.yaml found');
return [];
}
var yamlData = loadYaml(configFile.readAsStringSync());
print(yamlData);
List<FeatureModule> results = [];
for (Map module in yamlData) {
results.add(FeatureModule(name: module['name'], dartLibs: module['libraries']?.cast<String>(), assets: module['assets']?.cast<String>()));
}
return results;
}
void _moveSoFiles(Directory androidDir, String moduleName, BuildResult result, Environment env) {
// MOVE TO ASSEMBLE/POSTBUILD
print('MOVING SO FILES FOR $moduleName');
Directory moduleDir = androidDir.childDirectory(moduleName);
Directory jniLibsDir = moduleDir.childDirectory('src').childDirectory('main').childDirectory('jniLibs');
jniLibsDir.createSync(recursive: true);
List<FileSystemEntity> files = env.outputDir.listSync(recursive: true);
while (files.length != 0) {
FileSystemEntity file = files.last;
if (file is File) {
String subPath = file.path;
if (!subPath.contains('part.so')) {
files.removeLast();
continue;
}
subPath = subPath.substring(subPath.lastIndexOf('release/') + 8);
print(jniLibsDir.childFile(subPath).path);
jniLibsDir.childFile(subPath).createSync(recursive: true);
(file as File).copySync(jniLibsDir.childFile(subPath).path);
}
files.removeLast();
}
}
void _setupFiles(Directory androidDir, String moduleName, BuildResult result, Environment env) {
print('setupFiles for $moduleName');
Directory moduleDir = androidDir.childDirectory(moduleName);
File stringRes = androidDir.childDirectory('app').childDirectory('src').childDirectory('main').childDirectory('res').childDirectory('values').childFile('strings.xml');
stringRes.createSync(recursive: true);
stringRes.writeAsStringSync(
'''
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="${moduleName}Name">$moduleName</string>
</resources>
''', flush: true);
File androidManifest = moduleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
androidManifest.createSync(recursive: true);
// androidManifest.writeAsStringSync(
// '''
// <manifest xmlns:dist="http://schemas.android.com/apk/distribution"
// package="com.example.$moduleName"
// split="$moduleName"
// android:isFeatureSplit="${isBase ? false : true}">
// <dist:module dist:instant="false"
// dist:title="@string/$moduleName"
// <dist:fusing dist:include="true" />
// </dist:module>
// <dist:delivery>
// <dist:install-time>
// <dist:removable value="false" />
// </dist:install-time>
// <dist:on-demand/>
// </dist:delivery>
// <application android:hasCode="${isBase ? 'true' : 'false'}"${isBase ? ' tools:replace="android:hasCode"' : ''}>
// </application>
// </manifest>
// ''', flush: true);
androidManifest.writeAsStringSync(
'''
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.$moduleName">
<dist:module
dist:instant="false"
dist:title="@string/${moduleName}Name">
<dist:delivery>
<dist:on-demand />
</dist:delivery>
<dist:fusing dist:include="true" />
</dist:module>
</manifest>
''', flush: true);
// settings.gradle add ':module'
File settingsGradle = androidDir.childFile('settings.gradle');
File settingsGradleTemp = androidDir.childFile('settings.gradle.temp');
if (settingsGradleTemp.existsSync()) settingsGradleTemp.deleteSync();
List<String> lines = settingsGradle.readAsLinesSync();
for (String line in lines) {
if (line.length >= 7 && line.substring(0, 7) == 'include') {
List<String> elements = line.substring(7).split(', ');
bool moduleFound = false;
for (int i = 1; i < elements.length; i++) {
if (elements[i] == '\':$moduleName\'') {
moduleFound = true;
break;
}
}
if (!moduleFound) {
line += ', \':$moduleName\'';
}
}
settingsGradleTemp.writeAsStringSync('$line\n', mode: FileMode.append, flush: true);
}
settingsGradleTemp.copySync(settingsGradle.path);
settingsGradleTemp.deleteSync();
// app/build.gradle add dynamicFeatures = [':modules', ...]
File appBuildGradle = androidDir.childDirectory('app').childFile('build.gradle');
File appBuildGradleTemp = androidDir.childDirectory('app').childFile('build.gradle.temp');
if (appBuildGradleTemp.existsSync()) appBuildGradleTemp.deleteSync();
lines = appBuildGradle.readAsLinesSync();
bool inAndroidBlock = false;
int androidStartLineIndex = 0;
int androidEndLineIndex = 0;
for (int lineNum = 0; lineNum < lines.length; lineNum++) {
String line = lines[lineNum];
if (line.contains('android') && line.contains('{')) {
inAndroidBlock = true;
androidStartLineIndex = lineNum;
} else if (inAndroidBlock && line.length > 0 && line.substring(0, 1) == '}') {
inAndroidBlock = false;
androidEndLineIndex = lineNum;
appBuildGradleTemp.writeAsStringSync(' dynamicFeatures = [\':$moduleName\']\n', mode: FileMode.append, flush: true);
}
if (inAndroidBlock) {
if (line.contains('dynamicFeatures = [')) {
List<String> components = line.substring(line.lastIndexOf('dynamicFeatures = [\'') + 20, line.length - 2).split(', ');
if (!components.contains(':$moduleName')) {
components.add(':$moduleName');
}
line = ' dynamicFeatures = [\'${components.first}\'';
components.removeAt(0);
for (String component in components) {
line += ', \'$component\'';
}
line += ']';
inAndroidBlock = false;
}
}
appBuildGradleTemp.writeAsStringSync('$line\n', mode: FileMode.append, flush: true);
}
appBuildGradleTemp.copySync(appBuildGradle.path);
appBuildGradleTemp.deleteSync();
File moduleBuildGradle = moduleDir.childFile('build.gradle');
moduleBuildGradle.createSync(recursive: true);
moduleBuildGradle.writeAsStringSync(
'''
apply plugin: "com.android.dynamic-feature"
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation project(":app")
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support:support-annotations:28.0.0'
}
''', flush: true);
}
void createLoadingUnitGolden(List<String> loadingUnits, Directory androidDir) {
File goldenFile = androidDir.childFile('loading_unit_golden.yaml');
goldenFile.createSync(recursive: true);
String goldenContents =
'''
# The contents of this file are generated via the prebuild step of
# Flutter split AOT bundle building. It is not recommended to edit
# this manually.
#
# This file contains a list of loading units/dylibs that were
# produced by gen_snapshot during the prebuild/setup step of
# building split AOT apps with split features. During the split
# AOT build phase, we will verify the dylibs produced via
# gen_snapshot matches this file. If not, the prebuild step
# should be rerun and configuration verified.
''';
for (String loadingUnit in loadingUnits) {
goldenContents += '- $loadingUnit\n';
}
goldenFile.writeAsStringSync(goldenContents, flush: true);
}
void compareAgainstGolden(List<String> loadingUnits, Directory androidDir) {
File goldenFile = androidDir.childFile('loading_unit_golden.yaml');
if (!goldenFile.existsSync()) {
print('No loading_unit_golden.yaml found, setting this run\'s output as initial golden');
return;
}
var yamlData = loadYaml(goldenFile.readAsStringSync());
List<String> goldenLoadingUnits = List<String>.from(yamlData.cast<String>());
List<String> newLoadingUnits = [];
for (String loadingUnit in loadingUnits) {
if (goldenLoadingUnits.contains(loadingUnit)) {
goldenLoadingUnits.remove(loadingUnit);
} else {
newLoadingUnits.add(loadingUnit);
}
}
if (newLoadingUnits.isNotEmpty) {
print('New loading units were found: $newLoadingUnits');
}
if (goldenLoadingUnits.isNotEmpty) {
print('Previously existing loading units no longer exist: $goldenLoadingUnits');
}
if (newLoadingUnits.isEmpty && goldenLoadingUnits.isEmpty) {
print('No change in generated loading units.');
} else {
// verify with user if they want to set this configuration as new golden.
}
}
void createBundleConfig(List<String> loadingUnits, Environment env) {
File configFile = env.projectDir.childFile('bundle_config.yaml');
if (configFile.existsSync()) {
return;
}
configFile.createSync(recursive: true);
String configContents =
'''
# Defines the feature modules, which dart libraries and assets
# belong in each feature. A dart library must only be included
# in one feature. Any dart library not specified will be included
# in the base module. Assets included in more than one feature
# module will be replicated in each module.
#
# Example:
#
# - name: feature1
# libraries:
# - dartlib1
# - dartlibExtra
# assets:
# - uri/to/assets/*
# - assets/feature1/*
# - assets/file_extra_1.png
# - name: feature2
# libraries:
# - dartlib2
# assets:
# - assets/feature2/*
# - assets/file_extra_2.png
#
# The default configuration will bundle all splits into the
# base module. To create split dynamic modules, a valid config
# should be specified and the setup script should be re-run.
#
# Available dart libraries:
#
''';
for (String loadingUnit in loadingUnits) {
configContents +=
'''
# $loadingUnit
''';
}
configContents +=
'''
#
''';
configFile.writeAsStringSync(configContents, flush: true);
print("Created bundle_config.yaml. This file should be modified to specify desired splits, and the setup command should be re-run.");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment