// 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/context.dart'; import 'base/file_system.dart'; import 'base/platform.dart'; import 'compile.dart'; import 'globals.dart'; import 'project.dart'; const String _kMultiRootScheme = 'org-dartlang-app'; /// The [CodeGenerator] instance. /// /// If [experimentalBuildEnabled] is false, this will contain an unsupported /// implementation. CodeGenerator get codeGenerator => context[CodeGenerator]; /// 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.' bool get experimentalBuildEnabled { return _experimentalBuildEnabled ??= platform.environment['FLUTTER_EXPERIMENTAL_BUILD']?.toLowerCase() == 'true'; } bool _experimentalBuildEnabled; @visibleForTesting set experimentalBuildEnabled(bool value) { _experimentalBuildEnabled = value; } /// 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. abstract class CodeGenerator { const CodeGenerator(); /// Run a partial build include code generators but not kernel. Future generate({@required String mainPath}) async { await build( mainPath: mainPath, aot: false, linkPlatformKernelIn: false, trackWidgetCreation: false, targetProductVm: false, disableKernelGeneration: true, ); } /// Run a full 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 String mainPath, @required bool aot, @required bool linkPlatformKernelIn, @required bool trackWidgetCreation, @required bool targetProductVm, List extraFrontEndOptions = const [], bool disableKernelGeneration = false, }); /// Starts a persistent code generting daemon. /// /// The defines of the daemon command are the arguments required in the /// flutter_build kernel builder. Future daemon({ @required String mainPath, bool linkPlatformKernelIn = false, bool targetProductVm = false, bool trackWidgetCreation = false, List extraFrontEndOptions = const [], }); /// Invalidates a generated build script by deleting it. /// /// Must be called any time a pubspec file update triggers a corresponding change /// in .packages. Future invalidateBuildScript(); // Generates a synthetic package under .dart_tool/flutter_tool which is in turn // used to generate a build script. Future generateBuildScript(); } class UnsupportedCodeGenerator extends CodeGenerator { const UnsupportedCodeGenerator(); @override Future build({ String mainPath, bool aot, bool linkPlatformKernelIn, bool trackWidgetCreation, bool targetProductVm, List extraFrontEndOptions = const [], bool disableKernelGeneration = false, }) { throw UnsupportedError('build_runner is not currently supported.'); } @override Future generateBuildScript() { throw UnsupportedError('build_runner is not currently supported.'); } @override Future invalidateBuildScript() { throw UnsupportedError('build_runner is not currently supported.'); } @override Future daemon({ String mainPath, bool linkPlatformKernelIn = false, bool targetProductVm = false, bool trackWidgetCreation = false, List extraFrontEndOptions = const [], }) { throw UnsupportedError('build_runner is not currently supported.'); } } abstract class CodegenDaemon { /// Whether the previously enqueued build was successful. Stream get buildResults; CodegenStatus get lastStatus; /// Starts a new build. void startBuild(); File get packagesFile; File get dillFile; } /// The result of running a build through a [CodeGenerator]. /// /// If no dill or packages file is generated, they will be null. class CodeGenerationResult { const CodeGenerationResult(this.packagesFile, this.dillFile); final File packagesFile; final File dillFile; } /// 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 CodeGeneratingKernelCompiler implements KernelCompiler { const CodeGeneratingKernelCompiler(); @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 = TargetModel.flutter, }) 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'); } try { final CodeGenerationResult buildResult = await codeGenerator.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); } } } /// An implementation of a [ResidentCompiler] which runs a [BuildRunner] before /// talking to the CFE. class CodeGeneratingResidentCompiler implements ResidentCompiler { CodeGeneratingResidentCompiler._(this._residentCompiler, this._codegenDaemon); /// Creates a new [ResidentCompiler] and configures a [BuildDaemonClient] to /// run builds. static Future create({ @required String mainPath, bool trackWidgetCreation = false, CompilerMessageConsumer compilerMessageConsumer = printError, bool unsafePackageSerialization = false, }) async { final FlutterProject flutterProject = await FlutterProject.current(); final CodegenDaemon codegenDaemon = await codeGenerator.daemon( extraFrontEndOptions: [], linkPlatformKernelIn: false, mainPath: mainPath, targetProductVm: false, trackWidgetCreation: trackWidgetCreation, ); codegenDaemon.startBuild(); final CodegenStatus status = await codegenDaemon.buildResults.firstWhere((CodegenStatus status) { return status ==CodegenStatus.Succeeded || status == CodegenStatus.Failed; }); if (status == CodegenStatus.Failed) { printError('Codegeneration failed, halting build.'); } final ResidentCompiler residentCompiler = ResidentCompiler( artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath), trackWidgetCreation: trackWidgetCreation, packagesPath: codegenDaemon.packagesFile.path, fileSystemRoots: [ fs.path.join(flutterProject.generated.absolute.path, 'lib${platform.pathSeparator}'), fs.path.join(flutterProject.directory.path, 'lib${platform.pathSeparator}'), ], fileSystemScheme: _kMultiRootScheme, targetModel: TargetModel.flutter, unsafePackageSerialization: unsafePackageSerialization, ); return CodeGeneratingResidentCompiler._(residentCompiler, codegenDaemon); } final ResidentCompiler _residentCompiler; final CodegenDaemon _codegenDaemon; @override void accept() { _residentCompiler.accept(); } @override Future compileExpression(String expression, List definitions, List typeDefinitions, String libraryUri, String klass, bool isStatic) { return _residentCompiler.compileExpression(expression, definitions, typeDefinitions, libraryUri, klass, isStatic); } @override Future recompile(String mainPath, List invalidatedFiles, {String outputPath, String packagesFilePath}) async { if (_codegenDaemon.lastStatus != CodegenStatus.Succeeded && _codegenDaemon.lastStatus != CodegenStatus.Failed) { await _codegenDaemon.buildResults.firstWhere((CodegenStatus status) { return status ==CodegenStatus.Succeeded || status == CodegenStatus.Failed; }); } if (_codegenDaemon.lastStatus == CodegenStatus.Failed) { printError('Codegeneration failed, halting build.'); } // Delete this file so that the frontend_server can handle multi-root. // TODO(jonahwilliams): investigate frontend_server behavior in the presence // of multi-root and initialize from dill. if (await fs.file(outputPath).exists()) { await fs.file(outputPath).delete(); } return _residentCompiler.recompile( mainPath, invalidatedFiles, outputPath: outputPath, packagesFilePath: _codegenDaemon.packagesFile.path, ); } @override Future reject() { return _residentCompiler.reject(); } @override void reset() { _residentCompiler.reset(); } @override Future shutdown() { return _residentCompiler.shutdown(); } } /// The current status of a codegen build. enum CodegenStatus { /// The build has started running. /// /// If this is the current status when running a hot reload, an additional build does /// not need to be started. Started, /// The build succeeded. Succeeded, /// The build failed. Failed }