merge/audit-and-state-map #7

Merged
trevon merged 15 commits from merge/audit-and-state-map into main 2026-09-25 00:07:04 +00:00
3 changed files with 102 additions and 2 deletions
Showing only changes of commit 70262a8c2f - Show all commits

fix(sdk): refuse an unreadable SDK clearly, and fix the runtime fallback

An SDK directory that exists but cannot be read passed discovery, and the
analyzer then died with a raw FileSystemException stack trace. This
happens for real under macOS privacy protection: a Flutter SDK under
~/Documents stats fine but every open fails, and every app whose
package_config.json names that flutterRoot crashed:

  Unhandled exception:
  FileSystemException(path=.../dart-sdk/lib/_internal/allowed_experiments.json; ...)

The chosen SDK is now probed before the analysis collection is built, and
an unreadable one is a PipeException naming the path, where it was
discovered from, and the --sdk remedy.

The last-resort fallback to the running Dart SDK took the executable's
directory (<sdk>/bin) as the SDK root, which never contains
lib/libraries.json, so it could never succeed. sdkFromExecutable now
maps <sdk>/bin/dart to <sdk>.
trvon 2026-09-22 13:36:10 -06:00
Signed by: trevon
GPG key ID: 38686EA797D7A8A7

View file

@ -55,6 +55,7 @@ class Analyzer {
explicit: sdkOverride,
flutterRootHint: project.flutterRoot,
);
requireReadableSdk(resolution);
final collection = AnalysisContextCollection(
includedPaths: [project.root],
resourceProvider: PhysicalResourceProvider.INSTANCE,

View file

@ -45,6 +45,44 @@ String? _normalizedDirectory(String? path) {
return _isDirectory(path) ? p.normalize(p.absolute(path)) : null;
}
/// The SDK root holding a `<sdk>/bin/dart` executable, or null when the
/// executable does not sit inside a Dart SDK.
///
/// The executable's own directory is `<sdk>/bin`, which never contains
/// `lib/libraries.json`; checking it meant this fallback could never succeed.
String? sdkFromExecutable(String executable) {
final root = p.dirname(p.dirname(p.normalize(executable)));
return _isDartSdk(root) ? root : null;
}
/// Fails with an actionable error when the chosen SDK cannot be read.
///
/// Existence is not enough: macOS privacy protection (for example a Flutter SDK
/// under ~/Documents) lets `stat` succeed while every open fails, so discovery
/// accepted the SDK and the analyzer later died with a raw FileSystemException.
void requireReadableSdk(SdkResolution resolution) {
final sdk = resolution.sdkPath;
if (sdk == null) return;
for (final probe in [
p.join(sdk, 'version'),
p.join(sdk, 'lib', 'libraries.json'),
]) {
final file = File(probe);
if (!file.existsSync()) continue;
try {
file.openSync().closeSync();
} on FileSystemException catch (error) {
throw PipeException(
'the Dart SDK at $sdk (from ${resolution.source}) cannot be read: '
'${error.osError?.message ?? error.message}',
remedy:
'grant this process access to that directory, or pass '
'--sdk <path to a readable dart-sdk>',
);
}
}
}
/// Given a Flutter root, return its bundled Dart SDK if one is present.
String? dartSdkInFlutterRoot(String flutterRoot) {
final candidate = p.join(flutterRoot, 'bin', 'cache', 'dart-sdk');
@ -144,8 +182,8 @@ SdkResolution resolveSdk({String? explicit, String? flutterRootHint}) {
);
}
final runtimeSdk = p.dirname(p.normalize(Platform.resolvedExecutable));
if (_isDartSdk(runtimeSdk)) {
final runtimeSdk = sdkFromExecutable(Platform.resolvedExecutable);
if (runtimeSdk != null) {
return SdkResolution(
sdkPath: runtimeSdk,
flutterRoot: null,

61
test/sdk_test.dart Normal file
View file

@ -0,0 +1,61 @@
import 'dart:io';
import 'package:pipe/pipe.dart';
import 'package:pipe/src/sdk.dart';
import 'package:test/test.dart';
/// A directory laid out like a Dart SDK: `bin/dart`, `lib/libraries.json`,
/// `version`.
Directory _fakeSdk(String label) {
final sdk = Directory.systemTemp.createTempSync('pipe-$label');
File('${sdk.path}/version').writeAsStringSync('3.13.0\n');
File('${sdk.path}/lib/libraries.json')
..createSync(recursive: true)
..writeAsStringSync('{}');
File('${sdk.path}/bin/dart').createSync(recursive: true);
return sdk;
}
void main() {
test('an SDK that exists but cannot be read is a clear error', () {
// macOS privacy protection (a Flutter SDK under ~/Documents) lets stat
// succeed while every open fails, so discovery accepted the SDK and the
// analyzer later died with a raw FileSystemException stack trace.
final sdk = _fakeSdk('unreadable-sdk');
final version = File('${sdk.path}/version');
Process.runSync('chmod', ['000', version.path]);
try {
version.readAsStringSync();
markTestSkipped('chmod 000 does not restrict this process');
return;
} on FileSystemException {
// Expected: the restriction is real, so the check below is meaningful.
}
final resolution = resolveSdk(explicit: sdk.path);
expect(
() => requireReadableSdk(resolution),
throwsA(
isA<PipeException>()
.having((e) => e.message, 'message', contains(sdk.path))
.having((e) => e.remedy, 'remedy', contains('--sdk')),
),
);
});
test('a readable SDK passes the check', () {
final sdk = _fakeSdk('readable-sdk');
expect(
() => requireReadableSdk(resolveSdk(explicit: sdk.path)),
returnsNormally,
);
});
test('the running dart binary maps to its SDK root, not to bin/', () {
// dirname(<sdk>/bin/dart) is <sdk>/bin, which never contains
// lib/libraries.json, so this fallback could never succeed.
final sdk = _fakeSdk('runtime-sdk');
expect(sdkFromExecutable('${sdk.path}/bin/dart'), sdk.path);
expect(sdkFromExecutable('${sdk.path}/version'), isNull);
});
}