Skip to content

Commit

Permalink
Add package config to build step (#3495)
Browse files Browse the repository at this point in the history
This adds a `packageConfig` getter to `BuildStep`, which can be used to read the language version of packages involved in the build. The package config exposed in the build step hides the actual files of packages on the file system. Instead, 

By default, the package config will be resolved from the current isolate in `runBuilder`. However, `build_runner_core` also constructs a config based on the `PackageGraph` which avoids loading the raw package config multiple times.

Closes #3492.
  • Loading branch information
simolus3 committed May 8, 2023
1 parent d3a9ecf commit 09a4fef
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 23 deletions.
5 changes: 4 additions & 1 deletion build/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
## 2.3.2-dev
## 2.4.0-dev

- Add `BuildStep.packageConfig` getter to resolve a package config of all
packages involved in the current build.

## 2.3.1

Expand Down
12 changes: 12 additions & 0 deletions build/lib/src/builder/build_step.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:convert';

import 'package:analyzer/dart/element/element.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config_types.dart';

import '../analyzer/resolver.dart';
import '../asset/id.dart';
Expand Down Expand Up @@ -118,6 +119,17 @@ abstract class BuildStep implements AssetReader, AssetWriter {
/// `InvalidOutputException` when attempting to write an asset not part of
/// the [allowedOutputs].
Iterable<AssetId> get allowedOutputs;

/// Returns a [PackageConfig] resolvable from this build step.
///
/// The package config contains all packages involved in the build. Typically,
/// this is the package config taken from the current isolate.
///
/// The returned package config does not use `file:/`-based URIs and can't be
/// used to access package contents with `dart:io`. Instead, packages resolve
/// to `asset:/` URIs that can be parsed with [AssetId.resolve] and read with
/// [readAsBytes] or [readAsString].
Future<PackageConfig> get packageConfig;
}

abstract class StageTracker {
Expand Down
22 changes: 20 additions & 2 deletions build/lib/src/builder/build_step_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:analyzer/dart/element/element.dart';
import 'package:async/async.dart';
import 'package:crypto/crypto.dart';
import 'package:glob/glob.dart';
import 'package:package_config/package_config_types.dart';

import '../analyzer/resolver.dart';
import '../asset/exceptions.dart';
Expand Down Expand Up @@ -60,14 +61,31 @@ class BuildStepImpl implements BuildStep {

final void Function(Iterable<AssetId>)? _reportUnusedAssets;

BuildStepImpl(this.inputId, Iterable<AssetId> expectedOutputs, this._reader,
this._writer, this._resolvers, this._resourceManager,
final Future<PackageConfig> Function() _resolvePackageConfig;
Future<Result<PackageConfig>>? _resolvedPackageConfig;

BuildStepImpl(
this.inputId,
Iterable<AssetId> expectedOutputs,
this._reader,
this._writer,
this._resolvers,
this._resourceManager,
this._resolvePackageConfig,
{StageTracker? stageTracker,
void Function(Iterable<AssetId>)? reportUnusedAssets})
: allowedOutputs = UnmodifiableSetView(expectedOutputs.toSet()),
_stageTracker = stageTracker ?? NoOpStageTracker.instance,
_reportUnusedAssets = reportUnusedAssets;

@override
Future<PackageConfig> get packageConfig async {
final resolved =
_resolvedPackageConfig ??= Result.capture(_resolvePackageConfig());

return (await resolved).asFuture;
}

@override
Resolver get resolver {
if (_isComplete) throw BuildStepCompletedException();
Expand Down
72 changes: 64 additions & 8 deletions build/lib/src/generate/run_builder.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// 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:isolate';

import 'package:logging/logging.dart';
import 'package:package_config/package_config.dart';

import '../analyzer/resolver.dart';
import '../asset/id.dart';
Expand All @@ -28,22 +31,50 @@ import 'expected_outputs.dart';
/// If [reportUnusedAssetsForInput] is provided then all calls to
/// `BuildStep.reportUnusedAssets` in [builder] will be forwarded to this
/// function with the associated primary input.
Future<void> runBuilder(Builder builder, Iterable<AssetId> inputs,
AssetReader reader, AssetWriter writer, Resolvers? resolvers,
{Logger? logger,
ResourceManager? resourceManager,
StageTracker stageTracker = NoOpStageTracker.instance,
void Function(AssetId input, Iterable<AssetId> assets)?
reportUnusedAssetsForInput}) async {
Future<void> runBuilder(
Builder builder,
Iterable<AssetId> inputs,
AssetReader reader,
AssetWriter writer,
Resolvers? resolvers, {
Logger? logger,
ResourceManager? resourceManager,
StageTracker stageTracker = NoOpStageTracker.instance,
void Function(AssetId input, Iterable<AssetId> assets)?
reportUnusedAssetsForInput,
PackageConfig? packageConfig,
}) async {
var shouldDisposeResourceManager = resourceManager == null;
final resources = resourceManager ?? ResourceManager();
logger ??= Logger('runBuilder');

PackageConfig? transformedConfig;

Future<PackageConfig> loadPackageConfig() async {
if (transformedConfig != null) return transformedConfig!;

var config = packageConfig;
if (config == null) {
final uri = await Isolate.packageConfig;

if (uri == null) {
throw UnsupportedError(
'Isolate running the build does not have a package config and no '
'fallback has been provided');
}

config = await loadPackageConfigUri(uri);
}

return transformedConfig = config.transformToAssetUris();
}

//TODO(nbosch) check overlapping outputs?
Future<void> buildForInput(AssetId input) async {
var outputs = expectedOutputs(builder, input);
if (outputs.isEmpty) return;
var buildStep = BuildStepImpl(
input, outputs, reader, writer, resolvers, resources,
input, outputs, reader, writer, resolvers, resources, loadPackageConfig,
stageTracker: stageTracker,
reportUnusedAssets: reportUnusedAssetsForInput == null
? null
Expand All @@ -62,3 +93,28 @@ Future<void> runBuilder(Builder builder, Iterable<AssetId> inputs,
await resources.beforeExit();
}
}

extension on Package {
static final _lib = Uri.parse('lib/');

Package transformToAssetUris() {
return Package(
name,
Uri(scheme: 'asset', pathSegments: [name, '']),
packageUriRoot: _lib,
extraData: extraData,
languageVersion: languageVersion,
);
}
}

extension on PackageConfig {
PackageConfig transformToAssetUris() {
return PackageConfig(
[
for (final package in packages) package.transformToAssetUris(),
],
extraData: extraData,
);
}
}
3 changes: 2 additions & 1 deletion build/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: build
version: 2.3.2-dev
version: 2.4.0-dev
description: A package for authoring build_runner compatible code generators.
repository: https://github.com/dart-lang/build/tree/master/build

Expand All @@ -14,6 +14,7 @@ dependencies:
glob: ^2.0.0
logging: ^1.0.0
meta: ^1.3.0
package_config: ^2.1.0
path: ^1.8.0

dev_dependencies:
Expand Down
30 changes: 21 additions & 9 deletions build/test/builder/build_step_impl_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:build/src/builder/build_step.dart';
import 'package:build/src/builder/build_step_impl.dart';
import 'package:build_resolvers/build_resolvers.dart';
import 'package:build_test/build_test.dart';
import 'package:package_config/package_config.dart';
import 'package:test/test.dart';

void main() {
Expand All @@ -34,7 +35,7 @@ void main() {
primary = makeAssetId();
outputs = List.generate(5, (index) => makeAssetId());
buildStep = BuildStepImpl(primary, outputs, reader, writer,
AnalyzerResolvers(), resourceManager);
AnalyzerResolvers(), resourceManager, _unsupported);
});

test('doesnt allow non-expected outputs', () {
Expand Down Expand Up @@ -87,8 +88,15 @@ void main() {
};
addAssets(inputs, writer);
var outputId = AssetId.parse('$primary.copy');
var buildStep = BuildStepImpl(primary, [outputId], reader, writer,
AnalyzerResolvers(), resourceManager);
var buildStep = BuildStepImpl(
primary,
[outputId],
reader,
writer,
AnalyzerResolvers(),
resourceManager,
_unsupported,
);

await builder.build(buildStep);
await buildStep.complete();
Expand All @@ -112,8 +120,8 @@ void main() {
addAssets(inputs, writer);

var primary = makeAssetId('a|web/a.dart');
var buildStep = BuildStepImpl(
primary, [], reader, writer, AnalyzerResolvers(), resourceManager);
var buildStep = BuildStepImpl(primary, [], reader, writer,
AnalyzerResolvers(), resourceManager, _unsupported);
var resolver = buildStep.resolver;

var aLib = await resolver.libraryFor(primary);
Expand Down Expand Up @@ -143,7 +151,7 @@ void main() {
outputId = makeAssetId('a|test.txt');
outputContent = '$outputId';
buildStep = BuildStepImpl(primary, [outputId], StubAssetReader(),
assetWriter, AnalyzerResolvers(), resourceManager);
assetWriter, AnalyzerResolvers(), resourceManager, _unsupported);
});

test('Completes only after writes finish', () async {
Expand Down Expand Up @@ -191,7 +199,7 @@ void main() {
primary = makeAssetId();
output = makeAssetId();
buildStep = BuildStepImpl(primary, [output], reader, writer,
AnalyzerResolvers(), resourceManager,
AnalyzerResolvers(), resourceManager, _unsupported,
stageTracker: NoOpStageTracker.instance);
});

Expand All @@ -205,8 +213,8 @@ void main() {
var reader = StubAssetReader();
var writer = StubAssetWriter();
var unused = <AssetId>{};
var buildStep = BuildStepImpl(
makeAssetId(), [], reader, writer, AnalyzerResolvers(), resourceManager,
var buildStep = BuildStepImpl(makeAssetId(), [], reader, writer,
AnalyzerResolvers(), resourceManager, _unsupported,
reportUnusedAssets: unused.addAll);
var reported = [
makeAssetId(),
Expand Down Expand Up @@ -234,3 +242,7 @@ class SlowAssetWriter implements AssetWriter {
{Encoding encoding = utf8}) =>
_writeCompleter.future;
}

Future<PackageConfig> _unsupported() {
return Future.error(UnsupportedError('stub'));
}
43 changes: 43 additions & 0 deletions build/test/generate/run_builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:async';

import 'package:build/build.dart';
import 'package:build_test/build_test.dart';
import 'package:package_config/package_config_types.dart';
import 'package:test/test.dart';

void main() {
Expand Down Expand Up @@ -67,6 +68,48 @@ void main() {
expect(resourceDisposed, true);
});
});

group('can resolve package config', () {
setUp(() {
writer.assets[makeAssetId('build|lib/foo.txt')] = [1, 2, 3];

builder = TestBuilder(extraWork: (buildStep, __) async {
final config = await buildStep.packageConfig;

final buildPackage =
config.packages.singleWhere((p) => p.name == 'build');
expect(buildPackage.root, Uri.parse('asset:build/'));
expect(buildPackage.packageUriRoot, Uri.parse('asset:build/lib/'));
expect(buildPackage.languageVersion, LanguageVersion(2, 18));

final resolvedBuildUri =
config.resolve(Uri.parse('package:build/foo.txt'))!;
expect(
await buildStep.canRead(AssetId.resolve(resolvedBuildUri)), isTrue);
});
});

test('from default', () async {
await runBuilder(builder, inputs.keys, reader, writer, null);
});

test('when provided', () async {
await runBuilder(
builder,
inputs.keys,
reader,
writer,
null,
packageConfig: PackageConfig([
Package(
'build',
Uri.file('/foo/bar/'),
languageVersion: LanguageVersion(2, 18),
),
]),
);
});
});
}

class TrackingResourceManager extends ResourceManager {
Expand Down
1 change: 1 addition & 0 deletions build_runner_core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 7.2.8-dev

- Raise the minimum SDK constraint to 2.18.
- Optimize `BuildStep.packageConfig`

## 7.2.7

Expand Down
1 change: 1 addition & 0 deletions build_runner_core/lib/src/generate/build_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ class _SingleBuild {
stageTracker: tracker,
reportUnusedAssetsForInput: (_, assets) =>
unusedAssets.addAll(assets),
packageConfig: _packageGraph.asPackageConfig,
).catchError((void _) {
// Errors tracked through the logger
}));
Expand Down
22 changes: 21 additions & 1 deletion build_runner_core/lib/src/package_graph/package_graph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ class PackageGraph {
/// All [PackageNode]s indexed by package name.
final Map<String, PackageNode> allPackages;

/// A [PackageConfig] representation of this package graph.
final PackageConfig asPackageConfig;

PackageGraph._(this.root, Map<String, PackageNode> allPackages)
: allPackages = Map.unmodifiable(
: asPackageConfig = _packagesToConfig(allPackages.values),
allPackages = Map.unmodifiable(
Map<String, PackageNode>.from(allPackages)
..putIfAbsent(r'$sdk', () => _sdkPackageNode)) {
if (!root.isRoot) {
Expand Down Expand Up @@ -113,6 +117,22 @@ class PackageGraph {
static Future<PackageGraph> forThisPackage() =>
PackageGraph.forPath(p.current);

static PackageConfig _packagesToConfig(Iterable<PackageNode> packages) {
final relativeLib = Uri.parse('lib/');

return PackageConfig([
for (final package in packages)
if (package.name != _sdkPackageNode.name)
Package(
package.name,
Uri.file(
package.path.endsWith('/') ? package.path : '${package.path}/'),
languageVersion: package.languageVersion,
packageUriRoot: relativeLib,
),
]);
}

/// Shorthand to get a package by name.
PackageNode? operator [](String packageName) => allPackages[packageName];

Expand Down

0 comments on commit 09a4fef

Please sign in to comment.