blob: fa01f19a9c0e8984937c31713f489a582610f597 [file] [log] [blame]
// Copyright 2022 The Chromium 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:io';
import 'package:args/args.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;
import 'package:pub/pub.dart';
import 'resident_frontend_constants.dart';
import 'resident_frontend_utils.dart';
import 'sdk.dart';
typedef CompileRequestGeneratorCallback = String Function({
required String executable,
required String outputDill,
required ArgResults args,
String? packages,
});
/// Uses the resident frontend compiler to compute a kernel file for
/// [executable]. Throws a [FrontendCompilerException] if the compilation
/// fails or if the source code contians compilation errors.
///
/// [executable] is expected to contain a path to the dart source file and
/// a package_config file.
///
/// [serverInfoFile] is the location that should be checked to find an existing
/// Resident Frontend Compiler. If one does not exist, a server is created and
/// its address and port information is written to this file location.
///
/// [args] is the [ArgResults] object that is created by the DartDev commands.
/// This is where the optional path override for the serverInfoFile is passed
/// in.
///
/// [compileRequestGenerator] is applied to produce a request for the Resident
/// Frontend Server.
Future<DartExecutableWithPackageConfig> generateKernel(
DartExecutableWithPackageConfig executable,
File serverInfoFile,
ArgResults args,
CompileRequestGeneratorCallback compileRequestGenerator, {
bool aot = false,
}) async {
// Locates the package_config.json and cached kernel file, makes sure the
// resident frontend server is up and running, and computes a kernel.
final packageRoot = _packageRootFor(executable);
if (packageRoot == null) {
throw FrontendCompilerException._(
'resident mode is only supported for Dart packages.',
CompilationIssue.standaloneProgramError);
}
await _ensureCompileServerIsRunning(args, serverInfoFile);
// TODO: allow custom package paths with a --packages flag
final packageConfig = await _resolvePackageConfig(executable, packageRoot);
final cachedKernel = _cachedKernelPath(executable.executable, packageRoot);
Map<String, dynamic> result;
try {
result = await sendAndReceiveResponse(
compileRequestGenerator(
executable: p.canonicalize(executable.executable),
outputDill: cachedKernel,
packages: packageConfig,
args: args,
),
serverInfoFile,
);
} on FileSystemException catch (e) {
throw FrontendCompilerException._(e.message, CompilationIssue.serverError);
}
if (!result[responseSuccessString]) {
if (result.containsKey(responseErrorString)) {
throw FrontendCompilerException._(
result[responseErrorString],
CompilationIssue.serverError,
);
} else {
throw FrontendCompilerException._(
(result[responseOutputString] as List<dynamic>).join('\n'),
CompilationIssue.compilationError,
);
}
}
return DartExecutableWithPackageConfig(
executable: cachedKernel,
packageConfig: packageConfig,
);
}
/// Returns the absolute path to [executable]'s cached kernel file.
/// Throws a [FrontendCompilerException] if the cached kernel cannot be
/// created.
String _cachedKernelPath(String executable, String packageRoot) {
final executableDirPath = p.canonicalize(p.dirname(executable));
var cachedKernelDirectory = p.join(
packageRoot,
'.dart_tool',
dartdevKernelCache,
);
final subdirectoryList =
executableDirPath.replaceFirst(packageRoot, '').split(p.separator);
for (var directory in subdirectoryList) {
cachedKernelDirectory = p.join(cachedKernelDirectory, directory);
}
try {
Directory(cachedKernelDirectory).createSync(recursive: true);
} catch (e) {
throw FrontendCompilerException._(
e.toString(),
CompilationIssue.serverError,
);
}
return p.canonicalize(
p.join(
cachedKernelDirectory,
'${p.basename(executable)}-${sdk.version}.dill',
),
);
}
/// Ensures that the Resident Frontend Compiler is running, starting it if
/// necessary. Throws a [FrontendCompilerException] if starting the server
/// fails.
Future<void> _ensureCompileServerIsRunning(
ArgResults args,
File serverInfoFile,
) async {
if (serverInfoFile.existsSync()) {
return;
}
try {
Directory(p.dirname(serverInfoFile.path)).createSync(recursive: true);
// TODO replace this with the AOT executable when that is built.
final frontendServerProcess = await Process.start(
sdk.dart,
[
sdk.frontendServerSnapshot,
'--resident-info-file-name=${serverInfoFile.path}'
],
workingDirectory: home,
mode: ProcessStartMode.detachedWithStdio,
);
final serverOutput =
String.fromCharCodes(await frontendServerProcess.stdout.first);
if (serverOutput.startsWith('Error')) {
throw StateError(serverOutput);
}
print(serverOutput); // Prints the server's address and port information
} catch (e) {
throw FrontendCompilerException._(
e.toString(),
CompilationIssue.serverCreationError,
);
}
}
/// Returns the path to the root of the [executable]'s package, or null
/// if it is a standalone dart file.
String? _packageRootFor(DartExecutableWithPackageConfig executable) {
Directory currentDirectory =
Directory(p.dirname(p.canonicalize(executable.executable)));
while (currentDirectory.parent.path != currentDirectory.path) {
if (File(p.join(currentDirectory.path, 'pubspec.yaml')).existsSync() ||
File(p.join(currentDirectory.path, packageConfigName)).existsSync()) {
return currentDirectory.path;
}
currentDirectory = currentDirectory.parent;
}
return null;
}
/// Resolves the absolute path to [packageRoot]'s package_config.json file,
/// returning null if the package does not contain one, or if the source
/// being compiled is a standalone dart script not inside a package.
Future<String?> _resolvePackageConfig(
DartExecutableWithPackageConfig executable, String packageRoot) async {
final packageConfig = await findPackageConfigUri(
Uri.file(p.canonicalize(executable.executable)),
recurse: true,
onError: (_) {},
);
if (packageConfig != null) {
final dotPackageFile = File(p.join(packageRoot, '.packages'));
final packageConfigFile = File(p.join(packageRoot, packageConfigName));
return packageConfigFile.existsSync()
? packageConfigFile.path
: dotPackageFile.path;
}
return null;
}
/// Indicates the type of issue encountered with the
/// Resident Frontend Compiler
enum CompilationIssue {
/// Communication with the Resident Frontend Compiler failed.
serverError,
/// The Resident Frontend Compiler failed to launch
serverCreationError,
/// There were compilation errors in the Dart source code.
compilationError,
/// Resident mode is only supported for sources within Dart packages
standaloneProgramError,
}
/// Indicates an error with the Resident Frontend Compiler.
class FrontendCompilerException implements Exception {
final String message;
final CompilationIssue issue;
FrontendCompilerException._(this.message, this.issue);
@override
String toString() {
return 'FrontendCompilerException: $message';
}
}