| // 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'; |
| } |
| } |