Attach command: add Bazel filesystem support (#21082)

diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 2812b4c..66ea962 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -37,12 +37,17 @@
 class AttachCommand extends FlutterCommand {
   AttachCommand({bool verboseHelp = false, this.hotRunnerFactory}) {
     addBuildModeFlags(defaultToRelease: false);
+    usesTargetOption();
+    usesFilesystemOptions(hide: !verboseHelp);
     argParser
       ..addOption(
         'debug-port',
         help: 'Local port where the observatory is listening.',
-      )
-      ..addFlag(
+      )..addOption(
+        'project-root',
+        hide: !verboseHelp,
+        help: 'Normally used only in run target',
+      )..addFlag(
         'preview-dart-2',
         defaultsTo: true,
         hide: !verboseHelp,
@@ -53,7 +58,6 @@
           help: 'Handle machine structured JSON command input and provide output\n'
                 'and progress in machine friendly format.',
       );
-    usesTargetOption();
     hotRunnerFactory ??= new HotRunnerFactory();
   }
 
@@ -117,8 +121,14 @@
       observatoryUri = Uri.parse('http://$ipv4Loopback:$localPort/');
     }
     try {
-      final FlutterDevice flutterDevice = new FlutterDevice(device,
-          trackWidgetCreation: false, previewDart2: argResults['preview-dart-2']);
+      final FlutterDevice flutterDevice = new FlutterDevice(
+        device,
+        trackWidgetCreation: false,
+        previewDart2: argResults['preview-dart-2'],
+        dillOutputPath: argResults['output-dill'],
+        fileSystemRoots: argResults['filesystem-root'],
+        fileSystemScheme: argResults['filesystem-scheme'],
+      );
       flutterDevice.observatoryUris = <Uri>[ observatoryUri ];
       final HotRunner hotRunner = hotRunnerFactory.build(
         <FlutterDevice>[flutterDevice],
@@ -126,6 +136,8 @@
         debuggingOptions: new DebuggingOptions.enabled(getBuildInfo()),
         packagesFilePath: globalResults['packages'],
         usesTerminalUI: daemon == null,
+        projectRootPath: argResults['project-root'],
+        dillOutputPath: argResults['output-dill'],
       );
 
       if (daemon != null) {
@@ -178,4 +190,4 @@
     stayResident: stayResident,
     ipv6: ipv6,
   );
-}
\ No newline at end of file
+}
diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart
index 24c162e..5f34c98 100644
--- a/packages/flutter_tools/lib/src/commands/build_bundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_bundle.dart
@@ -13,6 +13,7 @@
 class BuildBundleCommand extends BuildSubCommand {
   BuildBundleCommand({bool verboseHelp = false}) {
     usesTargetOption();
+    usesFilesystemOptions(hide: !verboseHelp);
     addBuildModeFlags();
     argParser
       ..addFlag('precompiled', negatable: false)
@@ -58,18 +59,7 @@
       ..addFlag('report-licensed-packages',
         help: 'Whether to report the names of all the packages that are included '
               'in the application\'s LICENSE file.',
-        defaultsTo: false)
-      ..addMultiOption('filesystem-root',
-        hide: !verboseHelp,
-        help: 'Specify the path, that is used as root in a virtual file system\n'
-            'for compilation. Input file name should be specified as Uri in\n'
-            'filesystem-scheme scheme. Use only in Dart 2 mode.\n'
-            'Requires --output-dill option to be explicitly specified.\n')
-      ..addOption('filesystem-scheme',
-        defaultsTo: 'org-dartlang-root',
-        hide: !verboseHelp,
-        help: 'Specify the scheme that is used for virtual file system used in\n'
-            'compilation. See more details on filesystem-root option.\n');
+        defaultsTo: false);
     usesPubOption();
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 359fd54..2fbeeab 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -80,6 +80,7 @@
 
   RunCommand({ bool verboseHelp = false }) : super(verboseHelp: verboseHelp) {
     requiresPubspecYaml();
+    usesFilesystemOptions(hide: !verboseHelp);
 
     argParser
       ..addFlag('start-paused',
@@ -171,23 +172,8 @@
               'results out to "refresh_benchmark.json", and exit. This flag is\n'
               'intended for use in generating automated flutter benchmarks.',
       )
-      ..addOption('output-dill',
-        hide: !verboseHelp,
-        help: 'Specify the path to frontend server output kernel file.',
-      )
       ..addOption(FlutterOptions.kExtraFrontEndOptions, hide: true)
-      ..addOption(FlutterOptions.kExtraGenSnapshotOptions, hide: true)
-      ..addMultiOption('filesystem-root',
-        hide: !verboseHelp,
-        help: 'Specify the path, that is used as root in a virtual file system\n'
-            'for compilation. Input file name should be specified as Uri in\n'
-            'filesystem-scheme scheme. Use only in Dart 2 mode.\n'
-            'Requires --output-dill option to be explicitly specified.\n')
-      ..addOption('filesystem-scheme',
-        defaultsTo: 'org-dartlang-root',
-        hide: !verboseHelp,
-        help: 'Specify the scheme that is used for virtual file system used in\n'
-            'compilation. See more details on filesystem-root option.\n');
+      ..addOption(FlutterOptions.kExtraGenSnapshotOptions, hide: true);
   }
 
   List<Device> devices;
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 255756d..e9d901e 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -122,6 +122,31 @@
     _usesPubOption = true;
   }
 
+  /// Adds flags for using a specific filesystem root and scheme.
+  ///
+  /// [hide] indicates whether or not to hide these options when the user asks
+  /// for help.
+  void usesFilesystemOptions({@required bool hide}) {
+    argParser
+      ..addOption('output-dill',
+        hide: hide,
+        help: 'Specify the path to frontend server output kernel file.',
+      )
+      ..addMultiOption(FlutterOptions.kFileSystemRoot,
+        hide: hide,
+        help: 'Specify the path, that is used as root in a virtual file system\n'
+            'for compilation. Input file name should be specified as Uri in\n'
+            'filesystem-scheme scheme. Use only in Dart 2 mode.\n'
+            'Requires --output-dill option to be explicitly specified.\n',
+      )
+      ..addOption(FlutterOptions.kFileSystemScheme,
+        defaultsTo: 'org-dartlang-root',
+        hide: hide,
+        help: 'Specify the scheme that is used for virtual file system used in\n'
+            'compilation. See more details on filesystem-root option.\n',
+      );
+  }
+
   void usesBuildNumberOption() {
     argParser.addOption('build-number',
         help: 'An integer used as an internal version number.\n'
diff --git a/packages/flutter_tools/test/commands/attach_test.dart b/packages/flutter_tools/test/commands/attach_test.dart
index 475b2d0..8326f2b 100644
--- a/packages/flutter_tools/test/commands/attach_test.dart
+++ b/packages/flutter_tools/test/commands/attach_test.dart
@@ -11,6 +11,7 @@
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/attach.dart';
 import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/resident_runner.dart';
 import 'package:flutter_tools/src/run_hot.dart';
 import 'package:mockito/mockito.dart';
 
@@ -25,48 +26,132 @@
           .posix,
     );
 
-    setUpAll(() {
+    setUp(() {
       Cache.disableLocking();
       testFileSystem.directory('lib').createSync();
       testFileSystem.file('lib/main.dart').createSync();
     });
 
-    testUsingContext('finds observatory port and forwards', () async {
+    group('with one device and no specified target file', () {
       const int devicePort = 499;
       const int hostPort = 42;
-      final MockDeviceLogReader mockLogReader = new MockDeviceLogReader();
-      final MockPortForwarder portForwarder = new MockPortForwarder();
-      final MockAndroidDevice device = new MockAndroidDevice();
-      when(device.getLogReader()).thenAnswer((_) {
-        // Now that the reader is used, start writing messages to it.
-        Timer.run(() {
-          mockLogReader.addLine('Foo');
-          mockLogReader.addLine(
-              'Observatory listening on http://127.0.0.1:$devicePort');
+
+      MockDeviceLogReader mockLogReader;
+      MockPortForwarder portForwarder;
+      MockAndroidDevice device;
+
+      setUp(() {
+        mockLogReader = new MockDeviceLogReader();
+        portForwarder = new MockPortForwarder();
+        device = new MockAndroidDevice();
+        when(device.getLogReader()).thenAnswer((_) {
+          // Now that the reader is used, start writing messages to it.
+          Timer.run(() {
+            mockLogReader.addLine('Foo');
+            mockLogReader.addLine(
+                'Observatory listening on http://127.0.0.1:$devicePort');
+          });
+
+          return mockLogReader;
         });
+        when(device.portForwarder).thenReturn(portForwarder);
+        when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
+            .thenAnswer((_) async => hostPort);
+        when(portForwarder.forwardedPorts).thenReturn(
+            <ForwardedPort>[new ForwardedPort(hostPort, devicePort)]);
+        when(portForwarder.unforward(any)).thenAnswer((_) async => null);
 
-        return mockLogReader;
+        // We cannot add the device to a device manager because that is
+        // only enabled by the context of each testUsingContext call.
+        //
+        // Instead each test will add the device to the device manager
+        // on its own.
       });
-      when(device.portForwarder).thenReturn(portForwarder);
-      when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
-          .thenAnswer((_) async => hostPort);
-      when(portForwarder.forwardedPorts).thenReturn(
-          <ForwardedPort>[new ForwardedPort(hostPort, devicePort)]);
-      when(portForwarder.unforward(any)).thenAnswer((_) async => null);
-      testDeviceManager.addDevice(device);
 
-      final AttachCommand command = new AttachCommand();
+      tearDown(() {
+        mockLogReader.dispose();
+      });
 
-      await createTestCommandRunner(command).run(<String>['attach']);
+      testUsingContext('finds observatory port and forwards', () async {
+        testDeviceManager.addDevice(device);
 
-      verify(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
-          .called(1);
+        final AttachCommand command = new AttachCommand();
 
-      mockLogReader.dispose();
-    }, overrides: <Type, Generator>{
-      FileSystem: () => testFileSystem,
-    },
-    );
+        await createTestCommandRunner(command).run(<String>['attach']);
+
+        verify(
+          portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')),
+        ).called(1);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+      });
+
+      testUsingContext('accepts filesystem parameters', () async {
+        testDeviceManager.addDevice(device);
+
+        const String filesystemScheme = 'foo';
+        const String filesystemRoot = '/build-output/';
+        const String projectRoot = '/build-output/project-root';
+        const String outputDill = '/tmp/output.dill';
+
+        final MockHotRunnerFactory mockHotRunnerFactory = new MockHotRunnerFactory();
+        when(
+          mockHotRunnerFactory.build(
+            any,
+            target: anyNamed('target'),
+            projectRootPath: anyNamed('projectRootPath'),
+            dillOutputPath: anyNamed('dillOutputPath'),
+            debuggingOptions: anyNamed('debuggingOptions'),
+            packagesFilePath: anyNamed('packagesFilePath'),
+            usesTerminalUI: anyNamed('usesTerminalUI'),
+          ),
+        )..thenReturn(new MockHotRunner());
+
+        final AttachCommand command = new AttachCommand(
+          hotRunnerFactory: mockHotRunnerFactory,
+        );
+        await createTestCommandRunner(command).run(<String>[
+          'attach',
+          '--filesystem-scheme',
+          filesystemScheme,
+          '--filesystem-root',
+          filesystemRoot,
+          '--project-root',
+          projectRoot,
+          '--output-dill',
+          outputDill,
+          '-v',
+        ]);
+
+        // Validate the attach call built a mock runner with the right
+        // project root and output dill.
+        final VerificationResult verificationResult = verify(
+          mockHotRunnerFactory.build(
+            captureAny,
+            target: anyNamed('target'),
+            projectRootPath: projectRoot,
+            dillOutputPath: outputDill,
+            debuggingOptions: anyNamed('debuggingOptions'),
+            packagesFilePath: anyNamed('packagesFilePath'),
+            usesTerminalUI: anyNamed('usesTerminalUI'),
+          ),
+        )..called(1);
+
+        final List<FlutterDevice> flutterDevices = verificationResult.captured.first;
+        expect(flutterDevices, hasLength(1));
+
+        // Validate that the attach call built a flutter device with the right
+        // output dill, filesystem scheme, and filesystem root.
+        final FlutterDevice flutterDevice = flutterDevices.first;
+
+        expect(flutterDevice.dillOutputPath, outputDill);
+        expect(flutterDevice.fileSystemScheme, filesystemScheme);
+        expect(flutterDevice.fileSystemRoots, const <String>[filesystemRoot]);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+      });
+    });
+
 
     testUsingContext('selects specified target', () async {
       const int devicePort = 499;
@@ -102,6 +187,9 @@
       final File foo = fs.file('lib/foo.dart')
         ..createSync();
 
+      // Delete the main.dart file to be sure that attach works without it.
+      fs.file('lib/main.dart').deleteSync();
+
       final AttachCommand command = new AttachCommand(
           hotRunnerFactory: mockHotRunnerFactory);
       await createTestCommandRunner(command).run(