Unverified Commit fdb1fb18 authored by Lau Ching Jun's avatar Lau Ching Jun Committed by GitHub

Add MultiRootFileSystem to better support using --filesystem-root. (#82991)

parent bfff43cf
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' as io
show
Directory,
File,
FileStat,
FileSystemEntity,
FileSystemEntityType,
Link;
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p; // flutter_ignore: package_path_import
/// A [FileSystem] that wraps the [delegate] file system to create an overlay of
/// files from multiple [roots].
///
/// Regular paths or `file:` URIs are resolved directly in the underlying file
/// system, but URIs that use a special [scheme] are resolved by searching
/// under a set of given roots in order.
///
/// For example, consider the following inputs:
///
/// - scheme is `multi-root`
/// - the set of roots are `/a` and `/b`
/// - the underlying file system contains files:
/// /root_a/dir/only_a.dart
/// /root_a/dir/both.dart
/// /root_b/dir/only_b.dart
/// /root_b/dir/both.dart
/// /other/other.dart
///
/// Then:
///
/// - file:///other/other.dart is resolved as /other/other.dart
/// - multi-root:///dir/only_a.dart is resolved as /root_a/dir/only_a.dart
/// - multi-root:///dir/only_b.dart is resolved as /root_b/dir/only_b.dart
/// - multi-root:///dir/both.dart is resolved as /root_a/dir/only_a.dart
class MultiRootFileSystem extends ForwardingFileSystem {
MultiRootFileSystem({
required FileSystem delegate,
required String scheme,
required List<String> roots,
}) : assert(delegate != null),
assert(roots.isNotEmpty),
_scheme = scheme,
_roots = roots.map((String root) => delegate.path.normalize(root)).toList(),
super(delegate);
@visibleForTesting
FileSystem get fileSystem => delegate;
final String _scheme;
final List<String> _roots;
@override
File file(dynamic path) => MultiRootFile(
fileSystem: this,
delegate: delegate.file(_resolve(path)),
);
@override
Directory directory(dynamic path) => MultiRootDirectory(
fileSystem: this,
delegate: delegate.directory(_resolve(path)),
);
@override
Link link(dynamic path) => MultiRootLink(
fileSystem: this,
delegate: delegate.link(_resolve(path)),
);
@override
Future<io.FileStat> stat(String path) =>
delegate.stat(_resolve(path).toString());
@override
io.FileStat statSync(String path) =>
delegate.statSync(_resolve(path).toString());
@override
Future<bool> identical(String path1, String path2) =>
delegate.identical(_resolve(path1).toString(), _resolve(path2).toString());
@override
bool identicalSync(String path1, String path2) =>
delegate.identicalSync(_resolve(path1).toString(), _resolve(path2).toString());
@override
Future<io.FileSystemEntityType> type(String path, {bool followLinks = true}) =>
delegate.type(_resolve(path).toString(), followLinks: followLinks);
@override
io.FileSystemEntityType typeSync(String path, {bool followLinks = true}) =>
delegate.typeSync(_resolve(path).toString(), followLinks: followLinks);
// Caching the path context here and clearing when the currentDirectory setter
// is updated works since the flutter tool restricts usage of dart:io directly
// via the forbidden import tests. Otherwise, the path context's current
// working directory might get out of sync, leading to unexpected results from
// methods like `path.relative`.
@override
p.Context get path => _cachedPath ??= delegate.path;
p.Context? _cachedPath;
@override
set currentDirectory(dynamic path) {
_cachedPath = null;
delegate.currentDirectory = path;
}
/// If the path is a multiroot uri, resolve to the actual path of the
/// underlying file system. Otherwise, return as is.
dynamic _resolve(dynamic path) {
Uri uri;
if (path == null) {
return null;
} else if (path is String) {
uri = Uri.parse(path);
} else if (path is Uri) {
uri = path;
} else if (path is FileSystemEntity) {
uri = path.uri;
} else {
throw ArgumentError('Invalid type for "path": ${path?.runtimeType}');
}
if (!uri.hasScheme || uri.scheme != _scheme) {
return path;
}
String? firstRootPath;
final String relativePath = delegate.path.joinAll(uri.pathSegments);
for (final String root in _roots) {
final String pathWithRoot = delegate.path.join(root, relativePath);
if (delegate.typeSync(pathWithRoot, followLinks: false) !=
FileSystemEntityType.notFound) {
return pathWithRoot;
}
firstRootPath ??= pathWithRoot;
}
// If not found, construct the path with the first root.
return firstRootPath!;
}
Uri _toMultiRootUri(Uri uri) {
if (uri.scheme != 'file') {
return uri;
}
final p.Context pathContext = delegate.path;
final bool isWindows = pathContext.style == p.Style.windows;
final String path = uri.toFilePath(windows: isWindows);
for (final String root in _roots) {
if (path.startsWith('$root${pathContext.separator}')) {
String pathWithoutRoot = path.substring(root.length + 1);
if (isWindows) {
// Convert the path from Windows style
pathWithoutRoot = p.url.joinAll(pathContext.split(pathWithoutRoot));
}
return Uri.parse('$_scheme:///$pathWithoutRoot');
}
}
return uri;
}
@override
String toString() =>
'MultiRootFileSystem(scheme = $_scheme, roots = $_roots, delegate = $delegate)';
}
abstract class MultiRootFileSystemEntity<T extends FileSystemEntity,
D extends io.FileSystemEntity> extends ForwardingFileSystemEntity<T, D> {
MultiRootFileSystemEntity({
required this.fileSystem,
required this.delegate,
});
@override
final D delegate;
@override
final MultiRootFileSystem fileSystem;
@override
File wrapFile(io.File delegate) => MultiRootFile(
fileSystem: fileSystem,
delegate: delegate,
);
@override
Directory wrapDirectory(io.Directory delegate) => MultiRootDirectory(
fileSystem: fileSystem,
delegate: delegate,
);
@override
Link wrapLink(io.Link delegate) => MultiRootLink(
fileSystem: fileSystem,
delegate: delegate,
);
@override
Uri get uri => fileSystem._toMultiRootUri(delegate.uri);
}
class MultiRootFile extends MultiRootFileSystemEntity<File, io.File>
with ForwardingFile {
MultiRootFile({
required MultiRootFileSystem fileSystem,
required io.File delegate,
}) : super(
fileSystem: fileSystem,
delegate: delegate,
);
@override
String toString() =>
'MultiRootFile(fileSystem = $fileSystem, delegate = $delegate)';
}
class MultiRootDirectory
extends MultiRootFileSystemEntity<Directory, io.Directory>
with ForwardingDirectory<Directory> {
MultiRootDirectory({
required MultiRootFileSystem fileSystem,
required io.Directory delegate,
}) : super(
fileSystem: fileSystem,
delegate: delegate,
);
// For the childEntity methods, we first obtain an instance of the entity
// from the underlying file system, then invoke childEntity() on it, then
// wrap in the ErrorHandling version.
@override
Directory childDirectory(String basename) =>
fileSystem.directory(fileSystem.path.join(delegate.path, basename));
@override
File childFile(String basename) =>
fileSystem.file(fileSystem.path.join(delegate.path, basename));
@override
Link childLink(String basename) =>
fileSystem.link(fileSystem.path.join(delegate.path, basename));
@override
String toString() =>
'MultiRootDirectory(fileSystem = $fileSystem, delegate = $delegate)';
}
class MultiRootLink extends MultiRootFileSystemEntity<Link, io.Link>
with ForwardingLink {
MultiRootLink({
required MultiRootFileSystem fileSystem,
required io.Link delegate,
}) : super(
fileSystem: fileSystem,
delegate: delegate,
);
@override
String toString() =>
'MultiRootLink(fileSystem = $fileSystem, delegate = $delegate)';
}
......@@ -608,8 +608,8 @@ class RunCommand extends RunCommandBase {
for (final Device device in devices)
await FlutterDevice.create(
device,
fileSystemRoots: stringsArg(FlutterOptions.kFileSystemRoot),
fileSystemScheme: stringArg(FlutterOptions.kFileSystemScheme),
fileSystemRoots: fileSystemRoots,
fileSystemScheme: fileSystemScheme,
experimentalFlags: expFlags,
target: targetFile,
buildInfo: buildInfo,
......
......@@ -36,14 +36,14 @@ void _renderTemplateToFile(String template, dynamic context, File file, Template
Plugin _pluginFromPackage(String name, Uri packageRoot, Set<String> appDependencies, {FileSystem fileSystem}) {
final FileSystem fs = fileSystem ?? globals.fs;
final String pubspecPath = fs.path.fromUri(packageRoot.resolve('pubspec.yaml'));
if (!fs.isFileSync(pubspecPath)) {
final File pubspecFile = fs.file(packageRoot.resolve('pubspec.yaml'));
if (!pubspecFile.existsSync()) {
return null;
}
dynamic pubspec;
try {
pubspec = loadYaml(fs.file(pubspecPath).readAsStringSync());
pubspec = loadYaml(pubspecFile.readAsStringSync());
} on YamlException catch (err) {
globals.printTrace('Failed to parse plugin manifest for $name: $err');
// Do nothing, potentially not a plugin.
......
......@@ -1070,7 +1070,7 @@ abstract class ResidentRunner extends ResidentHandlers {
String dillOutputPath,
this.machine = false,
ResidentDevtoolsHandlerFactory devtoolsHandler = createDefaultHandler,
}) : mainPath = globals.fs.path.absolute(target),
}) : mainPath = globals.fs.file(target).absolute.path,
packagesFilePath = debuggingOptions.buildInfo.packagesPath,
projectRootPath = projectRootPath ?? globals.fs.currentDirectory.path,
_dillOutputPath = dillOutputPath,
......
......@@ -1242,7 +1242,8 @@ class ProjectFileInvalidator {
for (final Uri uri in urisToScan) {
waitList.add(pool.withResource<void>(
() => _fileSystem
.stat(uri.toFilePath(windows: _platform.isWindows))
.file(uri)
.stat()
.then((FileStat stat) {
final DateTime updatedAt = stat.modified;
if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
......@@ -1254,17 +1255,16 @@ class ProjectFileInvalidator {
await Future.wait<void>(waitList);
} else {
for (final Uri uri in urisToScan) {
final DateTime updatedAt = _fileSystem.statSync(
uri.toFilePath(windows: _platform.isWindows)).modified;
final DateTime updatedAt = _fileSystem.file(uri).statSync().modified;
if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
invalidatedFiles.add(uri);
}
}
}
// We need to check the .packages file too since it is not used in compilation.
final Uri packageUri = _fileSystem.file(packagesPath).uri;
final DateTime updatedAt = _fileSystem.statSync(
packageUri.toFilePath(windows: _platform.isWindows)).modified;
final File packageFile = _fileSystem.file(packagesPath);
final Uri packageUri = packageFile.uri;
final DateTime updatedAt = packageFile.statSync().modified;
if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
invalidatedFiles.add(packageUri);
packageConfig = await _createPackageConfig(packagesPath);
......
......@@ -289,6 +289,22 @@ abstract class FlutterCommand extends Command<void> {
/// This can be overridden by some of its subclasses.
String get packagesPath => globalResults['packages'] as String;
/// The value of the `--filesystem-scheme` argument.
///
/// This can be overridden by some of its subclasses.
String get fileSystemScheme =>
argParser.options.containsKey(FlutterOptions.kFileSystemScheme)
? stringArg(FlutterOptions.kFileSystemScheme)
: null;
/// The values of the `--filesystem-root` argument.
///
/// This can be overridden by some of its subclasses.
List<String> get fileSystemRoots =>
argParser.options.containsKey(FlutterOptions.kFileSystemRoot)
? stringsArg(FlutterOptions.kFileSystemRoot)
: null;
void usesPubOption({bool hide = false}) {
argParser.addFlag('pub',
defaultsTo: true,
......@@ -1032,12 +1048,8 @@ abstract class FlutterCommand extends Command<void> {
extraGenSnapshotOptions: extraGenSnapshotOptions?.isNotEmpty ?? false
? extraGenSnapshotOptions
: null,
fileSystemRoots: argParser.options.containsKey(FlutterOptions.kFileSystemRoot)
? stringsArg(FlutterOptions.kFileSystemRoot)
: null,
fileSystemScheme: argParser.options.containsKey(FlutterOptions.kFileSystemScheme)
? stringArg(FlutterOptions.kFileSystemScheme)
: null,
fileSystemRoots: fileSystemRoots,
fileSystemScheme: fileSystemScheme,
buildNumber: buildNumber,
buildName: argParser.options.containsKey('build-name')
? stringArg('build-name')
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' as io;
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/multi_root_file_system.dart';
import '../../src/common.dart';
void setupFileSystem({
required MemoryFileSystem fs,
required List<String> directories,
required List<String> files,
}) {
for (final String directory in directories) {
fs.directory(directory).createSync(recursive: true);
}
for (final String file in files) {
fs.file(file).writeAsStringSync('Content: $file');
}
}
void main() {
group('Posix style', () {
runTest(FileSystemStyle.posix);
});
group('Windows style', () {
runTest(FileSystemStyle.windows);
});
}
void runTest(FileSystemStyle style) {
final String sep = style == FileSystemStyle.windows ? r'\' : '/';
final String root = style == FileSystemStyle.windows ? r'C:\' : '/';
final String rootUri = style == FileSystemStyle.windows ? 'C:/' : '';
late MultiRootFileSystem fs;
setUp(() {
final MemoryFileSystem memory = MemoryFileSystem(style: style);
setupFileSystem(
fs: memory,
directories: <String>[
'${root}foo${sep}subdir',
'${root}bar',
'${root}bar${sep}bar_subdir',
'${root}other${sep}directory',
],
files: <String>[
'${root}foo${sep}only_in_foo',
'${root}foo${sep}in_both',
'${root}foo${sep}subdir${sep}in_subdir',
'${root}bar${sep}only_in_bar',
'${root}bar${sep}in_both',
'${root}bar${sep}bar_subdir${sep}in_subdir',
'${root}other${sep}directory${sep}file',
],
);
fs = MultiRootFileSystem(
delegate: memory,
scheme: 'scheme',
roots: <String>[
'${root}foo$sep',
'${root}bar',
],
);
});
testWithoutContext('file inside root', () {
final File file = fs.file('${root}foo${sep}only_in_foo');
expect(file.readAsStringSync(), 'Content: ${root}foo${sep}only_in_foo');
expect(file.path, '${root}foo${sep}only_in_foo');
expect(file.uri, Uri.parse('scheme:///only_in_foo'));
});
testWithoutContext('file inside second root', () {
final File file = fs.file('${root}bar${sep}only_in_bar');
expect(file.readAsStringSync(), 'Content: ${root}bar${sep}only_in_bar');
expect(file.path, '${root}bar${sep}only_in_bar');
expect(file.uri, Uri.parse('scheme:///only_in_bar'));
});
testWithoutContext('file outside root', () {
final File file = fs.file('${root}other${sep}directory${sep}file');
expect(file.readAsStringSync(),
'Content: ${root}other${sep}directory${sep}file');
expect(file.path, '${root}other${sep}directory${sep}file');
expect(file.uri, Uri.parse('file:///${rootUri}other/directory/file'));
});
testWithoutContext('file with file system scheme', () {
final File file = fs.file('scheme:///only_in_foo');
expect(file.readAsStringSync(), 'Content: ${root}foo${sep}only_in_foo');
expect(file.path, '${root}foo${sep}only_in_foo');
expect(file.uri, Uri.parse('scheme:///only_in_foo'));
});
testWithoutContext('file with file system scheme URI', () {
final File file = fs.file(Uri.parse('scheme:///only_in_foo'));
expect(file.readAsStringSync(), 'Content: ${root}foo${sep}only_in_foo');
expect(file.path, '${root}foo${sep}only_in_foo');
expect(file.uri, Uri.parse('scheme:///only_in_foo'));
});
testWithoutContext('file in second root with file system scheme', () {
final File file = fs.file('scheme:///only_in_bar');
expect(file.readAsStringSync(), 'Content: ${root}bar${sep}only_in_bar');
expect(file.path, '${root}bar${sep}only_in_bar');
expect(file.uri, Uri.parse('scheme:///only_in_bar'));
});
testWithoutContext('file in second root with file system scheme URI', () {
final File file = fs.file(Uri.parse('scheme:///only_in_bar'));
expect(file.readAsStringSync(), 'Content: ${root}bar${sep}only_in_bar');
expect(file.path, '${root}bar${sep}only_in_bar');
expect(file.uri, Uri.parse('scheme:///only_in_bar'));
});
testWithoutContext('file in both roots', () {
final File file = fs.file(Uri.parse('scheme:///in_both'));
expect(file.readAsStringSync(), 'Content: ${root}foo${sep}in_both');
expect(file.path, '${root}foo${sep}in_both');
expect(file.uri, Uri.parse('scheme:///in_both'));
});
testWithoutContext('file with scheme in subdirectory', () {
final File file = fs.file(Uri.parse('scheme:///subdir/in_subdir'));
expect(file.readAsStringSync(),
'Content: ${root}foo${sep}subdir${sep}in_subdir');
expect(file.path, '${root}foo${sep}subdir${sep}in_subdir');
expect(file.uri, Uri.parse('scheme:///subdir/in_subdir'));
});
testWithoutContext('file in second root with scheme in subdirectory', () {
final File file = fs.file(Uri.parse('scheme:///bar_subdir/in_subdir'));
expect(file.readAsStringSync(),
'Content: ${root}bar${sep}bar_subdir${sep}in_subdir');
expect(file.path, '${root}bar${sep}bar_subdir${sep}in_subdir');
expect(file.uri, Uri.parse('scheme:///bar_subdir/in_subdir'));
});
testWithoutContext('non-existent file with scheme', () {
final File file = fs.file(Uri.parse('scheme:///not_exist'));
expect(file.uri, Uri.parse('scheme:///not_exist'));
expect(file.path, '${root}foo${sep}not_exist');
});
testWithoutContext('stat', () async {
expect((await fs.stat('${root}foo${sep}only_in_foo')).type, io.FileSystemEntityType.file);
expect((await fs.stat('scheme:///only_in_foo')).type, io.FileSystemEntityType.file);
expect(fs.statSync('${root}foo${sep}only_in_foo').type, io.FileSystemEntityType.file);
expect(fs.statSync('scheme:///only_in_foo').type, io.FileSystemEntityType.file);
});
testWithoutContext('type', () async {
expect(await fs.type('${root}foo${sep}only_in_foo'), io.FileSystemEntityType.file);
expect(await fs.type('scheme:///only_in_foo'), io.FileSystemEntityType.file);
expect(await fs.type('${root}foo${sep}subdir'), io.FileSystemEntityType.directory);
expect(await fs.type('scheme:///subdir'), io.FileSystemEntityType.directory);
expect(await fs.type('${root}foo${sep}not_found'), io.FileSystemEntityType.notFound);
expect(await fs.type('scheme:///not_found'), io.FileSystemEntityType.notFound);
expect(fs.typeSync('${root}foo${sep}only_in_foo'), io.FileSystemEntityType.file);
expect(fs.typeSync('scheme:///only_in_foo'), io.FileSystemEntityType.file);
expect(fs.typeSync('${root}foo${sep}subdir'), io.FileSystemEntityType.directory);
expect(fs.typeSync('scheme:///subdir'), io.FileSystemEntityType.directory);
expect(fs.typeSync('${root}foo${sep}not_found'), io.FileSystemEntityType.notFound);
expect(fs.typeSync('scheme:///not_found'), io.FileSystemEntityType.notFound);
});
testWithoutContext('identical', () async {
expect(await fs.identical('${root}foo${sep}in_both', '${root}foo${sep}in_both'), true);
expect(await fs.identical('${root}foo${sep}in_both', 'scheme:///in_both'), true);
expect(await fs.identical('${root}foo${sep}in_both', 'scheme:///in_both'), true);
expect(await fs.identical('${root}bar${sep}in_both', 'scheme:///in_both'), false);
expect(fs.identicalSync('${root}foo${sep}in_both', '${root}foo${sep}in_both'), true);
expect(fs.identicalSync('${root}foo${sep}in_both', 'scheme:///in_both'), true);
expect(fs.identicalSync('${root}foo${sep}in_both', 'scheme:///in_both'), true);
expect(fs.identicalSync('${root}bar${sep}in_both', 'scheme:///in_both'), false);
});
}
......@@ -509,6 +509,18 @@ void main() {
expect(buildInfo.packagesPath, 'foo');
});
testUsingContext('use fileSystemScheme to generate BuildInfo', () async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(fileSystemScheme: 'foo');
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
expect(buildInfo.fileSystemScheme, 'foo');
});
testUsingContext('use fileSystemRoots to generate BuildInfo', () async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(fileSystemRoots: <String>['foo', 'bar']);
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
expect(buildInfo.fileSystemRoots, <String>['foo', 'bar']);
});
testUsingContext('dds options', () async {
final FakeDdsCommand ddsCommand = FakeDdsCommand();
final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
......
......@@ -16,6 +16,8 @@ class DummyFlutterCommand extends FlutterCommand {
this.name = 'dummy',
this.commandFunction,
this.packagesPath,
this.fileSystemScheme,
this.fileSystemRoots,
});
final bool noUsagePath;
......@@ -40,4 +42,10 @@ class DummyFlutterCommand extends FlutterCommand {
@override
final String packagesPath;
@override
final String fileSystemScheme;
@override
final List<String> fileSystemRoots;
}
......@@ -66,6 +66,7 @@ void main() {
fileSystem.path.join(flutterTools, 'lib', 'src', 'base', 'io.dart'),
fileSystem.path.join(flutterTools, 'lib', 'src', 'base', 'platform.dart'),
fileSystem.path.join(flutterTools, 'lib', 'src', 'base', 'error_handling_io.dart'),
fileSystem.path.join(flutterTools, 'lib', 'src', 'base', 'multi_root_file_system.dart'),
];
bool _isNotAllowed(FileSystemEntity entity) => allowedPaths.every((String path) => path != entity.path);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment