Dan Field c417c4623c
Refactor ShaderTarget to not explicitly mention impeller or Skia (#141460)
Refactors `ShaderTarget` to make it opaque as to whether it's using Impeller or SkSL and instead has it focus on the target platform it's generating for.

ImpellerC includes SkSL right now whether you ask for it or not. 

The tester target also might need SkSL or Vulkan depending on whether `--enable-impeller` is passed.
2024-01-31 21:30:02 +00:00

620 lines
22 KiB
Dart

// 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/process.dart';
import '../../build_info.dart';
import '../../cache.dart';
import '../../convert.dart';
import '../../dart/language_version.dart';
import '../../dart/package_map.dart';
import '../../flutter_plugins.dart';
import '../../globals.dart' as globals;
import '../../html_utils.dart';
import '../../project.dart';
import '../../web/compile.dart';
import '../../web/file_generators/flutter_service_worker_js.dart';
import '../../web/file_generators/main_dart.dart' as main_dart;
import '../../web/file_generators/wasm_bootstrap.dart' as wasm_bootstrap;
import '../build_system.dart';
import '../depfile.dart';
import '../exceptions.dart';
import 'assets.dart';
import 'localizations.dart';
/// Whether the application has web plugins.
const String kHasWebPlugins = 'HasWebPlugins';
/// Base href to set in index.html in flutter build command
const String kBaseHref = 'baseHref';
/// The caching strategy to use for service worker generation.
const String kServiceWorkerStrategy = 'ServiceWorkerStrategy';
/// 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 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 importedEntrypoint = packageConfig.toPackageUri(importUri)?.toString()
?? importUri.toString();
await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: environment.buildDir);
// The below works because `injectBuildTimePluginFiles` is configured to write
// the web_plugin_registrant.dart file alongside the generated main.dart
const String generatedImport = 'web_plugin_registrant.dart';
final String contents = main_dart.generateMainDartFile(importedEntrypoint,
languageVersion: languageVersion,
pluginRegistrantEntrypoint: generatedImport,
);
environment.buildDir.childFile('main.dart').writeAsStringSync(contents);
}
}
/// Compiles a web entry point with dart2js.
abstract class Dart2WebTarget extends Target {
const Dart2WebTarget(this.webRenderer);
final WebRendererMode webRenderer;
Source get compilerSnapshot;
@override
List<Target> get dependencies => const <Target>[
WebEntrypointTarget(),
GenerateLocalizationsTarget(),
];
@override
List<Source> get inputs => <Source>[
const Source.hostArtifact(HostArtifact.flutterWebSdk),
compilerSnapshot,
const Source.artifact(Artifact.engineDartBinary),
const Source.pattern('{BUILD_DIR}/main.dart'),
const Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'),
];
@override
List<Source> get outputs => const <Source>[];
}
class Dart2JSTarget extends Dart2WebTarget {
Dart2JSTarget(super.webRenderer);
@override
String get name => 'dart2js';
@override
Source get compilerSnapshot => const Source.artifact(Artifact.dart2jsSnapshot);
@override
List<String> get depfiles => const <String>[
'dart2js.d',
];
@override
Future<void> build(Environment environment) async {
final String? buildModeEnvironment = environment.defines[kBuildMode];
if (buildModeEnvironment == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment);
final JsCompilerConfig compilerConfig = JsCompilerConfig.fromBuildSystemEnvironment(environment.defines);
final Artifacts artifacts = environment.artifacts;
final String platformBinariesPath = getWebPlatformBinariesDirectory(artifacts, webRenderer).path;
final List<String> sharedCommandOptions = <String>[
artifacts.getArtifactPath(Artifact.engineDartBinary, platform: TargetPlatform.web_javascript),
'--disable-dart-dev',
artifacts.getArtifactPath(Artifact.dart2jsSnapshot, platform: TargetPlatform.web_javascript),
'--platform-binaries=$platformBinariesPath',
'--invoker=flutter_tool',
...decodeCommaSeparated(environment.defines, kExtraFrontEndOptions),
if (buildMode == BuildMode.profile)
'-Ddart.vm.profile=true'
else
'-Ddart.vm.product=true',
for (final String dartDefine in decodeDartDefines(environment.defines, kDartDefines))
'-D$dartDefine',
];
final List<String> compilationArgs = <String>[
...sharedCommandOptions,
...compilerConfig.toSharedCommandOptions(),
'-o',
environment.buildDir.childFile('app.dill').path,
'--packages=.dart_tool/package_config.json',
'--cfe-only',
environment.buildDir.childFile('main.dart').path, // dartfile
];
final ProcessUtils processUtils = ProcessUtils(
logger: environment.logger,
processManager: environment.processManager,
);
// Run the dart2js compilation in two stages, so that icon tree shaking can
// parse the kernel file for web builds.
await processUtils.run(compilationArgs, throwOnError: true);
final File outputJSFile = environment.buildDir.childFile('main.dart.js');
await processUtils.run(
throwOnError: true,
<String>[
...sharedCommandOptions,
if (buildMode == BuildMode.profile) '--no-minify',
...compilerConfig.toCommandOptions(),
'-o',
outputJSFile.path,
environment.buildDir.childFile('app.dill').path, // dartfile
],
);
final File dart2jsDeps = environment.buildDir.childFile('app.dill.deps');
if (!dart2jsDeps.existsSync()) {
environment.logger.printWarning(
'Warning: dart2js did not produce expected deps list at '
'${dart2jsDeps.path}',
);
return;
}
final DepfileService depFileService = environment.depFileService;
final Depfile depFile = depFileService.parseDart2js(
environment.buildDir.childFile('app.dill.deps'),
outputJSFile,
);
depFileService.writeToFile(
depFile,
environment.buildDir.childFile('dart2js.d'),
);
}
}
class Dart2WasmTarget extends Dart2WebTarget {
Dart2WasmTarget(super.webRenderer);
@override
Future<void> build(Environment environment) async {
final String? buildModeEnvironment = environment.defines[kBuildMode];
if (buildModeEnvironment == null) {
throw MissingDefineException(kBuildMode, name);
}
final WasmCompilerConfig compilerConfig = WasmCompilerConfig.fromBuildSystemEnvironment(environment.defines);
final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment);
final Artifacts artifacts = environment.artifacts;
final File outputWasmFile = environment.buildDir.childFile(
compilerConfig.runWasmOpt ? 'main.dart.unopt.wasm' : 'main.dart.wasm'
);
final File depFile = environment.buildDir.childFile('dart2wasm.d');
final String dartSdkPath = artifacts.getArtifactPath(Artifact.engineDartSdkPath, platform: TargetPlatform.web_javascript);
final String platformBinariesPath = getWebPlatformBinariesDirectory(artifacts, webRenderer).path;
final String platformFilePath = environment.fileSystem.path.join(platformBinariesPath, 'dart2wasm_platform.dill');
final List<String> compilationArgs = <String>[
artifacts.getArtifactPath(Artifact.engineDartAotRuntime, platform: TargetPlatform.web_javascript),
'--disable-dart-dev',
artifacts.getArtifactPath(Artifact.dart2wasmSnapshot, platform: TargetPlatform.web_javascript),
'--packages=.dart_tool/package_config.json',
'--dart-sdk=$dartSdkPath',
'--platform=$platformFilePath',
if (buildMode == BuildMode.profile)
'-Ddart.vm.profile=true'
else
'-Ddart.vm.product=true',
...decodeCommaSeparated(environment.defines, kExtraFrontEndOptions),
for (final String dartDefine in decodeDartDefines(environment.defines, kDartDefines))
'-D$dartDefine',
...compilerConfig.toCommandOptions(),
if (webRenderer == WebRendererMode.skwasm)
...<String>[
'--import-shared-memory',
'--shared-memory-max-pages=32768',
],
'--depfile=${depFile.path}',
environment.buildDir.childFile('main.dart').path, // dartfile
outputWasmFile.path,
];
final ProcessUtils processUtils = ProcessUtils(
logger: environment.logger,
processManager: environment.processManager,
);
await processUtils.run(
throwOnError: true,
compilationArgs,
);
if (compilerConfig.runWasmOpt) {
final String wasmOptBinary = artifacts.getArtifactPath(
Artifact.wasmOptBinary,
platform: TargetPlatform.web_javascript
);
final File optimizedOutput = environment.buildDir.childFile('main.dart.wasm');
final List<String> optimizeArgs = <String>[
wasmOptBinary,
'--all-features',
'--closed-world',
'--traps-never-happen',
'-O3',
'--type-ssa',
'--gufa',
'-O3',
'--type-merging',
if (compilerConfig.wasmOpt == WasmOptLevel.debug)
'--debuginfo',
outputWasmFile.path,
'-o',
optimizedOutput.path,
];
await processUtils.run(
throwOnError: true,
optimizeArgs,
);
// Rename the .mjs file not to have the `.unopt` bit
final File jsRuntimeFile = environment.buildDir.childFile('main.dart.unopt.mjs');
await jsRuntimeFile.rename(environment.buildDir.childFile('main.dart.mjs').path);
}
}
@override
Source get compilerSnapshot => const Source.artifact(Artifact.dart2wasmSnapshot);
@override
String get name => 'dart2wasm';
@override
List<String> get depfiles => const <String>[
'dart2wasm.d',
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/main.dart.wasm'),
Source.pattern('{OUTPUT_DIR}/main.dart.mjs'),
];
}
/// Unpacks the dart2js or dart2wasm compilation and resources to a given
/// output directory.
class WebReleaseBundle extends Target {
const WebReleaseBundle(this.webRenderer, {required this.isWasm});
final WebRendererMode webRenderer;
final bool isWasm;
String get outputFileNameNoSuffix => 'main.dart';
String get outputFileName => '$outputFileNameNoSuffix${isWasm ? '.wasm' : '.js'}';
String get wasmJSRuntimeFileName => '$outputFileNameNoSuffix.mjs';
@override
String get name => 'web_release_bundle';
@override
List<Target> get dependencies => <Target>[
if (isWasm) Dart2WasmTarget(webRenderer) else Dart2JSTarget(webRenderer),
];
@override
List<Source> get inputs => <Source>[
Source.pattern('{BUILD_DIR}/$outputFileName'),
const Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
if (isWasm) Source.pattern('{BUILD_DIR}/$wasmJSRuntimeFileName'),
];
@override
List<Source> get outputs => <Source>[
Source.pattern('{OUTPUT_DIR}/$outputFileName'),
if (isWasm) Source.pattern('{OUTPUT_DIR}/$wasmJSRuntimeFileName'),
];
@override
List<String> get depfiles => const <String>[
'dart2js.d',
'flutter_assets.d',
'web_resources.d',
];
bool shouldCopy(String name) =>
// Do not copy the deps file.
(name.contains(outputFileName) && !name.endsWith('.deps')) ||
(isWasm && name == wasmJSRuntimeFileName);
@override
Future<void> build(Environment environment) async {
for (final File outputFile in environment.buildDir.listSync(recursive: true).whereType<File>()) {
final String basename = environment.fileSystem.path.basename(outputFile.path);
if (shouldCopy(basename)) {
outputFile.copySync(
environment.outputDir.childFile(environment.fileSystem.path.basename(outputFile.path)).path
);
}
}
if (isWasm) {
// TODO(jacksongardner): Enable icon tree shaking once dart2wasm can do a two-phase compile.
// https://github.com/flutter/flutter/issues/117248
environment.defines[kIconTreeShakerFlag] = 'false';
}
createVersionFile(environment, environment.defines);
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 = environment.depFileService;
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 = environment.fileSystem.file(environment.fileSystem.path.join(
environment.outputDir.path,
environment.fileSystem.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 IndexHtml indexHtml = IndexHtml(inputFile.readAsStringSync());
indexHtml.applySubstitutions(
baseHref: environment.defines[kBaseHref] ?? '/',
serviceWorkerVersion: Random().nextInt(4294967296).toString(),
);
outputFile.writeAsStringSync(indexHtml.content);
continue;
}
inputFile.copySync(outputFile.path);
}
final Depfile resourceFile = Depfile(inputResourceFiles, outputResourcesFiles);
depfileService.writeToFile(
resourceFile,
environment.buildDir.childFile('web_resources.d'),
);
}
/// Create version.json file that contains data about version for package_info
void createVersionFile(Environment environment, Map<String, String> defines) {
final Map<String, dynamic> versionInfo =
jsonDecode(FlutterProject.current().getVersionInfo())
as Map<String, dynamic>;
if (defines.containsKey(kBuildNumber)) {
versionInfo['build_number'] = defines[kBuildNumber];
}
if (defines.containsKey(kBuildName)) {
versionInfo['version'] = defines[kBuildName];
}
environment.outputDir
.childFile('version.json')
.writeAsStringSync(jsonEncode(versionInfo));
}
}
/// Static assets provided by the Flutter SDK that do not change, such as
/// CanvasKit.
///
/// These assets can be cached until a new version of the flutter web sdk is
/// downloaded.
class WebBuiltInAssets extends Target {
const WebBuiltInAssets(this.fileSystem, this.webRenderer, {required this.isWasm});
final FileSystem fileSystem;
final WebRendererMode webRenderer;
final bool isWasm;
@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>[
Source.hostArtifact(HostArtifact.flutterWebSdk),
];
Directory get _canvasKitDirectory =>
globals.fs.directory(
fileSystem.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterWebSdk).path,
'canvaskit',
)
);
List<File> get _canvasKitFiles => _canvasKitDirectory.listSync(recursive: true).whereType<File>().toList();
String _filePathRelativeToCanvasKitDirectory(File file) =>
fileSystem.path.relative(file.path, from: _canvasKitDirectory.path);
@override
List<Source> get outputs => <Source>[
if (isWasm) const Source.pattern('{BUILD_DIR}/main.dart.js'),
const Source.pattern('{BUILD_DIR}/flutter.js'),
for (final File file in _canvasKitFiles)
Source.pattern('{BUILD_DIR}/canvaskit/${_filePathRelativeToCanvasKitDirectory(file)}'),
];
@override
Future<void> build(Environment environment) async {
for (final File file in _canvasKitFiles) {
final String relativePath = _filePathRelativeToCanvasKitDirectory(file);
final String targetPath = fileSystem.path.join(environment.outputDir.path, 'canvaskit', relativePath);
file.copySync(targetPath);
}
if (isWasm) {
final File bootstrapFile = environment.outputDir.childFile('main.dart.js');
bootstrapFile.writeAsStringSync(
wasm_bootstrap.generateWasmBootstrapFile(webRenderer == WebRendererMode.skwasm)
);
}
// Write the flutter.js file
final String flutterJsOut = fileSystem.path.join(environment.outputDir.path, 'flutter.js');
final File flutterJsFile = fileSystem.file(fileSystem.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
));
flutterJsFile.copySync(flutterJsOut);
}
}
/// Generate a service worker for a web target.
class WebServiceWorker extends Target {
const WebServiceWorker(this.fileSystem, this.webRenderer, {required this.isWasm});
final FileSystem fileSystem;
final WebRendererMode webRenderer;
final bool isWasm;
@override
String get name => 'web_service_worker';
@override
List<Target> get dependencies => <Target>[
if (isWasm) Dart2WasmTarget(webRenderer) else Dart2JSTarget(webRenderer),
WebReleaseBundle(webRenderer, isWasm: isWasm),
WebBuiltInAssets(fileSystem, webRenderer, isWasm: isWasm),
];
@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')
&& !environment.fileSystem.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 = environment.fileSystem.path.toUri(
environment.fileSystem.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 (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 =
ServiceWorkerStrategy.fromCliName(environment.defines[kServiceWorkerStrategy]);
final String fileGeneratorsPath =
environment.artifacts.getArtifactPath(Artifact.flutterToolsFileGenerators);
final String serviceWorker = generateServiceWorker(
fileGeneratorsPath,
urlToHash,
<String>[
'main.dart.js',
'index.html',
if (urlToHash.containsKey('assets/AssetManifest.bin.json'))
'assets/AssetManifest.bin.json',
if (urlToHash.containsKey('assets/FontManifest.json'))
'assets/FontManifest.json',
],
serviceWorkerStrategy: serviceWorkerStrategy,
);
serviceWorkerFile
.writeAsStringSync(serviceWorker);
environment.depFileService.writeToFile(
depfile,
environment.buildDir.childFile('service_worker.d'),
);
}
}