Add support for NDK discovery and add --prefer-shared-library option (#12788)

* Add support for NDK discovery and add --prefer-shared-library option

We would like to be able to use native tools (e.g. simpleperf, gdb) with
precompiled flutter apps.  The native tools work much better with *.so
files instead of the custom formats the Dart VM uses by default.

The reason for using blobs / instruction snapshots is that we do not
want to force flutter users to install the Android NDK.

This CL adds a `--prefer-shared-library` flag to e.g. `flutter build
apk` which will use the NDK compiler (if available) to turn the
precompiled app assembly file to an `*.so` file.  If the NDK compiler is
not available it will default to the default behavior.

* Rebase, add test for NDK detection, augment flutter.gradle with @Input for flag

* Use InMemoryFileSystem for test

* Remove unused import

* Address some analyzer warnings
diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle
index 80eeff1..036da77 100644
--- a/packages/flutter_tools/gradle/flutter.gradle
+++ b/packages/flutter_tools/gradle/flutter.gradle
@@ -235,6 +235,10 @@
         if (project.hasProperty('extra-gen-snapshot-options')) {
             extraGenSnapshotOptionsValue = project.property('extra-gen-snapshot-options')
         }
+        Boolean preferSharedLibraryValue = false
+        if (project.hasProperty('prefer-shared-library')) {
+            preferSharedLibraryValue = project.property('prefer-shared-library')
+        }
 
         project.android.applicationVariants.all { variant ->
             String flutterBuildMode = buildModeFor(variant.buildType)
@@ -253,6 +257,7 @@
                 localEngineSrcPath this.localEngineSrcPath
                 targetPath target
                 previewDart2 previewDart2Value
+                preferSharedLibrary preferSharedLibraryValue
                 sourceDir project.file(project.flutter.source)
                 intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
             }
@@ -266,6 +271,7 @@
                 localEngineSrcPath this.localEngineSrcPath
                 targetPath target
                 previewDart2 previewDart2Value
+                preferSharedLibrary preferSharedLibraryValue
                 sourceDir project.file(project.flutter.source)
                 intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
                 extraFrontEndOptions extraFrontEndOptionsValue
@@ -298,6 +304,8 @@
     String targetPath
     @Optional @Input
     Boolean previewDart2
+    @Optional @Input
+    Boolean preferSharedLibrary
     File sourceDir
     File intermediateDir
     @Optional @Input
@@ -338,10 +346,13 @@
                     args "--preview-dart-2"
                 }
                 if (extraFrontEndOptions != null) {
-                  args "--extra-front-end-options", "${extraFrontEndOptions}"
+                    args "--extra-front-end-options", "${extraFrontEndOptions}"
                 }
                 if (extraGenSnapshotOptions != null) {
-                  args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
+                    args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
+                }
+                if (preferSharedLibrary) {
+                    args "--prefer-shared-library"
                 }
                 args "--${buildMode}"
             }
@@ -392,15 +403,19 @@
 
     CopySpec getAssets() {
         return project.copySpec {
-	     from "${intermediateDir}/app.flx"
-	     from "${intermediateDir}/snapshot_blob.bin"
+            from "${intermediateDir}/app.flx"
+            from "${intermediateDir}/snapshot_blob.bin"
             if (buildMode != 'debug') {
+              if (preferSharedLibrary) {
+                from "${intermediateDir}/app.so"
+              } else {
                 from "${intermediateDir}/vm_snapshot_data"
                 from "${intermediateDir}/vm_snapshot_instr"
                 from "${intermediateDir}/isolate_snapshot_data"
                 from "${intermediateDir}/isolate_snapshot_instr"
+              }
             }
-        }
+      }
     }
 
     FileCollection readDependencies(File dependenciesFile) {
diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart
index fb88234..a3946d5 100644
--- a/packages/flutter_tools/lib/src/android/android_sdk.dart
+++ b/packages/flutter_tools/lib/src/android/android_sdk.dart
@@ -57,63 +57,131 @@
 }
 
 class AndroidSdk {
-  AndroidSdk(this.directory) {
+  AndroidSdk(this.directory, [this.ndkDirectory, this.ndkCompiler,
+      this.ndkCompilerArgs]) {
     _init();
   }
 
+  /// The path to the Android SDK.
   final String directory;
 
+  /// The path to the NDK (can be `null`).
+  final String ndkDirectory;
+
+  /// The path to the NDK compiler (can be `null`).
+  final String ndkCompiler;
+
+  /// The mandatory arguments to the NDK compiler (can be `null`).
+  final List<String> ndkCompilerArgs;
+
   List<AndroidSdkVersion> _sdkVersions;
   AndroidSdkVersion _latestVersion;
 
   static AndroidSdk locateAndroidSdk() {
-    String androidHomeDir;
+    String findAndroidHomeDir() {
+      String androidHomeDir;
+      if (config.containsKey('android-sdk')) {
+        androidHomeDir = config.getValue('android-sdk');
+      } else if (platform.environment.containsKey(kAndroidHome)) {
+        androidHomeDir = platform.environment[kAndroidHome];
+      } else if (platform.isLinux) {
+        if (homeDirPath != null)
+          androidHomeDir = fs.path.join(homeDirPath, 'Android', 'Sdk');
+      } else if (platform.isMacOS) {
+        if (homeDirPath != null)
+          androidHomeDir = fs.path.join(homeDirPath, 'Library', 'Android', 'sdk');
+      } else if (platform.isWindows) {
+        if (homeDirPath != null)
+          androidHomeDir = fs.path.join(homeDirPath, 'AppData', 'Local', 'Android', 'sdk');
+      }
 
-    if (config.containsKey('android-sdk')) {
-      androidHomeDir = config.getValue('android-sdk');
-    } else if (platform.environment.containsKey(kAndroidHome)) {
-      androidHomeDir = platform.environment[kAndroidHome];
-    } else if (platform.isLinux) {
-      if (homeDirPath != null)
-        androidHomeDir = fs.path.join(homeDirPath, 'Android', 'Sdk');
-    } else if (platform.isMacOS) {
-      if (homeDirPath != null)
-        androidHomeDir = fs.path.join(homeDirPath, 'Library', 'Android', 'sdk');
-    } else if (platform.isWindows) {
-      if (homeDirPath != null)
-        androidHomeDir = fs.path.join(homeDirPath, 'AppData', 'Local', 'Android', 'sdk');
+      if (androidHomeDir != null) {
+        if (validSdkDirectory(androidHomeDir))
+          return androidHomeDir;
+        if (validSdkDirectory(fs.path.join(androidHomeDir, 'sdk')))
+          return fs.path.join(androidHomeDir, 'sdk');
+      }
+
+      // in build-tools/$version/aapt
+      final List<File> aaptBins = os.whichAll('aapt');
+      for (File aaptBin in aaptBins) {
+        // Make sure we're using the aapt from the SDK.
+        aaptBin = fs.file(aaptBin.resolveSymbolicLinksSync());
+        final String dir = aaptBin.parent.parent.parent.path;
+        if (validSdkDirectory(dir))
+          return dir;
+      }
+
+      // in platform-tools/adb
+      final List<File> adbBins = os.whichAll('adb');
+      for (File adbBin in adbBins) {
+        // Make sure we're using the adb from the SDK.
+        adbBin = fs.file(adbBin.resolveSymbolicLinksSync());
+        final String dir = adbBin.parent.parent.path;
+        if (validSdkDirectory(dir))
+          return dir;
+      }
+
+      return null;
     }
 
-    if (androidHomeDir != null) {
-      if (validSdkDirectory(androidHomeDir))
-        return new AndroidSdk(androidHomeDir);
-      if (validSdkDirectory(fs.path.join(androidHomeDir, 'sdk')))
-        return new AndroidSdk(fs.path.join(androidHomeDir, 'sdk'));
+    String findNdk(String androidHomeDir) {
+      final String ndkDirectory = fs.path.join(androidHomeDir, 'ndk-bundle');
+      if (fs.isDirectorySync(ndkDirectory)) {
+        return ndkDirectory;
+      }
+      return null;
     }
 
-    // in build-tools/$version/aapt
-    final List<File> aaptBins = os.whichAll('aapt');
-    for (File aaptBin in aaptBins) {
-      // Make sure we're using the aapt from the SDK.
-      aaptBin = fs.file(aaptBin.resolveSymbolicLinksSync());
-      final String dir = aaptBin.parent.parent.parent.path;
-      if (validSdkDirectory(dir))
-        return new AndroidSdk(dir);
+    String findNdkCompiler(String ndkDirectory) {
+      String directory;
+      if (platform.isLinux) {
+        directory = 'linux-x86_64';
+      } else if (platform.isMacOS) {
+        directory = 'darwin-x86_64';
+      }
+      if (directory != null) {
+        final String ndkCompiler = fs.path.join(ndkDirectory,
+            'toolchains', 'arm-linux-androideabi-4.9', 'prebuilt', directory,
+            'bin', 'arm-linux-androideabi-gcc');
+        if (fs.isFileSync(ndkCompiler)) {
+          return ndkCompiler;
+        }
+      }
+      return null;
     }
 
-    // in platform-tools/adb
-    final List<File> adbBins = os.whichAll('adb');
-    for (File adbBin in adbBins) {
-      // Make sure we're using the adb from the SDK.
-      adbBin = fs.file(adbBin.resolveSymbolicLinksSync());
-      final String dir = adbBin.parent.parent.path;
-      if (validSdkDirectory(dir))
-        return new AndroidSdk(dir);
+    List<String> computeNdkCompilerArgs(String ndkDirectory) {
+      final String armPlatform = fs.path.join(ndkDirectory, 'platforms',
+          'android-9', 'arch-arm');
+      if (fs.isDirectorySync(armPlatform)) {
+        return <String>['--sysroot', armPlatform];
+      }
+      return null;
     }
 
-    // No dice.
-    printTrace('Unable to locate an Android SDK.');
-    return null;
+    final String androidHomeDir = findAndroidHomeDir();
+    if (androidHomeDir == null) {
+      // No dice.
+      printTrace('Unable to locate an Android SDK.');
+      return null;
+    }
+
+    // Try to find the NDK compiler.  If we can't find it, it's also ok.
+    final String ndkDir = findNdk(androidHomeDir);
+    String ndkCompiler;
+    List<String> ndkCompilerArgs;
+    if (ndkDir != null) {
+      ndkCompiler = findNdkCompiler(ndkDir);
+      if (ndkCompiler != null) {
+        ndkCompilerArgs = computeNdkCompilerArgs(ndkDir);
+        if (ndkCompilerArgs == null) {
+          ndkCompiler = null;
+        }
+      }
+    }
+
+    return new AndroidSdk(androidHomeDir, ndkDir, ndkCompiler, ndkCompilerArgs);
   }
 
   static bool validSdkDirectory(String dir) {
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index 1fc76ae..99d1f6f 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -119,6 +119,14 @@
 
     messages.add(new ValidationMessage('Android SDK at ${androidSdk.directory}'));
 
+    messages.add(new ValidationMessage(androidSdk.ndkDirectory == null
+          ? 'Unable to locate Android NDK.\n'
+          : 'Android NDK at ${androidSdk.ndkDirectory}'));
+
+    messages.add(new ValidationMessage(androidSdk.ndkCompiler == null
+          ? 'Unable to locate compiler in Android NDK.\n'
+          : 'Compiler in Android NDK at ${androidSdk.ndkCompiler}'));
+
     String sdkVersionText;
     if (androidSdk.latestVersion != null) {
       sdkVersionText = 'Android SDK ${androidSdk.latestVersion.buildToolsVersionName}';
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index bb930af..b3b5b74 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import '../android/android_sdk.dart';
 import '../artifacts.dart';
 import '../base/common.dart';
 import '../base/file_system.dart';
@@ -289,12 +290,17 @@
   if (target != null) {
     command.add('-Ptarget=$target');
   }
-  if (buildInfo.previewDart2)
+  if (buildInfo.previewDart2) {
     command.add('-Ppreview-dart-2=true');
   if (buildInfo.extraFrontEndOptions != null)
     command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
   if (buildInfo.extraGenSnapshotOptions != null)
     command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}');
+  }
+  if (buildInfo.preferSharedLibrary && androidSdk.ndkCompiler != null) {
+    command.add('-Pprefer-shared-library=true');
+  }
+
   command.add(assembleTask);
   final int exitCode = await runCommandAndStreamOutput(
       command,
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 3454552..154a2ec 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -13,7 +13,8 @@
   const BuildInfo(this.mode, this.flavor,
       {this.previewDart2,
       this.extraFrontEndOptions,
-      this.extraGenSnapshotOptions});
+      this.extraGenSnapshotOptions,
+      this.preferSharedLibrary});
 
   final BuildMode mode;
   /// Represents a custom Android product flavor or an Xcode scheme, null for
@@ -33,6 +34,9 @@
   /// Extra command-line options for gen_snapshot.
   final String extraGenSnapshotOptions;
 
+  // Whether to prefer AOT compiling to a *so file.
+  final bool preferSharedLibrary;
+
   static const BuildInfo debug = const BuildInfo(BuildMode.debug, null);
   static const BuildInfo profile = const BuildInfo(BuildMode.profile, null);
   static const BuildInfo release = const BuildInfo(BuildMode.release, null);
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index d2b1510..4339f52 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import '../android/android_sdk.dart';
 import '../artifacts.dart';
 import '../base/build.dart';
 import '../base/common.dart';
@@ -48,7 +49,9 @@
         allowMultiple: true,
         splitCommas: true,
         hide: true,
-      );
+      )
+      ..addFlag('prefer-shared-library', negatable: false,
+          help: 'Whether to prefer compiling to a *.so file (android only).');
   }
 
   @override
@@ -80,6 +83,7 @@
       previewDart2: argResults['preview-dart-2'],
       extraFrontEndOptions: argResults[FlutterOptions.kExtraFrontEndOptions],
       extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
+      preferSharedLibrary: argResults['prefer-shared-library'],
     );
     status?.stop();
 
@@ -110,6 +114,7 @@
   bool previewDart2: false,
   List<String> extraFrontEndOptions,
   List<String> extraGenSnapshotOptions,
+  bool preferSharedLibrary: false,
 }) async {
   outputPath ??= getAotBuildDirectory();
   try {
@@ -122,6 +127,7 @@
       previewDart2: previewDart2,
       extraFrontEndOptions: extraFrontEndOptions,
       extraGenSnapshotOptions: extraGenSnapshotOptions,
+      preferSharedLibrary: preferSharedLibrary,
     );
   } on String catch (error) {
     // Catch the String exceptions thrown from the `runCheckedSync` methods below.
@@ -140,6 +146,7 @@
   bool previewDart2: false,
   List<String> extraFrontEndOptions,
   List<String> extraGenSnapshotOptions,
+  bool preferSharedLibrary: false,
 }) async {
   outputPath ??= getAotBuildDirectory();
   if (!isAotBuildMode(buildMode) && !interpreter) {
@@ -161,6 +168,16 @@
   final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data');
   final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr');
   final String dependencies = fs.path.join(outputDir.path, 'snapshot.d');
+  final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');
+  final String assemblyO = fs.path.join(outputDir.path, 'snapshot_assembly.o');
+  final String assemblySo = fs.path.join(outputDir.path, 'app.so');
+  final bool compileToSharedLibrary =
+      preferSharedLibrary && androidSdk.ndkCompiler != null;
+
+  if (preferSharedLibrary && !compileToSharedLibrary) {
+    printStatus(
+        'Could not find NDK compiler. Not building in shared library mode');
+  }
 
   final String vmEntryPoints = artifacts.getArtifactPath(
     Artifact.dartVmEntryPointsTxt,
@@ -192,20 +209,22 @@
 
   // These paths are used only on iOS.
   String snapshotDartIOS;
-  String assembly;
 
   switch (platform) {
     case TargetPlatform.android_arm:
     case TargetPlatform.android_x64:
     case TargetPlatform.android_x86:
-      outputPaths.addAll(<String>[
-        vmSnapshotData,
-        isolateSnapshotData,
-      ]);
+      if (compileToSharedLibrary) {
+        outputPaths.add(assemblySo);
+      } else {
+        outputPaths.addAll(<String>[
+          vmSnapshotData,
+          isolateSnapshotData,
+        ]);
+      }
       break;
     case TargetPlatform.ios:
       snapshotDartIOS = artifacts.getArtifactPath(Artifact.snapshotDart, platform, buildMode);
-      assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');
       inputPaths.add(snapshotDartIOS);
       break;
     case TargetPlatform.darwin_x64:
@@ -260,16 +279,23 @@
   final String kIsolateSnapshotDataC = fs.path.join(outputDir.path, '$kIsolateSnapshotData.c');
   final String kVmSnapshotDataO = fs.path.join(outputDir.path, '$kVmSnapshotData.o');
   final String kIsolateSnapshotDataO = fs.path.join(outputDir.path, '$kIsolateSnapshotData.o');
-  final String assemblyO = fs.path.join(outputDir.path, 'snapshot_assembly.o');
 
   switch (platform) {
     case TargetPlatform.android_arm:
     case TargetPlatform.android_x64:
     case TargetPlatform.android_x86:
+      if (compileToSharedLibrary) {
+        genSnapshotCmd.add('--snapshot_kind=app-aot-assembly');
+        genSnapshotCmd.add('--assembly=$assembly');
+        outputPaths.add(assemblySo);
+      } else {
+        genSnapshotCmd.addAll(<String>[
+          '--snapshot_kind=app-aot-blobs',
+          '--vm_snapshot_instructions=$vmSnapshotInstructions',
+          '--isolate_snapshot_instructions=$isolateSnapshotInstructions',
+        ]);
+      }
       genSnapshotCmd.addAll(<String>[
-        '--snapshot_kind=app-aot-blobs',
-        '--vm_snapshot_instructions=$vmSnapshotInstructions',
-        '--isolate_snapshot_instructions=$isolateSnapshotInstructions',
         '--no-sim-use-hardfp',  // Android uses the softfloat ABI.
         '--no-use-integer-division',  // Not supported by the Pixel in 32-bit mode.
       ]);
@@ -396,6 +422,19 @@
       linkCommand.add(assemblyO);
     }
     await runCheckedAsync(linkCommand);
+  } else {
+    if (compileToSharedLibrary) {
+      // A word of warning: Instead of compiling via two steps, to a .o file and
+      // then to a .so file we use only one command.  When using two commands
+      // gcc will end up putting a .eh_frame and a .debug_frame into the shared
+      // library.  Without stripping .debug_frame afterwards, unwinding tools
+      // based upon libunwind use just one and ignore the contents of the other
+      // (which causes it to not look into the other section and therefore not
+      // find the correct unwinding information).
+      await runCheckedAsync(<String>[androidSdk.ndkCompiler]
+          ..addAll(androidSdk.ndkCompilerArgs)
+          ..addAll(<String>[ '-shared', '-nostdlib', '-o', assemblySo, assembly ]));
+    }
   }
 
   // Compute and record build fingerprint.
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index fc9d97f..ff53ab7 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -11,9 +11,13 @@
   BuildApkCommand() {
     usesTargetOption();
     addBuildModeFlags();
-    argParser.addFlag('preview-dart-2', negatable: false);
     usesFlavorOption();
     usesPubOption();
+
+    argParser
+      ..addFlag('preview-dart-2', negatable: false)
+      ..addFlag('prefer-shared-library', negatable: false,
+          help: 'Whether to prefer compiling to a *.so file (android only).');
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index a61beed..ca37fd7 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -161,7 +161,10 @@
           : null,
       extraGenSnapshotOptions: argParser.options.containsKey(FlutterOptions.kExtraGenSnapshotOptions)
           ? argResults[FlutterOptions.kExtraGenSnapshotOptions]
-          : null);
+          : null,
+      preferSharedLibrary: argParser.options.containsKey('prefer-shared-library')
+        ? argResults['prefer-shared-library']
+        : false);
   }
 
   void setupApplicationPackages() {
diff --git a/packages/flutter_tools/test/android/android_sdk_test.dart b/packages/flutter_tools/test/android/android_sdk_test.dart
index aa77b22..76f64e0 100644
--- a/packages/flutter_tools/test/android/android_sdk_test.dart
+++ b/packages/flutter_tools/test/android/android_sdk_test.dart
@@ -5,6 +5,8 @@
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/android/android_sdk.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/config.dart';
 import 'package:test/test.dart';
 
 import '../src/context.dart';
@@ -21,12 +23,14 @@
 
     tearDown(() {
       sdkDir?.deleteSync(recursive: true);
+      sdkDir = null;
     });
 
     testUsingContext('parse sdk', () {
       sdkDir = _createSdkDirectory();
-      final AndroidSdk sdk = new AndroidSdk(sdkDir.path);
+      Config.instance.setValue('android-sdk', sdkDir.path);
 
+      final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
       expect(sdk.latestVersion, isNotNull);
       expect(sdk.latestVersion.sdkLevel, 23);
     }, overrides: <Type, Generator>{
@@ -35,17 +39,71 @@
 
     testUsingContext('parse sdk N', () {
       sdkDir = _createSdkDirectory(withAndroidN: true);
-      final AndroidSdk sdk = new AndroidSdk(sdkDir.path);
+      Config.instance.setValue('android-sdk', sdkDir.path);
 
+      final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
       expect(sdk.latestVersion, isNotNull);
       expect(sdk.latestVersion.sdkLevel, 24);
     }, overrides: <Type, Generator>{
       FileSystem: () => fs,
     });
+
+    group('ndk', () {
+      const <String, String>{
+        'linux': 'linux-x86_64',
+        'macos': 'darwin-x86_64',
+      }.forEach((String os, String osDir) {
+        testUsingContext('detection on $os', () {
+          sdkDir = _createSdkDirectory(
+              withAndroidN: true, withNdkDir: osDir, withNdkSysroot: true);
+          Config.instance.setValue('android-sdk', sdkDir.path);
+
+          final String realSdkDir = sdkDir.path;
+          final String realNdkDir = fs.path.join(realSdkDir, 'ndk-bundle');
+          final String realNdkCompiler = fs.path.join(
+              realNdkDir,
+              'toolchains',
+              'arm-linux-androideabi-4.9',
+              'prebuilt',
+              osDir,
+              'bin',
+              'arm-linux-androideabi-gcc');
+          final String realNdkSysroot =
+              fs.path.join(realNdkDir, 'platforms', 'android-9', 'arch-arm');
+
+          final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+          expect(sdk.directory, realSdkDir);
+          expect(sdk.ndkDirectory, realNdkDir);
+          expect(sdk.ndkCompiler, realNdkCompiler);
+          expect(sdk.ndkCompilerArgs, <String>['--sysroot', realNdkSysroot]);
+        }, overrides: <Type, Generator>{
+          FileSystem: () => fs,
+          Platform: () => new FakePlatform(operatingSystem: os),
+        });
+      });
+
+      for (String os in <String>['linux', 'macos']) {
+        testUsingContext('detection on $os (no ndk available)', () {
+          sdkDir = _createSdkDirectory(withAndroidN: true);
+          Config.instance.setValue('android-sdk', sdkDir.path);
+
+          final String realSdkDir = sdkDir.path;
+          final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+          expect(sdk.directory, realSdkDir);
+          expect(sdk.ndkDirectory, null);
+          expect(sdk.ndkCompiler, null);
+          expect(sdk.ndkCompilerArgs, null);
+        }, overrides: <Type, Generator>{
+          FileSystem: () => fs,
+          Platform: () => new FakePlatform(operatingSystem: os),
+        });
+      }
+    });
   });
 }
 
-Directory _createSdkDirectory({ bool withAndroidN: false }) {
+Directory _createSdkDirectory(
+    {bool withAndroidN: false, String withNdkDir, bool withNdkSysroot: false}) {
   final Directory dir = fs.systemTempDirectory.createTempSync('android-sdk');
 
   _createSdkFile(dir, 'platform-tools/adb');
@@ -63,6 +121,23 @@
     _createSdkFile(dir, 'platforms/android-N/build.prop', contents: _buildProp);
   }
 
+  if (withNdkDir != null) {
+    final String ndkCompiler = fs.path.join(
+        'ndk-bundle',
+        'toolchains',
+        'arm-linux-androideabi-4.9',
+        'prebuilt',
+        withNdkDir,
+        'bin',
+        'arm-linux-androideabi-gcc');
+    _createSdkFile(dir, ndkCompiler);
+  }
+  if (withNdkSysroot) {
+    final String armPlatform =
+        fs.path.join('ndk-bundle', 'platforms', 'android-9', 'arch-arm');
+    _createDir(dir, armPlatform);
+  }
+
   return dir;
 }
 
@@ -74,6 +149,11 @@
   }
 }
 
+void _createDir(Directory dir, String path) {
+  final Directory directory = fs.directory(fs.path.join(dir.path, path));
+  directory.createSync(recursive: true);
+}
+
 const String _buildProp = r'''
 ro.build.version.incremental=1624448
 ro.build.version.sdk=24
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 8913488..f75f101 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -70,6 +70,21 @@
   ContextInitializer initializeContext: _defaultInitializeContext,
   bool skip, // should default to `false`, but https://github.com/dart-lang/test/issues/545 doesn't allow this
 }) {
+
+  // Ensure we don't rely on the default [Config] constructor which will
+  // leak a sticky $HOME/.flutter_settings behind!
+  Directory configDir;
+  tearDown(() {
+    configDir?.deleteSync(recursive: true);
+    configDir = null;
+  });
+  Config buildConfig(FileSystem fs) {
+    configDir = fs.systemTempDirectory.createTempSync('config-dir');
+    final File settingsFile = fs.file(
+        fs.path.join(configDir.path, '.flutter_settings'));
+    return new Config(settingsFile);
+  }
+
   test(description, () async {
     final AppContext testContext = new AppContext();
 
@@ -80,7 +95,7 @@
       ..putIfAbsent(FileSystem, () => const LocalFileSystem())
       ..putIfAbsent(ProcessManager, () => const LocalProcessManager())
       ..putIfAbsent(Logger, () => new BufferLogger())
-      ..putIfAbsent(Config, () => new Config());
+      ..putIfAbsent(Config, () => buildConfig(testContext[FileSystem]));
 
     // Apply the initializer after seeding the base value above.
     initializeContext(testContext);