blob: 35c255a5bcd92d42d379d0b347eeb168f8ed8bb4 [file] [log] [blame]
// Copyright (c) 2018, 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:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:analyzer/src/summary/summary_file_builder.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart' hide File;
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/dart/analysis/driver.dart' show AnalysisDriver;
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/generated/engine.dart'
show AnalysisOptions, AnalysisOptionsImpl;
import 'package:build/build.dart';
import 'package:logging/logging.dart';
import 'package:package_resolver/package_resolver.dart';
import 'package:path/path.dart' as p;
import 'package:yaml/yaml.dart';
import 'analysis_driver.dart';
import 'build_asset_uri_resolver.dart';
import 'human_readable_duration.dart';
final _logger = Logger('build_resolvers');
/// Implements [Resolver.libraries] and [Resolver.findLibraryByName] by crawling
/// down from entrypoints.
class PerActionResolver implements ReleasableResolver {
final AnalyzerResolver _delegate;
final BuildStep _step;
final Set<AssetId> _entryPoints;
PerActionResolver(this._delegate, this._step, Iterable<AssetId> entryPoints)
: _entryPoints = entryPoints.toSet();
@override
Stream<LibraryElement> get libraries async* {
final seen = <LibraryElement>{};
final toVisit = Queue<LibraryElement>();
// keep a copy of entry points in case [_resolveIfNecessary] is called
// before this stream is done.
final entryPoints = _entryPoints.toList();
for (final entryPoint in entryPoints) {
if (!await _delegate.isLibrary(entryPoint)) continue;
final library = await _delegate.libraryFor(entryPoint);
toVisit.add(library);
seen.add(library);
}
while (toVisit.isNotEmpty) {
final current = toVisit.removeFirst();
// TODO - avoid crawling or returning libraries which are not visible via
// `BuildStep.canRead`. They'd still be reachable by crawling the element
// model manually.
yield current;
final toCrawl = current.importedLibraries
.followedBy(current.exportedLibraries)
.where((l) => !seen.contains(l))
.toSet();
toVisit.addAll(toCrawl);
seen.addAll(toCrawl);
}
}
@override
Future<LibraryElement> findLibraryByName(String libraryName) async =>
libraries.firstWhere((l) => l.name == libraryName, orElse: () => null);
@override
Future<bool> isLibrary(AssetId assetId) async {
await _resolveIfNecesssary(assetId);
return _delegate.isLibrary(assetId);
}
@override
Future<LibraryElement> libraryFor(AssetId assetId) async {
await _resolveIfNecesssary(assetId);
return _delegate.libraryFor(assetId);
}
Future<void> _resolveIfNecesssary(AssetId id) async {
if (!_entryPoints.contains(id)) {
_entryPoints.add(id);
// the resolver will only visit assets that haven't been resolved in this
// step yet
await _delegate._uriResolver
.performResolve(_step, [id], _delegate._driver);
}
}
@override
void release() {
_delegate._uriResolver.notifyComplete(_step);
_delegate.release();
}
@override
Future<AssetId> assetIdForElement(Element element) =>
_delegate.assetIdForElement(element);
}
class AnalyzerResolver implements ReleasableResolver {
final BuildAssetUriResolver _uriResolver;
final AnalysisDriver _driver;
AnalyzerResolver(this._driver, this._uriResolver);
@override
Future<bool> isLibrary(AssetId assetId) async {
var source = _driver.sourceFactory.forUri2(assetId.uri);
return source != null &&
(await _driver.getSourceKind(assetPath(assetId))) == SourceKind.LIBRARY;
}
@override
Future<LibraryElement> libraryFor(AssetId assetId) async {
var path = assetPath(assetId);
var uri = assetId.uri;
var source = _driver.sourceFactory.forUri2(uri);
if (source == null) throw ArgumentError('missing source for $uri');
var kind = await _driver.getSourceKind(path);
if (kind != SourceKind.LIBRARY) throw NonLibraryAssetException(assetId);
return _driver.getLibraryByUri(assetId.uri.toString());
}
@override
// Do nothing
void release() {}
@override
Stream<LibraryElement> get libraries {
// We don't know what libraries to expose without leaking libraries written
// by later phases.
throw UnimplementedError();
}
@override
Future<LibraryElement> findLibraryByName(String libraryName) {
// We don't know what libraries to expose without leaking libraries written
// by later phases.
throw UnimplementedError();
}
@override
Future<AssetId> assetIdForElement(Element element) async {
final uri = element.source.uri;
if (!uri.isScheme('package') && !uri.isScheme('asset')) {
throw UnresolvableAssetException(
'${element.name} in ${element.source.uri}');
}
return AssetId.resolve('${element.source.uri}');
}
}
class AnalyzerResolvers implements Resolvers {
/// Nullable, the default analysis options are used if not provided.
final AnalysisOptions _analysisOptions;
/// A function that returns the path to the SDK summary when invoked.
///
/// Defaults to [_defaultSdkSummaryGenerator].
final Future<String> Function() _sdkSummaryGenerator;
// Lazy, all access must be preceded by a call to `_ensureInitialized`.
AnalyzerResolver _resolver;
BuildAssetUriResolver _uriResolver;
/// Nullable, should not be accessed outside of [_ensureInitialized].
Future<void> _initialized;
AnalyzerResolvers(
[this._analysisOptions, Future<String> Function() sdkSummaryGenerator])
: _sdkSummaryGenerator =
sdkSummaryGenerator ?? _defaultSdkSummaryGenerator;
/// Create a Resolvers backed by an `AnalysisContext` using options
/// [_analysisOptions].
Future<void> _ensureInitialized() {
return _initialized ??= () async {
_uriResolver = BuildAssetUriResolver();
var driver = analysisDriver(
_uriResolver, _analysisOptions, await _sdkSummaryGenerator());
_resolver = AnalyzerResolver(driver, _uriResolver);
}();
}
@override
Future<ReleasableResolver> get(BuildStep buildStep) async {
await _ensureInitialized();
await _uriResolver.performResolve(
buildStep, [buildStep.inputId], _resolver._driver);
return PerActionResolver(_resolver, buildStep, [buildStep.inputId]);
}
/// Must be called between each build.
@override
void reset() {
_uriResolver?.reset();
}
}
/// Lazily creates a summary of the users SDK and caches it under
/// `.dart_tool/build_resolvers`.
///
/// This is only intended for use in typical dart packages, which must
/// have an already existing `.dart_tool` directory (this is how we
/// validate we are running under a typical dart package and not a custom
/// environment).
Future<String> _defaultSdkSummaryGenerator() async {
var dartToolPath = '.dart_tool';
if (!await Directory(dartToolPath).exists()) {
throw StateError(
'The default analyzer resolver can only be used when the current '
'working directory is a standard pub package.');
}
var cacheDir = p.join(dartToolPath, 'build_resolvers');
var summaryPath = p.join(cacheDir, 'sdk.sum');
var depsFile = File('$summaryPath.deps');
var summaryFile = File(summaryPath);
var currentDeps = {
'sdk': Platform.version,
for (var package in _packageDepsToCheck)
package: await PackageResolver.current.packagePath(package),
};
// Invalidate existing summary/version/analyzer files if present.
if (await depsFile.exists()) {
if (!await _checkDeps(depsFile, currentDeps)) {
await depsFile.delete();
if (await summaryFile.exists()) await summaryFile.delete();
}
} else if (await summaryFile.exists()) {
// Fallback for cases where we could not do a proper version check.
await summaryFile.delete();
}
// Generate the summary and version files if necessary.
if (!await summaryFile.exists()) {
var watch = Stopwatch()..start();
_logger.info('Generating SDK summary...');
await summaryFile.create(recursive: true);
await summaryFile.writeAsBytes(_buildSdkSummary());
await _createDepsFile(depsFile, currentDeps);
watch.stop();
_logger.info('Generating SDK summary completed, took '
'${humanReadable(watch.elapsed)}\n');
}
return p.absolute(summaryPath);
}
final _packageDepsToCheck = ['analyzer', 'build_resolvers'];
Future<bool> _checkDeps(
File versionsFile, Map<String, Object> currentDeps) async {
var previous =
jsonDecode(await versionsFile.readAsString()) as Map<String, Object>;
if (previous.keys.length != currentDeps.keys.length) return false;
for (var entry in previous.entries) {
if (entry.value != currentDeps[entry.key]) return false;
}
return true;
}
Future<void> _createDepsFile(
File depsFile, Map<String, Object> currentDeps) async {
await depsFile.create(recursive: true);
await depsFile.writeAsString(jsonEncode(currentDeps));
}
List<int> _buildSdkSummary() {
var resourceProvider = PhysicalResourceProvider.INSTANCE;
var dartSdkFolder = resourceProvider.getFolder(_runningDartSdkPath);
var sdk = FolderBasedDartSdk(resourceProvider, dartSdkFolder)
..useSummary = false
..analysisOptions = AnalysisOptionsImpl();
if (isFlutter) {
_addFlutterLibraries(sdk, resourceProvider);
}
var sdkSources = {
for (var library in sdk.sdkLibraries) sdk.mapDartUri(library.shortName),
};
return SummaryBuilder(sdkSources, sdk.context).build();
}
/// Loads the flutter engine _embedder.yaml file and adds any new libraries to
/// [sdk].
void _addFlutterLibraries(
AbstractDartSdk sdk, ResourceProvider resourceProvider) {
var embedderYamlFile =
resourceProvider.getFile(p.join(_dartUiPath, '_embedder.yaml'));
if (!embedderYamlFile.exists) {
throw StateError('Unable to find flutter libraries, please run '
'`flutter precache` and try again.');
}
var embedderYaml = loadYaml(embedderYamlFile.readAsStringSync()) as YamlMap;
var flutterSdk = EmbedderSdk(resourceProvider,
{resourceProvider.getFolder(_dartUiPath): embedderYaml});
for (var library in flutterSdk.sdkLibraries) {
if (sdk.libraryMap.getLibrary(library.shortName) != null) continue;
sdk.libraryMap.setLibrary(library.shortName, library as SdkLibraryImpl);
}
}
/// Path to the running dart's SDK root.
final _runningDartSdkPath = p.dirname(p.dirname(Platform.resolvedExecutable));
/// Path where the dart:ui package will be found, if executing via the dart
/// binary provided by the Flutter SDK.
final _dartUiPath =
p.normalize(p.join(_runningDartSdkPath, '..', 'pkg', 'sky_engine', 'lib'));
/// `true` if the currently running dart was provided by the Flutter SDK.
final isFlutter =
Platform.version.contains('flutter') || Directory(_dartUiPath).existsSync();