[dynamic_modules] Add support for deduplicating library prefixes to dart2wasm

Change-Id: I325e4e1e3aff25c4e894bc2f3f23fcf02f15da16
Fixes: https://github.com/dart-lang/sdk/issues/62828
Tested: Dynamic module tests.
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/485980
Commit-Queue: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Auto-Submit: Nate Biggs <natebiggs@google.com>
This commit is contained in:
Nate Biggs
2026-03-06 05:39:21 -08:00
committed by Commit Queue
parent 64797f76c9
commit 44a049c082
7 changed files with 106 additions and 72 deletions
+5 -57
View File
@@ -15,7 +15,7 @@ import 'package:front_end/src/api_unstable/vm.dart'
parseExperimentalArguments,
parseExperimentalFlags,
resolveInputUri;
import 'package:kernel/ast.dart' show Component, Library, Source;
import 'package:kernel/ast.dart' show Component;
import 'package:vm/kernel_front_end.dart'
show
badUsageExitCode,
@@ -30,6 +30,8 @@ import 'package:vm/kernel_front_end.dart'
parseCommandLineDefines,
successExitCode,
writeDepfile;
import 'package:vm/transformations/prefix_library_uris.dart'
as prefix_library_uris;
import 'bytecode_serialization.dart' show BytecodeSizeStatistics;
import 'bytecode_generator.dart' show generateBytecode;
@@ -260,8 +262,8 @@ Future<int> runCompilerWithOptions({
if (errorDetector.hasCompilationErrors || component == null) {
return compileTimeErrorExitCode;
}
component =
prefixLibraryUris(component, results.loadedLibraries, libraryUrisPrefix);
component = prefix_library_uris.prefixLibraryUris(
component, results.loadedLibraries, libraryUrisPrefix);
if (bytecodeOptions.showBytecodeSizeStatistics) {
BytecodeSizeStatistics.reset();
}
@@ -291,57 +293,3 @@ Future<int> runCompilerWithOptions({
return successExitCode;
}
Component prefixLibraryUris(Component component, Set<Library> loadedLibraries,
String libraryUrisPrefix) {
if (libraryUrisPrefix.isEmpty) {
return component;
}
final prefixSegments = libraryUrisPrefix.split('/');
final importUriReplacements = <Uri, Uri>{};
for (final lib in component.libraries) {
// Skip libraries that come from the host app or the SDK.
if (loadedLibraries.contains(lib)) {
continue;
}
final newImportUri = prefixUri(lib.importUri, prefixSegments);
importUriReplacements[lib.importUri] = newImportUri;
lib.importUri = newImportUri;
}
// Update import uris in sources.
final allSourceFileUris = component.uriToSource.keys.toSet();
for (final fileUri in allSourceFileUris) {
final source = component.uriToSource[fileUri]!;
final importUriReplacement = importUriReplacements[source.importUri];
if (importUriReplacement == null) {
continue;
}
// Rewrite the source with the new import URI.
component.uriToSource[fileUri] = Source(
source.lineStarts,
source.source,
importUriReplacement,
source.fileUri,
)
..cachedText = source.cachedText
..constantCoverageConstructors = source.constantCoverageConstructors;
}
return component;
}
Uri prefixUri(Uri uri, List<String> prefixSegments) {
if (uri.scheme == 'package') {
// For package URIs, the first segment is dot-separated package path, so
// we prepend the prefix to the first segment.
final pathSegments = uri.pathSegments.toList();
pathSegments[0] = [...prefixSegments, pathSegments.first].join('.');
return uri.replace(pathSegments: pathSegments);
}
// For other schemes, we just prepend the prefix to the path segments.
return uri.replace(pathSegments: prefixSegments.followedBy(uri.pathSegments));
}
+1
View File
@@ -50,6 +50,7 @@ class WasmCompilerOptions {
Uri? loadsIdsUri;
Uri? programSplitConstraintsUri;
bool validateDynamicModules = true;
String? dynamicModuleLibraryPrefix;
Map<String, String> environment = {};
Map<fe.ExperimentalFlag, bool> feExperimentalFlags = const {};
String? multiRootScheme;
+5 -2
View File
@@ -115,8 +115,8 @@ final List<Option> options = [
(o, value) => o.translatorOptions.enableDeferredLoading = value,
defaultsTo: _d.translatorOptions.enableDeferredLoading),
UriOption("load-ids", (o, value) => o.loadsIdsUri = value),
UriOption("read-program-split",
(o, value) => o.programSplitConstraintsUri = value),
UriOption(
"read-program-split", (o, value) => o.programSplitConstraintsUri = value),
Flag("enable-multi-module-stress-test-mode",
(o, value) => o.translatorOptions.enableMultiModuleStressTestMode = value,
defaultsTo: _d.translatorOptions.enableMultiModuleStressTestMode),
@@ -147,6 +147,9 @@ final List<Option> options = [
Flag("validate-dynamic-modules",
(o, value) => o.validateDynamicModules = value,
defaultsTo: true, negatable: true),
StringOption("dynamic-module-library-prefix",
(o, value) => o.dynamicModuleLibraryPrefix = value),
UriOption("wasm-opt", (o, value) => o.wasmOptPath = value),
// The maximum number of concurrent wasm-opt processes to run. Defaults to the
// number of processors on the machine. Use -1 to run with no limit.
+13 -1
View File
@@ -13,6 +13,8 @@ import 'package:vm/transformations/devirtualization.dart';
import 'package:vm/transformations/dynamic_interface_annotator.dart'
as dynamic_interface_annotator;
import 'package:vm/transformations/pragma.dart';
import 'package:vm/transformations/prefix_library_uris.dart'
as library_prefix_uris;
import 'package:vm/transformations/type_flow/table_selector_assigner.dart';
import 'package:wasm_builder/wasm_builder.dart' as w;
@@ -255,6 +257,16 @@ class DynamicSubmoduleStrategy extends ModuleStrategy {
}
void _registerLibraries() {
final recordLibrary = component.libraries
.firstWhere((l) => '${l.importUri}' == dynamicModulesRecordsLibraryUri);
if (options.dynamicModuleLibraryPrefix case final uriPrefix?) {
library_prefix_uris.prefixLibraryUris(
component,
component.libraries
.where((l) => l.isFromMainModule(coreTypes))
.toSet(),
uriPrefix);
}
// Register each library with the SDK. This will ensure no duplicate
// libraries are included across dynamic modules.
final registerLibraryUris = coreTypes.index
@@ -263,7 +275,7 @@ class DynamicSubmoduleStrategy extends ModuleStrategy {
final libraryUris = ListLiteral([
...component
.getDynamicSubmoduleLibraries(coreTypes)
.where((l) => '${l.importUri}' != dynamicModulesRecordsLibraryUri)
.where((l) => l != recordLibrary)
.map((l) => StringLiteral(l.importUri.toString()))
], typeArgument: coreTypes.stringNonNullableRawType);
entryPoint.function.body = Block([
@@ -56,12 +56,13 @@ class Dart2wasmExecutor implements TargetExecutor {
}
Future _compile(
String testName,
DynamicModuleTest test,
String name,
String source,
Uri sourceDir,
bool isMain,
) async {
var testDir = _tmp.uri.resolve(testName).toFilePath();
var testDir = _tmp.uri.resolve(test.name).toFilePath();
var args = [
'--compiler-asserts',
'--packages=${repoRoot.toFilePath()}/.dart_tool/package_config.json',
@@ -73,9 +74,10 @@ class Dart2wasmExecutor implements TargetExecutor {
'--extra-compiler-option=--dynamic-module-type=${isMain ? "main" : "submodule"}',
'--extra-compiler-option=--dynamic-module-main=main.dart.dill',
'--extra-compiler-option=--dynamic-module-interface='
'$rootScheme:/data/$testName/dynamic_interface.yaml',
'$rootScheme:/data/${test.name}/dynamic_interface.yaml',
'--extra-compiler-option=--minify',
'$rootScheme:/data/$testName/$source',
'--dynamic-module-library-prefix=${test.name.endsWith('_prefixed') ? name : 'import/prefix'}',
'$rootScheme:/data/${test.name}/$source',
'$source.wasm',
];
await runProcess(
@@ -83,7 +85,7 @@ class Dart2wasmExecutor implements TargetExecutor {
args,
testDir,
_logger,
'compile $testName/$source',
'compile ${test.name}/$source',
);
}
@@ -91,14 +93,14 @@ class Dart2wasmExecutor implements TargetExecutor {
Future compileApplication(DynamicModuleTest test) async {
_ensureDirectory(test.name);
_logger.info('Compile ${test.name} app');
await _compile(test.name, test.main, test.folder, true);
await _compile(test, test.name, test.main, test.folder, true);
}
@override
Future compileDynamicModule(DynamicModuleTest test, String name) async {
_logger.info('Compile module ${test.name}.$name');
_ensureDirectory(test.name);
await _compile(test.name, test.dynamicModules[name]!, test.folder, false);
await _compile(test, name, test.dynamicModules[name]!, test.folder, false);
}
@override
@@ -0,0 +1,63 @@
// Copyright (c) 2026, 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 'package:kernel/ast.dart';
Component prefixLibraryUris(
Component component,
Set<Library> loadedLibraries,
String libraryUrisPrefix,
) {
if (libraryUrisPrefix.isEmpty) {
return component;
}
final prefixSegments = libraryUrisPrefix.split('/');
final importUriReplacements = <Uri, Uri>{};
for (final lib in component.libraries) {
// Skip libraries that come from the host app or the SDK.
if (loadedLibraries.contains(lib)) {
continue;
}
final newImportUri = prefixUri(lib.importUri, prefixSegments);
importUriReplacements[lib.importUri] = newImportUri;
lib.importUri = newImportUri;
}
// Update import uris in sources.
final allSourceFileUris = component.uriToSource.keys.toSet();
for (final fileUri in allSourceFileUris) {
final source = component.uriToSource[fileUri]!;
final importUriReplacement = importUriReplacements[source.importUri];
if (importUriReplacement == null) {
continue;
}
// Rewrite the source with the new import URI.
component.uriToSource[fileUri] =
Source(
source.lineStarts,
source.source,
importUriReplacement,
source.fileUri,
)
..cachedText = source.cachedText
..constantCoverageConstructors = source.constantCoverageConstructors;
}
return component;
}
Uri prefixUri(Uri uri, List<String> prefixSegments) {
if (uri.scheme == 'package') {
// For package URIs, the first segment is dot-separated package path, so
// we prepend the prefix to the first segment.
final pathSegments = uri.pathSegments.toList();
pathSegments[0] = [...prefixSegments, pathSegments.first].join('.');
return uri.replace(pathSegments: pathSegments);
}
// For other schemes, we just prepend the prefix to the path segments.
return uri.replace(pathSegments: prefixSegments.followedBy(uri.pathSegments));
}
@@ -1,18 +1,23 @@
// Copyright (c) 2026, 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 'package:dart2bytecode/dart2bytecode.dart' show prefixUri;
import 'package:test/test.dart';
import 'package:vm/transformations/prefix_library_uris.dart' show prefixUri;
void main() {
group('prefixUri', () {
test('prefixes file URIs', () {
expect(prefixUri(Uri.parse('file:///foo/bar.dart'), ['pre', 'fix']),
Uri.parse('file:///pre/fix/foo/bar.dart'));
expect(
prefixUri(Uri.parse('file:///foo/bar.dart'), ['pre', 'fix']),
Uri.parse('file:///pre/fix/foo/bar.dart'),
);
});
test('prefixes package URIs', () {
expect(prefixUri(Uri.parse('package:foo/bar.dart'), ['pre', 'fix']),
Uri.parse('package:pre.fix.foo/bar.dart'));
expect(
prefixUri(Uri.parse('package:foo/bar.dart'), ['pre', 'fix']),
Uri.parse('package:pre.fix.foo/bar.dart'),
);
});
});
}