Version 2.16.0-64.0.dev
Merge commit '20f1a954fe6681c1edaddeb13bac4899d7048edd' into 'dev'
diff --git a/pkg/compiler/lib/src/dart2js.dart b/pkg/compiler/lib/src/dart2js.dart
index 156cd9d..7df673c 100644
--- a/pkg/compiler/lib/src/dart2js.dart
+++ b/pkg/compiler/lib/src/dart2js.dart
@@ -664,10 +664,17 @@
// Wire up feature flags.
OptionHandler(Flags.canary, passThrough),
OptionHandler(Flags.noShipping, passThrough),
+ // Shipped features.
+ for (var feature in features.shipped)
+ OptionHandler('--${feature.flag}', passThrough),
+ for (var feature in features.shipped)
+ OptionHandler('--no-${feature.flag}', passThrough),
+ // Shipping features.
for (var feature in features.shipping)
OptionHandler('--${feature.flag}', passThrough),
for (var feature in features.shipping)
OptionHandler('--no-${feature.flag}', passThrough),
+ // Canary features.
for (var feature in features.canary)
OptionHandler('--${feature.flag}', passThrough),
for (var feature in features.canary)
diff --git a/pkg/compiler/lib/src/options.dart b/pkg/compiler/lib/src/options.dart
index d0a06d4..e6db1d9 100644
--- a/pkg/compiler/lib/src/options.dart
+++ b/pkg/compiler/lib/src/options.dart
@@ -18,6 +18,7 @@
}
enum FeatureStatus {
+ shipped,
shipping,
canary,
}
@@ -31,6 +32,10 @@
/// passed. The [isNegativeFlag] bool flips things around so while in [canary]
/// the [FeatureOption] is enabled unless explicitly disabled, and while in
/// [staging] it is disabled unless explicitly enabled.
+///
+/// Finally, mature features can be moved to [shipped], at which point we ignore
+/// the flag, but throw if the value of the flag is unexpected(i.e. if a
+/// positive flag is disabled, or a negative flag is enabled).
class FeatureOption {
final String flag;
final bool isNegativeFlag;
@@ -71,6 +76,9 @@
/// Whether to generate code compliant with Content Security Policy.
FeatureOption useContentSecurityPolicy = FeatureOption('csp');
+ /// [FeatureOption]s which are shipped and cannot be toggled.
+ late final List<FeatureOption> shipped = [];
+
/// [FeatureOption]s which default to enabled.
late final List<FeatureOption> shipping = [
legacyJavaScript,
@@ -107,6 +115,7 @@
/// Parses a [List<String>] and enables / disables features as necessary.
void parse(List<String> options) {
+ _verifyShippedFeatures(options, shipped);
_extractFeatures(options, shipping, FeatureStatus.shipping);
_extractFeatures(options, canary, FeatureStatus.canary);
}
@@ -908,4 +917,28 @@
}
}
+void _verifyShippedFeatures(
+ List<String> options, List<FeatureOption> features) {
+ for (var feature in features) {
+ String featureFlag = feature.flag;
+ String enableFeatureFlag = '--$featureFlag';
+ String disableFeatureFlag = '--no-$featureFlag';
+ bool enableFeature = _hasOption(options, enableFeatureFlag);
+ bool disableFeature = _hasOption(options, disableFeatureFlag);
+ if (enableFeature && disableFeature) {
+ throw ArgumentError("'$enableFeatureFlag' incompatible with "
+ "'$disableFeatureFlag'");
+ }
+ if (enableFeature && feature.isNegativeFlag) {
+ throw ArgumentError(
+ "$disableFeatureFlag has already shipped and cannot be enabled.");
+ }
+ if (disableFeature && !feature.isNegativeFlag) {
+ throw ArgumentError(
+ "$enableFeatureFlag has already shipped and cannot be disabled.");
+ }
+ feature.state = !feature.isNegativeFlag;
+ }
+}
+
const String _UNDETERMINED_BUILD_ID = "build number could not be determined";
diff --git a/pkg/compiler/test/end_to_end/feature_options_test.dart b/pkg/compiler/test/end_to_end/feature_options_test.dart
index d950d19..db2c87e 100644
--- a/pkg/compiler/test/end_to_end/feature_options_test.dart
+++ b/pkg/compiler/test/end_to_end/feature_options_test.dart
@@ -12,6 +12,8 @@
import 'package:compiler/src/options.dart' show FeatureOptions, FeatureOption;
class TestFeatureOptions extends FeatureOptions {
+ FeatureOption f1 = FeatureOption('f1');
+ FeatureOption noF2 = FeatureOption('f2', isNegativeFlag: true);
FeatureOption sf1 = FeatureOption('sf1');
FeatureOption sf2 = FeatureOption('sf2');
FeatureOption noSf3 = FeatureOption('sf3', isNegativeFlag: true);
@@ -22,6 +24,9 @@
FeatureOption noCf4 = FeatureOption('cf4', isNegativeFlag: true);
@override
+ List<FeatureOption> shipped;
+
+ @override
List<FeatureOption> shipping;
@override
@@ -29,6 +34,7 @@
// Initialize feature lists.
TestFeatureOptions() {
+ shipped = [f1, noF2];
shipping = [sf1, sf2, noSf3, noSf4];
canary = [cf1, cf2, noCf3, noCf4];
}
@@ -40,8 +46,14 @@
return tfo;
}
+void expectShipped(TestFeatureOptions tfo) {
+ Expect.isTrue(tfo.f1.isEnabled);
+ Expect.isTrue(tfo.noF2.isDisabled);
+}
+
void testShipping() {
var tfo = test([]);
+ expectShipped(tfo);
Expect.isTrue(tfo.sf1.isEnabled);
Expect.isTrue(tfo.sf2.isEnabled);
Expect.isTrue(tfo.noSf3.isDisabled);
@@ -54,6 +66,7 @@
void testNoShipping() {
var tfo = test([Flags.noShipping]);
+ expectShipped(tfo);
Expect.isTrue(tfo.sf1.isDisabled);
Expect.isTrue(tfo.sf2.isDisabled);
Expect.isTrue(tfo.noSf3.isEnabled);
@@ -66,6 +79,7 @@
void testCanary() {
var tfo = test([Flags.canary]);
+ expectShipped(tfo);
Expect.isTrue(tfo.sf1.isEnabled);
Expect.isTrue(tfo.sf2.isEnabled);
Expect.isTrue(tfo.noSf3.isDisabled);
@@ -78,6 +92,7 @@
void testShippingDisabled() {
var tfo = test(['--no-sf2', '--sf3']);
+ expectShipped(tfo);
Expect.isTrue(tfo.sf1.isEnabled);
Expect.isTrue(tfo.sf2.isDisabled);
Expect.isTrue(tfo.noSf3.isEnabled);
@@ -90,6 +105,7 @@
void testCanaryDisabled() {
var tfo = test([Flags.canary, '--no-sf2', '--sf3', '--no-cf1', '--cf3']);
+ expectShipped(tfo);
Expect.isTrue(tfo.sf1.isEnabled);
Expect.isTrue(tfo.sf2.isDisabled);
Expect.isTrue(tfo.noSf3.isEnabled);
@@ -102,6 +118,7 @@
void testNoShippingEnabled() {
var tfo = test([Flags.noShipping, '--sf1', '--no-sf3', '--cf2', '--no-cf3']);
+ expectShipped(tfo);
Expect.isTrue(tfo.sf1.isEnabled);
Expect.isTrue(tfo.sf2.isDisabled);
Expect.isTrue(tfo.noSf3.isDisabled);
@@ -114,6 +131,7 @@
void testNoCanaryEnabled() {
var tfo = test(['--cf1', '--no-cf3']);
+ expectShipped(tfo);
Expect.isTrue(tfo.sf1.isEnabled);
Expect.isTrue(tfo.sf2.isEnabled);
Expect.isTrue(tfo.noSf3.isDisabled);
@@ -128,6 +146,11 @@
Expect.throwsArgumentError(() => test(['--cf1', '--no-cf1']));
}
+void testNoShippedDisable() {
+ Expect.throwsArgumentError(() => test(['--no-f1']));
+ Expect.throwsArgumentError(() => test(['--f2']));
+}
+
void flavorStringTest(List<String> options, String expectedFlavorString) {
var tfo = test(options);
Expect.equals(expectedFlavorString, tfo.flavorString());
@@ -159,6 +182,7 @@
testNoCanaryEnabled();
testNoShippingEnabled();
testFlagCollision();
+ testNoShippedDisable();
// Supplemental tests.
flavorStringTests();
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index 5f70e3b..6f24604 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -8,7 +8,6 @@
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
-import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as path;
import 'package:vm_service/vm_service.dart' as vm;
@@ -949,12 +948,13 @@
/// 'additionalProjectPaths' in the launch arguments. An editor should include
/// the paths of all open workspace folders in 'additionalProjectPaths' to
/// support this feature correctly.
- bool isExternalPackageLibrary(Uri uri) {
+ Future<bool> isExternalPackageLibrary(ThreadInfo thread, Uri uri) async {
if (!uri.isScheme('package')) {
return false;
}
- final libraryPath = resolvePackageUri(uri);
- if (libraryPath == null) {
+
+ final packagePath = await thread.resolveUriToPackageLibPath(uri);
+ if (packagePath == null) {
return false;
}
@@ -962,9 +962,10 @@
// may have returned different casing (e.g. Windows drive letters). It's
// almost certain a user wouldn't have a "local" package and an "external"
// package with paths differing only be case.
- final libraryPathLower = libraryPath.toLowerCase();
- return !projectPaths.any((projectPath) =>
- path.isWithin(projectPath.toLowerCase(), libraryPathLower));
+ final packagePathLower = packagePath.toLowerCase();
+ return !projectPaths
+ .map((projectPath) => projectPath.toLowerCase())
+ .any((projectPath) => path.isWithin(projectPath, packagePathLower));
}
/// Checks whether this library is from the SDK.
@@ -1002,10 +1003,10 @@
///
/// Initial values are provided in the launch arguments, but may be updated
/// by the `updateDebugOptions` custom request.
- bool libaryIsDebuggable(Uri uri) {
+ Future<bool> libraryIsDebuggable(ThreadInfo thread, Uri uri) async {
if (isSdkLibrary(uri)) {
return _isolateManager.debugSdkLibraries;
- } else if (isExternalPackageLibrary(uri)) {
+ } else if (await isExternalPackageLibrary(thread, uri)) {
return _isolateManager.debugExternalPackageLibraries;
} else {
return true;
@@ -1024,12 +1025,6 @@
sendResponse();
}
- /// Resolves a `package: URI` to the real underlying source path.
- ///
- /// Returns `null` if no mapping was possible, for example if the package is
- /// not in the package mapping file.
- String? resolvePackageUri(Uri uri) => _converter.resolvePackageUri(uri);
-
/// restart is called by the client when the user invokes a restart (for
/// example with the button on the debug toolbar).
///
@@ -1296,6 +1291,15 @@
(frame) => frame.kind == vm.FrameKind.kAsyncSuspensionMarker,
);
+ // Pre-resolve all URIs in batch so the call below does not trigger
+ // many requests to the server.
+ final allUris = frames
+ .map((frame) => frame.location?.script?.uri)
+ .whereNotNull()
+ .map(Uri.parse)
+ .toList();
+ await thread.resolveUrisToPathsBatch(allUris);
+
Future<StackFrame> convert(int index, vm.Frame frame) async {
return _converter.convertVmToDapStackFrame(
thread,
@@ -1396,14 +1400,12 @@
/// Sets the package config file to use for `package: URI` resolution.
///
- /// TODO(dantup): Remove this once
- /// https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
- /// necessary.
+ /// It is no longer necessary to call this method as the package config file
+ /// is no longer used. URI lookups are done via the VM Service.
+ @Deprecated('No longer necessary, URI lookups are done via VM Service')
void usePackageConfigFile(File packageConfig) {
- _converter.packageConfig = PackageConfig.parseString(
- packageConfig.readAsStringSync(),
- Uri.file(packageConfig.path),
- );
+ // TODO(dantup): Remove this method after Flutter DA is updated not to use
+ // it.
}
/// [variablesRequest] is called by the client to request child variables for
diff --git a/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart b/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
index 3dca392..3560867 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
@@ -108,18 +108,6 @@
...?args.args,
];
- // Find the package_config file for this script.
- // TODO(dantup): Remove this once
- // https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
- // necessary.
- var possibleRoot = path.isAbsolute(args.program)
- ? path.dirname(args.program)
- : path.dirname(path.normalize(path.join(args.cwd ?? '', args.program)));
- final packageConfig = findPackageConfigFile(possibleRoot);
- if (packageConfig != null) {
- this.usePackageConfigFile(packageConfig);
- }
-
// If the client supports runInTerminal and args.console is set to either
// 'terminal' or 'runInTerminal' we won't run the process ourselves, but
// instead call the client to run it for us (this allows it to run in a
@@ -166,18 +154,6 @@
return;
}
- // Find the package_config file for this script.
- // TODO(dantup): Remove this once
- // https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
- // necessary.
- final cwd = args.cwd;
- if (cwd != null) {
- final packageConfig = findPackageConfigFile(cwd);
- if (packageConfig != null) {
- this.usePackageConfigFile(packageConfig);
- }
- }
-
final uri = vmServiceUri != null
? Uri.parse(vmServiceUri)
: await waitForVmServiceInfoFile(logger, File(vmServiceInfoFile!));
diff --git a/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart b/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart
index 9e517da..2a63b38 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart
@@ -6,7 +6,6 @@
import 'dart:convert';
import 'dart:io';
-import 'package:path/path.dart' as path;
import 'package:pedantic/pedantic.dart';
import 'package:vm_service/vm_service.dart' as vm;
@@ -118,18 +117,6 @@
...?args.args,
];
- // Find the package_config file for this script.
- // TODO(dantup): Remove this once
- // https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
- // necessary.
- var possibleRoot = path.isAbsolute(args.program)
- ? path.dirname(args.program)
- : path.dirname(path.normalize(path.join(args.cwd ?? '', args.program)));
- final packageConfig = findPackageConfigFile(possibleRoot);
- if (packageConfig != null) {
- this.usePackageConfigFile(packageConfig);
- }
-
// TODO(dantup): Support passing env to both of these.
logger?.call('Spawning $vmPath with $processArgs in ${args.cwd}');
diff --git a/pkg/dds/lib/src/dap/adapters/mixins.dart b/pkg/dds/lib/src/dap/adapters/mixins.dart
index 3b9991a..b9f307f 100644
--- a/pkg/dds/lib/src/dap/adapters/mixins.dart
+++ b/pkg/dds/lib/src/dap/adapters/mixins.dart
@@ -17,32 +17,13 @@
mixin PackageConfigUtils {
/// Find the `package_config.json` file for the program being launched.
///
- /// TODO(dantup): Remove this once
- /// https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
- /// necessary.
+ /// It is no longer necessary to call this method as the package config file
+ /// is no longer used. URI lookups are done via the VM Service.
+ @Deprecated('No longer necessary, URI lookups are done via VM Service')
File? findPackageConfigFile(String possibleRoot) {
- File? packageConfig;
- while (true) {
- packageConfig =
- File(path.join(possibleRoot, '.dart_tool', 'package_config.json'));
-
- // If this packageconfig exists, use it.
- if (packageConfig.existsSync()) {
- break;
- }
-
- final parent = path.dirname(possibleRoot);
-
- // If we can't go up anymore, the search failed.
- if (parent == possibleRoot) {
- packageConfig = null;
- break;
- }
-
- possibleRoot = parent;
- }
-
- return packageConfig;
+ // TODO(dantup): Remove this method after Flutter DA is updated not to use
+ // it.
+ return null;
}
}
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index 6e00463..6d678dc 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -4,8 +4,10 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:io';
import 'package:collection/collection.dart';
+import 'package:path/path.dart' as path;
import 'package:vm_service/vm_service.dart' as vm;
import 'adapters/dart.dart';
@@ -53,6 +55,9 @@
/// [debugExternalPackageLibraries] in one step.
bool debugExternalPackageLibraries = true;
+ /// The root of the Dart SDK containing the VM running the debug adapter.
+ late final String sdkRoot;
+
/// Tracks breakpoints last provided by the client so they can be sent to new
/// isolates that appear after initial breakpoints were sent.
final Map<String, List<SourceBreakpoint>> _clientBreakpointsByUri = {};
@@ -93,7 +98,10 @@
/// Any leading character matched in place of the dollar is in the first capture.
final _braceNotPrefixedByDollarOrBackslashPattern = RegExp(r'(^|[^\\\$]){');
- IsolateManager(this._adapter);
+ IsolateManager(this._adapter) {
+ final vmPath = Platform.resolvedExecutable;
+ sdkRoot = path.dirname(path.dirname(vmPath));
+ }
/// A list of all current active isolates.
///
@@ -110,7 +118,7 @@
await Future.wait(_threadsByThreadId.values.map(
// debuggable libraries is the only thing currently affected by these
// changable options.
- (isolate) => _sendLibraryDebuggables(isolate.isolate),
+ (thread) => _sendLibraryDebuggables(thread),
));
}
@@ -175,7 +183,7 @@
final registrationCompleter =
_isolateRegistrations.putIfAbsent(isolate.id!, () => Completer<void>());
- final info = _threadsByIsolateId.putIfAbsent(
+ final thread = _threadsByIsolateId.putIfAbsent(
isolate.id!,
() {
// The first time we see an isolate, start tracking it.
@@ -191,13 +199,13 @@
// If it's just become runnable (IsolateRunnable), configure the isolate
// by sending breakpoints etc.
- if (eventKind == vm.EventKind.kIsolateRunnable && !info.runnable) {
- info.runnable = true;
- await _configureIsolate(isolate);
+ if (eventKind == vm.EventKind.kIsolateRunnable && !thread.runnable) {
+ thread.runnable = true;
+ await _configureIsolate(thread);
registrationCompleter.complete();
}
- return info;
+ return thread;
}
/// Calls reloadSources for all isolates.
@@ -272,7 +280,7 @@
// Send the breakpoints to all existing threads.
await Future.wait(_threadsByThreadId.values
- .map((isolate) => _sendBreakpoints(isolate.isolate, uri: uri)));
+ .map((thread) => _sendBreakpoints(thread, uri: uri)));
}
/// Records exception pause mode as one of 'None', 'Unhandled' or 'All'. All
@@ -282,7 +290,7 @@
// Send to all existing threads.
await Future.wait(_threadsByThreadId.values.map(
- (isolate) => _sendExceptionPauseMode(isolate.isolate),
+ (thread) => _sendExceptionPauseMode(thread),
));
}
@@ -320,12 +328,15 @@
/// Configures a new isolate, setting it's exception-pause mode, which
/// libraries are debuggable, and sending all breakpoints.
- Future<void> _configureIsolate(vm.IsolateRef isolate) async {
+ Future<void> _configureIsolate(ThreadInfo thread) async {
+ // Libraries must be set as debuggable _before_ sending breakpoints, or
+ // they may fail for SDK sources.
await Future.wait([
- _sendLibraryDebuggables(isolate),
- _sendExceptionPauseMode(isolate),
- _sendBreakpoints(isolate),
+ _sendLibraryDebuggables(thread),
+ _sendExceptionPauseMode(thread),
], eagerError: true);
+
+ await _sendBreakpoints(thread);
}
/// Evaluates an expression, returning the result if it is a [vm.InstanceRef]
@@ -414,7 +425,7 @@
// For PausePostRequest we need to re-send all breakpoints; this happens
// after a hot restart.
if (eventKind == vm.EventKind.kPausePostRequest) {
- await _configureIsolate(isolate);
+ await _configureIsolate(thread);
if (resumeIfStarting) {
await resumeThread(thread.threadId);
}
@@ -493,6 +504,26 @@
}
}
+ /// Attempts to resolve [uris] to file:/// URIs via the VM Service.
+ ///
+ /// This method calls the VM service directly. Most requests to resolve URIs
+ /// should go through [ThreadInfo]'s resolveXxx methods which perform caching
+ /// of results.
+ Future<List<Uri?>?> _lookupResolvedPackageUris<T extends vm.Response>(
+ vm.IsolateRef isolate,
+ List<Uri> uris,
+ ) async {
+ final isolateId = isolate.id!;
+ final uriStrings = uris.map((uri) => uri.toString()).toList();
+ final res = await _adapter.vmService
+ ?.lookupResolvedPackageUris(isolateId, uriStrings);
+
+ return res?.uris
+ ?.cast<String?>()
+ .map((uri) => uri != null ? Uri.parse(uri) : null)
+ .toList();
+ }
+
/// Interpolates and prints messages for any log points.
///
/// Log Points are breakpoints with string messages attached. When the VM hits
@@ -546,13 +577,13 @@
/// when breakpoints are modified for a single file in the editor). Otherwise
/// breakpoints for all previously set URIs will be sent (used for
/// newly-created isolates).
- Future<void> _sendBreakpoints(vm.IsolateRef isolate, {String? uri}) async {
+ Future<void> _sendBreakpoints(ThreadInfo thread, {String? uri}) async {
final service = _adapter.vmService;
if (!debug || service == null) {
return;
}
- final isolateId = isolate.id!;
+ final isolateId = thread.isolate.id!;
// If we were passed a single URI, we should send breakpoints only for that
// (this means the request came from the client), otherwise we should send
@@ -572,8 +603,14 @@
final newBreakpoints = _clientBreakpointsByUri[uri] ?? const [];
await Future.forEach<SourceBreakpoint>(newBreakpoints, (bp) async {
try {
+ // Some file URIs (like SDK sources) need to be converted to
+ // appropriate internal URIs to be able to set breakpoints.
+ final vmUri = await thread.resolvePathToUri(
+ Uri.parse(uri).toFilePath(),
+ );
+
final vmBp = await service.addBreakpointWithScriptUri(
- isolateId, uri, bp.line,
+ isolateId, vmUri.toString(), bp.line,
column: bp.column);
existingBreakpointsForIsolateAndUri.add(vmBp);
_clientBreakpointsByVmId[vmBp.id!] = bp;
@@ -588,37 +625,47 @@
}
/// Sets the exception pause mode for an individual isolate.
- Future<void> _sendExceptionPauseMode(vm.IsolateRef isolate) async {
+ Future<void> _sendExceptionPauseMode(ThreadInfo thread) async {
final service = _adapter.vmService;
if (!debug || service == null) {
return;
}
await service.setIsolatePauseMode(
- isolate.id!,
+ thread.isolate.id!,
exceptionPauseMode: _exceptionPauseMode,
);
}
/// Calls setLibraryDebuggable for all libraries in the given isolate based
/// on the debug settings.
- Future<void> _sendLibraryDebuggables(vm.IsolateRef isolateRef) async {
+ Future<void> _sendLibraryDebuggables(ThreadInfo thread) async {
final service = _adapter.vmService;
if (!debug || service == null) {
return;
}
- final isolateId = isolateRef.id!;
+ final isolateId = thread.isolate.id!;
final isolate = await service.getIsolate(isolateId);
final libraries = isolate.libraries;
if (libraries == null) {
return;
}
+
+ // Pre-resolve all URIs in batch so the call below does not trigger
+ // many requests to the server.
+ final allUris = libraries
+ .map((library) => library.uri)
+ .whereNotNull()
+ .map(Uri.parse)
+ .toList();
+ await thread.resolveUrisToPackageLibPathsBatch(allUris);
+
await Future.wait(libraries.map((library) async {
final libraryUri = library.uri;
final isDebuggable = libraryUri != null
- ? _adapter.libaryIsDebuggable(Uri.parse(libraryUri))
+ ? await _adapter.libraryIsDebuggable(thread, Uri.parse(libraryUri))
: false;
await service.setLibraryDebuggable(isolateId, library.id!, isDebuggable);
}));
@@ -673,14 +720,23 @@
/// breakpoint or exception that occur early on.
bool hasBeenStarted = false;
- // The most recent pauseEvent for this isolate.
+ /// The most recent pauseEvent for this isolate.
vm.Event? pauseEvent;
- // A cache of requests (Futures) to fetch scripts, so that multiple requests
- // that require scripts (for example looking up locations for stack frames from
- // tokenPos) can share the same response.
+ /// A cache of requests (Futures) to fetch scripts, so that multiple requests
+ /// that require scripts (for example looking up locations for stack frames from
+ /// tokenPos) can share the same response.
final _scripts = <String, Future<vm.Script>>{};
+ /// A cache of requests (Futures) to resolve URIs to their local file paths.
+ ///
+ /// Used so that multiple requests that require them (for example looking up
+ /// locations for stack frames from tokenPos) can share the same response.
+ ///
+ /// Keys are URIs in string form.
+ /// Values are file paths (not file URIs!).
+ final _resolvedPaths = <String, Future<String?>>{};
+
/// Whether this isolate has an in-flight resume request that has not yet
/// been responded to.
var hasPendingResume = false;
@@ -699,9 +755,207 @@
return _scripts.putIfAbsent(script.id!, () => getObject<vm.Script>(script));
}
+ /// Resolves a source file path into a URI for the VM.
+ ///
+ /// sdk-path/lib/core/print.dart -> dart:core/print.dart
+ ///
+ /// This is required so that when the user sets a breakpoint in an SDK source
+ /// (which they may have nagivated to via the Analysis Server) we generate a
+ /// vaid URI that the VM would create a breakpoint for.
+ Future<Uri?> resolvePathToUri(String filePath) async {
+ // We don't currently need to call lookupPackageUris because the VM can
+ // handle incoming file:/// URIs for packages, and also the org-dartlang-sdk
+ // URIs directly for SDK sources (we do not need to convert to 'dart:'),
+ // however this method is Future-returning in case this changes in future
+ // and we need to include a call to lookupPackageUris here.
+ return _convertPathToOrgDartlangSdk(filePath) ?? Uri.file(filePath);
+ }
+
+ /// Batch resolves source URIs from the VM to a file path for the package lib
+ /// folder.
+ ///
+ /// This method is more performant than repeatedly calling
+ /// [resolveUrisToPackageLibPath] because it resolves multiple URIs in a
+ /// single request to the VM.
+ ///
+ /// Results are cached and shared with [resolveUrisToPackageLibPath] (and
+ /// [resolveUriToPath]) so it's reasonable to call this method up-front and
+ /// then use [resolveUrisToPackageLibPath] (and [resolveUriToPath]) to read
+ /// the results later.
+ Future<List<String?>> resolveUrisToPackageLibPathsBatch(
+ List<Uri> uris,
+ ) async {
+ final results = await resolveUrisToPathsBatch(uris);
+ return results
+ .mapIndexed((i, filePath) => _trimPathToLibFolder(filePath, uris[i]))
+ .toList();
+ }
+
+ /// Batch resolves source URIs from the VM to a file path.
+ ///
+ /// This method is more performant than repeatedly calling [resolveUriToPath]
+ /// because it resolves multiple URIs in a single request to the VM.
+ ///
+ /// Results are cached and shared with [resolveUriToPath] so it's reasonable
+ /// to call this method up-front and then use [resolveUriToPath] to read
+ /// the results later.
+ Future<List<String?>> resolveUrisToPathsBatch(List<Uri> uris) async {
+ // First find the set of URIs we don't already have results for.
+ final requiredUris = uris
+ .where((uri) => !uri.isScheme('file'))
+ .where((uri) => !_resolvedPaths.containsKey(uri.toString()))
+ .toSet() // Take only distinct values.
+ .toList();
+
+ if (requiredUris.isNotEmpty) {
+ // Populate completers for each URI before we start the request so that
+ // concurrent calls to this method will not start their own requests.
+ final completers = Map<String, Completer<String?>>.fromEntries(
+ requiredUris.map((uri) => MapEntry('$uri', Completer<String?>())),
+ );
+ completers.forEach(
+ (uri, completer) => _resolvedPaths[uri] = completer.future,
+ );
+ final results =
+ await _manager._lookupResolvedPackageUris(isolate, requiredUris);
+ if (results == null) {
+ // If no result, all of the results are null.
+ completers.forEach((uri, completer) => completer.complete(null));
+ } else {
+ // Otherwise, complete each one by index with the corresponding value.
+ results.map(_convertUriToFilePath).forEachIndexed((i, result) {
+ final uri = requiredUris[i].toString();
+ completers[uri]!.complete(result);
+ });
+ }
+ }
+
+ // Finally, assemble a list of the values by using the cached futures and
+ // the original list. Any non-file URI is guaranteed to be in [_resolvedPaths]
+ // because they were either filtered out of [requiredUris] because they were
+ // already there, or we then populated completers for them above.
+ final futures = uris.map((uri) async {
+ return uri.isScheme('file')
+ ? uri.toFilePath()
+ : await _resolvedPaths[uri.toString()]!;
+ });
+ return Future.wait(futures);
+ }
+
+ /// Resolves a source URI to a file path for the lib folder of its package.
+ ///
+ /// package:foo/a/b/c/d.dart -> /code/packages/foo/lib
+ ///
+ /// This method is an optimisation over calling [resolveUriToPath] where only
+ /// the package root is required (for example when determining whether a
+ /// package is within the users workspace). This method allows results to be
+ /// cached per-package to avoid hitting the VM Service for each individual
+ /// library within a package.
+ Future<String?> resolveUriToPackageLibPath(Uri uri) async {
+ final result = await resolveUrisToPackageLibPathsBatch([uri]);
+ return result.first;
+ }
+
+ /// Resolves a source URI from the VM to a file path.
+ ///
+ /// dart:core/print.dart -> sdk-path/lib/core/print.dart
+ ///
+ /// This is required so that when the user stops (or navigates via a stack
+ /// frame) we open the same file on their local disk. If we downloaded the
+ /// source from the VM, they would end up seeing two copies of files (and they
+ /// would each have their own breakpoints) which can be confusing.
+ Future<String?> resolveUriToPath(Uri uri) async {
+ final result = await resolveUrisToPathsBatch([uri]);
+ return result.first;
+ }
+
/// Stores some basic data indexed by an integer for use in "reference" fields
/// that are round-tripped to the client.
int storeData(Object data) => _manager.storeData(this, data);
+
+ /// Converts a URI in the form org-dartlang-sdk:///sdk/lib/collection/hash_set.dart
+ /// to a local file path based on the current SDK.
+ String? _convertOrgDartlangSdkToPath(Uri uri) {
+ // org-dartlang-sdk URIs can be in multiple forms:
+ //
+ // - org-dartlang-sdk:///sdk/lib/collection/hash_set.dart
+ // - org-dartlang-sdk:///runtime/lib/convert_patch.dart
+ //
+ // We currently only handle the sdk folder, as we don't know which runtime
+ // is being used (this code is shared) and do not want to map to the wrong
+ // sources.
+ if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'sdk') {
+ // TODO(dantup): Do we need to worry about this content not matching
+ // up with what's local (eg. for Flutter the VM running the app is
+ // on another device to the VM running this DA).
+ final sdkRoot = _manager.sdkRoot;
+ return path.joinAll([sdkRoot, ...uri.pathSegments.skip(1)]);
+ }
+
+ return null;
+ }
+
+ /// Converts a file path inside the current SDK root into a URI in the form
+ /// org-dartlang-sdk:///sdk/lib/collection/hash_set.dart.
+ Uri? _convertPathToOrgDartlangSdk(String input) {
+ final sdkRoot = _manager.sdkRoot;
+ if (path.isWithin(sdkRoot, input)) {
+ final relative = path.relative(input, from: sdkRoot);
+ return Uri(
+ scheme: 'org-dartlang-sdk',
+ host: '',
+ pathSegments: ['sdk', ...path.split(relative)],
+ );
+ }
+
+ return null;
+ }
+
+ /// Converts a URI to a file path.
+ ///
+ /// Supports file:// URIs and org-dartlang-sdk:// URIs.
+ String? _convertUriToFilePath(Uri? input) {
+ if (input == null) {
+ return null;
+ } else if (input.isScheme('file')) {
+ return input.toFilePath();
+ } else if (input.isScheme('org-dartlang-sdk')) {
+ return _convertOrgDartlangSdkToPath(input);
+ } else {
+ return null;
+ }
+ }
+
+ /// Helper to remove a libraries path from the a file path so it points at the
+ /// lib folder.
+ ///
+ /// [uri] should be the equivalent package: URI and is used to know how many
+ /// segments to remove from the file path to get to the lib folder.
+ String? _trimPathToLibFolder(String? filePath, Uri uri) {
+ if (filePath == null) {
+ return null;
+ }
+
+ final fileUri = Uri.file(filePath);
+
+ // Track how many segments from the path are from the lib folder to the
+ // libary that will need to be removed later.
+ final libraryPathSegments = uri.pathSegments.length - 1;
+
+ // It should never be the case that the returned value doesn't have at
+ // least as many segments as the path of the URI.
+ assert(fileUri.pathSegments.length > libraryPathSegments);
+ if (fileUri.pathSegments.length <= libraryPathSegments) {
+ return filePath;
+ }
+
+ // Strip off the correct number of segments to the resulting path points
+ // to the root of the package:/ URI.
+ final keepSegments = fileUri.pathSegments.length - libraryPathSegments;
+ return fileUri
+ .replace(pathSegments: fileUri.pathSegments.sublist(0, keepSegments))
+ .toFilePath();
+ }
}
class _StoredData {
diff --git a/pkg/dds/lib/src/dap/protocol_converter.dart b/pkg/dds/lib/src/dap/protocol_converter.dart
index a3e836e..0bedd50 100644
--- a/pkg/dds/lib/src/dap/protocol_converter.dart
+++ b/pkg/dds/lib/src/dap/protocol_converter.dart
@@ -6,7 +6,6 @@
import 'dart:io';
import 'package:collection/collection.dart';
-import 'package:package_config/package_config_types.dart';
import 'package:path/path.dart' as path;
import 'package:vm_service/vm_service.dart' as vm;
@@ -26,11 +25,6 @@
/// the debug session.
final DartDebugAdapter _adapter;
- /// Temporary PackageConfig used for resolving package: URIs.
- /// TODO(dantup): Replace this implementation with one that calls the VM
- /// Service once https://github.com/dart-lang/sdk/issues/45530 is done.
- PackageConfig packageConfig = PackageConfig.empty;
-
ProtocolConverter(this._adapter);
/// Converts an absolute path to one relative to the cwd used to launch the
@@ -388,13 +382,15 @@
final tokenPos = location.tokenPos;
final scriptRefUri = scriptRef?.uri;
final uri = scriptRefUri != null ? Uri.parse(scriptRefUri) : null;
+ final uriIsDart = uri?.isScheme('dart') ?? false;
final uriIsPackage = uri?.isScheme('package') ?? false;
- final sourcePath = uri != null ? await convertVmUriToSourcePath(uri) : null;
+ final sourcePath = uri != null ? await thread.resolveUriToPath(uri) : null;
var canShowSource = sourcePath != null && File(sourcePath).existsSync();
// Download the source if from a "dart:" uri.
int? sourceReference;
- if (uri != null &&
+ if (!canShowSource &&
+ uri != null &&
(uri.isScheme('dart') || uri.isScheme('org-dartlang-app')) &&
scriptRef != null) {
sourceReference = thread.storeData(scriptRef);
@@ -416,17 +412,18 @@
// SDK and debugSdkLibraries=false) then we should also mark it as
// deemphasized so that the editor can jump up the stack to the first frame
// of debuggable code.
- final isDebuggable = uri != null && _adapter.libaryIsDebuggable(uri);
+ final isDebuggable =
+ uri != null && await _adapter.libraryIsDebuggable(thread, uri);
final presentationHint = isDebuggable ? null : 'deemphasize';
final origin = uri != null && _adapter.isSdkLibrary(uri)
? 'from the SDK'
- : uri != null && _adapter.isExternalPackageLibrary(uri)
+ : uri != null && await _adapter.isExternalPackageLibrary(thread, uri)
? 'from external packages'
: null;
final source = canShowSource
? dap.Source(
- name: uriIsPackage
+ name: uriIsPackage || uriIsDart
? uri!.toString()
: sourcePath != null
? convertToRelativePath(sourcePath)
@@ -455,22 +452,6 @@
);
}
- /// Converts the source URI from the VM to a file path.
- ///
- /// This is required so that when the user stops (or navigates via a stack
- /// frame) we open the same file on their local disk. If we downloaded the
- /// source from the VM, they would end up seeing two copies of files (and they
- /// would each have their own breakpoints) which can be confusing.
- Future<String?> convertVmUriToSourcePath(Uri uri) async {
- if (uri.isScheme('file')) {
- return uri.toFilePath();
- } else if (uri.isScheme('package')) {
- return resolvePackageUri(uri);
- } else {
- return null;
- }
- }
-
/// Whether [kind] is a simple kind, and does not need to be mapped to a variable.
bool isSimpleKind(String? kind) {
return kind == 'String' ||
@@ -482,19 +463,6 @@
kind == 'Closure';
}
- /// Resolves a `package: URI` to the real underlying source path.
- ///
- /// Returns `null` if no mapping was possible, for example if the package is
- /// not in the package mapping file.
- String? resolvePackageUri(Uri uri) {
- // TODO(dantup): Replace this implementation with one that calls the VM
- // Service once https://github.com/dart-lang/sdk/issues/45530 is done.
- // This implementation makes assumptions about the package file being used
- // that might not be correct (for example if the user uses the --packages
- // flag).
- return packageConfig.resolve(uri)?.toFilePath();
- }
-
/// Invokes the toString() method on a [vm.InstanceRef] and converts the
/// response to a user-friendly display string.
///
diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml
index e323748..ef5124d 100644
--- a/pkg/dds/pubspec.yaml
+++ b/pkg/dds/pubspec.yaml
@@ -16,7 +16,6 @@
devtools_shared: ^2.3.0
json_rpc_2: ^3.0.0
meta: ^1.1.8
- package_config: ^2.0.0
path: ^1.8.0
pedantic: ^1.7.0
shelf: ^1.0.0
diff --git a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
index 8aee55c..03475cb 100644
--- a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
+++ b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
@@ -2,6 +2,9 @@
// 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:path/path.dart' as path;
import 'package:test/test.dart';
import 'test_client.dart';
@@ -24,6 +27,18 @@
await client.hitBreakpoint(testFile, breakpointLine);
});
+ test('stops at a line breakpoint in the SDK set via local sources',
+ () async {
+ final client = dap.client;
+ final testFile = dap.createTestFile(simpleBreakpointProgram);
+
+ // Add the breakpoint to the first line inside the SDK's print function.
+ final sdkFile = File(path.join(sdkRoot, 'lib', 'core', 'print.dart'));
+ final breakpointLine = lineWith(sdkFile, 'print(Object? object) {') + 1;
+
+ await client.hitBreakpoint(sdkFile, breakpointLine, entryFile: testFile);
+ });
+
test('stops at a line breakpoint and can be resumed', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
diff --git a/pkg/dds/test/dap/integration/debug_test.dart b/pkg/dds/test/dap/integration/debug_test.dart
index d6df8e6..6655b58 100644
--- a/pkg/dds/test/dap/integration/debug_test.dart
+++ b/pkg/dds/test/dap/integration/debug_test.dart
@@ -6,6 +6,7 @@
import 'dart:io';
import 'package:dds/src/dap/protocol_generated.dart';
+import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'test_client.dart';
@@ -159,6 +160,46 @@
// Source code should contain the implementation/signature of print().
final source = await client.getValidSource(topFrame.source!);
expect(source.content, contains('void print(Object? object) {'));
+ // Skipped because this test is not currently valid as source for print
+ // is mapped to local sources.
+ }, skip: true);
+
+ test('can map SDK source code to a local path', () async {
+ final client = dap.client;
+ final testFile = dap.createTestFile(simpleBreakpointProgram);
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+
+ // Hit the initial breakpoint.
+ final stop = await dap.client.hitBreakpoint(
+ testFile,
+ breakpointLine,
+ launch: () => client.launch(
+ testFile.path,
+ debugSdkLibraries: true,
+ ),
+ );
+
+ // Step in to go into print.
+ final responses = await Future.wait([
+ client.expectStop('step', sourceName: 'dart:core/print.dart'),
+ client.stepIn(stop.threadId!),
+ ], eagerError: true);
+ final stopResponse = responses.first as StoppedEventBody;
+
+ // Fetch the top stack frame (which should be inside print).
+ final stack = await client.getValidStack(
+ stopResponse.threadId!,
+ startFrame: 0,
+ numFrames: 1,
+ );
+ final topFrame = stack.stackFrames.first;
+
+ // SDK sources that have been mapped have no sourceReference but a path.
+ expect(
+ topFrame.source!.path,
+ equals(path.join(sdkRoot, 'lib', 'core', 'print.dart')),
+ );
+ expect(topFrame.source!.sourceReference, isNull);
});
test('can shutdown during startup', () async {
diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart
index 55f21c0..d960bec 100644
--- a/pkg/dds/test/dap/integration/test_client.dart
+++ b/pkg/dds/test/dap/integration/test_client.dart
@@ -488,11 +488,13 @@
Future<StoppedEventBody> hitBreakpoint(
File file,
int line, {
+ File? entryFile,
String? condition,
String? cwd,
List<String>? args,
Future<Response> Function()? launch,
}) async {
+ entryFile ??= file;
final stop = expectStop('breakpoint', file: file, line: line);
await Future.wait([
@@ -503,7 +505,7 @@
breakpoints: [SourceBreakpoint(line: line, condition: condition)],
),
),
- launch?.call() ?? this.launch(file.path, cwd: cwd, args: args),
+ launch?.call() ?? this.launch(entryFile.path, cwd: cwd, args: args),
], eagerError: true);
return stop;
diff --git a/pkg/dds/test/dap/integration/test_support.dart b/pkg/dds/test/dap/integration/test_support.dart
index 9201798..d4c2a8b 100644
--- a/pkg/dds/test/dap/integration/test_support.dart
+++ b/pkg/dds/test/dap/integration/test_support.dart
@@ -43,6 +43,9 @@
/// by the VM when not using --write-service-info.
final vmServiceBannerPattern = RegExp(r'Observatory listening on ([^\s]+)\s');
+/// The root of the SDK containing the current running VM.
+final sdkRoot = path.dirname(path.dirname(Platform.resolvedExecutable));
+
/// Expects the lines in [actual] to match the relevant matcher in [expected],
/// ignoring differences in line endings and trailing whitespace.
void expectLines(String actual, List<Object> expected) {
diff --git a/runtime/vm/dart.cc b/runtime/vm/dart.cc
index 7425dd6..e18e2a3 100644
--- a/runtime/vm/dart.cc
+++ b/runtime/vm/dart.cc
@@ -7,6 +7,7 @@
#include "vm/dart.h"
+#include "platform/thread_sanitizer.h"
#include "vm/app_snapshot.h"
#include "vm/code_observers.h"
#include "vm/compiler/runtime_offsets_extracted.h"
@@ -49,6 +50,13 @@
#include "vm/virtual_memory.h"
#include "vm/zone.h"
+#if defined(USING_THREAD_SANITIZER)
+// TODO(https://github.com/dart-lang/sdk/issues/46699): Remove.
+// TODO(https://bugs.fuchsia.dev/p/fuchsia/issues/detail?id=83298): Remove.
+#include "unicode/uchar.h"
+#include "unicode/uniset.h"
+#endif
+
namespace dart {
DECLARE_FLAG(bool, print_class_table);
@@ -264,6 +272,13 @@
Dart_CodeObserver* observer,
Dart_PostTaskCallback post_task,
void* post_task_data) {
+#if defined(USING_THREAD_SANITIZER)
+ // Trigger lazy initialization in ICU to avoid spurious TSAN warning.
+ // TODO(https://github.com/dart-lang/sdk/issues/46699): Remove.
+ // TODO(https://bugs.fuchsia.dev/p/fuchsia/issues/detail?id=83298): Remove.
+ u_getPropertyValueEnum(UCHAR_SCRIPT, "foo");
+#endif
+
CheckOffsets();
if (!Flags::Initialized()) {
diff --git a/tools/VERSION b/tools/VERSION
index e453e6c..536a2a3 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 16
PATCH 0
-PRERELEASE 63
+PRERELEASE 64
PRERELEASE_PATCH 0
\ No newline at end of file