application_package.dart 6.96 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../application_package.dart';
import '../base/file_system.dart';
7
import '../base/io.dart';
8
import '../base/utils.dart';
9
import '../build_info.dart';
10
import '../globals.dart' as globals;
11
import '../ios/plist_parser.dart';
12
import '../xcode_project.dart';
13

14
/// Tests whether a [FileSystemEntity] is an macOS bundle directory.
15 16 17 18
bool _isBundleDirectory(FileSystemEntity entity) =>
    entity is Directory && entity.path.endsWith('.app');

abstract class MacOSApp extends ApplicationPackage {
19
  MacOSApp({required String projectBundleId}) : super(id: projectBundleId);
20

21 22
  /// Creates a new [MacOSApp] from a macOS project directory.
  factory MacOSApp.fromMacOSProject(MacOSProject project) {
23 24
    // projectBundleId is unused for macOS apps. Use a placeholder bundle ID.
    return BuildableMacOSApp(project, 'com.example.placeholder');
25 26
  }

27 28 29 30 31
  /// Creates a new [MacOSApp] from an existing app bundle.
  ///
  /// `applicationBinary` is the path to the framework directory created by an
  /// Xcode build. By default, this is located under
  /// "~/Library/Developer/Xcode/DerivedData/" and contains an executable
32
  /// which is expected to start the application and send the vmService
33
  /// port over stdout.
34 35
  static MacOSApp? fromPrebuiltApp(FileSystemEntity applicationBinary) {
    final _BundleInfo? bundleInfo = _executableFromBundle(applicationBinary);
36 37 38 39
    if (bundleInfo == null) {
      return null;
    }

40
    return PrebuiltMacOSApp(
41 42
      uncompressedBundle: bundleInfo.uncompressedBundle,
      bundleName: bundleInfo.uncompressedBundle.path,
43 44
      projectBundleId: bundleInfo.id,
      executable: bundleInfo.executable,
45
      applicationPackage: applicationBinary,
46 47 48 49
    );
  }

  /// Look up the executable name for a macOS application bundle.
50
  static _BundleInfo? _executableFromBundle(FileSystemEntity applicationBundle) {
51
    final FileSystemEntityType entityType = globals.fs.typeSync(applicationBundle.path);
52
    if (entityType == FileSystemEntityType.notFound) {
53
      globals.printError('File "${applicationBundle.path}" does not exist.');
54 55
      return null;
    }
56
    Directory uncompressedBundle;
57
    if (entityType == FileSystemEntityType.directory) {
58
      final Directory directory = globals.fs.directory(applicationBundle);
59
      if (!_isBundleDirectory(directory)) {
60
        globals.printError('Folder "${applicationBundle.path}" is not an app bundle.');
61 62
        return null;
      }
63
      uncompressedBundle = globals.fs.directory(applicationBundle);
64
    } else {
65 66 67 68 69 70 71 72 73
      // Try to unpack as a zip.
      final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_app.');
      try {
        globals.os.unzip(globals.fs.file(applicationBundle), tempDir);
      } on ProcessException {
        globals.printError('Invalid prebuilt macOS app. Unable to extract bundle from archive.');
        return null;
      }
      try {
74
        uncompressedBundle = tempDir
75 76 77 78 79 80 81
            .listSync()
            .whereType<Directory>()
            .singleWhere(_isBundleDirectory);
      } on StateError {
        globals.printError('Archive "${applicationBundle.path}" does not contain a single app bundle.');
        return null;
      }
82
    }
83
    final String plistPath = globals.fs.path.join(uncompressedBundle.path, 'Contents', 'Info.plist');
84 85
    if (!globals.fs.file(plistPath).existsSync()) {
      globals.printError('Invalid prebuilt macOS app. Does not contain Info.plist.');
86 87
      return null;
    }
88
    final Map<String, dynamic> propertyValues = globals.plistParser.parseFile(plistPath);
89
    final String? id = propertyValues[PlistParser.kCFBundleIdentifierKey] as String?;
90
    final String? executableName = propertyValues[PlistParser.kCFBundleExecutableKey] as String?;
91
    if (id == null) {
92
      globals.printError('Invalid prebuilt macOS app. Info.plist does not contain bundle identifier');
93 94
      return null;
    }
95 96 97 98
    if (executableName == null) {
      globals.printError('Invalid prebuilt macOS app. Info.plist does not contain bundle executable');
      return null;
    }
99
    final String executable = globals.fs.path.join(uncompressedBundle.path, 'Contents', 'MacOS', executableName);
100 101
    if (!globals.fs.file(executable).existsSync()) {
      globals.printError('Could not find macOS binary at $executable');
102
    }
103
    return _BundleInfo(executable, id, uncompressedBundle);
104 105 106 107
  }

  @override
  String get displayName => id;
108

109
  String? applicationBundle(BuildInfo buildInfo);
110

111
  String? executable(BuildInfo buildInfo);
112 113
}

114
class PrebuiltMacOSApp extends MacOSApp implements PrebuiltApplicationPackage {
115
  PrebuiltMacOSApp({
116
    required this.uncompressedBundle,
117 118 119
    required this.bundleName,
    required this.projectBundleId,
    required String executable,
120
    required this.applicationPackage,
121 122
  }) : _executable = executable,
       super(projectBundleId: projectBundleId);
123

124 125 126 127 128
  /// The uncompressed bundle of the application.
  ///
  /// [MacOSApp.fromPrebuiltApp] will uncompress the application into a temporary
  /// directory even when an `.zip` file was used to create the [MacOSApp] instance.
  final Directory uncompressedBundle;
129
  final String bundleName;
130 131 132
  final String projectBundleId;

  final String _executable;
133 134 135

  @override
  String get name => bundleName;
136

137
  @override
138
  String? applicationBundle(BuildInfo buildInfo) => uncompressedBundle.path;
139 140

  @override
141
  String? executable(BuildInfo buildInfo) => _executable;
142 143 144 145 146 147

  /// A [File] or [Directory] pointing to the application bundle.
  ///
  /// This can be either a `.zip` file or an uncompressed `.app` directory.
  @override
  final FileSystemEntity applicationPackage;
148 149 150
}

class BuildableMacOSApp extends MacOSApp {
151
  BuildableMacOSApp(this.project, String projectBundleId): super(projectBundleId: projectBundleId);
152 153 154 155 156

  final MacOSProject project;

  @override
  String get name => 'macOS';
157 158

  @override
159
  String? applicationBundle(BuildInfo buildInfo) {
160 161
    final File appBundleNameFile = project.nameFile;
    if (!appBundleNameFile.existsSync()) {
162
      globals.printError('Unable to find app name. ${appBundleNameFile.path} does not exist');
163 164
      return null;
    }
165

166
    return globals.fs.path.join(
167 168 169
        getMacOSBuildDirectory(),
        'Build',
        'Products',
170
        bundleDirectory(buildInfo),
171 172 173
        appBundleNameFile.readAsStringSync().trim());
  }

174
  String bundleDirectory(BuildInfo buildInfo) {
175
    return sentenceCase(buildInfo.mode.cliName) + (buildInfo.flavor != null
176
      ? '-${buildInfo.flavor!}'
177 178 179
      : '');
  }

180
  @override
181 182
  String? executable(BuildInfo buildInfo) {
    final String? directory = applicationBundle(buildInfo);
183 184 185
    if (directory == null) {
      return null;
    }
186
    final _BundleInfo? bundleInfo = MacOSApp._executableFromBundle(globals.fs.directory(directory));
187
    return bundleInfo?.executable;
188
  }
189 190
}

191
class _BundleInfo {
192
  _BundleInfo(this.executable, this.id, this.uncompressedBundle);
193

194
  final Directory uncompressedBundle;
195 196
  final String executable;
  final String id;
197
}