Add initial implementation of flutter assemble (#32816)
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 42c1808..25b3904 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -14,6 +14,7 @@
import 'src/codegen.dart';
import 'src/commands/analyze.dart';
+import 'src/commands/assemble.dart';
import 'src/commands/attach.dart';
import 'src/commands/build.dart';
import 'src/commands/channel.dart';
@@ -61,6 +62,7 @@
await runner.run(args, <FlutterCommand>[
AnalyzeCommand(verboseHelp: verboseHelp),
+ AssembleCommand(),
AttachCommand(verboseHelp: verboseHelp),
BuildCommand(verboseHelp: verboseHelp),
ChannelCommand(verboseHelp: verboseHelp),
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index 1c7c178..65a8b63 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -14,10 +14,13 @@
import 'globals.dart';
enum Artifact {
+ /// The tool which compiles a dart kernel file into native code.
genSnapshot,
+ /// The flutter tester binary.
flutterTester,
snapshotDart,
flutterFramework,
+ /// The framework directory of the macOS desktop.
flutterMacOSFramework,
vmSnapshotData,
isolateSnapshotData,
@@ -25,12 +28,24 @@
platformLibrariesJson,
flutterPatchedSdkPath,
frontendServerSnapshotForEngineDartSdk,
+ /// The root directory of the dartk SDK.
engineDartSdkPath,
+ /// The dart binary used to execute any of the required snapshots.
engineDartBinary,
+ /// The dart snapshot of the dart2js compiler.
dart2jsSnapshot,
+ /// The dart snapshot of the dartdev compiler.
dartdevcSnapshot,
+ /// The dart snpashot of the kernel worker compiler.
kernelWorkerSnapshot,
+ /// The root of the web implementation of the dart SDK.
flutterWebSdk,
+ /// The root of the Linux desktop sources.
+ linuxDesktopPath,
+ /// The root of the Windows desktop sources.
+ windowsDesktopPath,
+ /// The root of the sky_engine package
+ skyEnginePath,
}
String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMode mode ]) {
@@ -47,6 +62,10 @@
case Artifact.flutterFramework:
return 'Flutter.framework';
case Artifact.flutterMacOSFramework:
+ if (platform != TargetPlatform.darwin_x64) {
+ throw Exception('${getNameForTargetPlatform(platform)} does not support'
+ ' macOS desktop development');
+ }
return 'FlutterMacOS.framework';
case Artifact.vmSnapshotData:
return 'vm_isolate_snapshot.bin';
@@ -74,6 +93,20 @@
return 'dartdevc.dart.snapshot';
case Artifact.kernelWorkerSnapshot:
return 'kernel_worker.dart.snapshot';
+ case Artifact.linuxDesktopPath:
+ if (platform != TargetPlatform.linux_x64) {
+ throw Exception('${getNameForTargetPlatform(platform)} does not support'
+ ' Linux desktop development');
+ }
+ return '';
+ case Artifact.windowsDesktopPath:
+ if (platform != TargetPlatform.windows_x64) {
+ throw Exception('${getNameForTargetPlatform(platform)} does not support'
+ ' Windows desktop development');
+ }
+ return '';
+ case Artifact.skyEnginePath:
+ return 'sky_engine';
}
assert(false, 'Invalid artifact $artifact.');
return null;
@@ -209,9 +242,14 @@
case Artifact.kernelWorkerSnapshot:
return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
case Artifact.flutterMacOSFramework:
+ case Artifact.linuxDesktopPath:
+ case Artifact.windowsDesktopPath:
final String engineArtifactsPath = cache.getArtifactDirectory('engine').path;
final String platformDirName = getNameForTargetPlatform(platform);
return fs.path.join(engineArtifactsPath, platformDirName, _artifactToFileName(artifact, platform, mode));
+ case Artifact.skyEnginePath:
+ final Directory dartPackageDirectory = cache.getCacheDir('pkg');
+ return fs.path.join(dartPackageDirectory.path, _artifactToFileName(artifact));
default:
assert(false, 'Artifact $artifact not available for platform $platform.');
return null;
@@ -302,6 +340,12 @@
return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
case Artifact.kernelWorkerSnapshot:
return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
+ case Artifact.linuxDesktopPath:
+ return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
+ case Artifact.windowsDesktopPath:
+ return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
+ case Artifact.skyEnginePath:
+ return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', _artifactToFileName(artifact));
}
assert(false, 'Invalid artifact $artifact.');
return null;
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index 8d2338a..00264f3 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -9,7 +9,6 @@
import '../artifacts.dart';
import '../build_info.dart';
import '../bundle.dart';
-import '../cache.dart';
import '../compile.dart';
import '../dart/package_map.dart';
import '../globals.dart';
@@ -95,10 +94,6 @@
IOSArch iosArch,
List<String> extraGenSnapshotOptions = const <String>[],
}) async {
- FlutterProject flutterProject;
- if (fs.file('pubspec.yaml').existsSync()) {
- flutterProject = FlutterProject.current();
- }
if (!_isValidAotPlatform(platform, buildMode)) {
printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
return 1;
@@ -122,8 +117,6 @@
final List<String> inputPaths = <String>[uiPath, vmServicePath, mainPath];
final Set<String> outputPaths = <String>{};
-
- final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d');
final List<String> genSnapshotArgs = <String>[
'--deterministic',
];
@@ -165,26 +158,6 @@
return 1;
}
- // If inputs and outputs have not changed since last run, skip the build.
- final Fingerprinter fingerprinter = Fingerprinter(
- fingerprintPath: '$depfilePath.fingerprint',
- paths: <String>[mainPath, ...inputPaths, ...outputPaths],
- properties: <String, String>{
- 'buildMode': buildMode.toString(),
- 'targetPlatform': platform.toString(),
- 'entryPoint': mainPath,
- 'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '),
- 'engineHash': Cache.instance.engineRevision,
- 'buildersUsed': '${flutterProject != null && flutterProject.hasBuilders}',
- },
- depfilePaths: <String>[],
- );
- // TODO(jonahwilliams): re-enable once this can be proved correct.
- // if (await fingerprinter.doesFingerprintMatch()) {
- // printTrace('Skipping AOT snapshot build. Fingerprint match.');
- // return 0;
- // }
-
final SnapshotType snapshotType = SnapshotType(platform, buildMode);
final int genSnapshotExitCode =
await _timedStep('snapshot(CompileTime)', 'aot-snapshot',
@@ -210,9 +183,6 @@
if (result.exitCode != 0)
return result.exitCode;
}
-
- // Compute and record build fingerprint.
- await fingerprinter.writeFingerprint();
return 0;
}
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 8736135..ac0489a 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -115,6 +115,32 @@
release,
}
+const List<String> _kBuildModes = <String>[
+ 'debug',
+ 'profile',
+ 'release',
+ 'dynamic-profile',
+ 'dynamic-release',
+];
+
+/// Return the name for the build mode, or "any" if null.
+String getNameForBuildMode(BuildMode buildMode) {
+ return _kBuildModes[buildMode.index];
+}
+
+/// Returns the [BuildMode] for a particular `name`.
+BuildMode getBuildModeForName(String name) {
+ switch (name) {
+ case 'debug':
+ return BuildMode.debug;
+ case 'profile':
+ return BuildMode.profile;
+ case 'release':
+ return BuildMode.release;
+ }
+ return null;
+}
+
String validatedBuildNumberForPlatform(TargetPlatform targetPlatform, String buildNumber) {
if (buildNumber == null) {
return null;
diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart
new file mode 100644
index 0000000..c0f9d22
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/build_system.dart
@@ -0,0 +1,686 @@
+// 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 'package:async/async.dart';
+import 'package:convert/convert.dart';
+import 'package:crypto/crypto.dart';
+import 'package:meta/meta.dart';
+import 'package:pool/pool.dart';
+
+import '../base/file_system.dart';
+import '../base/platform.dart';
+import '../cache.dart';
+import '../convert.dart';
+import '../globals.dart';
+import 'exceptions.dart';
+import 'file_hash_store.dart';
+import 'source.dart';
+import 'targets/assets.dart';
+import 'targets/dart.dart';
+import 'targets/ios.dart';
+import 'targets/linux.dart';
+import 'targets/macos.dart';
+import 'targets/windows.dart';
+
+export 'source.dart';
+
+/// The function signature of a build target which can be invoked to perform
+/// the underlying task.
+typedef BuildAction = FutureOr<void> Function(
+ Map<String, ChangeType> inputs, Environment environment);
+
+/// A description of the update to each input file.
+enum ChangeType {
+ /// The file was added.
+ Added,
+ /// The file was deleted.
+ Removed,
+ /// The file was modified.
+ Modified,
+}
+
+/// Configuration for the build system itself.
+class BuildSystemConfig {
+ /// Create a new [BuildSystemConfig].
+ const BuildSystemConfig({this.resourcePoolSize});
+
+ /// The maximum number of concurrent tasks the build system will run.
+ ///
+ /// If not provided, defaults to [platform.numberOfProcessors].
+ final int resourcePoolSize;
+}
+
+/// A Target describes a single step during a flutter build.
+///
+/// The target inputs are required to be files discoverable via a combination
+/// of at least one of the environment values and zero or more local values.
+///
+/// To determine if the action for a target needs to be executed, the
+/// [BuildSystem] performs a hash of the file contents for both inputs and
+/// outputs. This is tracked separately in the [FileHashStore].
+///
+/// A Target has both implicit and explicit inputs and outputs. Only the
+/// later are safe to evaluate before invoking the [buildAction]. For example,
+/// a wildcard output pattern requires the outputs to exist before it can
+/// glob files correctly.
+///
+/// - All listed inputs are considered explicit inputs.
+/// - Outputs which are provided as [Source.pattern].
+/// without wildcards are considered explicit.
+/// - The remaining outputs are considered implicit.
+///
+/// For each target, executing its action creates a corresponding stamp file
+/// which records both the input and output files. This file is read by
+/// subsequent builds to determine which file hashes need to be checked. If the
+/// stamp file is missing, the target's action is always rerun.
+///
+/// file: `example_target.stamp`
+///
+/// {
+/// "inputs": [
+/// "absolute/path/foo",
+/// "absolute/path/bar",
+/// ...
+/// ],
+/// "outputs": [
+/// "absolute/path/fizz"
+/// ]
+/// }
+///
+/// ## Code review
+///
+/// ### Targes should only depend on files that are provided as inputs
+///
+/// Example: gen_snapshot must be provided as an input to the aot_elf
+/// build steps, even though it isn't a source file. This ensures that changes
+/// to the gen_snapshot binary (during a local engine build) correctly
+/// trigger a corresponding build update.
+///
+/// Example: aot_elf has a dependency on the dill and packages file
+/// produced by the kernel_snapshot step.
+///
+/// ### Targest should declare all outputs produced
+///
+/// If a target produces an output it should be listed, even if it is not
+/// intended to be consumed by another target.
+///
+/// ## Unit testing
+///
+/// Most targets will invoke an external binary which makes unit testing
+/// trickier. It is recommend that for unit testing that a Fake is used and
+/// provided via the dependency injection system. a [Testbed] may be used to
+/// set up the environment before the test is run. Unit tests should fully
+/// exercise the rule, ensuring that the existing input and output verification
+/// logic can run, as well as verifying it correctly handles provided defines
+/// and meets any additional contracts present in the target.
+class Target {
+ const Target({
+ @required this.name,
+ @required this.inputs,
+ @required this.outputs,
+ @required this.buildAction,
+ this.dependencies = const <Target>[],
+ });
+
+ /// The user-readable name of the target.
+ ///
+ /// This information is surfaced in the assemble commands and used as an
+ /// argument to build a particular target.
+ final String name;
+
+ /// The dependencies of this target.
+ final List<Target> dependencies;
+
+ /// The input [Source]s which are diffed to determine if a target should run.
+ final List<Source> inputs;
+
+ /// The output [Source]s which we attempt to verify are correctly produced.
+ final List<Source> outputs;
+
+ /// The action which performs this build step.
+ final BuildAction buildAction;
+
+ /// Collect hashes for all inputs to determine if any have changed.
+ Future<Map<String, ChangeType>> computeChanges(
+ List<File> inputs,
+ Environment environment,
+ FileHashStore fileHashStore,
+ ) async {
+ final Map<String, ChangeType> updates = <String, ChangeType>{};
+ final File stamp = _findStampFile(environment);
+ final Set<String> previousInputs = <String>{};
+ final List<String> previousOutputs = <String>[];
+
+ // If the stamp file doesn't exist, we haven't run this step before and
+ // all inputs were added.
+ if (stamp.existsSync()) {
+ final String content = stamp.readAsStringSync();
+ // Something went wrong writing the stamp file.
+ if (content == null || content.isEmpty) {
+ stamp.deleteSync();
+ } else {
+ final Map<String, Object> values = json.decode(content);
+ final List<Object> inputs = values['inputs'];
+ final List<Object> outputs = values['outputs'];
+ inputs.cast<String>().forEach(previousInputs.add);
+ outputs.cast<String>().forEach(previousOutputs.add);
+ }
+ }
+
+ // For each input type, first determine if we've already computed the hash
+ // for it. If not and it is a directory we skip hashing and instead use a
+ // timestamp. If it is a file we collect it to be sent off for hashing as
+ // a group.
+ final List<File> sourcesToHash = <File>[];
+ final List<File> missingInputs = <File>[];
+ for (File file in inputs) {
+ if (!file.existsSync()) {
+ missingInputs.add(file);
+ continue;
+ }
+
+ final String absolutePath = file.resolveSymbolicLinksSync();
+ final String previousHash = fileHashStore.previousHashes[absolutePath];
+ if (fileHashStore.currentHashes.containsKey(absolutePath)) {
+ final String currentHash = fileHashStore.currentHashes[absolutePath];
+ if (currentHash != previousHash) {
+ updates[absolutePath] = previousInputs.contains(absolutePath)
+ ? ChangeType.Modified
+ : ChangeType.Added;
+ }
+ } else {
+ sourcesToHash.add(file);
+ }
+ }
+ // Check if any outputs were deleted or modified from the previous run.
+ for (String previousOutput in previousOutputs) {
+ final File file = fs.file(previousOutput);
+ if (!file.existsSync()) {
+ updates[previousOutput] = ChangeType.Removed;
+ continue;
+ }
+ final String absolutePath = file.resolveSymbolicLinksSync();
+ final String previousHash = fileHashStore.previousHashes[absolutePath];
+ if (fileHashStore.currentHashes.containsKey(absolutePath)) {
+ final String currentHash = fileHashStore.currentHashes[absolutePath];
+ if (currentHash != previousHash) {
+ updates[absolutePath] = previousInputs.contains(absolutePath)
+ ? ChangeType.Modified
+ : ChangeType.Added;
+ }
+ } else {
+ sourcesToHash.add(file);
+ }
+ }
+
+ if (missingInputs.isNotEmpty) {
+ throw MissingInputException(missingInputs, name);
+ }
+
+ // If we have files to hash, compute them asynchronously and then
+ // update the result.
+ if (sourcesToHash.isNotEmpty) {
+ final List<File> dirty = await fileHashStore.hashFiles(sourcesToHash);
+ for (File file in dirty) {
+ final String absolutePath = file.resolveSymbolicLinksSync();
+ updates[absolutePath] = previousInputs.contains(absolutePath)
+ ? ChangeType.Modified
+ : ChangeType.Added;
+ }
+ }
+
+ // Find which, if any, inputs have been deleted.
+ final Set<String> currentInputPaths = Set<String>.from(
+ inputs.map<String>((File entity) => entity.resolveSymbolicLinksSync())
+ );
+ for (String previousInput in previousInputs) {
+ if (!currentInputPaths.contains(previousInput)) {
+ updates[previousInput] = ChangeType.Removed;
+ }
+ }
+ return updates;
+ }
+
+ /// Invoke to remove the stamp file if the [buildAction] threw an exception;
+ void clearStamp(Environment environment) {
+ final File stamp = _findStampFile(environment);
+ if (stamp.existsSync()) {
+ stamp.deleteSync();
+ }
+ }
+
+ void _writeStamp(
+ List<File> inputs,
+ List<File> outputs,
+ Environment environment,
+ ) {
+ final File stamp = _findStampFile(environment);
+ final List<String> inputPaths = <String>[];
+ for (File input in inputs) {
+ inputPaths.add(input.resolveSymbolicLinksSync());
+ }
+ final List<String> outputPaths = <String>[];
+ for (File output in outputs) {
+ outputPaths.add(output.resolveSymbolicLinksSync());
+ }
+ final Map<String, Object> result = <String, Object>{
+ 'inputs': inputPaths,
+ 'outputs': outputPaths,
+ };
+ if (!stamp.existsSync()) {
+ stamp.createSync();
+ }
+ stamp.writeAsStringSync(json.encode(result));
+ }
+
+ /// Resolve the set of input patterns and functions into a concrete list of
+ /// files.
+ List<File> resolveInputs(
+ Environment environment,
+ ) {
+ return _resolveConfiguration(inputs, environment, implicit: true, inputs: true);
+ }
+
+ /// Find the current set of declared outputs, including wildcard directories.
+ ///
+ /// The [implicit] flag controls whether it is safe to evaluate [Source]s
+ /// which uses functions, behaviors, or patterns.
+ List<File> resolveOutputs(
+ Environment environment,
+ { bool implicit = true, }
+ ) {
+ final List<File> outputEntities = _resolveConfiguration(outputs, environment, implicit: implicit, inputs: false);
+ if (implicit) {
+ verifyOutputDirectories(outputEntities, environment, this);
+ }
+ return outputEntities;
+ }
+
+ /// Performs a fold across this target and its dependencies.
+ T fold<T>(T initialValue, T combine(T previousValue, Target target)) {
+ final T dependencyResult = dependencies.fold(
+ initialValue, (T prev, Target t) => t.fold(prev, combine));
+ return combine(dependencyResult, this);
+ }
+
+ /// Convert the target to a JSON structure appropriate for consumption by
+ /// external systems.
+ ///
+ /// This requires constants from the [Environment] to resolve the paths of
+ /// inputs and the output stamp.
+ Map<String, Object> toJson(Environment environment) {
+ return <String, Object>{
+ 'name': name,
+ 'dependencies': dependencies.map((Target target) => target.name).toList(),
+ 'inputs': resolveInputs(environment)
+ .map((File file) => file.resolveSymbolicLinksSync())
+ .toList(),
+ 'outputs': resolveOutputs(environment, implicit: false)
+ .map((File file) => file.path)
+ .toList(),
+ 'stamp': _findStampFile(environment).absolute.path,
+ };
+ }
+
+ /// Locate the stamp file for a particular target name and environment.
+ File _findStampFile(Environment environment) {
+ final String fileName = '$name.stamp';
+ return environment.buildDir.childFile(fileName);
+ }
+
+ static List<File> _resolveConfiguration(
+ List<Source> config, Environment environment, { bool implicit = true, bool inputs = true }) {
+ final SourceVisitor collector = SourceVisitor(environment, inputs);
+ for (Source source in config) {
+ source.accept(collector);
+ }
+ return collector.sources;
+ }
+}
+
+/// The [Environment] defines several constants for use during the build.
+///
+/// The environment contains configuration and file paths that are safe to
+/// depend on and reference during the build.
+///
+/// Example (Good):
+///
+/// Use the environment to determine where to write an output file.
+///
+/// environment.buildDir.childFile('output')
+/// ..createSync()
+/// ..writeAsStringSync('output data');
+///
+/// Example (Bad):
+///
+/// Use a hard-coded path or directory relative to the current working
+/// directory to write an output file.
+///
+/// fs.file('build/linux/out')
+/// ..createSync()
+/// ..writeAsStringSync('output data');
+///
+/// Example (Good):
+///
+/// Using the build mode to produce different output. Note that the action
+/// is still responsible for outputting a different file, as defined by the
+/// corresponding output [Source].
+///
+/// final BuildMode buildMode = getBuildModeFromDefines(environment.defines);
+/// if (buildMode == BuildMode.debug) {
+/// environment.buildDir.childFile('debug.output')
+/// ..createSync()
+/// ..writeAsStringSync('debug');
+/// } else {
+/// environment.buildDir.childFile('non_debug.output')
+/// ..createSync()
+/// ..writeAsStringSync('non_debug');
+/// }
+class Environment {
+ /// Create a new [Environment] object.
+ ///
+ /// Only [projectDir] is required. The remaining environment locations have
+ /// defaults based on it.
+ factory Environment({
+ @required Directory projectDir,
+ Directory buildDir,
+ Map<String, String> defines = const <String, String>{},
+ }) {
+ // Compute a unique hash of this build's particular environment.
+ // Sort the keys by key so that the result is stable. We always
+ // include the engine and dart versions.
+ String buildPrefix;
+ final List<String> keys = defines.keys.toList()..sort();
+ final StringBuffer buffer = StringBuffer();
+ for (String key in keys) {
+ buffer.write(key);
+ buffer.write(defines[key]);
+ }
+ // in case there was no configuration, provide some value.
+ buffer.write('Flutter is awesome');
+ final String output = buffer.toString();
+ final Digest digest = md5.convert(utf8.encode(output));
+ buildPrefix = hex.encode(digest.bytes);
+
+ final Directory rootBuildDir = buildDir ?? projectDir.childDirectory('build');
+ final Directory buildDirectory = rootBuildDir.childDirectory(buildPrefix);
+ return Environment._(
+ projectDir: projectDir,
+ buildDir: buildDirectory,
+ rootBuildDir: rootBuildDir,
+ cacheDir: Cache.instance.getRoot(),
+ defines: defines,
+ );
+ }
+
+ Environment._({
+ @required this.projectDir,
+ @required this.buildDir,
+ @required this.rootBuildDir,
+ @required this.cacheDir,
+ @required this.defines,
+ });
+
+ /// The [Source] value which is substituted with the path to [projectDir].
+ static const String kProjectDirectory = '{PROJECT_DIR}';
+
+ /// The [Source] value which is substituted with the path to [buildDir].
+ static const String kBuildDirectory = '{BUILD_DIR}';
+
+ /// The [Source] value which is substituted with the path to [cacheDir].
+ static const String kCacheDirectory = '{CACHE_DIR}';
+
+ /// The [Source] value which is substituted with a path to the flutter root.
+ static const String kFlutterRootDirectory = '{FLUTTER_ROOT}';
+
+ /// The `PROJECT_DIR` environment variable.
+ ///
+ /// This should be root of the flutter project where a pubspec and dart files
+ /// can be located.
+ final Directory projectDir;
+
+ /// The `BUILD_DIR` environment variable.
+ ///
+ /// Defaults to `{PROJECT_ROOT}/build`. The root of the output directory where
+ /// build step intermediates and outputs are written.
+ final Directory buildDir;
+
+ /// The `CACHE_DIR` environment variable.
+ ///
+ /// Defaults to `{FLUTTER_ROOT}/bin/cache`. The root of the artifact cache for
+ /// the flutter tool.
+ final Directory cacheDir;
+
+ /// Additional configuration passed to the build targets.
+ ///
+ /// Setting values here forces a unique build directory to be chosen
+ /// which prevents the config from leaking into different builds.
+ final Map<String, String> defines;
+
+ /// The root build directory shared by all builds.
+ final Directory rootBuildDir;
+}
+
+/// The result information from the build system.
+class BuildResult {
+ BuildResult(this.success, this.exceptions, this.performance);
+
+ final bool success;
+ final Map<String, ExceptionMeasurement> exceptions;
+ final Map<String, PerformanceMeasurement> performance;
+
+ bool get hasException => exceptions.isNotEmpty;
+}
+
+/// The build system is responsible for invoking and ordering [Target]s.
+class BuildSystem {
+ BuildSystem([Map<String, Target> targets])
+ : targets = targets ?? _defaultTargets;
+
+ /// All currently registered targets.
+ static final Map<String, Target> _defaultTargets = <String, Target>{
+ unpackMacos.name: unpackMacos,
+ macosApplication.name: macosApplication,
+ macoReleaseApplication.name: macoReleaseApplication,
+ unpackLinux.name: unpackLinux,
+ unpackWindows.name: unpackWindows,
+ copyAssets.name: copyAssets,
+ kernelSnapshot.name: kernelSnapshot,
+ aotElfProfile.name: aotElfProfile,
+ aotElfRelease.name: aotElfRelease,
+ aotAssemblyProfile.name: aotAssemblyProfile,
+ aotAssemblyRelease.name: aotAssemblyRelease,
+ releaseIosApplication.name: releaseIosApplication,
+ profileIosApplication.name: profileIosApplication,
+ debugIosApplication.name: debugIosApplication,
+ };
+
+ final Map<String, Target> targets;
+
+ /// Build the target `name` and all of its dependencies.
+ Future<BuildResult> build(
+ String name,
+ Environment environment,
+ BuildSystemConfig buildSystemConfig,
+ ) async {
+ final Target target = _getNamedTarget(name);
+ environment.buildDir.createSync(recursive: true);
+
+ // Load file hash store from previous builds.
+ final FileHashStore fileCache = FileHashStore(environment)
+ ..initialize();
+
+ // Perform sanity checks on build.
+ checkCycles(target);
+
+ final _BuildInstance buildInstance = _BuildInstance(environment, fileCache, buildSystemConfig);
+ bool passed = true;
+ try {
+ passed = await buildInstance.invokeTarget(target);
+ } finally {
+ // Always persist the file cache to disk.
+ fileCache.persist();
+ }
+ return BuildResult(
+ passed,
+ buildInstance.exceptionMeasurements,
+ buildInstance.stepTimings,
+ );
+ }
+
+ /// Describe the target `name` and all of its dependencies.
+ List<Map<String, Object>> describe(
+ String name,
+ Environment environment,
+ ) {
+ final Target target = _getNamedTarget(name);
+ environment.buildDir.createSync(recursive: true);
+ checkCycles(target);
+ // Cheat a bit and re-use the same map.
+ Map<String, Map<String, Object>> fold(Map<String, Map<String, Object>> accumulation, Target current) {
+ accumulation[current.name] = current.toJson(environment);
+ return accumulation;
+ }
+
+ final Map<String, Map<String, Object>> result =
+ <String, Map<String, Object>>{};
+ final Map<String, Map<String, Object>> targets = target.fold(result, fold);
+ return targets.values.toList();
+ }
+
+ // Returns the corresponding target or throws.
+ Target _getNamedTarget(String name) {
+ final Target target = targets[name];
+ if (target == null) {
+ throw Exception('No registered target:$name.');
+ }
+ return target;
+ }
+}
+
+/// An active instance of a build.
+class _BuildInstance {
+ _BuildInstance(this.environment, this.fileCache, this.buildSystemConfig)
+ : resourcePool = Pool(buildSystemConfig.resourcePoolSize ?? platform?.numberOfProcessors ?? 1);
+
+ final BuildSystemConfig buildSystemConfig;
+ final Pool resourcePool;
+ final Map<String, AsyncMemoizer<void>> pending = <String, AsyncMemoizer<void>>{};
+ final Environment environment;
+ final FileHashStore fileCache;
+
+ // Timings collected during target invocation.
+ final Map<String, PerformanceMeasurement> stepTimings = <String, PerformanceMeasurement>{};
+
+ // Exceptions caught during the build process.
+ final Map<String, ExceptionMeasurement> exceptionMeasurements = <String, ExceptionMeasurement>{};
+
+ Future<bool> invokeTarget(Target target) async {
+ final List<bool> results = await Future.wait(target.dependencies.map(invokeTarget));
+ if (results.any((bool result) => !result)) {
+ return false;
+ }
+ final AsyncMemoizer<bool> memoizer = pending[target.name] ??= AsyncMemoizer<bool>();
+ return memoizer.runOnce(() => _invokeInternal(target));
+ }
+
+ Future<bool> _invokeInternal(Target target) async {
+ final PoolResource resource = await resourcePool.request();
+ final Stopwatch stopwatch = Stopwatch()..start();
+ bool passed = true;
+ bool skipped = false;
+ try {
+ final List<File> inputs = target.resolveInputs(environment);
+ final Map<String, ChangeType> updates = await target.computeChanges(inputs, environment, fileCache);
+ if (updates.isEmpty) {
+ skipped = true;
+ printStatus('Skipping target: ${target.name}');
+ } else {
+ printStatus('${target.name}: Starting');
+ // build actions may be null.
+ await target?.buildAction(updates, environment);
+ printStatus('${target.name}: Complete');
+
+ final List<File> outputs = target.resolveOutputs(environment);
+ // Update hashes for output files.
+ await fileCache.hashFiles(outputs);
+ target._writeStamp(inputs, outputs, environment);
+ }
+ } catch (exception, stackTrace) {
+ // TODO(jonahwilliams): test
+ target.clearStamp(environment);
+ passed = false;
+ skipped = false;
+ exceptionMeasurements[target.name] = ExceptionMeasurement(
+ target.name, exception, stackTrace);
+ } finally {
+ resource.release();
+ stopwatch.stop();
+ stepTimings[target.name] = PerformanceMeasurement(
+ target.name, stopwatch.elapsedMilliseconds, skipped, passed);
+ }
+ return passed;
+ }
+}
+
+/// Helper class to collect exceptions.
+class ExceptionMeasurement {
+ ExceptionMeasurement(this.target, this.exception, this.stackTrace);
+
+ final String target;
+ final dynamic exception;
+ final StackTrace stackTrace;
+}
+
+/// Helper class to collect measurement data.
+class PerformanceMeasurement {
+ PerformanceMeasurement(this.target, this.elapsedMilliseconds, this.skiped, this.passed);
+ final int elapsedMilliseconds;
+ final String target;
+ final bool skiped;
+ final bool passed;
+}
+
+/// Check if there are any dependency cycles in the target.
+///
+/// Throws a [CycleException] if one is encountered.
+void checkCycles(Target initial) {
+ void checkInternal(Target target, Set<Target> visited, Set<Target> stack) {
+ if (stack.contains(target)) {
+ throw CycleException(stack..add(target));
+ }
+ if (visited.contains(target)) {
+ return;
+ }
+ visited.add(target);
+ stack.add(target);
+ for (Target dependency in target.dependencies) {
+ checkInternal(dependency, visited, stack);
+ }
+ stack.remove(target);
+ }
+ checkInternal(initial, <Target>{}, <Target>{});
+}
+
+/// Verifies that all files exist and are in a subdirectory of [Environment.buildDir].
+void verifyOutputDirectories(List<File> outputs, Environment environment, Target target) {
+ final String buildDirectory = environment.buildDir.resolveSymbolicLinksSync();
+ final String projectDirectory = environment.projectDir.resolveSymbolicLinksSync();
+ final List<File> missingOutputs = <File>[];
+ for (File sourceFile in outputs) {
+ if (!sourceFile.existsSync()) {
+ missingOutputs.add(sourceFile);
+ continue;
+ }
+ final String path = sourceFile.resolveSymbolicLinksSync();
+ if (!path.startsWith(buildDirectory) && !path.startsWith(projectDirectory)) {
+ throw MisplacedOutputException(path, target.name);
+ }
+ }
+ if (missingOutputs.isNotEmpty) {
+ throw MissingOutputException(missingOutputs, target.name);
+ }
+}
diff --git a/packages/flutter_tools/lib/src/build_system/exceptions.dart b/packages/flutter_tools/lib/src/build_system/exceptions.dart
new file mode 100644
index 0000000..918c051
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/exceptions.dart
@@ -0,0 +1,95 @@
+// 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/file_system.dart';
+
+import 'build_system.dart';
+
+/// An exception thrown when a rule declares an input that does not exist on
+/// disk.
+class MissingInputException implements Exception {
+ const MissingInputException(this.missing, this.target);
+
+ /// The file or directory we expected to find.
+ final List<File> missing;
+
+ /// The name of the target this file should have been output from.
+ final String target;
+
+ @override
+ String toString() {
+ final String files = missing.map((File file) => file.path).join(', ');
+ return '$files were declared as an inputs, but did not exist. '
+ 'Check the definition of target:$target for errors';
+ }
+}
+
+/// An exception thrown if we detect a cycle in the dependencies of a target.
+class CycleException implements Exception {
+ CycleException(this.targets);
+
+ final Set<Target> targets;
+
+ @override
+ String toString() => 'Dependency cycle detected in build: '
+ '${targets.map((Target target) => target.name).join(' -> ')}';
+}
+
+/// An exception thrown when a pattern is invalid.
+class InvalidPatternException implements Exception {
+ InvalidPatternException(this.pattern);
+
+ final String pattern;
+
+ @override
+ String toString() => 'The pattern "$pattern" is not valid';
+}
+
+/// An exception thrown when a rule declares an output that was not produced
+/// by the invocation.
+class MissingOutputException implements Exception {
+ const MissingOutputException(this.missing, this.target);
+
+ /// The files we expected to find.
+ final List<File> missing;
+
+ /// The name of the target this file should have been output from.
+ final String target;
+
+ @override
+ String toString() {
+ final String files = missing.map((File file) => file.path).join(', ');
+ return '$files were declared as outputs, but were not generated by '
+ 'the action. Check the definition of target:$target for errors';
+ }
+}
+
+/// An exception thrown when in output is placed outside of
+/// [Environment.buildDir].
+class MisplacedOutputException implements Exception {
+ MisplacedOutputException(this.path, this.target);
+
+ final String path;
+ final String target;
+
+ @override
+ String toString() {
+ return 'Target $target produced an output at $path'
+ ' which is outside of the current build or project directory';
+ }
+}
+
+/// An exception thrown if a build action is missing a required define.
+class MissingDefineException implements Exception {
+ MissingDefineException(this.define, this.target);
+
+ final String define;
+ final String target;
+
+ @override
+ String toString() {
+ return 'Target $target required define $define '
+ 'but it was not provided';
+ }
+}
diff --git a/packages/flutter_tools/lib/src/build_system/file_hash_store.dart b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart
new file mode 100644
index 0000000..398f95f
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/file_hash_store.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 'dart:async';
+import 'dart:collection';
+import 'dart:typed_data';
+
+import 'package:crypto/crypto.dart';
+
+import '../base/file_system.dart';
+import '../globals.dart';
+import 'build_system.dart';
+import 'filecache.pb.dart' as pb;
+
+/// A globally accessible cache of file hashes.
+///
+/// In cases where multiple targets read the same source files as inputs, we
+/// avoid recomputing or storing multiple copies of hashes by delegating
+/// through this class. All file hashes are held in memory during a build
+/// operation, and persisted to cache in the root build directory.
+///
+/// The format of the file store is subject to change and not part of its API.
+///
+/// To regenerate the protobuf entries used to construct the cache:
+/// 1. If not already installed, https://developers.google.com/protocol-buffers/docs/downloads
+/// 2. pub global active `protoc-gen-dart`
+/// 3. protoc -I=lib/src/build_system/ --dart_out=lib/src/build_system/ lib/src/build_system/filecache.proto
+/// 4. Add licenses headers to the newly generated file and check-in.
+///
+/// See also: https://developers.google.com/protocol-buffers/docs/darttutorial
+// TODO(jonahwilliams): find a better way to clear out old entries, perhaps
+// track the last access or modification date?
+class FileHashStore {
+ FileHashStore(this.environment);
+
+ final Environment environment;
+ final HashMap<String, String> previousHashes = HashMap<String, String>();
+ final HashMap<String, String> currentHashes = HashMap<String, String>();
+
+ // The name of the file which stores the file hashes.
+ static const String _kFileCache = '.filecache';
+
+ // The current version of the file cache storage format.
+ static const int _kVersion = 1;
+
+ /// Read file hashes from disk.
+ void initialize() {
+ printTrace('Initializing file store');
+ if (!_cacheFile.existsSync()) {
+ return;
+ }
+ final List<int> data = _cacheFile.readAsBytesSync();
+ final pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(data);
+ if (fileStorage.version != _kVersion) {
+ _cacheFile.deleteSync();
+ return;
+ }
+ for (pb.FileHash fileHash in fileStorage.files) {
+ previousHashes[fileHash.path] = fileHash.hash;
+ }
+ printTrace('Done initializing file store');
+ }
+
+ /// Persist file hashes to disk.
+ void persist() {
+ printTrace('Persisting file store');
+ final pb.FileStorage fileStorage = pb.FileStorage();
+ fileStorage.version = _kVersion;
+ final File file = _cacheFile;
+ if (!file.existsSync()) {
+ file.createSync();
+ }
+ for (MapEntry<String, String> entry in currentHashes.entries) {
+ previousHashes[entry.key] = entry.value;
+ }
+ for (MapEntry<String, String> entry in previousHashes.entries) {
+ final pb.FileHash fileHash = pb.FileHash();
+ fileHash.path = entry.key;
+ fileHash.hash = entry.value;
+ fileStorage.files.add(fileHash);
+ }
+ final Uint8List buffer = fileStorage.writeToBuffer();
+ file.writeAsBytesSync(buffer);
+ printTrace('Done persisting file store');
+ }
+
+ /// Computes a hash of the provided files and returns a list of entities
+ /// that were dirty.
+ // TODO(jonahwilliams): compare hash performance with md5 tool on macOS and
+ // linux and certutil on Windows, as well as dividing up computation across
+ // isolates. This also related to the current performance issue with checking
+ // APKs before installing them on device.
+ Future<List<File>> hashFiles(List<File> files) async {
+ final List<File> dirty = <File>[];
+ for (File file in files) {
+ final String absolutePath = file.resolveSymbolicLinksSync();
+ final String previousHash = previousHashes[absolutePath];
+ final List<int> bytes = file.readAsBytesSync();
+ final String currentHash = md5.convert(bytes).toString();
+
+ if (currentHash != previousHash) {
+ dirty.add(file);
+ }
+ currentHashes[absolutePath] = currentHash;
+ }
+ return dirty;
+ }
+
+ File get _cacheFile => environment.rootBuildDir.childFile(_kFileCache);
+}
diff --git a/packages/flutter_tools/lib/src/build_system/filecache.pb.dart b/packages/flutter_tools/lib/src/build_system/filecache.pb.dart
new file mode 100644
index 0000000..4c095ac
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/filecache.pb.dart
@@ -0,0 +1,96 @@
+// 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.
+
+///
+// Generated code. Do not modify.
+// source: lib/src/build_system/filecache.proto
+///
+// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name, sort_constructors_first
+
+import 'dart:core' as $core show bool, Deprecated, double, int, List, Map, override, pragma, String, dynamic;
+
+import 'package:protobuf/protobuf.dart' as $pb;
+
+class FileHash extends $pb.GeneratedMessage {
+ factory FileHash() => create();
+
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo('FileHash', package: const $pb.PackageName('flutter_tools'))
+ ..aOS(1, 'path')
+ ..aOS(2, 'hash')
+ ..hasRequiredFields = false;
+
+ FileHash._() : super();
+
+ factory FileHash.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+
+ factory FileHash.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+ @$core.override
+ FileHash clone() => FileHash()..mergeFromMessage(this);
+
+ @$core.override
+ FileHash copyWith(void Function(FileHash) updates) => super.copyWith(($core.dynamic message) => updates(message as FileHash));
+
+ @$core.override
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static FileHash create() => FileHash._();
+
+ @$core.override
+ FileHash createEmptyInstance() => create();
+ static $pb.PbList<FileHash> createRepeated() => $pb.PbList<FileHash>();
+ static FileHash getDefault() => _defaultInstance ??= create()..freeze();
+ static FileHash _defaultInstance;
+
+ $core.String get path => $_getS(0, '');
+ set path($core.String v) { $_setString(0, v); }
+ $core.bool hasPath() => $_has(0);
+ void clearPath() => clearField(1);
+
+ $core.String get hash => $_getS(1, '');
+ set hash($core.String v) { $_setString(1, v); }
+ $core.bool hasHash() => $_has(1);
+ void clearHash() => clearField(2);
+}
+
+class FileStorage extends $pb.GeneratedMessage {
+ factory FileStorage() => create();
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo('FileHashStore', package: const $pb.PackageName('flutter_tools'))
+ ..a<$core.int>(1, 'version', $pb.PbFieldType.O3)
+ ..pc<FileHash>(2, 'files', $pb.PbFieldType.PM,FileHash.create)
+ ..hasRequiredFields = false;
+
+ FileStorage._() : super();
+ factory FileStorage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+ factory FileStorage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+ @$core.override
+ FileStorage clone() => FileStorage()..mergeFromMessage(this);
+
+ @$core.override
+ FileStorage copyWith(void Function(FileStorage) updates) => super.copyWith(($core.dynamic message) => updates(message as FileStorage));
+
+ @$core.override
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static FileStorage create() => FileStorage._();
+
+ @$core.override
+ FileStorage createEmptyInstance() => create();
+
+ static $pb.PbList<FileStorage> createRepeated() => $pb.PbList<FileStorage>();
+
+ static FileStorage getDefault() => _defaultInstance ??= create()..freeze();
+
+ static FileStorage _defaultInstance;
+
+ $core.int get version => $_get(0, 0);
+ set version($core.int v) { $_setSignedInt32(0, v); }
+ $core.bool hasVersion() => $_has(0);
+ void clearVersion() => clearField(1);
+
+ $core.List<FileHash> get files => $_getList(1);
+}
diff --git a/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart b/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart
new file mode 100644
index 0000000..32f6f9e
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart
@@ -0,0 +1,25 @@
+// 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.
+
+///
+// Generated code. Do not modify.
+// source: lib/src/build_system/filecache.proto
+///
+// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name
+
+const Map<String, Object> FileHash$json = <String, Object>{
+ '1': 'FileHash',
+ '2': <Map<String, Object>>[
+ <String, Object>{'1': 'path', '3': 1, '4': 1, '5': 9, '10': 'path'},
+ <String, Object>{'1': 'hash', '3': 2, '4': 1, '5': 9, '10': 'hash'},
+ ],
+};
+
+const Map<String, Object> FileStorage$json = <String, Object>{
+ '1': 'FileHashStore',
+ '2': <Map<String, Object>>[
+ <String, Object>{'1': 'version', '3': 1, '4': 1, '5': 5, '10': 'version'},
+ <String, Object>{'1': 'files', '3': 2, '4': 3, '5': 11, '6': '.flutter_tools.FileHash', '10': 'files'},
+ ],
+};
diff --git a/packages/flutter_tools/lib/src/build_system/filecache.proto b/packages/flutter_tools/lib/src/build_system/filecache.proto
new file mode 100644
index 0000000..63be878
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/filecache.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+package flutter_tools;
+
+message FileHash {
+ // The absolute path to the file on disk.
+ string path = 1;
+
+ // The last computed file hash.
+ string hash = 2;
+}
+
+message FileStorage {
+ // The current version of the file store.
+ int32 version = 1;
+
+ // All currently stored files.
+ repeated FileHash files = 2;
+}
diff --git a/packages/flutter_tools/lib/src/build_system/source.dart b/packages/flutter_tools/lib/src/build_system/source.dart
new file mode 100644
index 0000000..e9f1e16
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/source.dart
@@ -0,0 +1,228 @@
+// 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 '../artifacts.dart';
+import '../base/file_system.dart';
+import '../build_info.dart';
+import '../globals.dart';
+import 'build_system.dart';
+import 'exceptions.dart';
+
+/// An input function produces a list of additional input files for an
+/// [Environment].
+typedef InputFunction = List<File> Function(Environment environment);
+
+/// Collects sources for a [Target] into a single list of [FileSystemEntities].
+class SourceVisitor {
+ /// Create a new [SourceVisitor] from an [Environment].
+ SourceVisitor(this.environment, [this.inputs = true]);
+
+ /// The current environment.
+ final Environment environment;
+
+ /// Whether we are visiting inputs or outputs.
+ ///
+ /// Defaults to `true`.
+ final bool inputs;
+
+ /// The entities are populated after visiting each source.
+ final List<File> sources = <File>[];
+
+ /// Visit a [Source] which contains a function.
+ ///
+ /// The function is expected to produce a list of [FileSystemEntities]s.
+ void visitFunction(InputFunction function) {
+ sources.addAll(function(environment));
+ }
+
+ /// Visit a [Source] which contains a file uri.
+ ///
+ /// The uri may that may include constants defined in an [Environment].
+ void visitPattern(String pattern) {
+ // perform substitution of the environmental values and then
+ // of the local values.
+ final List<String> segments = <String>[];
+ final List<String> rawParts = pattern.split('/');
+ final bool hasWildcard = rawParts.last.contains('*');
+ String wildcardFile;
+ if (hasWildcard) {
+ wildcardFile = rawParts.removeLast();
+ }
+ // If the pattern does not start with an env variable, then we have nothing
+ // to resolve it to, error out.
+ switch (rawParts.first) {
+ case Environment.kProjectDirectory:
+ segments.addAll(
+ fs.path.split(environment.projectDir.resolveSymbolicLinksSync()));
+ break;
+ case Environment.kBuildDirectory:
+ segments.addAll(fs.path.split(
+ environment.buildDir.resolveSymbolicLinksSync()));
+ break;
+ case Environment.kCacheDirectory:
+ segments.addAll(
+ fs.path.split(environment.cacheDir.resolveSymbolicLinksSync()));
+ break;
+ case Environment.kFlutterRootDirectory:
+ segments.addAll(
+ fs.path.split(environment.cacheDir.resolveSymbolicLinksSync()));
+ break;
+ default:
+ throw InvalidPatternException(pattern);
+ }
+ rawParts.skip(1).forEach(segments.add);
+ final String filePath = fs.path.joinAll(segments);
+ if (hasWildcard) {
+ // Perform a simple match by splitting the wildcard containing file one
+ // the `*`. For example, for `/*.dart`, we get [.dart]. We then check
+ // that part of the file matches. If there are values before and after
+ // the `*` we need to check that both match without overlapping. For
+ // example, `foo_*_.dart`. We want to match `foo_b_.dart` but not
+ // `foo_.dart`. To do so, we first subtract the first section from the
+ // string if the first segment matches.
+ final List<String> segments = wildcardFile.split('*');
+ if (segments.length > 2) {
+ throw InvalidPatternException(pattern);
+ }
+ if (!fs.directory(filePath).existsSync()) {
+ throw Exception('$filePath does not exist!');
+ }
+ for (FileSystemEntity entity in fs.directory(filePath).listSync()) {
+ final String filename = fs.path.basename(entity.path);
+ if (segments.isEmpty) {
+ sources.add(fs.file(entity.absolute));
+ } else if (segments.length == 1) {
+ if (filename.startsWith(segments[0]) ||
+ filename.endsWith(segments[0])) {
+ sources.add(entity.absolute);
+ }
+ } else if (filename.startsWith(segments[0])) {
+ if (filename.substring(segments[0].length).endsWith(segments[1])) {
+ sources.add(entity.absolute);
+ }
+ }
+ }
+ } else {
+ sources.add(fs.file(fs.path.normalize(filePath)));
+ }
+ }
+
+ /// Visit a [Source] which contains a [SourceBehavior].
+ void visitBehavior(SourceBehavior sourceBehavior) {
+ if (inputs) {
+ sources.addAll(sourceBehavior.inputs(environment));
+ } else {
+ sources.addAll(sourceBehavior.outputs(environment));
+ }
+ }
+
+ /// Visit a [Source] which is defined by an [Artifact] from the flutter cache.
+ ///
+ /// If the [Artifact] points to a directory then all child files are included.
+ void visitArtifact(Artifact artifact, TargetPlatform platform, BuildMode mode) {
+ final String path = artifacts.getArtifactPath(artifact, platform: platform, mode: mode);
+ if (fs.isDirectorySync(path)) {
+ sources.addAll(<File>[
+ for (FileSystemEntity entity in fs.directory(path).listSync(recursive: true))
+ if (entity is File)
+ entity
+ ]);
+ } else {
+ sources.add(fs.file(path));
+ }
+ }
+}
+
+/// A description of an input or output of a [Target].
+abstract class Source {
+ /// This source is a file-uri which contains some references to magic
+ /// environment variables.
+ const factory Source.pattern(String pattern) = _PatternSource;
+
+ /// This source is produced by invoking the provided function.
+ const factory Source.function(InputFunction function) = _FunctionSource;
+
+ /// This source is produced by the [SourceBehavior] class.
+ const factory Source.behavior(SourceBehavior behavior) = _SourceBehavior;
+
+ /// The source is provided by an [Artifact].
+ ///
+ /// If [artifact] points to a directory then all child files are included.
+ const factory Source.artifact(Artifact artifact, {TargetPlatform platform,
+ BuildMode mode}) = _ArtifactSource;
+
+ /// Visit the particular source type.
+ void accept(SourceVisitor visitor);
+
+ /// Whether the output source provided can be known before executing the rule.
+ ///
+ /// This does not apply to inputs, which are always explicit and must be
+ /// evaluated before the build.
+ ///
+ /// For example, [Source.pattern] and [Source.version] are not implicit
+ /// provided they do not use any wildcards. [Source.behavior] and
+ /// [Source.function] are always implicit.
+ bool get implicit;
+}
+
+/// An interface for describing input and output copies together.
+abstract class SourceBehavior {
+ const SourceBehavior();
+
+ /// The inputs for a particular target.
+ List<File> inputs(Environment environment);
+
+ /// The outputs for a particular target.
+ List<File> outputs(Environment environment);
+}
+
+class _SourceBehavior implements Source {
+ const _SourceBehavior(this.value);
+
+ final SourceBehavior value;
+
+ @override
+ void accept(SourceVisitor visitor) => visitor.visitBehavior(value);
+
+ @override
+ bool get implicit => true;
+}
+
+class _FunctionSource implements Source {
+ const _FunctionSource(this.value);
+
+ final InputFunction value;
+
+ @override
+ void accept(SourceVisitor visitor) => visitor.visitFunction(value);
+
+ @override
+ bool get implicit => true;
+}
+
+class _PatternSource implements Source {
+ const _PatternSource(this.value);
+
+ final String value;
+
+ @override
+ void accept(SourceVisitor visitor) => visitor.visitPattern(value);
+
+ @override
+ bool get implicit => value.contains('*');
+}
+
+class _ArtifactSource implements Source {
+ const _ArtifactSource(this.artifact, { this.platform, this.mode });
+
+ final Artifact artifact;
+ final TargetPlatform platform;
+ final BuildMode mode;
+
+ @override
+ void accept(SourceVisitor visitor) => visitor.visitArtifact(artifact, platform, mode);
+
+ @override
+ bool get implicit => false;
+}
diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart
new file mode 100644
index 0000000..e5545db
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart
@@ -0,0 +1,95 @@
+// 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:pool/pool.dart';
+
+import '../../asset.dart';
+import '../../base/file_system.dart';
+import '../../devfs.dart';
+import '../build_system.dart';
+
+/// The copying logic for flutter assets.
+// TODO(jonahwilliams): combine the asset bundle logic with this rule so that
+// we can compute the key for deleted assets. This is required to remove assets
+// from build directories that are no longer part of the manifest and to unify
+// the update/diff logic.
+class AssetBehavior extends SourceBehavior {
+ const AssetBehavior();
+
+ @override
+ List<File> inputs(Environment environment) {
+ final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+ assetBundle.build(
+ manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
+ packagesPath: environment.projectDir.childFile('.packages').path,
+ );
+ final List<File> results = <File>[];
+ final Iterable<DevFSFileContent> files = assetBundle.entries.values.whereType<DevFSFileContent>();
+ for (DevFSFileContent devFsContent in files) {
+ results.add(fs.file(devFsContent.file.path));
+ }
+ return results;
+ }
+
+ @override
+ List<File> outputs(Environment environment) {
+ final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+ assetBundle.build(
+ manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
+ packagesPath: environment.projectDir.childFile('.packages').path,
+ );
+ final List<File> results = <File>[];
+ for (MapEntry<String, DevFSContent> entry in assetBundle.entries.entries) {
+ final File file = fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', entry.key));
+ results.add(file);
+ }
+ return results;
+ }
+}
+
+/// Copies the asset files from the [copyAssets] rule into place.
+Future<void> copyAssetsInvocation(Map<String, ChangeType> updates, Environment environment) async {
+ final Directory output = environment
+ .buildDir
+ .childDirectory('flutter_assets');
+ if (output.existsSync()) {
+ output.deleteSync(recursive: true);
+ }
+ output.createSync(recursive: true);
+ final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+ await assetBundle.build(
+ manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
+ packagesPath: environment.projectDir.childFile('.packages').path,
+ );
+ // Limit number of open files to avoid running out of file descriptors.
+ final Pool pool = Pool(64);
+ await Future.wait<void>(
+ assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
+ final PoolResource resource = await pool.request();
+ try {
+ final File file = fs.file(fs.path.join(output.path, entry.key));
+ file.parent.createSync(recursive: true);
+ await file.writeAsBytes(await entry.value.contentsAsBytes());
+ } finally {
+ resource.release();
+ }
+ }));
+}
+
+/// Copy the assets used in the application into a build directory.
+const Target copyAssets = Target(
+ name: 'copy_assets',
+ inputs: <Source>[
+ Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
+ Source.behavior(AssetBehavior()),
+ ],
+ outputs: <Source>[
+ Source.pattern('{BUILD_DIR}/flutter_assets/AssetManifest.json'),
+ Source.pattern('{BUILD_DIR}/flutter_assets/FontManifest.json'),
+ Source.pattern('{BUILD_DIR}/flutter_assets/LICENSE'),
+ Source.behavior(AssetBehavior()), // <- everything in this subdirectory.
+ ],
+ dependencies: <Target>[],
+ buildAction: copyAssetsInvocation,
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
new file mode 100644
index 0000000..f7d17f6
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
@@ -0,0 +1,283 @@
+// 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 '../../artifacts.dart';
+import '../../base/build.dart';
+import '../../base/file_system.dart';
+import '../../base/io.dart';
+import '../../base/platform.dart';
+import '../../base/process_manager.dart';
+import '../../build_info.dart';
+import '../../compile.dart';
+import '../../dart/package_map.dart';
+import '../../globals.dart';
+import '../../project.dart';
+import '../build_system.dart';
+import '../exceptions.dart';
+
+/// The define to pass a [BuildMode].
+const String kBuildMode= 'BuildMode';
+
+/// The define to pass whether we compile 64-bit android-arm code.
+const String kTargetPlatform = 'TargetPlatform';
+
+/// The define to control what target file is used.
+const String kTargetFile = 'TargetFile';
+
+/// The define to control what iOS architectures are built for.
+///
+/// This is expected to be a comma-separated list of architectures. If not
+/// provided, defaults to arm64.
+///
+/// The other supported value is armv7, the 32-bit iOS architecture.
+const String kIosArchs = 'IosArchs';
+
+/// Supports compiling dart source to kernel with a subset of flags.
+///
+/// This is a non-incremental compile so the specific [updates] are ignored.
+Future<void> compileKernel(Map<String, ChangeType> updates, Environment environment) async {
+ final KernelCompiler compiler = await kernelCompilerFactory.create(
+ FlutterProject.fromDirectory(environment.projectDir),
+ );
+ if (environment.defines[kBuildMode] == null) {
+ throw MissingDefineException(kBuildMode, 'kernel_snapshot');
+ }
+ final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
+ final String targetFile = environment.defines[kTargetFile] ?? fs.path.join('lib', 'main.dart');
+
+ final CompilerOutput output = await compiler.compile(
+ sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
+ aot: buildMode != BuildMode.debug,
+ trackWidgetCreation: false,
+ targetModel: TargetModel.flutter,
+ targetProductVm: buildMode == BuildMode.release,
+ outputFilePath: environment
+ .buildDir
+ .childFile('main.app.dill')
+ .path,
+ depFilePath: null,
+ mainPath: targetFile,
+ );
+ if (output.errorCount != 0) {
+ throw Exception('Errors during snapshot creation: $output');
+ }
+}
+
+/// Supports compiling a dart kernel file to an ELF binary.
+Future<void> compileAotElf(Map<String, ChangeType> updates, Environment environment) async {
+ final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
+ final String outputPath = environment.buildDir.path;
+ if (environment.defines[kBuildMode] == null) {
+ throw MissingDefineException(kBuildMode, 'aot_elf');
+ }
+ if (environment.defines[kTargetPlatform] == null) {
+ throw MissingDefineException(kTargetPlatform, 'aot_elf');
+ }
+ final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
+ final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
+ final int snapshotExitCode = await snapshotter.build(
+ platform: targetPlatform,
+ buildMode: buildMode,
+ mainPath: environment.buildDir.childFile('main.app.dill').path,
+ packagesPath: environment.projectDir.childFile('.packages').path,
+ outputPath: outputPath,
+ );
+ if (snapshotExitCode != 0) {
+ throw Exception('AOT snapshotter exited with code $snapshotExitCode');
+ }
+}
+
+/// Finds the locations of all dart files within the project.
+///
+/// This does not attempt to determine if a file is used or imported, so it
+/// may otherwise report more files than strictly necessary.
+List<File> listDartSources(Environment environment) {
+ final Map<String, Uri> packageMap = PackageMap(environment.projectDir.childFile('.packages').path).map;
+ final List<File> dartFiles = <File>[];
+ for (Uri uri in packageMap.values) {
+ final Directory libDirectory = fs.directory(uri.toFilePath(windows: platform.isWindows));
+ for (FileSystemEntity entity in libDirectory.listSync(recursive: true)) {
+ if (entity is File && entity.path.endsWith('.dart')) {
+ dartFiles.add(entity);
+ }
+ }
+ }
+ return dartFiles;
+}
+
+/// Supports compiling a dart kernel file to an assembly file.
+///
+/// If more than one iOS arch is provided, then this rule will
+/// produce a univeral binary.
+Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment environment) async {
+ final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
+ final String outputPath = environment.buildDir.path;
+ if (environment.defines[kBuildMode] == null) {
+ throw MissingDefineException(kBuildMode, 'aot_assembly');
+ }
+ if (environment.defines[kTargetPlatform] == null) {
+ throw MissingDefineException(kTargetPlatform, 'aot_assembly');
+ }
+ final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
+ final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
+ final List<IOSArch> iosArchs = environment.defines[kIosArchs]?.split(',')?.map(getIOSArchForName)?.toList()
+ ?? <IOSArch>[IOSArch.arm64];
+ if (targetPlatform != TargetPlatform.ios) {
+ throw Exception('aot_assembly is only supported for iOS applications');
+ }
+
+ // If we're building for a single architecture (common), then skip the lipo.
+ if (iosArchs.length == 1) {
+ final int snapshotExitCode = await snapshotter.build(
+ platform: targetPlatform,
+ buildMode: buildMode,
+ mainPath: environment.buildDir.childFile('main.app.dill').path,
+ packagesPath: environment.projectDir.childFile('.packages').path,
+ outputPath: outputPath,
+ iosArch: iosArchs.single,
+ );
+ if (snapshotExitCode != 0) {
+ throw Exception('AOT snapshotter exited with code $snapshotExitCode');
+ }
+ } else {
+ // If we're building multiple iOS archs the binaries need to be lipo'd
+ // together.
+ final List<Future<int>> pending = <Future<int>>[];
+ for (IOSArch iosArch in iosArchs) {
+ pending.add(snapshotter.build(
+ platform: targetPlatform,
+ buildMode: buildMode,
+ mainPath: environment.buildDir.childFile('main.app.dill').path,
+ packagesPath: environment.projectDir.childFile('.packages').path,
+ outputPath: fs.path.join(outputPath, getNameForIOSArch(iosArch)),
+ iosArch: iosArch,
+ ));
+ }
+ final List<int> results = await Future.wait(pending);
+ if (results.any((int result) => result != 0)) {
+ throw Exception('AOT snapshotter exited with code ${results.join()}');
+ }
+ final ProcessResult result = await processManager.run(<String>[
+ 'lipo',
+ ...iosArchs.map((IOSArch iosArch) =>
+ fs.path.join(outputPath, getNameForIOSArch(iosArch), 'App.framework', 'App')),
+ '-create',
+ '-output',
+ fs.path.join(outputPath, 'App.framework', 'App'),
+ ]);
+ if (result.exitCode != 0) {
+ throw Exception('lipo exited with code ${result.exitCode}');
+ }
+ }
+}
+
+/// Generate a snapshot of the dart code used in the program.
+const Target kernelSnapshot = Target(
+ name: 'kernel_snapshot',
+ inputs: <Source>[
+ Source.function(listDartSources), // <- every dart file under {PROJECT_DIR}/lib and in .packages
+ Source.artifact(Artifact.platformKernelDill),
+ Source.artifact(Artifact.engineDartBinary),
+ Source.artifact(Artifact.frontendServerSnapshotForEngineDartSdk),
+ ],
+ outputs: <Source>[
+ Source.pattern('{BUILD_DIR}/main.app.dill'),
+ ],
+ dependencies: <Target>[],
+ buildAction: compileKernel,
+);
+
+/// Generate an ELF binary from a dart kernel file in profile mode.
+const Target aotElfProfile = Target(
+ name: 'aot_elf_profile',
+ inputs: <Source>[
+ Source.pattern('{BUILD_DIR}/main.app.dill'),
+ Source.pattern('{PROJECT_DIR}/.packages'),
+ Source.artifact(Artifact.engineDartBinary),
+ Source.artifact(Artifact.skyEnginePath),
+ Source.artifact(Artifact.genSnapshot,
+ platform: TargetPlatform.android_arm,
+ mode: BuildMode.profile,
+ ),
+ ],
+ outputs: <Source>[
+ Source.pattern('{BUILD_DIR}/app.so'),
+ ],
+ dependencies: <Target>[
+ kernelSnapshot,
+ ],
+ buildAction: compileAotElf,
+);
+
+/// Generate an ELF binary from a dart kernel file in release mode.
+const Target aotElfRelease= Target(
+ name: 'aot_elf_release',
+ inputs: <Source>[
+ Source.pattern('{BUILD_DIR}/main.app.dill'),
+ Source.pattern('{PROJECT_DIR}/.packages'),
+ Source.artifact(Artifact.engineDartBinary),
+ Source.artifact(Artifact.skyEnginePath),
+ Source.artifact(Artifact.genSnapshot,
+ platform: TargetPlatform.android_arm,
+ mode: BuildMode.release,
+ ),
+ ],
+ outputs: <Source>[
+ Source.pattern('{BUILD_DIR}/app.so'),
+ ],
+ dependencies: <Target>[
+ kernelSnapshot,
+ ],
+ buildAction: compileAotElf,
+);
+
+/// Generate an assembly target from a dart kernel file in profile mode.
+const Target aotAssemblyProfile = Target(
+ name: 'aot_assembly_profile',
+ inputs: <Source>[
+ Source.pattern('{BUILD_DIR}/main.app.dill'),
+ Source.pattern('{PROJECT_DIR}/.packages'),
+ Source.artifact(Artifact.engineDartBinary),
+ Source.artifact(Artifact.skyEnginePath),
+ Source.artifact(Artifact.genSnapshot,
+ platform: TargetPlatform.ios,
+ mode: BuildMode.profile,
+ ),
+ ],
+ outputs: <Source>[
+ // TODO(jonahwilliams): are these used or just a side effect?
+ // Source.pattern('{BUILD_DIR}/snapshot_assembly.S'),
+ // Source.pattern('{BUILD_DIR}/snapshot_assembly.o'),
+ Source.pattern('{BUILD_DIR}/App.framework/App'),
+ ],
+ dependencies: <Target>[
+ kernelSnapshot,
+ ],
+ buildAction: compileAotAssembly,
+);
+
+/// Generate an assembly target from a dart kernel file in release mode.
+const Target aotAssemblyRelease = Target(
+ name: 'aot_assembly_release',
+ inputs: <Source>[
+ Source.pattern('{BUILD_DIR}/main.app.dill'),
+ Source.pattern('{PROJECT_DIR}/.packages'),
+ Source.artifact(Artifact.engineDartBinary),
+ Source.artifact(Artifact.skyEnginePath),
+ Source.artifact(Artifact.genSnapshot,
+ platform: TargetPlatform.ios,
+ mode: BuildMode.release,
+ ),
+ ],
+ outputs: <Source>[
+ // TODO(jonahwilliams): are these used or just a side effect?
+ // Source.pattern('{BUILD_DIR}/snapshot_assembly.S'),
+ // Source.pattern('{BUILD_DIR}/snapshot_assembly.o'),
+ Source.pattern('{BUILD_DIR}/App.framework/App'),
+ ],
+ dependencies: <Target>[
+ kernelSnapshot,
+ ],
+ buildAction: compileAotAssembly,
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
new file mode 100644
index 0000000..19e38ea
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
@@ -0,0 +1,43 @@
+// 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 '../build_system.dart';
+import 'assets.dart';
+import 'dart.dart';
+
+/// Create an iOS debug application.
+const Target debugIosApplication = Target(
+ name: 'debug_ios_application',
+ buildAction: null,
+ inputs: <Source>[],
+ outputs: <Source>[],
+ dependencies: <Target>[
+ copyAssets,
+ kernelSnapshot,
+ ]
+);
+
+/// Create an iOS profile application.
+const Target profileIosApplication = Target(
+ name: 'profile_ios_application',
+ buildAction: null,
+ inputs: <Source>[],
+ outputs: <Source>[],
+ dependencies: <Target>[
+ copyAssets,
+ aotAssemblyProfile,
+ ]
+);
+
+/// Create an iOS debug application.
+const Target releaseIosApplication = Target(
+ name: 'release_ios_application',
+ buildAction: null,
+ inputs: <Source>[],
+ outputs: <Source>[],
+ dependencies: <Target>[
+ copyAssets,
+ aotAssemblyRelease,
+ ]
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/linux.dart b/packages/flutter_tools/lib/src/build_system/targets/linux.dart
new file mode 100644
index 0000000..c5ad64d
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/linux.dart
@@ -0,0 +1,46 @@
+// 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 '../../artifacts.dart';
+import '../../base/file_system.dart';
+import '../../globals.dart';
+import '../build_system.dart';
+
+// Copies all of the input files to the correct copy dir.
+Future<void> copyLinuxAssets(Map<String, ChangeType> updates,
+ Environment environment) async {
+ final String basePath = artifacts.getArtifactPath(Artifact.linuxDesktopPath);
+ for (String input in updates.keys) {
+ final String outputPath = fs.path.join(
+ environment.projectDir.path,
+ 'linux',
+ 'flutter',
+ fs.path.relative(input, from: basePath),
+ );
+ final File destinationFile = fs.file(outputPath);
+ if (!destinationFile.parent.existsSync()) {
+ destinationFile.parent.createSync(recursive: true);
+ }
+ fs.file(input).copySync(destinationFile.path);
+ }
+}
+
+/// Copies the Linux desktop embedding files to the copy directory.
+const Target unpackLinux = Target(
+ name: 'unpack_linux',
+ inputs: <Source>[
+ Source.artifact(Artifact.linuxDesktopPath),
+ ],
+ outputs: <Source>[
+ Source.pattern('{PROJECT_DIR}/linux/flutter/libflutter_linux.so'),
+ Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_export.h'),
+ Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_messenger.h'),
+ Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_plugin_registrar.h'),
+ Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_glfw.h'),
+ Source.pattern('{PROJECT_DIR}/linux/flutter/icudtl.dat'),
+ Source.pattern('{PROJECT_DIR}/linux/flutter/cpp_client_wrapper/*'),
+ ],
+ dependencies: <Target>[],
+ buildAction: copyLinuxAssets,
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
new file mode 100644
index 0000000..7bfb923
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
@@ -0,0 +1,100 @@
+// 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 '../../artifacts.dart';
+import '../../base/file_system.dart';
+import '../../base/io.dart';
+import '../../base/process_manager.dart';
+import '../../globals.dart';
+import '../build_system.dart';
+import 'assets.dart';
+import 'dart.dart';
+
+/// Copy the macOS framework to the correct copy dir by invoking 'cp -R'.
+///
+/// The shelling out is done to avoid complications with preserving special
+/// files (e.g., symbolic links) in the framework structure.
+///
+/// Removes any previous version of the framework that already exists in the
+/// target directory.
+// TODO(jonahwilliams): remove shell out.
+Future<void> copyFramework(Map<String, ChangeType> updates,
+ Environment environment) async {
+ final String basePath = artifacts.getArtifactPath(Artifact.flutterMacOSFramework);
+ final Directory targetDirectory = environment
+ .projectDir
+ .childDirectory('macos')
+ .childDirectory('Flutter')
+ .childDirectory('FlutterMacOS.framework');
+ if (targetDirectory.existsSync()) {
+ targetDirectory.deleteSync(recursive: true);
+ }
+
+ final ProcessResult result = processManager
+ .runSync(<String>['cp', '-R', basePath, targetDirectory.path]);
+ if (result.exitCode != 0) {
+ throw Exception(
+ 'Failed to copy framework (exit ${result.exitCode}:\n'
+ '${result.stdout}\n---\n${result.stderr}',
+ );
+ }
+}
+
+const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/FlutterMacOS.framework';
+
+/// Copies the macOS desktop framework to the copy directory.
+const Target unpackMacos = Target(
+ name: 'unpack_macos',
+ inputs: <Source>[
+ Source.artifact(Artifact.flutterMacOSFramework),
+ ],
+ outputs: <Source>[
+ Source.pattern('$_kOutputPrefix/FlutterMacOS'),
+ // Headers
+ Source.pattern('$_kOutputPrefix/Headers/FLEOpenGLContextHandling.h'),
+ Source.pattern('$_kOutputPrefix/Headers/FLEReshapeListener.h'),
+ Source.pattern('$_kOutputPrefix/Headers/FLEView.h'),
+ Source.pattern('$_kOutputPrefix/Headers/FLEViewController.h'),
+ Source.pattern('$_kOutputPrefix/Headers/FlutterBinaryMessenger.h'),
+ Source.pattern('$_kOutputPrefix/Headers/FlutterChannels.h'),
+ Source.pattern('$_kOutputPrefix/Headers/FlutterCodecs.h'),
+ Source.pattern('$_kOutputPrefix/Headers/FlutterMacOS.h'),
+ Source.pattern('$_kOutputPrefix/Headers/FlutterPluginMacOS.h'),
+ Source.pattern('$_kOutputPrefix/Headers/FlutterPluginRegistrarMacOS.h'),
+ // Modules
+ Source.pattern('$_kOutputPrefix/Modules/module.modulemap'),
+ // Resources
+ Source.pattern('$_kOutputPrefix/Resources/icudtl.dat'),
+ Source.pattern('$_kOutputPrefix/Resources/info.plist'),
+ // Ignore Versions folder for now
+ ],
+ dependencies: <Target>[],
+ buildAction: copyFramework,
+);
+
+/// Build a macOS application.
+const Target macosApplication = Target(
+ name: 'debug_macos_application',
+ buildAction: null,
+ inputs: <Source>[],
+ outputs: <Source>[],
+ dependencies: <Target>[
+ unpackMacos,
+ kernelSnapshot,
+ copyAssets,
+ ]
+);
+
+/// Build a macOS release application.
+const Target macoReleaseApplication = Target(
+ name: 'release_macos_application',
+ buildAction: null,
+ inputs: <Source>[],
+ outputs: <Source>[],
+ dependencies: <Target>[
+ unpackMacos,
+ aotElfRelease,
+ copyAssets,
+ ]
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/windows.dart b/packages/flutter_tools/lib/src/build_system/targets/windows.dart
new file mode 100644
index 0000000..2677cba
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/windows.dart
@@ -0,0 +1,50 @@
+// 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 '../../artifacts.dart';
+import '../../base/file_system.dart';
+import '../../globals.dart';
+import '../build_system.dart';
+
+/// Copies all of the input files to the correct copy dir.
+Future<void> copyWindowsAssets(Map<String, ChangeType> updates,
+ Environment environment) async {
+ // This path needs to match the prefix in the rule below.
+ final String basePath = artifacts.getArtifactPath(Artifact.windowsDesktopPath);
+ for (String input in updates.keys) {
+ final String outputPath = fs.path.join(
+ environment.projectDir.path,
+ 'windows',
+ 'flutter',
+ fs.path.relative(input, from: basePath),
+ );
+ final File destinationFile = fs.file(outputPath);
+ if (!destinationFile.parent.existsSync()) {
+ destinationFile.parent.createSync(recursive: true);
+ }
+ fs.file(input).copySync(destinationFile.path);
+ }
+}
+
+/// Copies the Windows desktop embedding files to the copy directory.
+const Target unpackWindows = Target(
+ name: 'unpack_windows',
+ inputs: <Source>[
+ Source.artifact(Artifact.windowsDesktopPath),
+ ],
+ outputs: <Source>[
+ Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll'),
+ Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll.exp'),
+ Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll.lib'),
+ Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll.pdb'),
+ Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_export.h'),
+ Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_messenger.h'),
+ Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_plugin_registrar.h'),
+ Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_glfw.h'),
+ Source.pattern('{PROJECT_DIR}/windows/flutter/icudtl.dat'),
+ Source.pattern('{PROJECT_DIR}/windows/flutter/cpp_client_wrapper/*'),
+ ],
+ dependencies: <Target>[],
+ buildAction: copyWindowsAssets,
+);
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart
new file mode 100644
index 0000000..e5a6659
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -0,0 +1,221 @@
+// 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/common.dart';
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../build_info.dart';
+import '../build_system/build_system.dart';
+import '../convert.dart';
+import '../globals.dart';
+import '../project.dart';
+import '../runner/flutter_command.dart';
+
+/// The [BuildSystem] instance.
+BuildSystem get buildSystem => context.get<BuildSystem>();
+
+/// Assemble provides a low level API to interact with the flutter tool build
+/// system.
+class AssembleCommand extends FlutterCommand {
+ AssembleCommand() {
+ addSubcommand(AssembleRun());
+ addSubcommand(AssembleDescribe());
+ addSubcommand(AssembleListInputs());
+ addSubcommand(AssembleBuildDirectory());
+ }
+ @override
+ String get description => 'Assemble and build flutter resources.';
+
+ @override
+ String get name => 'assemble';
+
+ @override
+ bool get isExperimental => true;
+
+ @override
+ Future<FlutterCommandResult> runCommand() {
+ return null;
+ }
+}
+
+abstract class AssembleBase extends FlutterCommand {
+ AssembleBase() {
+ argParser.addMultiOption(
+ 'define',
+ abbr: 'd',
+ help: 'Allows passing configuration to a target with --define=target=key=value.'
+ );
+ argParser.addOption(
+ 'build-mode',
+ allowed: const <String>[
+ 'debug',
+ 'profile',
+ 'release',
+ ],
+ );
+ argParser.addOption(
+ 'resource-pool-size',
+ help: 'The maximum number of concurrent tasks the build system will run.'
+ );
+ }
+
+ /// Returns the provided target platform.
+ ///
+ /// Throws a [ToolExit] if none is provided. This intentionally has no
+ /// default.
+ TargetPlatform get targetPlatform {
+ final String value = argResults['target-platform'] ?? 'darwin-x64';
+ if (value == null) {
+ throwToolExit('--target-platform is required for flutter assemble.');
+ }
+ return getTargetPlatformForName(value);
+ }
+
+ /// Returns the provided build mode.
+ ///
+ /// Throws a [ToolExit] if none is provided. This intentionally has no
+ /// default.
+ BuildMode get buildMode {
+ final String value = argResults['build-mode'] ?? 'debug';
+ if (value == null) {
+ throwToolExit('--build-mode is required for flutter assemble.');
+ }
+ return getBuildModeForName(value);
+ }
+
+ /// The name of the target we are describing or building.
+ String get targetName {
+ if (argResults.rest.isEmpty) {
+ throwToolExit('missing target name for flutter assemble.');
+ }
+ return argResults.rest.first;
+ }
+
+ /// The environmental configuration for a build invocation.
+ Environment get environment {
+ final FlutterProject flutterProject = FlutterProject.current();
+ final Environment result = Environment(
+ buildDir: fs.directory(getBuildDirectory()),
+ projectDir: flutterProject.directory,
+ defines: _parseDefines(argResults['define']),
+ );
+ return result;
+ }
+
+ static Map<String, String> _parseDefines(List<String> values) {
+ final Map<String, String> results = <String, String>{};
+ for (String chunk in values) {
+ final List<String> parts = chunk.split('=');
+ if (parts.length != 2) {
+ throwToolExit('Improperly formatted define flag: $chunk');
+ }
+ final String key = parts[0];
+ final String value = parts[1];
+ results[key] = value;
+ }
+ return results;
+ }
+}
+
+/// Execute a build starting from a target action.
+class AssembleRun extends AssembleBase {
+ @override
+ String get description => 'Execute the stages for a specified target.';
+
+ @override
+ String get name => 'run';
+
+ @override
+ bool get isExperimental => true;
+
+ @override
+ Future<FlutterCommandResult> runCommand() async {
+ final BuildResult result = await buildSystem.build(targetName, environment, BuildSystemConfig(
+ resourcePoolSize: argResults['resource-pool-size'],
+ ));
+ if (!result.success) {
+ for (MapEntry<String, ExceptionMeasurement> data in result.exceptions.entries) {
+ printError('Target ${data.key} failed: ${data.value.exception}');
+ printError('${data.value.exception}');
+ }
+ throwToolExit('build failed');
+ } else {
+ printStatus('build succeeded');
+ }
+ return null;
+ }
+}
+
+/// Fully describe a target and its dependencies.
+class AssembleDescribe extends AssembleBase {
+ @override
+ String get description => 'List the stages for a specified target.';
+
+ @override
+ String get name => 'describe';
+
+ @override
+ bool get isExperimental => true;
+
+ @override
+ Future<FlutterCommandResult> runCommand() {
+ try {
+ printStatus(
+ json.encode(buildSystem.describe(targetName, environment))
+ );
+ } on Exception catch (err, stackTrace) {
+ printTrace(stackTrace.toString());
+ throwToolExit(err.toString());
+ }
+ return null;
+ }
+}
+
+/// List input files for a target.
+class AssembleListInputs extends AssembleBase {
+ @override
+ String get description => 'List the inputs for a particular target.';
+
+ @override
+ String get name => 'inputs';
+
+ @override
+ bool get isExperimental => true;
+
+ @override
+ Future<FlutterCommandResult> runCommand() {
+ try {
+ final List<Map<String, Object>> results = buildSystem.describe(targetName, environment);
+ for (Map<String, Object> result in results) {
+ if (result['name'] == targetName) {
+ final List<String> inputs = result['inputs'];
+ inputs.forEach(printStatus);
+ }
+ }
+ } on Exception catch (err, stackTrace) {
+ printTrace(stackTrace.toString());
+ throwToolExit(err.toString());
+ }
+ return null;
+ }
+}
+
+/// Return the build directory for a configuiration.
+class AssembleBuildDirectory extends AssembleBase {
+ @override
+ String get description => 'List the inputs for a particular target.';
+
+ @override
+ String get name => 'build-dir';
+
+ @override
+ bool get isExperimental => true;
+
+ @override
+ Future<FlutterCommandResult> runCommand() {
+ printStatus(environment.buildDir.path);
+ return null;
+ }
+}
+
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index bc03cbe..533cfc6 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -21,6 +21,7 @@
import 'base/time.dart';
import 'base/user_messages.dart';
import 'base/utils.dart';
+import 'build_system/build_system.dart';
import 'cache.dart';
import 'compile.dart';
import 'devfs.dart';
@@ -67,6 +68,7 @@
Artifacts: () => CachedArtifacts(),
AssetBundleFactory: () => AssetBundleFactory.defaultInstance,
BotDetector: () => const BotDetector(),
+ BuildSystem: () => BuildSystem(),
Cache: () => Cache(),
ChromeLauncher: () => const ChromeLauncher(),
CocoaPods: () => CocoaPods(),