blob: d84691608e392d361dd6cb04c27b031f1f6b3cdd [file] [log] [blame]
// Copyright (c) 2013, 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.
/// A library for compiling Dart code and manipulating analyzer parse trees.
import 'dart:async';
import 'dart:io';
import 'package:analyzer/dart/analysis/context_builder.dart';
import 'package:analyzer/dart/analysis/context_locator.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/error.dart';
import 'package:frontend_server_client/frontend_server_client.dart';
import 'package:path/path.dart' as p;
import 'exceptions.dart';
import 'io.dart';
import 'log.dart' as log;
class AnalysisContextManager {
static final sessions = <String, AnalysisContextManager>{};
final String packagePath;
final AnalysisSession _session;
factory AnalysisContextManager(String packagePath) => sessions.putIfAbsent(
packagePath, () => AnalysisContextManager._(packagePath));
AnalysisContextManager._(this.packagePath)
: _session = ContextBuilder()
.createContext(
contextRoot: ContextLocator().locateRoots(
includedPaths: [p.absolute(p.normalize(packagePath))],
optionsFile:
// We don't want to take 'analysis_options.yaml' files into
// account. So we replace it with an empty file.
Platform.isWindows ? r'C:\NUL' : '/dev/null',
).first,
)
.currentSession;
/// Parse the file with the given [path] into AST.
///
/// One of the containing directories must be used to create analysis
/// contexts using [createContextsForDirectory]. Throws [StateError] if
/// this has not been done.
///
/// Throws [AnalyzerErrorGroup] is the file has parsing errors.
CompilationUnit parse(String path) {
path = p.normalize(p.absolute(path));
var parseResult = _session.getParsedUnit(path);
if (parseResult is ParsedUnitResult) {
if (parseResult.errors.isNotEmpty) {
throw AnalyzerErrorGroup(parseResult.errors);
}
return parseResult.unit;
} else {
throw StateError('Unable to parse $path, ${parseResult.runtimeType}');
}
}
/// Return import and export directives in the file with the given [path].
///
/// Throws [AnalyzerErrorGroup] is the file has parsing errors.
List<UriBasedDirective> parseImportsAndExports(String path) {
var unit = parse(path);
var uriDirectives = <UriBasedDirective>[];
for (var directive in unit.directives) {
if (directive is UriBasedDirective) {
uriDirectives.add(directive);
}
}
return uriDirectives;
}
}
/// An error class that contains multiple [AnalysisError]s.
class AnalyzerErrorGroup implements Exception {
final List<AnalysisError> errors;
AnalyzerErrorGroup(this.errors);
String get message => toString();
@override
String toString() => errors.join('\n');
}
/// Precompiles the Dart executable at [executablePath].
///
/// If the compilation succeeds it is saved to a kernel file at [outputPath].
///
/// If compilation fails, the output is cached at [incrementalDillOutputPath].
///
/// Whichever of [incrementalDillOutputPath] and [outputPath] already exists is
/// used to initialize the compiler run.
///
/// The [packageConfigPath] should point at the package config file to be used
/// for `package:` uri resolution.
///
/// The [name] is used to describe the executable in logs and error messages.
///
/// The [additionalSources], if provided, instruct the compiler to include
/// additional source files into compilation even if they are not referenced
/// from the main library.
///
/// The [nativeAssets], if provided, instruct the compiler include a native
/// assets map.
Future<void> precompile({
required String executablePath,
required String incrementalDillPath,
required String name,
required String outputPath,
required String packageConfigPath,
List<String> additionalSources = const [],
String? nativeAssets,
}) async {
ensureDir(p.dirname(outputPath));
ensureDir(p.dirname(incrementalDillPath));
const platformDill = 'lib/_internal/vm_platform_strong.dill';
final sdkRoot = p.relative(p.dirname(p.dirname(Platform.resolvedExecutable)));
String? tempDir;
FrontendServerClient? client;
try {
tempDir = createTempDir(p.dirname(incrementalDillPath), 'tmp');
// To avoid potential races we copy the incremental data to a temporary file
// for just this compilation.
final temporaryIncrementalDill =
p.join(tempDir, '${p.basename(incrementalDillPath)}.temp');
try {
if (fileExists(incrementalDillPath)) {
copyFile(incrementalDillPath, temporaryIncrementalDill);
} else if (fileExists(outputPath)) {
copyFile(outputPath, temporaryIncrementalDill);
}
} on FileSystemException {
// Not able to copy existing file, compilation will start from scratch.
}
client = await FrontendServerClient.start(
executablePath,
temporaryIncrementalDill,
platformDill,
sdkRoot: sdkRoot,
packagesJson: packageConfigPath,
additionalSources: additionalSources,
nativeAssets: nativeAssets,
printIncrementalDependencies: false,
);
final result = await client.compile();
final highlightedName = log.bold(name);
if (result.errorCount == 0) {
log.message('Built $highlightedName.');
// By using rename we ensure atomicity. An external observer will either
// see the old or the new snapshot.
renameFile(temporaryIncrementalDill, outputPath);
} else {
// By using rename we ensure atomicity. An external observer will either
// see the old or the new snapshot.
renameFile(temporaryIncrementalDill, incrementalDillPath);
// If compilation failed we don't want to leave an incorrect snapshot.
tryDeleteEntry(outputPath);
throw ApplicationException(
log.yellow('Failed to build $highlightedName:\n') +
result.compilerOutputLines.join('\n'));
}
} finally {
client?.kill();
if (tempDir != null) {
tryDeleteEntry(tempDir);
}
}
}