diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 42c18082a73..25b39042faa 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -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 main(List args) async { await runner.run(args, [ AnalyzeCommand(verboseHelp: verboseHelp), + AssembleCommand(), AttachCommand(verboseHelp: verboseHelp), BuildCommand(verboseHelp: verboseHelp), ChannelCommand(verboseHelp: verboseHelp), diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index 1c7c17896be..65a8b630f6e 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -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; diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 8d2338af660..00264f3c572 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -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 extraGenSnapshotOptions = const [], }) 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 inputPaths = [uiPath, vmServicePath, mainPath]; final Set outputPaths = {}; - - final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d'); final List genSnapshotArgs = [ '--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: [mainPath, ...inputPaths, ...outputPaths], - properties: { - 'buildMode': buildMode.toString(), - 'targetPlatform': platform.toString(), - 'entryPoint': mainPath, - 'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '), - 'engineHash': Cache.instance.engineRevision, - 'buildersUsed': '${flutterProject != null && flutterProject.hasBuilders}', - }, - depfilePaths: [], - ); - // 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; } diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 87361354e5c..ac0489a3638 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -115,6 +115,32 @@ enum BuildMode { release, } +const List _kBuildModes = [ + 'debug', + 'profile', + 'release', + 'dynamic-profile', + 'dynamic-release', +]; + +/// Return the name for the build mode, or "any" if null. +String getNameForBuildMode(BuildMode buildMode) { + return _kBuildModes[buildMode.index]; +} + +/// Returns the [BuildMode] for a particular `name`. +BuildMode getBuildModeForName(String name) { + switch (name) { + case 'debug': + return BuildMode.debug; + case 'profile': + return BuildMode.profile; + case 'release': + return BuildMode.release; + } + return null; +} + String validatedBuildNumberForPlatform(TargetPlatform targetPlatform, String buildNumber) { if (buildNumber == null) { return null; diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart new file mode 100644 index 00000000000..c0f9d224e2e --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -0,0 +1,686 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:meta/meta.dart'; +import 'package:pool/pool.dart'; + +import '../base/file_system.dart'; +import '../base/platform.dart'; +import '../cache.dart'; +import '../convert.dart'; +import '../globals.dart'; +import 'exceptions.dart'; +import 'file_hash_store.dart'; +import 'source.dart'; +import 'targets/assets.dart'; +import 'targets/dart.dart'; +import 'targets/ios.dart'; +import 'targets/linux.dart'; +import 'targets/macos.dart'; +import 'targets/windows.dart'; + +export 'source.dart'; + +/// The function signature of a build target which can be invoked to perform +/// the underlying task. +typedef BuildAction = FutureOr Function( + Map 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 [], + }); + + /// 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 dependencies; + + /// The input [Source]s which are diffed to determine if a target should run. + final List inputs; + + /// The output [Source]s which we attempt to verify are correctly produced. + final List outputs; + + /// The action which performs this build step. + final BuildAction buildAction; + + /// Collect hashes for all inputs to determine if any have changed. + Future> computeChanges( + List inputs, + Environment environment, + FileHashStore fileHashStore, + ) async { + final Map updates = {}; + final File stamp = _findStampFile(environment); + final Set previousInputs = {}; + final List previousOutputs = []; + + // 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 values = json.decode(content); + final List inputs = values['inputs']; + final List outputs = values['outputs']; + inputs.cast().forEach(previousInputs.add); + outputs.cast().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 sourcesToHash = []; + final List missingInputs = []; + 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 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 currentInputPaths = Set.from( + inputs.map((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 inputs, + List outputs, + Environment environment, + ) { + final File stamp = _findStampFile(environment); + final List inputPaths = []; + for (File input in inputs) { + inputPaths.add(input.resolveSymbolicLinksSync()); + } + final List outputPaths = []; + for (File output in outputs) { + outputPaths.add(output.resolveSymbolicLinksSync()); + } + final Map result = { + '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 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 resolveOutputs( + Environment environment, + { bool implicit = true, } + ) { + final List 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 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 toJson(Environment environment) { + return { + '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 _resolveConfiguration( + List 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 defines = const {}, + }) { + // 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 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 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 exceptions; + final Map performance; + + bool get hasException => exceptions.isNotEmpty; +} + +/// The build system is responsible for invoking and ordering [Target]s. +class BuildSystem { + BuildSystem([Map targets]) + : targets = targets ?? _defaultTargets; + + /// All currently registered targets. + static final Map _defaultTargets = { + 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 targets; + + /// Build the target `name` and all of its dependencies. + Future 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> 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> fold(Map> accumulation, Target current) { + accumulation[current.name] = current.toJson(environment); + return accumulation; + } + + final Map> result = + >{}; + final Map> 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> pending = >{}; + final Environment environment; + final FileHashStore fileCache; + + // Timings collected during target invocation. + final Map stepTimings = {}; + + // Exceptions caught during the build process. + final Map exceptionMeasurements = {}; + + Future invokeTarget(Target target) async { + final List results = await Future.wait(target.dependencies.map(invokeTarget)); + if (results.any((bool result) => !result)) { + return false; + } + final AsyncMemoizer memoizer = pending[target.name] ??= AsyncMemoizer(); + return memoizer.runOnce(() => _invokeInternal(target)); + } + + Future _invokeInternal(Target target) async { + final PoolResource resource = await resourcePool.request(); + final Stopwatch stopwatch = Stopwatch()..start(); + bool passed = true; + bool skipped = false; + try { + final List inputs = target.resolveInputs(environment); + final Map 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 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 visited, Set 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, {}, {}); +} + +/// Verifies that all files exist and are in a subdirectory of [Environment.buildDir]. +void verifyOutputDirectories(List outputs, Environment environment, Target target) { + final String buildDirectory = environment.buildDir.resolveSymbolicLinksSync(); + final String projectDirectory = environment.projectDir.resolveSymbolicLinksSync(); + final List missingOutputs = []; + for (File sourceFile in outputs) { + if (!sourceFile.existsSync()) { + missingOutputs.add(sourceFile); + continue; + } + final String path = sourceFile.resolveSymbolicLinksSync(); + if (!path.startsWith(buildDirectory) && !path.startsWith(projectDirectory)) { + throw MisplacedOutputException(path, target.name); + } + } + if (missingOutputs.isNotEmpty) { + throw MissingOutputException(missingOutputs, target.name); + } +} diff --git a/packages/flutter_tools/lib/src/build_system/exceptions.dart b/packages/flutter_tools/lib/src/build_system/exceptions.dart new file mode 100644 index 00000000000..918c0517814 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/exceptions.dart @@ -0,0 +1,95 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../base/file_system.dart'; + +import 'build_system.dart'; + +/// An exception thrown when a rule declares an input that does not exist on +/// disk. +class MissingInputException implements Exception { + const MissingInputException(this.missing, this.target); + + /// The file or directory we expected to find. + final List 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 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 missing; + + /// The name of the target this file should have been output from. + final String target; + + @override + String toString() { + final String files = missing.map((File file) => file.path).join(', '); + return '$files were declared as outputs, but were not generated by ' + 'the action. Check the definition of target:$target for errors'; + } +} + +/// An exception thrown when in output is placed outside of +/// [Environment.buildDir]. +class MisplacedOutputException implements Exception { + MisplacedOutputException(this.path, this.target); + + final String path; + final String target; + + @override + String toString() { + return 'Target $target produced an output at $path' + ' which is outside of the current build or project directory'; + } +} + +/// An exception thrown if a build action is missing a required define. +class MissingDefineException implements Exception { + MissingDefineException(this.define, this.target); + + final String define; + final String target; + + @override + String toString() { + return 'Target $target required define $define ' + 'but it was not provided'; + } +} diff --git a/packages/flutter_tools/lib/src/build_system/file_hash_store.dart b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart new file mode 100644 index 00000000000..398f95f45e7 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart @@ -0,0 +1,111 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; + +import '../base/file_system.dart'; +import '../globals.dart'; +import 'build_system.dart'; +import 'filecache.pb.dart' as pb; + +/// A globally accessible cache of file hashes. +/// +/// In cases where multiple targets read the same source files as inputs, we +/// avoid recomputing or storing multiple copies of hashes by delegating +/// through this class. All file hashes are held in memory during a build +/// operation, and persisted to cache in the root build directory. +/// +/// The format of the file store is subject to change and not part of its API. +/// +/// To regenerate the protobuf entries used to construct the cache: +/// 1. If not already installed, https://developers.google.com/protocol-buffers/docs/downloads +/// 2. pub global active `protoc-gen-dart` +/// 3. protoc -I=lib/src/build_system/ --dart_out=lib/src/build_system/ lib/src/build_system/filecache.proto +/// 4. Add licenses headers to the newly generated file and check-in. +/// +/// See also: https://developers.google.com/protocol-buffers/docs/darttutorial +// TODO(jonahwilliams): find a better way to clear out old entries, perhaps +// track the last access or modification date? +class FileHashStore { + FileHashStore(this.environment); + + final Environment environment; + final HashMap previousHashes = HashMap(); + final HashMap currentHashes = HashMap(); + + // 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 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 entry in currentHashes.entries) { + previousHashes[entry.key] = entry.value; + } + for (MapEntry 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> hashFiles(List files) async { + final List dirty = []; + for (File file in files) { + final String absolutePath = file.resolveSymbolicLinksSync(); + final String previousHash = previousHashes[absolutePath]; + final List bytes = file.readAsBytesSync(); + final String currentHash = md5.convert(bytes).toString(); + + if (currentHash != previousHash) { + dirty.add(file); + } + currentHashes[absolutePath] = currentHash; + } + return dirty; + } + + File get _cacheFile => environment.rootBuildDir.childFile(_kFileCache); +} diff --git a/packages/flutter_tools/lib/src/build_system/filecache.pb.dart b/packages/flutter_tools/lib/src/build_system/filecache.pb.dart new file mode 100644 index 00000000000..4c095acef54 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/filecache.pb.dart @@ -0,0 +1,96 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// +// Generated code. Do not modify. +// source: lib/src/build_system/filecache.proto +/// +// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name, sort_constructors_first + +import 'dart:core' as $core show bool, Deprecated, double, int, List, Map, override, pragma, String, dynamic; + +import 'package:protobuf/protobuf.dart' as $pb; + +class FileHash extends $pb.GeneratedMessage { + factory FileHash() => create(); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo('FileHash', package: const $pb.PackageName('flutter_tools')) + ..aOS(1, 'path') + ..aOS(2, 'hash') + ..hasRequiredFields = false; + + FileHash._() : super(); + + factory FileHash.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + + factory FileHash.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + @$core.override + FileHash clone() => FileHash()..mergeFromMessage(this); + + @$core.override + FileHash copyWith(void Function(FileHash) updates) => super.copyWith(($core.dynamic message) => updates(message as FileHash)); + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static FileHash create() => FileHash._(); + + @$core.override + FileHash createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + 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(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 createRepeated() => $pb.PbList(); + + 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 get files => $_getList(1); +} diff --git a/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart b/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart new file mode 100644 index 00000000000..32f6f9eafc9 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart @@ -0,0 +1,25 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// +// Generated code. Do not modify. +// source: lib/src/build_system/filecache.proto +/// +// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name + +const Map FileHash$json = { + '1': 'FileHash', + '2': >[ + {'1': 'path', '3': 1, '4': 1, '5': 9, '10': 'path'}, + {'1': 'hash', '3': 2, '4': 1, '5': 9, '10': 'hash'}, + ], +}; + +const Map FileStorage$json = { + '1': 'FileHashStore', + '2': >[ + {'1': 'version', '3': 1, '4': 1, '5': 5, '10': 'version'}, + {'1': 'files', '3': 2, '4': 3, '5': 11, '6': '.flutter_tools.FileHash', '10': 'files'}, + ], +}; diff --git a/packages/flutter_tools/lib/src/build_system/filecache.proto b/packages/flutter_tools/lib/src/build_system/filecache.proto new file mode 100644 index 00000000000..63be878f4fc --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/filecache.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package flutter_tools; + +message FileHash { + // The absolute path to the file on disk. + string path = 1; + + // The last computed file hash. + string hash = 2; +} + +message FileStorage { + // The current version of the file store. + int32 version = 1; + + // All currently stored files. + repeated FileHash files = 2; +} diff --git a/packages/flutter_tools/lib/src/build_system/source.dart b/packages/flutter_tools/lib/src/build_system/source.dart new file mode 100644 index 00000000000..e9f1e1642cb --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/source.dart @@ -0,0 +1,228 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../artifacts.dart'; +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../globals.dart'; +import 'build_system.dart'; +import 'exceptions.dart'; + +/// An input function produces a list of additional input files for an +/// [Environment]. +typedef InputFunction = List 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 sources = []; + + /// 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 segments = []; + final List 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 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([ + 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 inputs(Environment environment); + + /// The outputs for a particular target. + List outputs(Environment environment); +} + +class _SourceBehavior implements Source { + const _SourceBehavior(this.value); + + final SourceBehavior value; + + @override + void accept(SourceVisitor visitor) => visitor.visitBehavior(value); + + @override + bool get implicit => true; +} + +class _FunctionSource implements Source { + const _FunctionSource(this.value); + + final InputFunction value; + + @override + void accept(SourceVisitor visitor) => visitor.visitFunction(value); + + @override + bool get implicit => true; +} + +class _PatternSource implements Source { + const _PatternSource(this.value); + + final String value; + + @override + void accept(SourceVisitor visitor) => visitor.visitPattern(value); + + @override + bool get implicit => value.contains('*'); +} + +class _ArtifactSource implements Source { + const _ArtifactSource(this.artifact, { this.platform, this.mode }); + + final Artifact artifact; + final TargetPlatform platform; + final BuildMode mode; + + @override + void accept(SourceVisitor visitor) => visitor.visitArtifact(artifact, platform, mode); + + @override + bool get implicit => false; +} diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart new file mode 100644 index 00000000000..e5545db43df --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart @@ -0,0 +1,95 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pool/pool.dart'; + +import '../../asset.dart'; +import '../../base/file_system.dart'; +import '../../devfs.dart'; +import '../build_system.dart'; + +/// The copying logic for flutter assets. +// TODO(jonahwilliams): combine the asset bundle logic with this rule so that +// we can compute the key for deleted assets. This is required to remove assets +// from build directories that are no longer part of the manifest and to unify +// the update/diff logic. +class AssetBehavior extends SourceBehavior { + const AssetBehavior(); + + @override + List 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 results = []; + final Iterable files = assetBundle.entries.values.whereType(); + for (DevFSFileContent devFsContent in files) { + results.add(fs.file(devFsContent.file.path)); + } + return results; + } + + @override + List 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 results = []; + for (MapEntry 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 copyAssetsInvocation(Map 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( + assetBundle.entries.entries.map>((MapEntry 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.pattern('{PROJECT_DIR}/pubspec.yaml'), + Source.behavior(AssetBehavior()), + ], + outputs: [ + 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: [], + buildAction: copyAssetsInvocation, +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart new file mode 100644 index 00000000000..f7d17f66431 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart @@ -0,0 +1,283 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../artifacts.dart'; +import '../../base/build.dart'; +import '../../base/file_system.dart'; +import '../../base/io.dart'; +import '../../base/platform.dart'; +import '../../base/process_manager.dart'; +import '../../build_info.dart'; +import '../../compile.dart'; +import '../../dart/package_map.dart'; +import '../../globals.dart'; +import '../../project.dart'; +import '../build_system.dart'; +import '../exceptions.dart'; + +/// The define to pass a [BuildMode]. +const String kBuildMode= 'BuildMode'; + +/// The define to pass whether we compile 64-bit android-arm code. +const String kTargetPlatform = 'TargetPlatform'; + +/// The define to control what target file is used. +const String kTargetFile = 'TargetFile'; + +/// The define to control what iOS architectures are built for. +/// +/// This is expected to be a comma-separated list of architectures. If not +/// provided, defaults to arm64. +/// +/// The other supported value is armv7, the 32-bit iOS architecture. +const String kIosArchs = 'IosArchs'; + +/// Supports compiling dart source to kernel with a subset of flags. +/// +/// This is a non-incremental compile so the specific [updates] are ignored. +Future compileKernel(Map 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 compileAotElf(Map 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 listDartSources(Environment environment) { + final Map packageMap = PackageMap(environment.projectDir.childFile('.packages').path).map; + final List dartFiles = []; + 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 compileAotAssembly(Map 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 iosArchs = environment.defines[kIosArchs]?.split(',')?.map(getIOSArchForName)?.toList() + ?? [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> pending = >[]; + 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 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([ + '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.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.pattern('{BUILD_DIR}/main.app.dill'), + ], + dependencies: [], + buildAction: compileKernel, +); + +/// Generate an ELF binary from a dart kernel file in profile mode. +const Target aotElfProfile = Target( + name: 'aot_elf_profile', + inputs: [ + 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.pattern('{BUILD_DIR}/app.so'), + ], + dependencies: [ + 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.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.pattern('{BUILD_DIR}/app.so'), + ], + dependencies: [ + 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.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: [ + // 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: [ + 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.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: [ + // 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: [ + kernelSnapshot, + ], + buildAction: compileAotAssembly, +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart new file mode 100644 index 00000000000..19e38ea484b --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -0,0 +1,43 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../build_system.dart'; +import 'assets.dart'; +import 'dart.dart'; + +/// Create an iOS debug application. +const Target debugIosApplication = Target( + name: 'debug_ios_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + copyAssets, + kernelSnapshot, + ] +); + +/// Create an iOS profile application. +const Target profileIosApplication = Target( + name: 'profile_ios_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + copyAssets, + aotAssemblyProfile, + ] +); + +/// Create an iOS debug application. +const Target releaseIosApplication = Target( + name: 'release_ios_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + copyAssets, + aotAssemblyRelease, + ] +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/linux.dart b/packages/flutter_tools/lib/src/build_system/targets/linux.dart new file mode 100644 index 00000000000..c5ad64d1389 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/linux.dart @@ -0,0 +1,46 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../artifacts.dart'; +import '../../base/file_system.dart'; +import '../../globals.dart'; +import '../build_system.dart'; + +// Copies all of the input files to the correct copy dir. +Future copyLinuxAssets(Map 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.artifact(Artifact.linuxDesktopPath), + ], + outputs: [ + 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: [], + buildAction: copyLinuxAssets, +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart new file mode 100644 index 00000000000..7bfb9239f7e --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -0,0 +1,100 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../artifacts.dart'; +import '../../base/file_system.dart'; +import '../../base/io.dart'; +import '../../base/process_manager.dart'; +import '../../globals.dart'; +import '../build_system.dart'; +import 'assets.dart'; +import 'dart.dart'; + +/// Copy the macOS framework to the correct copy dir by invoking 'cp -R'. +/// +/// The shelling out is done to avoid complications with preserving special +/// files (e.g., symbolic links) in the framework structure. +/// +/// Removes any previous version of the framework that already exists in the +/// target directory. +// TODO(jonahwilliams): remove shell out. +Future copyFramework(Map 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(['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.artifact(Artifact.flutterMacOSFramework), + ], + outputs: [ + 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: [], + buildAction: copyFramework, +); + +/// Build a macOS application. +const Target macosApplication = Target( + name: 'debug_macos_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + unpackMacos, + kernelSnapshot, + copyAssets, + ] +); + +/// Build a macOS release application. +const Target macoReleaseApplication = Target( + name: 'release_macos_application', + buildAction: null, + inputs: [], + outputs: [], + dependencies: [ + unpackMacos, + aotElfRelease, + copyAssets, + ] +); diff --git a/packages/flutter_tools/lib/src/build_system/targets/windows.dart b/packages/flutter_tools/lib/src/build_system/targets/windows.dart new file mode 100644 index 00000000000..2677cba6983 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/windows.dart @@ -0,0 +1,50 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../artifacts.dart'; +import '../../base/file_system.dart'; +import '../../globals.dart'; +import '../build_system.dart'; + +/// Copies all of the input files to the correct copy dir. +Future copyWindowsAssets(Map 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.artifact(Artifact.windowsDesktopPath), + ], + outputs: [ + 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: [], + buildAction: copyWindowsAssets, +); diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart new file mode 100644 index 00000000000..e5a66593103 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -0,0 +1,221 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../base/common.dart'; +import '../base/context.dart'; +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../build_system/build_system.dart'; +import '../convert.dart'; +import '../globals.dart'; +import '../project.dart'; +import '../runner/flutter_command.dart'; + +/// The [BuildSystem] instance. +BuildSystem get buildSystem => context.get(); + +/// 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 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 [ + '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 _parseDefines(List values) { + final Map results = {}; + for (String chunk in values) { + final List 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 runCommand() async { + final BuildResult result = await buildSystem.build(targetName, environment, BuildSystemConfig( + resourcePoolSize: argResults['resource-pool-size'], + )); + if (!result.success) { + for (MapEntry 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 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 runCommand() { + try { + final List> results = buildSystem.describe(targetName, environment); + for (Map result in results) { + if (result['name'] == targetName) { + final List 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 runCommand() { + printStatus(environment.buildDir.path); + return null; + } +} + diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index bc03cbe4474..533cfc6fbdf 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -21,6 +21,7 @@ import 'base/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 runInContext( Artifacts: () => CachedArtifacts(), AssetBundleFactory: () => AssetBundleFactory.defaultInstance, BotDetector: () => const BotDetector(), + BuildSystem: () => BuildSystem(), Cache: () => Cache(), ChromeLauncher: () => const ChromeLauncher(), CocoaPods: () => CocoaPods(), diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 2a8a7ebd4db..6a573d1bd8b 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -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" diff --git a/packages/flutter_tools/test/build_system/build_system_test.dart b/packages/flutter_tools/test/build_system/build_system_test.dart new file mode 100644 index 00000000000..e0c2ffa7ab6 --- /dev/null +++ b/packages/flutter_tools/test/build_system/build_system_test.dart @@ -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.pattern('{PROJECT_DIR}/foo.dart'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + dependencies: [], + buildAction: (Map updates, Environment environment) { + environment + .buildDir + .childFile('out') + ..createSync(recursive: true) + ..writeAsStringSync('hey'); + fooInvocations++; + } + ); + barTarget = Target( + name: 'bar', + inputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/bar'), + ], + dependencies: [fooTarget], + buildAction: (Map updates, Environment environment) { + environment.buildDir + .childFile('bar') + ..createSync(recursive: true) + ..writeAsStringSync('there'); + barInvocations++; + } + ); + fizzTarget = Target( + name: 'fizz', + inputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/fizz'), + ], + dependencies: [fooTarget], + buildAction: (Map updates, Environment environment) { + throw Exception('something bad happens'); + } + ); + buildSystem = BuildSystem({ + fooTarget.name: fooTarget, + barTarget.name: barTarget, + fizzTarget.name: fizzTarget, + }); + }, + overrides: { + Platform: () => mockPlatform, + } + ); + }); + + test('can describe build rules', () => testbed.run(() { + expect(buildSystem.describe('foo', environment), [ + { + 'name': 'foo', + 'dependencies': [], + 'inputs': ['/foo.dart'], + 'outputs': [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())); + })); + + 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()); + })); + + test('Throws exception if it does not produce a specified output', () => testbed.run(() async { + final Target badTarget = Target + (buildAction: (Map inputs, Environment environment) {}, + inputs: const [ + Source.pattern('{PROJECT_DIR}/foo.dart'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/out') + ], + name: 'bad' + ); + buildSystem = BuildSystem({ + badTarget.name: badTarget, + }); + final BuildResult result = await buildSystem.build('bad', environment, const BuildSystemConfig()); + + expect(result.hasException, true); + expect(result.exceptions.values.single.exception, isInstanceOf()); + })); + + 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 stampContents = json.decode(stampFile.readAsStringSync()); + expect(stampContents['inputs'], ['/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), { + 'inputs': [ + '/foo.dart' + ], + 'outputs': [ + fs.path.join(environment.buildDir.path, 'out'), + ], + 'dependencies': [], + '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 inputs = fooTarget.resolveInputs(environment); + final Map changes = await fooTarget.computeChanges(inputs, environment, fileCache); + fileCache.persist(); + + expect(changes, { + '/foo.dart': ChangeType.Added + }); + + await buildSystem.build('foo', environment, const BuildSystemConfig()); + final Map secondChanges = await fooTarget.computeChanges(inputs, environment, fileCache); + + expect(secondChanges, {}); + })); + }); + + 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 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]); + fileCache.persist(); + final String currentHash = fileCache.currentHashes[file.resolveSymbolicLinksSync()]; + final List 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.pattern('{PROJECT_DIR}/foo.dart'), + ], + outputs: const [], + dependencies: [], + buildAction: (Map updates, Environment environment) { + shared += 1; + } + ); + final Target fooTarget = Target( + name: 'foo', + inputs: const [ + Source.pattern('{PROJECT_DIR}/foo.dart'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + dependencies: [sharedTarget], + buildAction: (Map updates, Environment environment) { + environment + .buildDir + .childFile('out') + ..createSync(recursive: true) + ..writeAsStringSync('hey'); + } + ); + final Target barTarget = Target( + name: 'bar', + inputs: const [ + Source.pattern('{BUILD_DIR}/out'), + ], + outputs: const [ + Source.pattern('{BUILD_DIR}/bar'), + ], + dependencies: [fooTarget, sharedTarget], + buildAction: (Map updates, Environment environment) { + environment + .buildDir + .childFile('bar') + ..createSync(recursive: true) + ..writeAsStringSync('there'); + } + ); + buildSystem = BuildSystem({ + fooTarget.name: fooTarget, + barTarget.name: barTarget, + sharedTarget.name: sharedTarget, + }); + }, + overrides: { + 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) => []).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())); + })); + + + test('can\'t substitute foo', () => testbed.run(() { + const Source invalidBase = Source.pattern('foo'); + + expect(() => invalidBase.accept(visitor), throwsA(isInstanceOf())); + })); + }); + + + + test('Can find dependency cycles', () { + final Target barTarget = Target( + name: 'bar', + inputs: [], + outputs: [], + buildAction: null, + dependencies: nonconst([]) + ); + final Target fooTarget = Target( + name: 'foo', + inputs: [], + outputs: [], + buildAction: null, + dependencies: nonconst([]) + ); + barTarget.dependencies.add(fooTarget); + fooTarget.dependencies.add(barTarget); + expect(() => checkCycles(barTarget), throwsA(isInstanceOf())); + }); +} + +class MockPlatform extends Mock implements Platform {} + +// Work-around for silly lint check. +T nonconst(T input) => input; + +class TestBehavior extends SourceBehavior { + @override + List inputs(Environment environment) { + return null; + } + + @override + List outputs(Environment environment) { + return null; + } +} diff --git a/packages/flutter_tools/test/build_system/exceptions_test.dart b/packages/flutter_tools/test/build_system/exceptions_test.dart new file mode 100644 index 00000000000..68486141e66 --- /dev/null +++ b/packages/flutter_tools/test/build_system/exceptions_test.dart @@ -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( + [fs.file('foo'), fs.file('bar')], 'example'); + final CycleException cycleException = CycleException(const { + Target( + name: 'foo', + buildAction: null, + inputs: [], + outputs: [], + ), + Target( + name: 'bar', + buildAction: null, + inputs: [], + outputs: [], + ) + }); + final InvalidPatternException invalidPatternException = InvalidPatternException( + 'ABC' + ); + final MissingOutputException missingOutputException = MissingOutputException( + [ 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' + ); + }); +} diff --git a/packages/flutter_tools/test/build_system/targets/assets_test.dart b/packages/flutter_tools/test/build_system/targets/assets_test.dart new file mode 100644 index 00000000000..43021558039 --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/assets_test.dart @@ -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({ + 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); + })); + }); +} diff --git a/packages/flutter_tools/test/build_system/targets/dart_test.dart b/packages/flutter_tools/test/build_system/targets/dart_test.dart new file mode 100644 index 00000000000..0d742efb3fe --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/dart_test.dart @@ -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: { + kBuildMode: getNameForBuildMode(BuildMode.profile), + kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm), + } + ); + iosEnvironment = Environment( + projectDir: fs.currentDirectory, + defines: { + 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 paths = [ + 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: { + 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()); + })); + + 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()); + })); + + + 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()); + })); + + + 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()); + })); + + 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()); + })); + + 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()); + })); + + 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: { + ProcessManager: () => mockProcessManager, + })); + }); +} + +class MockProcessManager extends Mock implements ProcessManager {} + +class FakeGenSnapshot implements GenSnapshot { + @override + Future run({SnapshotType snapshotType, IOSArch iosArch, Iterable additionalArgs = const []}) 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 create(FlutterProject flutterProject) async { + return kernelCompiler; + } +} + +class FakeKernelCompiler implements KernelCompiler { + @override + Future compile({ + String sdkRoot, + String mainPath, + String outputFilePath, + String depFilePath, + TargetModel targetModel = TargetModel.flutter, + bool linkPlatformKernelIn = false, + bool aot = false, + bool trackWidgetCreation, + List extraFrontEndOptions, + String incrementalCompilerByteStorePath, + String packagesPath, + List fileSystemRoots, + String fileSystemScheme, + bool targetProductVm = false, + String initializeFromDill}) async { + fs.file(outputFilePath).createSync(recursive: true); + return CompilerOutput(outputFilePath, 0, null); + } +} diff --git a/packages/flutter_tools/test/build_system/targets/linux_test.dart b/packages/flutter_tools/test/build_system/targets/linux_test.dart new file mode 100644 index 00000000000..4b22c9aee76 --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/linux_test.dart @@ -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({ + 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: { + 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 {} diff --git a/packages/flutter_tools/test/build_system/targets/macos_test.dart b/packages/flutter_tools/test/build_system/targets/macos_test.dart new file mode 100644 index 00000000000..5deadb5342a --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/macos_test.dart @@ -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({ + unpackMacos.name: unpackMacos, + }); + final List inputs = [ + 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 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: { + 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 = ''; +} diff --git a/packages/flutter_tools/test/build_system/targets/windows_test.dart b/packages/flutter_tools/test/build_system/targets/windows_test.dart new file mode 100644 index 00000000000..f46896ad4ec --- /dev/null +++ b/packages/flutter_tools/test/build_system/targets/windows_test.dart @@ -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({ + 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: { + 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 {} diff --git a/packages/flutter_tools/test/commands/assemble_test.dart b/packages/flutter_tools/test/commands/assemble_test.dart new file mode 100644 index 00000000000..366eda77805 --- /dev/null +++ b/packages/flutter_tools/test/commands/assemble_test.dart @@ -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: { + BuildSystem: () => mockBuildSystem, + }); + }); + + test('Can list the output directory relative to project root', () => testbed.run(() async { + final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(['assemble', '--flutter-root=.', 'build-dir', '-dBuildMode=debug']); + final BufferLogger bufferLogger = logger; + final Environment environment = Environment( + defines: { + '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(>[ + {'fizz': 'bar'}, + ]); + final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(['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(>[ + {'name': 'foobar', 'inputs': ['bar', 'baz']}, + ]); + final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(['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 {}, const {}); + }); + final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(['assemble', 'run', 'foobar']); + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText.trim(), 'build succeeded'); + })); + }); +} + +class MockBuildSystem extends Mock implements BuildSystem {} diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart index ee01dbf78d4..3714684bf23 100644 --- a/packages/flutter_tools/test/src/testbed.dart +++ b/packages/flutter_tools/test/src/testbed.dart @@ -76,7 +76,7 @@ class Testbed { : _setup = setup, _overrides = overrides; - final Future Function() _setup; + final FutureOr Function() _setup; final Map _overrides; /// Runs `test` within a tool zone.