diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 41dd11ebe9..947d0d02b7 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -167,10 +167,22 @@ Future _runBuildTests() async { await _flutterBuildApk(path); await _flutterBuildIpa(path); } + await _flutterBuildDart2js(path.join('dev', 'integration_tests', 'web')); print('${bold}DONE: All build tests successful.$reset'); } +Future _flutterBuildDart2js(String relativePathToApplication) async { + print('Running Dart2JS build tests...'); + await runCommand(flutter, + ['build', 'web', '-v'], + workingDirectory: path.join(flutterRoot, relativePathToApplication), + expectNonZeroExit: false, + timeout: _kShortTimeout, + ); + print('Done.'); +} + Future _flutterBuildAot(String relativePathToApplication) async { print('Running AOT build tests...'); await runCommand(flutter, diff --git a/dev/integration_tests/web/lib/main.dart b/dev/integration_tests/web/lib/main.dart new file mode 100644 index 0000000000..49c544cb7c --- /dev/null +++ b/dev/integration_tests/web/lib/main.dart @@ -0,0 +1,13 @@ +// 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/widgets.dart'; + +void main() { + runApp(Center( + // Can remove when https://github.com/dart-lang/sdk/issues/35801 is fixed. + // ignore: prefer_const_constructors + child: Text('Hello, World', textDirection: TextDirection.ltr), + )); +} diff --git a/dev/integration_tests/web/pubspec.yaml b/dev/integration_tests/web/pubspec.yaml new file mode 100644 index 0000000000..8749907e23 --- /dev/null +++ b/dev/integration_tests/web/pubspec.yaml @@ -0,0 +1,17 @@ +name: web_integration +description: Integration test for web compilation. + +environment: + # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. + sdk: ">=2.0.0-dev.68.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +# PUBSPEC CHECKSUM: d53c diff --git a/packages/flutter/lib/src/foundation/basic_types.dart b/packages/flutter/lib/src/foundation/basic_types.dart index 73637cd7da..2c6c875787 100644 --- a/packages/flutter/lib/src/foundation/basic_types.dart +++ b/packages/flutter/lib/src/foundation/basic_types.dart @@ -8,6 +8,7 @@ import 'dart:collection'; // COMMON SIGNATURES export 'dart:ui' show VoidCallback; +export 'bitfield.dart' if (dart.library.html) 'bitfield_unsupported.dart'; /// Signature for callbacks that report that an underlying value has changed. /// @@ -66,69 +67,6 @@ typedef AsyncValueSetter = Future Function(T value); /// * [AsyncValueSetter], the setter equivalent of this signature. typedef AsyncValueGetter = Future Function(); - -// BITFIELD - -/// The largest SMI value. -/// -/// See -const int kMaxUnsignedSMI = 0x3FFFFFFFFFFFFFFF; - -/// A BitField over an enum (or other class whose values implement "index"). -/// Only the first 62 values of the enum can be used as indices. -class BitField { - /// Creates a bit field of all zeros. - /// - /// The given length must be at most 62. - BitField(this._length) - : assert(_length <= _smiBits), - _bits = _allZeros; - - /// Creates a bit field filled with a particular value. - /// - /// If the value argument is true, the bits are filled with ones. Otherwise, - /// the bits are filled with zeros. - /// - /// The given length must be at most 62. - BitField.filled(this._length, bool value) - : assert(_length <= _smiBits), - _bits = value ? _allOnes : _allZeros; - - final int _length; - int _bits; - - static const int _smiBits = 62; // see https://www.dartlang.org/articles/numeric-computation/#smis-and-mints - static const int _allZeros = 0; - static const int _allOnes = kMaxUnsignedSMI; // 2^(_kSMIBits+1)-1 - - /// Returns whether the bit with the given index is set to one. - bool operator [](T index) { - assert(index.index < _length); - return (_bits & 1 << index.index) > 0; - } - - /// Sets the bit with the given index to the given value. - /// - /// If value is true, the bit with the given index is set to one. Otherwise, - /// the bit is set to zero. - void operator []=(T index, bool value) { - assert(index.index < _length); - if (value) - _bits = _bits | (1 << index.index); - else - _bits = _bits & ~(1 << index.index); - } - - /// Sets all the bits to the given value. - /// - /// If the value is true, the bits are all set to one. Otherwise, the bits are - /// all set to zero. Defaults to setting all the bits to zero. - void reset([ bool value = false ]) { - _bits = value ? _allOnes : _allZeros; - } -} - - // LAZY CACHING ITERATOR /// A lazy caching version of [Iterable]. diff --git a/packages/flutter/lib/src/foundation/bitfield.dart b/packages/flutter/lib/src/foundation/bitfield.dart new file mode 100644 index 0000000000..5240bc0c13 --- /dev/null +++ b/packages/flutter/lib/src/foundation/bitfield.dart @@ -0,0 +1,67 @@ +// Copyright 2015 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. + +/// The largest SMI value. +/// +/// See +/// +/// When compiling to JavaScript, this value is not supported since it is +/// larger than the maximum safe 32bit integer. +const int kMaxUnsignedSMI = 0x3FFFFFFFFFFFFFFF; + +/// A BitField over an enum (or other class whose values implement "index"). +/// Only the first 62 values of the enum can be used as indices. +/// +/// When compiling to JavaScript, this class is not supported. +class BitField { + /// Creates a bit field of all zeros. + /// + /// The given length must be at most 62. + BitField(this._length) + : assert(_length <= _smiBits), + _bits = _allZeros; + + /// Creates a bit field filled with a particular value. + /// + /// If the value argument is true, the bits are filled with ones. Otherwise, + /// the bits are filled with zeros. + /// + /// The given length must be at most 62. + BitField.filled(this._length, bool value) + : assert(_length <= _smiBits), + _bits = value ? _allOnes : _allZeros; + + final int _length; + int _bits; + + static const int _smiBits = 62; // see https://www.dartlang.org/articles/numeric-computation/#smis-and-mints + static const int _allZeros = 0; + static const int _allOnes = kMaxUnsignedSMI; // 2^(_kSMIBits+1)-1 + + /// Returns whether the bit with the given index is set to one. + bool operator [](T index) { + assert(index.index < _length); + return (_bits & 1 << index.index) > 0; + } + + /// Sets the bit with the given index to the given value. + /// + /// If value is true, the bit with the given index is set to one. Otherwise, + /// the bit is set to zero. + void operator []=(T index, bool value) { + assert(index.index < _length); + if (value) + _bits = _bits | (1 << index.index); + else + _bits = _bits & ~(1 << index.index); + } + + /// Sets all the bits to the given value. + /// + /// If the value is true, the bits are all set to one. Otherwise, the bits are + /// all set to zero. Defaults to setting all the bits to zero. + void reset([ bool value = false ]) { + _bits = value ? _allOnes : _allZeros; + } +} \ No newline at end of file diff --git a/packages/flutter/lib/src/foundation/bitfield_unsupported.dart b/packages/flutter/lib/src/foundation/bitfield_unsupported.dart new file mode 100644 index 0000000000..c7baf470b2 --- /dev/null +++ b/packages/flutter/lib/src/foundation/bitfield_unsupported.dart @@ -0,0 +1,34 @@ +// 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. + +/// Unsupported. +const int kMaxUnsignedSMI = 0; + +/// Unsupported. +class BitField { + /// Unsupported. + // Ignored so that both bitfield implementations have the same API. + // ignore: avoid_unused_constructor_parameters + BitField(int length); + + /// Unsupported. + // Ignored so that both bitfield implementations have the same API. + // ignore: avoid_unused_constructor_parameters + BitField.filled(int length, bool value); + + /// Unsupported. + bool operator [](T index) { + throw UnsupportedError('Not supported when compiling to JavaScript'); + } + + /// Unsupported. + void operator []=(T index, bool value) { + throw UnsupportedError('Not supported when compiling to JavaScript'); + } + + /// Unsupported. + void reset([ bool value = false ]) { + throw UnsupportedError('Not supported when compiling to JavaScript'); + } +} diff --git a/packages/flutter/lib/src/foundation/unsupported.dart b/packages/flutter/lib/src/foundation/unsupported.dart new file mode 100644 index 0000000000..7ad4207f1e --- /dev/null +++ b/packages/flutter/lib/src/foundation/unsupported.dart @@ -0,0 +1,7 @@ +/// The largest SMI value. +/// +/// See +/// +/// When compiling to JavaScript, this value is not supported since it is +/// larger than the maximum safe 32bit integer. +const int kMaxUnsignedSMI = 0x3FFFFFFFFFFFFFFF; \ No newline at end of file diff --git a/packages/flutter/lib/src/foundation/unsupported_web.dart b/packages/flutter/lib/src/foundation/unsupported_web.dart new file mode 100644 index 0000000000..8a23100484 --- /dev/null +++ b/packages/flutter/lib/src/foundation/unsupported_web.dart @@ -0,0 +1,7 @@ +/// The largest SMI value. +/// +/// See +/// +/// When compiling to JavaScript, this value is not supported since it is +/// larger than the maximum safe 32bit integer. +const int kMaxUnsignedSMI = 0; \ No newline at end of file diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart index fe8f9687a1..989ca6fb76 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart @@ -49,6 +49,7 @@ class ApplicationPackageFactory { case TargetPlatform.linux_x64: case TargetPlatform.windows_x64: case TargetPlatform.fuchsia: + case TargetPlatform.web: return null; } assert(platform != null); @@ -352,6 +353,7 @@ class ApplicationPackageStore { case TargetPlatform.windows_x64: case TargetPlatform.fuchsia: case TargetPlatform.tester: + case TargetPlatform.web: return null; } return null; diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index 6cfb8bcd9a..9bf928009b 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -26,6 +26,8 @@ enum Artifact { frontendServerSnapshotForEngineDartSdk, engineDartSdkPath, engineDartBinary, + dart2jsSnapshot, + kernelWorkerSnapshot, } String _artifactToFileName(Artifact artifact, [TargetPlatform platform, BuildMode mode]) { @@ -70,6 +72,10 @@ String _artifactToFileName(Artifact artifact, [TargetPlatform platform, BuildMod return 'frontend_server.dart.snapshot'; case Artifact.engineDartBinary: return 'dart'; + case Artifact.dart2jsSnapshot: + return 'flutter_dart2js.dart.snapshot'; + case Artifact.kernelWorkerSnapshot: + return 'flutter_kernel_worker.dart.snapshot'; } assert(false, 'Invalid artifact $artifact.'); return null; @@ -121,6 +127,7 @@ class CachedArtifacts extends Artifacts { case TargetPlatform.windows_x64: case TargetPlatform.fuchsia: case TargetPlatform.tester: + case TargetPlatform.web: return _getHostArtifactPath(artifact, platform, mode); } assert(false, 'Invalid platform $platform.'); @@ -183,13 +190,17 @@ class CachedArtifacts extends Artifacts { case Artifact.engineDartSdkPath: return dartSdkPath; case Artifact.engineDartBinary: - return fs.path.join(dartSdkPath,'bin', _artifactToFileName(artifact)); + return fs.path.join(dartSdkPath, 'bin', _artifactToFileName(artifact)); case Artifact.platformKernelDill: return fs.path.join(_getFlutterPatchedSdkPath(), _artifactToFileName(artifact)); case Artifact.platformLibrariesJson: return fs.path.join(_getFlutterPatchedSdkPath(), 'lib', _artifactToFileName(artifact)); case Artifact.flutterPatchedSdkPath: return _getFlutterPatchedSdkPath(); + case Artifact.dart2jsSnapshot: + return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact)); + case Artifact.kernelWorkerSnapshot: + return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact)); default: assert(false, 'Artifact $artifact not available for platform $platform.'); return null; @@ -205,6 +216,7 @@ class CachedArtifacts extends Artifacts { case TargetPlatform.windows_x64: case TargetPlatform.fuchsia: case TargetPlatform.tester: + case TargetPlatform.web: assert(mode == null, 'Platform $platform does not support different build modes.'); return fs.path.join(engineDir, platformName); case TargetPlatform.ios: @@ -265,6 +277,10 @@ class LocalEngineArtifacts extends Artifacts { return fs.path.join(_hostEngineOutPath, 'dart-sdk'); case Artifact.engineDartBinary: return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', _artifactToFileName(artifact)); + case Artifact.dart2jsSnapshot: + return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact)); + case Artifact.kernelWorkerSnapshot: + return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact)); } assert(false, 'Invalid artifact $artifact.'); return null; diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index cd8ce05a77..2e3322b9a5 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -266,6 +266,7 @@ enum TargetPlatform { windows_x64, fuchsia, tester, + web, } /// iOS target device architecture. @@ -325,6 +326,8 @@ String getNameForTargetPlatform(TargetPlatform platform) { return 'fuchsia'; case TargetPlatform.tester: return 'flutter-tester'; + case TargetPlatform.web: + return 'web'; } assert(false); return null; @@ -346,6 +349,8 @@ TargetPlatform getTargetPlatformForName(String platform) { return TargetPlatform.darwin_x64; case 'linux-x64': return TargetPlatform.linux_x64; + case 'web': + return TargetPlatform.web; } assert(platform != null); return null; @@ -400,6 +405,11 @@ String getIosBuildDirectory() { return fs.path.join(getBuildDirectory(), 'ios'); } +/// Returns the web build output directory. +String getWebBuildDirectory() { + return fs.path.join(getBuildDirectory(), 'web'); +} + /// Returns directory used by incremental compiler (IKG - incremental kernel /// generator) to store cached intermediate state. String getIncrementalCompilerByteStoreDirectory() { diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart index c31f741718..64100e2be4 100644 --- a/packages/flutter_tools/lib/src/commands/build.dart +++ b/packages/flutter_tools/lib/src/commands/build.dart @@ -11,6 +11,7 @@ import 'build_appbundle.dart'; import 'build_bundle.dart'; import 'build_flx.dart'; import 'build_ios.dart'; +import 'build_web.dart'; class BuildCommand extends FlutterCommand { BuildCommand({bool verboseHelp = false}) { @@ -20,6 +21,7 @@ class BuildCommand extends FlutterCommand { addSubcommand(BuildIOSCommand()); addSubcommand(BuildFlxCommand()); addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp)); + addSubcommand(BuildWebCommand()); } @override diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart new file mode 100644 index 0000000000..a1a464585e --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/build_web.dart @@ -0,0 +1,38 @@ +// 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 '../base/logger.dart'; +import '../build_info.dart'; +import '../globals.dart'; +import '../runner/flutter_command.dart' show ExitStatus, FlutterCommandResult; +import '../web/compile.dart'; +import 'build.dart'; + +class BuildWebCommand extends BuildSubCommand { + BuildWebCommand() { + usesTargetOption(); + usesPubOption(); + defaultBuildMode = BuildMode.release; + } + + @override + final String name = 'web'; + + @override + bool get hidden => true; + + @override + final String description = '(EXPERIMENTAL) build a web application bundle.'; + + @override + Future runCommand() async { + final String target = argResults['target']; + final Status status = logger.startProgress('Compiling $target to JavaScript...', timeout: null); + final int result = await webCompiler.compile(target: target); + status.stop(); + return FlutterCommandResult(result == 0 ? ExitStatus.success : ExitStatus.fail); + } +} diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index ba6a2b1db1..7a4490baf1 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -39,6 +39,7 @@ import 'macos/macos_workflow.dart'; import 'run_hot.dart'; import 'usage.dart'; import 'version.dart'; +import 'web/compile.dart'; import 'windows/windows_workflow.dart'; Future runInContext( @@ -91,6 +92,7 @@ Future runInContext( Usage: () => Usage(), UserMessages: () => UserMessages(), WindowsWorkflow: () => const WindowsWorkflow(), + WebCompiler: () => const WebCompiler(), Xcode: () => Xcode(), XcodeProjectInterpreter: () => XcodeProjectInterpreter(), }, diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart new file mode 100644 index 0000000000..4dab7488f8 --- /dev/null +++ b/packages/flutter_tools/lib/src/web/compile.dart @@ -0,0 +1,70 @@ +// 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:meta/meta.dart'; + +import '../artifacts.dart'; +import '../base/common.dart'; +import '../base/context.dart'; +import '../base/file_system.dart'; +import '../base/io.dart'; +import '../base/process_manager.dart'; +import '../build_info.dart'; +import '../convert.dart'; +import '../globals.dart'; + +/// The [WebCompiler] instance. +WebCompiler get webCompiler => context[WebCompiler]; + +/// A wrapper around dart2js for web compilation. +class WebCompiler { + const WebCompiler(); + + /// Compile `target` using dart2js. + /// + /// `minify` controls whether minifaction of the source is enabled. Defaults to `true`. + /// `enabledAssertions` controls whether assertions are enabled. Defaults to `false`. + Future compile({@required String target, bool minify = true, bool enabledAssertions = false}) async { + final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary); + final String dart2jsPath = artifacts.getArtifactPath(Artifact.dart2jsSnapshot); + final String flutterPatchedSdkPath = artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath); + final String librariesPath = fs.path.join(flutterPatchedSdkPath, 'libraries.json'); + final Directory outputDir = fs.directory(getWebBuildDirectory()); + if (!outputDir.existsSync()) { + outputDir.createSync(recursive: true); + } + final String outputPath = fs.path.join(outputDir.path, 'main.dart.js'); + if (!processManager.canRun(engineDartPath)) { + throwToolExit('Unable to find Dart binary at $engineDartPath'); + } + final List command = [ + engineDartPath, + dart2jsPath, + target, + '-o', + '$outputPath', + '--libraries-spec=$librariesPath', + '--platform-binaries=$flutterPatchedSdkPath', + ]; + if (minify) { + command.add('-m'); + } + if (enabledAssertions) { + command.add('--enable-asserts'); + } + printTrace(command.join(' ')); + final Process result = await processManager.start(command); + result + .stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(printStatus); + result + .stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(printError); + return result.exitCode; + } +} diff --git a/packages/flutter_tools/test/web/compile_test.dart b/packages/flutter_tools/test/web/compile_test.dart new file mode 100644 index 0000000000..0316a33a5b --- /dev/null +++ b/packages/flutter_tools/test/web/compile_test.dart @@ -0,0 +1,54 @@ +// 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/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/globals.dart'; +import 'package:flutter_tools/src/web/compile.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../src/context.dart'; + +void main() { + final MockProcessManager mockProcessManager = MockProcessManager(); + final MockProcess mockProcess = MockProcess(); + final BufferLogger mockLogger = BufferLogger(); + + testUsingContext('invokes dart2js with correct arguments', () async { + const WebCompiler webCompiler = WebCompiler(); + final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary); + final String dart2jsPath = artifacts.getArtifactPath(Artifact.dart2jsSnapshot); + final String flutterPatchedSdkPath = artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath); + final String librariesPath = fs.path.join(flutterPatchedSdkPath, 'libraries.json'); + + when(mockProcess.stdout).thenAnswer((Invocation invocation) => const Stream>.empty()); + when(mockProcess.stderr).thenAnswer((Invocation invocation) => const Stream>.empty()); + when(mockProcess.exitCode).thenAnswer((Invocation invocation) async => 0); + when(mockProcessManager.start(any)).thenAnswer((Invocation invocation) async => mockProcess); + when(mockProcessManager.canRun(engineDartPath)).thenReturn(true); + + await webCompiler.compile(target: 'lib/main.dart'); + + final String outputPath = fs.path.join('build', 'web', 'main.dart.js'); + verify(mockProcessManager.start([ + engineDartPath, + dart2jsPath, + 'lib/main.dart', + '-o', + outputPath, + '--libraries-spec=$librariesPath', + '--platform-binaries=$flutterPatchedSdkPath', + '-m', + ])).called(1); + }, overrides: { + ProcessManager: () => mockProcessManager, + Logger: () => mockLogger, + }); +} + +class MockProcessManager extends Mock implements ProcessManager {} +class MockProcess extends Mock implements Process {} \ No newline at end of file