Created
May 27, 2022 03:05
-
-
Save soloxiao/b67750c1447689796f6bad4b14bbb313 to your computer and use it in GitHub Desktop.
Beike_AspectD Flutter 3.0.0 flutter_tools patch gist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @dart = 2.8 | |
import 'package:flutter_tools/src/aop/hook_factory.dart'; | |
import 'package:flutter_tools/src/dart/package_map.dart'; | |
import 'package:package_config/package_config.dart'; | |
import 'package:yaml/yaml.dart'; | |
import 'package:file/file.dart' as f; | |
import '../globals.dart' as globals; | |
import '../artifacts.dart'; | |
import '../build_info.dart'; | |
import '../globals.dart'; | |
/// 创建时间:2020-03-28 | |
/// 作者:liujingguang | |
/// 描述:AopManager管理类 | |
/// | |
/// aop_config.yaml文件配置需要处理app.dill文件的工程, 在根项目下进行配置. | |
/// project_name为项目名称 | |
/// exec_path为生成的snapshot的路径 | |
/// | |
/// 配置样例如下: | |
/// | |
/// flutter_tools_hook: | |
/// - project_name: 'beike_aspectd' | |
/// exec_path: 'bin/starter.snapshot' | |
/// | |
/// - project_name: 'flutter_aop_data_parse' | |
/// exec_path: 'bin/starter.snapshot' | |
class AopManager { | |
static const String sYamlConfigName = 'aop_config.yaml'; | |
static const String key_flutter_tools_hook = 'flutter_tools_hook'; | |
static const String key_project_name = 'project_name'; | |
static const String key_exec_path = 'exec_path'; | |
static Future<void> hookBuildBundleCommand( | |
String productDirPath, | |
BuildMode buildMode, | |
) async { | |
await _handleHook(productDirPath, buildMode, CommandType4Aop.Bundle); | |
} | |
static Future<void> hookBuildAotCommand( | |
String productDirPath, | |
BuildMode buildMode, | |
) async { | |
await _handleHook(productDirPath, buildMode, CommandType4Aop.Aot); | |
} | |
static Future<void> hookSnapshotCommand( | |
String productDirPath, | |
BuildMode buildMode, | |
) async { | |
await _handleHook(productDirPath, buildMode, CommandType4Aop.Snapshot); | |
} | |
static Future<List<String>> aopArgs(BuildMode buildMode) async { | |
final String dart_path = globals.artifacts | |
.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk); | |
final List proceduresList = await _aopProcedures(); | |
final List<String> args = [ | |
'--build-mode', | |
buildMode.name, | |
'--dart-path', | |
dart_path | |
]; | |
if (proceduresList != null && proceduresList.isNotEmpty) { | |
final String produces = proceduresList.join(','); | |
args.add('--aop-packages'); | |
args.add(produces); | |
} | |
return args; | |
} | |
static Future<List<String>> _aopProcedures() async { | |
List<String> procedures = List<String>(); | |
final String configYamlPath = | |
fs.path.join(fs.currentDirectory.path, sYamlConfigName); | |
if (fs.file(configYamlPath).existsSync()) { | |
final dynamic yamlInfo = | |
loadYaml(fs.file(configYamlPath).readAsStringSync()); | |
if (yamlInfo == null) { | |
return null; | |
} | |
if (yamlInfo[key_flutter_tools_hook] is! YamlList) { | |
return null; | |
} | |
final YamlList yamlNodes = yamlInfo[key_flutter_tools_hook] as YamlList; | |
for (dynamic v in yamlNodes) { | |
if (v == null) { | |
continue; | |
} | |
final String projectName = v[key_project_name] as String; | |
final String execPath = v[key_exec_path] as String; | |
if (projectName == null || execPath == null) { | |
continue; | |
} | |
final String packagePath = await _findAopPackagePath(projectName); | |
if (packagePath == null) { | |
continue; | |
} | |
procedures.add(fs.path.join(packagePath, execPath)); | |
} | |
} | |
return procedures; | |
} | |
static Future<void> _handleHook( | |
String productDirPath, BuildMode buildMode, CommandType4Aop type) async { | |
// return Future.value(); | |
try { | |
final String configYamlPath = | |
fs.path.join(fs.currentDirectory.path, sYamlConfigName); | |
print(configYamlPath); | |
if (fs.file(configYamlPath).existsSync()) { | |
final dynamic yamlInfo = | |
loadYaml(fs.file(configYamlPath).readAsStringSync()); | |
if (yamlInfo == null) { | |
return; | |
} | |
if (yamlInfo[key_flutter_tools_hook] is! YamlList) { | |
return; | |
} | |
final YamlList yamlNodes = yamlInfo[key_flutter_tools_hook] as YamlList; | |
for (dynamic v in yamlNodes) { | |
if (v == null) { | |
return; | |
} | |
final String projectName = v[key_project_name] as String; | |
final String execPath = v[key_exec_path] as String; | |
if (projectName == null || execPath == null) { | |
return; | |
} | |
final String packagePath = await _findAopPackagePath(projectName); | |
if (packagePath == null) { | |
return; | |
} | |
await HookFactory.hook(productDirPath, | |
fs.path.join(packagePath, execPath), buildMode, type); | |
} | |
} | |
// ignore: avoid_catches_without_on_clauses | |
} catch (e) { | |
printTrace('error in _handleHook of $type : ${e.toString()}'); | |
} | |
} | |
/// 获取项目中引用flutter_aop_data_parse的路径 | |
static Future<String> _findAopPackagePath(String projectName) async { | |
Map<String, dynamic> packages; | |
try { | |
final String packagesFilePath = | |
fs.path.join(fs.currentDirectory.path, '.packages'); | |
final f.File packageFile = fs.file(packagesFilePath); | |
final PackageConfig packageConfig = | |
await loadPackageConfigWithLogging(packageFile); | |
packages = PackageConfig.toJson(packageConfig); | |
for (Package package in packageConfig.packages) { | |
if (package.name == projectName) { | |
final Uri uri = package.packageUriRoot; | |
final String uriString = uri.path; | |
return uriString; | |
} | |
} | |
// ignore: avoid_catches_without_on_clauses | |
} catch (e) { | |
printTrace('Invalid .packages file: $e'); | |
return null; | |
} | |
return null; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2018 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. | |
// @dart = 2.8 | |
import 'dart:io'; | |
import 'package:crypto/crypto.dart'; | |
import 'package:yaml/yaml.dart'; | |
import 'package:package_config/package_config.dart'; | |
import '../artifacts.dart'; | |
import '../base/common.dart'; | |
import '../build_info.dart'; | |
import '../base/file_system.dart'; | |
import '../build_system/build_system.dart'; | |
import '../build_system/targets/common.dart'; | |
import '../cache.dart'; | |
import '../compile.dart'; | |
import '../dart/package_map.dart'; | |
import '../globals.dart' as globals; | |
const String aspectdImplPackageRelPath = '..'; | |
const String frontendServerDartSnapshot = 'frontend_server.dart.snapshot'; | |
const String sYamlConfigName = 'aop_config.yaml'; | |
const String key_flutter_tools_hook = 'flutter_tools_hook'; | |
const String key_project_name = 'project_name'; | |
const String key_exec_path = 'exec_path'; | |
const String beike_aspectd = 'beike_aspectd'; | |
const String inner_path = 'inner'; | |
const String globalPackagesPath = '.packages'; | |
class AspectdHook { | |
static Future<Directory> getPackagePathFromConfig( | |
String packageConfigPath, String packageName) async { | |
final PackageConfig packageConfig = await loadPackageConfigWithLogging( | |
globals.fs.file(packageConfigPath), | |
logger: globals.logger, | |
); | |
if ((packageConfig?.packages?.length ?? 0) > 0) { | |
final Package aspectdPackage = packageConfig.packages.toList().firstWhere( | |
(Package element) => element.name == packageName, | |
orElse: () => null); | |
if (aspectdPackage == null) { | |
return null; | |
} | |
return globals.fs.directory(aspectdPackage.root.toFilePath()); | |
} | |
return null; | |
} | |
static Future<Directory> getFlutterFrontendServerDirectory( | |
String packagesPath) async { | |
final Directory directory = | |
await getPackagePathFromConfig(packagesPath, beike_aspectd); | |
if (directory == null) { | |
return null; | |
} | |
return globals.fs.directory(globals.fs.path | |
.join(directory.absolute.path, inner_path, 'flutter_frontend_server')); | |
} | |
static bool configFileExists() { | |
final String configYamlPath = | |
globals.fs.path.join(globals.fs.currentDirectory.path, sYamlConfigName); | |
if (globals.fs.file(configYamlPath).existsSync()) { | |
final dynamic yamlInfo = | |
loadYaml(globals.fs.file(configYamlPath).readAsStringSync()); | |
if (yamlInfo == null) { | |
return false; | |
} | |
if (yamlInfo[key_flutter_tools_hook] is! YamlList) { | |
return false; | |
} | |
final YamlList yamlNodes = yamlInfo[key_flutter_tools_hook] as YamlList; | |
if (yamlNodes.nodes.isNotEmpty) { | |
return true; | |
} | |
} | |
return false; | |
} | |
static Directory getAspectdDirectory(Directory rootProjectDir) { | |
return globals.fs.directory(globals.fs.path.normalize(globals.fs.path | |
.join(rootProjectDir.path, aspectdImplPackageRelPath, beike_aspectd))); | |
} | |
static Future<void> enableAspectd() async { | |
final Directory currentDirectory = globals.fs.currentDirectory; | |
final String packagesPath = globals.fs.path | |
.join(currentDirectory.absolute.path, globalPackagesPath); | |
final Directory beikeAspectdDirectory = | |
await getPackagePathFromConfig(packagesPath, beike_aspectd); | |
final Directory flutterFrontendServerDirectory = | |
await getFlutterFrontendServerDirectory(packagesPath); | |
if (beikeAspectdDirectory == null || | |
flutterFrontendServerDirectory == null) { | |
return; | |
} | |
final String aspectdPackagesPath = globals.fs.path.join( | |
beikeAspectdDirectory.absolute.path, inner_path, globalPackagesPath); | |
await checkAspectdFlutterFrontendServerSnapshot( | |
aspectdPackagesPath, flutterFrontendServerDirectory); | |
} | |
static Future<void> checkAspectdFlutterFrontendServerSnapshot( | |
String packagesPath, Directory flutterFrontendServerDirectory) async { | |
final String aspectdFlutterFrontendServerSnapshot = globals.fs.path.join( | |
flutterFrontendServerDirectory.absolute.path, | |
frontendServerDartSnapshot); | |
final String defaultFlutterFrontendServerSnapshot = globals.artifacts | |
.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk); | |
final File defaultServerFile = | |
globals.fs.file(defaultFlutterFrontendServerSnapshot); | |
final File aspectdServerFile = | |
globals.fs.file(aspectdFlutterFrontendServerSnapshot); | |
if (defaultServerFile.existsSync()) { | |
if (md5.convert(defaultServerFile.readAsBytesSync()) == | |
md5.convert(aspectdServerFile.readAsBytesSync())) { | |
return true; | |
} | |
globals.fs.file(defaultFlutterFrontendServerSnapshot).deleteSync(); | |
} | |
aspectdServerFile.copySync(defaultFlutterFrontendServerSnapshot); | |
} | |
static Future<String> getDartSdkDependency(String aspectdDir) async { | |
final ProcessResult processResult = await globals.processManager.run( | |
<String>[ | |
globals.fs.path.join( | |
globals.artifacts.getArtifactPath( | |
Artifact.frontendServerSnapshotForEngineDartSdk), | |
'bin', | |
'pub'), | |
'get', | |
'--verbosity=warning' | |
], | |
workingDirectory: aspectdDir, | |
environment: <String, String>{'FLUTTER_ROOT': Cache.flutterRoot}); | |
if (processResult.exitCode != 0) { | |
throwToolExit( | |
'Aspectd unexpected error: ${processResult.stderr.toString()}'); | |
} | |
final Directory kernelDir = await getPackagePathFromConfig( | |
globals.fs.path.join(aspectdDir, globalPackagesPath), 'kernel'); | |
return kernelDir.parent.parent.path; | |
} | |
Future<void> runBuildDillCommand(Environment environment) async { | |
print('aop front end compiling'); | |
final Directory mainDirectory = globals.fs.currentDirectory; | |
String relativeDir = environment.outputDir.absolute.path | |
.substring(environment.projectDir.absolute.path.length + 1); | |
final String outputDir = | |
globals.fs.path.join(mainDirectory.path, relativeDir); | |
final String buildDir = | |
globals.fs.path.join(mainDirectory.path, '.dart_tool', 'flutter_build'); | |
final Map<String, String> defines = environment.defines; | |
relativeDir = defines[kTargetFile] | |
.substring(environment.projectDir.absolute.path.length + 1); | |
String targetFile = environment.defines[kTargetFile]; | |
targetFile ??= globals.fs.path.join(mainDirectory.path, 'lib', 'main.dart'); | |
defines[kTargetFile] = targetFile; | |
final Environment auxEnvironment = Environment( | |
projectDir: mainDirectory, | |
outputDir: globals.fs.directory(outputDir), | |
cacheDir: environment.cacheDir, | |
flutterRootDir: environment.flutterRootDir, | |
fileSystem: environment.fileSystem, | |
logger: environment.logger, | |
artifacts: environment.artifacts, | |
processManager: environment.processManager, | |
engineVersion: environment.engineVersion, | |
buildDir: globals.fs.directory(buildDir), | |
defines: defines, | |
inputs: environment.inputs); | |
const KernelSnapshot auxKernelSnapshot = KernelSnapshot(); | |
final CompilerOutput compilerOutput = | |
await auxKernelSnapshot.buildImpl(auxEnvironment); | |
final String aspectdDill = compilerOutput.outputFilename; | |
print('Aspectdill path : ' + aspectdDill); | |
final File originalDillFile = globals.fs.file( | |
globals.fs.path.join(environment.buildDir.absolute.path, 'app.dill')); | |
print('originalDillFile path : ' + originalDillFile.path); | |
if (originalDillFile.existsSync()) { | |
await originalDillFile.copy(originalDillFile.absolute.path + '.bak'); | |
} | |
globals.fs.file(aspectdDill).copySync(originalDillFile.absolute.path); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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. | |
// @dart = 2.8 | |
import '../base/common.dart'; | |
import '../build_info.dart'; | |
import '../bundle.dart'; | |
import '../bundle_builder.dart'; | |
import '../features.dart'; | |
import '../globals.dart' as globals; | |
import '../project.dart'; | |
import '../reporting/reporting.dart'; | |
import '../runner/flutter_command.dart'; | |
import 'build.dart'; | |
import '../aop/aspectd.dart'; | |
class BuildBundleCommand extends BuildSubCommand { | |
BuildBundleCommand({ | |
bool verboseHelp = false, | |
this.bundleBuilder, | |
}) : super(verboseHelp: verboseHelp) { | |
usesTargetOption(); | |
usesFilesystemOptions(hide: !verboseHelp); | |
usesBuildNumberOption(); | |
addBuildModeFlags(verboseHelp: verboseHelp, defaultToRelease: false); | |
usesDartDefineOption(); | |
usesExtraDartFlagOptions(verboseHelp: verboseHelp); | |
argParser | |
..addOption('depfile', | |
defaultsTo: defaultDepfilePath, | |
help: 'A file path where a depfile will be written. ' | |
'This contains all build inputs and outputs in a Make-style syntax.') | |
..addOption( | |
'target-platform', | |
defaultsTo: 'android-arm', | |
allowed: const <String>[ | |
'android-arm', | |
'android-arm64', | |
'android-x86', | |
'android-x64', | |
'ios', | |
'darwin', | |
'linux-x64', | |
'linux-arm64', | |
'windows-x64', | |
], | |
help: 'The architecture for which to build the application.', | |
) | |
..addOption( | |
'asset-dir', | |
defaultsTo: getAssetBuildDirectory(), | |
help: | |
'The output directory for the kernel_blob.bin file, the native snapshot, the assets, etc. ' | |
'Can be used to redirect the output when driving the Flutter toolchain from another build system.', | |
) | |
..addFlag( | |
'tree-shake-icons', | |
negatable: true, | |
defaultsTo: false, | |
hide: !verboseHelp, | |
help: | |
'(deprecated) Icon font tree shaking is not supported by this command.', | |
); | |
usesPubOption(); | |
usesTrackWidgetCreation(verboseHelp: verboseHelp); | |
bundleBuilder ??= BundleBuilder(); | |
} | |
BundleBuilder bundleBuilder; | |
@override | |
final String name = 'bundle'; | |
@override | |
final String description = | |
'Build the Flutter assets directory from your app.'; | |
@override | |
final String usageFooter = 'The Flutter assets directory contains your ' | |
'application code and resources; they are used by some Flutter Android and' | |
' iOS runtimes.'; | |
@override | |
Future<CustomDimensions> get usageValues async { | |
final String projectDir = globals.fs.file(targetFile).parent.parent.path; | |
final FlutterProject flutterProject = | |
FlutterProject.fromDirectory(globals.fs.directory(projectDir)); | |
if (flutterProject == null) { | |
return const CustomDimensions(); | |
} | |
return CustomDimensions( | |
commandBuildBundleTargetPlatform: stringArg('target-platform'), | |
commandBuildBundleIsModule: flutterProject.isModule, | |
); | |
} | |
@override | |
Future<void> validateCommand() async { | |
if (argResults['tree-shake-icons'] as bool) { | |
throwToolExit( | |
'The "--tree-shake-icons" flag is deprecated for "build bundle" and will be removed in a future version of Flutter.'); | |
} | |
return super.validateCommand(); | |
} | |
@override | |
Future<FlutterCommandResult> runCommand() async { | |
await AspectdHook.enableAspectd(); | |
final String targetPlatform = stringArg('target-platform'); | |
final TargetPlatform platform = getTargetPlatformForName(targetPlatform); | |
if (platform == null) { | |
throwToolExit('Unknown platform: $targetPlatform'); | |
} | |
// Check for target platforms that are only allowed via feature flags. | |
switch (platform) { | |
case TargetPlatform.darwin: | |
if (!featureFlags.isMacOSEnabled) { | |
throwToolExit('macOS is not a supported target platform.'); | |
} | |
break; | |
case TargetPlatform.windows_x64: | |
case TargetPlatform.windows_uwp_x64: | |
if (!featureFlags.isWindowsEnabled) { | |
throwToolExit('Windows is not a supported target platform.'); | |
} | |
break; | |
case TargetPlatform.linux_x64: | |
case TargetPlatform.linux_arm64: | |
if (!featureFlags.isLinuxEnabled) { | |
throwToolExit('Linux is not a supported target platform.'); | |
} | |
break; | |
case TargetPlatform.android: | |
case TargetPlatform.android_arm: | |
case TargetPlatform.android_arm64: | |
case TargetPlatform.android_x64: | |
case TargetPlatform.android_x86: | |
case TargetPlatform.fuchsia_arm64: | |
case TargetPlatform.fuchsia_x64: | |
case TargetPlatform.ios: | |
case TargetPlatform.tester: | |
case TargetPlatform.web_javascript: | |
break; | |
} | |
final BuildInfo buildInfo = await getBuildInfo(); | |
displayNullSafetyMode(buildInfo); | |
await bundleBuilder.build( | |
platform: platform, | |
buildInfo: buildInfo, | |
mainPath: targetFile, | |
depfilePath: stringArg('depfile'), | |
assetDirPath: stringArg('asset-dir'), | |
); | |
return FlutterCommandResult.success(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 'package:package_config/package_config.dart'; | |
import '../../aop/aspectd.dart'; | |
import '../../artifacts.dart'; | |
import '../../base/build.dart'; | |
import '../../base/file_system.dart'; | |
import '../../build_info.dart'; | |
import '../../compile.dart'; | |
import '../../dart/package_map.dart'; | |
import '../../globals.dart' as globals show xcode; | |
import '../build_system.dart'; | |
import '../depfile.dart'; | |
import '../exceptions.dart'; | |
import 'assets.dart'; | |
import 'dart_plugin_registrant.dart'; | |
import 'icon_tree_shaker.dart'; | |
import 'localizations.dart'; | |
/// Copies the pre-built flutter bundle. | |
// This is a one-off rule for implementing build bundle in terms of assemble. | |
class CopyFlutterBundle extends Target { | |
const CopyFlutterBundle(); | |
@override | |
String get name => 'copy_flutter_bundle'; | |
@override | |
List<Source> get inputs => const <Source>[ | |
Source.artifact(Artifact.vmSnapshotData, mode: BuildMode.debug), | |
Source.artifact(Artifact.isolateSnapshotData, mode: BuildMode.debug), | |
Source.pattern('{BUILD_DIR}/app.dill'), | |
...IconTreeShaker.inputs, | |
]; | |
@override | |
List<Source> get outputs => const <Source>[ | |
Source.pattern('{OUTPUT_DIR}/vm_snapshot_data'), | |
Source.pattern('{OUTPUT_DIR}/isolate_snapshot_data'), | |
Source.pattern('{OUTPUT_DIR}/kernel_blob.bin'), | |
]; | |
@override | |
List<String> get depfiles => <String>['flutter_assets.d']; | |
@override | |
Future<void> build(Environment environment) async { | |
final String? buildModeEnvironment = environment.defines[kBuildMode]; | |
if (buildModeEnvironment == null) { | |
throw MissingDefineException(kBuildMode, 'copy_flutter_bundle'); | |
} | |
final BuildMode buildMode = getBuildModeForName(buildModeEnvironment); | |
environment.outputDir.createSync(recursive: true); | |
// Only copy the prebuilt runtimes and kernel blob in debug mode. | |
if (buildMode == BuildMode.debug) { | |
final String vmSnapshotData = environment.artifacts | |
.getArtifactPath(Artifact.vmSnapshotData, mode: BuildMode.debug); | |
final String isolateSnapshotData = environment.artifacts | |
.getArtifactPath(Artifact.isolateSnapshotData, mode: BuildMode.debug); | |
environment.buildDir | |
.childFile('app.dill') | |
.copySync(environment.outputDir.childFile('kernel_blob.bin').path); | |
environment.fileSystem | |
.file(vmSnapshotData) | |
.copySync(environment.outputDir.childFile('vm_snapshot_data').path); | |
environment.fileSystem.file(isolateSnapshotData).copySync( | |
environment.outputDir.childFile('isolate_snapshot_data').path); | |
} | |
final Depfile assetDepfile = await copyAssets( | |
environment, | |
environment.outputDir, | |
targetPlatform: TargetPlatform.android, | |
buildMode: buildMode, | |
); | |
final DepfileService depfileService = DepfileService( | |
fileSystem: environment.fileSystem, | |
logger: environment.logger, | |
); | |
depfileService.writeToFile( | |
assetDepfile, | |
environment.buildDir.childFile('flutter_assets.d'), | |
); | |
} | |
@override | |
List<Target> get dependencies => const <Target>[ | |
KernelSnapshot(), | |
]; | |
} | |
/// Copies the pre-built flutter bundle for release mode. | |
class ReleaseCopyFlutterBundle extends CopyFlutterBundle { | |
const ReleaseCopyFlutterBundle(); | |
@override | |
String get name => 'release_flutter_bundle'; | |
@override | |
List<Source> get inputs => const <Source>[]; | |
@override | |
List<Source> get outputs => const <Source>[]; | |
@override | |
List<String> get depfiles => const <String>[ | |
'flutter_assets.d', | |
]; | |
@override | |
List<Target> get dependencies => const <Target>[]; | |
} | |
/// Generate a snapshot of the dart code used in the program. | |
/// | |
/// Note that this target depends on the `.dart_tool/package_config.json` file | |
/// even though it is not listed as an input. Pub inserts a timestamp into | |
/// the file which causes unnecessary rebuilds, so instead a subset of the contents | |
/// are used an input instead. | |
class KernelSnapshot extends Target { | |
const KernelSnapshot(); | |
@override | |
String get name => 'kernel_snapshot'; | |
@override | |
List<Source> get inputs => const <Source>[ | |
Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'), | |
Source.pattern( | |
'{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/common.dart'), | |
Source.artifact(Artifact.platformKernelDill), | |
Source.hostArtifact(HostArtifact.engineDartBinary), | |
Source.artifact(Artifact.frontendServerSnapshotForEngineDartSdk), | |
]; | |
@override | |
List<Source> get outputs => const <Source>[]; | |
@override | |
List<String> get depfiles => <String>[ | |
'kernel_snapshot.d', | |
]; | |
@override | |
List<Target> get dependencies => const <Target>[ | |
GenerateLocalizationsTarget(), | |
DartPluginRegistrantTarget(), | |
]; | |
@override | |
Future<void> build(Environment environment) async { | |
await AspectdHook.enableAspectd(); | |
await buildImpl(environment); | |
} | |
Future<CompilerOutput> buildImpl(Environment environment) async { | |
final KernelCompiler compiler = KernelCompiler( | |
fileSystem: environment.fileSystem, | |
logger: environment.logger, | |
processManager: environment.processManager, | |
artifacts: environment.artifacts, | |
fileSystemRoots: <String>[], | |
); | |
final String? buildModeEnvironment = environment.defines[kBuildMode]; | |
if (buildModeEnvironment == null) { | |
throw MissingDefineException(kBuildMode, 'kernel_snapshot'); | |
} | |
final String? targetPlatformEnvironment = | |
environment.defines[kTargetPlatform]; | |
if (targetPlatformEnvironment == null) { | |
throw MissingDefineException(kTargetPlatform, 'kernel_snapshot'); | |
} | |
final BuildMode buildMode = getBuildModeForName(buildModeEnvironment); | |
final String targetFile = environment.defines[kTargetFile] ?? | |
environment.fileSystem.path.join('lib', 'main.dart'); | |
final File packagesFile = environment.projectDir | |
.childDirectory('.dart_tool') | |
.childFile('package_config.json'); | |
final String targetFileAbsolute = | |
environment.fileSystem.file(targetFile).absolute.path; | |
// everything besides 'false' is considered to be enabled. | |
final bool trackWidgetCreation = | |
environment.defines[kTrackWidgetCreation] != 'false'; | |
final TargetPlatform targetPlatform = | |
getTargetPlatformForName(targetPlatformEnvironment); | |
// This configuration is all optional. | |
final List<String> extraFrontEndOptions = | |
decodeCommaSeparated(environment.defines, kExtraFrontEndOptions); | |
final List<String>? fileSystemRoots = | |
environment.defines[kFileSystemRoots]?.split(','); | |
final String? fileSystemScheme = environment.defines[kFileSystemScheme]; | |
TargetModel targetModel = TargetModel.flutter; | |
if (targetPlatform == TargetPlatform.fuchsia_x64 || | |
targetPlatform == TargetPlatform.fuchsia_arm64) { | |
targetModel = TargetModel.flutterRunner; | |
} | |
// Force linking of the platform for desktop embedder targets since these | |
// do not correctly load the core snapshots in debug mode. | |
// See https://github.com/flutter/flutter/issues/44724 | |
bool forceLinkPlatform; | |
switch (targetPlatform) { | |
case TargetPlatform.darwin: | |
case TargetPlatform.windows_x64: | |
case TargetPlatform.linux_x64: | |
forceLinkPlatform = true; | |
break; | |
case TargetPlatform.android: | |
case TargetPlatform.android_arm: | |
case TargetPlatform.android_arm64: | |
case TargetPlatform.android_x64: | |
case TargetPlatform.android_x86: | |
case TargetPlatform.fuchsia_arm64: | |
case TargetPlatform.fuchsia_x64: | |
case TargetPlatform.ios: | |
case TargetPlatform.linux_arm64: | |
case TargetPlatform.tester: | |
case TargetPlatform.web_javascript: | |
case TargetPlatform.windows_uwp_x64: | |
forceLinkPlatform = false; | |
break; | |
} | |
final PackageConfig packageConfig = await loadPackageConfigWithLogging( | |
packagesFile, | |
logger: environment.logger, | |
); | |
final CompilerOutput? output = await compiler.compile( | |
sdkRoot: environment.artifacts.getArtifactPath( | |
Artifact.flutterPatchedSdkPath, | |
platform: targetPlatform, | |
mode: buildMode, | |
), | |
aot: buildMode.isPrecompiled, | |
buildMode: buildMode, | |
trackWidgetCreation: trackWidgetCreation && buildMode == BuildMode.debug, | |
targetModel: targetModel, | |
outputFilePath: environment.buildDir.childFile('app.dill').path, | |
packagesPath: packagesFile.path, | |
linkPlatformKernelIn: forceLinkPlatform || buildMode.isPrecompiled, | |
mainPath: targetFileAbsolute, | |
depFilePath: environment.buildDir.childFile('kernel_snapshot.d').path, | |
extraFrontEndOptions: extraFrontEndOptions, | |
fileSystemRoots: fileSystemRoots, | |
fileSystemScheme: fileSystemScheme, | |
dartDefines: decodeDartDefines(environment.defines, kDartDefines), | |
packageConfig: packageConfig, | |
buildDir: environment.buildDir, | |
checkDartPluginRegistry: environment.generateDartPluginRegistry, | |
); | |
if (output == null || output.errorCount != 0) { | |
throw Exception(); | |
} | |
return output; | |
} | |
} | |
/// Supports compiling a dart kernel file to an ELF binary. | |
abstract class AotElfBase extends Target { | |
const AotElfBase(); | |
@override | |
String get analyticsName => 'android_aot'; | |
@override | |
Future<void> build(Environment environment) async { | |
final AOTSnapshotter snapshotter = AOTSnapshotter( | |
fileSystem: environment.fileSystem, | |
logger: environment.logger, | |
xcode: globals.xcode!, | |
processManager: environment.processManager, | |
artifacts: environment.artifacts, | |
); | |
final String outputPath = environment.buildDir.path; | |
final String? buildModeEnvironment = environment.defines[kBuildMode]; | |
if (buildModeEnvironment == null) { | |
throw MissingDefineException(kBuildMode, 'aot_elf'); | |
} | |
final String? targetPlatformEnvironment = | |
environment.defines[kTargetPlatform]; | |
if (targetPlatformEnvironment == null) { | |
throw MissingDefineException(kTargetPlatform, 'aot_elf'); | |
} | |
final List<String> extraGenSnapshotOptions = | |
decodeCommaSeparated(environment.defines, kExtraGenSnapshotOptions); | |
final BuildMode buildMode = getBuildModeForName(buildModeEnvironment); | |
final TargetPlatform targetPlatform = | |
getTargetPlatformForName(targetPlatformEnvironment); | |
final String? splitDebugInfo = environment.defines[kSplitDebugInfo]; | |
final bool dartObfuscation = | |
environment.defines[kDartObfuscation] == 'true'; | |
final String? codeSizeDirectory = environment.defines[kCodeSizeDirectory]; | |
if (codeSizeDirectory != null) { | |
final File codeSizeFile = environment.fileSystem | |
.directory(codeSizeDirectory) | |
.childFile('snapshot.${environment.defines[kTargetPlatform]}.json'); | |
final File precompilerTraceFile = environment.fileSystem | |
.directory(codeSizeDirectory) | |
.childFile('trace.${environment.defines[kTargetPlatform]}.json'); | |
extraGenSnapshotOptions | |
.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}'); | |
extraGenSnapshotOptions | |
.add('--trace-precompiler-to=${precompilerTraceFile.path}'); | |
} | |
final int snapshotExitCode = await snapshotter.build( | |
platform: targetPlatform, | |
buildMode: buildMode, | |
mainPath: environment.buildDir.childFile('app.dill').path, | |
outputPath: outputPath, | |
bitcode: false, | |
extraGenSnapshotOptions: extraGenSnapshotOptions, | |
splitDebugInfo: splitDebugInfo, | |
dartObfuscation: dartObfuscation, | |
); | |
if (snapshotExitCode != 0) { | |
throw Exception('AOT snapshotter exited with code $snapshotExitCode'); | |
} | |
} | |
} | |
/// Generate an ELF binary from a dart kernel file in profile mode. | |
class AotElfProfile extends AotElfBase { | |
const AotElfProfile(this.targetPlatform); | |
@override | |
String get name => 'aot_elf_profile'; | |
@override | |
List<Source> get inputs => <Source>[ | |
const Source.pattern( | |
'{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/common.dart'), | |
const Source.pattern('{BUILD_DIR}/app.dill'), | |
const Source.hostArtifact(HostArtifact.engineDartBinary), | |
const Source.artifact(Artifact.skyEnginePath), | |
Source.artifact( | |
Artifact.genSnapshot, | |
platform: targetPlatform, | |
mode: BuildMode.profile, | |
), | |
]; | |
@override | |
List<Source> get outputs => const <Source>[ | |
Source.pattern('{BUILD_DIR}/app.so'), | |
]; | |
@override | |
List<Target> get dependencies => const <Target>[ | |
KernelSnapshot(), | |
]; | |
final TargetPlatform targetPlatform; | |
} | |
/// Generate an ELF binary from a dart kernel file in release mode. | |
class AotElfRelease extends AotElfBase { | |
const AotElfRelease(this.targetPlatform); | |
@override | |
String get name => 'aot_elf_release'; | |
@override | |
List<Source> get inputs => <Source>[ | |
const Source.pattern( | |
'{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/common.dart'), | |
const Source.pattern('{BUILD_DIR}/app.dill'), | |
const Source.hostArtifact(HostArtifact.engineDartBinary), | |
const Source.artifact(Artifact.skyEnginePath), | |
Source.artifact( | |
Artifact.genSnapshot, | |
platform: targetPlatform, | |
mode: BuildMode.release, | |
), | |
]; | |
@override | |
List<Source> get outputs => const <Source>[ | |
Source.pattern('{BUILD_DIR}/app.so'), | |
]; | |
@override | |
List<Target> get dependencies => const <Target>[ | |
KernelSnapshot(), | |
]; | |
final TargetPlatform targetPlatform; | |
} | |
/// Copies the pre-built flutter aot bundle. | |
// This is a one-off rule for implementing build aot in terms of assemble. | |
abstract class CopyFlutterAotBundle extends Target { | |
const CopyFlutterAotBundle(); | |
@override | |
List<Source> get inputs => const <Source>[ | |
Source.pattern('{BUILD_DIR}/app.so'), | |
]; | |
@override | |
List<Source> get outputs => const <Source>[ | |
Source.pattern('{OUTPUT_DIR}/app.so'), | |
]; | |
@override | |
Future<void> build(Environment environment) async { | |
final File outputFile = environment.outputDir.childFile('app.so'); | |
if (!outputFile.parent.existsSync()) { | |
outputFile.parent.createSync(recursive: true); | |
} | |
environment.buildDir.childFile('app.so').copySync(outputFile.path); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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:async'; | |
import 'dart:typed_data'; | |
import 'package:meta/meta.dart'; | |
import 'package:package_config/package_config.dart'; | |
import 'package:process/process.dart'; | |
import 'package:usage/uuid/uuid.dart'; | |
import 'artifacts.dart'; | |
import 'base/common.dart'; | |
import 'base/file_system.dart'; | |
import 'base/io.dart'; | |
import 'base/logger.dart'; | |
import 'base/platform.dart'; | |
import 'build_info.dart'; | |
import 'convert.dart'; | |
import 'aop/aspectd.dart'; | |
/// The target model describes the set of core libraries that are available within | |
/// the SDK. | |
class TargetModel { | |
/// Parse a [TargetModel] from a raw string. | |
/// | |
/// Throws an exception if passed a value other than 'flutter', | |
/// 'flutter_runner', 'vm', or 'dartdevc'. | |
factory TargetModel(String rawValue) { | |
switch (rawValue) { | |
case 'flutter': | |
return flutter; | |
case 'flutter_runner': | |
return flutterRunner; | |
case 'vm': | |
return vm; | |
case 'dartdevc': | |
return dartdevc; | |
} | |
throw Exception('Unexpected target model $rawValue'); | |
} | |
const TargetModel._(this._value); | |
/// The Flutter patched Dart SDK. | |
static const TargetModel flutter = TargetModel._('flutter'); | |
/// The Fuchsia patched SDK. | |
static const TargetModel flutterRunner = TargetModel._('flutter_runner'); | |
/// The Dart VM. | |
static const TargetModel vm = TargetModel._('vm'); | |
/// The development compiler for JavaScript. | |
static const TargetModel dartdevc = TargetModel._('dartdevc'); | |
final String _value; | |
@override | |
String toString() => _value; | |
} | |
class CompilerOutput { | |
const CompilerOutput(this.outputFilename, this.errorCount, this.sources, | |
{this.expressionData}); | |
final String outputFilename; | |
final int errorCount; | |
final List<Uri> sources; | |
/// This field is only non-null for expression compilation requests. | |
final Uint8List? expressionData; | |
} | |
enum StdoutState { CollectDiagnostic, CollectDependencies } | |
/// Handles stdin/stdout communication with the frontend server. | |
class StdoutHandler { | |
StdoutHandler({ | |
required Logger logger, | |
required FileSystem fileSystem, | |
}) : _logger = logger, | |
_fileSystem = fileSystem { | |
reset(); | |
} | |
final Logger _logger; | |
final FileSystem _fileSystem; | |
String? boundaryKey; | |
StdoutState state = StdoutState.CollectDiagnostic; | |
Completer<CompilerOutput?>? compilerOutput; | |
final List<Uri> sources = <Uri>[]; | |
bool _suppressCompilerMessages = false; | |
bool _expectSources = true; | |
bool _readFile = false; | |
void handler(String message) { | |
const String kResultPrefix = 'result '; | |
if (boundaryKey == null && message.startsWith(kResultPrefix)) { | |
boundaryKey = message.substring(kResultPrefix.length); | |
return; | |
} | |
final String? messageBoundaryKey = boundaryKey; | |
if (messageBoundaryKey != null && message.startsWith(messageBoundaryKey)) { | |
if (_expectSources) { | |
if (state == StdoutState.CollectDiagnostic) { | |
state = StdoutState.CollectDependencies; | |
return; | |
} | |
} | |
if (message.length <= messageBoundaryKey.length) { | |
compilerOutput?.complete(null); | |
return; | |
} | |
final int spaceDelimiter = message.lastIndexOf(' '); | |
final String fileName = | |
message.substring(messageBoundaryKey.length + 1, spaceDelimiter); | |
final int errorCount = | |
int.parse(message.substring(spaceDelimiter + 1).trim()); | |
Uint8List? expressionData; | |
if (_readFile) { | |
expressionData = _fileSystem.file(fileName).readAsBytesSync(); | |
} | |
final CompilerOutput output = CompilerOutput( | |
fileName, | |
errorCount, | |
sources, | |
expressionData: expressionData, | |
); | |
compilerOutput?.complete(output); | |
return; | |
} | |
if (state == StdoutState.CollectDiagnostic) { | |
if (!_suppressCompilerMessages) { | |
_logger.printError(message); | |
} else { | |
_logger.printTrace(message); | |
} | |
} else { | |
assert(state == StdoutState.CollectDependencies); | |
switch (message[0]) { | |
case '+': | |
sources.add(Uri.parse(message.substring(1))); | |
break; | |
case '-': | |
sources.remove(Uri.parse(message.substring(1))); | |
break; | |
default: | |
_logger.printTrace('Unexpected prefix for $message uri - ignoring'); | |
} | |
} | |
} | |
// This is needed to get ready to process next compilation result output, | |
// with its own boundary key and new completer. | |
void reset( | |
{bool suppressCompilerMessages = false, | |
bool expectSources = true, | |
bool readFile = false}) { | |
boundaryKey = null; | |
compilerOutput = Completer<CompilerOutput?>(); | |
_suppressCompilerMessages = suppressCompilerMessages; | |
_expectSources = expectSources; | |
_readFile = readFile; | |
state = StdoutState.CollectDiagnostic; | |
} | |
} | |
/// List the preconfigured build options for a given build mode. | |
List<String> buildModeOptions(BuildMode mode, List<String> dartDefines) { | |
switch (mode) { | |
case BuildMode.debug: | |
return <String>[ | |
'-Ddart.vm.profile=false', | |
// This allows the CLI to override the value of this define for unit | |
// testing the framework. | |
if (!dartDefines | |
.any((String define) => define.startsWith('dart.vm.product'))) | |
'-Ddart.vm.product=false', | |
'--enable-asserts', | |
]; | |
case BuildMode.profile: | |
return <String>[ | |
'-Ddart.vm.profile=true', | |
'-Ddart.vm.product=false', | |
]; | |
case BuildMode.release: | |
return <String>[ | |
'-Ddart.vm.profile=false', | |
'-Ddart.vm.product=true', | |
]; | |
} | |
throw Exception('Unknown BuildMode: $mode'); | |
} | |
/// A compiler interface for producing single (non-incremental) kernel files. | |
class KernelCompiler { | |
KernelCompiler({ | |
required FileSystem fileSystem, | |
required Logger logger, | |
required ProcessManager processManager, | |
required Artifacts artifacts, | |
required List<String> fileSystemRoots, | |
String? fileSystemScheme, | |
@visibleForTesting StdoutHandler? stdoutHandler, | |
}) : _logger = logger, | |
_fileSystem = fileSystem, | |
_artifacts = artifacts, | |
_processManager = processManager, | |
_fileSystemScheme = fileSystemScheme, | |
_fileSystemRoots = fileSystemRoots, | |
_stdoutHandler = stdoutHandler ?? | |
StdoutHandler(logger: logger, fileSystem: fileSystem); | |
final FileSystem _fileSystem; | |
final Artifacts _artifacts; | |
final ProcessManager _processManager; | |
final Logger _logger; | |
final String? _fileSystemScheme; | |
final List<String> _fileSystemRoots; | |
final StdoutHandler _stdoutHandler; | |
Future<CompilerOutput?> compile({ | |
required String sdkRoot, | |
String? mainPath, | |
String? outputFilePath, | |
String? depFilePath, | |
TargetModel targetModel = TargetModel.flutter, | |
bool linkPlatformKernelIn = false, | |
bool aot = false, | |
List<String>? extraFrontEndOptions, | |
List<String>? fileSystemRoots, | |
String? fileSystemScheme, | |
String? initializeFromDill, | |
String? platformDill, | |
Directory? buildDir, | |
bool checkDartPluginRegistry = false, | |
required String? packagesPath, | |
required BuildMode buildMode, | |
required bool trackWidgetCreation, | |
required List<String> dartDefines, | |
required PackageConfig packageConfig, | |
}) async { | |
final String frontendServer = _artifacts | |
.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk); | |
// This is a URI, not a file path, so the forward slash is correct even on Windows. | |
if (!sdkRoot.endsWith('/')) { | |
sdkRoot = '$sdkRoot/'; | |
} | |
final String engineDartPath = | |
_artifacts.getHostArtifact(HostArtifact.engineDartBinary).path; | |
if (!_processManager.canRun(engineDartPath)) { | |
throwToolExit('Unable to find Dart binary at $engineDartPath'); | |
} | |
String? mainUri; | |
final File mainFile = _fileSystem.file(mainPath); | |
final Uri mainFileUri = mainFile.uri; | |
if (packagesPath != null) { | |
mainUri = packageConfig.toPackageUri(mainFileUri)?.toString(); | |
} | |
mainUri ??= toMultiRootPath(mainFileUri, _fileSystemScheme, | |
_fileSystemRoots, _fileSystem.path.separator == r'\'); | |
if (outputFilePath != null && !_fileSystem.isFileSync(outputFilePath)) { | |
_fileSystem.file(outputFilePath).createSync(recursive: true); | |
} | |
if (buildDir != null && checkDartPluginRegistry) { | |
// Check if there's a Dart plugin registrant. | |
// This is contained in the file `generated_main.dart` under `.dart_tools/flutter_build/`. | |
final File newMainDart = buildDir.parent.childFile('generated_main.dart'); | |
if (newMainDart.existsSync()) { | |
mainUri = newMainDart.path; | |
} | |
} | |
final List<String> command = <String>[ | |
engineDartPath, | |
'--disable-dart-dev', | |
frontendServer, | |
'--sdk-root', | |
sdkRoot, | |
'--target=$targetModel', | |
'--no-print-incremental-dependencies', | |
for (final Object dartDefine in dartDefines) '-D$dartDefine', | |
...buildModeOptions(buildMode, dartDefines), | |
if (trackWidgetCreation) '--track-widget-creation', | |
if (!linkPlatformKernelIn) '--no-link-platform', | |
if (aot) ...<String>[ | |
'--aot', | |
'--tfa', | |
], | |
if (packagesPath != null) ...<String>[ | |
'--packages', | |
packagesPath, | |
], | |
if (outputFilePath != null) ...<String>[ | |
'--output-dill', | |
outputFilePath, | |
], | |
if (depFilePath != null && | |
(fileSystemRoots == null || fileSystemRoots.isEmpty)) ...<String>[ | |
'--depfile', | |
depFilePath, | |
], | |
if (fileSystemRoots != null) | |
for (final String root in fileSystemRoots) ...<String>[ | |
'--filesystem-root', | |
root, | |
], | |
if (fileSystemScheme != null) ...<String>[ | |
'--filesystem-scheme', | |
fileSystemScheme, | |
], | |
if (initializeFromDill != null) ...<String>[ | |
'--initialize-from-dill', | |
initializeFromDill, | |
], | |
if (platformDill != null) ...<String>[ | |
'--platform', | |
platformDill, | |
], | |
...?extraFrontEndOptions, | |
mainUri, | |
]; | |
_logger.printTrace(command.join(' ')); | |
final Process server = await _processManager.start(command); | |
server.stderr.transform<String>(utf8.decoder).listen(_logger.printError); | |
server.stdout | |
.transform<String>(utf8.decoder) | |
.transform<String>(const LineSplitter()) | |
.listen(_stdoutHandler.handler); | |
final int exitCode = await server.exitCode; | |
if (exitCode == 0) { | |
return _stdoutHandler.compilerOutput?.future; | |
} | |
return null; | |
} | |
} | |
/// Class that allows to serialize compilation requests to the compiler. | |
abstract class _CompilationRequest { | |
_CompilationRequest(this.completer); | |
Completer<CompilerOutput?> completer; | |
Future<CompilerOutput?> _run(DefaultResidentCompiler compiler); | |
Future<void> run(DefaultResidentCompiler compiler) async { | |
completer.complete(await _run(compiler)); | |
} | |
} | |
class _RecompileRequest extends _CompilationRequest { | |
_RecompileRequest( | |
Completer<CompilerOutput?> completer, | |
this.mainUri, | |
this.invalidatedFiles, | |
this.outputPath, | |
this.packageConfig, | |
this.suppressErrors, | |
) : super(completer); | |
Uri mainUri; | |
List<Uri>? invalidatedFiles; | |
String outputPath; | |
PackageConfig packageConfig; | |
bool suppressErrors; | |
@override | |
Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => | |
compiler._recompile(this); | |
} | |
class _CompileExpressionRequest extends _CompilationRequest { | |
_CompileExpressionRequest( | |
Completer<CompilerOutput?> completer, | |
this.expression, | |
this.definitions, | |
this.typeDefinitions, | |
this.libraryUri, | |
this.klass, | |
this.isStatic, | |
) : super(completer); | |
String expression; | |
List<String>? definitions; | |
List<String>? typeDefinitions; | |
String? libraryUri; | |
String? klass; | |
bool isStatic; | |
@override | |
Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => | |
compiler._compileExpression(this); | |
} | |
class _CompileExpressionToJsRequest extends _CompilationRequest { | |
_CompileExpressionToJsRequest( | |
Completer<CompilerOutput?> completer, | |
this.libraryUri, | |
this.line, | |
this.column, | |
this.jsModules, | |
this.jsFrameValues, | |
this.moduleName, | |
this.expression, | |
) : super(completer); | |
final String? libraryUri; | |
final int line; | |
final int column; | |
final Map<String, String>? jsModules; | |
final Map<String, String>? jsFrameValues; | |
final String? moduleName; | |
final String? expression; | |
@override | |
Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => | |
compiler._compileExpressionToJs(this); | |
} | |
class _RejectRequest extends _CompilationRequest { | |
_RejectRequest(Completer<CompilerOutput?> completer) : super(completer); | |
@override | |
Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => | |
compiler._reject(); | |
} | |
/// Wrapper around incremental frontend server compiler, that communicates with | |
/// server via stdin/stdout. | |
/// | |
/// The wrapper is intended to stay resident in memory as user changes, reloads, | |
/// restarts the Flutter app. | |
abstract class ResidentCompiler { | |
factory ResidentCompiler( | |
String sdkRoot, { | |
required BuildMode buildMode, | |
required Logger logger, | |
required ProcessManager processManager, | |
required Artifacts artifacts, | |
required Platform platform, | |
required FileSystem fileSystem, | |
bool testCompilation, | |
bool trackWidgetCreation, | |
String packagesPath, | |
List<String> fileSystemRoots, | |
String? fileSystemScheme, | |
String initializeFromDill, | |
TargetModel targetModel, | |
bool unsafePackageSerialization, | |
List<String> extraFrontEndOptions, | |
String platformDill, | |
List<String>? dartDefines, | |
String librariesSpec, | |
}) = DefaultResidentCompiler; | |
// TODO(zanderso): find a better way to configure additional file system | |
// roots from the runner. | |
// See: https://github.com/flutter/flutter/issues/50494 | |
void addFileSystemRoot(String root); | |
/// If invoked for the first time, it compiles Dart script identified by | |
/// [mainPath], [invalidatedFiles] list is ignored. | |
/// On successive runs [invalidatedFiles] indicates which files need to be | |
/// recompiled. If [mainPath] is [null], previously used [mainPath] entry | |
/// point that is used for recompilation. | |
/// Binary file name is returned if compilation was successful, otherwise | |
/// null is returned. | |
/// | |
/// If [checkDartPluginRegistry] is true, it is the caller's responsibility | |
/// to ensure that the generated registrant file has been updated such that | |
/// it is wrapping [mainUri]. | |
Future<CompilerOutput?> recompile( | |
Uri mainUri, | |
List<Uri>? invalidatedFiles, { | |
required String outputPath, | |
required PackageConfig packageConfig, | |
required FileSystem fs, | |
String? projectRootPath, | |
bool suppressErrors = false, | |
bool checkDartPluginRegistry = false, | |
}); | |
Future<CompilerOutput?> compileExpression( | |
String expression, | |
List<String>? definitions, | |
List<String>? typeDefinitions, | |
String? libraryUri, | |
String? klass, | |
bool isStatic, | |
); | |
/// Compiles [expression] in [libraryUri] at [line]:[column] to JavaScript | |
/// in [moduleName]. | |
/// | |
/// Values listed in [jsFrameValues] are substituted for their names in the | |
/// [expression]. | |
/// | |
/// Ensures that all [jsModules] are loaded and accessible inside the | |
/// expression. | |
/// | |
/// Example values of parameters: | |
/// [moduleName] is of the form '/packages/hello_world_main.dart' | |
/// [jsFrameValues] is a map from js variable name to its primitive value | |
/// or another variable name, for example | |
/// { 'x': '1', 'y': 'y', 'o': 'null' } | |
/// [jsModules] is a map from variable name to the module name, where | |
/// variable name is the name originally used in JavaScript to contain the | |
/// module object, for example: | |
/// { 'dart':'dart_sdk', 'main': '/packages/hello_world_main.dart' } | |
/// Returns a [CompilerOutput] including the name of the file containing the | |
/// compilation result and a number of errors. | |
Future<CompilerOutput?> compileExpressionToJs( | |
String libraryUri, | |
int line, | |
int column, | |
Map<String, String> jsModules, | |
Map<String, String> jsFrameValues, | |
String moduleName, | |
String expression, | |
); | |
/// Should be invoked when results of compilation are accepted by the client. | |
/// | |
/// Either [accept] or [reject] should be called after every [recompile] call. | |
void accept(); | |
/// Should be invoked when results of compilation are rejected by the client. | |
/// | |
/// Either [accept] or [reject] should be called after every [recompile] call. | |
Future<CompilerOutput?> reject(); | |
/// Should be invoked when frontend server compiler should forget what was | |
/// accepted previously so that next call to [recompile] produces complete | |
/// kernel file. | |
void reset(); | |
Future<Object> shutdown(); | |
} | |
@visibleForTesting | |
class DefaultResidentCompiler implements ResidentCompiler { | |
DefaultResidentCompiler( | |
String sdkRoot, { | |
required this.buildMode, | |
required Logger logger, | |
required ProcessManager processManager, | |
required Artifacts artifacts, | |
required Platform platform, | |
required FileSystem fileSystem, | |
this.testCompilation = false, | |
this.trackWidgetCreation = true, | |
this.packagesPath, | |
List<String> fileSystemRoots = const <String>[], | |
this.fileSystemScheme, | |
this.initializeFromDill, | |
this.targetModel = TargetModel.flutter, | |
this.unsafePackageSerialization = false, | |
this.extraFrontEndOptions, | |
this.platformDill, | |
List<String>? dartDefines, | |
this.librariesSpec, | |
@visibleForTesting StdoutHandler? stdoutHandler, | |
}) : assert(sdkRoot != null), | |
_logger = logger, | |
_processManager = processManager, | |
_artifacts = artifacts, | |
_stdoutHandler = stdoutHandler ?? | |
StdoutHandler(logger: logger, fileSystem: fileSystem), | |
_platform = platform, | |
dartDefines = dartDefines ?? const <String>[], | |
// This is a URI, not a file path, so the forward slash is correct even on Windows. | |
sdkRoot = sdkRoot.endsWith('/') ? sdkRoot : '$sdkRoot/', | |
// Make a copy, we might need to modify it later. | |
fileSystemRoots = List<String>.from(fileSystemRoots); | |
final Logger _logger; | |
final ProcessManager _processManager; | |
final Artifacts _artifacts; | |
final Platform _platform; | |
final bool testCompilation; | |
final BuildMode buildMode; | |
final bool trackWidgetCreation; | |
final String? packagesPath; | |
final TargetModel targetModel; | |
final List<String> fileSystemRoots; | |
final String? fileSystemScheme; | |
final String? initializeFromDill; | |
final bool unsafePackageSerialization; | |
final List<String>? extraFrontEndOptions; | |
final List<String> dartDefines; | |
final String? librariesSpec; | |
@override | |
void addFileSystemRoot(String root) { | |
fileSystemRoots.add(root); | |
} | |
/// The path to the root of the Dart SDK used to compile. | |
/// | |
/// This is used to resolve the [platformDill]. | |
final String sdkRoot; | |
/// The path to the platform dill file. | |
/// | |
/// This does not need to be provided for the normal Flutter workflow. | |
final String? platformDill; | |
Process? _server; | |
final StdoutHandler _stdoutHandler; | |
bool _compileRequestNeedsConfirmation = false; | |
final StreamController<_CompilationRequest> _controller = | |
StreamController<_CompilationRequest>(); | |
@override | |
Future<CompilerOutput?> recompile( | |
Uri mainUri, | |
List<Uri>? invalidatedFiles, { | |
required String outputPath, | |
required PackageConfig packageConfig, | |
bool suppressErrors = false, | |
bool checkDartPluginRegistry = false, | |
String? projectRootPath, | |
FileSystem? fs, | |
}) async { | |
assert(outputPath != null); | |
if (!_controller.hasListener) { | |
_controller.stream.listen(_handleCompilationRequest); | |
} | |
// `generated_main.dart` contains the Dart plugin registry. | |
if (checkDartPluginRegistry && projectRootPath != null && fs != null) { | |
final File generatedMainDart = fs.file( | |
fs.path.join( | |
projectRootPath, | |
'.dart_tool', | |
'flutter_build', | |
'generated_main.dart', | |
), | |
); | |
if (generatedMainDart != null && generatedMainDart.existsSync()) { | |
mainUri = generatedMainDart.uri; | |
} | |
} | |
final Completer<CompilerOutput?> completer = Completer<CompilerOutput?>(); | |
_controller.add(_RecompileRequest(completer, mainUri, invalidatedFiles, | |
outputPath, packageConfig, suppressErrors)); | |
return completer.future; | |
} | |
Future<CompilerOutput?> _recompile(_RecompileRequest request) async { | |
_stdoutHandler.reset(); | |
_compileRequestNeedsConfirmation = true; | |
_stdoutHandler._suppressCompilerMessages = request.suppressErrors; | |
final String mainUri = | |
request.packageConfig.toPackageUri(request.mainUri)?.toString() ?? | |
toMultiRootPath(request.mainUri, fileSystemScheme, fileSystemRoots, | |
_platform.isWindows); | |
final Process? server = _server; | |
if (server == null) { | |
return _compile(mainUri, request.outputPath); | |
} | |
final String inputKey = Uuid().generateV4(); | |
server.stdin.writeln('recompile $mainUri $inputKey'); | |
_logger.printTrace('<- recompile $mainUri $inputKey'); | |
final List<Uri>? invalidatedFiles = request.invalidatedFiles; | |
if (invalidatedFiles != null) { | |
for (final Uri fileUri in invalidatedFiles) { | |
String message; | |
if (fileUri.scheme == 'package') { | |
message = fileUri.toString(); | |
} else { | |
message = request.packageConfig.toPackageUri(fileUri)?.toString() ?? | |
toMultiRootPath(fileUri, fileSystemScheme, fileSystemRoots, | |
_platform.isWindows); | |
} | |
server.stdin.writeln(message); | |
_logger.printTrace(message); | |
} | |
} | |
server.stdin.writeln(inputKey); | |
_logger.printTrace('<- $inputKey'); | |
return _stdoutHandler.compilerOutput?.future; | |
} | |
final List<_CompilationRequest> _compilationQueue = <_CompilationRequest>[]; | |
Future<void> _handleCompilationRequest(_CompilationRequest request) async { | |
final bool isEmpty = _compilationQueue.isEmpty; | |
_compilationQueue.add(request); | |
// Only trigger processing if queue was empty - i.e. no other requests | |
// are currently being processed. This effectively enforces "one | |
// compilation request at a time". | |
if (isEmpty) { | |
while (_compilationQueue.isNotEmpty) { | |
final _CompilationRequest request = _compilationQueue.first; | |
await request.run(this); | |
_compilationQueue.removeAt(0); | |
} | |
} | |
} | |
Future<CompilerOutput?> _compile( | |
String scriptUri, | |
String? outputPath, | |
) async { | |
await AspectdHook.enableAspectd(); | |
final String frontendServer = _artifacts | |
.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk); | |
final List<String> command = <String>[ | |
_artifacts.getHostArtifact(HostArtifact.engineDartBinary).path, | |
'--disable-dart-dev', | |
frontendServer, | |
'--sdk-root', | |
sdkRoot, | |
'--incremental', | |
if (testCompilation) '--no-print-incremental-dependencies', | |
'--target=$targetModel', | |
// TODO(zanderso): remove once this becomes the default behavior | |
// in the frontend_server. | |
// https://github.com/flutter/flutter/issues/52693 | |
'--debugger-module-names', | |
// TODO(annagrin): remove once this becomes the default behavior | |
// in the frontend_server. | |
// https://github.com/flutter/flutter/issues/59902 | |
'--experimental-emit-debug-metadata', | |
for (final Object dartDefine in dartDefines) '-D$dartDefine', | |
if (outputPath != null) ...<String>[ | |
'--output-dill', | |
outputPath, | |
], | |
if (librariesSpec != null) ...<String>[ | |
'--libraries-spec', | |
librariesSpec!, | |
], | |
if (packagesPath != null) ...<String>[ | |
'--packages', | |
packagesPath!, | |
], | |
...buildModeOptions(buildMode, dartDefines), | |
if (trackWidgetCreation) '--track-widget-creation', | |
if (fileSystemRoots != null) | |
for (final String root in fileSystemRoots) ...<String>[ | |
'--filesystem-root', | |
root, | |
], | |
if (fileSystemScheme != null) ...<String>[ | |
'--filesystem-scheme', | |
fileSystemScheme!, | |
], | |
if (initializeFromDill != null) ...<String>[ | |
'--initialize-from-dill', | |
initializeFromDill!, | |
], | |
if (platformDill != null) ...<String>[ | |
'--platform', | |
platformDill!, | |
], | |
if (unsafePackageSerialization == true) '--unsafe-package-serialization', | |
...?extraFrontEndOptions, | |
]; | |
_logger.printTrace(command.join(' ')); | |
_server = await _processManager.start(command); | |
_server?.stdout | |
.transform<String>(utf8.decoder) | |
.transform<String>(const LineSplitter()) | |
.listen(_stdoutHandler.handler, onDone: () { | |
// when outputFilename future is not completed, but stdout is closed | |
// process has died unexpectedly. | |
if (_stdoutHandler.compilerOutput?.isCompleted == false) { | |
_stdoutHandler.compilerOutput?.complete(null); | |
throwToolExit('the Dart compiler exited unexpectedly.'); | |
} | |
}); | |
_server?.stderr | |
.transform<String>(utf8.decoder) | |
.transform<String>(const LineSplitter()) | |
.listen(_logger.printError); | |
unawaited(_server?.exitCode.then((int code) { | |
if (code != 0) { | |
throwToolExit('the Dart compiler exited unexpectedly.'); | |
} | |
})); | |
_server?.stdin.writeln('compile $scriptUri'); | |
_logger.printTrace('<- compile $scriptUri'); | |
return _stdoutHandler.compilerOutput?.future; | |
} | |
@override | |
Future<CompilerOutput?> compileExpression( | |
String expression, | |
List<String>? definitions, | |
List<String>? typeDefinitions, | |
String? libraryUri, | |
String? klass, | |
bool isStatic, | |
) async { | |
if (!_controller.hasListener) { | |
_controller.stream.listen(_handleCompilationRequest); | |
} | |
final Completer<CompilerOutput?> completer = Completer<CompilerOutput?>(); | |
final _CompileExpressionRequest request = _CompileExpressionRequest( | |
completer, | |
expression, | |
definitions, | |
typeDefinitions, | |
libraryUri, | |
klass, | |
isStatic); | |
_controller.add(request); | |
return completer.future; | |
} | |
Future<CompilerOutput?> _compileExpression( | |
_CompileExpressionRequest request) async { | |
_stdoutHandler.reset( | |
suppressCompilerMessages: true, expectSources: false, readFile: true); | |
// 'compile-expression' should be invoked after compiler has been started, | |
// program was compiled. | |
final Process? server = _server; | |
if (server == null) { | |
return null; | |
} | |
final String inputKey = Uuid().generateV4(); | |
server.stdin | |
..writeln('compile-expression $inputKey') | |
..writeln(request.expression); | |
request.definitions?.forEach(server.stdin.writeln); | |
server.stdin.writeln(inputKey); | |
request.typeDefinitions?.forEach(server.stdin.writeln); | |
server.stdin | |
..writeln(inputKey) | |
..writeln(request.libraryUri ?? '') | |
..writeln(request.klass ?? '') | |
..writeln(request.isStatic); | |
return _stdoutHandler.compilerOutput?.future; | |
} | |
@override | |
Future<CompilerOutput?> compileExpressionToJs( | |
String libraryUri, | |
int line, | |
int column, | |
Map<String, String> jsModules, | |
Map<String, String> jsFrameValues, | |
String moduleName, | |
String expression, | |
) { | |
if (!_controller.hasListener) { | |
_controller.stream.listen(_handleCompilationRequest); | |
} | |
final Completer<CompilerOutput?> completer = Completer<CompilerOutput?>(); | |
_controller.add(_CompileExpressionToJsRequest(completer, libraryUri, line, | |
column, jsModules, jsFrameValues, moduleName, expression)); | |
return completer.future; | |
} | |
Future<CompilerOutput?> _compileExpressionToJs( | |
_CompileExpressionToJsRequest request) async { | |
_stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false); | |
// 'compile-expression-to-js' should be invoked after compiler has been started, | |
// program was compiled. | |
final Process? server = _server; | |
if (server == null) { | |
return null; | |
} | |
final String inputKey = Uuid().generateV4(); | |
server.stdin | |
..writeln('compile-expression-to-js $inputKey') | |
..writeln(request.libraryUri ?? '') | |
..writeln(request.line) | |
..writeln(request.column); | |
request.jsModules?.forEach((String k, String v) { | |
server.stdin.writeln('$k:$v'); | |
}); | |
server.stdin.writeln(inputKey); | |
request.jsFrameValues?.forEach((String k, String v) { | |
server.stdin.writeln('$k:$v'); | |
}); | |
server.stdin | |
..writeln(inputKey) | |
..writeln(request.moduleName ?? '') | |
..writeln(request.expression ?? ''); | |
return _stdoutHandler.compilerOutput?.future; | |
} | |
@override | |
void accept() { | |
if (_compileRequestNeedsConfirmation) { | |
_server?.stdin.writeln('accept'); | |
_logger.printTrace('<- accept'); | |
} | |
_compileRequestNeedsConfirmation = false; | |
} | |
@override | |
Future<CompilerOutput?> reject() { | |
if (!_controller.hasListener) { | |
_controller.stream.listen(_handleCompilationRequest); | |
} | |
final Completer<CompilerOutput?> completer = Completer<CompilerOutput?>(); | |
_controller.add(_RejectRequest(completer)); | |
return completer.future; | |
} | |
Future<CompilerOutput?> _reject() async { | |
if (!_compileRequestNeedsConfirmation) { | |
return Future<CompilerOutput?>.value(); | |
} | |
_stdoutHandler.reset(expectSources: false); | |
_server?.stdin.writeln('reject'); | |
_logger.printTrace('<- reject'); | |
_compileRequestNeedsConfirmation = false; | |
return _stdoutHandler.compilerOutput?.future; | |
} | |
@override | |
void reset() { | |
_server?.stdin.writeln('reset'); | |
_logger.printTrace('<- reset'); | |
} | |
@override | |
Future<Object> shutdown() async { | |
// Server was never successfully created. | |
final Process? server = _server; | |
if (server == null) { | |
return 0; | |
} | |
_logger.printTrace('killing pid ${server.pid}'); | |
server.kill(); | |
return server.exitCode; | |
} | |
} | |
/// Convert a file URI into a multi-root scheme URI if provided, otherwise | |
/// return unmodified. | |
@visibleForTesting | |
String toMultiRootPath( | |
Uri fileUri, String? scheme, List<String> fileSystemRoots, bool windows) { | |
if (scheme == null || fileSystemRoots.isEmpty || fileUri.scheme != 'file') { | |
return fileUri.toString(); | |
} | |
final String filePath = fileUri.toFilePath(windows: windows); | |
for (final String fileSystemRoot in fileSystemRoots) { | |
if (filePath.startsWith(fileSystemRoot)) { | |
return '$scheme://${filePath.substring(fileSystemRoot.length)}'; | |
} | |
} | |
return fileUri.toString(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @dart = 2.8 | |
import 'dart:io'; | |
import 'package:flutter_tools/src/artifacts.dart'; | |
import 'package:flutter_tools/src/base/common.dart'; | |
import 'package:flutter_tools/src/base/file_system.dart'; | |
import 'package:flutter_tools/src/build_info.dart'; | |
import 'package:flutter_tools/src/globals.dart'; | |
/// 创建时间:2020-03-28 | |
/// 作者:liujingguang | |
/// 描述:Hook处理工厂 | |
enum CommandType4Aop { Bundle, Aot, Snapshot } | |
class HookFactory { | |
static Future<void> hook(String productDirPath, String executorPath, | |
BuildMode buildMode, CommandType4Aop type) async { | |
if (productDirPath == null || | |
executorPath == null || | |
!fs.file(executorPath).existsSync()) { | |
return; | |
} | |
String inputPath; | |
switch (type) { | |
case CommandType4Aop.Bundle: | |
inputPath = _getBundleInputPath(productDirPath); | |
break; | |
case CommandType4Aop.Aot: | |
inputPath = _getAotInputPath(productDirPath); | |
break; | |
case CommandType4Aop.Snapshot: | |
inputPath = _getSnapShotInputPath(productDirPath); | |
break; | |
} | |
if (!inputPath.startsWith(fs.currentDirectory.path)) { | |
inputPath = fs.path.join(fs.currentDirectory.path, inputPath); | |
} | |
if (!fs.file(inputPath).existsSync()) { | |
return; | |
} | |
final String outputPath = | |
inputPath + '.${type.toString().toLowerCase()}.result.dill'; | |
/// 执行hook命令 | |
final List<String> command = <String>[ | |
artifacts | |
.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), | |
executorPath, | |
'--input', | |
inputPath, | |
'--output', | |
outputPath, | |
if (buildMode != BuildMode.release) ...<String>[ | |
'--sdk-root', | |
fs | |
.file(artifacts.getArtifactPath(Artifact.platformKernelDill)) | |
.parent | |
.path + | |
fs.path.separator | |
], | |
]; | |
print(command.toString()); | |
final ProcessResult result = await processManager.run(command); | |
if (result.exitCode != 0) { | |
print(result.stderr); | |
throwToolExit( | |
'hook by aop terminated unexpectedly in ${type.toString()}.'); | |
return; | |
} | |
print('aop hook succeed'); | |
/// 删除input输入文件 | |
final File inputFile = fs.file(inputPath); | |
if (inputFile.existsSync()) { | |
// inputFile.copySync(inputPath + '.old'); //调试时可以打开查看信息 | |
inputFile.deleteSync(); | |
} | |
/// 将Aop处理生成后的output文件重命名为input文件名 | |
fs.file(outputPath).renameSync(inputPath); | |
} | |
static String _getBundleInputPath(String assetsDir) => | |
fs.path.join(assetsDir, 'kernel_blob.bin'); | |
static String _getAotInputPath(String path) => | |
fs.path.join(path ?? getAotBuildDirectory(), 'app.dill'); | |
static String _getSnapShotInputPath(String path) => path; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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:math'; | |
import 'package:crypto/crypto.dart'; | |
import 'package:package_config/package_config.dart'; | |
import '../../artifacts.dart'; | |
import '../../base/file_system.dart'; | |
import '../../base/io.dart'; | |
import '../../build_info.dart'; | |
import '../../cache.dart'; | |
import '../../convert.dart'; | |
import '../../dart/language_version.dart'; | |
import '../../dart/package_map.dart'; | |
import '../../globals.dart' as globals; | |
import '../../project.dart'; | |
import '../build_system.dart'; | |
import '../depfile.dart'; | |
import '../exceptions.dart'; | |
import 'assets.dart'; | |
import 'localizations.dart'; | |
import '../../aop/aop_manager.dart'; | |
/// Whether the application has web plugins. | |
const String kHasWebPlugins = 'HasWebPlugins'; | |
/// An override for the dart2js build mode. | |
/// | |
/// Valid values are O1 (lowest, profile default) to O4 (highest, release default). | |
const String kDart2jsOptimization = 'Dart2jsOptimization'; | |
/// Whether to disable dynamic generation code to satisfy csp policies. | |
const String kCspMode = 'cspMode'; | |
/// Base href to set in index.html in flutter build command | |
const String kBaseHref = 'baseHref'; | |
/// Placeholder for base href | |
const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF'; | |
/// The caching strategy to use for service worker generation. | |
const String kServiceWorkerStrategy = 'ServiceWorkerStrategy'; | |
/// Whether the dart2js build should output source maps. | |
const String kSourceMapsEnabled = 'SourceMaps'; | |
/// Whether the dart2js native null assertions are enabled. | |
const String kNativeNullAssertions = 'NativeNullAssertions'; | |
/// The caching strategy for the generated service worker. | |
enum ServiceWorkerStrategy { | |
/// Download the app shell eagerly and all other assets lazily. | |
/// Prefer the offline cached version. | |
offlineFirst, | |
/// Do not generate a service worker, | |
none, | |
} | |
const String kOfflineFirst = 'offline-first'; | |
const String kNoneWorker = 'none'; | |
/// Convert a [value] into a [ServiceWorkerStrategy]. | |
ServiceWorkerStrategy _serviceWorkerStrategyFromString(String? value) { | |
switch (value) { | |
case kNoneWorker: | |
return ServiceWorkerStrategy.none; | |
// offline-first is the default value for any invalid requests. | |
default: | |
return ServiceWorkerStrategy.offlineFirst; | |
} | |
} | |
/// Generates an entry point for a web target. | |
// Keep this in sync with build_runner/resident_web_runner.dart | |
class WebEntrypointTarget extends Target { | |
const WebEntrypointTarget(); | |
@override | |
String get name => 'web_entrypoint'; | |
@override | |
List<Target> get dependencies => const <Target>[]; | |
@override | |
List<Source> get inputs => const <Source>[ | |
Source.pattern( | |
'{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'), | |
]; | |
@override | |
List<Source> get outputs => const <Source>[ | |
Source.pattern('{BUILD_DIR}/main.dart'), | |
]; | |
@override | |
Future<void> build(Environment environment) async { | |
final String? targetFile = environment.defines[kTargetFile]; | |
final bool hasPlugins = environment.defines[kHasWebPlugins] == 'true'; | |
final Uri importUri = environment.fileSystem.file(targetFile).absolute.uri; | |
// TODO(zanderso): support configuration of this file. | |
const String packageFile = '.packages'; | |
final PackageConfig packageConfig = await loadPackageConfigWithLogging( | |
environment.fileSystem.file(packageFile), | |
logger: environment.logger, | |
); | |
final FlutterProject flutterProject = FlutterProject.current(); | |
final LanguageVersion languageVersion = determineLanguageVersion( | |
environment.fileSystem.file(targetFile), | |
packageConfig[flutterProject.manifest.appName], | |
Cache.flutterRoot!, | |
); | |
// Use the PackageConfig to find the correct package-scheme import path | |
// for the user application. If the application has a mix of package-scheme | |
// and relative imports for a library, then importing the entrypoint as a | |
// file-scheme will cause said library to be recognized as two distinct | |
// libraries. This can cause surprising behavior as types from that library | |
// will be considered distinct from each other. | |
// By construction, this will only be null if the .packages file does not | |
// have an entry for the user's application or if the main file is | |
// outside of the lib/ directory. | |
final String mainImport = | |
packageConfig.toPackageUri(importUri)?.toString() ?? | |
importUri.toString(); | |
String contents; | |
if (hasPlugins) { | |
final Uri generatedUri = environment.projectDir | |
.childDirectory('lib') | |
.childFile('generated_plugin_registrant.dart') | |
.absolute | |
.uri; | |
final String generatedImport = | |
packageConfig.toPackageUri(generatedUri)?.toString() ?? | |
generatedUri.toString(); | |
contents = ''' | |
// @dart=${languageVersion.major}.${languageVersion.minor} | |
import 'dart:ui' as ui; | |
import 'package:flutter_web_plugins/flutter_web_plugins.dart'; | |
import '$generatedImport'; | |
import '$mainImport' as entrypoint; | |
Future<void> main() async { | |
registerPlugins(webPluginRegistrar); | |
await ui.webOnlyInitializePlatform(); | |
entrypoint.main(); | |
} | |
'''; | |
} else { | |
contents = ''' | |
// @dart=${languageVersion.major}.${languageVersion.minor} | |
import 'dart:ui' as ui; | |
import '$mainImport' as entrypoint; | |
Future<void> main() async { | |
await ui.webOnlyInitializePlatform(); | |
entrypoint.main(); | |
} | |
'''; | |
} | |
environment.buildDir.childFile('main.dart').writeAsStringSync(contents); | |
} | |
} | |
/// Compiles a web entry point with dart2js. | |
class Dart2JSTarget extends Target { | |
const Dart2JSTarget(); | |
@override | |
String get name => 'dart2js'; | |
@override | |
List<Target> get dependencies => const <Target>[ | |
WebEntrypointTarget(), | |
GenerateLocalizationsTarget(), | |
]; | |
@override | |
List<Source> get inputs => const <Source>[ | |
Source.hostArtifact(HostArtifact.flutterWebSdk), | |
Source.hostArtifact(HostArtifact.dart2jsSnapshot), | |
Source.hostArtifact(HostArtifact.engineDartBinary), | |
Source.pattern('{BUILD_DIR}/main.dart'), | |
Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'), | |
]; | |
@override | |
List<Source> get outputs => const <Source>[]; | |
@override | |
List<String> get depfiles => const <String>[ | |
'dart2js.d', | |
]; | |
String _collectOutput(ProcessResult result) { | |
final String stdout = result.stdout is List<int> | |
? utf8.decode(result.stdout as List<int>) | |
: result.stdout as String; | |
final String stderr = result.stderr is List<int> | |
? utf8.decode(result.stderr as List<int>) | |
: result.stderr as String; | |
return stdout + stderr; | |
} | |
@override | |
Future<void> build(Environment environment) async { | |
final String? buildModeEnvironment = environment.defines[kBuildMode]; | |
if (buildModeEnvironment == null) { | |
throw MissingDefineException(kBuildMode, name); | |
} | |
final BuildMode buildMode = getBuildModeForName(buildModeEnvironment); | |
final bool sourceMapsEnabled = | |
environment.defines[kSourceMapsEnabled] == 'true'; | |
final bool nativeNullAssertions = | |
environment.defines[kNativeNullAssertions] == 'true'; | |
final Artifacts artifacts = globals.artifacts!; | |
final String librariesSpec = | |
(artifacts.getHostArtifact(HostArtifact.flutterWebSdk) as Directory) | |
.childFile('libraries.json') | |
.path; | |
final List<String> sharedCommandOptions = <String>[ | |
artifacts.getHostArtifact(HostArtifact.engineDartBinary).path, | |
'--disable-dart-dev', | |
artifacts.getHostArtifact(HostArtifact.dart2jsSnapshot).path, | |
'--libraries-spec=$librariesSpec', | |
...decodeCommaSeparated(environment.defines, kExtraFrontEndOptions), | |
if (nativeNullAssertions) '--native-null-assertions', | |
if (buildMode == BuildMode.profile) | |
'-Ddart.vm.profile=true' | |
else | |
'-Ddart.vm.product=true', | |
for (final String dartDefine | |
in decodeDartDefines(environment.defines, kDartDefines)) | |
'-D$dartDefine', | |
if (!sourceMapsEnabled) '--no-source-maps', | |
]; | |
// Run the dart2js compilation in two stages, so that icon tree shaking can | |
// parse the kernel file for web builds. | |
final ProcessResult kernelResult = | |
await globals.processManager.run(<String>[ | |
...sharedCommandOptions, | |
'-o', | |
environment.buildDir.childFile('app.dill').path, | |
'--packages=.packages', | |
'--cfe-only', | |
environment.buildDir.childFile('main.dart').path, // dartfile | |
]); | |
if (kernelResult.exitCode != 0) { | |
throw Exception(_collectOutput(kernelResult)); | |
} | |
await AopManager.hookSnapshotCommand( | |
environment.buildDir.childFile('app.dill').path, buildMode); | |
final String? dart2jsOptimization = | |
environment.defines[kDart2jsOptimization]; | |
final File outputJSFile = environment.buildDir.childFile('main.dart.js'); | |
final bool csp = environment.defines[kCspMode] == 'true'; | |
final ProcessResult javaScriptResult = | |
await environment.processManager.run(<String>[ | |
...sharedCommandOptions, | |
if (dart2jsOptimization != null) '-$dart2jsOptimization' else '-O4', | |
if (buildMode == BuildMode.profile) '--no-minify', | |
if (csp) '--csp', | |
'-o', | |
outputJSFile.path, | |
environment.buildDir.childFile('app.dill').path, // dartfile | |
]); | |
if (javaScriptResult.exitCode != 0) { | |
throw Exception(_collectOutput(javaScriptResult)); | |
} | |
final File dart2jsDeps = environment.buildDir.childFile('app.dill.deps'); | |
if (!dart2jsDeps.existsSync()) { | |
globals.printWarning( | |
'Warning: dart2js did not produced expected deps list at ' | |
'${dart2jsDeps.path}'); | |
return; | |
} | |
final DepfileService depfileService = DepfileService( | |
fileSystem: globals.fs, | |
logger: globals.logger, | |
); | |
final Depfile depfile = depfileService.parseDart2js( | |
environment.buildDir.childFile('app.dill.deps'), | |
outputJSFile, | |
); | |
depfileService.writeToFile( | |
depfile, | |
environment.buildDir.childFile('dart2js.d'), | |
); | |
} | |
} | |
/// Unpacks the dart2js compilation and resources to a given output directory. | |
class WebReleaseBundle extends Target { | |
const WebReleaseBundle(); | |
@override | |
String get name => 'web_release_bundle'; | |
@override | |
List<Target> get dependencies => const <Target>[ | |
Dart2JSTarget(), | |
]; | |
@override | |
List<Source> get inputs => const <Source>[ | |
Source.pattern('{BUILD_DIR}/main.dart.js'), | |
Source.pattern('{PROJECT_DIR}/pubspec.yaml'), | |
]; | |
@override | |
List<Source> get outputs => const <Source>[ | |
Source.pattern('{OUTPUT_DIR}/main.dart.js'), | |
]; | |
@override | |
List<String> get depfiles => const <String>[ | |
'dart2js.d', | |
'flutter_assets.d', | |
'web_resources.d', | |
]; | |
@override | |
Future<void> build(Environment environment) async { | |
for (final File outputFile | |
in environment.buildDir.listSync(recursive: true).whereType<File>()) { | |
final String basename = globals.fs.path.basename(outputFile.path); | |
if (!basename.contains('main.dart.js')) { | |
continue; | |
} | |
// Do not copy the deps file. | |
if (basename.endsWith('.deps')) { | |
continue; | |
} | |
outputFile.copySync(environment.outputDir | |
.childFile(globals.fs.path.basename(outputFile.path)) | |
.path); | |
} | |
final String versionInfo = FlutterProject.current().getVersionInfo(); | |
environment.outputDir | |
.childFile('version.json') | |
.writeAsStringSync(versionInfo); | |
final Directory outputDirectory = | |
environment.outputDir.childDirectory('assets'); | |
outputDirectory.createSync(recursive: true); | |
final Depfile depfile = await copyAssets( | |
environment, | |
environment.outputDir.childDirectory('assets'), | |
targetPlatform: TargetPlatform.web_javascript, | |
); | |
final DepfileService depfileService = DepfileService( | |
fileSystem: globals.fs, | |
logger: globals.logger, | |
); | |
depfileService.writeToFile( | |
depfile, | |
environment.buildDir.childFile('flutter_assets.d'), | |
); | |
final Directory webResources = environment.projectDir.childDirectory('web'); | |
final List<File> inputResourceFiles = | |
webResources.listSync(recursive: true).whereType<File>().toList(); | |
// Copy other resource files out of web/ directory. | |
final List<File> outputResourcesFiles = <File>[]; | |
for (final File inputFile in inputResourceFiles) { | |
final File outputFile = globals.fs.file(globals.fs.path.join( | |
environment.outputDir.path, | |
globals.fs.path.relative(inputFile.path, from: webResources.path))); | |
if (!outputFile.parent.existsSync()) { | |
outputFile.parent.createSync(recursive: true); | |
} | |
outputResourcesFiles.add(outputFile); | |
// insert a random hash into the requests for service_worker.js. This is not a content hash, | |
// because it would need to be the hash for the entire bundle and not just the resource | |
// in question. | |
if (environment.fileSystem.path.basename(inputFile.path) == | |
'index.html') { | |
final String randomHash = Random().nextInt(4294967296).toString(); | |
String resultString = inputFile | |
.readAsStringSync() | |
.replaceFirst( | |
'var serviceWorkerVersion = null', | |
"var serviceWorkerVersion = '$randomHash'", | |
) | |
// This is for legacy index.html that still use the old service | |
// worker loading mechanism. | |
.replaceFirst( | |
"navigator.serviceWorker.register('flutter_service_worker.js')", | |
"navigator.serviceWorker.register('flutter_service_worker.js?v=$randomHash')", | |
); | |
final String? baseHref = environment.defines[kBaseHref]; | |
if (resultString.contains(kBaseHrefPlaceholder) && baseHref == null) { | |
resultString = resultString.replaceAll(kBaseHrefPlaceholder, '/'); | |
} else if (resultString.contains(kBaseHrefPlaceholder) && | |
baseHref != null) { | |
resultString = | |
resultString.replaceAll(kBaseHrefPlaceholder, baseHref); | |
} | |
outputFile.writeAsStringSync(resultString); | |
continue; | |
} | |
inputFile.copySync(outputFile.path); | |
} | |
final Depfile resourceFile = | |
Depfile(inputResourceFiles, outputResourcesFiles); | |
depfileService.writeToFile( | |
resourceFile, | |
environment.buildDir.childFile('web_resources.d'), | |
); | |
} | |
} | |
/// Static assets provided by the Flutter SDK that do not change, such as | |
/// CanvasKit. | |
/// | |
/// These assets can be cached forever and are only invalidated when the | |
/// Flutter SDK is upgraded to a new version. | |
class WebBuiltInAssets extends Target { | |
const WebBuiltInAssets(this.fileSystem, this.cache); | |
final FileSystem fileSystem; | |
final Cache cache; | |
@override | |
String get name => 'web_static_assets'; | |
@override | |
List<Target> get dependencies => const <Target>[]; | |
@override | |
List<String> get depfiles => const <String>[]; | |
@override | |
List<Source> get inputs => const <Source>[]; | |
@override | |
List<Source> get outputs => const <Source>[]; | |
@override | |
Future<void> build(Environment environment) async { | |
// TODO(yjbanov): https://github.com/flutter/flutter/issues/52588 | |
// | |
// Update this when we start building CanvasKit from sources. In the | |
// meantime, get the Web SDK directory from cache rather than through | |
// Artifacts. The latter is sensitive to `--local-engine`, which changes | |
// the directory to point to ENGINE/src/out. However, CanvasKit is not yet | |
// built as part of the engine, but fetched from CIPD, and so it won't be | |
// found in ENGINE/src/out. | |
final Directory flutterWebSdk = cache.getWebSdkDirectory(); | |
final Directory canvasKitDirectory = | |
flutterWebSdk.childDirectory('canvaskit'); | |
for (final File file | |
in canvasKitDirectory.listSync(recursive: true).whereType<File>()) { | |
final String relativePath = | |
fileSystem.path.relative(file.path, from: canvasKitDirectory.path); | |
final String targetPath = fileSystem.path | |
.join(environment.outputDir.path, 'canvaskit', relativePath); | |
file.copySync(targetPath); | |
} | |
} | |
} | |
/// Generate a service worker for a web target. | |
class WebServiceWorker extends Target { | |
const WebServiceWorker(this.fileSystem, this.cache); | |
final FileSystem fileSystem; | |
final Cache cache; | |
@override | |
String get name => 'web_service_worker'; | |
@override | |
List<Target> get dependencies => <Target>[ | |
const Dart2JSTarget(), | |
const WebReleaseBundle(), | |
WebBuiltInAssets(fileSystem, cache), | |
]; | |
@override | |
List<String> get depfiles => const <String>[ | |
'service_worker.d', | |
]; | |
@override | |
List<Source> get inputs => const <Source>[]; | |
@override | |
List<Source> get outputs => const <Source>[]; | |
@override | |
Future<void> build(Environment environment) async { | |
final List<File> contents = environment.outputDir | |
.listSync(recursive: true) | |
.whereType<File>() | |
.where((File file) => | |
!file.path.endsWith('flutter_service_worker.js') && | |
!globals.fs.path.basename(file.path).startsWith('.')) | |
.toList(); | |
final Map<String, String> urlToHash = <String, String>{}; | |
for (final File file in contents) { | |
// Do not force caching of source maps. | |
if (file.path.endsWith('main.dart.js.map') || | |
file.path.endsWith('.part.js.map')) { | |
continue; | |
} | |
final String url = globals.fs.path | |
.toUri( | |
globals.fs.path | |
.relative(file.path, from: environment.outputDir.path), | |
) | |
.toString(); | |
final String hash = md5.convert(await file.readAsBytes()).toString(); | |
urlToHash[url] = hash; | |
// Add an additional entry for the base URL. | |
if (globals.fs.path.basename(url) == 'index.html') { | |
urlToHash['/'] = hash; | |
} | |
} | |
final File serviceWorkerFile = | |
environment.outputDir.childFile('flutter_service_worker.js'); | |
final Depfile depfile = Depfile(contents, <File>[serviceWorkerFile]); | |
final ServiceWorkerStrategy serviceWorkerStrategy = | |
_serviceWorkerStrategyFromString( | |
environment.defines[kServiceWorkerStrategy], | |
); | |
final String serviceWorker = generateServiceWorker( | |
urlToHash, | |
<String>[ | |
'/', | |
'main.dart.js', | |
'index.html', | |
'assets/NOTICES', | |
if (urlToHash.containsKey('assets/AssetManifest.json')) | |
'assets/AssetManifest.json', | |
if (urlToHash.containsKey('assets/FontManifest.json')) | |
'assets/FontManifest.json', | |
], | |
serviceWorkerStrategy: serviceWorkerStrategy, | |
); | |
serviceWorkerFile.writeAsStringSync(serviceWorker); | |
final DepfileService depfileService = DepfileService( | |
fileSystem: globals.fs, | |
logger: globals.logger, | |
); | |
depfileService.writeToFile( | |
depfile, | |
environment.buildDir.childFile('service_worker.d'), | |
); | |
} | |
} | |
/// Generate a service worker with an app-specific cache name a map of | |
/// resource files. | |
/// | |
/// The tool embeds file hashes directly into the worker so that the byte for byte | |
/// invalidation will automatically reactivate workers whenever a new | |
/// version is deployed. | |
String generateServiceWorker( | |
Map<String, String> resources, | |
List<String> coreBundle, { | |
required ServiceWorkerStrategy serviceWorkerStrategy, | |
}) { | |
if (serviceWorkerStrategy == ServiceWorkerStrategy.none) { | |
return ''; | |
} | |
return ''' | |
'use strict'; | |
const MANIFEST = 'flutter-app-manifest'; | |
const TEMP = 'flutter-temp-cache'; | |
const CACHE_NAME = 'flutter-app-cache'; | |
const RESOURCES = { | |
${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")} | |
}; | |
// The application shell files that are downloaded before a service worker can | |
// start. | |
const CORE = [ | |
${coreBundle.map((String file) => '"$file"').join(',\n')}]; | |
// During install, the TEMP cache is populated with the application shell files. | |
self.addEventListener("install", (event) => { | |
self.skipWaiting(); | |
return event.waitUntil( | |
caches.open(TEMP).then((cache) => { | |
return cache.addAll( | |
CORE.map((value) => new Request(value, {'cache': 'reload'}))); | |
}) | |
); | |
}); | |
// During activate, the cache is populated with the temp files downloaded in | |
// install. If this service worker is upgrading from one with a saved | |
// MANIFEST, then use this to retain unchanged resource files. | |
self.addEventListener("activate", function(event) { | |
return event.waitUntil(async function() { | |
try { | |
var contentCache = await caches.open(CACHE_NAME); | |
var tempCache = await caches.open(TEMP); | |
var manifestCache = await caches.open(MANIFEST); | |
var manifest = await manifestCache.match('manifest'); | |
// When there is no prior manifest, clear the entire cache. | |
if (!manifest) { | |
await caches.delete(CACHE_NAME); | |
contentCache = await caches.open(CACHE_NAME); | |
for (var request of await tempCache.keys()) { | |
var response = await tempCache.match(request); | |
await contentCache.put(request, response); | |
} | |
await caches.delete(TEMP); | |
// Save the manifest to make future upgrades efficient. | |
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES))); | |
return; | |
} | |
var oldManifest = await manifest.json(); | |
var origin = self.location.origin; | |
for (var request of await contentCache.keys()) { | |
var key = request.url.substring(origin.length + 1); | |
if (key == "") { | |
key = "/"; | |
} | |
// If a resource from the old manifest is not in the new cache, or if | |
// the MD5 sum has changed, delete it. Otherwise the resource is left | |
// in the cache and can be reused by the new service worker. | |
if (!RESOURCES[key] || RESOURCES[key] != oldManifest[key]) { | |
await contentCache.delete(request); | |
} | |
} | |
// Populate the cache with the app shell TEMP files, potentially overwriting | |
// cache files preserved above. | |
for (var request of await tempCache.keys()) { | |
var response = await tempCache.match(request); | |
await contentCache.put(request, response); | |
} | |
await caches.delete(TEMP); | |
// Save the manifest to make future upgrades efficient. | |
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES))); | |
return; | |
} catch (err) { | |
// On an unhandled exception the state of the cache cannot be guaranteed. | |
console.error('Failed to upgrade service worker: ' + err); | |
await caches.delete(CACHE_NAME); | |
await caches.delete(TEMP); | |
await caches.delete(MANIFEST); | |
} | |
}()); | |
}); | |
// The fetch handler redirects requests for RESOURCE files to the service | |
// worker cache. | |
self.addEventListener("fetch", (event) => { | |
if (event.request.method !== 'GET') { | |
return; | |
} | |
var origin = self.location.origin; | |
var key = event.request.url.substring(origin.length + 1); | |
// Redirect URLs to the index.html | |
if (key.indexOf('?v=') != -1) { | |
key = key.split('?v=')[0]; | |
} | |
if (event.request.url == origin || event.request.url.startsWith(origin + '/#') || key == '') { | |
key = '/'; | |
} | |
// If the URL is not the RESOURCE list then return to signal that the | |
// browser should take over. | |
if (!RESOURCES[key]) { | |
return; | |
} | |
// If the URL is the index.html, perform an online-first request. | |
if (key == '/') { | |
return onlineFirst(event); | |
} | |
event.respondWith(caches.open(CACHE_NAME) | |
.then((cache) => { | |
return cache.match(event.request).then((response) => { | |
// Either respond with the cached resource, or perform a fetch and | |
// lazily populate the cache. | |
return response || fetch(event.request).then((response) => { | |
cache.put(event.request, response.clone()); | |
return response; | |
}); | |
}) | |
}) | |
); | |
}); | |
self.addEventListener('message', (event) => { | |
// SkipWaiting can be used to immediately activate a waiting service worker. | |
// This will also require a page refresh triggered by the main worker. | |
if (event.data === 'skipWaiting') { | |
self.skipWaiting(); | |
return; | |
} | |
if (event.data === 'downloadOffline') { | |
downloadOffline(); | |
return; | |
} | |
}); | |
// Download offline will check the RESOURCES for all files not in the cache | |
// and populate them. | |
async function downloadOffline() { | |
var resources = []; | |
var contentCache = await caches.open(CACHE_NAME); | |
var currentContent = {}; | |
for (var request of await contentCache.keys()) { | |
var key = request.url.substring(origin.length + 1); | |
if (key == "") { | |
key = "/"; | |
} | |
currentContent[key] = true; | |
} | |
for (var resourceKey of Object.keys(RESOURCES)) { | |
if (!currentContent[resourceKey]) { | |
resources.push(resourceKey); | |
} | |
} | |
return contentCache.addAll(resources); | |
} | |
// Attempt to download the resource online before falling back to | |
// the offline cache. | |
function onlineFirst(event) { | |
return event.respondWith( | |
fetch(event.request).then((response) => { | |
return caches.open(CACHE_NAME).then((cache) => { | |
cache.put(event.request, response.clone()); | |
return response; | |
}); | |
}).catch((error) => { | |
return caches.open(CACHE_NAME).then((cache) => { | |
return cache.match(event.request).then((response) => { | |
if (response != null) { | |
return response; | |
} | |
throw error; | |
}); | |
}); | |
}) | |
); | |
} | |
'''; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment