From ccc3dd968ca754172ca700d140a1fcfd7995931a Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Mon, 30 Sep 2019 18:26:28 -0700 Subject: [PATCH] Switch to assemble API for dart2js (#41447) --- dev/bots/test.dart | 3 +- .../lib/src/build_runner/build_script.dart | 125 +--------- .../src/build_runner/resident_web_runner.dart | 19 +- .../lib/src/build_runner/web_fs.dart | 191 ++++++++++----- .../lib/src/build_system/build_system.dart | 7 + .../lib/src/build_system/file_hash_store.dart | 31 ++- .../lib/src/build_system/targets/assets.dart | 10 +- .../lib/src/build_system/targets/dart.dart | 40 ++-- .../lib/src/build_system/targets/macos.dart | 2 +- .../lib/src/build_system/targets/web.dart | 203 ++++++++++++++++ .../lib/src/commands/assemble.dart | 2 + .../flutter_tools/lib/src/web/compile.dart | 84 +++---- .../build_system/filecache_test.dart | 16 +- .../build_system/targets/web_test.dart | 221 ++++++++++++++++++ .../commands/build_web_test.dart | 42 ---- .../general.shard/web/asset_server_test.dart | 2 +- 16 files changed, 667 insertions(+), 331 deletions(-) create mode 100644 packages/flutter_tools/lib/src/build_system/targets/web.dart create mode 100644 packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart diff --git a/dev/bots/test.dart b/dev/bots/test.dart index e37521359d3..9bed4d58789 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -282,10 +282,9 @@ Future _runBuildTests() async { } // Web compilation tests. await _flutterBuildDart2js(path.join('dev', 'integration_tests', 'web'), path.join('lib', 'main.dart')); - // Should fail to compile with dart:io. + // Should not fail to compile with dart:io. await _flutterBuildDart2js(path.join('dev', 'integration_tests', 'web_compile_tests'), path.join('lib', 'dart_io_import.dart'), - expectNonZeroExit: true, ); print('${bold}DONE: All build tests successful.$reset'); diff --git a/packages/flutter_tools/lib/src/build_runner/build_script.dart b/packages/flutter_tools/lib/src/build_runner/build_script.dart index 0638b8f1e38..69d0569b1c8 100644 --- a/packages/flutter_tools/lib/src/build_runner/build_script.dart +++ b/packages/flutter_tools/lib/src/build_runner/build_script.dart @@ -4,21 +4,17 @@ // ignore_for_file: implementation_imports import 'dart:async'; -import 'dart:convert'; // ignore: dart_convert_import -import 'dart:io'; // ignore: dart_io_import import 'dart:isolate'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:archive/archive.dart'; import 'package:build/build.dart'; import 'package:build_config/build_config.dart'; import 'package:build_modules/build_modules.dart'; import 'package:build_modules/builders.dart'; import 'package:build_modules/src/module_builder.dart'; import 'package:build_modules/src/platform.dart'; -import 'package:build_modules/src/workers.dart'; import 'package:build_runner/build_runner.dart' as build_runner; import 'package:build_runner_core/build_runner_core.dart' as core; import 'package:build_test/builder.dart'; @@ -26,10 +22,7 @@ import 'package:build_test/src/debug_test_builder.dart'; import 'package:build_web_compilers/build_web_compilers.dart'; import 'package:build_web_compilers/builders.dart'; import 'package:build_web_compilers/src/dev_compiler_bootstrap.dart'; -import 'package:crypto/crypto.dart'; -import 'package:glob/glob.dart'; import 'package:path/path.dart' as path; // ignore: package_path_import -import 'package:scratch_space/scratch_space.dart'; import 'package:test_core/backend.dart'; const String ddcBootstrapExtension = '.dart.bootstrap.js'; @@ -248,12 +241,8 @@ class FlutterWebEntrypointBuilder implements Builder { @override Future build(BuildStep buildStep) async { - if (release || profile) { - await bootstrapDart2Js(buildStep, flutterWebSdk, profile); - } else { - await bootstrapDdc(buildStep, platform: flutterWebPlatform, - skipPlatformCheckPackages: skipPlatformCheckPackages); - } + await bootstrapDdc(buildStep, platform: flutterWebPlatform, + skipPlatformCheckPackages: skipPlatformCheckPackages); } } @@ -422,116 +411,6 @@ Future main() async { }; } -Future bootstrapDart2Js(BuildStep buildStep, String flutterWebSdk, bool profile) async { - final AssetId dartEntrypointId = buildStep.inputId; - final AssetId moduleId = dartEntrypointId.changeExtension(moduleExtension(flutterWebPlatform)); - final Module module = Module.fromJson(json.decode(await buildStep.readAsString(moduleId))); - final List allDeps = await module.computeTransitiveDependencies( - buildStep, - throwIfUnsupported: true, - skipPlatformCheckPackages: skipPlatformCheckPackages, - )..add(module); - final ScratchSpace scratchSpace = await buildStep.fetchResource(scratchSpaceResource); - final Iterable allSrcs = allDeps.expand((Module module) => module.sources); - await scratchSpace.ensureAssets(allSrcs, buildStep); - - final String packageFile = _createPackageFile(allSrcs, buildStep, scratchSpace); - final String dartPath = dartEntrypointId.path.startsWith('lib/') - ? 'package:${dartEntrypointId.package}/' - '${dartEntrypointId.path.substring('lib/'.length)}' - : dartEntrypointId.path; - final String jsOutputPath = - '${path.withoutExtension(dartPath.replaceFirst('package:', 'packages/'))}' - '$jsEntrypointExtension'; - final String flutterWebSdkPath = flutterWebSdk; - final String librariesPath = path.join(flutterWebSdkPath, 'libraries.json'); - final List args = [ - '--libraries-spec="$librariesPath"', - if (profile) - '-O1' - else - '-O4', - '-o', - '$jsOutputPath', - '--packages="$packageFile"', - if (profile) - '-Ddart.vm.profile=true' - else - '-Ddart.vm.product=true', - dartPath, - ]; - final Dart2JsBatchWorkerPool dart2js = await buildStep.fetchResource(dart2JsWorkerResource); - final Dart2JsResult result = await dart2js.compile(args); - final AssetId jsOutputId = dartEntrypointId.changeExtension(jsEntrypointExtension); - final File jsOutputFile = scratchSpace.fileFor(jsOutputId); - if (result.succeeded && jsOutputFile.existsSync()) { - final String rootDir = path.dirname(jsOutputFile.path); - final String dartFile = path.basename(dartEntrypointId.path); - final Glob fileGlob = Glob('$dartFile.js*'); - final Archive archive = Archive(); - await for (FileSystemEntity jsFile in fileGlob.list(root: rootDir)) { - if (jsFile.path.endsWith(jsEntrypointExtension) || - jsFile.path.endsWith(jsEntrypointSourceMapExtension)) { - // These are explicitly output, and are not part of the archive. - continue; - } - if (jsFile is File) { - final String fileName = path.relative(jsFile.path, from: rootDir); - final FileStat fileStats = jsFile.statSync(); - archive.addFile( - ArchiveFile(fileName, fileStats.size, await jsFile.readAsBytes()) - ..mode = fileStats.mode - ..lastModTime = fileStats.modified.millisecondsSinceEpoch); - } - } - if (archive.isNotEmpty) { - final AssetId archiveId = dartEntrypointId.changeExtension(jsEntrypointArchiveExtension); - await buildStep.writeAsBytes(archiveId, TarEncoder().encode(archive)); - } - - // Explicitly write out the original js file and sourcemap - we can't output - // these as part of the archive because they already have asset nodes. - await scratchSpace.copyOutput(jsOutputId, buildStep); - final AssetId jsSourceMapId = - dartEntrypointId.changeExtension(jsEntrypointSourceMapExtension); - await _copyIfExists(jsSourceMapId, scratchSpace, buildStep); - } else { - log.severe(result.output); - } -} - -Future _copyIfExists( - AssetId id, ScratchSpace scratchSpace, AssetWriter writer) async { - final File file = scratchSpace.fileFor(id); - if (file.existsSync()) { - await scratchSpace.copyOutput(id, writer); - } -} - -/// Creates a `.packages` file unique to this entrypoint at the root of the -/// scratch space and returns it's filename. -/// -/// Since mulitple invocations of Dart2Js will share a scratch space and we only -/// know the set of packages involved the current entrypoint we can't construct -/// a `.packages` file that will work for all invocations of Dart2Js so a unique -/// file is created for every entrypoint that is run. -/// -/// The filename is based off the MD5 hash of the asset path so that files are -/// unique regarless of situations like `web/foo/bar.dart` vs -/// `web/foo-bar.dart`. -String _createPackageFile(Iterable inputSources, BuildStep buildStep, ScratchSpace scratchSpace) { - final Uri inputUri = buildStep.inputId.uri; - final String packageFileName = - '.package-${md5.convert(inputUri.toString().codeUnits)}'; - final File packagesFile = - scratchSpace.fileFor(AssetId(buildStep.inputId.package, packageFileName)); - final Set packageNames = inputSources.map((AssetId s) => s.package).toSet(); - final String packagesFileContent = - packageNames.map((String name) => '$name:packages/$name/').join('\n'); - packagesFile .writeAsStringSync('# Generated for $inputUri\n$packagesFileContent'); - return packageFileName; -} - /// Returns whether or not [dartId] is an app entrypoint (basically, whether /// or not it has a `main` function). Future _isAppEntryPoint(AssetId dartId, AssetReader reader) async { diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart index 2ed586b4b12..f4d1f940b14 100644 --- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:vm_service/vm_service.dart' as vmservice; +import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; import '../application_package.dart'; import '../base/common.dart'; @@ -20,6 +21,7 @@ import '../globals.dart'; import '../project.dart'; import '../reporting/reporting.dart'; import '../resident_runner.dart'; +import '../web/chrome.dart'; import '../web/web_device.dart'; import '../web/web_runner.dart'; import 'web_fs.dart'; @@ -170,7 +172,7 @@ class ResidentWebRunner extends ResidentRunner { initializePlatform: debuggingOptions.initializePlatform, hostname: debuggingOptions.hostname, port: debuggingOptions.port, - skipDwds: device is WebServerDevice, + skipDwds: device is WebServerDevice || !debuggingOptions.buildInfo.isDebug, ); // When connecting to a browser, update the message with a seemsSlow notification // to handle the case where we fail to connect. @@ -291,6 +293,21 @@ class ResidentWebRunner extends ResidentRunner { ).send(); } } + // Allows browser refresh hot restart on non-debug builds. + if (device is ChromeDevice && debuggingOptions.browserLaunch) { + try { + final Chrome chrome = await ChromeLauncher.connectedInstance; + final ChromeTab chromeTab = await chrome.chromeConnection.getTab((ChromeTab chromeTab) { + return chromeTab.url.contains(debuggingOptions.hostname); + }); + final WipConnection wipConnection = await chromeTab.connect(); + await wipConnection.sendCommand('Page.reload'); + status.stop(); + return OperationResult.ok; + } catch (err) { + // Ignore error and continue with posted message; + } + } status.stop(); printStatus('Recompile complete. Page requires refresh.'); return OperationResult.ok; diff --git a/packages/flutter_tools/lib/src/build_runner/web_fs.dart b/packages/flutter_tools/lib/src/build_runner/web_fs.dart index 456cf077a0d..c2cca4d0824 100644 --- a/packages/flutter_tools/lib/src/build_runner/web_fs.dart +++ b/packages/flutter_tools/lib/src/build_runner/web_fs.dart @@ -35,6 +35,7 @@ import '../platform_plugins.dart'; import '../plugins.dart'; import '../project.dart'; import '../web/chrome.dart'; +import '../web/compile.dart'; /// The name of the built web project. const String kBuildTargetName = 'web'; @@ -89,6 +90,11 @@ class WebFs { this._dwds, this.uri, this._assetServer, + this._useBuildRunner, + this._flutterProject, + this._target, + this._buildInfo, + this._initializePlatform, ); /// The server uri. @@ -98,12 +104,17 @@ class WebFs { final Dwds _dwds; final BuildDaemonClient _client; final AssetServer _assetServer; + final bool _useBuildRunner; + final FlutterProject _flutterProject; + final String _target; + final BuildInfo _buildInfo; + final bool _initializePlatform; StreamSubscription _connectedApps; static const String _kHostName = 'localhost'; Future stop() async { - await _client.close(); + await _client?.close(); await _dwds?.stop(); await _server.close(force: true); await _connectedApps?.cancel(); @@ -132,6 +143,10 @@ class WebFs { /// Recompile the web application and return whether this was successful. Future recompile() async { + if (!_useBuildRunner) { + await buildWeb(_flutterProject, _target, _buildInfo, _initializePlatform); + return true; + } _client.startBuild(); await for (BuildResults results in _client.buildResults) { final BuildResult result = results.results.firstWhere((BuildResult result) { @@ -161,39 +176,7 @@ class WebFs { if (!flutterProject.dartTool.existsSync()) { flutterProject.dartTool.createSync(recursive: true); } - final bool hasWebPlugins = findPlugins(flutterProject) - .any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey)); - // Start the build daemon and run an initial build. - final Completer inititalBuild = Completer(); - final BuildDaemonClient client = await buildDaemonCreator - .startBuildDaemon(fs.currentDirectory.path, - release: buildInfo.isRelease, - profile: buildInfo.isProfile, - hasPlugins: hasWebPlugins, - initializePlatform: initializePlatform, - ); - client.startBuild(); - // Only provide relevant build results - final Stream filteredBuildResults = client.buildResults - .asyncMap((BuildResults results) { - return results.results - .firstWhere((BuildResult result) => result.target == kBuildTargetName); - }); - final StreamSubscription firstBuild = client.buildResults.listen((BuildResults buildResults) { - if (inititalBuild.isCompleted) { - return; - } - final BuildResult result = buildResults.results.firstWhere((BuildResult result) { - return result.target == kBuildTargetName; - }); - if (result.status == BuildStatus.failed) { - inititalBuild.complete(false); - } - if (result.status == BuildStatus.succeeded) { - inititalBuild.complete(true); - } - }); - final int daemonAssetPort = buildDaemonCreator.assetServerPort(fs.currentDirectory); + final Completer firstBuildCompleter = Completer(); // Initialize the asset bundle. final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); @@ -201,7 +184,7 @@ class WebFs { await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries); final String targetBaseName = fs.path - .withoutExtension(target).replaceFirst('lib${fs.path.separator}', ''); + .withoutExtension(target).replaceFirst('lib${fs.path.separator}', ''); final Map mappedUrls = { 'main.dart.js': 'packages/${flutterProject.manifest.appName}/' '${targetBaseName}_web_entrypoint.dart.js', @@ -236,29 +219,78 @@ class WebFs { } }; }); + Handler handler; Dwds dwds; - if (!skipDwds) { - dwds = await dwdsFactory( - hostname: hostname ?? _kHostName, - applicationPort: hostPort, - 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), - ); - handler = pipeline.addHandler(dwds.handler); + BuildDaemonClient client; + StreamSubscription firstBuild; + if (buildInfo.isDebug) { + final bool hasWebPlugins = findPlugins(flutterProject) + .any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey)); + // Start the build daemon and run an initial build. + client = await buildDaemonCreator + .startBuildDaemon(fs.currentDirectory.path, + release: buildInfo.isRelease, + profile: buildInfo.isProfile, + hasPlugins: hasWebPlugins, + initializePlatform: initializePlatform, + ); + client.startBuild(); + // Only provide relevant build results + final Stream filteredBuildResults = client.buildResults + .asyncMap((BuildResults results) { + return results.results + .firstWhere((BuildResult result) => result.target == kBuildTargetName); + }); + // Start the build daemon and run an initial build. + firstBuild = client.buildResults.listen((BuildResults buildResults) { + if (firstBuildCompleter.isCompleted) { + return; + } + final BuildResult result = buildResults.results.firstWhere((BuildResult result) { + return result.target == kBuildTargetName; + }); + if (result.status == BuildStatus.failed) { + firstBuildCompleter.complete(false); + } + if (result.status == BuildStatus.succeeded) { + firstBuildCompleter.complete(true); + } + }); + 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); + if (!skipDwds) { + dwds = await dwdsFactory( + hostname: hostname ?? _kHostName, + applicationPort: hostPort, + 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), + ); + handler = pipeline.addHandler(dwds.handler); + } else { + handler = pipeline.addHandler(proxyHandler('http://localhost:$daemonAssetPort/web/')); + } } else { - handler = pipeline.addHandler(proxyHandler('http://localhost:$daemonAssetPort/web/')); + await buildWeb(flutterProject, target, buildInfo, initializePlatform); + firstBuildCompleter.complete(true); } - final AssetServer assetServer = AssetServer(flutterProject, targetBaseName); + + final AssetServer assetServer = buildInfo.isDebug + ? DebugAssetServer(flutterProject, targetBaseName) + : ReleaseAssetServer(); Cascade cascade = Cascade(); cascade = cascade.add(handler); cascade = cascade.add(assetServer.handle); @@ -270,23 +302,65 @@ class WebFs { dwds, 'http://$_kHostName:$hostPort/', assetServer, + buildInfo.isDebug, + flutterProject, + target, + buildInfo, + initializePlatform, ); - if (!await inititalBuild.future) { + if (!await firstBuildCompleter.future) { throw Exception('Failed to compile for the web.'); } - await firstBuild.cancel(); + await firstBuild?.cancel(); return webFS; } } -class AssetServer { - AssetServer(this.flutterProject, this.targetBaseName); +abstract class AssetServer { + Future handle(Request request); + + void dispose() {} +} + +class ReleaseAssetServer extends AssetServer { + @override + Future handle(Request request) async { + final Uri artifactUri = fs.directory(getWebBuildDirectory()).uri.resolveUri(request.url); + final File file = fs.file(artifactUri); + if (file.existsSync()) { + return Response.ok(file.readAsBytesSync(), headers: { + 'Content-Type': _guessExtension(file), + }); + } + if (request.url.path == '') { + final File file = fs.file(fs.path.join(getWebBuildDirectory(), 'index.html')); + return Response.ok(file.readAsBytesSync(), headers: { + 'Content-Type': _guessExtension(file), + }); + } + return Response.notFound(''); + } + + String _guessExtension(File file) { + switch (fs.path.extension(file.path)) { + case '.js': + return 'text/javascript'; + case '.html': + return 'text/html'; + } + return 'text'; + } +} + +class DebugAssetServer extends AssetServer { + DebugAssetServer(this.flutterProject, this.targetBaseName); final FlutterProject flutterProject; final String targetBaseName; final PackageMap packageMap = PackageMap(PackageMap.globalPackagesPath); Directory partFiles; + @override Future handle(Request request) async { if (request.url.path.endsWith('.html')) { final Uri htmlUri = flutterProject.web.directory.uri.resolveUri(request.url); @@ -411,6 +485,7 @@ class AssetServer { return Response.notFound(''); } + @override void dispose() { partFiles?.deleteSync(recursive: true); } diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart index b1efeff6c5b..e2bdbb8ccdc 100644 --- a/packages/flutter_tools/lib/src/build_system/build_system.dart +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -25,6 +25,13 @@ export 'source.dart'; /// The [BuildSystem] instance. BuildSystem get buildSystem => context.get(); +/// A reasonable amount of files to open at the same time. +/// +/// This number is somewhat arbitrary - it is difficult to detect whether +/// or not we'll run out of file descriptiors when using async dart:io +/// APIs. +const int kMaxOpenFiles = 64; + /// Configuration for the build system itself. class BuildSystemConfig { /// Create a new [BuildSystemConfig]. diff --git a/packages/flutter_tools/lib/src/build_system/file_hash_store.dart b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart index b43c31188d9..ef54b119623 100644 --- a/packages/flutter_tools/lib/src/build_system/file_hash_store.dart +++ b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart @@ -7,6 +7,7 @@ import 'dart:collection'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; +import 'package:pool/pool.dart'; import '../base/file_system.dart'; import '../convert.dart'; @@ -69,13 +70,6 @@ class FileHash { /// /// The format of the file store is subject to change and not part of its API. /// -/// To regenerate the protobuf entries used to construct the cache: -/// 1. If not already installed, https://developers.google.com/protocol-buffers/docs/downloads -/// 2. pub global active `protoc-gen-dart` -/// 3. protoc -I=lib/src/build_system/ --dart_out=lib/src/build_system/ lib/src/build_system/filecache.proto -/// 4. Add licenses headers to the newly generated file and check-in. -/// -/// See also: https://developers.google.com/protocol-buffers/docs/darttutorial // TODO(jonahwilliams): find a better way to clear out old entries, perhaps // track the last access or modification date? class FileHashStore { @@ -141,24 +135,27 @@ class FileHashStore { /// Computes a hash of the provided files and returns a list of entities /// that were dirty. - // TODO(jonahwilliams): compare hash performance with md5 tool on macOS and - // linux and certutil on Windows, as well as dividing up computation across - // isolates. This also related to the current performance issue with checking - // APKs before installing them on device. Future> hashFiles(List files) async { final List dirty = []; - for (File file in files) { - final String absolutePath = file.resolveSymbolicLinksSync(); - final String previousHash = previousHashes[absolutePath]; - final List bytes = file.readAsBytesSync(); - final String currentHash = md5.convert(bytes).toString(); + await Future.wait(>[ + for (File file in files) _hashFile(file, dirty, Pool(kMaxOpenFiles))]); + return dirty; + } + Future _hashFile(File file, List dirty, Pool pool) async { + final PoolResource resource = await pool.request(); + try { + final String absolutePath = file.path; + final String previousHash = previousHashes[absolutePath]; + final Digest digest = md5.convert(await file.readAsBytes()); + final String currentHash = digest.toString(); if (currentHash != previousHash) { dirty.add(file); } currentHashes[absolutePath] = currentHash; + } finally { + resource.release(); } - return dirty; } File get _cacheFile => environment.buildDir.childFile(_kFileCache); diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart index 127d4326569..f7832372f40 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/assets.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart @@ -50,7 +50,9 @@ class AssetBehavior extends SourceBehavior { /// A specific asset behavior for building bundles. class AssetOutputBehavior extends SourceBehavior { - const AssetOutputBehavior(); + const AssetOutputBehavior([this._pathSuffix = '']); + + final String _pathSuffix; @override List inputs(Environment environment) { @@ -64,7 +66,7 @@ class AssetOutputBehavior extends SourceBehavior { final List results = []; final Iterable files = assetBundle.entries.values.whereType(); for (DevFSFileContent devFsContent in files) { - results.add(fs.file(devFsContent.file.path)); + results.add(fs.file(fs.path.join(_pathSuffix, devFsContent.file.path))); } return results; } @@ -78,7 +80,7 @@ class AssetOutputBehavior extends SourceBehavior { ); final List results = []; for (String key in assetBundle.entries.keys) { - final File file = fs.file(fs.path.join(environment.outputDir.path, key)); + final File file = fs.file(fs.path.join(environment.outputDir.path, _pathSuffix, key)); results.add(file); } return results; @@ -125,7 +127,7 @@ class CopyAssets extends Target { packagesPath: environment.projectDir.childFile('.packages').path, ); // Limit number of open files to avoid running out of file descriptors. - final Pool pool = Pool(64); + final Pool pool = Pool(kMaxOpenFiles); await Future.wait( assetBundle.entries.entries.map>((MapEntry entry) async { final PoolResource resource = await pool.request(); diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart index 92474cdcf03..02a40212726 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/dart.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart @@ -115,23 +115,7 @@ class CopyFlutterBundle extends Target { final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); await assetBundle.build(); - final Pool pool = Pool(64); - await Future.wait( - assetBundle.entries.entries.map>((MapEntry entry) async { - final PoolResource resource = await pool.request(); - try { - final File file = fs.file(fs.path.join(environment.outputDir.path, entry.key)); - file.parent.createSync(recursive: true); - final DevFSContent content = entry.value; - if (content is DevFSFileContent && content.file is File) { - await (content.file as File).copy(file.path); - } else { - await file.writeAsBytes(await entry.value.contentsAsBytes()); - } - } finally { - resource.release(); - } - })); + await copyAssets(assetBundle, environment); } @override @@ -140,6 +124,28 @@ class CopyFlutterBundle extends Target { ]; } +/// A helper function to copy an [assetBundle] into an [environment]'s output directory, +/// plus an optional [pathSuffix] +Future copyAssets(AssetBundle assetBundle, Environment environment, [String pathSuffix = '']) async { + final Pool pool = Pool(kMaxOpenFiles); + await Future.wait( + assetBundle.entries.entries.map>((MapEntry entry) async { + final PoolResource resource = await pool.request(); + try { + final File file = fs.file(fs.path.join(environment.outputDir.path, pathSuffix, entry.key)); + file.parent.createSync(recursive: true); + final DevFSContent content = entry.value; + if (content is DevFSFileContent && content.file is File) { + await (content.file as File).copy(file.path); + } else { + await file.writeAsBytes(await entry.value.contentsAsBytes()); + } + } finally { + resource.release(); + } + })); +} + /// Copies the prebuilt flutter bundle for release mode. class ReleaseCopyFlutterBundle extends CopyFlutterBundle { const ReleaseCopyFlutterBundle(); diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index b05b3b8092e..a448b5b6365 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -336,7 +336,7 @@ abstract class MacOSBundleFlutterAssets extends Target { } // Limit number of open files to avoid running out of file descriptors. try { - final Pool pool = Pool(64); + final Pool pool = Pool(kMaxOpenFiles); await Future.wait( assetBundle.entries.entries.map>((MapEntry entry) async { final PoolResource resource = await pool.request(); diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart new file mode 100644 index 00000000000..d097dcbd817 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -0,0 +1,203 @@ +// 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 '../../artifacts.dart'; +import '../../asset.dart'; +import '../../base/file_system.dart'; +import '../../base/io.dart'; +import '../../base/process_manager.dart'; +import '../../build_info.dart'; +import '../../dart/package_map.dart'; +import '../../globals.dart'; +import '../../project.dart'; +import '../build_system.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'; + +/// Generates an entrypoint for a web target. +class WebEntrypointTarget extends Target { + const WebEntrypointTarget(); + + @override + String get name => 'web_entrypoint'; + + @override + List get dependencies => const []; + + @override + List get inputs => const [ + Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'), + ]; + + @override + List get outputs => const [ + Source.pattern('{BUILD_DIR}/main.dart'), + ]; + + @override + Future 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 import = fs.file(fs.path.absolute(targetFile)).uri.toString(); + + String contents; + if (hasPlugins) { + contents = ''' +import 'dart:ui' as ui; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +import 'generated_plugin_registrant.dart'; +import "$import" as entrypoint; + +Future main() async { + registerPlugins(webPluginRegistry); + if ($shouldInitializePlatform) { + await ui.webOnlyInitializePlatform(); + } + entrypoint.main(); +} +'''; + } else { + contents = ''' +import 'dart:ui' as ui; + +import "$import" as entrypoint; + +Future main() async { + if ($shouldInitializePlatform) { + await ui.webOnlyInitializePlatform(); + } + entrypoint.main(); +} +'''; + } + environment.buildDir.childFile('main.dart') + ..writeAsStringSync(contents); + } +} + +/// Compiles a web entrypoint with dart2js. +class Dart2JSTarget extends Target { + const Dart2JSTarget(); + + @override + String get name => 'dart2js'; + + @override + List get dependencies => const [ + WebEntrypointTarget() + ]; + + @override + List get inputs => const [ + Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'), + Source.artifact(Artifact.flutterWebSdk), + Source.artifact(Artifact.dart2jsSnapshot), + Source.artifact(Artifact.engineDartBinary), + Source.artifact(Artifact.engineDartSdkPath), + Source.pattern('{BUILD_DIR}/main.dart'), + Source.pattern('{PROJECT_DIR}/.packages'), + Source.function(listDartSources), // <- every dart file under {PROJECT_DIR}/lib and in .packages + ]; + + @override + List get outputs => const [ + Source.pattern('{BUILD_DIR}/main.dart.js'), + ]; + + @override + Future build(Environment environment) async { + final String dart2jsOptimization = environment.defines[kDart2jsOptimization]; + final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); + final String specPath = fs.path.join(artifacts.getArtifactPath(Artifact.flutterWebSdk), 'libraries.json'); + final String packageFile = FlutterProject.fromDirectory(environment.projectDir).hasBuilders + ? PackageMap.globalGeneratedPackagesPath + : PackageMap.globalPackagesPath; + final ProcessResult result = await processManager.run([ + artifacts.getArtifactPath(Artifact.engineDartBinary), + artifacts.getArtifactPath(Artifact.dart2jsSnapshot), + '--libraries-spec=$specPath', + if (dart2jsOptimization != null) + '-$dart2jsOptimization' + else if (buildMode == BuildMode.profile) + '-O1' + else + '-O4', + '-o', + environment.buildDir.childFile('main.dart.js').path, + '--packages=$packageFile', + if (buildMode == BuildMode.profile) + '-Ddart.vm.profile=true' + else + '-Ddart.vm.product=true', + environment.buildDir.childFile('main.dart').path, + ]); + if (result.exitCode != 0) { + throw Exception(result.stdout + result.stderr); + } + } +} + +/// Unpacks the dart2js compilation to a given output directory +class WebReleaseBundle extends Target { + const WebReleaseBundle(); + + @override + String get name => 'web_release_bundle'; + + @override + List get dependencies => const [ + Dart2JSTarget(), + ]; + + @override + List get inputs => const [ + Source.pattern('{BUILD_DIR}/main.dart.js'), + Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'), + Source.behavior(AssetOutputBehavior('assets')), + Source.pattern('{PROJECT_DIR}/web/index.html'), + ]; + + @override + List get outputs => const [ + Source.pattern('{OUTPUT_DIR}/main.dart.js'), + Source.pattern('{OUTPUT_DIR}/assets/AssetManifest.json'), + Source.pattern('{OUTPUT_DIR}/assets/FontManifest.json'), + Source.pattern('{OUTPUT_DIR}/assets/LICENSE'), + Source.pattern('{OUTPUT_DIR}/index.html'), + Source.behavior(AssetOutputBehavior('assets')) + ]; + + @override + Future build(Environment environment) async { + for (File outputFile in environment.buildDir.listSync(recursive: true).whereType()) { + if (!fs.path.basename(outputFile.path).contains('main.dart.js')) { + continue; + } + outputFile.copySync( + environment.outputDir.childFile(fs.path.basename(outputFile.path)).path + ); + } + environment.projectDir + .childDirectory('web') + .childFile('index.html') + .copySync(fs.path.join(environment.outputDir.path, 'index.html')); + final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); + await assetBundle.build(); + await copyAssets(assetBundle, environment, 'assets'); + } +} diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart index 8fdb70852e2..c8048cb6e04 100644 --- a/packages/flutter_tools/lib/src/commands/assemble.dart +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -12,6 +12,7 @@ import '../build_system/targets/dart.dart'; import '../build_system/targets/ios.dart'; import '../build_system/targets/linux.dart'; import '../build_system/targets/macos.dart'; +import '../build_system/targets/web.dart'; import '../build_system/targets/windows.dart'; import '../globals.dart'; import '../project.dart'; @@ -31,6 +32,7 @@ const List _kDefaultTargets = [ DebugMacOSBundleFlutterAssets(), ProfileMacOSBundleFlutterAssets(), ReleaseMacOSBundleFlutterAssets(), + WebReleaseBundle(), ]; /// Assemble provides a low level API to interact with the flutter tool build diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart index a682b6b68ea..c924c3e547a 100644 --- a/packages/flutter_tools/lib/src/web/compile.dart +++ b/packages/flutter_tools/lib/src/web/compile.dart @@ -2,17 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:archive/archive.dart'; import 'package:meta/meta.dart'; -import '../asset.dart'; import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../build_info.dart'; -import '../bundle.dart'; +import '../build_system/build_system.dart'; +import '../build_system/targets/dart.dart'; +import '../build_system/targets/web.dart'; import '../globals.dart'; +import '../platform_plugins.dart'; +import '../plugins.dart'; import '../project.dart'; import '../reporting/reporting.dart'; @@ -23,64 +25,32 @@ Future buildWeb(FlutterProject flutterProject, String target, BuildInfo bu if (!flutterProject.web.existsSync()) { throwToolExit('Missing index.html.'); } + final bool hasWebPlugins = findPlugins(flutterProject) + .any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey)); final Status status = logger.startProgress('Compiling $target for the Web...', timeout: null); final Stopwatch sw = Stopwatch()..start(); - final Directory outputDir = fs.directory(getWebBuildDirectory()) - ..createSync(recursive: true); - bool result; - try { - result = await webCompilationProxy.initialize( - projectDirectory: FlutterProject.current().directory, - mode: buildInfo.mode, - projectName: flutterProject.manifest.appName, - initializePlatform: initializePlatform, - ); - if (result) { - // Places assets adjacent to the web stuff. - final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); - await assetBundle.build(); - await writeBundle(fs.directory(fs.path.join(outputDir.path, 'assets')), assetBundle.entries); - - // Copy results to output directory. - final String outputPath = fs.path.join( - flutterProject.dartTool.path, - 'build', - 'flutter_web', - flutterProject.manifest.appName, - '${fs.path.withoutExtension(target)}_web_entrypoint.dart.js', - ); - // Check for deferred import outputs. - final File dart2jsArchive = fs.file(fs.path.join( - flutterProject.dartTool.path, - 'build', - 'flutter_web', - '${flutterProject.manifest.appName}', - '${fs.path.withoutExtension(target)}_web_entrypoint.dart.js.tar.gz'), - ); - fs.file(outputPath).copySync(fs.path.join(outputDir.path, 'main.dart.js')); - fs.file('$outputPath.map').copySync(fs.path.join(outputDir.path, 'main.dart.js.map')); - flutterProject.web.indexFile.copySync(fs.path.join(outputDir.path, 'index.html')); - if (dart2jsArchive.existsSync()) { - final Archive archive = TarDecoder().decodeBytes(dart2jsArchive.readAsBytesSync()); - for (ArchiveFile file in archive.files) { - outputDir.childFile(file.name).writeAsBytesSync(file.content); - } - } + final BuildResult result = await const BuildSystem().build(const WebReleaseBundle(), Environment( + outputDir: fs.directory(getWebBuildDirectory()), + projectDir: fs.currentDirectory, + buildDir: flutterProject.directory + .childDirectory('.dart_tool') + .childDirectory('flutter_build'), + defines: { + kBuildMode: getNameForBuildMode(buildInfo.mode), + kTargetFile: target, + kInitializePlatform: initializePlatform.toString(), + kHasWebPlugins: hasWebPlugins.toString(), + }, + )); + if (!result.success) { + for (ExceptionMeasurement measurement in result.exceptions.values) { + printError(measurement.stackTrace.toString()); + printError(measurement.exception.toString()); } - } catch (err) { - printError(err.toString()); - result = false; - } finally { - status.stop(); + throwToolExit('Failed to compile application for the Web.'); } - if (result == false) { - throwToolExit('Failed to compile $target for the Web.'); - } - String buildName = 'ddc'; - if (buildInfo.isRelease) { - buildName = 'dart2js'; - } - flutterUsage.sendTiming('build', buildName, Duration(milliseconds: sw.elapsedMilliseconds)); + status.stop(); + flutterUsage.sendTiming('build', 'dart2js', Duration(milliseconds: sw.elapsedMilliseconds)); } /// An indirection on web compilation. diff --git a/packages/flutter_tools/test/general.shard/build_system/filecache_test.dart b/packages/flutter_tools/test/general.shard/build_system/filecache_test.dart index 051e55fb9d1..ca53e666eaa 100644 --- a/packages/flutter_tools/test/general.shard/build_system/filecache_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/filecache_test.dart @@ -39,37 +39,37 @@ void main() { expect(fileStorage.version, 2); })); - test('saves and restores to file cache', () => testbed.run(() { + test('saves and restores to file cache', () => testbed.run(() async { final File file = fs.file('foo.dart') ..createSync() ..writeAsStringSync('hello'); final FileHashStore fileCache = FileHashStore(environment); fileCache.initialize(); - fileCache.hashFiles([file]); + await fileCache.hashFiles([file]); fileCache.persist(); - final String currentHash = fileCache.currentHashes[file.resolveSymbolicLinksSync()]; + final String currentHash = fileCache.currentHashes[file.path]; final List buffer = fs.file(fs.path.join(environment.buildDir.path, '.filecache')) .readAsBytesSync(); FileStorage fileStorage = FileStorage.fromBuffer(buffer); expect(fileStorage.files.single.hash, currentHash); - expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync()); + expect(fileStorage.files.single.path, file.path); final FileHashStore newFileCache = FileHashStore(environment); newFileCache.initialize(); expect(newFileCache.currentHashes, isEmpty); - expect(newFileCache.previousHashes[fs.path.absolute('foo.dart')], currentHash); + expect(newFileCache.previousHashes['foo.dart'], currentHash); newFileCache.persist(); // Still persisted correctly. fileStorage = FileStorage.fromBuffer(buffer); expect(fileStorage.files.single.hash, currentHash); - expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync()); + expect(fileStorage.files.single.path, file.path); })); - test('handles persisting with a missing build directory', () => testbed.run(() { + test('handles persisting with a missing build directory', () => testbed.run(() async { final File file = fs.file('foo.dart') ..createSync() ..writeAsStringSync('hello'); @@ -77,7 +77,7 @@ void main() { fileCache.initialize(); environment.buildDir.deleteSync(recursive: true); - fileCache.hashFiles([file]); + await fileCache.hashFiles([file]); // Does not throw. fileCache.persist(); })); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart new file mode 100644 index 00000000000..0af986ab1e4 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart @@ -0,0 +1,221 @@ +// 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 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/process_manager.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/targets/dart.dart'; +import 'package:flutter_tools/src/build_system/targets/web.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../../../src/common.dart'; +import '../../../src/mocks.dart'; +import '../../../src/testbed.dart'; + +void main() { + Testbed testbed; + Environment environment; + MockPlatform mockPlatform; + MockPlatform mockWindowsPlatform; + + setUp(() { + mockPlatform = MockPlatform(); + mockWindowsPlatform = MockPlatform(); + + when(mockPlatform.isWindows).thenReturn(false); + when(mockPlatform.isMacOS).thenReturn(true); + when(mockPlatform.isLinux).thenReturn(false); + + when(mockWindowsPlatform.isWindows).thenReturn(true); + when(mockWindowsPlatform.isMacOS).thenReturn(false); + when(mockWindowsPlatform.isLinux).thenReturn(false); + + testbed = Testbed(setup: () { + environment = Environment( + projectDir: fs.currentDirectory, + outputDir: fs.currentDirectory, + buildDir: fs.currentDirectory, + defines: { + kTargetFile: fs.path.join('lib', 'main.dart'), + } + ); + environment.buildDir.createSync(recursive: true); + }, overrides: { + Platform: () => mockPlatform, + }); + }); + + test('WebEntrypointTarget generates an entrypoint with plugins and init platform', () => testbed.run(() async { + environment.defines[kHasWebPlugins] = 'true'; + environment.defines[kInitializePlatform] = 'true'; + await const WebEntrypointTarget().build(environment); + + final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); + + // Plugins + expect(generated, contains("import 'generated_plugin_registrant.dart';")); + expect(generated, contains('registerPlugins(webPluginRegistry);')); + + // Platform + expect(generated, contains('if (true) {')); + + // Main + expect(generated, contains('entrypoint.main();')); + + // Import. + expect(generated, contains('import "file:///lib/main.dart" as entrypoint;')); + })); + + test('WebEntrypointTarget generates an entrypoint with plugins and init platform on windows', () => testbed.run(() async { + environment.defines[kHasWebPlugins] = 'true'; + environment.defines[kInitializePlatform] = 'true'; + await const WebEntrypointTarget().build(environment); + + final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); + + // Plugins + expect(generated, contains("import 'generated_plugin_registrant.dart';")); + expect(generated, contains('registerPlugins(webPluginRegistry);')); + + // Platform + expect(generated, contains('if (true) {')); + + // Main + expect(generated, contains('entrypoint.main();')); + + // Import. + expect(generated, contains('import "file:///C:/lib/main.dart" as entrypoint;')); + }, overrides: { + Platform: () => mockWindowsPlatform, + })); + + test('WebEntrypointTarget generates an entrypoint without plugins and init platform', () => testbed.run(() async { + environment.defines[kHasWebPlugins] = 'false'; + environment.defines[kInitializePlatform] = 'true'; + await const WebEntrypointTarget().build(environment); + + final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); + + // Plugins + expect(generated, isNot(contains("import 'generated_plugin_registrant.dart';"))); + expect(generated, isNot(contains('registerPlugins(webPluginRegistry);'))); + + // Platform + expect(generated, contains('if (true) {')); + + // Main + expect(generated, contains('entrypoint.main();')); + })); + + test('WebEntrypointTarget generates an entrypoint with plugins and without init platform', () => testbed.run(() async { + environment.defines[kHasWebPlugins] = 'true'; + environment.defines[kInitializePlatform] = 'false'; + await const WebEntrypointTarget().build(environment); + + final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); + + // Plugins + expect(generated, contains("import 'generated_plugin_registrant.dart';")); + expect(generated, contains('registerPlugins(webPluginRegistry);')); + + // Platform + expect(generated, contains('if (false) {')); + + // Main + expect(generated, contains('entrypoint.main();')); + })); + + test('WebEntrypointTarget generates an entrypoint without plugins and without init platform', () => testbed.run(() async { + environment.defines[kHasWebPlugins] = 'false'; + environment.defines[kInitializePlatform] = 'false'; + await const WebEntrypointTarget().build(environment); + + final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); + + // Plugins + expect(generated, isNot(contains("import 'generated_plugin_registrant.dart';"))); + expect(generated, isNot(contains('registerPlugins(webPluginRegistry);'))); + + // Platform + expect(generated, contains('if (false) {')); + + // Main + expect(generated, contains('entrypoint.main();')); + })); + + test('Dart2JSTarget calls dart2js with expected args in profile mode', () => testbed.run(() async { + environment.defines[kBuildMode] = 'profile'; + when(processManager.run(any)).thenAnswer((Invocation invocation) async { + return FakeProcessResult(exitCode: 0); + }); + await const Dart2JSTarget().build(environment); + + final List expected = [ + fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'), + fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'), + '--libraries-spec=' + fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'), + '-O1', // lowest optimizations. + '-o', + environment.buildDir.childFile('main.dart.js').absolute.path, + '--packages=.packages', + '-Ddart.vm.profile=true', + environment.buildDir.childFile('main.dart').absolute.path, + ]; + verify(processManager.run(expected)).called(1); + }, overrides: { + ProcessManager: () => MockProcessManager(), + })); + + test('Dart2JSTarget calls dart2js with expected args in release mode', () => testbed.run(() async { + environment.defines[kBuildMode] = 'release'; + when(processManager.run(any)).thenAnswer((Invocation invocation) async { + return FakeProcessResult(exitCode: 0); + }); + await const Dart2JSTarget().build(environment); + + final List expected = [ + fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'), + fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'), + '--libraries-spec=' + fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'), + '-O4', // highest optimizations. + '-o', + environment.buildDir.childFile('main.dart.js').absolute.path, + '--packages=.packages', + '-Ddart.vm.product=true', + environment.buildDir.childFile('main.dart').absolute.path, + ]; + verify(processManager.run(expected)).called(1); + }, overrides: { + ProcessManager: () => MockProcessManager(), + })); + + test('Dart2JSTarget calls dart2js with expected args in release with dart2js optimization override', () => testbed.run(() async { + environment.defines[kBuildMode] = 'release'; + environment.defines[kDart2jsOptimization] = 'O3'; + when(processManager.run(any)).thenAnswer((Invocation invocation) async { + return FakeProcessResult(exitCode: 0); + }); + await const Dart2JSTarget().build(environment); + + final List expected = [ + fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'), + fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'), + '--libraries-spec=' + fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'), + '-O3', // configured optimizations. + '-o', + environment.buildDir.childFile('main.dart.js').absolute.path, + '--packages=.packages', + '-Ddart.vm.product=true', + environment.buildDir.childFile('main.dart').absolute.path, + ]; + verify(processManager.run(expected)).called(1); + }, overrides: { + ProcessManager: () => MockProcessManager(), + })); +} + +class MockProcessManager extends Mock implements ProcessManager {} +class MockPlatform extends Mock implements Platform {} diff --git a/packages/flutter_tools/test/general.shard/commands/build_web_test.dart b/packages/flutter_tools/test/general.shard/commands/build_web_test.dart index a2bdeff5a19..555b4d2e2ee 100644 --- a/packages/flutter_tools/test/general.shard/commands/build_web_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/build_web_test.dart @@ -2,11 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:convert'; - -import 'package:archive/archive.dart'; import 'package:args/command_runner.dart'; -import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/platform.dart'; @@ -26,10 +22,8 @@ import '../../src/common.dart'; import '../../src/testbed.dart'; void main() { - MockWebCompilationProxy mockWebCompilationProxy; Testbed testbed; MockPlatform mockPlatform; - bool addArchive = false; setUpAll(() { Cache.flutterRoot = ''; @@ -37,8 +31,6 @@ void main() { }); setUp(() { - addArchive = false; - mockWebCompilationProxy = MockWebCompilationProxy(); testbed = Testbed(setup: () { fs.file('pubspec.yaml') ..createSync() @@ -46,47 +38,13 @@ void main() { fs.file('.packages').createSync(); fs.file(fs.path.join('web', 'index.html')).createSync(recursive: true); fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); - when(mockWebCompilationProxy.initialize( - projectName: anyNamed('projectName'), - projectDirectory: anyNamed('projectDirectory'), - mode: anyNamed('mode'), - initializePlatform: anyNamed('initializePlatform'), - )).thenAnswer((Invocation invocation) { - final String prefix = fs.path.join('.dart_tool', 'build', 'flutter_web', 'foo', 'lib'); - final String path = fs.path.join(prefix, 'main_web_entrypoint.dart.js'); - fs.file(path).createSync(recursive: true); - fs.file('$path.map').createSync(); - if (addArchive) { - final List bytes = utf8.encode('void main() {}'); - final TarEncoder encoder = TarEncoder(); - final Archive archive = Archive() - ..addFile(ArchiveFile.noCompress('main_web_entrypoint.1.dart.js', bytes.length, bytes)); - fs.file(fs.path.join(prefix, 'main_web_entrypoint.dart.js.tar.gz')) - ..writeAsBytes(encoder.encode(archive)); - } - return Future.value(true); - }); }, overrides: { - WebCompilationProxy: () => mockWebCompilationProxy, Platform: () => mockPlatform, FlutterVersion: () => MockFlutterVersion(), FeatureFlags: () => TestFeatureFlags(isWebEnabled: true), }); }); - test('Copies generated part files out of build directory', () => testbed.run(() async { - addArchive = true; - await buildWeb( - FlutterProject.current(), - fs.path.join('lib', 'main.dart'), - BuildInfo.release, - false, - ); - - expect(fs.file(fs.path.join('build', 'web', 'main_web_entrypoint.1.dart.js')), exists); - expect(fs.file(fs.path.join('build', 'web', 'main.dart.js')), exists); - })); - test('Refuses to build for web when missing index.html', () => testbed.run(() async { fs.file(fs.path.join('web', 'index.html')).deleteSync(); diff --git a/packages/flutter_tools/test/general.shard/web/asset_server_test.dart b/packages/flutter_tools/test/general.shard/web/asset_server_test.dart index c0988025116..3a854252770 100644 --- a/packages/flutter_tools/test/general.shard/web/asset_server_test.dart +++ b/packages/flutter_tools/test/general.shard/web/asset_server_test.dart @@ -22,7 +22,7 @@ void main() { fs.file(fs.path.join('web', 'index.html')) ..createSync(recursive: true) ..writeAsStringSync('hello'); - assetServer = AssetServer(FlutterProject.current(), fs.path.join('main')); + assetServer = DebugAssetServer(FlutterProject.current(), fs.path.join('main')); } ); });