Flutter assemble for macos take 2! (#36987)

This commit is contained in:
Jonah Williams 2019-07-31 16:19:22 -07:00 committed by GitHub
parent dd1fb3bcb7
commit 2ab469952b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 679 additions and 225 deletions

View file

@ -3,8 +3,6 @@
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # 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() { RunCommand() {
if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
echo "$*" echo "$*"
@ -33,18 +31,6 @@ if [[ -n "$FLUTTER_TARGET" ]]; then
target_path="${FLUTTER_TARGET}" target_path="${FLUTTER_TARGET}"
fi 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 if [[ -n "$FLUTTER_ENGINE" ]]; then
flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}" flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}"
fi fi
@ -63,22 +49,40 @@ if [[ -n "$LOCAL_ENGINE" ]]; then
exit -1 exit -1
fi fi
local_engine_flag="--local-engine=${LOCAL_ENGINE}" local_engine_flag="--local-engine=${LOCAL_ENGINE}"
flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/${framework_name}"
fi fi
RunCommand mkdir -p -- "$ephemeral_dir"
RunCommand rm -rf -- "${ephemeral_dir}/${framework_name}"
RunCommand cp -Rp -- "${flutter_framework}" "${ephemeral_dir}"
# Set the build mode # Set the build mode
build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")" 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 \ RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \
${verbose_flag} \ ${verbose_flag} \
build bundle \
--target-platform=darwin-x64 \
--target="${target_path}" \
--${build_mode} \
${track_widget_creation_flag} \
${flutter_engine_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}"

View file

@ -54,6 +54,8 @@ enum Artifact {
windowsDesktopPath, windowsDesktopPath,
/// The root of the sky_engine package /// The root of the sky_engine package
skyEnginePath, skyEnginePath,
/// The location of the macOS engine podspec file.
flutterMacOSPodspec,
} }
String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMode mode ]) { String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMode mode ]) {
@ -119,6 +121,8 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo
return ''; return '';
case Artifact.skyEnginePath: case Artifact.skyEnginePath:
return 'sky_engine'; return 'sky_engine';
case Artifact.flutterMacOSPodspec:
return 'FlutterMacOS.podspec';
} }
assert(false, 'Invalid artifact $artifact.'); assert(false, 'Invalid artifact $artifact.');
return null; return null;
@ -269,6 +273,7 @@ class CachedArtifacts extends Artifacts {
case Artifact.flutterMacOSFramework: case Artifact.flutterMacOSFramework:
case Artifact.linuxDesktopPath: case Artifact.linuxDesktopPath:
case Artifact.windowsDesktopPath: case Artifact.windowsDesktopPath:
case Artifact.flutterMacOSPodspec:
final String engineArtifactsPath = cache.getArtifactDirectory('engine').path; final String engineArtifactsPath = cache.getArtifactDirectory('engine').path;
final String platformDirName = getNameForTargetPlatform(platform); final String platformDirName = getNameForTargetPlatform(platform);
return fs.path.join(engineArtifactsPath, platformDirName, _artifactToFileName(artifact, platform, mode)); return fs.path.join(engineArtifactsPath, platformDirName, _artifactToFileName(artifact, platform, mode));
@ -384,6 +389,8 @@ class LocalEngineArtifacts extends Artifacts {
return fs.path.join(_hostEngineOutPath, artifactFileName); return fs.path.join(_hostEngineOutPath, artifactFileName);
case Artifact.skyEnginePath: case Artifact.skyEnginePath:
return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', artifactFileName); return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', artifactFileName);
case Artifact.flutterMacOSPodspec:
return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
} }
assert(false, 'Invalid artifact $artifact.'); assert(false, 'Invalid artifact $artifact.');
return null; return null;

View file

@ -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 it. Then collect it to be sent off for hashing as a group.
for (String previousOutput in previousOutputs) { for (String previousOutput in previousOutputs) {
final File file = fs.file(previousOutput); final File file = fs.file(previousOutput);
@ -436,11 +436,19 @@ class Environment {
/// The result information from the build system. /// The result information from the build system.
class BuildResult { class BuildResult {
BuildResult(this.success, this.exceptions, this.performance); BuildResult({
@required this.success,
this.exceptions = const <String, ExceptionMeasurement>{},
this.performance = const <String, PerformanceMeasurement>{},
this.inputFiles = const <File>[],
this.outputFiles = const <File>[],
});
final bool success; final bool success;
final Map<String, ExceptionMeasurement> exceptions; final Map<String, ExceptionMeasurement> exceptions;
final Map<String, PerformanceMeasurement> performance; final Map<String, PerformanceMeasurement> performance;
final List<File> inputFiles;
final List<File> outputFiles;
bool get hasException => exceptions.isNotEmpty; bool get hasException => exceptions.isNotEmpty;
} }
@ -472,10 +480,31 @@ class BuildSystem {
// Always persist the file cache to disk. // Always persist the file cache to disk.
fileCache.persist(); 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( return BuildResult(
passed, success: passed,
buildInstance.exceptionMeasurements, exceptions: buildInstance.exceptionMeasurements,
buildInstance.stepTimings, 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<String, AsyncMemoizer<void>> pending = <String, AsyncMemoizer<void>>{}; final Map<String, AsyncMemoizer<void>> pending = <String, AsyncMemoizer<void>>{};
final Environment environment; final Environment environment;
final FileHashStore fileCache; final FileHashStore fileCache;
final Map<String, File> inputFiles = <String, File>{};
final Map<String, File> outputFiles = <String, File>{};
// Timings collected during target invocation. // Timings collected during target invocation.
final Map<String, PerformanceMeasurement> stepTimings = <String, PerformanceMeasurement>{}; final Map<String, PerformanceMeasurement> stepTimings = <String, PerformanceMeasurement>{};
@ -514,18 +545,41 @@ class _BuildInstance {
try { try {
final List<File> inputs = target.resolveInputs(environment); final List<File> inputs = target.resolveInputs(environment);
final bool canSkip = await target.computeChanges(inputs, environment, fileCache); 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) { if (canSkip) {
skipped = true; skipped = true;
printStatus('Skipping target: ${target.name}'); printStatus('Skipping target: ${target.name}');
final List<File> outputs = target.resolveOutputs(environment, implicit: true);
for (File output in outputs) {
outputFiles[output.resolveSymbolicLinksSync()] = output;
}
} else { } else {
printStatus('${target.name}: Starting'); printStatus('${target.name}: Starting');
await target.build(inputs, environment); await target.build(inputs, environment);
printStatus('${target.name}: Complete'); printStatus('${target.name}: Complete');
final List<File> outputs = target.resolveOutputs(environment); final List<File> outputs = target.resolveOutputs(environment, implicit: true);
// Update hashes for output files. // Update hashes for output files.
await fileCache.hashFiles(outputs); await fileCache.hashFiles(outputs);
target._writeStamp(inputs, outputs, environment); target._writeStamp(inputs, outputs, environment);
for (File output in outputs) {
outputFiles[output.resolveSymbolicLinksSync()] = output;
}
} }
} catch (exception, stackTrace) { } catch (exception, stackTrace) {
target.clearStamp(environment); target.clearStamp(environment);
@ -554,10 +608,10 @@ class ExceptionMeasurement {
/// Helper class to collect measurement data. /// Helper class to collect measurement data.
class PerformanceMeasurement { class PerformanceMeasurement {
PerformanceMeasurement(this.target, this.elapsedMilliseconds, this.skiped, this.passed); PerformanceMeasurement(this.target, this.elapsedMilliseconds, this.skipped, this.passed);
final int elapsedMilliseconds; final int elapsedMilliseconds;
final String target; final String target;
final bool skiped; final bool skipped;
final bool passed; final bool passed;
} }

View file

@ -38,8 +38,10 @@ class SourceVisitor {
/// Visit a [Source] which contains a file uri. /// Visit a [Source] which contains a file uri.
/// ///
/// The uri may that may include constants defined in an [Environment]. /// The uri may include constants defined in an [Environment]. If
void visitPattern(String pattern) { /// [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 // perform substitution of the environmental values and then
// of the local values. // of the local values.
final List<String> segments = <String>[]; final List<String> segments = <String>[];
@ -74,38 +76,41 @@ class SourceVisitor {
} }
rawParts.skip(1).forEach(segments.add); rawParts.skip(1).forEach(segments.add);
final String filePath = fs.path.joinAll(segments); final String filePath = fs.path.joinAll(segments);
if (hasWildcard) { if (!hasWildcard) {
// Perform a simple match by splitting the wildcard containing file one if (optional && !fs.isFileSync(filePath)) {
// the `*`. For example, for `/*.dart`, we get [.dart]. We then check return;
// that part of the file matches. If there are values before and after
// the `*` we need to check that both match without overlapping. For
// example, `foo_*_.dart`. We want to match `foo_b_.dart` but not
// `foo_.dart`. To do so, we first subtract the first section from the
// string if the first segment matches.
final List<String> segments = wildcardFile.split('*');
if (segments.length > 2) {
throw InvalidPatternException(pattern);
} }
if (!fs.directory(filePath).existsSync()) { sources.add(fs.file(fs.path.normalize(filePath)));
throw Exception('$filePath does not exist!'); return;
} }
for (FileSystemEntity entity in fs.directory(filePath).listSync()) { // Perform a simple match by splitting the wildcard containing file one
final String filename = fs.path.basename(entity.path); // the `*`. For example, for `/*.dart`, we get [.dart]. We then check
if (segments.isEmpty) { // that part of the file matches. If there are values before and after
sources.add(fs.file(entity.absolute)); // the `*` we need to check that both match without overlapping. For
} else if (segments.length == 1) { // example, `foo_*_.dart`. We want to match `foo_b_.dart` but not
if (filename.startsWith(segments[0]) || // `foo_.dart`. To do so, we first subtract the first section from the
filename.endsWith(segments[0])) { // string if the first segment matches.
sources.add(entity.absolute); final List<String> wildcardSegments = wildcardFile.split('*');
} if (wildcardSegments.length > 2) {
} else if (filename.startsWith(segments[0])) { throw InvalidPatternException(pattern);
if (filename.substring(segments[0].length).endsWith(segments[1])) { }
sources.add(entity.absolute); 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 { abstract class Source {
/// This source is a file-uri which contains some references to magic /// This source is a file-uri which contains some references to magic
/// environment variables. /// 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. /// This source is produced by invoking the provided function.
const factory Source.function(InputFunction function) = _FunctionSource; const factory Source.function(InputFunction function) = _FunctionSource;
@ -203,12 +208,13 @@ class _FunctionSource implements Source {
} }
class _PatternSource implements Source { class _PatternSource implements Source {
const _PatternSource(this.value); const _PatternSource(this.value, { this.optional = false });
final String value; final String value;
final bool optional;
@override @override
void accept(SourceVisitor visitor) => visitor.visitPattern(value); void accept(SourceVisitor visitor) => visitor.visitPattern(value, optional);
@override @override
bool get implicit => value.contains('*'); bool get implicit => value.contains('*');

View file

@ -7,6 +7,8 @@ import 'package:pool/pool.dart';
import '../../asset.dart'; import '../../asset.dart';
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../devfs.dart'; import '../../devfs.dart';
import '../../plugins.dart';
import '../../project.dart';
import '../build_system.dart'; import '../build_system.dart';
/// The copying logic for flutter assets. /// 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<Target> get dependencies => const <Target>[];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/assets.dart'),
Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{PROJECT_DIR}/.flutter-plugins')
];
@override
Future<void> build(List<File> 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<Plugin> plugins = findPlugins(project);
final String pluginManifest = plugins
.map<String>((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);
}
}
}

View file

@ -86,6 +86,9 @@ class KernelSnapshot extends Target {
} }
final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
final String targetFile = environment.defines[kTargetFile] ?? fs.path.join('lib', 'main.dart'); 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( final CompilerOutput output = await compiler.compile(
sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode), sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
@ -95,7 +98,8 @@ class KernelSnapshot extends Target {
targetProductVm: buildMode == BuildMode.release, targetProductVm: buildMode == BuildMode.release,
outputFilePath: environment.buildDir.childFile('app.dill').path, outputFilePath: environment.buildDir.childFile('app.dill').path,
depFilePath: null, depFilePath: null,
mainPath: targetFile, packagesPath: packagesPath,
mainPath: packageUriMapper.map(targetFile)?.toString() ?? targetFile,
); );
if (output.errorCount != 0) { if (output.errorCount != 0) {
throw Exception('Errors during snapshot creation: $output'); throw Exception('Errors during snapshot creation: $output');

View file

@ -6,10 +6,16 @@ import '../../artifacts.dart';
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../base/io.dart'; import '../../base/io.dart';
import '../../base/process_manager.dart'; import '../../base/process_manager.dart';
import '../../build_info.dart';
import '../../globals.dart'; import '../../globals.dart';
import '../../macos/cocoapods.dart';
import '../../project.dart';
import '../build_system.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'. /// Copy the macOS framework to the correct copy dir by invoking 'cp -R'.
/// ///
@ -63,6 +69,7 @@ class UnpackMacOS extends Target {
.projectDir .projectDir
.childDirectory('macos') .childDirectory('macos')
.childDirectory('Flutter') .childDirectory('Flutter')
.childDirectory('ephemeral')
.childDirectory('FlutterMacOS.framework'); .childDirectory('FlutterMacOS.framework');
if (targetDirectory.existsSync()) { if (targetDirectory.existsSync()) {
targetDirectory.deleteSync(recursive: true); 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<Source> get inputs => const <Source>[
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<Source> get outputs => const <Source>[
// 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<Target> get dependencies => const <Target>[
UnpackMacOS(),
FlutterPlugins(),
];
@override
Future<void> build(List<File> 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<void> build(List<File> 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<Target> get dependencies => const <Target>[
FlutterPlugins(),
UnpackMacOS(),
KernelSnapshot(),
CopyAssets(),
DebugMacOSPodInstall(),
];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{BUILD_DIR}/app.dill')
];
@override
String get name => 'debug_macos_application';
@override
List<Source> get outputs => const <Source>[
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';
}

View file

@ -2,8 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:meta/meta.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/context.dart'; import '../base/context.dart';
import '../base/file_system.dart';
import '../build_system/build_system.dart'; import '../build_system/build_system.dart';
import '../build_system/targets/assets.dart'; import '../build_system/targets/assets.dart';
import '../build_system/targets/dart.dart'; import '../build_system/targets/dart.dart';
@ -29,6 +32,9 @@ const List<Target> _kDefaultTargets = <Target>[
AotElfRelease(), AotElfRelease(),
AotAssemblyProfile(), AotAssemblyProfile(),
AotAssemblyRelease(), AotAssemblyRelease(),
DebugMacOSApplication(),
ProfileMacOSApplication(),
ReleaseMacOSApplication(),
]; ];
/// Assemble provides a low level API to interact with the flutter tool build /// Assemble provides a low level API to interact with the flutter tool build
@ -40,14 +46,14 @@ class AssembleCommand extends FlutterCommand {
abbr: 'd', abbr: 'd',
help: 'Allows passing configuration to a target with --define=target=key=value.' help: 'Allows passing configuration to a target with --define=target=key=value.'
); );
argParser.addOption( argParser.addOption('build-inputs', help: 'A file path where a newline '
'build-mode', 'separated file containing all inputs used will be written after a build.'
allowed: const <String>[ ' This file is not included as a build input or output. This file is not'
'debug', ' written if the build fails for any reason.');
'profile', argParser.addOption('build-outputs', help: 'A file path where a newline '
'release', '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( argParser.addOption(
'resource-pool-size', 'resource-pool-size',
help: 'The maximum number of concurrent tasks the build system will run.' 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('Target ${data.key} failed: ${data.value.exception}');
printError('${data.value.exception}'); printError('${data.value.exception}');
} }
throwToolExit('build failed'); throwToolExit('build failed.');
} else { }
printStatus('build succeeded'); 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; return null;
} }
} }
@visibleForTesting
void writeListIfChanged(List<File> 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);
}
}

View file

@ -257,7 +257,6 @@ class AttachCommand extends FlutterCommand {
device, device,
flutterProject: flutterProject, flutterProject: flutterProject,
trackWidgetCreation: argResults['track-widget-creation'], trackWidgetCreation: argResults['track-widget-creation'],
dillOutputPath: argResults['output-dill'],
fileSystemRoots: argResults['filesystem-root'], fileSystemRoots: argResults['filesystem-root'],
fileSystemScheme: argResults['filesystem-scheme'], fileSystemScheme: argResults['filesystem-scheme'],
viewFilter: argResults['isolate-filter'], viewFilter: argResults['isolate-filter'],

View file

@ -14,7 +14,7 @@ import '../project.dart';
import '../runner/flutter_command.dart' show FlutterCommandResult; import '../runner/flutter_command.dart' show FlutterCommandResult;
import 'build.dart'; 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 { class BuildMacosCommand extends BuildSubCommand {
BuildMacosCommand() { BuildMacosCommand() {
usesTargetOption(); usesTargetOption();

View file

@ -406,7 +406,6 @@ class AppDomain extends Domain {
device, device,
flutterProject: flutterProject, flutterProject: flutterProject,
trackWidgetCreation: trackWidgetCreation, trackWidgetCreation: trackWidgetCreation,
dillOutputPath: dillOutputPath,
viewFilter: isolateFilter, viewFilter: isolateFilter,
target: target, target: target,
buildMode: options.buildInfo.mode, buildMode: options.buildInfo.mode,

View file

@ -397,7 +397,6 @@ class RunCommand extends RunCommandBase {
device, device,
flutterProject: flutterProject, flutterProject: flutterProject,
trackWidgetCreation: argResults['track-widget-creation'], trackWidgetCreation: argResults['track-widget-creation'],
dillOutputPath: argResults['output-dill'],
fileSystemRoots: argResults['filesystem-root'], fileSystemRoots: argResults['filesystem-root'],
fileSystemScheme: argResults['filesystem-scheme'], fileSystemScheme: argResults['filesystem-scheme'],
viewFilter: argResults['isolate-filter'], viewFilter: argResults['isolate-filter'],

View file

@ -48,13 +48,15 @@ Future<void> updateGeneratedXcodeProperties({
String targetOverride, String targetOverride,
bool useMacOSConfig = false, bool useMacOSConfig = false,
bool setSymroot = true, bool setSymroot = true,
String buildDirOverride,
}) async { }) async {
final List<String> xcodeBuildSettings = _xcodeBuildSettingsLines( final List<String> xcodeBuildSettings = _xcodeBuildSettingsLines(
project: project, project: project,
buildInfo: buildInfo, buildInfo: buildInfo,
targetOverride: targetOverride, targetOverride: targetOverride,
useMacOSConfig: useMacOSConfig, useMacOSConfig: useMacOSConfig,
setSymroot: setSymroot setSymroot: setSymroot,
buildDirOverride: buildDirOverride
); );
_updateGeneratedXcodePropertiesFile( _updateGeneratedXcodePropertiesFile(
@ -121,6 +123,7 @@ List<String> _xcodeBuildSettingsLines({
String targetOverride, String targetOverride,
bool useMacOSConfig = false, bool useMacOSConfig = false,
bool setSymroot = true, bool setSymroot = true,
String buildDirOverride,
}) { }) {
final List<String> xcodeBuildSettings = <String>[]; final List<String> xcodeBuildSettings = <String>[];
@ -135,7 +138,7 @@ List<String> _xcodeBuildSettingsLines({
xcodeBuildSettings.add('FLUTTER_TARGET=$targetOverride'); xcodeBuildSettings.add('FLUTTER_TARGET=$targetOverride');
// The build outputs directory, relative to FLUTTER_APPLICATION_PATH. // The build outputs directory, relative to FLUTTER_APPLICATION_PATH.
xcodeBuildSettings.add('FLUTTER_BUILD_DIR=${getBuildDirectory()}'); xcodeBuildSettings.add('FLUTTER_BUILD_DIR=${buildDirOverride ?? getBuildDirectory()}');
if (setSymroot) { if (setSymroot) {
xcodeBuildSettings.add('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}'); xcodeBuildSettings.add('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}');

View file

@ -6,7 +6,6 @@ import 'package:meta/meta.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../build_info.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/plist_utils.dart' as plist; import '../ios/plist_utils.dart' as plist;
import '../project.dart'; import '../project.dart';
@ -31,18 +30,17 @@ abstract class MacOSApp extends ApplicationPackage {
/// which is expected to start the application and send the observatory /// which is expected to start the application and send the observatory
/// port over stdout. /// port over stdout.
factory MacOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) { factory MacOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) {
final _ExecutableAndId executableAndId = _executableFromBundle(applicationBinary); final ExecutableAndId executableAndId = executableFromBundle(applicationBinary);
final Directory applicationBundle = fs.directory(applicationBinary); final Directory applicationBundle = fs.directory(applicationBinary);
return PrebuiltMacOSApp( return PrebuiltMacOSApp(
bundleDir: applicationBundle, bundleDir: applicationBundle,
bundleName: applicationBundle.path, bundleName: applicationBundle.path,
projectBundleId: executableAndId.id, executableAndId: executableAndId,
executable: executableAndId.executable,
); );
} }
/// Look up the executable name for a macOS application bundle. /// 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); final FileSystemEntityType entityType = fs.typeSync(applicationBundle.path);
if (entityType == FileSystemEntityType.notFound) { if (entityType == FileSystemEntityType.notFound) {
printError('File "${applicationBundle.path}" does not exist.'); printError('File "${applicationBundle.path}" does not exist.');
@ -75,40 +73,28 @@ abstract class MacOSApp extends ApplicationPackage {
if (!fs.file(executable).existsSync()) { if (!fs.file(executable).existsSync()) {
printError('Could not find macOS binary at $executable'); printError('Could not find macOS binary at $executable');
} }
return _ExecutableAndId(executable, id); return ExecutableAndId(executable, id);
} }
@override @override
String get displayName => id; String get displayName => id;
String applicationBundle(BuildMode buildMode);
String executable(BuildMode buildMode);
} }
class PrebuiltMacOSApp extends MacOSApp { class PrebuiltMacOSApp extends MacOSApp {
PrebuiltMacOSApp({ PrebuiltMacOSApp({
@required this.bundleDir, @required this.bundleDir,
@required this.bundleName, @required this.bundleName,
@required this.projectBundleId, @required this.executableAndId,
@required String executable, });
}) : _executable = executable,
super(projectBundleId: projectBundleId);
final Directory bundleDir; final Directory bundleDir;
final String bundleName; final String bundleName;
final String projectBundleId; final ExecutableAndId executableAndId;
final String _executable;
@override @override
String get name => bundleName; String get name => bundleName;
@override String get executable => executableAndId.executable;
String applicationBundle(BuildMode buildMode) => bundleDir.path;
@override
String executable(BuildMode buildMode) => _executable;
} }
class BuildableMacOSApp extends MacOSApp { class BuildableMacOSApp extends MacOSApp {
@ -118,35 +104,10 @@ class BuildableMacOSApp extends MacOSApp {
@override @override
String get name => 'macOS'; 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 { class ExecutableAndId {
_ExecutableAndId(this.executable, this.id); ExecutableAndId(this.executable, this.id);
final String executable; final String executable;
final String id; final String id;

View file

@ -8,25 +8,34 @@ import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/process_manager.dart'; import '../base/process_manager.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../build_system/build_system.dart';
import '../build_system/targets/dart.dart';
import '../convert.dart'; import '../convert.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/xcodeproj.dart'; import '../ios/xcodeproj.dart';
import '../project.dart'; import '../project.dart';
import '../reporting/reporting.dart'; import '../reporting/reporting.dart';
import 'application_package.dart';
import 'cocoapod_utils.dart'; /// Builds the macOS project through xcodebuild and returns the app bundle.
Future<PrebuiltMacOSApp> buildMacOS({
/// Builds the macOS project through xcodebuild.
// TODO(jonahwilliams): refactor to share code with the existing iOS code.
Future<void> buildMacOS({
FlutterProject flutterProject, FlutterProject flutterProject,
BuildInfo buildInfo, BuildInfo buildInfo,
String targetOverride, String targetOverride = 'lib/main.dart',
}) async { }) async {
final Directory flutterBuildDir = fs.directory(getMacOSBuildDirectory()); // Create the environment used to process the build. This needs to match what
if (!flutterBuildDir.existsSync()) { // is provided in bin/macos_build_flutter_assets.sh otherwise the directories
flutterBuildDir.createSync(recursive: true); // will be different.
} final Environment environment = Environment(
projectDir: flutterProject.directory,
buildDir: flutterProject.dartTool.childDirectory('flutter_build'),
defines: <String, String>{
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. // Write configuration to an xconfig file in a standard location.
await updateGeneratedXcodeProperties( await updateGeneratedXcodeProperties(
project: flutterProject, project: flutterProject,
@ -34,27 +43,34 @@ Future<void> buildMacOS({
targetOverride: targetOverride, targetOverride: targetOverride,
useMacOSConfig: true, useMacOSConfig: true,
setSymroot: false, 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. // Set debug or release mode.
String config = 'Debug'; String config = 'Debug';
if (buildInfo.isRelease) { if (buildInfo.isRelease ?? false) {
config = 'Release'; config = 'Release';
} }
// Run build script provided by application. // Invoke Xcode with correct configuration.
final Stopwatch sw = Stopwatch()..start(); final Stopwatch sw = Stopwatch()..start();
final Process process = await processManager.start(<String>[ final List<String> command = <String>[
'/usr/bin/env', '/usr/bin/env',
'xcrun', 'xcrun',
'xcodebuild', 'xcodebuild',
'-workspace', flutterProject.macos.xcodeWorkspace.path, '-workspace', flutterProject.macos.xcodeWorkspace.path,
'-configuration', '$config', '-configuration', config,
'-scheme', 'Runner', '-scheme', 'Runner',
'-derivedDataPath', flutterBuildDir.absolute.path, '-derivedDataPath', environment.buildDir.path,
'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}', 'OBJROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Intermediates.noindex')}',
'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}', 'SYMROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Products')}',
], runInShell: true); ];
final Process process = await processManager.start(command);
final Status status = logger.startProgress( final Status status = logger.startProgress(
'Building macOS application...', 'Building macOS application...',
timeout: null, timeout: null,
@ -77,4 +93,13 @@ Future<void> buildMacOS({
throwToolExit('Build process failed'); throwToolExit('Build process failed');
} }
flutterUsage.sendTiming('build', 'xcode-macos', Duration(milliseconds: sw.elapsedMilliseconds)); 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);
} }

View file

@ -80,10 +80,13 @@ class MacOSDevice extends Device {
bool usesTerminalUi = true, bool usesTerminalUi = true,
bool ipv6 = false, bool ipv6 = false,
}) async { }) async {
Cache.releaseLockEarly();
// Stop any running applications with the same executable. // Stop any running applications with the same executable.
if (!prebuiltApplication) { PrebuiltMacOSApp prebuiltMacOSApp;
Cache.releaseLockEarly(); if (prebuiltApplication) {
await buildMacOS( prebuiltMacOSApp = package;
} else {
prebuiltMacOSApp = await buildMacOS(
flutterProject: FlutterProject.current(), flutterProject: FlutterProject.current(),
buildInfo: debuggingOptions?.buildInfo, buildInfo: debuggingOptions?.buildInfo,
targetOverride: mainPath, targetOverride: mainPath,
@ -91,8 +94,7 @@ class MacOSDevice extends Device {
} }
// Ensure that the executable is locatable. // Ensure that the executable is locatable.
final String executable = package.executable(debuggingOptions?.buildInfo?.mode); if (prebuiltMacOSApp == null) {
if (executable == null) {
printError('Unable to find executable to run'); printError('Unable to find executable to run');
return LaunchResult.failed(); return LaunchResult.failed();
} }
@ -100,7 +102,7 @@ class MacOSDevice extends Device {
// Make sure to call stop app after we've built. // Make sure to call stop app after we've built.
await stopApp(package); await stopApp(package);
final Process process = await processManager.start(<String>[ final Process process = await processManager.start(<String>[
executable prebuiltMacOSApp.executable,
]); ]);
if (debuggingOptions?.buildInfo?.isRelease == true) { if (debuggingOptions?.buildInfo?.isRelease == true) {
return LaunchResult.succeeded(); return LaunchResult.succeeded();
@ -111,7 +113,7 @@ class MacOSDevice extends Device {
final Uri observatoryUri = await observatoryDiscovery.uri; final Uri observatoryUri = await observatoryDiscovery.uri;
// Bring app to foreground. // Bring app to foreground.
await processManager.run(<String>[ await processManager.run(<String>[
'open', package.applicationBundle(debuggingOptions?.buildInfo?.mode), 'open', prebuiltMacOSApp.bundleName,
]); ]);
return LaunchResult.succeeded(observatoryUri: observatoryUri); return LaunchResult.succeeded(observatoryUri: observatoryUri);
} catch (error) { } catch (error) {
@ -125,9 +127,8 @@ class MacOSDevice extends Device {
// TODO(jonahwilliams): implement using process manager. // TODO(jonahwilliams): implement using process manager.
// currently we rely on killing the isolate taking down the application. // currently we rely on killing the isolate taking down the application.
@override @override
Future<bool> stopApp(covariant MacOSApp app) async { Future<bool> stopApp(covariant PrebuiltMacOSApp app) async {
// Assume debug for now. return killProcess(app.executable);
return killProcess(app.executable(BuildMode.debug));
} }
@override @override

View file

@ -664,6 +664,14 @@ class MacOSProject implements XcodeBasedProject {
/// checked in should live here. /// checked in should live here.
Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral'); 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 @override
File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig'); File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig');

View file

@ -31,7 +31,6 @@ class FlutterDevice {
FlutterDevice( FlutterDevice(
this.device, { this.device, {
@required this.trackWidgetCreation, @required this.trackWidgetCreation,
this.dillOutputPath,
this.fileSystemRoots, this.fileSystemRoots,
this.fileSystemScheme, this.fileSystemScheme,
this.viewFilter, this.viewFilter,
@ -56,7 +55,6 @@ class FlutterDevice {
@required bool trackWidgetCreation, @required bool trackWidgetCreation,
@required String target, @required String target,
@required BuildMode buildMode, @required BuildMode buildMode,
String dillOutputPath,
List<String> fileSystemRoots, List<String> fileSystemRoots,
String fileSystemScheme, String fileSystemScheme,
String viewFilter, String viewFilter,
@ -82,7 +80,6 @@ class FlutterDevice {
return FlutterDevice( return FlutterDevice(
device, device,
trackWidgetCreation: trackWidgetCreation, trackWidgetCreation: trackWidgetCreation,
dillOutputPath: dillOutputPath,
fileSystemRoots: fileSystemRoots, fileSystemRoots: fileSystemRoots,
fileSystemScheme:fileSystemScheme, fileSystemScheme:fileSystemScheme,
viewFilter: viewFilter, viewFilter: viewFilter,
@ -99,7 +96,6 @@ class FlutterDevice {
List<VMService> vmServices; List<VMService> vmServices;
DevFS devFS; DevFS devFS;
ApplicationPackage package; ApplicationPackage package;
String dillOutputPath;
List<String> fileSystemRoots; List<String> fileSystemRoots;
String fileSystemScheme; String fileSystemScheme;
StreamSubscription<String> _loggingSubscription; StreamSubscription<String> _loggingSubscription;
@ -476,6 +472,7 @@ class FlutterDevice {
bool fullRestart = false, bool fullRestart = false,
String projectRootPath, String projectRootPath,
String pathToReload, String pathToReload,
@required String dillOutputPath,
@required List<Uri> invalidatedFiles, @required List<Uri> invalidatedFiles,
}) async { }) async {
final Status devFSStatus = logger.startProgress( final Status devFSStatus = logger.startProgress(
@ -527,12 +524,25 @@ abstract class ResidentRunner {
this.usesTerminalUi = true, this.usesTerminalUi = true,
this.stayResident = true, this.stayResident = true,
this.hotMode = true, this.hotMode = true,
this.dillOutputPath,
}) { }) {
_mainPath = findMainDartFile(target); _mainPath = findMainDartFile(target);
_projectRootPath = projectRootPath ?? fs.currentDirectory.path; _projectRootPath = projectRootPath ?? fs.currentDirectory.path;
_packagesFilePath = _packagesFilePath =
packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath); packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath);
_assetBundle = AssetBundleFactory.instance.createBundle(); _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<FlutterDevice> flutterDevices; final List<FlutterDevice> flutterDevices;
@ -542,6 +552,7 @@ abstract class ResidentRunner {
final bool stayResident; final bool stayResident;
final bool ipv6; final bool ipv6;
final Completer<int> _finished = Completer<int>(); final Completer<int> _finished = Completer<int>();
final String dillOutputPath;
bool _exited = false; bool _exited = false;
bool hotMode ; bool hotMode ;
String _packagesFilePath; String _packagesFilePath;

View file

@ -63,7 +63,7 @@ class HotRunner extends ResidentRunner {
this.hostIsIde = false, this.hostIsIde = false,
String projectRootPath, String projectRootPath,
String packagesFilePath, String packagesFilePath,
this.dillOutputPath, String dillOutputPath,
bool stayResident = true, bool stayResident = true,
bool ipv6 = false, bool ipv6 = false,
}) : super(devices, }) : super(devices,
@ -74,13 +74,13 @@ class HotRunner extends ResidentRunner {
packagesFilePath: packagesFilePath, packagesFilePath: packagesFilePath,
stayResident: stayResident, stayResident: stayResident,
hotMode: true, hotMode: true,
dillOutputPath: dillOutputPath,
ipv6: ipv6); ipv6: ipv6);
final bool benchmarkMode; final bool benchmarkMode;
final File applicationBinary; final File applicationBinary;
final bool hostIsIde; final bool hostIsIde;
bool _didAttach = false; bool _didAttach = false;
final String dillOutputPath;
final Map<String, List<int>> benchmarkData = <String, List<int>>{}; final Map<String, List<int>> benchmarkData = <String, List<int>>{};
// The initial launch is from a snapshot. // The initial launch is from a snapshot.
@ -304,6 +304,7 @@ class HotRunner extends ResidentRunner {
projectRootPath: projectRootPath, projectRootPath: projectRootPath,
pathToReload: getReloadPath(fullRestart: fullRestart), pathToReload: getReloadPath(fullRestart: fullRestart),
invalidatedFiles: invalidatedFiles, invalidatedFiles: invalidatedFiles,
dillOutputPath: dillOutputPath,
)); ));
} }
return results; return results;

View file

@ -138,6 +138,14 @@ void main() {
expect(stampContents['inputs'], <Object>['/foo.dart']); expect(stampContents['inputs'], <Object>['/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 { test('Does not re-invoke build if stamp is valid', () => testbed.run(() async {
await buildSystem.build(fooTarget, environment); await buildSystem.build(fooTarget, environment);
await buildSystem.build(fooTarget, environment); await buildSystem.build(fooTarget, environment);

View file

@ -135,6 +135,14 @@ void main() {
expect(() => invalidBase.accept(visitor), throwsA(isInstanceOf<InvalidPatternException>())); expect(() => invalidBase.accept(visitor), throwsA(isInstanceOf<InvalidPatternException>()));
})); }));
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 { class TestBehavior extends SourceBehavior {

View file

@ -65,4 +65,15 @@ flutter:
// See https://github.com/flutter/flutter/issues/35293 // 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); 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(<File>[], Environment(
projectDir: fs.currentDirectory,
));
expect(fs.file('.flutter-plugins').existsSync(), true);
}));
} }

View file

@ -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/platform.dart';
import 'package:flutter_tools/src/base/process_manager.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/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/build_system/targets/macos.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/macos/cocoapods.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
@ -17,7 +20,6 @@ import '../../../src/testbed.dart';
void main() { void main() {
Testbed testbed; Testbed testbed;
const BuildSystem buildSystem = BuildSystem();
Environment environment; Environment environment;
MockPlatform mockPlatform; MockPlatform mockPlatform;
@ -31,6 +33,7 @@ void main() {
when(mockPlatform.isWindows).thenReturn(false); when(mockPlatform.isWindows).thenReturn(false);
when(mockPlatform.isMacOS).thenReturn(true); when(mockPlatform.isMacOS).thenReturn(true);
when(mockPlatform.isLinux).thenReturn(false); when(mockPlatform.isLinux).thenReturn(false);
when(mockPlatform.environment).thenReturn(const <String, String>{});
testbed = Testbed(setup: () { testbed = Testbed(setup: () {
environment = Environment( environment = Environment(
projectDir: fs.currentDirectory, projectDir: fs.currentDirectory,
@ -79,28 +82,117 @@ void main() {
}); });
test('Copies files to correct cache directory', () => testbed.run(() async { test('Copies files to correct cache directory', () => testbed.run(() async {
await buildSystem.build(const UnpackMacOS(), environment); await const UnpackMacOS().build(<File>[], environment);
expect(fs.directory('macos/Flutter/FlutterMacOS.framework').existsSync(), true); expect(fs.directory('macos/Flutter/ephemeral/FlutterMacOS.framework').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/FlutterMacOS').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/FlutterMacOS').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEReshapeListener.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEReshapeListener.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEView.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEView.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEViewController.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEViewController.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterChannels.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterChannels.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterCodecs.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterCodecs.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterMacOS.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterMacOS.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Modules/module.modulemap').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Modules/module.modulemap').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/icudtl.dat').existsSync(), true); expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Resources/icudtl.dat').existsSync(), true);
expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/info.plist').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(<File>[], 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(<File>[], 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(<File>[], 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(<File>[], environment),
throwsA(isInstanceOf<MissingDefineException>()));
}));
test('DebugMacOSPodInstall skips if podfile does not exist', () => testbed.run(() async {
await const DebugMacOSPodInstall().build(<File>[], Environment(
projectDir: fs.currentDirectory,
defines: <String, String>{
kBuildMode: 'debug'
}
));
verifyNever(cocoaPods.processPods(
xcodeProject: anyNamed('xcodeProject'),
engineDir: anyNamed('engineDir'),
isSwift: true,
dependenciesChanged: true));
}, overrides: <Type, Generator>{
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(<File>[], Environment(
projectDir: fs.currentDirectory,
defines: <String, String>{
kBuildMode: 'debug'
}
));
verify(cocoaPods.processPods(
xcodeProject: anyNamed('xcodeProject'),
engineDir: anyNamed('engineDir'),
isSwift: true,
dependenciesChanged: true)).called(1);
}, overrides: <Type, Generator>{
CocoaPods: () => MockCocoaPods(),
}));
test('b', () => testbed.run(() async {
}));
} }
class MockPlatform extends Mock implements Platform {} class MockPlatform extends Mock implements Platform {}
class MockCocoaPods extends Mock implements CocoaPods {}
class MockProcessManager extends Mock implements ProcessManager {} class MockProcessManager extends Mock implements ProcessManager {}
class FakeProcessResult implements ProcessResult { class FakeProcessResult implements ProcessResult {
@override @override
@ -115,3 +207,5 @@ class FakeProcessResult implements ProcessResult {
@override @override
String stdout = ''; String stdout = '';
} }

View file

@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:args/command_runner.dart'; 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/base/logger.dart';
import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
@ -31,13 +32,54 @@ void main() {
test('Can run a build', () => testbed.run(() async { test('Can run a build', () => testbed.run(() async {
when(mockBuildSystem.build(any, any, buildSystemConfig: anyNamed('buildSystemConfig'))) when(mockBuildSystem.build(any, any, buildSystemConfig: anyNamed('buildSystemConfig')))
.thenAnswer((Invocation invocation) async { .thenAnswer((Invocation invocation) async {
return BuildResult(true, const <String, ExceptionMeasurement>{}, const <String, PerformanceMeasurement>{}); return BuildResult(success: true);
}); });
final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand()); final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
await commandRunner.run(<String>['assemble', 'unpack_macos']); await commandRunner.run(<String>['assemble', 'unpack_macos']);
final BufferLogger bufferLogger = logger; 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: <File>[fs.file('foo')..createSync()],
outputFiles: <File>[fs.file('bar')..createSync()],
);
});
final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
await commandRunner.run(<String>['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(<String>['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: <File>[fs.file('foo'), fs.file('fizz')..createSync()],
outputFiles: <File>[fs.file('bar')]);
});
await commandRunner.run(<String>['assemble', '--build-outputs=outputs', '--build-inputs=inputs', 'unpack_macos']);
expect(inputs.readAsStringSync(), contains('foo'));
expect(inputs.readAsStringSync(), contains('fizz'));
expect(inputs.lastModifiedSync(), isNot(theDistantPast));
})); }));
} }

View file

@ -168,7 +168,6 @@ void main() {
// output dill, filesystem scheme, and filesystem root. // output dill, filesystem scheme, and filesystem root.
final FlutterDevice flutterDevice = flutterDevices.first; final FlutterDevice flutterDevice = flutterDevices.first;
expect(flutterDevice.dillOutputPath, outputDill);
expect(flutterDevice.fileSystemScheme, filesystemScheme); expect(flutterDevice.fileSystemScheme, filesystemScheme);
expect(flutterDevice.fileSystemRoots, const <String>[filesystemRoot]); expect(flutterDevice.fileSystemRoots, const <String>[filesystemRoot]);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{

View file

@ -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/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.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/cache.dart';
import 'package:flutter_tools/src/commands/build.dart'; import 'package:flutter_tools/src/commands/build.dart';
import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/features.dart';
@ -86,25 +87,35 @@ void main() {
fs.file('.packages').createSync(); fs.file('.packages').createSync();
fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fs.currentDirectory); 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: <String, String>{
kBuildMode: 'release',
kTargetFile: fs.path.absolute(fs.path.join('lib', 'main.dart')),
kTargetPlatform: 'darwin-x64',
}
);
when(mockProcessManager.start(<String>[ when(mockProcessManager.start(<String>[
'/usr/bin/env', '/usr/bin/env',
'xcrun', 'xcrun',
'xcodebuild', 'xcodebuild',
'-workspace', flutterProject.macos.xcodeWorkspace.path, '-workspace', flutterProject.macos.xcodeWorkspace.path,
'-configuration', 'Debug', '-configuration', 'Release',
'-scheme', 'Runner', '-scheme', 'Runner',
'-derivedDataPath', flutterBuildDir.absolute.path, '-derivedDataPath', environment.buildDir.path,
'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}', 'OBJROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Intermediates.noindex')}',
'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}', 'SYMROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Products')}',
], runInShell: true)).thenAnswer((Invocation invocation) async { ])).thenAnswer((Invocation invocation) async {
fs.file(fs.path.join('macos', 'Flutter', 'ephemeral', '.app_filename'))
..createSync(recursive: true)
..writeAsStringSync('example.app');
return mockProcess; return mockProcess;
}); });
await createTestCommandRunner(command).run( expect(createTestCommandRunner(command).run(
const <String>['build', 'macos'] const <String>['build', 'macos', '--release']
); ), throwsA(isInstanceOf<AssertionError>()));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => memoryFilesystem, FileSystem: () => memoryFilesystem,
ProcessManager: () => mockProcessManager, ProcessManager: () => mockProcessManager,

View file

@ -36,7 +36,6 @@ void main() {
testUsingContext('defaults', () async { testUsingContext('defaults', () async {
final MockMacOSApp mockMacOSApp = MockMacOSApp(); final MockMacOSApp mockMacOSApp = MockMacOSApp();
when(mockMacOSApp.executable(any)).thenReturn('foo');
expect(await device.targetPlatform, TargetPlatform.darwin_x64); expect(await device.targetPlatform, TargetPlatform.darwin_x64);
expect(device.name, 'macOS'); expect(device.name, 'macOS');
expect(await device.installApp(mockMacOSApp), true); 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 tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applications/foo
'''; ''';
final MockMacOSApp mockMacOSApp = MockMacOSApp(); final MockMacOSApp mockMacOSApp = MockMacOSApp();
when(mockMacOSApp.executable(any)).thenReturn('/Applications/foo'); when(mockMacOSApp.executable).thenReturn('tester');
when(mockProcessManager.run(<String>['ps', 'aux'])).thenAnswer((Invocation invocation) async { when(mockProcessManager.run(<String>['ps', 'aux'])).thenAnswer((Invocation invocation) async {
return ProcessResult(1, 0, psOut, ''); 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', () { group('startApp', () {
final MockMacOSApp macOSApp = MockMacOSApp(); MockMacOSApp macOSApp;
final MockFileSystem mockFileSystem = MockFileSystem(); MockFileSystem mockFileSystem;
final MockProcessManager mockProcessManager = MockProcessManager(); MockProcessManager mockProcessManager;
final MockFile mockFile = MockFile(); MockFile mockFile;
when(macOSApp.executable(any)).thenReturn('test');
when(mockFileSystem.file('test')).thenReturn(mockFile);
when(mockFile.existsSync()).thenReturn(true); setUp(() {
when(mockProcessManager.start(<String>['test'])).thenAnswer((Invocation invocation) async { macOSApp = MockMacOSApp();
return FakeProcess( mockFileSystem = MockFileSystem();
exitCode: Completer<int>().future, mockProcessManager = MockProcessManager();
stdout: Stream<List<int>>.fromIterable(<List<int>>[ mockFile = MockFile();
utf8.encode('Observatory listening on http://127.0.0.1/0\n'), when(mockFileSystem.file('test')).thenReturn(mockFile);
]), when(mockFile.existsSync()).thenReturn(true);
stderr: const Stream<List<int>>.empty(), when(macOSApp.executable).thenReturn('test');
); when(mockProcessManager.start(<String>['test'])).thenAnswer((Invocation invocation) async {
}); return FakeProcess(
when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async { exitCode: Completer<int>().future,
return ProcessResult(0, 1, '', ''); stdout: Stream<List<int>>.fromIterable(<List<int>>[
utf8.encode('Observatory listening on http://127.0.0.1/0\n'),
]),
stderr: const Stream<List<int>>.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); final LaunchResult result = await device.startApp(macOSApp, prebuiltApplication: true);
expect(result.started, true); expect(result.started, true);
expect(result.observatoryUri, Uri.parse('http://127.0.0.1/0')); 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 MockPlatform extends Mock implements Platform {}
class MockMacOSApp extends Mock implements MacOSApp {} class MockMacOSApp extends Mock implements PrebuiltMacOSApp {}
class MockFileSystem extends Mock implements FileSystem {} class MockFileSystem extends Mock implements FileSystem {}

View file

@ -68,6 +68,7 @@ void main() {
fullRestart: anyNamed('fullRestart'), fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'), projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'), pathToReload: anyNamed('pathToReload'),
dillOutputPath: anyNamed('dillOutputPath'),
)).thenAnswer((Invocation invocation) async { )).thenAnswer((Invocation invocation) async {
return UpdateFSReport( return UpdateFSReport(
success: true, success: true,
@ -175,6 +176,7 @@ void main() {
projectRootPath: anyNamed('projectRootPath'), projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'), pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'), invalidatedFiles: anyNamed('invalidatedFiles'),
dillOutputPath: anyNamed('dillOutputPath'),
)).thenThrow(RpcException(666, 'something bad happened')); )).thenThrow(RpcException(666, 'something bad happened'));
final OperationResult result = await residentRunner.restart(fullRestart: false); final OperationResult result = await residentRunner.restart(fullRestart: false);
@ -279,6 +281,7 @@ void main() {
projectRootPath: anyNamed('projectRootPath'), projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'), pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'), invalidatedFiles: anyNamed('invalidatedFiles'),
dillOutputPath: anyNamed('dillOutputPath'),
)).thenThrow(RpcException(666, 'something bad happened')); )).thenThrow(RpcException(666, 'something bad happened'));
final OperationResult result = await residentRunner.restart(fullRestart: true); final OperationResult result = await residentRunner.restart(fullRestart: true);