blob: 83271f0c49e124e2937a423fb8d18de83eaa740f [file] [log] [blame]
// Copyright (c) 2025, 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:io';
import 'package:kernel/kernel.dart';
import 'package:kernel/binary/ast_to_binary.dart';
import 'package:front_end/src/kernel/dynamic_module_validator.dart';
/// Tool to trim an .dill file.
///
/// This function reads full .dill files and trims them with a simple goal:
/// produce small .dill files that preserve all the information needed for
/// modular compilation by the bytecode compiler. This is done as a combination
/// of removing unnecessary dependencies and stripping out details, like method
/// bodies. This function would become simpler once CFE can directly produce
/// outlines matching what's needed by the bytecode compiler.
///
/// Currently this function preserves method bodies of mixin declarations and
/// const constructors, which are needed by the compiler. Currently there is not
/// a fine-grain definition of which mixin or const constructors may be used,
/// but this algorithm could be extended to handle trimming in a more fine-grain
/// fashion in the future.
///
/// This function only accepts inputs containing libraries with a Dart version
/// 3.0 or newer. That allows us to ignore legacy mixin declarations. We also
/// assume that the input .dill contains all transitive dependencies needed to
/// properly serialize and visit the AST (this typically includes platform
/// libraries).
///
/// This function expects the caller to provide details about which libraries
/// are known entry points that need to be preserved. It doesn't do a fine-grain
/// tree-shaking, but will delete libraries that can't be reached from those
/// entry points. These entry points can be derived from the
/// `dynamic_interface.yaml` used by dynamic modules.
Future<void> createTrimmedCopy(TrimOptions options) async {
Component component = loadComponentFromBinary(options.inputPlatformPath);
loadComponentFromBinary(options.inputAppPath, component);
bool Function(Library) isExtendable;
bool Function(Library) isRoot;
if (options.dynamicInterfaceContents == null) {
// Include all libraries from a set of user libraries.
isExtendable = (lib) => true;
isRoot = (Library lib) =>
_isRootFromPatterns(lib, options.requiredUserLibraries);
} else {
// Include all libraries declared as accessible from the dynamic_interface
// specification.
DynamicInterfaceSpecification spec = new DynamicInterfaceSpecification(
options.dynamicInterfaceContents!,
options.dynamicInterfaceUri!,
component,
);
Library enclosingLibrary(TreeNode node) => switch (node) {
Member() => node.enclosingLibrary,
Class() => node.enclosingLibrary,
Library() => node,
_ => throw 'Unexpected node ${node.runtimeType} $node',
};
Set<Library> extendableLibraries = spec.extendable
.map(enclosingLibrary)
.toSet();
Set<Library> roots = {
...spec.callable.map(enclosingLibrary),
...extendableLibraries,
...spec.canBeOverridden.map(enclosingLibrary),
};
isExtendable = extendableLibraries.contains;
isRoot = roots.contains;
}
Set<Library> included = {};
// Validate version and clear method bodies.
component.accept(new Trimmer(options.librariesToClear, isExtendable));
// Include all root libraries according to flags or dynamic interface.
addReachable(component.libraries, isRoot, included);
// Also include libraries needed by the required platform libraries.
addReachable(
component.libraries,
(Library lib) => _isRootFromPatterns(lib, options.requiredDartLibraries),
included,
);
component.uriToSource.clear();
component.setMainMethodAndMode(null, true);
Future<void> emit(String path, bool isPlatform) async {
Set<Library> filteredSet = included
.where(
(lib) => isPlatform
? lib.importUri.isScheme('dart')
: !lib.importUri.isScheme('dart'),
)
.toSet();
IOSink sink = new File(path).openWrite();
BinaryPrinter printer = new BinaryPrinter(
sink,
libraryFilter: filteredSet.contains,
includeSources: false,
includeSourceBytes: false,
);
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
}
if (options.outputPlatformPath != null) {
await emit(options.outputPlatformPath!, true);
}
await emit(options.outputAppPath, false);
}
/// Helper to determine whether a library is an included root, if provided
/// with [TrimOptions.requiredUserLibraries] or
/// [TrimOptions.requiredDartLibraries].
bool _isRootFromPatterns(Library lib, Set<String> patterns) {
List<String> prefixPatterns = patterns
.where((p) => p.endsWith('*'))
.map((p) => p.substring(0, p.length - 1))
.toList();
Set<String> exactPatterns = patterns.where((p) => !p.endsWith('*')).toSet();
String uriString = '${lib.importUri}';
if (exactPatterns.contains(uriString)) return true;
if (prefixPatterns.any((p) => uriString.startsWith(p))) return true;
return false;
}
/// Validates that all libraries with extendable classes are 3.0 or higher, then
/// trims contents as much as possible, while enabling modular compilation later
/// on.
///
/// Currently we:
/// * deletes bodies of constructors and procedures, except when deemed
/// necessary for mixin applications and constants.
/// * clear libraries whose contents are unnecessary, even if reachable.
/// * clear unnecessary field initializers.
class Trimmer extends RecursiveVisitor {
/// Platform libraries that will be cleared internally.
///
/// `Target.extraRequiredLibraries` demands that some platform libraries are
/// always included in the platform .dill file. However, there are libraries,
/// that are required by the target that are used only for non-release builds.
/// Until we can tailor the required libraries to specific configurations, we
/// add this step to remove the contents of those libraries, without removing
/// the library node itself.
final Set<String> librariesToClear;
/// Subset of libraries that may contain extendable classes according to the
/// dynamic interface (defaults to all libraries if the dynamic interface is
/// not provided).
///
/// Used by the trimmer to determine whether legacy mixins may be at play in
/// the dynamic interface.
final bool Function(Library) isExtendable;
/// Whether we are within a mixin declaration in an extendable library, and
/// hence member bodies need to be preserved.
bool preserveMemberBodies = false;
Trimmer(this.librariesToClear, this.isExtendable);
@override
void visitLibrary(Library node) {
Uri uri = node.importUri;
if (isExtendable(node) && node.languageVersion.major < 3) {
print(
'Error: Only libraries 3.0 or newer may be used for extendable '
'classes. The library `"$uri" includes extendable classes, but has '
'version ${node.languageVersion.toText()}, which is older than 3.0.',
);
exit(1);
}
if (librariesToClear.contains(uri.toString())) {
node.classes.clear();
node.procedures.clear();
node.extensions.clear();
node.fields.clear();
node.typedefs.clear();
node.extensionTypeDeclarations.clear();
node.parts.clear();
node.dependencies.clear();
node.additionalExports.clear();
return;
}
super.visitLibrary(node);
}
@override
void visitClass(Class node) {
preserveMemberBodies =
isExtendable(node.enclosingLibrary) &&
(node.isMixinClass || node.isMixinDeclaration);
super.visitClass(node);
preserveMemberBodies = false;
}
@override
void visitConstructor(Constructor node) {
// Mixin class constructors are not needed, only mixin method bodies.
node.function.body = null;
// Initializers can be removed in general, except for initializers of const
// constructors. Those are needed for constant evaluation in the CFE and
// proper canonicalization.
if (!node.isConst) {
node.initializers.clear();
}
}
@override
void visitProcedure(Procedure node) {
// Preserve method bodies of mixin declarations, these are copied when
// mixins are applied in subtypes.
if (!preserveMemberBodies) {
node.function.body = null;
}
}
@override
void visitField(Field node) {
// Constant initializers are necessary for constant evaluation
if (node.isConst) return;
if (!node.isStatic && node.enclosingClass!.hasConstConstructor) return;
// Unfortunately a `null` initializer may be misinterpreted by the CFE or
// the compiler. Ideally the kernel representation should have a sentinel
// marker so the actual initializer could be removed.
//
// These exceptions are a result of this issue:
// * Late final fields (may get an implicit setter)
// * Static fields (may change the code generated for accessing the field)
if (node.isLate && node.isFinal) return;
if (node.isStatic) return;
// Preserve field initializers in mixin declarations, these are copied when
// mixins are applied in subtypes.
if (!preserveMemberBodies) {
node.initializer = null;
}
}
}
/// Select [libraries] whose import belongs to any of the [patterns] and
/// any other transitively reachable library.
void addReachable(
List<Library> libraries,
bool Function(Library) isRoot,
Set<Library> result,
) {
List<Library> pending = [
for (Library lib in libraries)
if (isRoot(lib)) lib,
];
while (!pending.isEmpty) {
Library lib = pending.removeLast();
if (result.add(lib)) {
pending.addAll(lib.dependencies.map((dep) => dep.targetLibrary));
}
}
}
/// Options to configure the behavior of [createTrimmedCopy].
class TrimOptions {
/// Path to the input dill file containing the application contents.
final String inputAppPath;
/// Path to the input dill file containing the platform libraries.
final String inputPlatformPath;
/// Path to the output dill file containing the application contents.
final String outputAppPath;
/// Path to the output dill file containing the platform libraries.
final String? outputPlatformPath;
/// Contents of the `dynamic_interface.yaml` file, used to compute required
/// user libraries.
///
/// Must be null if [requiredUserLibraries] is not empty.
// Note: we do not provide a file-system path in order to support kernel files
// that use custom schemes (e.g. not `file:/`).
final String? dynamicInterfaceContents;
/// Base uri of the `dynamic_interface.yaml` needed to resolve library
/// references within that file. This can be a `file:` or a custom scheme Uri.
final Uri? dynamicInterfaceUri;
/// User libraries that must be preserved in the .dill file.
final Set<String> requiredUserLibraries;
/// Platform libraries that must be preserved in the .dill file.
///
/// Leave empty to produce a .dill containing user-code only.
final Set<String> requiredDartLibraries;
/// Libraries that are not needed for production builds and that should
/// be possible to clear when trimming .dill files, even if they need
/// to be present in the dill file for other reasons.
final Set<String> librariesToClear;
TrimOptions({
required this.inputAppPath,
required this.inputPlatformPath,
required this.outputAppPath,
required this.outputPlatformPath,
this.dynamicInterfaceContents,
this.dynamicInterfaceUri,
this.requiredUserLibraries = const {},
required this.requiredDartLibraries,
this.librariesToClear = const {},
}) {
if (dynamicInterfaceContents != null && requiredUserLibraries.isNotEmpty) {
throw new ArgumentError(
'Both dynamic interface and required user '
'libraries specified at once. Only one expected',
);
}
if (dynamicInterfaceContents == null && requiredUserLibraries.isEmpty) {
throw new ArgumentError(
'Both dynamic interface and required user '
'libraries missing. Only one expected',
);
}
}
}