// Copyright 2014 The Flutter 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 'package:build_daemon/client.dart'; import 'package:build_daemon/data/build_status.dart'; import 'package:build_daemon/data/build_status.dart' as build; import 'package:build_daemon/data/build_target.dart'; import 'package:build_daemon/data/server_log.dart'; import 'package:crypto/crypto.dart' show md5; import 'package:yaml/yaml.dart'; import '../artifacts.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../codegen.dart'; import '../dart/pub.dart'; import '../dart/sdk.dart'; import '../globals.dart' as globals; import '../project.dart'; /// The minimum version of build_runner we can support in the flutter tool. const String kMinimumBuildRunnerVersion = '1.7.1'; const String kSupportedBuildDaemonVersion = '2.1.0'; /// 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 extends CodeGenerator { const BuildRunner(); @override Future<void> generateBuildScript(FlutterProject flutterProject) async { final Directory entrypointDirectory = globals.fs.directory(globals.fs.path.join(flutterProject.dartTool.path, 'build', 'entrypoint')); final Directory generatedDirectory = globals.fs.directory(globals.fs.path.join(flutterProject.dartTool.path, 'flutter_tool')); final File buildSnapshot = entrypointDirectory.childFile('build.dart.snapshot'); final File scriptIdFile = entrypointDirectory.childFile('id'); final File syntheticPubspec = generatedDirectory.childFile('pubspec.yaml'); // Check if contents of builders changed. If so, invalidate build script // and regenerate. final YamlMap builders = flutterProject.builders; final List<int> appliedBuilderDigest = _produceScriptId(builders); if (scriptIdFile.existsSync() && buildSnapshot.existsSync()) { final List<int> previousAppliedBuilderDigest = scriptIdFile.readAsBytesSync(); bool digestsAreEqual = false; if (appliedBuilderDigest.length == previousAppliedBuilderDigest.length) { digestsAreEqual = true; for (int i = 0; i < appliedBuilderDigest.length; i++) { if (appliedBuilderDigest[i] != previousAppliedBuilderDigest[i]) { digestsAreEqual = false; break; } } } if (digestsAreEqual) { return; } } // Clean-up all existing artifacts. if (flutterProject.dartTool.existsSync()) { flutterProject.dartTool.deleteSync(recursive: true); } final Status status = globals.logger.startProgress('generating build script...', timeout: null); try { generatedDirectory.createSync(recursive: true); entrypointDirectory.createSync(recursive: true); flutterProject.dartTool.childDirectory('build').childDirectory('generated').createSync(recursive: true); final StringBuffer stringBuffer = StringBuffer(); stringBuffer.writeln('name: flutter_tool'); stringBuffer.writeln('dependencies:'); final YamlMap builders = flutterProject.builders; if (builders != null) { for (final String name in builders.keys.cast<String>()) { final Object node = builders[name]; // For relative paths, make sure it is accounted for // parent directories. if (node is YamlMap && node['path'] != null) { final String path = node['path'] as String; if (globals.fs.path.isRelative(path)) { final String convertedPath = globals.fs.path.join('..', '..', path); stringBuffer.writeln(' $name:'); stringBuffer.writeln(' path: $convertedPath'); } else { stringBuffer.writeln(' $name: $node'); } } else { stringBuffer.writeln(' $name: $node'); } } } stringBuffer.writeln(' build_runner: ^$kMinimumBuildRunnerVersion'); stringBuffer.writeln(' build_daemon: $kSupportedBuildDaemonVersion'); syntheticPubspec.writeAsStringSync(stringBuffer.toString()); await pub.get( context: PubContext.pubGet, directory: generatedDirectory.path, upgrade: false, checkLastModified: false, ); if (!scriptIdFile.existsSync()) { scriptIdFile.createSync(recursive: true); } scriptIdFile.writeAsBytesSync(appliedBuilderDigest); final ProcessResult generateResult = await globals.processManager.run(<String>[ sdkBinaryName('pub'), 'run', 'build_runner', 'generate-build-script', ], workingDirectory: syntheticPubspec.parent.path); if (generateResult.exitCode != 0) { throwToolExit('Error generating build_script snapshot: ${generateResult.stderr}'); } final File buildScript = globals.fs.file(generateResult.stdout.trim()); final ProcessResult result = await globals.processManager.run(<String>[ globals.artifacts.getArtifactPath(Artifact.engineDartBinary), '--snapshot=${buildSnapshot.path}', '--snapshot-kind=app-jit', '--packages=${globals.fs.path.join(generatedDirectory.path, '.packages')}', buildScript.path, ]); if (result.exitCode != 0) { throwToolExit('Error generating build_script snapshot: ${result.stderr}'); } } finally { status.stop(); } } @override Future<CodegenDaemon> daemon( FlutterProject flutterProject, { String mainPath, bool linkPlatformKernelIn = false, bool trackWidgetCreation = false, List<String> extraFrontEndOptions = const <String> [], }) async { await generateBuildScript(flutterProject); final String engineDartBinaryPath = globals.artifacts.getArtifactPath(Artifact.engineDartBinary); final File buildSnapshot = flutterProject .dartTool .childDirectory('build') .childDirectory('entrypoint') .childFile('build.dart.snapshot'); final String scriptPackagesPath = flutterProject .dartTool .childDirectory('flutter_tool') .childFile('.packages') .path; final Status status = globals.logger.startProgress('starting build daemon...', timeout: null); BuildDaemonClient buildDaemonClient; try { final List<String> command = <String>[ engineDartBinaryPath, '--packages=$scriptPackagesPath', buildSnapshot.path, 'daemon', '--skip-build-script-check', '--delete-conflicting-outputs', ]; buildDaemonClient = await BuildDaemonClient.connect( flutterProject.directory.path, command, logHandler: (ServerLog log) { if (log.message != null) { globals.printTrace(log.message); } }, ); } finally { status.stop(); } // Empty string indicates we should build everything. final OutputLocation outputLocation = OutputLocation((OutputLocationBuilder b) => b ..output = '' ..useSymlinks = false ..hoist = false, ); buildDaemonClient.registerBuildTarget(DefaultBuildTarget((DefaultBuildTargetBuilder builder) { builder.target = 'lib'; builder.outputLocation = outputLocation.toBuilder(); })); buildDaemonClient.registerBuildTarget(DefaultBuildTarget((DefaultBuildTargetBuilder builder) { builder.target = 'test'; builder.outputLocation = outputLocation.toBuilder(); })); return _BuildRunnerCodegenDaemon(buildDaemonClient); } } class _BuildRunnerCodegenDaemon implements CodegenDaemon { _BuildRunnerCodegenDaemon(this.buildDaemonClient); final BuildDaemonClient buildDaemonClient; @override CodegenStatus get lastStatus => _lastStatus; CodegenStatus _lastStatus; @override Stream<CodegenStatus> get buildResults => buildDaemonClient.buildResults.map((build.BuildResults results) { if (results.results.first.status == BuildStatus.failed) { return _lastStatus = CodegenStatus.Failed; } if (results.results.first.status == BuildStatus.started) { return _lastStatus = CodegenStatus.Started; } if (results.results.first.status == BuildStatus.succeeded) { return _lastStatus = CodegenStatus.Succeeded; } _lastStatus = null; return null; }); @override void startBuild() { buildDaemonClient.startBuild(); } } // Sorts the builders by name and produces a hashcode of the resulting iterable. List<int> _produceScriptId(YamlMap builders) { if (builders == null || builders.isEmpty) { return md5.convert(globals.platform.version.codeUnits).bytes; } final List<String> orderedBuilderNames = builders.keys .cast<String>() .toList()..sort(); final List<String> orderedBuilderValues = builders.values .map((dynamic value) => value.toString()) .toList()..sort(); return md5.convert(<String>[ ...orderedBuilderNames, ...orderedBuilderValues, globals.platform.version, ].join('').codeUnits).bytes; }