[Tool] Fix null check operator crash in currentPackageConfig (#188454)
The Flutter tool crashes with a `Null check operator used on a null
value` in sandboxed or package-managed environments (like NixOS or
stand-alone AOT runs) where the isolate does not provide package
resolution metadata.
This change implements:
1. Removing the strict null assertion (`!`) on
`Isolate.packageConfigSync`.
2. Adding a robust fallback: if null, it attempts to resolve
`.dart_tool/package_config.json` relative to the current working
directory and then the executing script.
3. If all fail, it throws a clean `ToolExit` prompting the user to run
`flutter pub get` instead of a raw runtime crash.
Fixes https://github.com/flutter/flutter/issues/188449
diff --git a/packages/flutter_tools/lib/src/dart/package_map.dart b/packages/flutter_tools/lib/src/dart/package_map.dart
index b71f97c..b7ac029 100644
--- a/packages/flutter_tools/lib/src/dart/package_map.dart
+++ b/packages/flutter_tools/lib/src/dart/package_map.dart
@@ -5,15 +5,51 @@
import 'dart:isolate';
import 'dart:typed_data';
+import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
+import '../globals.dart' as globals;
+
+/// Whether to ignore [Isolate.packageConfigSync] and force the fallback
+/// path in [currentPackageConfig].
+@visibleForTesting
+bool debugIgnorePackageConfigSync = false;
+
+const String _fileScheme = 'file';
/// Loads the package configuration of the current isolate.
Future<PackageConfig> currentPackageConfig() async {
- return loadPackageConfigUri(Isolate.packageConfigSync!);
+ final Uri? packageConfigUri = debugIgnorePackageConfigSync ? null : Isolate.packageConfigSync;
+ if (packageConfigUri != null) {
+ return loadPackageConfigUri(packageConfigUri);
+ }
+
+ final FileSystem fileSystem = globals.fs;
+ final Directory cwd = fileSystem.currentDirectory;
+ File? packageConfigFile = findPackageConfigFile(cwd);
+
+ if (packageConfigFile == null) {
+ final Uri scriptUri = globals.platform.script;
+ if (scriptUri.scheme == _fileScheme) {
+ final File scriptFile = fileSystem.file(scriptUri);
+ packageConfigFile = findPackageConfigFile(scriptFile.parent);
+ }
+ }
+
+ if (packageConfigFile == null) {
+ throwToolExit(
+ 'Failed to resolve package configuration.\n'
+ 'Isolate.packageConfigSync was null, and no .dart_tool/package_config.json '
+ 'could be found in the current working directory (${cwd.path}) or '
+ 'relative to the script (${globals.platform.script}).\n'
+ 'Did you run "flutter pub get"?',
+ );
+ }
+
+ return loadPackageConfigWithLogging(packageConfigFile, logger: globals.logger);
}
/// Locates the `.dart_tool/package_config.json` relevant to [dir].
diff --git a/packages/flutter_tools/test/general.shard/dart/package_map_test.dart b/packages/flutter_tools/test/general.shard/dart/package_map_test.dart
index f28cf38..3f695c9 100644
--- a/packages/flutter_tools/test/general.shard/dart/package_map_test.dart
+++ b/packages/flutter_tools/test/general.shard/dart/package_map_test.dart
@@ -4,9 +4,12 @@
import 'package:file/file.dart';
import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/dart/package_map.dart';
+import 'package:package_config/package_config.dart';
import '../../src/common.dart';
+import '../../src/context.dart';
void main() {
group('findPackageConfigFile', () {
@@ -55,4 +58,81 @@
expect(findPackageConfigFile(fileSystem.directory('.')), isNotNull);
});
});
+
+ group('currentPackageConfig', () {
+ late FileSystem fileSystem;
+
+ setUp(() {
+ fileSystem = MemoryFileSystem.test();
+ });
+
+ tearDown(() {
+ debugIgnorePackageConfigSync = false;
+ });
+
+ testUsingContext(
+ 'should load from CWD if Isolate.packageConfigSync is null',
+ () async {
+ debugIgnorePackageConfigSync = true;
+
+ // Create a valid package config in CWD
+ final File packageConfig = fileSystem.file('.dart_tool/package_config.json');
+ packageConfig.createSync(recursive: true);
+ packageConfig.writeAsStringSync('{"configVersion": 2, "packages": []}');
+
+ final PackageConfig config = await currentPackageConfig();
+ expect(config.version, 2);
+ },
+ overrides: <Type, Generator>{
+ FileSystem: () => fileSystem,
+ ProcessManager: () => FakeProcessManager.any(),
+ Platform: () => FakePlatform(script: Uri.parse('file:///ambient/bin/main.dart')),
+ },
+ );
+
+ testUsingContext(
+ 'should load from script directory if CWD has no package config',
+ () async {
+ debugIgnorePackageConfigSync = true;
+
+ // CWD = '/cwd_dir'
+ // Script = '/script_dir/bin/flutter_tools.dart'
+ // Package config = '/script_dir/.dart_tool/package_config.json'
+
+ final Directory cwd = fileSystem.directory('/cwd_dir')..createSync();
+ fileSystem.currentDirectory = cwd;
+
+ final File packageConfig = fileSystem.file('/script_dir/.dart_tool/package_config.json');
+ packageConfig.createSync(recursive: true);
+ packageConfig.writeAsStringSync('{"configVersion": 2, "packages": []}');
+
+ final PackageConfig config = await currentPackageConfig();
+ expect(config.version, 2);
+ },
+ overrides: <Type, Generator>{
+ FileSystem: () => fileSystem,
+ ProcessManager: () => FakeProcessManager.any(),
+ Platform: () =>
+ FakePlatform(script: Uri.parse('file:///script_dir/bin/flutter_tools.dart')),
+ },
+ );
+
+ testUsingContext(
+ 'should throw ToolExit if package config cannot be found',
+ () async {
+ debugIgnorePackageConfigSync = true;
+
+ expect(
+ () => currentPackageConfig(),
+ throwsToolExit(message: 'Failed to resolve package configuration'),
+ );
+ },
+ overrides: <Type, Generator>{
+ FileSystem: () => fileSystem,
+ ProcessManager: () => FakeProcessManager.any(),
+ Platform: () =>
+ FakePlatform(script: Uri.parse('file:///script_dir/bin/flutter_tools.dart')),
+ },
+ );
+ });
}