blob: eb708aa09d939dc21825012788b878000816fd51 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:pool/pool.dart';
import '../../artifacts.dart';
import '../../asset.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../base/process.dart';
import '../../base/process_manager.dart';
import '../../build_info.dart';
import '../../devfs.dart';
import '../../globals.dart';
import '../../macos/xcode.dart';
import '../../project.dart';
import '../build_system.dart';
import 'dart.dart';
const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/ephemeral/FlutterMacOS.framework';
/// The copying logic for flutter assets in macOS.
// TODO(jonahwilliams): remove once build planning lands.
class MacOSAssetBehavior extends SourceBehavior {
const MacOSAssetBehavior();
@override
List<File> inputs(Environment environment) {
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
assetBundle.build(
manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
packagesPath: environment.projectDir.childFile('.packages').path,
);
// Filter the file type to remove the files that are generated by this
// command as inputs.
final List<File> results = <File>[];
final Iterable<DevFSFileContent> files = assetBundle.entries.values.whereType<DevFSFileContent>();
for (DevFSFileContent devFsContent in files) {
results.add(fs.file(devFsContent.file.path));
}
return results;
}
@override
List<File> outputs(Environment environment) {
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
assetBundle.build(
manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
packagesPath: environment.projectDir.childFile('.packages').path,
);
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
final String prefix = fs.path.join(flutterProject.macos.ephemeralDirectory.path,
'App.framework', 'Resources', 'flutter_assets');
final List<File> results = <File>[];
for (String key in assetBundle.entries.keys) {
final File file = fs.file(fs.path.join(prefix, key));
results.add(file);
}
return results;
}
}
/// Copy the macOS framework to the correct copy dir by invoking 'cp -R'.
///
/// The shelling out is done to avoid complications with preserving special
/// files (e.g., symbolic links) in the framework structure.
///
/// Removes any previous version of the framework that already exists in the
/// target directory.
// TODO(jonahwilliams): remove shell out.
class UnpackMacOS extends Target {
const UnpackMacOS();
@override
String get name => 'unpack_macos';
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/macos.dart'),
Source.artifact(Artifact.flutterMacOSFramework),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('$_kOutputPrefix/FlutterMacOS'),
// Headers
Source.pattern('$_kOutputPrefix/Headers/FlutterDartProject.h'),
Source.pattern('$_kOutputPrefix/Headers/FlutterEngine.h'),
Source.pattern('$_kOutputPrefix/Headers/FlutterViewController.h'),
Source.pattern('$_kOutputPrefix/Headers/FlutterBinaryMessenger.h'),
Source.pattern('$_kOutputPrefix/Headers/FlutterChannels.h'),
Source.pattern('$_kOutputPrefix/Headers/FlutterCodecs.h'),
Source.pattern('$_kOutputPrefix/Headers/FlutterMacros.h'),
Source.pattern('$_kOutputPrefix/Headers/FlutterPluginMacOS.h'),
Source.pattern('$_kOutputPrefix/Headers/FlutterPluginRegistrarMacOS.h'),
Source.pattern('$_kOutputPrefix/Headers/FlutterMacOS.h'),
// Modules
Source.pattern('$_kOutputPrefix/Modules/module.modulemap'),
// Resources
Source.pattern('$_kOutputPrefix/Resources/icudtl.dat'),
Source.pattern('$_kOutputPrefix/Resources/Info.plist'),
// Ignore Versions folder for now
];
@override
List<Target> get dependencies => <Target>[];
@override
Future<void> build(List<File> inputFiles, Environment environment) async {
final String basePath = artifacts.getArtifactPath(Artifact.flutterMacOSFramework);
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
final Directory targetDirectory = flutterProject.macos
.ephemeralDirectory
.childDirectory('FlutterMacOS.framework');
if (targetDirectory.existsSync()) {
targetDirectory.deleteSync(recursive: true);
}
final ProcessResult result = await processManager
.run(<String>['cp', '-R', basePath, targetDirectory.path]);
if (result.exitCode != 0) {
throw Exception(
'Failed to copy framework (exit ${result.exitCode}:\n'
'${result.stdout}\n---\n${result.stderr}',
);
}
}
}
/// Create an App.framework for debug macOS targets.
///
/// This framework needs to exist for the Xcode project to link/bundle,
/// but it isn't actually executed. To generate something valid, we compile a trivial
/// constant.
class DebugMacOSFramework extends Target {
const DebugMacOSFramework();
@override
String get name => 'debug_macos_framework';
@override
Future<void> build(List<File> inputFiles, Environment environment) async {
final File outputFile = fs.file(fs.path.join(
environment.buildDir.path, 'App.framework', 'App'));
outputFile.createSync(recursive: true);
final File debugApp = environment.buildDir.childFile('debug_app.cc')
..writeAsStringSync(r'''
static const int Moo = 88;
''');
final RunResult result = await xcode.clang(<String>[
'-x',
'c',
debugApp.path,
'-arch', 'x86_64',
'-dynamiclib',
'-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
'-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
'-install_name', '@rpath/App.framework/App',
'-o', outputFile.path,
]);
if (result.exitCode != 0) {
throw Exception('Failed to compile debug App.framework');
}
}
@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/macos.dart'),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{BUILD_DIR}/App.framework/App'),
];
}
/// Bundle the flutter assets, app.dill, and precompiled runtimes into the App.framework.
///
/// See https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html
/// for more information on Framework structure.
class DebugBundleFlutterAssets extends Target {
const DebugBundleFlutterAssets();
@override
String get name => 'debug_bundle_flutter_assets';
@override
Future<void> build(List<File> inputFiles, Environment environment) async {
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
final Directory frameworkRootDirectory = flutterProject.macos
.ephemeralDirectory
.childDirectory('App.framework');
final Directory outputDirectory = frameworkRootDirectory
.childDirectory('Versions')
.childDirectory('A')
..createSync(recursive: true);
// Copy App into framework directory.
environment.buildDir
.childDirectory('App.framework')
.childFile('App')
.copySync(outputDirectory.childFile('App').path);
// Copy assets into asset directory.
final Directory assetDirectory = outputDirectory
.childDirectory('Resources')
.childDirectory('flutter_assets');
// We're not smart enough to only remove assets that are removed. If
// anything changes blow away the whole directory.
if (assetDirectory.existsSync()) {
assetDirectory.deleteSync(recursive: true);
}
assetDirectory.createSync(recursive: true);
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
final int result = await assetBundle.build(
manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
packagesPath: environment.projectDir.childFile('.packages').path,
);
if (result != 0) {
throw Exception('Failed to create asset bundle: $result');
}
// Limit number of open files to avoid running out of file descriptors.
try {
final Pool pool = Pool(64);
await Future.wait<void>(
assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
final PoolResource resource = await pool.request();
try {
final File file = fs.file(fs.path.join(assetDirectory.path, entry.key));
file.parent.createSync(recursive: true);
await file.writeAsBytes(await entry.value.contentsAsBytes());
} finally {
resource.release();
}
}));
} catch (err, st) {
throw Exception('Failed to copy assets: $st');
}
// Copy Info.plist template.
assetDirectory.parent.childFile('Info.plist')
..createSync()
..writeAsStringSync(r'''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
''');
// Copy dill file.
try {
final File sourceFile = environment.buildDir.childFile('app.dill');
sourceFile.copySync(assetDirectory.childFile('kernel_blob.bin').path);
} catch (err) {
throw Exception('Failed to copy app.dill: $err');
}
// Copy precompiled runtimes.
try {
final String vmSnapshotData = artifacts.getArtifactPath(Artifact.vmSnapshotData,
platform: TargetPlatform.darwin_x64, mode: BuildMode.debug);
final String isolateSnapshotData = artifacts.getArtifactPath(Artifact.isolateSnapshotData,
platform: TargetPlatform.darwin_x64, mode: BuildMode.debug);
fs.file(vmSnapshotData).copySync(
assetDirectory.childFile('vm_snapshot_data').path);
fs.file(isolateSnapshotData).copySync(
assetDirectory.childFile('isolate_snapshot_data').path);
} catch (err) {
throw Exception('Failed to copy precompiled runtimes: $err');
}
// Create symlink to current version.
try {
final Link currentVersion = outputDirectory.parent
.childLink('Current');
if (!currentVersion.existsSync()) {
currentVersion.createSync(outputDirectory.path);
}
// Create symlink to current resources.
final Link currentResources = frameworkRootDirectory
.childLink('Resources');
if (!currentResources.existsSync()) {
currentResources.createSync(fs.path.join(currentVersion.path, 'Resources'));
}
// Create symlink to current binary.
final Link currentFramework = frameworkRootDirectory
.childLink('App');
if (!currentFramework.existsSync()) {
currentFramework.createSync(fs.path.join(currentVersion.path, 'App'));
}
} on FileSystemException {
throw Exception('Failed to create symlinks for framework. try removing '
'the "${flutterProject.macos.ephemeralDirectory.path}" directory and rerunning');
}
}
@override
List<Target> get dependencies => const <Target>[
KernelSnapshot(),
DebugMacOSFramework(),
UnpackMacOS(),
];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
Source.behavior(MacOSAssetBehavior()),
Source.pattern('{BUILD_DIR}/app.dill'),
Source.artifact(Artifact.isolateSnapshotData, platform: TargetPlatform.darwin_x64, mode: BuildMode.debug),
Source.artifact(Artifact.vmSnapshotData, platform: TargetPlatform.darwin_x64, mode: BuildMode.debug),
];
@override
List<Source> get outputs => const <Source>[
Source.behavior(MacOSAssetBehavior()),
Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/Versions/A/App'),
Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/Versions/A/Resources/Info.plist'),
Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/Versions/A/Resources/flutter_assets/AssetManifest.json'),
Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/Versions/A/Resources/flutter_assets/FontManifest.json'),
Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/Versions/A/Resources/flutter_assets/LICENSE'),
Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/Versions/A/Resources/flutter_assets/kernel_blob.bin'),
Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/Versions/A/Resources/flutter_assets/vm_snapshot_data'),
Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/Versions/A/Resources/flutter_assets/isolate_snapshot_data'),
];
}