Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
Support flutter build web --wasm (#117075)
Browse files Browse the repository at this point in the history
* Work in progress.

* Some fixes to the command line.

* Bootstrapping works.

* Change kickoff order to maximize concurrency.

* Fix analyzer errors and formatting issues.

* Fix doc comment.

* Added unit tests for some of the web targets.

* Format issue.

* Add an integration test that builds an app to wasm.

* Add a todo for depfiles.

* Formatting.

* Apparently the license header needs to say 2014.

* `file://` URIs confuse dart2wasm on Windows. Just use absolute paths.

* Update unit tests to match new path passing.

* Have a distinct build directory for wasm, and fixes for some upstream changes.
  • Loading branch information
eyebrowsoffire committed Dec 19, 2022
1 parent 70f391d commit 9f2c5d8
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 61 deletions.
25 changes: 21 additions & 4 deletions packages/flutter_tools/lib/src/artifacts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ enum Artifact {
engineDartSdkPath,
/// The dart binary used to execute any of the required snapshots.
engineDartBinary,
/// The dart binary for running aot snapshots
engineDartAotRuntime,
/// The snapshot of frontend_server compiler.
frontendServerSnapshotForEngineDartSdk,
/// The dart snapshot of the dart2js compiler.
dart2jsSnapshot,
/// The dart snapshot of the dart2wasm compiler.
dart2wasmSnapshot,

/// The root of the Linux desktop sources.
linuxDesktopPath,
Expand Down Expand Up @@ -168,8 +172,12 @@ String? _artifactToFileName(Artifact artifact, Platform hostPlatform, [ BuildMod
return 'dart-sdk';
case Artifact.engineDartBinary:
return 'dart$exe';
case Artifact.engineDartAotRuntime:
return 'dartaotruntime$exe';
case Artifact.dart2jsSnapshot:
return 'dart2js.dart.snapshot';
case Artifact.dart2wasmSnapshot:
return 'dart2wasm_product.snapshot';
case Artifact.frontendServerSnapshotForEngineDartSdk:
return 'frontend_server.dart.snapshot';
case Artifact.linuxDesktopPath:
Expand Down Expand Up @@ -488,7 +496,9 @@ class CachedArtifacts implements Artifacts {
return _fileSystem.path.join(engineDir, hostPlatform, _artifactToFileName(artifact, _platform));
case Artifact.engineDartSdkPath:
case Artifact.engineDartBinary:
case Artifact.engineDartAotRuntime:
case Artifact.dart2jsSnapshot:
case Artifact.dart2wasmSnapshot:
case Artifact.frontendServerSnapshotForEngineDartSdk:
case Artifact.constFinder:
case Artifact.flutterFramework:
Expand Down Expand Up @@ -525,7 +535,9 @@ class CachedArtifacts implements Artifacts {
return _getIosEngineArtifactPath(engineDir, environmentType, _fileSystem, _platform);
case Artifact.engineDartSdkPath:
case Artifact.engineDartBinary:
case Artifact.engineDartAotRuntime:
case Artifact.dart2jsSnapshot:
case Artifact.dart2wasmSnapshot:
case Artifact.frontendServerSnapshotForEngineDartSdk:
case Artifact.constFinder:
case Artifact.flutterMacOSFramework:
Expand Down Expand Up @@ -580,7 +592,9 @@ class CachedArtifacts implements Artifacts {
case Artifact.fontSubset:
case Artifact.engineDartSdkPath:
case Artifact.engineDartBinary:
case Artifact.engineDartAotRuntime:
case Artifact.dart2jsSnapshot:
case Artifact.dart2wasmSnapshot:
case Artifact.frontendServerSnapshotForEngineDartSdk:
case Artifact.icuData:
case Artifact.isolateSnapshotData:
Expand Down Expand Up @@ -613,6 +627,7 @@ class CachedArtifacts implements Artifacts {
// android_arm in profile mode because it is available on all supported host platforms.
return _getAndroidArtifactPath(artifact, TargetPlatform.android_arm, BuildMode.profile);
case Artifact.dart2jsSnapshot:
case Artifact.dart2wasmSnapshot:
case Artifact.frontendServerSnapshotForEngineDartSdk:
return _fileSystem.path.join(
_dartSdkPath(_cache), 'bin', 'snapshots',
Expand All @@ -634,6 +649,7 @@ class CachedArtifacts implements Artifacts {
case Artifact.engineDartSdkPath:
return _dartSdkPath(_cache);
case Artifact.engineDartBinary:
case Artifact.engineDartAotRuntime:
return _fileSystem.path.join(_dartSdkPath(_cache), 'bin', _artifactToFileName(artifact, _platform));
case Artifact.flutterMacOSFramework:
case Artifact.linuxDesktopPath:
Expand Down Expand Up @@ -925,13 +941,12 @@ class CachedLocalEngineArtifacts implements Artifacts {
case Artifact.engineDartSdkPath:
return _getDartSdkPath();
case Artifact.engineDartBinary:
case Artifact.engineDartAotRuntime:
return _fileSystem.path.join(_getDartSdkPath(), 'bin', artifactFileName);
case Artifact.dart2jsSnapshot:
return _fileSystem.path.join(_getDartSdkPath(), 'bin', 'snapshots', artifactFileName);
case Artifact.dart2wasmSnapshot:
case Artifact.frontendServerSnapshotForEngineDartSdk:
return _fileSystem.path.join(
_getDartSdkPath(), 'bin', 'snapshots', artifactFileName,
);
return _fileSystem.path.join(_getDartSdkPath(), 'bin', 'snapshots', artifactFileName);
}
}

Expand Down Expand Up @@ -1048,10 +1063,12 @@ class CachedLocalWebSdkArtifacts implements Artifacts {
case Artifact.engineDartSdkPath:
return _getDartSdkPath();
case Artifact.engineDartBinary:
case Artifact.engineDartAotRuntime:
return _fileSystem.path.join(
_getDartSdkPath(), 'bin',
_artifactToFileName(artifact, _platform, mode));
case Artifact.dart2jsSnapshot:
case Artifact.dart2wasmSnapshot:
case Artifact.frontendServerSnapshotForEngineDartSdk:
return _fileSystem.path.join(
_getDartSdkPath(), 'bin', 'snapshots',
Expand Down
4 changes: 2 additions & 2 deletions packages/flutter_tools/lib/src/build_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -914,8 +914,8 @@ String getMacOSBuildDirectory() {
}

/// Returns the web build output directory.
String getWebBuildDirectory() {
return globals.fs.path.join(getBuildDirectory(), 'web');
String getWebBuildDirectory([bool isWasm = false]) {
return globals.fs.path.join(getBuildDirectory(), isWasm ? 'web_wasm' : 'web');
}

/// Returns the Linux build output directory.
Expand Down
157 changes: 127 additions & 30 deletions packages/flutter_tools/lib/src/build_system/targets/web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import '../../web/compile.dart';
import '../../web/file_generators/flutter_js.dart' as flutter_js;
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';
Expand Down Expand Up @@ -141,13 +142,11 @@ class WebEntrypointTarget extends Target {
}

/// Compiles a web entry point with dart2js.
class Dart2JSTarget extends Target {
const Dart2JSTarget(this.webRenderer);
abstract class Dart2WebTarget extends Target {
const Dart2WebTarget(this.webRenderer);

final WebRendererMode webRenderer;

@override
String get name => 'dart2js';
Source get compilerSnapshot;

@override
List<Target> get dependencies => const <Target>[
Expand All @@ -156,22 +155,17 @@ class Dart2JSTarget extends Target {
];

@override
List<Source> get inputs => const <Source>[
Source.hostArtifact(HostArtifact.flutterWebSdk),
Source.artifact(Artifact.dart2jsSnapshot),
Source.artifact(Artifact.engineDartBinary),
Source.pattern('{BUILD_DIR}/main.dart'),
Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'),
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>[];

@override
List<String> get depfiles => const <String>[
'dart2js.d',
];

String _collectOutput(ProcessResult result) {
final String stdout = result.stdout is List<int>
? utf8.decode(result.stdout as List<int>)
Expand All @@ -181,6 +175,21 @@ class Dart2JSTarget extends Target {
: result.stderr as String;
return stdout + stderr;
}
}

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 {
Expand Down Expand Up @@ -270,29 +279,94 @@ class Dart2JSTarget extends Target {
}
}

/// Unpacks the dart2js compilation and resources to a given output directory.
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 BuildMode buildMode = getBuildModeForName(buildModeEnvironment);
final Artifacts artifacts = globals.artifacts!;
final File outputWasmFile = environment.buildDir.childFile('main.dart.wasm');
final String dartSdkPath = artifacts.getArtifactPath(Artifact.engineDartSdkPath, platform: TargetPlatform.web_javascript);
final String dartSdkRoot = environment.fileSystem.directory(dartSdkPath).parent.path;

final List<String> compilationArgs = <String>[
artifacts.getArtifactPath(Artifact.engineDartAotRuntime, platform: TargetPlatform.web_javascript),
'--disable-dart-dev',
artifacts.getArtifactPath(Artifact.dart2wasmSnapshot, platform: TargetPlatform.web_javascript),
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',
'--packages=.dart_tool/package_config.json',
'--dart-sdk=$dartSdkPath',
'--multi-root-scheme',
'org-dartlang-sdk',
'--multi-root',
artifacts.getHostArtifact(HostArtifact.flutterWebSdk).path,
'--multi-root',
dartSdkRoot,
'--libraries-spec',
artifacts.getHostArtifact(HostArtifact.flutterWebLibrariesJson).path,

environment.buildDir.childFile('main.dart').path, // dartfile
outputWasmFile.path,
];
final ProcessResult compileResult = await globals.processManager.run(compilationArgs);
if (compileResult.exitCode != 0) {
throw Exception(_collectOutput(compileResult));
}
}

@override
Source get compilerSnapshot => const Source.artifact(Artifact.dart2wasmSnapshot);

@override
String get name => 'dart2wasm';

@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/main.dart.wasm'),
];

// TODO(jacksongardner): override `depfiles` once dart2wasm begins producing
// them: https://github.com/dart-lang/sdk/issues/50747
}

/// Unpacks the dart2js or dart2wasm compilation and resources to a given
/// output directory.
class WebReleaseBundle extends Target {
const WebReleaseBundle(this.webRenderer);
const WebReleaseBundle(this.webRenderer, this.isWasm);

final WebRendererMode webRenderer;
final bool isWasm;

String get outputFileName => isWasm ? 'main.dart.wasm' : 'main.dart.js';

@override
String get name => 'web_release_bundle';

@override
List<Target> get dependencies => <Target>[
Dart2JSTarget(webRenderer),
if (isWasm) Dart2WasmTarget(webRenderer) else Dart2JSTarget(webRenderer),
];

@override
List<Source> get inputs => const <Source>[
Source.pattern('{BUILD_DIR}/main.dart.js'),
Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
List<Source> get inputs => <Source>[
Source.pattern('{BUILD_DIR}/$outputFileName'),
const Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
];

@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/main.dart.js'),
List<Source> get outputs => <Source>[
Source.pattern('{OUTPUT_DIR}/$outputFileName'),
];

@override
Expand All @@ -306,7 +380,7 @@ class WebReleaseBundle extends Target {
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')) {
if (!basename.contains(outputFileName)) {
continue;
}
// Do not copy the deps file.
Expand All @@ -318,6 +392,12 @@ class WebReleaseBundle extends Target {
);
}

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);
Expand Down Expand Up @@ -413,10 +493,11 @@ class WebReleaseBundle extends Target {
/// These assets can be cached forever and are only invalidated when the
/// Flutter SDK is upgraded to a new version.
class WebBuiltInAssets extends Target {
const WebBuiltInAssets(this.fileSystem, this.cache);
const WebBuiltInAssets(this.fileSystem, this.cache, this.isWasm);

final FileSystem fileSystem;
final Cache cache;
final bool isWasm;

@override
String get name => 'web_static_assets';
Expand Down Expand Up @@ -451,6 +532,21 @@ class WebBuiltInAssets extends Target {
file.copySync(targetPath);
}

if (isWasm) {
final String dartSdkPath =
globals.artifacts!.getArtifactPath(Artifact.engineDartSdkPath);
final File dart2wasmRuntime = fileSystem.directory(dartSdkPath)
.childDirectory('bin')
.childFile('dart2wasm_runtime.mjs');
final String targetPath = fileSystem.path.join(
environment.outputDir.path,
'dart2wasm_runtime.mjs');
dart2wasmRuntime.copySync(targetPath);

final File bootstrapFile = environment.outputDir.childFile('main.dart.js');
bootstrapFile.writeAsStringSync(wasm_bootstrap.generateWasmBootstrapFile());
}

// Write the flutter.js file
final File flutterJsFile = environment.outputDir.childFile('flutter.js');
flutterJsFile.writeAsStringSync(flutter_js.generateFlutterJsFile());
Expand All @@ -459,20 +555,21 @@ class WebBuiltInAssets extends Target {

/// Generate a service worker for a web target.
class WebServiceWorker extends Target {
const WebServiceWorker(this.fileSystem, this.cache, this.webRenderer);
const WebServiceWorker(this.fileSystem, this.cache, this.webRenderer, this.isWasm);

final FileSystem fileSystem;
final Cache cache;
final WebRendererMode webRenderer;
final bool isWasm;

@override
String get name => 'web_service_worker';

@override
List<Target> get dependencies => <Target>[
Dart2JSTarget(webRenderer),
WebReleaseBundle(webRenderer),
WebBuiltInAssets(fileSystem, cache),
if (isWasm) Dart2WasmTarget(webRenderer) else Dart2JSTarget(webRenderer),
WebReleaseBundle(webRenderer, isWasm),
WebBuiltInAssets(fileSystem, cache, isWasm),
];

@override
Expand Down
5 changes: 5 additions & 0 deletions packages/flutter_tools/lib/src/commands/build_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class BuildWebCommand extends BuildSubCommand {
'to view and debug the original source code of a compiled and minified Dart '
'application.'
);
argParser.addFlag(
'wasm',
help: 'Compile to WebAssembly rather than Javascript (experimental).'
);

argParser.addOption('pwa-strategy',
defaultsTo: kOfflineFirst,
Expand Down Expand Up @@ -140,6 +144,7 @@ class BuildWebCommand extends BuildSubCommand {
stringArgDeprecated('pwa-strategy')!,
boolArgDeprecated('source-maps'),
boolArgDeprecated('native-null-assertions'),
boolArgDeprecated('wasm'),
baseHref: baseHref,
dart2jsOptimization: stringArgDeprecated('dart2js-optimization'),
outputDirectoryPath: outputDirectoryPath,
Expand Down
Loading

0 comments on commit 9f2c5d8

Please sign in to comment.