merge/audit-and-state-map #7
3 changed files with 102 additions and 2 deletions
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>.
commit
70262a8c2f
|
|
@ -55,6 +55,7 @@ class Analyzer {
|
||||||
explicit: sdkOverride,
|
explicit: sdkOverride,
|
||||||
flutterRootHint: project.flutterRoot,
|
flutterRootHint: project.flutterRoot,
|
||||||
);
|
);
|
||||||
|
requireReadableSdk(resolution);
|
||||||
final collection = AnalysisContextCollection(
|
final collection = AnalysisContextCollection(
|
||||||
includedPaths: [project.root],
|
includedPaths: [project.root],
|
||||||
resourceProvider: PhysicalResourceProvider.INSTANCE,
|
resourceProvider: PhysicalResourceProvider.INSTANCE,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,44 @@ String? _normalizedDirectory(String? path) {
|
||||||
return _isDirectory(path) ? p.normalize(p.absolute(path)) : null;
|
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.
|
/// Given a Flutter root, return its bundled Dart SDK if one is present.
|
||||||
String? dartSdkInFlutterRoot(String flutterRoot) {
|
String? dartSdkInFlutterRoot(String flutterRoot) {
|
||||||
final candidate = p.join(flutterRoot, 'bin', 'cache', 'dart-sdk');
|
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));
|
final runtimeSdk = sdkFromExecutable(Platform.resolvedExecutable);
|
||||||
if (_isDartSdk(runtimeSdk)) {
|
if (runtimeSdk != null) {
|
||||||
return SdkResolution(
|
return SdkResolution(
|
||||||
sdkPath: runtimeSdk,
|
sdkPath: runtimeSdk,
|
||||||
flutterRoot: null,
|
flutterRoot: null,
|
||||||
|
|
|
||||||
61
test/sdk_test.dart
Normal file
61
test/sdk_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue