blob: cbaa2a0f04e37546407c2919a93d19cd32724b08 [file] [log] [blame]
// 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:crypto/crypto.dart';
import '../../artifacts.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../build_info.dart';
import '../../compile.dart';
import '../../dart/package_map.dart';
import '../../globals.dart' as globals;
import '../build_system.dart';
import '../depfile.dart';
import 'assets.dart';
import 'dart.dart';
/// Whether web builds should call the platform initialization logic.
const String kInitializePlatform = 'InitializePlatform';
/// 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';
/// 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 shouldInitializePlatform = environment.defines[kInitializePlatform] == 'true';
final bool hasPlugins = environment.defines[kHasWebPlugins] == 'true';
final String importPath = globals.fs.path.absolute(targetFile);
// Use the package uri mapper 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.
final PackageUriMapper packageUriMapper = PackageUriMapper(
importPath,
PackageMap.globalPackagesPath,
null,
null,
);
// 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 = packageUriMapper.map(importPath)?.toString()
?? globals.fs.file(importPath).absolute.uri.toString();
String contents;
if (hasPlugins) {
final String generatedPath = environment.projectDir
.childDirectory('lib')
.childFile('generated_plugin_registrant.dart')
.absolute.path;
final String generatedImport = packageUriMapper.map(generatedPath)?.toString()
?? globals.fs.file(generatedPath).absolute.uri.toString();
contents = '''
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(webPluginRegistry);
if ($shouldInitializePlatform) {
await ui.webOnlyInitializePlatform();
}
entrypoint.main();
}
''';
} else {
contents = '''
import 'dart:ui' as ui;
import '$mainImport' as entrypoint;
Future<void> main() async {
if ($shouldInitializePlatform) {
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()
];
@override
List<Source> get inputs => const <Source>[
Source.artifact(Artifact.flutterWebSdk),
Source.artifact(Artifact.dart2jsSnapshot),
Source.artifact(Artifact.engineDartBinary),
Source.pattern('{BUILD_DIR}/main.dart'),
Source.pattern('{PROJECT_DIR}/.packages'),
];
@override
List<Source> get outputs => const <Source>[];
@override
List<String> get depfiles => const <String>[
'dart2js.d',
];
@override
Future<void> build(Environment environment) async {
final String dart2jsOptimization = environment.defines[kDart2jsOptimization];
final bool csp = environment.defines[kCspMode] == 'true';
final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
final String specPath = globals.fs.path.join(globals.artifacts.getArtifactPath(Artifact.flutterWebSdk), 'libraries.json');
final String packageFile = PackageMap.globalPackagesPath;
final File outputKernel = environment.buildDir.childFile('app.dill');
final File outputFile = environment.buildDir.childFile('main.dart.js');
final List<String> dartDefines = parseDartDefines(environment);
// 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>[
globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
globals.artifacts.getArtifactPath(Artifact.dart2jsSnapshot),
'--libraries-spec=$specPath',
'-o',
outputKernel.path,
for (final String dartDefine in dartDefines)
'-D$dartDefine',
'--packages=$packageFile',
'--cfe-only',
environment.buildDir.childFile('main.dart').path,
]);
if (kernelResult.exitCode != 0) {
throw Exception(kernelResult.stdout + kernelResult.stderr);
}
final ProcessResult javaScriptResult = await globals.processManager.run(<String>[
globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
globals.artifacts.getArtifactPath(Artifact.dart2jsSnapshot),
'--libraries-spec=$specPath',
if (dart2jsOptimization != null)
'-$dart2jsOptimization'
else
'-O4',
if (buildMode == BuildMode.profile)
'-Ddart.vm.profile=true'
else
'-Ddart.vm.product=true',
for (final String dartDefine in dartDefines)
'-D$dartDefine',
if (buildMode == BuildMode.profile)
'--no-minify',
if (csp)
'--csp',
'-o',
outputFile.path,
environment.buildDir.childFile('app.dill').path,
]);
if (javaScriptResult.exitCode != 0) {
throw Exception(javaScriptResult.stdout + javaScriptResult.stderr);
}
final File dart2jsDeps = environment.buildDir
.childFile('app.dill.deps');
if (!dart2jsDeps.existsSync()) {
globals.printError('Warning: dart2js did not produced expected deps list at '
'${dart2jsDeps.path}');
return;
}
final DepfileService depfileService = DepfileService(
fileSystem: globals.fs,
logger: globals.logger,
platform: globals.platform,
);
final Depfile depfile = depfileService.parseDart2js(
environment.buildDir.childFile('app.dill.deps'),
outputFile,
);
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 Directory outputDirectory = environment.outputDir.childDirectory('assets');
outputDirectory.createSync(recursive: true);
final Depfile depfile = await copyAssets(environment, environment.outputDir.childDirectory('assets'));
final DepfileService depfileService = DepfileService(
fileSystem: globals.fs,
logger: globals.logger,
platform: globals.platform,
);
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);
}
inputFile.copySync(outputFile.path);
outputResourcesFiles.add(outputFile);
}
final Depfile resourceFile = Depfile(inputResourceFiles, outputResourcesFiles);
depfileService.writeToFile(
resourceFile,
environment.buildDir.childFile('web_resources.d'),
);
}
}
/// Generate a service worker for a web target.
class WebServiceWorker extends Target {
const WebServiceWorker();
@override
String get name => 'web_service_worker';
@override
List<Target> get dependencies => const <Target>[
Dart2JSTarget(),
WebReleaseBundle(),
];
@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')) {
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 String serviceWorker = generateServiceWorker(urlToHash);
serviceWorkerFile
.writeAsStringSync(serviceWorker);
final DepfileService depfileService = DepfileService(
fileSystem: globals.fs,
logger: globals.logger,
platform: globals.platform,
);
depfileService.writeToFile(
depfile,
environment.buildDir.childFile('service_worker.d'),
);
}
}
/// Generate a service worker with an app-specific cache name a map of
/// resource files.
///
/// We embed file hashes directly into the worker so that the byte for byte
/// invalidation will automatically reactivate workers whenever a new
/// version is deployed.
// TODO(jonahwilliams): on re-activate, only evict stale assets.
String generateServiceWorker(Map<String, String> resources) {
return '''
'use strict';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")}
};
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheName) {
return caches.delete(cacheName);
}).then(function (_) {
return caches.open(CACHE_NAME);
}).then(function (cache) {
return cache.addAll(Object.keys(RESOURCES));
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
''';
}