blob: f7460076a102ad5b9bacac69742acce9766ec6fd [file] [log] [blame]
// Copyright 2019 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.
import 'dart:async';
import 'package:build_daemon/client.dart';
import 'package:build_daemon/constants.dart';
import 'package:build_daemon/constants.dart' hide BuildMode;
import 'package:build_daemon/constants.dart' as daemon show BuildMode;
import 'package:build_daemon/data/build_status.dart';
import 'package:build_daemon/data/build_target.dart';
import 'package:build_daemon/data/server_log.dart';
import 'package:dwds/dwds.dart';
import 'package:http_multi_server/http_multi_server.dart';
import 'package:meta/meta.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' hide StackTrace;
import '../artifacts.dart';
import '../asset.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../build_info.dart';
import '../bundle.dart';
import '../cache.dart';
import '../dart/package_map.dart';
import '../globals.dart';
import '../project.dart';
import '../web/chrome.dart';
/// The name of the built web project.
const String kBuildTargetName = 'web';
/// A factory for creating a [Dwds] instance.
DwdsFactory get dwdsFactpory => context.get<DwdsFactory>() ?? Dwds.start;
/// The [BuildDaemonCreator] instance.
BuildDaemonCreator get buildDaemonCreator => context.get<BuildDaemonCreator>() ?? const BuildDaemonCreator();
/// A factory for creating a [WebFs] instance.
WebFsFactory get webFsFactory => context.get<WebFsFactory>() ?? WebFs.start;
/// A factory for creating an [HttpMultiServer] instance.
HttpMultiServerFactory get httpMultiServerFactory => context.get<HttpMultiServerFactory>() ?? HttpMultiServer.bind;
/// A function with the same signature as [HttpMultiServier.bind].
typedef HttpMultiServerFactory = Future<HttpServer> Function(dynamic address, int port);
/// A function with the same signatire as [Dwds.start].
typedef DwdsFactory = Future<Dwds> Function({
@required int applicationPort,
@required int assetServerPort,
@required String applicationTarget,
@required Stream<BuildResult> buildResults,
@required ConnectionProvider chromeConnection,
String hostname,
ReloadConfiguration reloadConfiguration,
bool serveDevTools,
LogWriter logWriter,
bool verbose,
bool enableDebugExtension,
});
/// A function with the same signatuure as [WebFs.start].
typedef WebFsFactory = Future<WebFs> Function({
@required String target,
@required FlutterProject flutterProject,
@required BuildInfo buildInfo,
});
/// The dev filesystem responsible for building and serving web applications.
class WebFs {
@visibleForTesting
WebFs(
this._client,
this._server,
this._dwds,
this._chrome,
);
final HttpServer _server;
final Dwds _dwds;
final Chrome _chrome;
final BuildDaemonClient _client;
static const String _kHostName = 'localhost';
Future<void> stop() async {
await _client.close();
await _dwds.stop();
await _server.close(force: true);
await _chrome.close();
}
/// Retrieve the [DebugConnection] for the current application.
Future<DebugConnection> runAndDebug() async {
final AppConnection appConnection = await _dwds.connectedApps.first;
appConnection.runMain();
return _dwds.debugConnection(appConnection);
}
/// Perform a hard refresh of all connected browser tabs.
Future<void> hardRefresh() async {
final List<ChromeTab> tabs = await _chrome.chromeConnection.getTabs();
for (ChromeTab tab in tabs) {
if (!tab.url.contains('localhost')) {
continue;
}
final WipConnection connection = await tab.connect();
await connection.sendCommand('Page.reload');
}
}
/// Recompile the web application and return whether this was successful.
Future<bool> recompile() async {
_client.startBuild();
await for (BuildResults results in _client.buildResults) {
final BuildResult result = results.results.firstWhere((BuildResult result) {
return result.target == 'web';
});
if (result.status == BuildStatus.failed) {
return false;
}
if (result.status == BuildStatus.succeeded) {
return true;
}
}
return true;
}
/// Start the web compiler and asset server.
static Future<WebFs> start({
@required String target,
@required FlutterProject flutterProject,
@required BuildInfo buildInfo
}) async {
// Start the build daemon and run an initial build.
final BuildDaemonClient client = await buildDaemonCreator
.startBuildDaemon(fs.currentDirectory.path, release: buildInfo.isRelease, profile: buildInfo.isProfile);
client.startBuild();
// Only provide relevant build results
final Stream<BuildResult> filteredBuildResults = client.buildResults
.asyncMap<BuildResult>((BuildResults results) {
return results.results
.firstWhere((BuildResult result) => result.target == kBuildTargetName);
});
final int daemonAssetPort = buildDaemonCreator.assetServerPort(fs.currentDirectory);
// Initialize the asset bundle.
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
await assetBundle.build();
await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);
// Initialize the dwds server.
final int port = await os.findFreePort();
final Dwds dwds = await dwdsFactpory(
hostname: _kHostName,
applicationPort: port,
applicationTarget: kBuildTargetName,
assetServerPort: daemonAssetPort,
buildResults: filteredBuildResults,
chromeConnection: () async {
return (await ChromeLauncher.connectedInstance).chromeConnection;
},
reloadConfiguration: ReloadConfiguration.none,
serveDevTools: true,
verbose: false,
enableDebugExtension: true,
logWriter: (dynamic level, String message) => printTrace(message),
);
// Map the bootstrap files to the correct package directory.
final String targetBaseName = fs.path
.withoutExtension(target).replaceFirst('lib${fs.path.separator}', '');
final Map<String, String> mappedUrls = <String, String>{
'main.dart.js': 'packages/${flutterProject.manifest.appName}/'
'${targetBaseName}_web_entrypoint.dart.js',
'${targetBaseName}_web_entrypoint.dart.js.map': 'packages/${flutterProject.manifest.appName}/'
'${targetBaseName}_web_entrypoint.dart.js.map',
'${targetBaseName}_web_entrypoint.dart.bootstrap.js': 'packages/${flutterProject.manifest.appName}/'
'${targetBaseName}_web_entrypoint.dart.bootstrap.js',
'${targetBaseName}_web_entrypoint.digests': 'packages/${flutterProject.manifest.appName}/'
'${targetBaseName}_web_entrypoint.digests',
};
final Handler handler = const Pipeline().addMiddleware((Handler innerHandler) {
return (Request request) async {
// Redirect the main.dart.js to the target file we decided to serve.
if (mappedUrls.containsKey(request.url.path)) {
final String newPath = mappedUrls[request.url.path];
return innerHandler(
Request(
request.method,
Uri.parse(request.requestedUri.toString()
.replaceFirst(request.requestedUri.path, '/$newPath')),
headers: request.headers,
url: Uri.parse(request.url.toString()
.replaceFirst(request.url.path, newPath)),
),
);
} else {
return innerHandler(request);
}
};
})
.addHandler(dwds.handler);
Cascade cascade = Cascade();
cascade = cascade.add(handler);
cascade = cascade.add(_assetHandler(flutterProject));
final HttpServer server = await httpMultiServerFactory(_kHostName, port);
shelf_io.serveRequests(server, cascade.handler);
final Chrome chrome = await chromeLauncher.launch('http://$_kHostName:$port/');
return WebFs(
client,
server,
dwds,
chrome,
);
}
static Future<Response> Function(Request request) _assetHandler(FlutterProject flutterProject) {
final PackageMap packageMap = PackageMap(PackageMap.globalPackagesPath);
return (Request request) async {
if (request.url.path.contains('stack_trace_mapper')) {
final File file = fs.file(fs.path.join(
artifacts.getArtifactPath(Artifact.engineDartSdkPath),
'lib',
'dev_compiler',
'web',
'dart_stack_trace_mapper.js'
));
return Response.ok(file.readAsBytesSync(), headers: <String, String>{
'Content-Type': 'text/javascript',
});
} else if (request.url.path.contains('require.js')) {
final File file = fs.file(fs.path.join(
artifacts.getArtifactPath(Artifact.engineDartSdkPath),
'lib',
'dev_compiler',
'kernel',
'amd',
'require.js'
));
return Response.ok(file.readAsBytesSync(), headers: <String, String>{
'Content-Type': 'text/javascript',
});
} else if (request.url.path.contains('dart_sdk')) {
final File file = fs.file(fs.path.join(
artifacts.getArtifactPath(Artifact.flutterWebSdk),
'kernel',
'amd',
'dart_sdk.js',
));
return Response.ok(file.readAsBytesSync(), headers: <String, String>{
'Content-Type': 'text/javascript',
});
} else if (request.url.path.endsWith('.dart')) {
// This is likely a sourcemap request. The first segment is the
// package name, and the rest is the path to the file relative to
// the package uri. For example, `foo/bar.dart` would represent a
// file at a path like `foo/lib/bar.dart`. If there is no leading
// segment, then we assume it is from the current package.
final String packageName = request.url.pathSegments.length == 1
? flutterProject.manifest.appName
: request.url.pathSegments.first;
String filePath = fs.path.joinAll(request.url.pathSegments.length == 1
? request.url.pathSegments
: request.url.pathSegments.skip(1));
String packagePath = packageMap.map[packageName]?.toFilePath(windows: platform.isWindows);
// If the package isn't found, then we have an issue with relative
// paths within the main project.
if (packagePath == null) {
packagePath = packageMap.map[flutterProject.manifest.appName]
.toFilePath(windows: platform.isWindows);
filePath = request.url.path;
}
final File file = fs.file(fs.path.join(packagePath, filePath));
if (file.existsSync()) {
return Response.ok(file.readAsBytesSync());
}
return Response.notFound('');
} else if (request.url.path.contains('assets')) {
final String assetPath = request.url.path.replaceFirst('assets/', '');
final File file = fs.file(fs.path.join(getAssetBuildDirectory(), assetPath));
return Response.ok(file.readAsBytesSync());
}
return Response.notFound('');
};
}
}
/// A testable interface for starting a build daemon.
class BuildDaemonCreator {
const BuildDaemonCreator();
/// Start a build daemon and register the web targets.
Future<BuildDaemonClient> startBuildDaemon(String workingDirectory, {bool release = false, bool profile = false }) async {
try {
final BuildDaemonClient client = await _connectClient(
workingDirectory,
release: release,
profile: profile,
);
_registerBuildTargets(client);
return client;
} on OptionsSkew {
throwToolExit(
'Incompatible options with current running build daemon.\n\n'
'Please stop other flutter_tool instances running in this directory '
'before starting a new instance with these options.');
}
return null;
}
void _registerBuildTargets(
BuildDaemonClient client,
) {
final OutputLocation outputLocation = OutputLocation((OutputLocationBuilder b) => b
..output = ''
..useSymlinks = true
..hoist = false);
client.registerBuildTarget(DefaultBuildTarget((DefaultBuildTargetBuilder b) => b
..target = 'web'
..outputLocation = outputLocation?.toBuilder()));
}
Future<BuildDaemonClient> _connectClient(
String workingDirectory,
{ bool release, bool profile }
) {
final String flutterToolsPackages = fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools', '.packages');
final String buildScript = fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools', 'lib', 'src', 'build_runner', 'build_script.dart');
final String flutterWebSdk = artifacts.getArtifactPath(Artifact.flutterWebSdk);
return BuildDaemonClient.connect(
workingDirectory,
// On Windows we need to call the snapshot directly otherwise
// the process will start in a disjoint cmd without access to
// STDIO.
<String>[
artifacts.getArtifactPath(Artifact.engineDartBinary),
'--packages=$flutterToolsPackages',
buildScript,
'daemon',
'--skip-build-script-check',
'--define', 'flutter_tools:ddc=flutterWebSdk=$flutterWebSdk',
'--define', 'flutter_tools:entrypoint=flutterWebSdk=$flutterWebSdk',
'--define', 'flutter_tools:entrypoint=release=$release',
'--define', 'flutter_tools:entrypoint=profile=$profile',
'--define', 'flutter_tools:shell=flutterWebSdk=$flutterWebSdk',
],
logHandler: (ServerLog serverLog) {
switch (serverLog.level) {
case Level.SEVERE:
case Level.SHOUT:
// This message is always returned once since we're running the
// build script from source.
if (serverLog.message.contains('Warning: Interpreting this as package URI')) {
return;
}
printError(serverLog.message);
if (serverLog.error != null) {
printError(serverLog.error);
}
if (serverLog.stackTrace != null) {
printTrace(serverLog.stackTrace);
}
break;
default:
printTrace(serverLog.message);
}
},
buildMode: daemon.BuildMode.Manual,
);
}
/// Retrieve the asset server port for the current daemon.
int assetServerPort(Directory workingDirectory) {
final String portFilePath = fs.path.join(daemonWorkspace(workingDirectory.path), '.asset_server_port');
return int.tryParse(fs.file(portFilePath).readAsStringSync());
}
}