Skip to content

refactor build fn to use a class for managing state #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 5 additions & 263 deletions lib/src/generate/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,17 @@
// for details. 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 'dart:io';

import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;

import '../asset/asset.dart';
import '../asset/exceptions.dart';
import '../asset/cache.dart';
import '../asset/file_based.dart';
import '../asset/id.dart';
import '../asset/reader.dart';
import '../asset/writer.dart';
import '../asset_graph/graph.dart';
import '../asset_graph/node.dart';
import '../builder/builder.dart';
import '../builder/build_step_impl.dart';
import '../package_graph/package_graph.dart';
import 'build_impl.dart';
import 'build_result.dart';
import 'input_set.dart';
import 'phase.dart';

/// Runs all of the [Phases] in [phaseGroups].
Expand Down Expand Up @@ -54,261 +45,12 @@ Future<BuildResult> build(List<List<Phase>> phaseGroups,
writer ??=
new CachedAssetWriter(cache, new FileBasedAssetWriter(packageGraph));

/// Run the build in a zone.
var result = await runZoned(() async {
try {
/// Delete all previous outputs!
await _deletePreviousOutputs(phaseGroups);

/// Run a fresh build.
var result = await _runPhases(phaseGroups);

// Write out the new build_outputs file.
var buildOutputsAsset = new Asset(
_buildOutputsId,
JSON.encode(
result.outputs.map((output) => output.id.serialize()).toList()));
await writer.writeAsString(buildOutputsAsset);

return result;
} catch (e, s) {
return new BuildResult(BuildStatus.Failure, BuildType.Full, [],
exception: e, stackTrace: s);
}
}, zoneValues: {
_assetGraphKey: new AssetGraph(),
_assetReaderKey: reader,
_assetWriterKey: writer,
_packageGraphKey: packageGraph,
});
/// Run the build!
var result = await new BuildImpl(
new AssetGraph(), reader, writer, packageGraph, phaseGroups)
.runBuild();

await logListener.cancel();

return result;
}

/// Keys for reading zone local values.
Symbol _assetGraphKey = #buildAssetGraph;
Symbol _assetReaderKey = #buildAssetReader;
Symbol _assetWriterKey = #buildAssetWriter;
Symbol _packageGraphKey = #buildPackageGraph;

/// Getters for zone local values.
AssetGraph get _assetGraph => Zone.current[_assetGraphKey];
AssetReader get _reader => Zone.current[_assetReaderKey];
AssetWriter get _writer => Zone.current[_assetWriterKey];
PackageGraph get _packageGraph => Zone.current[_packageGraphKey];

/// Asset containing previous build outputs.
AssetId get _buildOutputsId =>
new AssetId(_packageGraph.root.name, '.build/build_outputs.json');

/// Deletes all previous output files.
Future _deletePreviousOutputs(List<List<Phase>> phaseGroups) async {
if (await _reader.hasInput(_buildOutputsId)) {
// Cache file exists, just delete all outputs contained in it.
var previousOutputs =
JSON.decode(await _reader.readAsString(_buildOutputsId));
await _writer.delete(_buildOutputsId);
await Future.wait(previousOutputs.map((output) {
return _writer.delete(new AssetId.deserialize(output));
}));
return;
}

// No cache file exists, run `declareOutputs` on all phases and collect all
// outputs which conflict with existing assets.
final allInputs = await _allInputs(phaseGroups);
final conflictingOutputs = new Set<AssetId>();
for (var group in phaseGroups) {
final groupOutputIds = <AssetId>[];
for (var phase in group) {
var inputs = _matchingInputs(allInputs, phase.inputSets);
for (var input in inputs) {
for (var builder in phase.builders) {
var outputs = builder.declareOutputs(input);

groupOutputIds.addAll(outputs);
for (var output in outputs) {
if (allInputs[output.package]?.contains(output) == true) {
conflictingOutputs.add(output);
}
}
}
}
}

/// Once the group is done, add all outputs so they can be used in the next
/// phase.
for (var outputId in groupOutputIds) {
allInputs.putIfAbsent(outputId.package, () => new Set<AssetId>());
allInputs[outputId.package].add(outputId);
}
}

// Check conflictingOuputs, prompt user to delete files.
if (conflictingOutputs.isEmpty) return;

stdout.writeln('Found ${conflictingOutputs.length} declared outputs '
'which already exist on disk. This is likely because the `.build` '
'folder was deleted.');
var done = false;
while (!done) {
stdout.write('Delete these files (y/n) (or list them (l))?: ');
var input = stdin.readLineSync();
switch (input) {
case 'y':
stdout.writeln('Deleting files...');
await Future.wait(conflictingOutputs.map((output) {
return _writer.delete(output);
}));
done = true;
break;
case 'n':
stdout.writeln('Exiting...');
exit(1);
break;
case 'l':
for (var output in conflictingOutputs) {
stdout.writeln(output);
}
break;
default:
stdout.writeln('Unrecognized option $input, (y/n/l) expected.');
}
}
}

/// Runs the [phaseGroups] and returns a [Future<BuildResult>] which completes
/// once all [Phase]s are done.
Future<BuildResult> _runPhases(List<List<Phase>> phaseGroups) async {
final allInputs = await _allInputs(phaseGroups);
final outputs = <Asset>[];
int phaseGroupNum = 0;
for (var group in phaseGroups) {
final groupOutputs = <Asset>[];
for (var phase in group) {
var inputs = _matchingInputs(allInputs, phase.inputSets);
for (var builder in phase.builders) {
// TODO(jakemac): Optimize, we can run all the builders in a phase
// at the same time instead of sequentially.
await for (var output
in _runBuilder(builder, inputs, allInputs, phaseGroupNum)) {
groupOutputs.add(output);
outputs.add(output);
}
}
}

/// Once the group is done, add all outputs so they can be used in the next
/// phase.
for (var output in groupOutputs) {
allInputs.putIfAbsent(output.id.package, () => new Set<AssetId>());
allInputs[output.id.package].add(output.id);
}
phaseGroupNum++;
}
return new BuildResult(BuildStatus.Success, BuildType.Full, outputs);
}

/// Returns a map of all the available inputs by package.
Future<Map<String, Set<AssetId>>> _allInputs(
List<List<Phase>> phaseGroups) async {
final packages = new Set<String>();
for (var group in phaseGroups) {
for (var phase in group) {
for (var inputSet in phase.inputSets) {
packages.add(inputSet.package);
}
}
}

var inputSets = packages.map((package) => new InputSet(package));
var allInputs = await _reader.listAssetIds(inputSets).toList();
var inputsByPackage = {};
for (var input in allInputs) {
inputsByPackage.putIfAbsent(input.package, () => new Set<AssetId>());

if (_isValidInput(input)) {
inputsByPackage[input.package].add(input);
}
}
return inputsByPackage;
}

/// Gets a list of all inputs matching [inputSets] given [allInputs].
Set<AssetId> _matchingInputs(
Map<String, Set<AssetId>> inputsByPackage, Iterable<InputSet> inputSets) {
var inputs = new Set<AssetId>();
for (var inputSet in inputSets) {
assert(inputsByPackage.containsKey(inputSet.package));
for (var input in inputsByPackage[inputSet.package]) {
if (inputSet.globs.any((g) => g.matches(input.path))) {
inputs.add(input);
}
}
}
return inputs;
}

/// Checks if an [input] is valid.
bool _isValidInput(AssetId input) {
var parts = path.split(input.path);
// Files must be in a top level directory.
if (parts.length == 1) return false;
if (input.package != _packageGraph.root.name) return parts[0] == 'lib';
return true;
}

/// Runs [builder] with [inputs] as inputs.
Stream<Asset> _runBuilder(Builder builder, Iterable<AssetId> primaryInputs,
Map<String, Set<AssetId>> allInputs, int phaseGroupNum) async* {
for (var input in primaryInputs) {
var expectedOutputs = builder.declareOutputs(input);

/// Validate [expectedOutputs].
for (var output in expectedOutputs) {
if (output.package != _packageGraph.root.name) {
throw new InvalidOutputException(new Asset(output, ''));
}
if (allInputs[output.package]?.contains(output) == true) {
throw new InvalidOutputException(new Asset(output, ''));
}
}

/// Add nodes to the [AssetGraph] for [expectedOutputs] and [input].
var inputNode = _assetGraph.addIfAbsent(input, () => new AssetNode(input));
for (var output in expectedOutputs) {
inputNode.outputs.add(output);
_assetGraph.addIfAbsent(
output,
() => new GeneratedAssetNode(
builder, input, phaseGroupNum, true, output));
}

/// Skip the build step if none of the outputs need updating.
var skipBuild = !expectedOutputs.any((output) =>
(_assetGraph.get(output) as GeneratedAssetNode).needsUpdate);
if (skipBuild) continue;

var inputAsset = new Asset(input, await _reader.readAsString(input));
var buildStep = new BuildStepImpl(inputAsset, expectedOutputs, _reader,
_writer, _packageGraph.root.name);
await builder.build(buildStep);
await buildStep.complete();

/// Update the asset graph based on the dependencies discovered.
for (var dependency in buildStep.dependencies) {
var dependencyNode =
_assetGraph.addIfAbsent(dependency, () => new AssetNode(dependency));

/// We care about all [expectedOutputs], not just real outputs.
dependencyNode.outputs.addAll(expectedOutputs);
}

/// Yield the outputs.
for (var output in buildStep.outputs) {
yield output;
}
}
}
Loading