Add initial implementation of flutter assemble (#32816)

This commit is contained in:
Jonah Williams 2019-07-11 16:53:17 -07:00 committed by GitHub
parent fb9ff9296e
commit e91b98a41f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 3473 additions and 32 deletions

View file

@ -14,6 +14,7 @@ import 'src/build_runner/web_compilation_delegate.dart';
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 @@ Future<void> main(List<String> args) async {
await runner.run(args, <FlutterCommand>[
AnalyzeCommand(verboseHelp: verboseHelp),
AssembleCommand(),
AttachCommand(verboseHelp: verboseHelp),
BuildCommand(verboseHelp: verboseHelp),
ChannelCommand(verboseHelp: verboseHelp),

View file

@ -14,10 +14,13 @@ import 'dart/sdk.dart';
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 @@ enum Artifact {
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 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo
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 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo
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 @@ class CachedArtifacts extends Artifacts {
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 @@ class LocalEngineArtifacts extends Artifacts {
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;

View file

@ -9,7 +9,6 @@ import 'package:meta/meta.dart';
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 @@ class AOTSnapshotter {
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 @@ class AOTSnapshotter {
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 @@ class AOTSnapshotter {
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 @@ class AOTSnapshotter {
if (result.exitCode != 0)
return result.exitCode;
}
// Compute and record build fingerprint.
await fingerprinter.writeFingerprint();
return 0;
}

View file

@ -115,6 +115,32 @@ enum BuildMode {
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;

View file

@ -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);
}
}

View file

@ -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';
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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'},
],
};

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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,
);

View file

@ -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,
);

View file

@ -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,
]
);

View file

@ -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,
);

View file

@ -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,
]
);

View file

@ -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,
);

View file

@ -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;
}
}

View file

@ -21,6 +21,7 @@ import 'base/platform.dart';
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 @@ Future<T> runInContext<T>(
Artifacts: () => CachedArtifacts(),
AssetBundleFactory: () => AssetBundleFactory.defaultInstance,
BotDetector: () => const BotDetector(),
BuildSystem: () => BuildSystem(),
Cache: () => Cache(),
ChromeLauncher: () => const ChromeLauncher(),
CocoaPods: () => CocoaPods(),

View file

@ -38,6 +38,7 @@ dependencies:
yaml: 2.1.16
flutter_goldens_client:
path: ../flutter_goldens_client
protobuf: 0.13.15
# We depend on very specific internal implementation details of the
# 'test' package, which change between versions, so when upgrading
@ -83,7 +84,6 @@ dependencies:
pedantic: 1.8.0+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
petitparser: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pool: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
protobuf: 0.13.15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pub_semver: 1.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pubspec_parse: 0.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
scratch_space: 0.0.3+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"

View file

@ -0,0 +1,554 @@
// 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:flutter_tools/src/artifacts.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/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/exceptions.dart';
import 'package:flutter_tools/src/build_system/file_hash_store.dart';
import 'package:flutter_tools/src/build_system/filecache.pb.dart' as pb;
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:mockito/mockito.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/testbed.dart';
void main() {
setUpAll(() {
Cache.disableLocking();
});
group(Target, () {
Testbed testbed;
MockPlatform mockPlatform;
Environment environment;
Target fooTarget;
Target barTarget;
Target fizzTarget;
BuildSystem buildSystem;
int fooInvocations;
int barInvocations;
setUp(() {
fooInvocations = 0;
barInvocations = 0;
mockPlatform = MockPlatform();
// Keep file paths the same.
when(mockPlatform.isWindows).thenReturn(false);
testbed = Testbed(
setup: () {
environment = Environment(
projectDir: fs.currentDirectory,
);
fs.file('foo.dart').createSync(recursive: true);
fs.file('pubspec.yaml').createSync();
fooTarget = Target(
name: 'foo',
inputs: const <Source>[
Source.pattern('{PROJECT_DIR}/foo.dart'),
],
outputs: const <Source>[
Source.pattern('{BUILD_DIR}/out'),
],
dependencies: <Target>[],
buildAction: (Map<String, ChangeType> updates, Environment environment) {
environment
.buildDir
.childFile('out')
..createSync(recursive: true)
..writeAsStringSync('hey');
fooInvocations++;
}
);
barTarget = Target(
name: 'bar',
inputs: const <Source>[
Source.pattern('{BUILD_DIR}/out'),
],
outputs: const <Source>[
Source.pattern('{BUILD_DIR}/bar'),
],
dependencies: <Target>[fooTarget],
buildAction: (Map<String, ChangeType> updates, Environment environment) {
environment.buildDir
.childFile('bar')
..createSync(recursive: true)
..writeAsStringSync('there');
barInvocations++;
}
);
fizzTarget = Target(
name: 'fizz',
inputs: const <Source>[
Source.pattern('{BUILD_DIR}/out'),
],
outputs: const <Source>[
Source.pattern('{BUILD_DIR}/fizz'),
],
dependencies: <Target>[fooTarget],
buildAction: (Map<String, ChangeType> updates, Environment environment) {
throw Exception('something bad happens');
}
);
buildSystem = BuildSystem(<String, Target>{
fooTarget.name: fooTarget,
barTarget.name: barTarget,
fizzTarget.name: fizzTarget,
});
},
overrides: <Type, Generator>{
Platform: () => mockPlatform,
}
);
});
test('can describe build rules', () => testbed.run(() {
expect(buildSystem.describe('foo', environment), <Object>[
<String, Object>{
'name': 'foo',
'dependencies': <String>[],
'inputs': <String>['/foo.dart'],
'outputs': <String>[fs.path.join(environment.buildDir.path, 'out')],
'stamp': fs.path.join(environment.buildDir.path, 'foo.stamp'),
}
]);
}));
test('Throws exception if asked to build non-existent target', () => testbed.run(() {
expect(buildSystem.build('not_real', environment, const BuildSystemConfig()), throwsA(isInstanceOf<Exception>()));
}));
test('Throws exception if asked to build with missing inputs', () => testbed.run(() async {
// Delete required input file.
fs.file('foo.dart').deleteSync();
final BuildResult buildResult = await buildSystem.build('foo', environment, const BuildSystemConfig());
expect(buildResult.hasException, true);
expect(buildResult.exceptions.values.single.exception, isInstanceOf<MissingInputException>());
}));
test('Throws exception if it does not produce a specified output', () => testbed.run(() async {
final Target badTarget = Target
(buildAction: (Map<String, ChangeType> inputs, Environment environment) {},
inputs: const <Source>[
Source.pattern('{PROJECT_DIR}/foo.dart'),
],
outputs: const <Source>[
Source.pattern('{BUILD_DIR}/out')
],
name: 'bad'
);
buildSystem = BuildSystem(<String, Target>{
badTarget.name: badTarget,
});
final BuildResult result = await buildSystem.build('bad', environment, const BuildSystemConfig());
expect(result.hasException, true);
expect(result.exceptions.values.single.exception, isInstanceOf<MissingOutputException>());
}));
test('Saves a stamp file with inputs and outputs', () => testbed.run(() async {
await buildSystem.build('foo', environment, const BuildSystemConfig());
final File stampFile = fs.file(fs.path.join(environment.buildDir.path, 'foo.stamp'));
expect(stampFile.existsSync(), true);
final Map<String, Object> stampContents = json.decode(stampFile.readAsStringSync());
expect(stampContents['inputs'], <Object>['/foo.dart']);
}));
test('Does not re-invoke build if stamp is valid', () => testbed.run(() async {
await buildSystem.build('foo', environment, const BuildSystemConfig());
await buildSystem.build('foo', environment, const BuildSystemConfig());
expect(fooInvocations, 1);
}));
test('Re-invoke build if input is modified', () => testbed.run(() async {
await buildSystem.build('foo', environment, const BuildSystemConfig());
fs.file('foo.dart').writeAsStringSync('new contents');
await buildSystem.build('foo', environment, const BuildSystemConfig());
expect(fooInvocations, 2);
}));
test('does not re-invoke build if input timestamp changes', () => testbed.run(() async {
await buildSystem.build('foo', environment, const BuildSystemConfig());
fs.file('foo.dart').writeAsStringSync('');
await buildSystem.build('foo', environment, const BuildSystemConfig());
expect(fooInvocations, 1);
}));
test('does not re-invoke build if output timestamp changes', () => testbed.run(() async {
await buildSystem.build('foo', environment, const BuildSystemConfig());
environment.buildDir.childFile('out').writeAsStringSync('hey');
await buildSystem.build('foo', environment, const BuildSystemConfig());
expect(fooInvocations, 1);
}));
test('Re-invoke build if output is modified', () => testbed.run(() async {
await buildSystem.build('foo', environment, const BuildSystemConfig());
environment.buildDir.childFile('out').writeAsStringSync('Something different');
await buildSystem.build('foo', environment, const BuildSystemConfig());
expect(fooInvocations, 2);
}));
test('Runs dependencies of targets', () => testbed.run(() async {
await buildSystem.build('bar', environment, const BuildSystemConfig());
expect(fs.file(fs.path.join(environment.buildDir.path, 'bar')).existsSync(), true);
expect(fooInvocations, 1);
expect(barInvocations, 1);
}));
test('handles a throwing build action', () => testbed.run(() async {
final BuildResult result = await buildSystem.build('fizz', environment, const BuildSystemConfig());
expect(result.hasException, true);
}));
test('Can describe itself with JSON output', () => testbed.run(() {
environment.buildDir.createSync(recursive: true);
expect(fooTarget.toJson(environment), <String, dynamic>{
'inputs': <Object>[
'/foo.dart'
],
'outputs': <Object>[
fs.path.join(environment.buildDir.path, 'out'),
],
'dependencies': <Object>[],
'name': 'foo',
'stamp': fs.path.join(environment.buildDir.path, 'foo.stamp'),
});
}));
test('Compute update recognizes added files', () => testbed.run(() async {
fs.directory('build').createSync();
final FileHashStore fileCache = FileHashStore(environment);
fileCache.initialize();
final List<File> inputs = fooTarget.resolveInputs(environment);
final Map<String, ChangeType> changes = await fooTarget.computeChanges(inputs, environment, fileCache);
fileCache.persist();
expect(changes, <String, ChangeType>{
'/foo.dart': ChangeType.Added
});
await buildSystem.build('foo', environment, const BuildSystemConfig());
final Map<String, ChangeType> secondChanges = await fooTarget.computeChanges(inputs, environment, fileCache);
expect(secondChanges, <String, ChangeType>{});
}));
});
group('FileCache', () {
Testbed testbed;
Environment environment;
setUp(() {
testbed = Testbed(setup: () {
fs.directory('build').createSync();
environment = Environment(
projectDir: fs.currentDirectory,
);
});
});
test('Initializes file cache', () => testbed.run(() {
final FileHashStore fileCache = FileHashStore(environment);
fileCache.initialize();
fileCache.persist();
expect(fs.file(fs.path.join('build', '.filecache')).existsSync(), true);
final List<int> buffer = fs.file(fs.path.join('build', '.filecache')).readAsBytesSync();
final pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(buffer);
expect(fileStorage.files, isEmpty);
expect(fileStorage.version, 1);
}));
test('saves and restores to file cache', () => testbed.run(() {
final File file = fs.file('foo.dart')
..createSync()
..writeAsStringSync('hello');
final FileHashStore fileCache = FileHashStore(environment);
fileCache.initialize();
fileCache.hashFiles(<File>[file]);
fileCache.persist();
final String currentHash = fileCache.currentHashes[file.resolveSymbolicLinksSync()];
final List<int> buffer = fs.file(fs.path.join('build', '.filecache')).readAsBytesSync();
pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(buffer);
expect(fileStorage.files.single.hash, currentHash);
expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync());
final FileHashStore newFileCache = FileHashStore(environment);
newFileCache.initialize();
expect(newFileCache.currentHashes, isEmpty);
expect(newFileCache.previousHashes[fs.path.absolute('foo.dart')], currentHash);
newFileCache.persist();
// Still persisted correctly.
fileStorage = pb.FileStorage.fromBuffer(buffer);
expect(fileStorage.files.single.hash, currentHash);
expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync());
}));
});
group('Target', () {
Testbed testbed;
MockPlatform mockPlatform;
Environment environment;
Target sharedTarget;
BuildSystem buildSystem;
int shared;
setUp(() {
shared = 0;
Cache.flutterRoot = '';
mockPlatform = MockPlatform();
// Keep file paths the same.
when(mockPlatform.isWindows).thenReturn(false);
when(mockPlatform.isLinux).thenReturn(true);
when(mockPlatform.isMacOS).thenReturn(false);
testbed = Testbed(
setup: () {
environment = Environment(
projectDir: fs.currentDirectory,
);
fs.file('foo.dart').createSync(recursive: true);
fs.file('pubspec.yaml').createSync();
sharedTarget = Target(
name: 'shared',
inputs: const <Source>[
Source.pattern('{PROJECT_DIR}/foo.dart'),
],
outputs: const <Source>[],
dependencies: <Target>[],
buildAction: (Map<String, ChangeType> updates, Environment environment) {
shared += 1;
}
);
final Target fooTarget = Target(
name: 'foo',
inputs: const <Source>[
Source.pattern('{PROJECT_DIR}/foo.dart'),
],
outputs: const <Source>[
Source.pattern('{BUILD_DIR}/out'),
],
dependencies: <Target>[sharedTarget],
buildAction: (Map<String, ChangeType> updates, Environment environment) {
environment
.buildDir
.childFile('out')
..createSync(recursive: true)
..writeAsStringSync('hey');
}
);
final Target barTarget = Target(
name: 'bar',
inputs: const <Source>[
Source.pattern('{BUILD_DIR}/out'),
],
outputs: const <Source>[
Source.pattern('{BUILD_DIR}/bar'),
],
dependencies: <Target>[fooTarget, sharedTarget],
buildAction: (Map<String, ChangeType> updates, Environment environment) {
environment
.buildDir
.childFile('bar')
..createSync(recursive: true)
..writeAsStringSync('there');
}
);
buildSystem = BuildSystem(<String, Target>{
fooTarget.name: fooTarget,
barTarget.name: barTarget,
sharedTarget.name: sharedTarget,
});
},
overrides: <Type, Generator>{
Platform: () => mockPlatform,
}
);
});
test('Only invokes shared target once', () => testbed.run(() async {
await buildSystem.build('bar', environment, const BuildSystemConfig());
expect(shared, 1);
}));
});
group('Source', () {
Testbed testbed;
SourceVisitor visitor;
Environment environment;
setUp(() {
testbed = Testbed(setup: () {
fs.directory('cache').createSync();
environment = Environment(
projectDir: fs.currentDirectory,
buildDir: fs.directory('build'),
);
visitor = SourceVisitor(environment);
environment.buildDir.createSync(recursive: true);
});
});
test('configures implicit vs explict correctly', () => testbed.run(() {
expect(const Source.pattern('{PROJECT_DIR}/foo').implicit, false);
expect(const Source.pattern('{PROJECT_DIR}/*foo').implicit, true);
expect(Source.function((Environment environment) => <File>[]).implicit, true);
expect(Source.behavior(TestBehavior()).implicit, true);
}));
test('can substitute {PROJECT_DIR}/foo', () => testbed.run(() {
fs.file('foo').createSync();
const Source fooSource = Source.pattern('{PROJECT_DIR}/foo');
fooSource.accept(visitor);
expect(visitor.sources.single.path, fs.path.absolute('foo'));
}));
test('can substitute {BUILD_DIR}/bar', () => testbed.run(() {
final String path = fs.path.join(environment.buildDir.path, 'bar');
fs.file(path).createSync();
const Source barSource = Source.pattern('{BUILD_DIR}/bar');
barSource.accept(visitor);
expect(visitor.sources.single.path, fs.path.absolute(path));
}));
test('can substitute Artifact', () => testbed.run(() {
final String path = fs.path.join(
Cache.instance.getArtifactDirectory('engine').path,
'windows-x64',
'foo',
);
fs.file(path).createSync(recursive: true);
const Source fizzSource = Source.artifact(Artifact.windowsDesktopPath, platform: TargetPlatform.windows_x64);
fizzSource.accept(visitor);
expect(visitor.sources.single.resolveSymbolicLinksSync(), fs.path.absolute(path));
}));
test('can substitute {PROJECT_DIR}/*.fizz', () => testbed.run(() {
const Source fizzSource = Source.pattern('{PROJECT_DIR}/*.fizz');
fizzSource.accept(visitor);
expect(visitor.sources, isEmpty);
fs.file('foo.fizz').createSync();
fs.file('foofizz').createSync();
fizzSource.accept(visitor);
expect(visitor.sources.single.path, fs.path.absolute('foo.fizz'));
}));
test('can substitute {PROJECT_DIR}/fizz.*', () => testbed.run(() {
const Source fizzSource = Source.pattern('{PROJECT_DIR}/fizz.*');
fizzSource.accept(visitor);
expect(visitor.sources, isEmpty);
fs.file('fizz.foo').createSync();
fs.file('fizz').createSync();
fizzSource.accept(visitor);
expect(visitor.sources.single.path, fs.path.absolute('fizz.foo'));
}));
test('can substitute {PROJECT_DIR}/a*bc', () => testbed.run(() {
const Source fizzSource = Source.pattern('{PROJECT_DIR}/bc*bc');
fizzSource.accept(visitor);
expect(visitor.sources, isEmpty);
fs.file('bcbc').createSync();
fs.file('bc').createSync();
fizzSource.accept(visitor);
expect(visitor.sources.single.path, fs.path.absolute('bcbc'));
}));
test('crashes on bad substitute of two **', () => testbed.run(() {
const Source fizzSource = Source.pattern('{PROJECT_DIR}/*.*bar');
fs.file('abcd.bar').createSync();
expect(() => fizzSource.accept(visitor), throwsA(isInstanceOf<InvalidPatternException>()));
}));
test('can\'t substitute foo', () => testbed.run(() {
const Source invalidBase = Source.pattern('foo');
expect(() => invalidBase.accept(visitor), throwsA(isInstanceOf<InvalidPatternException>()));
}));
});
test('Can find dependency cycles', () {
final Target barTarget = Target(
name: 'bar',
inputs: <Source>[],
outputs: <Source>[],
buildAction: null,
dependencies: nonconst(<Target>[])
);
final Target fooTarget = Target(
name: 'foo',
inputs: <Source>[],
outputs: <Source>[],
buildAction: null,
dependencies: nonconst(<Target>[])
);
barTarget.dependencies.add(fooTarget);
fooTarget.dependencies.add(barTarget);
expect(() => checkCycles(barTarget), throwsA(isInstanceOf<CycleException>()));
});
}
class MockPlatform extends Mock implements Platform {}
// Work-around for silly lint check.
T nonconst<T>(T input) => input;
class TestBehavior extends SourceBehavior {
@override
List<File> inputs(Environment environment) {
return null;
}
@override
List<File> outputs(Environment environment) {
return null;
}
}

View file

@ -0,0 +1,72 @@
// 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:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/exceptions.dart';
import '../src/common.dart';
void main() {
test('Exceptions', () {
final MissingInputException missingInputException = MissingInputException(
<File>[fs.file('foo'), fs.file('bar')], 'example');
final CycleException cycleException = CycleException(const <Target>{
Target(
name: 'foo',
buildAction: null,
inputs: <Source>[],
outputs: <Source>[],
),
Target(
name: 'bar',
buildAction: null,
inputs: <Source>[],
outputs: <Source>[],
)
});
final InvalidPatternException invalidPatternException = InvalidPatternException(
'ABC'
);
final MissingOutputException missingOutputException = MissingOutputException(
<File>[ fs.file('foo'), fs.file('bar') ],
'example'
);
final MisplacedOutputException misplacedOutputException = MisplacedOutputException(
'foo',
'example',
);
final MissingDefineException missingDefineException = MissingDefineException(
'foobar',
'example',
);
expect(
missingInputException.toString(),
'foo, bar were declared as an inputs, '
'but did not exist. Check the definition of target:example for errors');
expect(
cycleException.toString(),
'Dependency cycle detected in build: foo -> bar'
);
expect(
invalidPatternException.toString(),
'The pattern "ABC" is not valid'
);
expect(
missingOutputException.toString(),
'foo, bar were declared as outputs, but were not generated by the '
'action. Check the definition of target:example for errors'
);
expect(
misplacedOutputException.toString(),
'Target example produced an output at foo which is outside of the '
'current build or project directory',
);
expect(
missingDefineException.toString(),
'Target example required define foobar but it was not provided'
);
});
}

View file

@ -0,0 +1,70 @@
// 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:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/assets.dart';
import '../../src/common.dart';
import '../../src/testbed.dart';
void main() {
group('copy_assets', () {
Testbed testbed;
BuildSystem buildSystem;
Environment environment;
setUp(() {
testbed = Testbed(setup: () {
environment = Environment(
projectDir: fs.currentDirectory,
);
buildSystem = BuildSystem(<String, Target>{
copyAssets.name: copyAssets,
});
fs.file(fs.path.join('assets', 'foo', 'bar.png'))
..createSync(recursive: true);
fs.file('.packages')
..createSync();
fs.file('pubspec.yaml')
..createSync()
..writeAsStringSync('''
name: example
flutter:
assets:
- assets/foo/bar.png
''');
});
});
test('Copies files to correct asset directory', () => testbed.run(() async {
await buildSystem.build('copy_assets', environment, const BuildSystemConfig());
expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'AssetManifest.json')).existsSync(), true);
expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'FontManifest.json')).existsSync(), true);
expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'LICENSE')).existsSync(), true);
// See https://github.com/flutter/flutter/issues/35293
expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), true);
}));
test('Does not leave stale files in build directory', () => testbed.run(() async {
await buildSystem.build('copy_assets', environment, const BuildSystemConfig());
expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), true);
// Modify manifest to remove asset.
fs.file('pubspec.yaml')
..createSync()
..writeAsStringSync('''
name: example
flutter:
''');
await buildSystem.build('copy_assets', environment, const BuildSystemConfig());
// See https://github.com/flutter/flutter/issues/35293
expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), false);
}));
});
}

View file

@ -0,0 +1,224 @@
// 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:flutter_tools/src/base/build.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/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/exceptions.dart';
import 'package:flutter_tools/src/build_system/targets/dart.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/common.dart';
import '../../src/mocks.dart';
import '../../src/testbed.dart';
void main() {
group('dart rules', () {
Testbed testbed;
BuildSystem buildSystem;
Environment androidEnvironment;
Environment iosEnvironment;
MockProcessManager mockProcessManager;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
mockProcessManager = MockProcessManager();
testbed = Testbed(setup: () {
androidEnvironment = Environment(
projectDir: fs.currentDirectory,
defines: <String, String>{
kBuildMode: getNameForBuildMode(BuildMode.profile),
kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
}
);
iosEnvironment = Environment(
projectDir: fs.currentDirectory,
defines: <String, String>{
kBuildMode: getNameForBuildMode(BuildMode.profile),
kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios),
}
);
buildSystem = BuildSystem();
HostPlatform hostPlatform;
if (platform.isWindows) {
hostPlatform = HostPlatform.windows_x64;
} else if (platform.isLinux) {
hostPlatform = HostPlatform.linux_x64;
} else if (platform.isMacOS) {
hostPlatform = HostPlatform.darwin_x64;
} else {
assert(false);
}
final String skyEngineLine = platform.isWindows
? r'sky_engine:file:///C:/bin/cache/pkg/sky_engine/lib/'
: 'sky_engine:file:///bin/cache/pkg/sky_engine/lib/';
fs.file('.packages')
..createSync()
..writeAsStringSync('''
# Generated
$skyEngineLine
flutter_tools:lib/''');
final String engineArtifacts = fs.path.join('bin', 'cache',
'artifacts', 'engine');
final List<String> paths = <String>[
fs.path.join('bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui',
'ui.dart'),
fs.path.join('bin', 'cache', 'pkg', 'sky_engine', 'sdk_ext',
'vmservice_io.dart'),
fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
fs.path.join(engineArtifacts, getNameForHostPlatform(hostPlatform),
'frontend_server.dart.snapshot'),
fs.path.join(engineArtifacts, 'android-arm-profile',
getNameForHostPlatform(hostPlatform), 'gen_snapshot'),
fs.path.join(engineArtifacts, 'ios-profile', 'gen_snapshot'),
fs.path.join(engineArtifacts, 'common', 'flutter_patched_sdk',
'platform_strong.dill'),
fs.path.join('lib', 'foo.dart'),
fs.path.join('lib', 'bar.dart'),
fs.path.join('lib', 'fizz'),
];
for (String path in paths) {
fs.file(path).createSync(recursive: true);
}
}, overrides: <Type, Generator>{
KernelCompilerFactory: () => FakeKernelCompilerFactory(),
GenSnapshot: () => FakeGenSnapshot(),
});
});
test('kernel_snapshot Produces correct output directory', () => testbed.run(() async {
await buildSystem.build('kernel_snapshot', androidEnvironment, const BuildSystemConfig());
expect(fs.file(fs.path.join(androidEnvironment.buildDir.path,'main.app.dill')).existsSync(), true);
}));
test('kernel_snapshot throws error if missing build mode', () => testbed.run(() async {
final BuildResult result = await buildSystem.build('kernel_snapshot',
androidEnvironment..defines.remove(kBuildMode), const BuildSystemConfig());
expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
}));
test('aot_elf_profile Produces correct output directory', () => testbed.run(() async {
await buildSystem.build('aot_elf_profile', androidEnvironment, const BuildSystemConfig());
expect(fs.file(fs.path.join(androidEnvironment.buildDir.path, 'main.app.dill')).existsSync(), true);
expect(fs.file(fs.path.join(androidEnvironment.buildDir.path, 'app.so')).existsSync(), true);
}));
test('aot_elf_profile throws error if missing build mode', () => testbed.run(() async {
final BuildResult result = await buildSystem.build('aot_elf_profile',
androidEnvironment..defines.remove(kBuildMode), const BuildSystemConfig());
expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
}));
test('aot_elf_profile throws error if missing target platform', () => testbed.run(() async {
final BuildResult result = await buildSystem.build('aot_elf_profile',
androidEnvironment..defines.remove(kTargetPlatform), const BuildSystemConfig());
expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
}));
test('aot_assembly_profile throws error if missing build mode', () => testbed.run(() async {
final BuildResult result = await buildSystem.build('aot_assembly_profile',
iosEnvironment..defines.remove(kBuildMode), const BuildSystemConfig());
expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
}));
test('aot_assembly_profile throws error if missing target platform', () => testbed.run(() async {
final BuildResult result = await buildSystem.build('aot_assembly_profile',
iosEnvironment..defines.remove(kTargetPlatform), const BuildSystemConfig());
expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
}));
test('aot_assembly_profile throws error if built for non-iOS platform', () => testbed.run(() async {
final BuildResult result = await buildSystem.build('aot_assembly_profile',
androidEnvironment, const BuildSystemConfig());
expect(result.exceptions.values.single.exception, isInstanceOf<Exception>());
}));
test('aot_assembly_profile will lipo binaries together when multiple archs are requested', () => testbed.run(() async {
iosEnvironment.defines[kIosArchs] ='armv7,arm64';
when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
fs.file(fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App'))
.createSync(recursive: true);
return FakeProcessResult(
stdout: '',
stderr: '',
);
});
final BuildResult result = await buildSystem.build('aot_assembly_profile',
iosEnvironment, const BuildSystemConfig());
expect(result.success, true);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
}));
});
}
class MockProcessManager extends Mock implements ProcessManager {}
class FakeGenSnapshot implements GenSnapshot {
@override
Future<int> run({SnapshotType snapshotType, IOSArch iosArch, Iterable<String> additionalArgs = const <String>[]}) async {
final Directory out = fs.file(additionalArgs.last).parent;
if (iosArch == null) {
out.childFile('app.so').createSync();
out.childFile('gen_snapshot.d').createSync();
return 0;
}
out.childDirectory('App.framework').childFile('App').createSync(recursive: true);
out.childFile('snapshot_assembly.S').createSync();
out.childFile('snapshot_assembly.o').createSync();
return 0;
}
}
class FakeKernelCompilerFactory implements KernelCompilerFactory {
FakeKernelCompiler kernelCompiler = FakeKernelCompiler();
@override
Future<KernelCompiler> create(FlutterProject flutterProject) async {
return kernelCompiler;
}
}
class FakeKernelCompiler implements KernelCompiler {
@override
Future<CompilerOutput> compile({
String sdkRoot,
String mainPath,
String outputFilePath,
String depFilePath,
TargetModel targetModel = TargetModel.flutter,
bool linkPlatformKernelIn = false,
bool aot = false,
bool trackWidgetCreation,
List<String> extraFrontEndOptions,
String incrementalCompilerByteStorePath,
String packagesPath,
List<String> fileSystemRoots,
String fileSystemScheme,
bool targetProductVm = false,
String initializeFromDill}) async {
fs.file(outputFilePath).createSync(recursive: true);
return CompilerOutput(outputFilePath, 0, null);
}
}

View file

@ -0,0 +1,84 @@
// 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:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/linux.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/testbed.dart';
void main() {
group('unpack_linux', () {
Testbed testbed;
BuildSystem buildSystem;
Environment environment;
MockPlatform mockPlatform;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
mockPlatform = MockPlatform();
when(mockPlatform.isWindows).thenReturn(false);
when(mockPlatform.isMacOS).thenReturn(false);
when(mockPlatform.isLinux).thenReturn(true);
testbed = Testbed(setup: () {
Cache.flutterRoot = '';
environment = Environment(
projectDir: fs.currentDirectory,
);
buildSystem = BuildSystem(<String, Target>{
unpackLinux.name: unpackLinux,
});
fs.file('bin/cache/artifacts/engine/linux-x64/libflutter_linux.so').createSync(recursive: true);
fs.file('bin/cache/artifacts/engine/linux-x64/flutter_export.h').createSync();
fs.file('bin/cache/artifacts/engine/linux-x64/flutter_messenger.h').createSync();
fs.file('bin/cache/artifacts/engine/linux-x64/flutter_plugin_registrar.h').createSync();
fs.file('bin/cache/artifacts/engine/linux-x64/flutter_glfw.h').createSync();
fs.file('bin/cache/artifacts/engine/linux-x64/icudtl.dat').createSync();
fs.file('bin/cache/artifacts/engine/linux-x64/cpp_client_wrapper/foo').createSync(recursive: true);
fs.directory('linux').createSync();
}, overrides: <Type, Generator>{
Platform: () => mockPlatform,
});
});
test('Copies files to correct cache directory', () => testbed.run(() async {
final BuildResult result = await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
expect(result.hasException, false);
expect(fs.file('linux/flutter/libflutter_linux.so').existsSync(), true);
expect(fs.file('linux/flutter/flutter_export.h').existsSync(), true);
expect(fs.file('linux/flutter/flutter_messenger.h').existsSync(), true);
expect(fs.file('linux/flutter/flutter_plugin_registrar.h').existsSync(), true);
expect(fs.file('linux/flutter/flutter_glfw.h').existsSync(), true);
expect(fs.file('linux/flutter/icudtl.dat').existsSync(), true);
expect(fs.file('linux/flutter/cpp_client_wrapper/foo').existsSync(), true);
}));
test('Does not re-copy files unecessarily', () => testbed.run(() async {
await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
final DateTime modified = fs.file('linux/flutter/libflutter_linux.so').statSync().modified;
await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
expect(fs.file('linux/flutter/libflutter_linux.so').statSync().modified, equals(modified));
}));
test('Detects changes in input cache files', () => testbed.run(() async {
await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
fs.file('bin/cache/artifacts/engine/linux-x64/libflutter_linux.so').writeAsStringSync('asd'); // modify cache.
await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
expect(fs.file('linux/flutter/libflutter_linux.so').readAsStringSync(), 'asd');
}));
});
}
class MockPlatform extends Mock implements Platform {}

View file

@ -0,0 +1,115 @@
// 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:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process_manager.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/macos.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/common.dart';
import '../../src/testbed.dart';
void main() {
group('unpack_macos', () {
Testbed testbed;
BuildSystem buildSystem;
Environment environment;
MockPlatform mockPlatform;
setUp(() {
mockPlatform = MockPlatform();
when(mockPlatform.isWindows).thenReturn(false);
when(mockPlatform.isMacOS).thenReturn(true);
when(mockPlatform.isLinux).thenReturn(false);
testbed = Testbed(setup: () {
environment = Environment(
projectDir: fs.currentDirectory,
);
buildSystem = BuildSystem(<String, Target>{
unpackMacos.name: unpackMacos,
});
final List<File> inputs = <File>[
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/FlutterMacOS'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEReshapeListener.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEView.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEViewController.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterChannels.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterCodecs.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterMacOS.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Modules/module.modulemap'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Resources/icudtl.dat'),
fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Resources/info.plist'),
];
for (File input in inputs) {
input.createSync(recursive: true);
}
when(processManager.runSync(any)).thenAnswer((Invocation invocation) {
final List<String> arguments = invocation.positionalArguments.first;
final Directory source = fs.directory(arguments[arguments.length - 2]);
final Directory target = fs.directory(arguments.last)
..createSync(recursive: true);
for (FileSystemEntity entity in source.listSync(recursive: true)) {
if (entity is File) {
final String relative = fs.path.relative(entity.path, from: source.path);
final String destination = fs.path.join(target.path, relative);
if (!fs.file(destination).parent.existsSync()) {
fs.file(destination).parent.createSync();
}
entity.copySync(destination);
}
}
return FakeProcessResult()..exitCode = 0;
});
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
Platform: () => mockPlatform,
});
});
test('Copies files to correct cache directory', () => testbed.run(() async {
await buildSystem.build('unpack_macos', environment, const BuildSystemConfig());
expect(fs.directory('macos/Flutter/FlutterMacOS.framework').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/FlutterMacOS').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEReshapeListener.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEView.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEViewController.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterChannels.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterCodecs.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterMacOS.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Modules/module.modulemap').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/icudtl.dat').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/info.plist').existsSync(), true);
}));
});
}
class MockPlatform extends Mock implements Platform {}
class MockProcessManager extends Mock implements ProcessManager {}
class FakeProcessResult implements ProcessResult {
@override
int exitCode;
@override
int pid = 0;
@override
String stderr = '';
@override
String stdout = '';
}

View file

@ -0,0 +1,97 @@
// 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/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/windows.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/testbed.dart';
void main() {
group('unpack_windows', () {
Testbed testbed;
BuildSystem buildSystem;
Environment environment;
Platform platform;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
Cache.flutterRoot = '';
platform = MockPlatform();
when(platform.isWindows).thenReturn(true);
when(platform.isMacOS).thenReturn(false);
when(platform.isLinux).thenReturn(false);
when(platform.pathSeparator).thenReturn(r'\');
testbed = Testbed(setup: () {
environment = Environment(
projectDir: fs.currentDirectory,
);
buildSystem = BuildSystem(<String, Target>{
unpackWindows.name: unpackWindows,
});
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_export.h').createSync(recursive: true);
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_messenger.h').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll.exp').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll.lib').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll.pdb').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\lutter_export.h').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_messenger.h').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_plugin_registrar.h').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_glfw.h').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\icudtl.dat').createSync();
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\cpp_client_wrapper\foo').createSync(recursive: true);
fs.directory('windows').createSync();
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(style: FileSystemStyle.windows),
Platform: () => platform,
});
});
test('Copies files to correct cache directory', () => testbed.run(() async {
await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
expect(fs.file(r'C:\windows\flutter\flutter_export.h').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\flutter_messenger.h').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\flutter_windows.dll').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\flutter_windows.dll.exp').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\flutter_windows.dll.lib').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\flutter_windows.dll.pdb').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\flutter_export.h').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\flutter_messenger.h').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\flutter_plugin_registrar.h').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\flutter_glfw.h').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\icudtl.dat').existsSync(), true);
expect(fs.file(r'C:\windows\flutter\cpp_client_wrapper\foo').existsSync(), true);
}));
test('Does not re-copy files unecessarily', () => testbed.run(() async {
await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
final DateTime modified = fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified;
await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
expect(fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified, equals(modified));
}));
test('Detects changes in input cache files', () => testbed.run(() async {
await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
final DateTime modified = fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified;
fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_export.h').writeAsStringSync('asd'); // modify cache.
await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
expect(fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified, isNot(modified));
}));
});
}
class MockPlatform extends Mock implements Platform {}

View file

@ -0,0 +1,84 @@
// 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:args/command_runner.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/assemble.dart';
import 'package:flutter_tools/src/globals.dart';
import 'package:mockito/mockito.dart';
import '../src/common.dart';
import '../src/testbed.dart';
void main() {
group('Assemble', () {
Testbed testbed;
MockBuildSystem mockBuildSystem;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
mockBuildSystem = MockBuildSystem();
testbed = Testbed(overrides: <Type, Generator>{
BuildSystem: () => mockBuildSystem,
});
});
test('Can list the output directory relative to project root', () => testbed.run(() async {
final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
await commandRunner.run(<String>['assemble', '--flutter-root=.', 'build-dir', '-dBuildMode=debug']);
final BufferLogger bufferLogger = logger;
final Environment environment = Environment(
defines: <String, String>{
'BuildMode': 'debug'
}, projectDir: fs.currentDirectory,
buildDir: fs.directory(getBuildDirectory()),
);
expect(bufferLogger.statusText.trim(),
fs.path.relative(environment.buildDir.path, from: fs.currentDirectory.path));
}));
test('Can describe a target', () => testbed.run(() async {
when(mockBuildSystem.describe('foobar', any)).thenReturn(<Map<String, Object>>[
<String, Object>{'fizz': 'bar'},
]);
final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
await commandRunner.run(<String>['assemble', '--flutter-root=.', 'describe', 'foobar']);
final BufferLogger bufferLogger = logger;
expect(bufferLogger.statusText.trim(), '[{"fizz":"bar"}]');
}));
test('Can describe a target\'s inputs', () => testbed.run(() async {
when(mockBuildSystem.describe('foobar', any)).thenReturn(<Map<String, Object>>[
<String, Object>{'name': 'foobar', 'inputs': <String>['bar', 'baz']},
]);
final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
await commandRunner.run(<String>['assemble', '--flutter-root=.', 'inputs', 'foobar']);
final BufferLogger bufferLogger = logger;
expect(bufferLogger.statusText.trim(), 'bar\nbaz');
}));
test('Can run a build', () => testbed.run(() async {
when(mockBuildSystem.build('foobar', any, any)).thenAnswer((Invocation invocation) async {
return BuildResult(true, const <String, ExceptionMeasurement>{}, const <String, PerformanceMeasurement>{});
});
final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
await commandRunner.run(<String>['assemble', 'run', 'foobar']);
final BufferLogger bufferLogger = logger;
expect(bufferLogger.statusText.trim(), 'build succeeded');
}));
});
}
class MockBuildSystem extends Mock implements BuildSystem {}

View file

@ -76,7 +76,7 @@ class Testbed {
: _setup = setup,
_overrides = overrides;
final Future<void> Function() _setup;
final FutureOr<void> Function() _setup;
final Map<Type, Generator> _overrides;
/// Runs `test` within a tool zone.