Check whether FLUTTER_ROOT and FLUTTER_ROOT/bin are writable. (#34291)
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index 52e1ca7..5b8b34e 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -95,8 +95,49 @@
final Directory _rootOverride;
final List<CachedArtifact> _artifacts = <CachedArtifact>[];
+ // Check whether there is a writable bit in the usr permissions.
+ static bool _hasUserWritePermission(FileStat stat) {
+ // First grab the set of permissions for the usr group.
+ final int permissions = ((stat.mode & 0xFFF) >> 6) & 0x7;
+ // These values represent all of the octal permission bits that have
+ // readable and writable permission, though technically if we're missing
+ // readable we probably didn't make it this far.
+ return permissions == 6
+ || permissions == 7;
+ }
+
+ // Unfortunately the memory file system by default specifies a mode of `0`
+ // and is used by the majority of our tests. Default to false and only set
+ // to true when we know it is safe.
+ static bool checkPermissions = false;
+
// Initialized by FlutterCommandRunner on startup.
- static String flutterRoot;
+ static String get flutterRoot => _flutterRoot;
+ static String _flutterRoot;
+ static set flutterRoot(String value) {
+ if (value == null) {
+ _flutterRoot = null;
+ return;
+ }
+ if (checkPermissions) {
+ // Verify that we have writable permission in the flutter root. If not,
+ // we're liable to crash in unintuitive ways. This can happen if the user
+ // is using a homebrew or other unofficial channel, or otherwise installs
+ // Flutter into directory without permissions.
+ final FileStat binStat = fs.statSync(fs.path.join(value, 'bin'));
+ final FileStat rootStat = fs.statSync(value);
+ if (!_hasUserWritePermission(binStat) || !_hasUserWritePermission(rootStat)) {
+ throwToolExit(
+ 'Warning: Flutter is missing permissions to write files '
+ 'in its installation directory - "$value". '
+ 'Please install Flutter from an official channel in a directory '
+ 'where you have write permissions and that does not require '
+ 'administrative or root access. For more information see '
+ 'https://flutter.dev/docs/get-started/install');
+ }
+ }
+ _flutterRoot = value;
+ }
// Whether to cache artifacts for all platforms. Defaults to only caching
// artifacts for the current platform.
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
index d332cc1..cf275b2 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -343,6 +343,12 @@
// We must set Cache.flutterRoot early because other features use it (e.g.
// enginePath's initializer uses it).
final String flutterRoot = topLevelResults['flutter-root'] ?? defaultFlutterRoot;
+ bool checkPermissions = true;
+ assert(() {
+ checkPermissions = false;
+ return true;
+ }());
+ Cache.checkPermissions = checkPermissions;
Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot));
// Set up the tooling configuration.
@@ -477,10 +483,6 @@
return EngineBuildPaths(targetEngine: engineBuildPath, hostEngine: engineHostBuildPath);
}
- static void initFlutterRoot() {
- Cache.flutterRoot ??= defaultFlutterRoot;
- }
-
/// Get the root directories of the repo - the directories containing Dart packages.
List<String> getRepoRoots() {
final String root = fs.path.absolute(Cache.flutterRoot);
diff --git a/packages/flutter_tools/test/cache_test.dart b/packages/flutter_tools/test/cache_test.dart
index a95964a..e3cdb85 100644
--- a/packages/flutter_tools/test/cache_test.dart
+++ b/packages/flutter_tools/test/cache_test.dart
@@ -6,6 +6,8 @@
import 'package:file/file.dart';
import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
@@ -40,7 +42,7 @@
await Cache.lock();
Cache.checkLockAcquired();
}, overrides: <Type, Generator>{
- FileSystem: () => MockFileSystem(),
+ FileSystem: () => FakeFileSystem(),
});
testUsingContext('should not throw when FLUTTER_ALREADY_LOCKED is set', () async {
@@ -139,12 +141,50 @@
expect(flattenNameSubdirs(Uri.parse('http://docs.flutter.io/foo/bar')), 'docs.flutter.io/foo/bar');
expect(flattenNameSubdirs(Uri.parse('https://www.flutter.dev')), 'www.flutter.dev');
}, overrides: <Type, Generator>{
- FileSystem: () => MockFileSystem(),
+ FileSystem: () => FakeFileSystem(),
+ });
+
+
+ group('Permissions test', () {
+ MockFileSystem mockFileSystem;
+ MockFileStat mockFileStat;
+
+ setUp(() {
+ mockFileSystem = MockFileSystem();
+ mockFileStat = MockFileStat();
+ when(mockFileSystem.path).thenReturn(fs.path);
+ Cache.checkPermissions = true;
+ });
+
+ tearDown(() {
+ Cache.checkPermissions = false;
+ });
+
+ testUsingContext('Throws error if missing usr write permissions in flutterRoot', () {
+ when(mockFileSystem.statSync(any)).thenReturn(mockFileStat);
+ when(mockFileStat.mode).thenReturn(344);
+
+ expect(() => Cache.flutterRoot = '', throwsA(isInstanceOf<ToolExit>()));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => mockFileSystem,
+ }, initializeFlutterRoot: false);
+
+ testUsingContext('Doesnt error if we have usr write permissions in flutterRoot', () {
+ when(mockFileSystem.statSync(any)).thenReturn(mockFileStat);
+ when(mockFileStat.mode).thenReturn(493); // 0755 in decimal.
+
+ Cache.flutterRoot = '';
+ }, overrides: <Type, Generator>{
+ FileSystem: () => mockFileSystem,
+ }, initializeFlutterRoot: false);
+
});
}
-class MockFileSystem extends ForwardingFileSystem {
- MockFileSystem() : super(MemoryFileSystem());
+class MockFileSystem extends Mock implements FileSystem {}
+class MockFileStat extends Mock implements FileStat {}
+class FakeFileSystem extends ForwardingFileSystem {
+ FakeFileSystem() : super(MemoryFileSystem());
@override
File file(dynamic path) {
diff --git a/packages/flutter_tools/test/commands/analyze_continuously_test.dart b/packages/flutter_tools/test/commands/analyze_continuously_test.dart
index 7fa979a..77e3158 100644
--- a/packages/flutter_tools/test/commands/analyze_continuously_test.dart
+++ b/packages/flutter_tools/test/commands/analyze_continuously_test.dart
@@ -6,6 +6,7 @@
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/dart/analysis.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/dart/sdk.dart';
@@ -19,7 +20,7 @@
Directory tempDir;
setUp(() {
- FlutterCommandRunner.initFlutterRoot();
+ Cache.flutterRoot = FlutterCommandRunner.defaultFlutterRoot;
tempDir = fs.systemTempDirectory.createTempSync('flutter_analysis_test.');
});