diff --git a/packages/flutter_tools/lib/src/build_runner/build_kernel_compiler.dart b/packages/flutter_tools/lib/src/build_runner/build_kernel_compiler.dart new file mode 100644 index 00000000000..af9b8f73687 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_runner/build_kernel_compiler.dart @@ -0,0 +1,64 @@ +// 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 '../base/file_system.dart'; +import '../compile.dart'; +import '../globals.dart'; +import 'build_runner.dart'; + +/// An implementation of the [KernelCompiler] which delegates to build_runner. +/// +/// Only a subset of the arguments provided to the [KernelCompiler] are +/// supported here. Using the build pipeline implies a fixed multiroot +/// filesystem and requires a pubspec. +/// +/// This is only safe to use if [experimentalBuildEnabled] is true. +class BuildKernelCompiler implements KernelCompiler { + const BuildKernelCompiler(); + + @override + Future compile({ + String mainPath, + String outputFilePath, + bool linkPlatformKernelIn = false, + bool aot = false, + bool trackWidgetCreation, + List extraFrontEndOptions, + String incrementalCompilerByteStorePath, + bool targetProductVm = false, + // These arguments are currently unused. + String sdkRoot, + String packagesPath, + List fileSystemRoots, + String fileSystemScheme, + String depFilePath, + TargetModel targetModel, + }) async { + if (fileSystemRoots != null || fileSystemScheme != null || depFilePath != null || targetModel != null || sdkRoot != null || packagesPath != null) { + printTrace('fileSystemRoots, fileSystemScheme, depFilePath, targetModel,' + 'sdkRoot, packagesPath are not supported when using the experimental ' + 'build* pipeline'); + } + final BuildRunner buildRunner = buildRunnerFactory.create(); + try { + final BuildResult buildResult = await buildRunner.build( + aot: aot, + linkPlatformKernelIn: linkPlatformKernelIn, + trackWidgetCreation: trackWidgetCreation, + mainPath: mainPath, + targetProductVm: targetProductVm, + extraFrontEndOptions: extraFrontEndOptions + ); + final File outputFile = fs.file(outputFilePath); + if (!await outputFile.exists()) { + await outputFile.create(); + } + await outputFile.writeAsBytes(await buildResult.dillFile.readAsBytes()); + return CompilerOutput(outputFilePath, 0); + } on Exception catch (err) { + printError('Compilation Failed: $err'); + return const CompilerOutput(null, 1); + } + } +} diff --git a/packages/flutter_tools/lib/src/build_runner/build_runner.dart b/packages/flutter_tools/lib/src/build_runner/build_runner.dart new file mode 100644 index 00000000000..7035dddd8df --- /dev/null +++ b/packages/flutter_tools/lib/src/build_runner/build_runner.dart @@ -0,0 +1,153 @@ +// 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 'dart:convert'; + +import 'package:meta/meta.dart'; + +import '../artifacts.dart'; +import '../base/context.dart'; +import '../base/file_system.dart'; +import '../base/io.dart'; +import '../base/platform.dart'; +import '../base/process_manager.dart'; +import '../cache.dart'; +import '../dart/package_map.dart'; +import '../globals.dart'; +import '../project.dart'; + +/// The [BuildRunnerFactory] instance. +BuildRunnerFactory get buildRunnerFactory => context[BuildRunnerFactory]; + +/// Whether to attempt to build a flutter project using build* libraries. +/// +/// This requires both an experimental opt in via the environment variable +/// 'FLUTTER_EXPERIMENTAL_BUILD' and that the project itself has a +/// dependency on the package 'flutter_build' and 'build_runner.' +FutureOr get experimentalBuildEnabled async { + if (_experimentalBuildEnabled != null) { + return _experimentalBuildEnabled; + } + final bool flagEnabled = platform.environment['FLUTTER_EXPERIMENTAL_BUILD']?.toLowerCase() == 'true'; + if (!flagEnabled) { + return _experimentalBuildEnabled = false; + } + final FlutterProject flutterProject = await FlutterProject.current(); + final Map packages = PackageMap(flutterProject.packagesFile.path).map; + return _experimentalBuildEnabled = packages.containsKey('flutter_build') && packages.containsKey('build_runner'); +} +bool _experimentalBuildEnabled; + +@visibleForTesting +set experimentalBuildEnabled(bool value) { + _experimentalBuildEnabled = value; +} + +/// An injectable factory to create instances of [BuildRunner]. +class BuildRunnerFactory { + const BuildRunnerFactory(); + + /// Creates a new [BuildRunner] instance. + BuildRunner create() { + return BuildRunner(); + } +} + +/// A wrapper for a build_runner process which delegates to a generated +/// build script. +/// +/// This is only enabled if [experimentalBuildEnabled] is true, and only for +/// external flutter users. +class BuildRunner { + + /// Run a build_runner build and return the resulting .packages and dill file. + /// + /// The defines of the build command are the arguments required in the + /// flutter_build kernel builder. + Future build({ + @required bool aot, + @required bool linkPlatformKernelIn, + @required bool trackWidgetCreation, + @required bool targetProductVm, + @required String mainPath, + @required List extraFrontEndOptions, + }) async { + final FlutterProject flutterProject = await FlutterProject.current(); + final String frontendServerPath = artifacts.getArtifactPath( + Artifact.frontendServerSnapshotForEngineDartSdk + ); + final String pubExecutable = fs.path.join(Cache.flutterRoot, 'bin', 'cache', 'dart-sdk','bin', 'pub'); + final String sdkRoot = artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath); + final String engineDartBinaryPath = artifacts.getArtifactPath(Artifact.engineDartBinary); + final String packagesPath = flutterProject.packagesFile.absolute.path; + final Process process = await processManager.start([ + '$pubExecutable', + 'run', + 'build_runner', + 'build', + '--define', 'flutter_build|kernel=disabled=false', + '--define', 'flutter_build|kernel=aot=$aot', + '--define', 'flutter_build|kernel=linkPlatformKernelIn=$linkPlatformKernelIn', + '--define', 'flutter_build|kernel=trackWidgetCreation=$trackWidgetCreation', + '--define', 'flutter_build|kernel=targetProductVm=$targetProductVm', + '--define', 'flutter_build|kernel=mainPath=$mainPath', + '--define', 'flutter_build|kernel=packagesPath=$packagesPath', + '--define', 'flutter_build|kernel=sdkRoot=$sdkRoot', + '--define', 'flutter_build|kernel=frontendServerPath=$frontendServerPath', + '--define', 'flutter_build|kernel=engineDartBinaryPath=$engineDartBinaryPath', + '--define', 'flutter_build|kernel=extraFrontEndOptions=${extraFrontEndOptions ?? const []}', + ]); + process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(_handleOutput); + process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(_handleError); + final int exitCode = await process.exitCode; + if (exitCode != 0) { + throw Exception('build_runner exited with non-zero exit code: $exitCode'); + } + /// We don't check for this above because it might be generated for the + /// first time by invoking the build. + final Directory dartTool = flutterProject.dartTool; + final String projectName = flutterProject.manifest.appName; + final Directory generatedDirectory = dartTool + .absolute + .childDirectory('build') + .childDirectory('generated') + .childDirectory(projectName); + if (!await generatedDirectory.exists()) { + throw Exception('build_runner cannot find generated directory'); + } + final String relativeMain = fs.path.relative(mainPath, from: flutterProject.directory.path); + final File packagesFile = fs.file( + fs.path.join(generatedDirectory.path, fs.path.setExtension(relativeMain, '.packages')) + ); + final File dillFile = fs.file( + fs.path.join(generatedDirectory.path, fs.path.setExtension(relativeMain, '.app.dill')) + ); + if (!await packagesFile.exists() || !await dillFile.exists()) { + throw Exception('build_runner did not produce output at expected location: ${dillFile.path} missing'); + } + return BuildResult(packagesFile, dillFile); + } + + void _handleOutput(String line) { + printTrace(line); + } + + void _handleError(String line) { + printError(line); + } +} + +class BuildResult { + const BuildResult(this.packagesFile, this.dillFile); + + final File packagesFile; + final File dillFile; +} diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 260d29256b3..54ff657227a 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -21,6 +21,7 @@ import 'base/platform.dart'; import 'base/time.dart'; import 'base/user_messages.dart'; import 'base/utils.dart'; +import 'build_runner/build_runner.dart'; import 'cache.dart'; import 'compile.dart'; import 'devfs.dart'; @@ -59,6 +60,7 @@ Future runInContext( Artifacts: () => CachedArtifacts(), AssetBundleFactory: () => AssetBundleFactory.defaultInstance, BotDetector: () => const BotDetector(), + BuildRunnerFactory: () => const BuildRunnerFactory(), Cache: () => Cache(), CocoaPods: () => CocoaPods(), CocoaPodsValidator: () => const CocoaPodsValidator(), diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 77741dfd9a6..5ac90fba6a0 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -103,6 +103,9 @@ class FlutterProject { /// The `.flutter-plugins` file of this project. File get flutterPluginsFile => directory.childFile('.flutter-plugins'); + /// The `.dart-tool` directory of this project. + Directory get dartTool => directory.childDirectory('.dart_tool'); + /// The example sub-project of this project. FlutterProject get example => FlutterProject( _exampleDirectory(directory), diff --git a/packages/flutter_tools/test/build_runner/build_kernel_compiler.dart b/packages/flutter_tools/test/build_runner/build_kernel_compiler.dart new file mode 100644 index 00000000000..f6ccad300c4 --- /dev/null +++ b/packages/flutter_tools/test/build_runner/build_kernel_compiler.dart @@ -0,0 +1,60 @@ +// 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/build_runner/build_kernel_compiler.dart'; +import 'package:flutter_tools/src/build_runner/build_runner.dart'; +import 'package:flutter_tools/src/compile.dart'; +import 'package:mockito/mockito.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; + +void main() { + group(BuildKernelCompiler, () { + final MockBuildRunnerFactory mockBuildRunnerFactory = MockBuildRunnerFactory(); + final MockBuildRunner mockBuildRunner = MockBuildRunner(); + final MockFileSystem mockFileSystem = MockFileSystem(); + final MockFile packagesFile = MockFile(); + final MockFile dillFile = MockFile(); + final MockFile outputFile = MockFile(); + + when(mockFileSystem.file('main.app.dill')).thenReturn(dillFile); + when(mockFileSystem.file('.packages')).thenReturn(packagesFile); + when(mockFileSystem.file('output.app.dill')).thenReturn(outputFile); + when(packagesFile.exists()).thenAnswer((Invocation invocation) async => true); + when(dillFile.exists()).thenAnswer((Invocation invocation) async => true); + when(outputFile.exists()).thenAnswer((Invocation invocation) async => true); + when(mockBuildRunnerFactory.create()).thenReturn(mockBuildRunner); + when(dillFile.readAsBytes()).thenAnswer((Invocation invocation) async => [0, 1, 2, 3]); + + testUsingContext('delegates to build_runner', () async { + const BuildKernelCompiler kernelCompiler = BuildKernelCompiler(); + when(mockBuildRunner.build( + aot: anyNamed('aot'), + extraFrontEndOptions: anyNamed('extraFrontEndOptions'), + linkPlatformKernelIn: anyNamed('linkPlatformKernelIn'), + mainPath: anyNamed('mainPath'), + targetProductVm: anyNamed('targetProductVm'), + trackWidgetCreation: anyNamed('trackWidgetCreation') + )).thenAnswer((Invocation invocation) async { + return BuildResult(fs.file('.packages'), fs.file('main.app.dill')); + }); + final CompilerOutput buildResult = await kernelCompiler.compile( + outputFilePath: 'output.app.dill', + ); + expect(buildResult.outputFilename, 'output.app.dill'); + expect(buildResult.errorCount, 0); + verify(outputFile.writeAsBytes([0, 1, 2, 3])).called(1); + }, overrides: { + BuildRunnerFactory: () => mockBuildRunnerFactory, + FileSystem: () => mockFileSystem, + }); + }); +} + +class MockBuildRunnerFactory extends Mock implements BuildRunnerFactory {} +class MockBuildRunner extends Mock implements BuildRunner {} +class MockFileSystem extends Mock implements FileSystem {} +class MockFile extends Mock implements File {} diff --git a/packages/flutter_tools/test/build_runner/build_runner_test.dart b/packages/flutter_tools/test/build_runner/build_runner_test.dart new file mode 100644 index 00000000000..289adbd1cc0 --- /dev/null +++ b/packages/flutter_tools/test/build_runner/build_runner_test.dart @@ -0,0 +1,104 @@ +// 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:convert'; + +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_runner/build_runner.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; + +void main() { + group('experimentalBuildEnabled', () { + final MockProcessManager mockProcessManager = MockProcessManager(); + final MockPlatform mockPlatform = MockPlatform(); + final MockFileSystem mockFileSystem = MockFileSystem(); + + setUp(() { + experimentalBuildEnabled = null; + }); + testUsingContext('is enabled if environment variable is enabled and project ' + 'contains a dependency on flutter_build and build_runner', () async { + final MockDirectory projectDirectory = MockDirectory(); + final MockDirectory exampleDirectory = MockDirectory(); + final MockFile packagesFile = MockFile(); + final MockFile pubspecFile = MockFile(); + final MockFile examplePubspecFile = MockFile(); + const String packages = r''' +flutter_build:file:///Users/tester/.pub-cache/hosted/pub.dartlang.org/flutter_build/lib/ +build_runner:file:///Users/tester/.pub-cache/hosted/pub.dartlang.org/build_runner/lib/ +example:lib/ +'''; + when(mockPlatform.environment).thenReturn({'FLUTTER_EXPERIMENTAL_BUILD': 'true'}); + when(mockFileSystem.currentDirectory).thenReturn(projectDirectory); + when(mockFileSystem.isFileSync(any)).thenReturn(false); + when(projectDirectory.childFile('pubspec.yaml')).thenReturn(pubspecFile); + when(projectDirectory.childFile('.packages')).thenReturn(packagesFile); + when(projectDirectory.childDirectory('example')).thenReturn(exampleDirectory); + when(exampleDirectory.childFile('pubspec.yaml')).thenReturn(examplePubspecFile); + when(packagesFile.path).thenReturn('/test/.packages'); + when(pubspecFile.path).thenReturn('/test/pubspec.yaml'); + when(examplePubspecFile.path).thenReturn('/test/example/pubspec.yaml'); + when(mockFileSystem.file('/test/.packages')).thenReturn(packagesFile); + when(packagesFile.readAsBytesSync()).thenReturn(utf8.encode(packages)); + + expect(await experimentalBuildEnabled, true); + }, overrides: { + ProcessManager: () => mockProcessManager, + Platform: () => mockPlatform, + FileSystem: () => mockFileSystem, + }); + + testUsingContext('is not enabled if environment variable is enabled and project ' + 'does not contain a dependency on flutter_build', () async { + final MockDirectory projectDirectory = MockDirectory(); + final MockDirectory exampleDirectory = MockDirectory(); + final MockFile packagesFile = MockFile(); + final MockFile pubspecFile = MockFile(); + final MockFile examplePubspecFile = MockFile(); + const String packages = r''' +build_runner:file:///Users/tester/.pub-cache/hosted/pub.dartlang.org/build_runner/lib/ +example:lib/ +'''; + when(mockPlatform.environment).thenReturn({'FLUTTER_EXPERIMENTAL_BUILD': 'true'}); + when(mockFileSystem.currentDirectory).thenReturn(projectDirectory); + when(mockFileSystem.isFileSync(any)).thenReturn(false); + when(projectDirectory.childFile('pubspec.yaml')).thenReturn(pubspecFile); + when(projectDirectory.childFile('.packages')).thenReturn(packagesFile); + when(projectDirectory.childDirectory('example')).thenReturn(exampleDirectory); + when(exampleDirectory.childFile('pubspec.yaml')).thenReturn(examplePubspecFile); + when(packagesFile.path).thenReturn('/test/.packages'); + when(pubspecFile.path).thenReturn('/test/pubspec.yaml'); + when(examplePubspecFile.path).thenReturn('/test/example/pubspec.yaml'); + when(mockFileSystem.file('/test/.packages')).thenReturn(packagesFile); + when(packagesFile.readAsBytesSync()).thenReturn(utf8.encode(packages)); + + expect(await experimentalBuildEnabled, false); + }, overrides: { + ProcessManager: () => mockProcessManager, + Platform: () => mockPlatform, + FileSystem: () => mockFileSystem, + }); + + + testUsingContext('is not enabed if environment varable is not enabled', () async { + when(mockPlatform.environment).thenReturn({}); + expect(await experimentalBuildEnabled, false); + }, overrides: { + ProcessManager: () => mockProcessManager, + Platform: () => mockPlatform, + FileSystem: () => mockFileSystem, + }); + }); +} + +class MockProcessManager extends Mock implements ProcessManager {} +class MockPlatform extends Mock implements Platform {} +class MockFileSystem extends Mock implements FileSystem {} +class MockDirectory extends Mock implements Directory {} +class MockFile extends Mock implements File {}