[flutter_tool] Build a Fuchsia package (#32519)

diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 6db4f0e..c841d54 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -392,11 +392,16 @@
   return fs.path.join(getBuildDirectory(), 'web');
 }
 
-/// Returns the linux build output directory.
+/// Returns the Linux build output directory.
 String getLinuxBuildDirectory() {
   return fs.path.join(getBuildDirectory(), 'linux');
 }
 
+/// Returns the Fuchsia build output directory.
+String getFuchsiaBuildDirectory() {
+  return fs.path.join(getBuildDirectory(), 'fuchsia');
+}
+
 /// Returns directory used by incremental compiler (IKG - incremental kernel
 /// generator) to store cached intermediate state.
 String getIncrementalCompilerByteStoreDirectory() {
diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart
index e97e7f3..1691b57 100644
--- a/packages/flutter_tools/lib/src/commands/build.dart
+++ b/packages/flutter_tools/lib/src/commands/build.dart
@@ -13,6 +13,7 @@
 import 'build_apk.dart';
 import 'build_appbundle.dart';
 import 'build_bundle.dart';
+import 'build_fuchsia.dart';
 import 'build_ios.dart';
 import 'build_web.dart';
 
@@ -27,6 +28,7 @@
     addSubcommand(BuildMacosCommand());
     addSubcommand(BuildLinuxCommand());
     addSubcommand(BuildWindowsCommand());
+    addSubcommand(BuildFuchsiaCommand(verboseHelp: verboseHelp));
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/build_fuchsia.dart b/packages/flutter_tools/lib/src/commands/build_fuchsia.dart
new file mode 100644
index 0000000..473f943
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/build_fuchsia.dart
@@ -0,0 +1,66 @@
+// Copyright 2019 The Chromium 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:async';
+
+import '../base/common.dart';
+import '../base/file_system.dart';
+import '../base/platform.dart';
+import '../build_info.dart';
+import '../cache.dart';
+import '../fuchsia/fuchsia_build.dart';
+import '../project.dart';
+import '../runner/flutter_command.dart' show FlutterCommandResult;
+import 'build.dart';
+
+/// A command to build a Fuchsia target.
+class BuildFuchsiaCommand extends BuildSubCommand {
+  BuildFuchsiaCommand({bool verboseHelp = false}) {
+    usesTargetOption();
+    addBuildModeFlags(verboseHelp: verboseHelp);
+  }
+
+  @override
+  final String name = 'fuchsia';
+
+  @override
+  bool isExperimental = true;
+
+  @override
+  bool hidden = true;
+
+  @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => <DevelopmentArtifact>{
+    DevelopmentArtifact.fuchsia,
+    DevelopmentArtifact.universal,
+  };
+
+  @override
+  String get description => 'build the Fuchsia target (Experimental).';
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    Cache.releaseLockEarly();
+    final BuildInfo buildInfo = getBuildInfo();
+    final FlutterProject flutterProject = FlutterProject.current();
+    if (!platform.isLinux && !platform.isMacOS) {
+      throwToolExit('"build Fuchsia" only supported on Linux and MacOS hosts.');
+    }
+    if (!flutterProject.fuchsia.existsSync()) {
+      throwToolExit('No Fuchsia project configured.');
+    }
+    final String appName = flutterProject.fuchsia.project.manifest.appName;
+    final String cmxPath = fs.path.join(
+        flutterProject.fuchsia.meta.path, '$appName.cmx');
+    final File cmxFile = fs.file(cmxPath);
+    if (!cmxFile.existsSync()) {
+      throwToolExit('Fuchsia build requires a .cmx file at $cmxPath for the app');
+    }
+    await buildFuchsia(
+        fuchsiaProject: flutterProject.fuchsia,
+        target: targetFile,
+        buildInfo: buildInfo);
+    return null;
+  }
+}
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index eb7dea5..f69b872 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -27,6 +27,8 @@
 import 'device.dart';
 import 'doctor.dart';
 import 'emulator.dart';
+import 'fuchsia/fuchsia_kernel_compiler.dart';
+import 'fuchsia/fuchsia_pm.dart';
 import 'fuchsia/fuchsia_sdk.dart';
 import 'fuchsia/fuchsia_workflow.dart';
 import 'ios/cocoapods.dart';
@@ -52,36 +54,38 @@
     body: runner,
     overrides: overrides,
     fallbacks: <Type, Generator>{
+      AndroidLicenseValidator: () => AndroidLicenseValidator(),
       AndroidSdk: AndroidSdk.locateAndroidSdk,
       AndroidStudio: AndroidStudio.latestValid,
-      AndroidWorkflow: () => AndroidWorkflow(),
       AndroidValidator: () => AndroidValidator(),
-      AndroidLicenseValidator: () => AndroidLicenseValidator(),
+      AndroidWorkflow: () => AndroidWorkflow(),
       ApplicationPackageFactory: () => ApplicationPackageFactory(),
       Artifacts: () => CachedArtifacts(),
       AssetBundleFactory: () => AssetBundleFactory.defaultInstance,
       BotDetector: () => const BotDetector(),
       Cache: () => Cache(),
+      ChromeLauncher: () => const ChromeLauncher(),
       CocoaPods: () => CocoaPods(),
       CocoaPodsValidator: () => const CocoaPodsValidator(),
       Config: () => Config(),
-      ChromeLauncher: () => const ChromeLauncher(),
       DevFSConfig: () => DevFSConfig(),
       DeviceManager: () => DeviceManager(),
       Doctor: () => const Doctor(),
       DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance,
       EmulatorManager: () => EmulatorManager(),
-      FuchsiaSdk: () => FuchsiaSdk(),
-      FuchsiaArtifacts: () => FuchsiaArtifacts.find(),
-      FuchsiaWorkflow: () => FuchsiaWorkflow(),
       Flags: () => const EmptyFlags(),
       FlutterVersion: () => FlutterVersion(const SystemClock()),
+      FuchsiaArtifacts: () => FuchsiaArtifacts.find(),
+      FuchsiaKernelCompiler: () => FuchsiaKernelCompiler(),
+      FuchsiaPM: () => FuchsiaPM(),
+      FuchsiaSdk: () => FuchsiaSdk(),
+      FuchsiaWorkflow: () => FuchsiaWorkflow(),
       GenSnapshot: () => const GenSnapshot(),
       HotRunnerConfig: () => HotRunnerConfig(),
       IMobileDevice: () => const IMobileDevice(),
       IOSSimulatorUtils: () => IOSSimulatorUtils(),
-      IOSWorkflow: () => const IOSWorkflow(),
       IOSValidator: () => const IOSValidator(),
+      IOSWorkflow: () => const IOSWorkflow(),
       KernelCompilerFactory: () => const KernelCompilerFactory(),
       LinuxWorkflow: () => const LinuxWorkflow(),
       Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger(),
@@ -89,13 +93,13 @@
       OperatingSystemUtils: () => OperatingSystemUtils(),
       PlistBuddy: () => const PlistBuddy(),
       SimControl: () => SimControl(),
-      SystemClock: () => const SystemClock(),
       Stdio: () => const Stdio(),
+      SystemClock: () => const SystemClock(),
       TimeoutConfiguration: () => const TimeoutConfiguration(),
       Usage: () => Usage(),
       UserMessages: () => UserMessages(),
-      WindowsWorkflow: () => const WindowsWorkflow(),
       WebCompiler: () => const WebCompiler(),
+      WindowsWorkflow: () => const WindowsWorkflow(),
       Xcode: () => Xcode(),
       XcodeProjectInterpreter: () => XcodeProjectInterpreter(),
     },
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart
new file mode 100644
index 0000000..c39b83b
--- /dev/null
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart
@@ -0,0 +1,111 @@
+// Copyright 2019 The Chromium 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 'package:meta/meta.dart';
+
+import '../asset.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../build_info.dart';
+import '../bundle.dart';
+import '../devfs.dart';
+import '../project.dart';
+
+import 'fuchsia_kernel_compiler.dart';
+import 'fuchsia_pm.dart';
+
+// Building a Fuchsia package has a few steps:
+// 1. Do the custom kernel compile using the kernel compiler from the Fuchsia
+//    SDK. This produces .dilp files (among others) and a manifest file.
+// 2. Create a manifest file for assets.
+// 3. Using these manifests, use the Fuchsia SDK 'pm' tool to create the
+//    Fuchsia package.
+Future<void> buildFuchsia(
+    {@required FuchsiaProject fuchsiaProject,
+    @required String target, // E.g., lib/main.dart
+    BuildInfo buildInfo = BuildInfo.debug}) async {
+  final Directory outDir = fs.directory(getFuchsiaBuildDirectory());
+  if (!outDir.existsSync()) {
+    outDir.createSync(recursive: true);
+  }
+
+  await fuchsiaKernelCompiler.build(
+      fuchsiaProject: fuchsiaProject, target: target, buildInfo: buildInfo);
+  await _buildAssets(fuchsiaProject, target, buildInfo);
+  await _buildPackage(fuchsiaProject, target, buildInfo);
+}
+
+Future<void> _buildAssets(
+    FuchsiaProject fuchsiaProject,
+    String target, // lib/main.dart
+    BuildInfo buildInfo) async {
+  final String assetDir = getAssetBuildDirectory();
+  final AssetBundle assets = await buildAssets(
+    manifestPath: fuchsiaProject.project.pubspecFile.path,
+    packagesPath: fuchsiaProject.project.packagesFile.path,
+    assetDirPath: assetDir,
+    includeDefaultFonts: false,
+  );
+
+  final Map<String, DevFSContent> assetEntries =
+      Map<String, DevFSContent>.from(assets.entries);
+  await writeBundle(fs.directory(assetDir), assetEntries);
+
+  final String appName = fuchsiaProject.project.manifest.appName;
+  final String outDir = getFuchsiaBuildDirectory();
+  final String assetManifest = fs.path.join(outDir, '${appName}_pkgassets');
+
+  final File destFile = fs.file(assetManifest);
+  await destFile.create(recursive: true);
+  final IOSink outFile = destFile.openWrite();
+
+  for (String path in assets.entries.keys) {
+    outFile.write('data/$appName/$path=$assetDir/$path\n');
+  }
+  await outFile.flush();
+  await outFile.close();
+}
+
+// TODO(zra): Allow supplying a signing key.
+Future<void> _buildPackage(
+    FuchsiaProject fuchsiaProject,
+    String target, // lib/main.dart
+    BuildInfo buildInfo) async {
+  final String outDir = getFuchsiaBuildDirectory();
+  final String pkgDir = fs.path.join(outDir, 'pkg');
+  final String appName = fuchsiaProject.project.manifest.appName;
+  final String dilpmanifest = fs.path.join(outDir, '$appName.dilpmanifest');
+  final String pkgassets = fs.path.join(outDir, '${appName}_pkgassets');
+  final String packageManifest = fs.path.join(pkgDir, 'package_manifest');
+  final String devKeyPath = fs.path.join(pkgDir, 'development.key');
+
+  final Directory pkg = fs.directory(pkgDir);
+  if (!pkg.existsSync()) {
+    pkg.createSync(recursive: true);
+  }
+
+  // Concatenate dilpmanifest and pkgassets into package_manifest.
+  final File manifestFile = fs.file(packageManifest);
+  manifestFile.writeAsStringSync(fs.file(dilpmanifest).readAsStringSync());
+  manifestFile.writeAsStringSync(fs.file(pkgassets).readAsStringSync(),
+      mode: FileMode.append);
+  manifestFile.writeAsStringSync(
+      'meta/$appName.cmx=${fuchsiaProject.meta.path}/$appName.cmx\n',
+      mode: FileMode.append);
+  manifestFile.writeAsStringSync('meta/package=$pkgDir/meta/package\n',
+      mode: FileMode.append);
+
+  if (!await fuchsiaPM.init(pkgDir, appName)) {
+    return;
+  }
+  if (!await fuchsiaPM.genkey(pkgDir, devKeyPath)) {
+    return;
+  }
+  if (!await fuchsiaPM.build(pkgDir, devKeyPath, packageManifest)) {
+    return;
+  }
+  if (!await fuchsiaPM.archive(pkgDir, devKeyPath, packageManifest)) {
+    return;
+  }
+}
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_kernel_compiler.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_kernel_compiler.dart
new file mode 100644
index 0000000..e31a5bf
--- /dev/null
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_kernel_compiler.dart
@@ -0,0 +1,114 @@
+// Copyright 2019 The Chromium 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 'package:meta/meta.dart';
+
+import '../artifacts.dart';
+import '../base/common.dart';
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/logger.dart';
+import '../base/process_manager.dart';
+import '../build_info.dart';
+import '../convert.dart';
+import '../globals.dart';
+import '../project.dart';
+
+import 'fuchsia_sdk.dart';
+
+/// The [FuchsiaKernelCompiler] instance.
+FuchsiaKernelCompiler get fuchsiaKernelCompiler =>
+    context.get<FuchsiaKernelCompiler>();
+
+class FuchsiaKernelCompiler {
+  /// Compiles the [fuchsiaProject] with entrypoint [target] to a collection of
+  /// .dilp files (consisting of the app split along package: boundaries, but
+  /// the Flutter tool should make no use of that fact), and a manifest that
+  /// refers to them.
+  Future<void> build({
+    @required FuchsiaProject fuchsiaProject,
+    @required String target, // E.g., lib/main.dart
+    BuildInfo buildInfo = BuildInfo.debug,
+  }) async {
+    // TODO(zra): Use filesystem root and scheme information from buildInfo.
+    const String multiRootScheme = 'main-root';
+    final String packagesFile = fuchsiaProject.project.packagesFile.path;
+    final String outDir = getFuchsiaBuildDirectory();
+    final String appName = fuchsiaProject.project.manifest.appName;
+    final String fsRoot = fuchsiaProject.project.directory.path;
+    final String relativePackagesFile =
+        fs.path.relative(packagesFile, from: fsRoot);
+    final String manifestPath = fs.path.join(outDir, '$appName.dilpmanifest');
+    List<String> flags = <String>[
+      // https://github.com/dart-lang/sdk/issues/36639:
+      // Remove when new constant eval supports dilp files.
+      '--enable-experiment=no-constant-update-2018',
+      '--target', 'flutter_runner',
+      '--platform', fuchsiaArtifacts.platformKernelDill.path,
+      '--filesystem-scheme', 'main-root',
+      '--filesystem-root', fsRoot,
+      '--packages', '$multiRootScheme:///$relativePackagesFile',
+      '--output', fs.path.join(outDir, '$appName.dil'),
+      '--no-link-platform',
+      '--split-output-by-packages',
+      '--manifest', manifestPath,
+      '--component-name', appName,
+    ];
+
+    if (buildInfo.isDebug) {
+      flags += <String>[
+        '--embed-sources',
+      ];
+    } else if (buildInfo.isProfile) {
+      flags += <String>[
+        '--no-embed-sources',
+        '-Ddart.vm.profile=true',
+        '--gen-bytecode',
+        '--drop-ast',
+      ];
+    } else if (buildInfo.isRelease) {
+      flags += <String>[
+        '--no-embed-sources',
+        '-Ddart.vm.release=true',
+        '--gen-bytecode',
+        '--drop-ast',
+      ];
+    } else {
+      throwToolExit('Expected build type to be debug, profile, or release');
+    }
+
+    flags += <String>[
+      '$multiRootScheme:///$target',
+    ];
+
+    final List<String> command = <String>[
+          artifacts.getArtifactPath(Artifact.engineDartBinary),
+          fuchsiaArtifacts.kernelCompiler.path,
+        ]..addAll(flags);
+    printTrace("Running: '${command.join(' ')}'");
+    final Process process = await processManager.start(command);
+    final Status status = logger.startProgress(
+      'Building Fuchsia application...',
+      timeout: null,
+    );
+    int result;
+    try {
+      process.stderr
+          .transform(utf8.decoder)
+          .transform(const LineSplitter())
+          .listen(printError);
+      process.stdout
+          .transform(utf8.decoder)
+          .transform(const LineSplitter())
+          .listen(printTrace);
+      result = await process.exitCode;
+    } finally {
+      status.cancel();
+    }
+    if (result != 0) {
+      throwToolExit('Build process failed');
+    }
+  }
+}
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_pm.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_pm.dart
new file mode 100644
index 0000000..755e08d
--- /dev/null
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_pm.dart
@@ -0,0 +1,134 @@
+// Copyright 2019 The Chromium 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 '../base/context.dart';
+import '../base/io.dart';
+import '../base/process_manager.dart';
+import '../globals.dart';
+
+import 'fuchsia_sdk.dart';
+
+/// The [FuchsiaPM] instance.
+FuchsiaPM get fuchsiaPM => context.get<FuchsiaPM>();
+
+/// This is a basic wrapper class for the Fuchsia SDK's `pm` tool.
+class FuchsiaPM {
+  /// Initializes the staging area at [buildPath] for creating the Fuchsia
+  /// package for the app named [appName].
+  ///
+  /// When successful, this creates a file under [buildPath] at `meta/package`.
+  ///
+  /// NB: The [buildPath] should probably be e.g. `build/fuchsia/pkg`, and the
+  /// [appName] should probably be the name of the app from the pubspec file.
+  Future<bool> init(String buildPath, String appName) async {
+    final List<String> command = <String>[
+      fuchsiaArtifacts.pm.path,
+      '-o',
+      buildPath,
+      '-n',
+      appName,
+      'init',
+    ];
+    printTrace("Running: '${command.join(' ')}'");
+    final ProcessResult result = await processManager.run(command);
+    if (result.exitCode != 0) {
+      printError('Error initializing Fuchsia package for $appName: ');
+      printError(result.stdout);
+      printError(result.stderr);
+      return false;
+    }
+    return true;
+  }
+
+  /// Generates a new private key to be used to sign a Fuchsia package.
+  ///
+  /// [buildPath] should be the same [buildPath] passed to [init].
+  Future<bool> genkey(String buildPath, String outKeyPath) async {
+    final List<String> command = <String>[
+      fuchsiaArtifacts.pm.path,
+      '-o',
+      buildPath,
+      '-k',
+      outKeyPath,
+      'genkey',
+    ];
+    printTrace("Running: '${command.join(' ')}'");
+    final ProcessResult result = await processManager.run(command);
+    if (result.exitCode != 0) {
+      printError('Error generating key for Fuchsia package: ');
+      printError(result.stdout);
+      printError(result.stderr);
+      return false;
+    }
+    return true;
+  }
+
+  /// Updates, signs, and seals a Fuchsia package.
+  ///
+  /// [buildPath] should be the same [buildPath] passed to [init].
+  /// [manifestPath] must be a file containing lines formatted as follows:
+  ///
+  ///     data/path/to/file/in/the/package=/path/to/file/on/the/host
+  ///
+  /// which describe the contents of the Fuchsia package. It must also contain
+  /// two other entries:
+  ///
+  ///     meta/$APPNAME.cmx=/path/to/cmx/on/the/host/$APPNAME.cmx
+  ///     meta/package=/path/to/package/file/from/init/package
+  ///
+  /// where $APPNAME is the same [appName] passed to [init], and meta/package
+  /// is set up to be the file `meta/package` created by [init].
+  Future<bool> build(
+      String buildPath, String keyPath, String manifestPath) async {
+    final List<String> command = <String>[
+      fuchsiaArtifacts.pm.path,
+      '-o',
+      buildPath,
+      '-k',
+      keyPath,
+      '-m',
+      manifestPath,
+      'build',
+    ];
+    printTrace("Running: '${command.join(' ')}'");
+    final ProcessResult result = await processManager.run(command);
+    if (result.exitCode != 0) {
+      printError('Error building Fuchsia package: ');
+      printError(result.stdout);
+      printError(result.stderr);
+      return false;
+    }
+    return true;
+  }
+
+  /// Constructs a .far representation of the Fuchsia package.
+  ///
+  /// When successful, creates a file `app_name-0.far` under [buildPath], which
+  /// is the Fuchsia package.
+  ///
+  /// [buildPath] should be the same path passed to [init], and [manfiestPath]
+  /// should be the same manifest passed to [build].
+  Future<bool> archive(
+      String buildPath, String keyPath, String manifestPath) async {
+    final List<String> command = <String>[
+      fuchsiaArtifacts.pm.path,
+      '-o',
+      buildPath,
+      '-k',
+      keyPath,
+      '-m',
+      manifestPath,
+      'archive',
+    ];
+    printTrace("Running: '${command.join(' ')}'");
+    final ProcessResult result = await processManager.run(command);
+    if (result.exitCode != 0) {
+      printError('Error archiving Fuchsia package: ');
+      printError(result.stdout);
+      printError(result.stderr);
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
index d7d3a51..256eae2 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
@@ -86,6 +86,8 @@
     this.devFinder,
     this.platformKernelDill,
     this.flutterPatchedSdk,
+    this.kernelCompiler,
+    this.pm,
   });
 
   /// Creates a new [FuchsiaArtifacts] using the cached Fuchsia SDK.
@@ -116,6 +118,9 @@
           dartPrebuilts, 'flutter_runner', 'platform_strong.dill')),
       flutterPatchedSdk: fs.file(fs.path.join(
           dartPrebuilts, 'flutter_runner')),
+      kernelCompiler: fs.file(fs.path.join(
+          dartPrebuilts, 'kernel_compiler.snapshot')),
+      pm: fs.file(fs.path.join(tools, 'pm')),
     );
   }
 
@@ -135,4 +140,10 @@
 
   /// The directory containing [platformKernelDill].
   final File flutterPatchedSdk;
+
+  /// The snapshot of the Fuchsia kernel compiler.
+  final File kernelCompiler;
+
+  /// The pm tool.
+  final File pm;
 }
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index ae8e405..2decfcf 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -91,22 +91,32 @@
   }
 
   /// The iOS sub project of this project.
-  IosProject get ios => IosProject.fromFlutter(this);
+  IosProject _ios;
+  IosProject get ios => _ios ??= IosProject.fromFlutter(this);
 
   /// The Android sub project of this project.
-  AndroidProject get android => AndroidProject._(this);
+  AndroidProject _android;
+  AndroidProject get android => _android ??= AndroidProject._(this);
 
   /// The web sub project of this project.
-  WebProject get web => WebProject._(this);
+  WebProject _web;
+  WebProject get web => _web ??= WebProject._(this);
 
-  /// The macos sub project of this project.
-  MacOSProject get macos => MacOSProject._(this);
+  /// The MacOS sub project of this project.
+  MacOSProject _macos;
+  MacOSProject get macos => _macos ??= MacOSProject._(this);
 
-  /// The linux sub project of this project.
-  LinuxProject get linux => LinuxProject._(this);
+  /// The Linux sub project of this project.
+  LinuxProject _linux;
+  LinuxProject get linux => _linux ??= LinuxProject._(this);
 
-  /// The windows sub project of this project.
-  WindowsProject get windows => WindowsProject._(this);
+  /// The Windows sub project of this project.
+  WindowsProject _windows;
+  WindowsProject get windows => _windows ??= WindowsProject._(this);
+
+  /// The Fuchsia sub project of this project.
+  FuchsiaProject _fuchsia;
+  FuchsiaProject get fuchsia => _fuchsia ??= FuchsiaProject._(this);
 
   /// The `pubspec.yaml` file of this project.
   File get pubspecFile => directory.childFile('pubspec.yaml');
@@ -606,3 +616,20 @@
   /// The Linux project makefile.
   File get makeFile => editableHostAppDirectory.childFile('Makefile');
 }
+
+/// The Fuchisa sub project
+class FuchsiaProject {
+  FuchsiaProject._(this.project);
+
+  final FlutterProject project;
+
+  Directory _editableHostAppDirectory;
+  Directory get editableHostAppDirectory =>
+      _editableHostAppDirectory ??= project.directory.childDirectory('fuchsia');
+
+  bool existsSync() => editableHostAppDirectory.existsSync();
+
+  Directory _meta;
+  Directory get meta =>
+      _meta ??= editableHostAppDirectory.childDirectory('meta');
+}
diff --git a/packages/flutter_tools/test/commands/build_fuchsia_test.dart b/packages/flutter_tools/test/commands/build_fuchsia_test.dart
new file mode 100644
index 0000000..a31f596
--- /dev/null
+++ b/packages/flutter_tools/test/commands/build_fuchsia_test.dart
@@ -0,0 +1,191 @@
+// Copyright 2019 The Chromium 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 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/build.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/mocks.dart';
+
+void main() {
+  Cache.disableLocking();
+
+  MemoryFileSystem memoryFileSystem;
+  MockPlatform linuxPlatform;
+  MockPlatform windowsPlatform;
+  MockFuchsiaPM fuchsiaPM;
+  MockFuchsiaKernelCompiler fuchsiaKernelCompiler;
+
+  setUp(() {
+    memoryFileSystem = MemoryFileSystem();
+    linuxPlatform = MockPlatform();
+    windowsPlatform = MockPlatform();
+    fuchsiaPM = MockFuchsiaPM();
+    fuchsiaKernelCompiler = MockFuchsiaKernelCompiler();
+
+    when(linuxPlatform.isLinux).thenReturn(true);
+    when(windowsPlatform.isWindows).thenReturn(true);
+    when(windowsPlatform.isLinux).thenReturn(false);
+    when(windowsPlatform.isMacOS).thenReturn(false);
+  });
+
+  testUsingContext('Fuchsia build fails when there is no fuchsia project',
+      () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    expect(
+        createTestCommandRunner(command)
+            .run(const <String>['build', 'fuchsia']),
+        throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => linuxPlatform,
+    FileSystem: () => memoryFileSystem,
+  });
+
+  testUsingContext('Fuchsia build fails when there is no cmx file', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    fs.directory('fuchsia').createSync(recursive: true);
+    fs.file('.packages').createSync();
+    fs.file('pubspec.yaml').createSync();
+
+    expect(
+        createTestCommandRunner(command)
+            .run(const <String>['build', 'fuchsia']),
+        throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => linuxPlatform,
+    FileSystem: () => memoryFileSystem,
+  });
+
+  testUsingContext('Fuchsia build fails on Windows platform', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    const String appName = 'app_name';
+    fs
+        .file(fs.path.join('fuchsia', 'meta', '$appName.cmx'))
+        .createSync(recursive: true);
+    fs.file('.packages').createSync();
+    final File pubspecFile = fs.file('pubspec.yaml')..createSync();
+    pubspecFile.writeAsStringSync('name: $appName');
+
+    expect(
+        createTestCommandRunner(command)
+            .run(const <String>['build', 'fuchsia']),
+        throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => windowsPlatform,
+    FileSystem: () => memoryFileSystem,
+  });
+
+  testUsingContext('Fuchsia build parts fit together right', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    const String appName = 'app_name';
+    fs
+        .file(fs.path.join('fuchsia', 'meta', '$appName.cmx'))
+        .createSync(recursive: true);
+    fs.file('.packages').createSync();
+    fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+    final File pubspecFile = fs.file('pubspec.yaml')..createSync();
+    pubspecFile.writeAsStringSync('name: $appName');
+
+    await createTestCommandRunner(command)
+        .run(const <String>['build', 'fuchsia']);
+    final String farPath =
+        fs.path.join(getFuchsiaBuildDirectory(), 'pkg', 'app_name-0.far');
+    expect(fs.file(farPath).existsSync(), isTrue);
+  }, overrides: <Type, Generator>{
+    Platform: () => linuxPlatform,
+    FileSystem: () => memoryFileSystem,
+    FuchsiaPM: () => fuchsiaPM,
+    FuchsiaKernelCompiler: () => fuchsiaKernelCompiler,
+  });
+}
+
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{
+    'FLUTTER_ROOT': '/',
+  };
+}
+
+class MockFuchsiaPM extends Mock implements FuchsiaPM {
+  String _appName;
+
+  @override
+  Future<bool> init(String buildPath, String appName) async {
+    if (!fs.directory(buildPath).existsSync()) {
+      return false;
+    }
+    fs
+        .file(fs.path.join(buildPath, 'meta', 'package'))
+        .createSync(recursive: true);
+    _appName = appName;
+    return true;
+  }
+
+  @override
+  Future<bool> genkey(String buildPath, String outKeyPath) async {
+    if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync()) {
+      return false;
+    }
+    fs.file(outKeyPath).createSync(recursive: true);
+    return true;
+  }
+
+  @override
+  Future<bool> build(
+      String buildPath, String keyPath, String manifestPath) async {
+    if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() ||
+        !fs.file(keyPath).existsSync() ||
+        !fs.file(manifestPath).existsSync()) {
+      return false;
+    }
+    fs.file(fs.path.join(buildPath, 'meta.far')).createSync(recursive: true);
+    return true;
+  }
+
+  @override
+  Future<bool> archive(
+      String buildPath, String keyPath, String manifestPath) async {
+    if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() ||
+        !fs.file(keyPath).existsSync() ||
+        !fs.file(manifestPath).existsSync()) {
+      return false;
+    }
+    if (_appName == null) {
+      return false;
+    }
+    fs
+        .file(fs.path.join(buildPath, '$_appName-0.far'))
+        .createSync(recursive: true);
+    return true;
+  }
+}
+
+class MockFuchsiaKernelCompiler extends Mock implements FuchsiaKernelCompiler {
+  @override
+  Future<void> build({
+    @required FuchsiaProject fuchsiaProject,
+    @required String target, // E.g., lib/main.dart
+    BuildInfo buildInfo = BuildInfo.debug,
+  }) async {
+    final String outDir = getFuchsiaBuildDirectory();
+    final String appName = fuchsiaProject.project.manifest.appName;
+    final String manifestPath = fs.path.join(outDir, '$appName.dilpmanifest');
+    fs.file(manifestPath).createSync(recursive: true);
+  }
+}