macos.dart 13.2 KB
Newer Older
1 2 3 4
// 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.

5 6
import 'package:pool/pool.dart';

7
import '../../artifacts.dart';
8
import '../../asset.dart';
9 10
import '../../base/file_system.dart';
import '../../base/io.dart';
11
import '../../base/process.dart';
12
import '../../base/process_manager.dart';
13
import '../../build_info.dart';
14
import '../../devfs.dart';
15
import '../../globals.dart';
16
import '../../macos/xcode.dart';
17
import '../../project.dart';
18
import '../build_system.dart';
19
import 'dart.dart';
20

21
const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/ephemeral/FlutterMacOS.framework';
22

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
/// 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,
54
        'App.framework', 'Resources', 'flutter_assets');
55 56 57 58 59 60 61 62 63
    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;
  }
}

64 65 66 67 68 69 70 71
/// 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.
72 73
class UnpackMacOS extends Target {
  const UnpackMacOS();
74

75 76
  @override
  String get name => 'unpack_macos';
77

78 79 80
  @override
  List<Source> get inputs => const <Source>[
    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/macos.dart'),
81
    Source.artifact(Artifact.flutterMacOSFramework),
82 83 84 85
  ];

  @override
  List<Source> get outputs => const <Source>[
86 87
    Source.pattern('$_kOutputPrefix/FlutterMacOS'),
    // Headers
88 89 90
    Source.pattern('$_kOutputPrefix/Headers/FlutterDartProject.h'),
    Source.pattern('$_kOutputPrefix/Headers/FlutterEngine.h'),
    Source.pattern('$_kOutputPrefix/Headers/FlutterViewController.h'),
91 92 93
    Source.pattern('$_kOutputPrefix/Headers/FlutterBinaryMessenger.h'),
    Source.pattern('$_kOutputPrefix/Headers/FlutterChannels.h'),
    Source.pattern('$_kOutputPrefix/Headers/FlutterCodecs.h'),
94
    Source.pattern('$_kOutputPrefix/Headers/FlutterMacros.h'),
95 96
    Source.pattern('$_kOutputPrefix/Headers/FlutterPluginMacOS.h'),
    Source.pattern('$_kOutputPrefix/Headers/FlutterPluginRegistrarMacOS.h'),
97
    Source.pattern('$_kOutputPrefix/Headers/FlutterMacOS.h'),
98 99 100 101
    // Modules
    Source.pattern('$_kOutputPrefix/Modules/module.modulemap'),
    // Resources
    Source.pattern('$_kOutputPrefix/Resources/icudtl.dat'),
102
    Source.pattern('$_kOutputPrefix/Resources/Info.plist'),
103
    // Ignore Versions folder for now
104 105 106 107
  ];

  @override
  List<Target> get dependencies => <Target>[];
108

109 110 111
  @override
  Future<void> build(List<File> inputFiles, Environment environment) async {
    final String basePath = artifacts.getArtifactPath(Artifact.flutterMacOSFramework);
112 113 114
    final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
    final Directory targetDirectory = flutterProject.macos
      .ephemeralDirectory
115 116 117 118
      .childDirectory('FlutterMacOS.framework');
    if (targetDirectory.existsSync()) {
      targetDirectory.deleteSync(recursive: true);
    }
119

120 121 122 123 124 125 126 127 128 129
    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}',
      );
    }
  }
}
130

131 132 133 134 135 136 137 138 139 140 141 142 143 144
/// 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(
145
        environment.buildDir.path, 'App.framework', 'App'));
146 147 148 149 150 151 152 153 154 155 156 157 158 159
    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',
160
      '-o', outputFile.path,
161 162 163 164 165
    ]);
    if (result.exitCode != 0) {
      throw Exception('Failed to compile debug App.framework');
    }
  }
166 167

  @override
168
  List<Target> get dependencies => const <Target>[];
169 170 171

  @override
  List<Source> get inputs => const <Source>[
172
    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/macos.dart'),
173 174 175 176
  ];

  @override
  List<Source> get outputs => const <Source>[
177
    Source.pattern('{BUILD_DIR}/App.framework/App'),
178
  ];
179 180 181
}

/// Bundle the flutter assets, app.dill, and precompiled runtimes into the App.framework.
182 183 184
///
/// See https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html
/// for more information on Framework structure.
185 186
class DebugBundleFlutterAssets extends Target {
  const DebugBundleFlutterAssets();
187 188

  @override
189
  String get name => 'debug_bundle_flutter_assets';
190 191 192

  @override
  Future<void> build(List<File> inputFiles, Environment environment) async {
193
    final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
194 195 196 197 198 199 200 201 202 203 204 205 206 207
    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);

208
    // Copy assets into asset directory.
209 210 211
    final Directory assetDirectory = outputDirectory
      .childDirectory('Resources')
      .childDirectory('flutter_assets');
212 213 214 215
    // 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);
216
    }
217
    assetDirectory.createSync(recursive: true);
218 219 220 221
    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,
222
    );
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    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();
          }
        }));
240
    } catch (err, st) {
241 242
      throw Exception('Failed to copy assets: $st');
    }
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
    // 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>

''');

272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
    // 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');
291
    }
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
    // 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');
    }
315 316 317 318 319
  }

  @override
  List<Target> get dependencies => const <Target>[
    KernelSnapshot(),
320 321
    DebugMacOSFramework(),
    UnpackMacOS(),
322 323 324 325
  ];

  @override
  List<Source> get inputs => const <Source>[
326 327 328 329 330
    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),
331 332 333 334
  ];

  @override
  List<Source> get outputs => const <Source>[
335
    Source.behavior(MacOSAssetBehavior()),
336 337 338 339 340 341 342 343
    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'),
344 345
  ];
}