blob: 76e4f9aa096ae4133ab80bb9a415ce8ebd450578 [file] [log] [blame]
// 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;
}