Skip to content

Commit 4944950

Browse files
Check whether FLUTTER_ROOT and FLUTTER_ROOT/bin are writable. (flutter#34291)
1 parent d9983e1 commit 4944950

File tree

4 files changed

+94
-10
lines changed

4 files changed

+94
-10
lines changed

packages/flutter_tools/lib/src/cache.dart

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,49 @@ class Cache {
9595
final Directory _rootOverride;
9696
final List<CachedArtifact> _artifacts = <CachedArtifact>[];
9797

98+
// Check whether there is a writable bit in the usr permissions.
99+
static bool _hasUserWritePermission(FileStat stat) {
100+
// First grab the set of permissions for the usr group.
101+
final int permissions = ((stat.mode & 0xFFF) >> 6) & 0x7;
102+
// These values represent all of the octal permission bits that have
103+
// readable and writable permission, though technically if we're missing
104+
// readable we probably didn't make it this far.
105+
return permissions == 6
106+
|| permissions == 7;
107+
}
108+
109+
// Unfortunately the memory file system by default specifies a mode of `0`
110+
// and is used by the majority of our tests. Default to false and only set
111+
// to true when we know it is safe.
112+
static bool checkPermissions = false;
113+
98114
// Initialized by FlutterCommandRunner on startup.
99-
static String flutterRoot;
115+
static String get flutterRoot => _flutterRoot;
116+
static String _flutterRoot;
117+
static set flutterRoot(String value) {
118+
if (value == null) {
119+
_flutterRoot = null;
120+
return;
121+
}
122+
if (checkPermissions) {
123+
// Verify that we have writable permission in the flutter root. If not,
124+
// we're liable to crash in unintuitive ways. This can happen if the user
125+
// is using a homebrew or other unofficial channel, or otherwise installs
126+
// Flutter into directory without permissions.
127+
final FileStat binStat = fs.statSync(fs.path.join(value, 'bin'));
128+
final FileStat rootStat = fs.statSync(value);
129+
if (!_hasUserWritePermission(binStat) || !_hasUserWritePermission(rootStat)) {
130+
throwToolExit(
131+
'Warning: Flutter is missing permissions to write files '
132+
'in its installation directory - "$value". '
133+
'Please install Flutter from an official channel in a directory '
134+
'where you have write permissions and that does not require '
135+
'administrative or root access. For more information see '
136+
'https://flutter.dev/docs/get-started/install');
137+
}
138+
}
139+
_flutterRoot = value;
140+
}
100141

101142
// Whether to cache artifacts for all platforms. Defaults to only caching
102143
// artifacts for the current platform.

packages/flutter_tools/lib/src/runner/flutter_command_runner.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,12 @@ class FlutterCommandRunner extends CommandRunner<void> {
343343
// We must set Cache.flutterRoot early because other features use it (e.g.
344344
// enginePath's initializer uses it).
345345
final String flutterRoot = topLevelResults['flutter-root'] ?? defaultFlutterRoot;
346+
bool checkPermissions = true;
347+
assert(() {
348+
checkPermissions = false;
349+
return true;
350+
}());
351+
Cache.checkPermissions = checkPermissions;
346352
Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot));
347353

348354
// Set up the tooling configuration.
@@ -477,10 +483,6 @@ class FlutterCommandRunner extends CommandRunner<void> {
477483
return EngineBuildPaths(targetEngine: engineBuildPath, hostEngine: engineHostBuildPath);
478484
}
479485

480-
static void initFlutterRoot() {
481-
Cache.flutterRoot ??= defaultFlutterRoot;
482-
}
483-
484486
/// Get the root directories of the repo - the directories containing Dart packages.
485487
List<String> getRepoRoots() {
486488
final String root = fs.path.absolute(Cache.flutterRoot);

packages/flutter_tools/test/cache_test.dart

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'dart:async';
66

77
import 'package:file/file.dart';
88
import 'package:file/memory.dart';
9+
import 'package:flutter_tools/src/base/common.dart';
10+
import 'package:flutter_tools/src/base/file_system.dart';
911
import 'package:mockito/mockito.dart';
1012
import 'package:platform/platform.dart';
1113

@@ -40,7 +42,7 @@ void main() {
4042
await Cache.lock();
4143
Cache.checkLockAcquired();
4244
}, overrides: <Type, Generator>{
43-
FileSystem: () => MockFileSystem(),
45+
FileSystem: () => FakeFileSystem(),
4446
});
4547

4648
testUsingContext('should not throw when FLUTTER_ALREADY_LOCKED is set', () async {
@@ -139,12 +141,50 @@ void main() {
139141
expect(flattenNameSubdirs(Uri.parse('http://docs.flutter.io/foo/bar')), 'docs.flutter.io/foo/bar');
140142
expect(flattenNameSubdirs(Uri.parse('https://www.flutter.dev')), 'www.flutter.dev');
141143
}, overrides: <Type, Generator>{
142-
FileSystem: () => MockFileSystem(),
144+
FileSystem: () => FakeFileSystem(),
145+
});
146+
147+
148+
group('Permissions test', () {
149+
MockFileSystem mockFileSystem;
150+
MockFileStat mockFileStat;
151+
152+
setUp(() {
153+
mockFileSystem = MockFileSystem();
154+
mockFileStat = MockFileStat();
155+
when(mockFileSystem.path).thenReturn(fs.path);
156+
Cache.checkPermissions = true;
157+
});
158+
159+
tearDown(() {
160+
Cache.checkPermissions = false;
161+
});
162+
163+
testUsingContext('Throws error if missing usr write permissions in flutterRoot', () {
164+
when(mockFileSystem.statSync(any)).thenReturn(mockFileStat);
165+
when(mockFileStat.mode).thenReturn(344);
166+
167+
expect(() => Cache.flutterRoot = '', throwsA(isInstanceOf<ToolExit>()));
168+
}, overrides: <Type, Generator>{
169+
FileSystem: () => mockFileSystem,
170+
}, initializeFlutterRoot: false);
171+
172+
testUsingContext('Doesnt error if we have usr write permissions in flutterRoot', () {
173+
when(mockFileSystem.statSync(any)).thenReturn(mockFileStat);
174+
when(mockFileStat.mode).thenReturn(493); // 0755 in decimal.
175+
176+
Cache.flutterRoot = '';
177+
}, overrides: <Type, Generator>{
178+
FileSystem: () => mockFileSystem,
179+
}, initializeFlutterRoot: false);
180+
143181
});
144182
}
145183

146-
class MockFileSystem extends ForwardingFileSystem {
147-
MockFileSystem() : super(MemoryFileSystem());
184+
class MockFileSystem extends Mock implements FileSystem {}
185+
class MockFileStat extends Mock implements FileStat {}
186+
class FakeFileSystem extends ForwardingFileSystem {
187+
FakeFileSystem() : super(MemoryFileSystem());
148188

149189
@override
150190
File file(dynamic path) {

packages/flutter_tools/test/commands/analyze_continuously_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66

77
import 'package:flutter_tools/src/base/file_system.dart';
88
import 'package:flutter_tools/src/base/os.dart';
9+
import 'package:flutter_tools/src/cache.dart';
910
import 'package:flutter_tools/src/dart/analysis.dart';
1011
import 'package:flutter_tools/src/dart/pub.dart';
1112
import 'package:flutter_tools/src/dart/sdk.dart';
@@ -19,7 +20,7 @@ void main() {
1920
Directory tempDir;
2021

2122
setUp(() {
22-
FlutterCommandRunner.initFlutterRoot();
23+
Cache.flutterRoot = FlutterCommandRunner.defaultFlutterRoot;
2324
tempDir = fs.systemTempDirectory.createTempSync('flutter_analysis_test.');
2425
});
2526

0 commit comments

Comments
 (0)