From 2ab469952b63d69a2f961d4e90df546662ad547f Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 31 Jul 2019 16:19:22 -0700 Subject: [PATCH] Flutter assemble for macos take 2! (#36987) --- .../bin/macos_build_flutter_assets.sh | 54 ++++---- packages/flutter_tools/lib/src/artifacts.dart | 7 + .../lib/src/build_system/build_system.dart | 70 ++++++++-- .../lib/src/build_system/source.dart | 74 +++++----- .../lib/src/build_system/targets/assets.dart | 44 ++++++ .../lib/src/build_system/targets/dart.dart | 6 +- .../lib/src/build_system/targets/macos.dart | 118 +++++++++++++++- .../lib/src/commands/assemble.dart | 51 +++++-- .../lib/src/commands/attach.dart | 1 - .../lib/src/commands/build_macos.dart | 2 +- .../lib/src/commands/daemon.dart | 1 - .../flutter_tools/lib/src/commands/run.dart | 1 - .../flutter_tools/lib/src/ios/xcodeproj.dart | 7 +- .../lib/src/macos/application_package.dart | 59 ++------ .../lib/src/macos/build_macos.dart | 65 ++++++--- .../lib/src/macos/macos_device.dart | 21 +-- packages/flutter_tools/lib/src/project.dart | 8 ++ .../lib/src/resident_runner.dart | 19 ++- packages/flutter_tools/lib/src/run_hot.dart | 5 +- .../build_system/build_system_test.dart | 8 ++ .../build_system/source_test.dart | 8 ++ .../build_system/targets/assets_test.dart | 11 ++ .../build_system/targets/macos_test.dart | 130 +++++++++++++++--- .../general.shard/commands/assemble_test.dart | 46 ++++++- .../general.shard/commands/attach_test.dart | 1 - .../commands/build_macos_test.dart | 33 +++-- .../macos/macos_device_test.dart | 51 ++++--- .../general.shard/resident_runner_test.dart | 3 + 28 files changed, 679 insertions(+), 225 deletions(-) diff --git a/packages/flutter_tools/bin/macos_build_flutter_assets.sh b/packages/flutter_tools/bin/macos_build_flutter_assets.sh index d576a87abf6..1380fbf8a37 100755 --- a/packages/flutter_tools/bin/macos_build_flutter_assets.sh +++ b/packages/flutter_tools/bin/macos_build_flutter_assets.sh @@ -3,8 +3,6 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -# TODO(jonahwilliams): refactor this and xcode_backend.sh into one script -# once macOS supports the same configuration as iOS. RunCommand() { if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then echo "♦ $*" @@ -33,18 +31,6 @@ if [[ -n "$FLUTTER_TARGET" ]]; then target_path="${FLUTTER_TARGET}" fi -# Set the track widget creation flag. -track_widget_creation_flag="" -if [[ -n "$TRACK_WIDGET_CREATION" ]]; then - track_widget_creation_flag="--track-widget-creation" -fi - -# Copy the framework and handle local engine builds. -framework_name="FlutterMacOS.framework" -ephemeral_dir="${SOURCE_ROOT}/Flutter/ephemeral" -framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/darwin-x64" -flutter_framework="${framework_path}/${framework_name}" - if [[ -n "$FLUTTER_ENGINE" ]]; then flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}" fi @@ -63,22 +49,40 @@ if [[ -n "$LOCAL_ENGINE" ]]; then exit -1 fi local_engine_flag="--local-engine=${LOCAL_ENGINE}" - flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/${framework_name}" fi -RunCommand mkdir -p -- "$ephemeral_dir" -RunCommand rm -rf -- "${ephemeral_dir}/${framework_name}" -RunCommand cp -Rp -- "${flutter_framework}" "${ephemeral_dir}" - # Set the build mode build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")" +case "$build_mode" in + debug) + build_target="debug_macos_application" + ;; + profile) + build_target="profile_macos_application" + ;; + release) + build_target="release_macos_application" + ;; + *) + EchoError "Unknown build mode ${build_mode}" + exit -1 + ;; +esac +# The path where the input/output xcfilelists are stored. These are used by xcode +# to conditionally skip this script phase if neither have changed. +build_inputs_path="${SOURCE_ROOT}/Flutter/ephemeral/FlutterInputs.xcfilelist" +build_outputs_path="${SOURCE_ROOT}/Flutter/ephemeral/FlutterOutputs.xcfilelist" + +# TODO(jonahwilliams): support flavors https://github.com/flutter/flutter/issues/32923 RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \ ${verbose_flag} \ - build bundle \ - --target-platform=darwin-x64 \ - --target="${target_path}" \ - --${build_mode} \ - ${track_widget_creation_flag} \ ${flutter_engine_flag} \ - ${local_engine_flag} + ${local_engine_flag} \ + assemble \ + -dTargetFile="${target_path}" \ + -dTargetPlatform=darwin-x64 \ + -dBuildMode="${build_mode}" \ + --build-inputs="${build_inputs_path}" \ + --build-outputs="${build_outputs_path}" \ + "${build_target}" diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index a763fc40b52..711092ea792 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -54,6 +54,8 @@ enum Artifact { windowsDesktopPath, /// The root of the sky_engine package skyEnginePath, + /// The location of the macOS engine podspec file. + flutterMacOSPodspec, } String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMode mode ]) { @@ -119,6 +121,8 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo return ''; case Artifact.skyEnginePath: return 'sky_engine'; + case Artifact.flutterMacOSPodspec: + return 'FlutterMacOS.podspec'; } assert(false, 'Invalid artifact $artifact.'); return null; @@ -269,6 +273,7 @@ class CachedArtifacts extends Artifacts { case Artifact.flutterMacOSFramework: case Artifact.linuxDesktopPath: case Artifact.windowsDesktopPath: + case Artifact.flutterMacOSPodspec: final String engineArtifactsPath = cache.getArtifactDirectory('engine').path; final String platformDirName = getNameForTargetPlatform(platform); return fs.path.join(engineArtifactsPath, platformDirName, _artifactToFileName(artifact, platform, mode)); @@ -384,6 +389,8 @@ class LocalEngineArtifacts extends Artifacts { return fs.path.join(_hostEngineOutPath, artifactFileName); case Artifact.skyEnginePath: return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', artifactFileName); + case Artifact.flutterMacOSPodspec: + return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact)); } assert(false, 'Invalid artifact $artifact.'); 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 index 5d0a88b8658..c3cf8cd90eb 100644 --- a/packages/flutter_tools/lib/src/build_system/build_system.dart +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -171,7 +171,7 @@ abstract class Target { } } - // For each outut, first determine if we've already computed the hash + // For each output, first determine if we've already computed the hash // for it. Then collect it to be sent off for hashing as a group. for (String previousOutput in previousOutputs) { final File file = fs.file(previousOutput); @@ -436,11 +436,19 @@ class Environment { /// The result information from the build system. class BuildResult { - BuildResult(this.success, this.exceptions, this.performance); + BuildResult({ + @required this.success, + this.exceptions = const {}, + this.performance = const {}, + this.inputFiles = const [], + this.outputFiles = const [], + }); final bool success; final Map exceptions; final Map performance; + final List inputFiles; + final List outputFiles; bool get hasException => exceptions.isNotEmpty; } @@ -472,10 +480,31 @@ class BuildSystem { // Always persist the file cache to disk. fileCache.persist(); } + // TODO(jonahwilliams): this is a bit of a hack, due to various parts of + // the flutter tool writing these files unconditionally. Since Xcode uses + // timestamps to track files, this leads to unecessary rebuilds if they + // are included. Once all the places that write these files have been + // tracked down and moved into assemble, these checks should be removable. + { + buildInstance.inputFiles.removeWhere((String path, File file) { + return path.contains('pubspec.yaml') || + path.contains('.flutter-plugins') || + path.contains('xcconfig'); + }); + buildInstance.outputFiles.removeWhere((String path, File file) { + return path.contains('pubspec.yaml') || + path.contains('.flutter-plugins') || + path.contains('xcconfig'); + }); + } return BuildResult( - passed, - buildInstance.exceptionMeasurements, - buildInstance.stepTimings, + success: passed, + exceptions: buildInstance.exceptionMeasurements, + performance: buildInstance.stepTimings, + inputFiles: buildInstance.inputFiles.values.toList() + ..sort((File a, File b) => a.path.compareTo(b.path)), + outputFiles: buildInstance.outputFiles.values.toList() + ..sort((File a, File b) => a.path.compareTo(b.path)), ); } } @@ -490,6 +519,8 @@ class _BuildInstance { final Map> pending = >{}; final Environment environment; final FileHashStore fileCache; + final Map inputFiles = {}; + final Map outputFiles = {}; // Timings collected during target invocation. final Map stepTimings = {}; @@ -514,18 +545,41 @@ class _BuildInstance { try { final List inputs = target.resolveInputs(environment); final bool canSkip = await target.computeChanges(inputs, environment, fileCache); + for (File input in inputs) { + // The build system should produce a list of aggregate input and output + // files for the overall build. The goal is to provide this to a hosting + // build system, such as Xcode, to configure logic for when to skip the + // rule/phase which contains the flutter build. When looking at the + // inputs and outputs for the individual rules, we need to be careful to + // remove inputs that were actually output from previous build steps. + // This indicates that the file is actual an output or intermediary. If + // these files are included as both inputs and outputs then it isn't + // possible to construct a DAG describing the build. + final String resolvedPath = input.resolveSymbolicLinksSync(); + if (outputFiles.containsKey(resolvedPath)) { + continue; + } + inputFiles[resolvedPath] = input; + } if (canSkip) { skipped = true; printStatus('Skipping target: ${target.name}'); + final List outputs = target.resolveOutputs(environment, implicit: true); + for (File output in outputs) { + outputFiles[output.resolveSymbolicLinksSync()] = output; + } } else { printStatus('${target.name}: Starting'); await target.build(inputs, environment); printStatus('${target.name}: Complete'); - final List outputs = target.resolveOutputs(environment); + final List outputs = target.resolveOutputs(environment, implicit: true); // Update hashes for output files. await fileCache.hashFiles(outputs); target._writeStamp(inputs, outputs, environment); + for (File output in outputs) { + outputFiles[output.resolveSymbolicLinksSync()] = output; + } } } catch (exception, stackTrace) { target.clearStamp(environment); @@ -554,10 +608,10 @@ class ExceptionMeasurement { /// Helper class to collect measurement data. class PerformanceMeasurement { - PerformanceMeasurement(this.target, this.elapsedMilliseconds, this.skiped, this.passed); + PerformanceMeasurement(this.target, this.elapsedMilliseconds, this.skipped, this.passed); final int elapsedMilliseconds; final String target; - final bool skiped; + final bool skipped; final bool passed; } diff --git a/packages/flutter_tools/lib/src/build_system/source.dart b/packages/flutter_tools/lib/src/build_system/source.dart index a9727e808f8..6aeda2c30fb 100644 --- a/packages/flutter_tools/lib/src/build_system/source.dart +++ b/packages/flutter_tools/lib/src/build_system/source.dart @@ -38,8 +38,10 @@ class SourceVisitor { /// Visit a [Source] which contains a file uri. /// - /// The uri may that may include constants defined in an [Environment]. - void visitPattern(String pattern) { + /// The uri may include constants defined in an [Environment]. If + /// [optional] is true, the file is not required to exist. In this case, it + /// is never resolved as an input. + void visitPattern(String pattern, bool optional) { // perform substitution of the environmental values and then // of the local values. final List segments = []; @@ -74,38 +76,41 @@ class SourceVisitor { } 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 (!hasWildcard) { + if (optional && !fs.isFileSync(filePath)) { + return; } - 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); - } + sources.add(fs.file(fs.path.normalize(filePath))); + return; + } + // 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 wildcardSegments = wildcardFile.split('*'); + if (wildcardSegments.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 (wildcardSegments.isEmpty) { + sources.add(fs.file(entity.absolute)); + } else if (wildcardSegments.length == 1) { + if (filename.startsWith(wildcardSegments[0]) || + filename.endsWith(wildcardSegments[0])) { + sources.add(entity.absolute); + } + } else if (filename.startsWith(wildcardSegments[0])) { + if (filename.substring(wildcardSegments[0].length).endsWith(wildcardSegments[1])) { + sources.add(entity.absolute); } } - } else { - sources.add(fs.file(fs.path.normalize(filePath))); } } @@ -139,7 +144,7 @@ class SourceVisitor { abstract class Source { /// This source is a file-uri which contains some references to magic /// environment variables. - const factory Source.pattern(String pattern) = _PatternSource; + const factory Source.pattern(String pattern, { bool optional }) = _PatternSource; /// This source is produced by invoking the provided function. const factory Source.function(InputFunction function) = _FunctionSource; @@ -203,12 +208,13 @@ class _FunctionSource implements Source { } class _PatternSource implements Source { - const _PatternSource(this.value); + const _PatternSource(this.value, { this.optional = false }); final String value; + final bool optional; @override - void accept(SourceVisitor visitor) => visitor.visitPattern(value); + void accept(SourceVisitor visitor) => visitor.visitPattern(value, optional); @override bool get implicit => value.contains('*'); diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart index 7b21df8f524..934fc0fa4c4 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/assets.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart @@ -7,6 +7,8 @@ import 'package:pool/pool.dart'; import '../../asset.dart'; import '../../base/file_system.dart'; import '../../devfs.dart'; +import '../../plugins.dart'; +import '../../project.dart'; import '../build_system.dart'; /// The copying logic for flutter assets. @@ -100,3 +102,45 @@ class CopyAssets extends Target { })); } } + +/// Rewrites the `.flutter-plugins` file of [project] based on the plugin +/// dependencies declared in `pubspec.yaml`. +// TODO(jonahwiliams): this should be per platform and located in build +// outputs. +class FlutterPlugins extends Target { + const FlutterPlugins(); + + @override + String get name => 'flutter_plugins'; + + @override + List get dependencies => const []; + + @override + List get inputs => const [ + Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/assets.dart'), + Source.pattern('{PROJECT_DIR}/pubspec.yaml'), + ]; + + @override + List get outputs => const [ + Source.pattern('{PROJECT_DIR}/.flutter-plugins') + ]; + + @override + Future build(List inputFiles, Environment environment) async { + // The pubspec may change for reasons other than plugins changing, so we compare + // the manifest before writing. Some hosting build systems use timestamps + // so we need to be careful to avoid tricking them into doing more work than + // necessary. + final FlutterProject project = FlutterProject.fromDirectory(environment.projectDir); + final List plugins = findPlugins(project); + final String pluginManifest = plugins + .map((Plugin p) => '${p.name}=${escapePath(p.path)}') + .join('\n'); + final File flutterPluginsFile = environment.projectDir.childFile('.flutter-plugins'); + if (!flutterPluginsFile.existsSync() || flutterPluginsFile.readAsStringSync() != pluginManifest) { + flutterPluginsFile.writeAsStringSync(pluginManifest); + } + } +} diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart index 6b7be2e4f97..2c2d4d7f306 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/dart.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart @@ -86,6 +86,9 @@ class KernelSnapshot extends Target { } final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); final String targetFile = environment.defines[kTargetFile] ?? fs.path.join('lib', 'main.dart'); + final String packagesPath = environment.projectDir.childFile('.packages').path; + final PackageUriMapper packageUriMapper = PackageUriMapper(targetFile, + packagesPath, null, null); final CompilerOutput output = await compiler.compile( sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode), @@ -95,7 +98,8 @@ class KernelSnapshot extends Target { targetProductVm: buildMode == BuildMode.release, outputFilePath: environment.buildDir.childFile('app.dill').path, depFilePath: null, - mainPath: targetFile, + packagesPath: packagesPath, + mainPath: packageUriMapper.map(targetFile)?.toString() ?? targetFile, ); if (output.errorCount != 0) { throw Exception('Errors during snapshot creation: $output'); diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index 3d14ca7a7ce..95d5edff4a0 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -6,10 +6,16 @@ import '../../artifacts.dart'; import '../../base/file_system.dart'; import '../../base/io.dart'; import '../../base/process_manager.dart'; +import '../../build_info.dart'; import '../../globals.dart'; +import '../../macos/cocoapods.dart'; +import '../../project.dart'; import '../build_system.dart'; +import '../exceptions.dart'; +import 'assets.dart'; +import 'dart.dart'; -const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/FlutterMacOS.framework'; +const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/ephemeral/FlutterMacOS.framework'; /// Copy the macOS framework to the correct copy dir by invoking 'cp -R'. /// @@ -63,6 +69,7 @@ class UnpackMacOS extends Target { .projectDir .childDirectory('macos') .childDirectory('Flutter') + .childDirectory('ephemeral') .childDirectory('FlutterMacOS.framework'); if (targetDirectory.existsSync()) { targetDirectory.deleteSync(recursive: true); @@ -78,3 +85,112 @@ class UnpackMacOS extends Target { } } } + +/// Tell cocoapods to re-fetch dependencies. +class DebugMacOSPodInstall extends Target { + const DebugMacOSPodInstall(); + + @override + String get name => 'debug_macos_pod_install'; + + @override + List get inputs => const [ + Source.artifact(Artifact.flutterMacOSPodspec, + platform: TargetPlatform.darwin_x64, + mode: BuildMode.debug + ), + Source.pattern('{PROJECT_DIR}/macos/Podfile', optional: true), + Source.pattern('{PROJECT_DIR}/macos/Runner.xcodeproj/project.pbxproj'), + Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/Flutter-Generated.xcconfig'), + ]; + + @override + List get outputs => const [ + // TODO(jonahwilliams): introduce configuration/planning phase to build. + // No outputs because Cocoapods is fully responsible for tracking. plus there + // is no concept of an optional output. Instead we will need a build config + // phase to conditionally add this rule so that it can be written properly. + ]; + + @override + List get dependencies => const [ + UnpackMacOS(), + FlutterPlugins(), + ]; + + @override + Future build(List inputFiles, Environment environment) async { + if (environment.defines[kBuildMode] == null) { + throw MissingDefineException(kBuildMode, 'debug_macos_pod_install'); + } + // If there is no podfile do not perform any pods actions. + if (!environment.projectDir.childDirectory('macos') + .childFile('Podfile').existsSync()) { + return; + } + final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); + final FlutterProject project = FlutterProject.fromDirectory(environment.projectDir); + final String enginePath = artifacts.getArtifactPath(Artifact.flutterMacOSPodspec, + mode: buildMode, platform: TargetPlatform.darwin_x64); + + await cocoaPods.processPods( + xcodeProject: project.macos, + engineDir: enginePath, + isSwift: true, + dependenciesChanged: true, + ); + } +} + +/// Build all of the artifacts for a debug macOS application. +class DebugMacOSApplication extends Target { + const DebugMacOSApplication(); + + @override + Future build(List inputFiles, Environment environment) async { + final File sourceFile = environment.buildDir.childFile('app.dill'); + final File destinationFile = environment.buildDir + .childDirectory('flutter_assets') + .childFile('kernel_blob.bin'); + if (!destinationFile.parent.existsSync()) { + destinationFile.parent.createSync(recursive: true); + } + sourceFile.copySync(destinationFile.path); + } + + @override + List get dependencies => const [ + FlutterPlugins(), + UnpackMacOS(), + KernelSnapshot(), + CopyAssets(), + DebugMacOSPodInstall(), + ]; + + @override + List get inputs => const [ + Source.pattern('{BUILD_DIR}/app.dill') + ]; + + @override + String get name => 'debug_macos_application'; + + @override + List get outputs => const [ + Source.pattern('{BUILD_DIR}/flutter_assets/kernel_blob.bin'), + ]; +} + +// TODO(jonahwilliams): real AOT implementation. +class ReleaseMacOSApplication extends DebugMacOSApplication { + const ReleaseMacOSApplication(); + + @override + String get name => 'release_macos_application'; +} +class ProfileMacOSApplication extends DebugMacOSApplication { + const ProfileMacOSApplication(); + + @override + String get name => 'profile_macos_application'; +} diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart index 78cb1ee6503..476d7889b15 100644 --- a/packages/flutter_tools/lib/src/commands/assemble.dart +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -2,8 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:meta/meta.dart'; + import '../base/common.dart'; import '../base/context.dart'; +import '../base/file_system.dart'; import '../build_system/build_system.dart'; import '../build_system/targets/assets.dart'; import '../build_system/targets/dart.dart'; @@ -29,6 +32,9 @@ const List _kDefaultTargets = [ AotElfRelease(), AotAssemblyProfile(), AotAssemblyRelease(), + DebugMacOSApplication(), + ProfileMacOSApplication(), + ReleaseMacOSApplication(), ]; /// Assemble provides a low level API to interact with the flutter tool build @@ -40,14 +46,14 @@ class AssembleCommand extends FlutterCommand { 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('build-inputs', help: 'A file path where a newline ' + 'separated file containing all inputs used will be written after a build.' + ' This file is not included as a build input or output. This file is not' + ' written if the build fails for any reason.'); + argParser.addOption('build-outputs', help: 'A file path where a newline ' + 'separated file containing all outputs used will be written after a build.' + ' This file is not included as a build input or output. This file is not' + ' written if the build fails for any reason.'); argParser.addOption( 'resource-pool-size', help: 'The maximum number of concurrent tasks the build system will run.' @@ -106,10 +112,33 @@ class AssembleCommand extends FlutterCommand { printError('Target ${data.key} failed: ${data.value.exception}'); printError('${data.value.exception}'); } - throwToolExit('build failed'); - } else { - printStatus('build succeeded'); + throwToolExit('build failed.'); + } + printStatus('build succeeded.'); + if (argResults.wasParsed('build-inputs')) { + writeListIfChanged(result.inputFiles, argResults['build-inputs']); + } + if (argResults.wasParsed('build-outputs')) { + writeListIfChanged(result.outputFiles, argResults['build-outputs']); } return null; } } + +@visibleForTesting +void writeListIfChanged(List files, String path) { + final File file = fs.file(path); + final StringBuffer buffer = StringBuffer(); + // These files are already sorted. + for (File file in files) { + buffer.writeln(file.resolveSymbolicLinksSync()); + } + final String newContents = buffer.toString(); + if (!file.existsSync()) { + file.writeAsStringSync(newContents); + } + final String currentContents = file.readAsStringSync(); + if (currentContents != newContents) { + file.writeAsStringSync(newContents); + } +} diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index 32521d73552..a9b1caf5c73 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -257,7 +257,6 @@ class AttachCommand extends FlutterCommand { device, flutterProject: flutterProject, trackWidgetCreation: argResults['track-widget-creation'], - dillOutputPath: argResults['output-dill'], fileSystemRoots: argResults['filesystem-root'], fileSystemScheme: argResults['filesystem-scheme'], viewFilter: argResults['isolate-filter'], diff --git a/packages/flutter_tools/lib/src/commands/build_macos.dart b/packages/flutter_tools/lib/src/commands/build_macos.dart index f87118651f0..dc27a170144 100644 --- a/packages/flutter_tools/lib/src/commands/build_macos.dart +++ b/packages/flutter_tools/lib/src/commands/build_macos.dart @@ -14,7 +14,7 @@ import '../project.dart'; import '../runner/flutter_command.dart' show FlutterCommandResult; import 'build.dart'; -/// A command to build a macos desktop target through a build shell script. +/// A command to build a macOS desktop target through a build shell script. class BuildMacosCommand extends BuildSubCommand { BuildMacosCommand() { usesTargetOption(); diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index a441ecf8786..e1bf5ec9b6f 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -406,7 +406,6 @@ class AppDomain extends Domain { device, flutterProject: flutterProject, trackWidgetCreation: trackWidgetCreation, - dillOutputPath: dillOutputPath, viewFilter: isolateFilter, target: target, buildMode: options.buildInfo.mode, diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 298386cd975..6e9d86933d2 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -397,7 +397,6 @@ class RunCommand extends RunCommandBase { device, flutterProject: flutterProject, trackWidgetCreation: argResults['track-widget-creation'], - dillOutputPath: argResults['output-dill'], fileSystemRoots: argResults['filesystem-root'], fileSystemScheme: argResults['filesystem-scheme'], viewFilter: argResults['isolate-filter'], diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index af9630890e2..bd9b6d1bbc5 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -48,13 +48,15 @@ Future updateGeneratedXcodeProperties({ String targetOverride, bool useMacOSConfig = false, bool setSymroot = true, + String buildDirOverride, }) async { final List xcodeBuildSettings = _xcodeBuildSettingsLines( project: project, buildInfo: buildInfo, targetOverride: targetOverride, useMacOSConfig: useMacOSConfig, - setSymroot: setSymroot + setSymroot: setSymroot, + buildDirOverride: buildDirOverride ); _updateGeneratedXcodePropertiesFile( @@ -121,6 +123,7 @@ List _xcodeBuildSettingsLines({ String targetOverride, bool useMacOSConfig = false, bool setSymroot = true, + String buildDirOverride, }) { final List xcodeBuildSettings = []; @@ -135,7 +138,7 @@ List _xcodeBuildSettingsLines({ xcodeBuildSettings.add('FLUTTER_TARGET=$targetOverride'); // The build outputs directory, relative to FLUTTER_APPLICATION_PATH. - xcodeBuildSettings.add('FLUTTER_BUILD_DIR=${getBuildDirectory()}'); + xcodeBuildSettings.add('FLUTTER_BUILD_DIR=${buildDirOverride ?? getBuildDirectory()}'); if (setSymroot) { xcodeBuildSettings.add('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}'); diff --git a/packages/flutter_tools/lib/src/macos/application_package.dart b/packages/flutter_tools/lib/src/macos/application_package.dart index cfa4abcd53e..000bb7f1149 100644 --- a/packages/flutter_tools/lib/src/macos/application_package.dart +++ b/packages/flutter_tools/lib/src/macos/application_package.dart @@ -6,7 +6,6 @@ import 'package:meta/meta.dart'; import '../application_package.dart'; import '../base/file_system.dart'; -import '../build_info.dart'; import '../globals.dart'; import '../ios/plist_utils.dart' as plist; import '../project.dart'; @@ -31,18 +30,17 @@ abstract class MacOSApp extends ApplicationPackage { /// which is expected to start the application and send the observatory /// port over stdout. factory MacOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) { - final _ExecutableAndId executableAndId = _executableFromBundle(applicationBinary); + final ExecutableAndId executableAndId = executableFromBundle(applicationBinary); final Directory applicationBundle = fs.directory(applicationBinary); return PrebuiltMacOSApp( bundleDir: applicationBundle, bundleName: applicationBundle.path, - projectBundleId: executableAndId.id, - executable: executableAndId.executable, + executableAndId: executableAndId, ); } /// Look up the executable name for a macOS application bundle. - static _ExecutableAndId _executableFromBundle(Directory applicationBundle) { + static ExecutableAndId executableFromBundle(Directory applicationBundle) { final FileSystemEntityType entityType = fs.typeSync(applicationBundle.path); if (entityType == FileSystemEntityType.notFound) { printError('File "${applicationBundle.path}" does not exist.'); @@ -75,40 +73,28 @@ abstract class MacOSApp extends ApplicationPackage { if (!fs.file(executable).existsSync()) { printError('Could not find macOS binary at $executable'); } - return _ExecutableAndId(executable, id); + return ExecutableAndId(executable, id); } @override String get displayName => id; - - String applicationBundle(BuildMode buildMode); - - String executable(BuildMode buildMode); } class PrebuiltMacOSApp extends MacOSApp { PrebuiltMacOSApp({ @required this.bundleDir, @required this.bundleName, - @required this.projectBundleId, - @required String executable, - }) : _executable = executable, - super(projectBundleId: projectBundleId); + @required this.executableAndId, + }); final Directory bundleDir; final String bundleName; - final String projectBundleId; - - final String _executable; + final ExecutableAndId executableAndId; @override String get name => bundleName; - @override - String applicationBundle(BuildMode buildMode) => bundleDir.path; - - @override - String executable(BuildMode buildMode) => _executable; + String get executable => executableAndId.executable; } class BuildableMacOSApp extends MacOSApp { @@ -118,35 +104,10 @@ class BuildableMacOSApp extends MacOSApp { @override String get name => 'macOS'; - - @override - String applicationBundle(BuildMode buildMode) { - final File appBundleNameFile = project.nameFile; - if (!appBundleNameFile.existsSync()) { - printError('Unable to find app name. ${appBundleNameFile.path} does not exist'); - return null; - } - return fs.path.join( - getMacOSBuildDirectory(), - 'Build', - 'Products', - buildMode == BuildMode.debug ? 'Debug' : 'Release', - appBundleNameFile.readAsStringSync().trim()); - } - - @override - String executable(BuildMode buildMode) { - final String directory = applicationBundle(buildMode); - if (directory == null) { - return null; - } - final _ExecutableAndId executableAndId = MacOSApp._executableFromBundle(fs.directory(directory)); - return executableAndId.executable; - } } -class _ExecutableAndId { - _ExecutableAndId(this.executable, this.id); +class ExecutableAndId { + ExecutableAndId(this.executable, this.id); final String executable; final String id; diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index 2b45525fcdb..a72a8b8838c 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -8,25 +8,34 @@ import '../base/io.dart'; import '../base/logger.dart'; import '../base/process_manager.dart'; import '../build_info.dart'; +import '../build_system/build_system.dart'; +import '../build_system/targets/dart.dart'; import '../convert.dart'; import '../globals.dart'; import '../ios/xcodeproj.dart'; import '../project.dart'; import '../reporting/reporting.dart'; +import 'application_package.dart'; -import 'cocoapod_utils.dart'; - -/// Builds the macOS project through xcodebuild. -// TODO(jonahwilliams): refactor to share code with the existing iOS code. -Future buildMacOS({ +/// Builds the macOS project through xcodebuild and returns the app bundle. +Future buildMacOS({ FlutterProject flutterProject, BuildInfo buildInfo, - String targetOverride, + String targetOverride = 'lib/main.dart', }) async { - final Directory flutterBuildDir = fs.directory(getMacOSBuildDirectory()); - if (!flutterBuildDir.existsSync()) { - flutterBuildDir.createSync(recursive: true); - } + // Create the environment used to process the build. This needs to match what + // is provided in bin/macos_build_flutter_assets.sh otherwise the directories + // will be different. + final Environment environment = Environment( + projectDir: flutterProject.directory, + buildDir: flutterProject.dartTool.childDirectory('flutter_build'), + defines: { + kBuildMode: buildInfo.isDebug == true ? 'debug' : 'release', + kTargetPlatform: 'darwin-x64', + kTargetFile: fs.file(targetOverride).absolute.path + }, + ); + // Write configuration to an xconfig file in a standard location. await updateGeneratedXcodeProperties( project: flutterProject, @@ -34,27 +43,34 @@ Future buildMacOS({ targetOverride: targetOverride, useMacOSConfig: true, setSymroot: false, + buildDirOverride: environment.buildDir.path, ); - await processPodsIfNeeded(flutterProject.macos, getMacOSBuildDirectory(), buildInfo.mode); - + // If the xcfilelists do not exist, create empty version. + if (!flutterProject.macos.inputFileList.existsSync()) { + flutterProject.macos.inputFileList.createSync(recursive: true); + } + if (!flutterProject.macos.outputFileList.existsSync()) { + flutterProject.macos.outputFileList.createSync(recursive: true); + } // Set debug or release mode. String config = 'Debug'; - if (buildInfo.isRelease) { + if (buildInfo.isRelease ?? false) { config = 'Release'; } - // Run build script provided by application. + // Invoke Xcode with correct configuration. final Stopwatch sw = Stopwatch()..start(); - final Process process = await processManager.start([ + final List command = [ '/usr/bin/env', 'xcrun', 'xcodebuild', '-workspace', flutterProject.macos.xcodeWorkspace.path, - '-configuration', '$config', + '-configuration', config, '-scheme', 'Runner', - '-derivedDataPath', flutterBuildDir.absolute.path, - 'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}', - 'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}', - ], runInShell: true); + '-derivedDataPath', environment.buildDir.path, + 'OBJROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Intermediates.noindex')}', + 'SYMROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Products')}', + ]; + final Process process = await processManager.start(command); final Status status = logger.startProgress( 'Building macOS application...', timeout: null, @@ -77,4 +93,13 @@ Future buildMacOS({ throwToolExit('Build process failed'); } flutterUsage.sendTiming('build', 'xcode-macos', Duration(milliseconds: sw.elapsedMilliseconds)); + final File appBundleNameFile = flutterProject.macos.nameFile; + final Directory bundleDir = fs.directory(fs.path.join( + environment.buildDir.path, + 'Build', + 'Products', + buildInfo.mode == BuildMode.debug ? 'Debug' : 'Release', + appBundleNameFile.readAsStringSync().trim(), + )); + return MacOSApp.fromPrebuiltApp(bundleDir); } diff --git a/packages/flutter_tools/lib/src/macos/macos_device.dart b/packages/flutter_tools/lib/src/macos/macos_device.dart index 27ca0bb981f..fb37cb6e980 100644 --- a/packages/flutter_tools/lib/src/macos/macos_device.dart +++ b/packages/flutter_tools/lib/src/macos/macos_device.dart @@ -80,10 +80,13 @@ class MacOSDevice extends Device { bool usesTerminalUi = true, bool ipv6 = false, }) async { + Cache.releaseLockEarly(); // Stop any running applications with the same executable. - if (!prebuiltApplication) { - Cache.releaseLockEarly(); - await buildMacOS( + PrebuiltMacOSApp prebuiltMacOSApp; + if (prebuiltApplication) { + prebuiltMacOSApp = package; + } else { + prebuiltMacOSApp = await buildMacOS( flutterProject: FlutterProject.current(), buildInfo: debuggingOptions?.buildInfo, targetOverride: mainPath, @@ -91,8 +94,7 @@ class MacOSDevice extends Device { } // Ensure that the executable is locatable. - final String executable = package.executable(debuggingOptions?.buildInfo?.mode); - if (executable == null) { + if (prebuiltMacOSApp == null) { printError('Unable to find executable to run'); return LaunchResult.failed(); } @@ -100,7 +102,7 @@ class MacOSDevice extends Device { // Make sure to call stop app after we've built. await stopApp(package); final Process process = await processManager.start([ - executable + prebuiltMacOSApp.executable, ]); if (debuggingOptions?.buildInfo?.isRelease == true) { return LaunchResult.succeeded(); @@ -111,7 +113,7 @@ class MacOSDevice extends Device { final Uri observatoryUri = await observatoryDiscovery.uri; // Bring app to foreground. await processManager.run([ - 'open', package.applicationBundle(debuggingOptions?.buildInfo?.mode), + 'open', prebuiltMacOSApp.bundleName, ]); return LaunchResult.succeeded(observatoryUri: observatoryUri); } catch (error) { @@ -125,9 +127,8 @@ class MacOSDevice extends Device { // TODO(jonahwilliams): implement using process manager. // currently we rely on killing the isolate taking down the application. @override - Future stopApp(covariant MacOSApp app) async { - // Assume debug for now. - return killProcess(app.executable(BuildMode.debug)); + Future stopApp(covariant PrebuiltMacOSApp app) async { + return killProcess(app.executable); } @override diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index d9d7a3eb8d9..513dcee4f5e 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -664,6 +664,14 @@ class MacOSProject implements XcodeBasedProject { /// checked in should live here. Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral'); + /// The xcfilelist used to track the inputs for the Flutter script phase in + /// the Xcode build. + File get inputFileList => ephemeralDirectory.childFile('FlutterInputs.xcfilelist'); + + /// The xcfilelist used to track the outputs for the Flutter script phase in + /// the Xcode build. + File get outputFileList => ephemeralDirectory.childFile('FlutterOutputs.xcfilelist'); + @override File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig'); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index da4cbe97222..88ab5dc2dd5 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -31,7 +31,6 @@ class FlutterDevice { FlutterDevice( this.device, { @required this.trackWidgetCreation, - this.dillOutputPath, this.fileSystemRoots, this.fileSystemScheme, this.viewFilter, @@ -56,7 +55,6 @@ class FlutterDevice { @required bool trackWidgetCreation, @required String target, @required BuildMode buildMode, - String dillOutputPath, List fileSystemRoots, String fileSystemScheme, String viewFilter, @@ -82,7 +80,6 @@ class FlutterDevice { return FlutterDevice( device, trackWidgetCreation: trackWidgetCreation, - dillOutputPath: dillOutputPath, fileSystemRoots: fileSystemRoots, fileSystemScheme:fileSystemScheme, viewFilter: viewFilter, @@ -99,7 +96,6 @@ class FlutterDevice { List vmServices; DevFS devFS; ApplicationPackage package; - String dillOutputPath; List fileSystemRoots; String fileSystemScheme; StreamSubscription _loggingSubscription; @@ -476,6 +472,7 @@ class FlutterDevice { bool fullRestart = false, String projectRootPath, String pathToReload, + @required String dillOutputPath, @required List invalidatedFiles, }) async { final Status devFSStatus = logger.startProgress( @@ -527,12 +524,25 @@ abstract class ResidentRunner { this.usesTerminalUi = true, this.stayResident = true, this.hotMode = true, + this.dillOutputPath, }) { _mainPath = findMainDartFile(target); _projectRootPath = projectRootPath ?? fs.currentDirectory.path; _packagesFilePath = packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath); _assetBundle = AssetBundleFactory.instance.createBundle(); + // TODO(jonahwilliams): this is transitionary logic to allow us to support + // platforms that are not yet using flutter assemble. In the "new world", + // builds are isolated based on a number of factors. Thus, we cannot assume + // that a debug build will create the expected `build/app.dill` file. For + // now, I'm working around this by just creating it if it is missing here. + // In the future, once build & run are more strongly separated, the build + // environment will be plumbed through so that it all comes from a single + // source of truth, the [Environment]. + final File dillOutput = fs.file(dillOutputPath ?? fs.path.join('build', 'app.dill')); + if (!dillOutput.existsSync()) { + dillOutput.createSync(recursive: true); + } } final List flutterDevices; @@ -542,6 +552,7 @@ abstract class ResidentRunner { final bool stayResident; final bool ipv6; final Completer _finished = Completer(); + final String dillOutputPath; bool _exited = false; bool hotMode ; String _packagesFilePath; diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 08a4956fb48..7a8105d01bd 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -63,7 +63,7 @@ class HotRunner extends ResidentRunner { this.hostIsIde = false, String projectRootPath, String packagesFilePath, - this.dillOutputPath, + String dillOutputPath, bool stayResident = true, bool ipv6 = false, }) : super(devices, @@ -74,13 +74,13 @@ class HotRunner extends ResidentRunner { packagesFilePath: packagesFilePath, stayResident: stayResident, hotMode: true, + dillOutputPath: dillOutputPath, ipv6: ipv6); final bool benchmarkMode; final File applicationBinary; final bool hostIsIde; bool _didAttach = false; - final String dillOutputPath; final Map> benchmarkData = >{}; // The initial launch is from a snapshot. @@ -304,6 +304,7 @@ class HotRunner extends ResidentRunner { projectRootPath: projectRootPath, pathToReload: getReloadPath(fullRestart: fullRestart), invalidatedFiles: invalidatedFiles, + dillOutputPath: dillOutputPath, )); } return results; diff --git a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart index ca49ef0af4f..e806251cc38 100644 --- a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart @@ -138,6 +138,14 @@ void main() { expect(stampContents['inputs'], ['/foo.dart']); })); + test('Creates a BuildResult with inputs and outputs', () => testbed.run(() async { + final BuildResult result = await buildSystem.build(fooTarget, environment); + + expect(result.inputFiles.single.path, fs.path.absolute('foo.dart')); + expect(result.outputFiles.single.path, + fs.path.absolute(fs.path.join(environment.buildDir.path, 'out'))); + })); + test('Does not re-invoke build if stamp is valid', () => testbed.run(() async { await buildSystem.build(fooTarget, environment); await buildSystem.build(fooTarget, environment); diff --git a/packages/flutter_tools/test/general.shard/build_system/source_test.dart b/packages/flutter_tools/test/general.shard/build_system/source_test.dart index d49b2e31c28..8d0c2a4beaf 100644 --- a/packages/flutter_tools/test/general.shard/build_system/source_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/source_test.dart @@ -135,6 +135,14 @@ void main() { expect(() => invalidBase.accept(visitor), throwsA(isInstanceOf())); })); + + test('can substitute optional files', () => testbed.run(() { + const Source missingSource = Source.pattern('{PROJECT_DIR}/foo', optional: true); + + expect(fs.file('foo').existsSync(), false); + missingSource.accept(visitor); + expect(visitor.sources, isEmpty); + })); } class TestBehavior extends SourceBehavior { diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart index 2541115f945..f4fbb50fe0f 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart @@ -65,4 +65,15 @@ flutter: // 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); })); + + test('FlutterPlugins updates required files as needed', () => testbed.run(() async { + fs.file('pubspec.yaml') + ..writeAsStringSync('name: foo\ndependencies:\n foo: any\n'); + + await const FlutterPlugins().build([], Environment( + projectDir: fs.currentDirectory, + )); + + expect(fs.file('.flutter-plugins').existsSync(), true); + })); } diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index 554f1477ba5..b6e2f765fc7 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -7,8 +7,11 @@ 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/exceptions.dart'; +import 'package:flutter_tools/src/build_system/targets/dart.dart'; import 'package:flutter_tools/src/build_system/targets/macos.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/macos/cocoapods.dart'; import 'package:mockito/mockito.dart'; import 'package:process/process.dart'; @@ -17,7 +20,6 @@ import '../../../src/testbed.dart'; void main() { Testbed testbed; - const BuildSystem buildSystem = BuildSystem(); Environment environment; MockPlatform mockPlatform; @@ -31,6 +33,7 @@ void main() { when(mockPlatform.isWindows).thenReturn(false); when(mockPlatform.isMacOS).thenReturn(true); when(mockPlatform.isLinux).thenReturn(false); + when(mockPlatform.environment).thenReturn(const {}); testbed = Testbed(setup: () { environment = Environment( projectDir: fs.currentDirectory, @@ -79,28 +82,117 @@ void main() { }); test('Copies files to correct cache directory', () => testbed.run(() async { - await buildSystem.build(const UnpackMacOS(), environment); + await const UnpackMacOS().build([], environment); - 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); + expect(fs.directory('macos/Flutter/ephemeral/FlutterMacOS.framework').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/FlutterMacOS').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEReshapeListener.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEView.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEViewController.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterChannels.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterCodecs.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterMacOS.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Modules/module.modulemap').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Resources/icudtl.dat').existsSync(), true); + expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Resources/info.plist').existsSync(), true); })); + + test('debug macOS application copies kernel blob', () => testbed.run(() async { + final String inputKernel = fs.path.join(environment.buildDir.path, 'app.dill'); + final String outputKernel = fs.path.join(environment.buildDir.path, 'flutter_assets', 'kernel_blob.bin'); + fs.file(inputKernel) + ..createSync(recursive: true) + ..writeAsStringSync('testing'); + + await const DebugMacOSApplication().build([], environment); + + expect(fs.file(outputKernel).readAsStringSync(), 'testing'); + })); + + test('profile macOS application copies kernel blob', () => testbed.run(() async { + final String inputKernel = fs.path.join(environment.buildDir.path, 'app.dill'); + final String outputKernel = fs.path.join(environment.buildDir.path, 'flutter_assets', 'kernel_blob.bin'); + fs.file(inputKernel) + ..createSync(recursive: true) + ..writeAsStringSync('testing'); + + await const ProfileMacOSApplication().build([], environment); + + expect(fs.file(outputKernel).readAsStringSync(), 'testing'); + })); + + test('release macOS application copies kernel blob', () => testbed.run(() async { + final String inputKernel = fs.path.join(environment.buildDir.path, 'app.dill'); + final String outputKernel = fs.path.join(environment.buildDir.path, 'flutter_assets', 'kernel_blob.bin'); + fs.file(inputKernel) + ..createSync(recursive: true) + ..writeAsStringSync('testing'); + + await const ReleaseMacOSApplication().build([], environment); + + expect(fs.file(outputKernel).readAsStringSync(), 'testing'); + })); + + // Changing target names will require a corresponding update in flutter_tools/bin/macos_build_flutter_assets.sh. + test('Target names match those expected by bin scripts', () => testbed.run(() async { + expect(const DebugMacOSApplication().name, 'debug_macos_application'); + expect(const ProfileMacOSApplication().name, 'profile_macos_application'); + expect(const ReleaseMacOSApplication().name, 'release_macos_application'); + })); + + + test('DebugMacOSPodInstall throws if missing build mode', () => testbed.run(() async { + expect(() => const DebugMacOSPodInstall().build([], environment), + throwsA(isInstanceOf())); + })); + + test('DebugMacOSPodInstall skips if podfile does not exist', () => testbed.run(() async { + await const DebugMacOSPodInstall().build([], Environment( + projectDir: fs.currentDirectory, + defines: { + kBuildMode: 'debug' + } + )); + + verifyNever(cocoaPods.processPods( + xcodeProject: anyNamed('xcodeProject'), + engineDir: anyNamed('engineDir'), + isSwift: true, + dependenciesChanged: true)); + }, overrides: { + CocoaPods: () => MockCocoaPods(), + })); + + test('DebugMacOSPodInstall invokes processPods with podfile', () => testbed.run(() async { + fs.file(fs.path.join('macos', 'Podfile')).createSync(recursive: true); + await const DebugMacOSPodInstall().build([], Environment( + projectDir: fs.currentDirectory, + defines: { + kBuildMode: 'debug' + } + )); + + verify(cocoaPods.processPods( + xcodeProject: anyNamed('xcodeProject'), + engineDir: anyNamed('engineDir'), + isSwift: true, + dependenciesChanged: true)).called(1); + }, overrides: { + CocoaPods: () => MockCocoaPods(), + })); + + test('b', () => testbed.run(() async { + + })); + } class MockPlatform extends Mock implements Platform {} - +class MockCocoaPods extends Mock implements CocoaPods {} class MockProcessManager extends Mock implements ProcessManager {} class FakeProcessResult implements ProcessResult { @override @@ -115,3 +207,5 @@ class FakeProcessResult implements ProcessResult { @override String stdout = ''; } + + diff --git a/packages/flutter_tools/test/general.shard/commands/assemble_test.dart b/packages/flutter_tools/test/general.shard/commands/assemble_test.dart index 61ea1a83059..638582ba1bd 100644 --- a/packages/flutter_tools/test/general.shard/commands/assemble_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/assemble_test.dart @@ -3,6 +3,7 @@ // 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_system/build_system.dart'; import 'package:flutter_tools/src/cache.dart'; @@ -31,13 +32,54 @@ void main() { test('Can run a build', () => testbed.run(() async { when(mockBuildSystem.build(any, any, buildSystemConfig: anyNamed('buildSystemConfig'))) .thenAnswer((Invocation invocation) async { - return BuildResult(true, const {}, const {}); + return BuildResult(success: true); }); final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); await commandRunner.run(['assemble', 'unpack_macos']); final BufferLogger bufferLogger = logger; - expect(bufferLogger.statusText.trim(), 'build succeeded'); + expect(bufferLogger.statusText.trim(), 'build succeeded.'); + })); + + test('Only writes input and output files when the values change', () => testbed.run(() async { + when(mockBuildSystem.build(any, any, buildSystemConfig: anyNamed('buildSystemConfig'))) + .thenAnswer((Invocation invocation) async { + return BuildResult( + success: true, + inputFiles: [fs.file('foo')..createSync()], + outputFiles: [fs.file('bar')..createSync()], + ); + }); + + final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(['assemble', '--build-outputs=outputs', '--build-inputs=inputs', 'unpack_macos']); + + final File inputs = fs.file('inputs'); + final File outputs = fs.file('outputs'); + expect(inputs.readAsStringSync(), contains('foo')); + expect(outputs.readAsStringSync(), contains('bar')); + + final DateTime theDistantPast = DateTime(1991, 8, 23); + inputs.setLastModifiedSync(theDistantPast); + outputs.setLastModifiedSync(theDistantPast); + await commandRunner.run(['assemble', '--build-outputs=outputs', '--build-inputs=inputs', 'unpack_macos']); + + expect(inputs.lastModifiedSync(), theDistantPast); + expect(outputs.lastModifiedSync(), theDistantPast); + + + when(mockBuildSystem.build(any, any, buildSystemConfig: anyNamed('buildSystemConfig'))) + .thenAnswer((Invocation invocation) async { + return BuildResult( + success: true, + inputFiles: [fs.file('foo'), fs.file('fizz')..createSync()], + outputFiles: [fs.file('bar')]); + }); + await commandRunner.run(['assemble', '--build-outputs=outputs', '--build-inputs=inputs', 'unpack_macos']); + + expect(inputs.readAsStringSync(), contains('foo')); + expect(inputs.readAsStringSync(), contains('fizz')); + expect(inputs.lastModifiedSync(), isNot(theDistantPast)); })); } diff --git a/packages/flutter_tools/test/general.shard/commands/attach_test.dart b/packages/flutter_tools/test/general.shard/commands/attach_test.dart index c708f1e573c..bfe29a98fe5 100644 --- a/packages/flutter_tools/test/general.shard/commands/attach_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/attach_test.dart @@ -168,7 +168,6 @@ void main() { // output dill, filesystem scheme, and filesystem root. final FlutterDevice flutterDevice = flutterDevices.first; - expect(flutterDevice.dillOutputPath, outputDill); expect(flutterDevice.fileSystemScheme, filesystemScheme); expect(flutterDevice.fileSystemRoots, const [filesystemRoot]); }, overrides: { diff --git a/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart b/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart index 462e2d83e28..63f76c65d66 100644 --- a/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart @@ -8,7 +8,8 @@ import 'package:flutter_tools/src/base/common.dart'; 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/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/targets/dart.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/build.dart'; import 'package:flutter_tools/src/features.dart'; @@ -86,25 +87,35 @@ void main() { fs.file('.packages').createSync(); fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); final FlutterProject flutterProject = FlutterProject.fromDirectory(fs.currentDirectory); - final Directory flutterBuildDir = fs.directory(getMacOSBuildDirectory()); - + final Environment environment = Environment( + projectDir: flutterProject.directory, + buildDir: flutterProject.dartTool.childDirectory('flutter_build'), + defines: { + kBuildMode: 'release', + kTargetFile: fs.path.absolute(fs.path.join('lib', 'main.dart')), + kTargetPlatform: 'darwin-x64', + } + ); when(mockProcessManager.start([ '/usr/bin/env', 'xcrun', 'xcodebuild', '-workspace', flutterProject.macos.xcodeWorkspace.path, - '-configuration', 'Debug', + '-configuration', 'Release', '-scheme', 'Runner', - '-derivedDataPath', flutterBuildDir.absolute.path, - 'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}', - 'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}', - ], runInShell: true)).thenAnswer((Invocation invocation) async { + '-derivedDataPath', environment.buildDir.path, + 'OBJROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Intermediates.noindex')}', + 'SYMROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Products')}', + ])).thenAnswer((Invocation invocation) async { + fs.file(fs.path.join('macos', 'Flutter', 'ephemeral', '.app_filename')) + ..createSync(recursive: true) + ..writeAsStringSync('example.app'); return mockProcess; }); - await createTestCommandRunner(command).run( - const ['build', 'macos'] - ); + expect(createTestCommandRunner(command).run( + const ['build', 'macos', '--release'] + ), throwsA(isInstanceOf())); }, overrides: { FileSystem: () => memoryFilesystem, ProcessManager: () => mockProcessManager, diff --git a/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart index 2dcb59edd75..d0d7c3bc648 100644 --- a/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart @@ -36,7 +36,6 @@ void main() { testUsingContext('defaults', () async { final MockMacOSApp mockMacOSApp = MockMacOSApp(); - when(mockMacOSApp.executable(any)).thenReturn('foo'); expect(await device.targetPlatform, TargetPlatform.darwin_x64); expect(device.name, 'macOS'); expect(await device.installApp(mockMacOSApp), true); @@ -54,7 +53,7 @@ void main() { tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applications/foo '''; final MockMacOSApp mockMacOSApp = MockMacOSApp(); - when(mockMacOSApp.executable(any)).thenReturn('/Applications/foo'); + when(mockMacOSApp.executable).thenReturn('tester'); when(mockProcessManager.run(['ps', 'aux'])).thenAnswer((Invocation invocation) async { return ProcessResult(1, 0, psOut, ''); }); @@ -68,27 +67,35 @@ tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applica }); group('startApp', () { - final MockMacOSApp macOSApp = MockMacOSApp(); - final MockFileSystem mockFileSystem = MockFileSystem(); - final MockProcessManager mockProcessManager = MockProcessManager(); - final MockFile mockFile = MockFile(); - when(macOSApp.executable(any)).thenReturn('test'); - when(mockFileSystem.file('test')).thenReturn(mockFile); - when(mockFile.existsSync()).thenReturn(true); - when(mockProcessManager.start(['test'])).thenAnswer((Invocation invocation) async { - return FakeProcess( - exitCode: Completer().future, - stdout: Stream>.fromIterable(>[ - utf8.encode('Observatory listening on http://127.0.0.1/0\n'), - ]), - stderr: const Stream>.empty(), - ); - }); - when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async { - return ProcessResult(0, 1, '', ''); + MockMacOSApp macOSApp; + MockFileSystem mockFileSystem; + MockProcessManager mockProcessManager; + MockFile mockFile; + + + setUp(() { + macOSApp = MockMacOSApp(); + mockFileSystem = MockFileSystem(); + mockProcessManager = MockProcessManager(); + mockFile = MockFile(); + when(mockFileSystem.file('test')).thenReturn(mockFile); + when(mockFile.existsSync()).thenReturn(true); + when(macOSApp.executable).thenReturn('test'); + when(mockProcessManager.start(['test'])).thenAnswer((Invocation invocation) async { + return FakeProcess( + exitCode: Completer().future, + stdout: Stream>.fromIterable(>[ + utf8.encode('Observatory listening on http://127.0.0.1/0\n'), + ]), + stderr: const Stream>.empty(), + ); + }); + when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async { + return ProcessResult(0, 1, '', ''); + }); }); - testUsingContext('Can run from prebuilt application', () async { + testUsingContext('can run from prebuilt application', () async { final LaunchResult result = await device.startApp(macOSApp, prebuiltApplication: true); expect(result.started, true); expect(result.observatoryUri, Uri.parse('http://127.0.0.1/0')); @@ -137,7 +144,7 @@ tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applica class MockPlatform extends Mock implements Platform {} -class MockMacOSApp extends Mock implements MacOSApp {} +class MockMacOSApp extends Mock implements PrebuiltMacOSApp {} class MockFileSystem extends Mock implements FileSystem {} diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 644f828ffe1..9a1a7f74ade 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -68,6 +68,7 @@ void main() { fullRestart: anyNamed('fullRestart'), projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'), + dillOutputPath: anyNamed('dillOutputPath'), )).thenAnswer((Invocation invocation) async { return UpdateFSReport( success: true, @@ -175,6 +176,7 @@ void main() { projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'), invalidatedFiles: anyNamed('invalidatedFiles'), + dillOutputPath: anyNamed('dillOutputPath'), )).thenThrow(RpcException(666, 'something bad happened')); final OperationResult result = await residentRunner.restart(fullRestart: false); @@ -279,6 +281,7 @@ void main() { projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'), invalidatedFiles: anyNamed('invalidatedFiles'), + dillOutputPath: anyNamed('dillOutputPath'), )).thenThrow(RpcException(666, 'something bad happened')); final OperationResult result = await residentRunner.restart(fullRestart: true);