Open12

Flutterの "Add Swift Package Manager as new opt-in feature for iOS and macOS #146256 " を読む

koji-1009koji-1009

dev/bots/analyze.dart は気にしなくてOK。 Package.swift ファイルに対しては、ファイル内にライセンスが書いてあるかどうかのチェックをスキップする、という処理になっているだけ。
Package.swift はライセンス(ApatchとかMITとか)をまず書かないので、この処理はそのまま読めば良い。

koji-1009koji-1009

packages/integration_testは既存のCocoaPods向けテストを、Swift PMでもテストできるよう、階層をいじっているだけ。

CocoaPodsでは /Classes/ 配下にコードを置いていたが、Swift PMでは /Source/ 以下にコードを置くことになる。また、/Package.swiftがSwift PM用に追加されているので、このswiftファイルをCocoaPodsで扱うソースコードから外す狙いもある(ハズ)。

koji-1009koji-1009

packages/flutter_tools が本丸。packages/flutter_tools/binpackages/flutter_tools/lib/srcでSwift PM対応がされている。

packages/flutter_tools/lib/src/migrationsは面白そうだが、一番難しそうなので最後に回す。ソースコードはiOS向けとmacOS向けがあるが、PRの内容を把握するのであればiOS向けだけ読めばOKだと思う。
なので、次のディレクトリを読んでいく。

  • packages/flutter_tools/bin
  • packages/flutter_tools/lib/src
  • packages/flutter_tools/lib/src/commands
  • packages/flutter_tools/lib/src/ios
  • packages/flutter_tools/test/commands.shared/hermetic
  • packages/flutter_tools/test/general.shared
  • packages/flutter_tools/test/general.shared/ios

下の2箇所は後回し。また、integration testはちょっと理解できるかわからないので保留。

  • packages/flutter_tools/lib/src/migration
  • packages/flutter_tools/test/general.shared/migration
koji-1009koji-1009

packages/flutter_tools/lib/src/commands からみると良さそう。

packages/flutter_tools/lib/src/commands/assemble.dart

次の3つのunpackが追加されているだけ。

  • const DebugUnpackIOS()
  • const ProfileUnpackIOS()
  • const ReleaseUnpackIOS()

これらは flutter_tools/lib/src/build_system/targets/ios.dart に定義されている。

https://github.com/flutter/flutter/blob/3.19.0/packages/flutter_tools/lib/src/build_system/targets/ios.dart

_kDefaultTargetsは実装を見ていくと、assembleコマンドとして実行可能な処理の一覧を指しているっぽい。ReleaseUnpackIOSを見ると

/// Unpack the release prebuilt engine framework.
class ReleaseUnpackIOS extends UnpackIOS {
  const ReleaseUnpackIOS();

  
  String get name => 'release_unpack_ios';

  
  BuildMode get buildMode => BuildMode.release;
}

とあるので、release_unpack_iosdebug_unpack_iosをassemble時に指定できるようにしている、と理解しておく。

packages/flutter_tools/lib/src/commands/build_ios.dart

差分を改行などを慣らした上で展開してみる。

  await diagnoseXcodeBuildFailure(
    result,
    analytics: globals.analytics,
+   fileSystem: globals.fs,
    flutterUsage: globals.flutterUsage,
    logger: globals.logger,
+   platform: SupportedPlatform.ios,
+   project: app.project.parent,
  );

なので、fileSytemplatformprojectが引数に追加された。この引数はv3.19.0には存在しないので、今回のPRで追加された処理になっている(ハズ)。

https://github.com/flutter/flutter/blob/3.19.0/packages/flutter_tools/lib/src/ios/mac.dart#L591-L620

packages/flutter_tools/lib/src/commands/clean.dart

deleteFile(flutterProject.ios.flutterPluginSwiftPackageDirectory);deleteFile(flutterProject.macos.flutterPluginSwiftPackageDirectory); なので、clean時に削除されることがわかる。

CocoaPodsの場合、 /ios/Podsflutter clean コマンドで削除されない。このため、いい点としては flutter clean でライブラリのキャッシュもリフレッシュされる。他方、flutter clean 後に都度Swift PMの依存関係解決が行われるので、想定するよりも時間がかかるかもしれない。

koji-1009koji-1009

追加コミットで packages/flutter_tools/lib/src/commands/clean.dart の変更が削除され、packages/flutter_tools/lib/src/commands/build_ios_framework.dartpackages/flutter_tools/lib/src/commands/build_macos_framework.dartが追加された。

clean.dartの処理が戻ったコミット。opt-inなので、利用しないケースで影響が出ないようにしたのかな。

https://github.com/flutter/flutter/pull/146256/commits/e7f667eb5c836a3362f4229b227f6cd1d99ff142

ios-frameworkとmacos-frameworkを生成するときには、CocoaPodsを利用するように変更されたコミット。
このフラグが立ってる状態でSwift PMを利用する処理に入ると、 Swift Package Manager does not yet support this command. とワーニング表示されるので、flutterのプラグインとしてiOSやmacOS向けのxcframeworkを作る処理がうまく動かないのかもしれない。

https://github.com/flutter/flutter/pull/146256/commits/503ae5b9324028d3c4bad797873a9d13469ab33b

koji-1009koji-1009

packages/flutter_tools/bin/macos_assemble.sh

後述の xcode_backend.dart のmacos版の様子。
macos向けのビルドはわからないので、スキップ。

packages/flutter_tools/bin/podhelper.rb

「Swift PMが有効であれば、CocoaPodsを走らせなくてもいいのではないか?」という気もするけれど、そうとも言い切れないケース対応なのかな? という雰囲気。

https://github.com/flutter/flutter/pull/146256#issuecomment-2040448755

Swift PMとCocoaPodsが混在している場合、ライブラリの依存関係がうまく解決できないことがある。
Package 1とPackage 2、Package 3の3つのライブラリがあり、Package 1とPackage 2がそれぞれPackage 3に依存しているケースを考えるとわかりやすい。Package 1をSwift PMで、Package 2をCocoaPodsで管理している時、Package 3の適切なバージョンを解決するのはどっち……? 状態になってしまう。正解は開発者が頑張るしかないので、この運用はあまりうまくいかないはず。

そう考えると

  1. Swift PMが有効、そしてライブラリが Package.swift を持っているケース
  2. ライブラリが Package.swift を持っているが、 ~.podspec を持っていないケース

を考慮しているのは、Swift PMへの移行期を見た対応なのかなと。印象です。

packages/flutter_tools/bin/xcode_backend.dart

ファイルをぱっと見てもなんのこっちゃ、となったのでPRを遡ってみた。どうやら、もともとbashで書かれていた処理を、dartにしたものらしい。

https://github.com/flutter/flutter/pull/86753

なので、たぶんiOSやmacOS向けのビルドに必要なステップが定義されている。そうみてみると、このPRでの変更点は大きく分けて2つ。

  1. スクリプトに prepare コマンドが追加された
  2. build コマンドの処理に ${build_mode}_ios_bundle_flutter_assets が追加された

prepareコマンドは、先ほど見た ${build_mode}_unpack_ios を呼び出す処理となっている。今まで pod install して依存関係を解決していたものを、prepareで対応するんだろうなと予想。

${build_mode}IosApplicationBundleなどは、先ほどと同じくflutter_tools/lib/src/build_system/targets/ios.dartに定義されている。この処理がどこで呼ばれていたかを見てみると、build_ios_framework.dartで発見。

https://github.com/flutter/flutter/blob/3.19.0/packages/flutter_tools/lib/src/commands/build_ios_framework.dart

このステップは、ログに ' ├─Building App.xcframework...', を書き出す様子。見た覚えがない気がする(flutter build iosだと出ている……?)ので、関連issueを見てみる。

https://github.com/flutter/flutter/issues/104866

issueの内容から察するに、おそらくiOS向けに通常のアプリを作っている場合には、利用したことがないのではないかなと。Swift PMの導入によって、ログ出力に(当然)変更があるので、この辺りに注目してみてもいいかも。


build コマンドを見ていくと、最後に次のログを出力している。

    streamOutput('done');
    streamOutput(' └─Compiling, linking and signing...');

    echo('Project $projectPath built and packaged successfully.');

なので、見慣れたアプリのビルド処理の様子。
release_ios_bundle_flutter_assetsで呼び出される処理を見ていくと、次の箇所であることがわかる。

https://github.com/flutter/flutter/blob/3.19.0/packages/flutter_tools/lib/src/build_system/targets/ios.dart#L590-L630

継承元のクラスが、非常に気になる名前をしているので見てみる。

https://github.com/flutter/flutter/blob/3.19.0/packages/flutter_tools/lib/src/build_system/targets/ios.dart#L560-L575

ということで、ここでbundleしているのはdSYMの様子。確かにSwift PMで落としてきたコードをコンパイルしたら、そのdSYMをセットしないとマズイですね。

koji-1009koji-1009

features

次の2ファイル。

  • packages/flutter_tools/lib/src/features.dart
  • packages/flutter_tools/lib/src/flutter_features.dart

flutter configコマンドで有効、無効を切り替えるコンフィグの設定ファイルの様子。Swift PMは flutter config --enable-swift-package-manager で設定が有効になるように定義された。

/// Enable Swift Package Mangaer as a darwin dependency manager.
const Feature swiftPackageManager = Feature(
  name: 'support for Swift Package Manager for iOS and macOS',
  configSetting: 'enable-swift-package-manager',
  environmentOverride: 'SWIFT_PACKAGE_MANAGER',
  master: FeatureChannelSetting(
    available: true,
  ),
);

masterは、betastableのチャンネルごとにデフォルト値を設定するための機構らしい。まだ開発中なので、masterチャンネルでのみデフォルト有効、ってことですね。

packages/flutter_tools/lib/src/flutter_manifest.dart

pabspec.yamlflutter:で設定されているオプションを見ている様子。
プロジェクトごとにSwift PMをdisableにするオプション、disable-swift-package-managerが定義されている。これがなんで必要かは、先述の通り。

利用する場合は、テストケースにあるように、disable-swift-package-manager: trueを設定すればOK。

packages/flutter_tools/lib/src/project.dart

.flutter-plugins-dependenciesを構築するための処理っぽい。
既存と違いが生まれるのは、多分2点。1つ目は、Flutterの開発中にも影響があるのかもしれない。

refreshPluginsList

コードを引っ張ってきたほうがわかりやすいので、ちょっとだけ引用。

/// Rewrites the `.flutter-plugins` file of [project] based on the plugin
/// dependencies declared in `pubspec.yaml`.
///
/// Assumes `pub get` has been executed since last change to `pubspec.yaml`.
Future<void> refreshPluginsList(
  FlutterProject project, {
  bool iosPlatform = false,
  bool macOSPlatform = false,
  bool forceCocoaPodsOnly = false, ← ここが差分
}) async {

変更としては、引数に forceCocoaPodsOnly が加わった。

- final bool changed = _writeFlutterPluginsList(project, plugins);
- if (changed || legacyChanged) {
+ final bool changed = _writeFlutterPluginsList(
+   project,
+   plugins,
+   forceCocoaPodsOnly: forceCocoaPodsOnly,
+ );
+ if (changed || legacyChanged || forceCocoaPodsOnly) {
    createPluginSymlinks(project, force: true);
    if (iosPlatform) {
      globals.cocoaPods?.invalidatePodInstallOutput(project.ios);
    }
    if (macOSPlatform) {
      globals.cocoaPods?.invalidatePodInstallOutput(project.macos);
    }
  }

差分を見ると、forceCocoaPodsOnlychangedの判定に影響を与えることになる。forceCocoaPodsOnlyの変更は確かにしょうがないというか、Swift PMからCocoaPodsに切り替えるかその逆なので、都度リフレッシュされるべき。これは判定として、非常に良いものに見える。

一方でforceCocoaPodsOnly=trueの時には、flutter pub getのたびにinvalidatePodInstallOutputが走るようになる。/ios/Pods以下にキャッシュはあるはずなので、そこまで影響はないのかな……? むしろ今までも走ってもらいたかったような……?


injectPlugins

packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart に追加された DarwinDependencyManagement が引数になったことが大きな違いになる。

変更前のコードが、次の通り。

if (!project.isModule) {
  final List<XcodeBasedProject> darwinProjects = <XcodeBasedProject>[
    if (iosPlatform) project.ios,
    if (macOSPlatform) project.macos,
  ];
  for (final XcodeBasedProject subproject in darwinProjects) {
    if (plugins.isNotEmpty) {
      await globals.cocoaPods?.setupPodfile(subproject);
    }
    /// The user may have a custom maintained Podfile that they're running `pod install`
    /// on themselves.
    else if (subproject.podfile.existsSync() && subproject.podfileLock.existsSync()) {
      globals.cocoaPods?.addPodsDependencyToFlutterXcconfig(subproject);
    }

ios/PodfileにMethod Channel経由で利用するためのPodsを追加していたようなケースで利用している箇所なのかな? と推測している。同様の処理はSwift PMでも必要になるはずなので、今回の修正に含まれることにも納得感がありそう。

packages/flutter_tools/lib/src/plugins.dart

Dartのプラグイン(ライブラリ)を表現しているクラス。
String? pluginSwiftPackageManifestPathString? pluginPodspecPathのメソッドからイメージできる通り、プラグインを構成するファイルから必要なpath(など)を取得できるようになっている。

packages/flutter_tools/lib/src/project.dart

pubspec.yamlをパースし、flutterのプロジェクトそのものを管理するクラス。
今回のPRでは、プロジェクトが「Swift PMを利用できる状態か」を取得するusesSwiftPackageManagerプロパティを追加している。利用できるかどうかは、pubspec.yamlと実行されているマシンに依存(Xcode 15.0以上が必要)なので、次のような実装になっている。

/// True if this project doesn't have Swift Package Manager disabled in the
/// pubspec, has either an iOS or macOS platform implementation, is not a
/// module project, Xcode is 15 or greater, and the Swift Package Manager
/// feature is enabled.
bool get usesSwiftPackageManager {
  if (!manifest.disabledSwiftPackageManager &&
      (ios.existsSync() || macos.existsSync()) &&
      !isModule) {
    final Xcode? xcode = globals.xcode;
    final Version? xcodeVersion = xcode?.currentVersion;
    if (xcodeVersion == null || xcodeVersion.major < 15) {
      return false;
    }
    return featureFlags.isSwiftPackageManagerEnabled;
  }
  return false;
}

packages/flutter_tools/lib/src/xcode_project.dart

/ios/macosディレクトリ以下の構造、つまりiOSやmacOS用のプロジェクトそのものをDartで扱うクラス……だと解釈しています。

PRではIosProjectに定義されていたプロパティを、XcodeBasedProjectに移動しています。この移動により、XcodeBasedProjectを継承しているMacOSProjectもSwift PMの対応ができるようになっているはずです。
Swift PMがXcodeで管理される仕組みなので、特にCocoaPodsからSwift PMへの移行を行うために、xcode_project.dartのリファクタリングがされたような印象です。

koji-1009koji-1009

packages/flutter_tools/lib/src/ios/mac.dart

PRの目玉その1。

やっていることは、次の3つ。

  • migratorsにSwift PMを使っている場合はMigration処理を追加する
    • この処理の詳細は後ほど。ざっとみる感じ、一度migrationされていればスキップされる。
    • migration処理を非同期対応するPRは別でマージ済み #146537
  • IPHONEOS_DEPLOYMENT_TARGET が設定されている場合には、設定されている値に更新する処理
  • diagnoseXcodeBuildFailure のアップデート、次のようなケースを考慮
    • Swift PMを利用しているケースにおける、ビルドエラー
    • Swift PMとCocoaPodsを併用した時のビルドエラー
    • CocoaPodsを使っているが、利用したプラグインがSwift PMしかサポートしていない時のエラー

次の2ファイルはdiagnoseXcodeBuildFailureの更新に合わせた修正なので、ここでまとめて見たことにする。

  • packages/flutter_tools/lib/src/ios/devices.dart
  • packages/flutter_tools/lib/src/ios/simulators.dart

packages/flutter_tools/lib/src/ios/plist_parser.dart

plistをjsonにパースする処理。マイグレーション処理で利用される。
ProcessUtils経由でコマンドを渡している。気になるけど、今回の主な調査対象ではないので調査はここまで。

packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart

koji-1009koji-1009

packages/flutter_tools/lib/src/macos/build_macos.dart

iOSのビルドと同じようにmigrationとtarget sdk versionを更新する……のだが、次のissueのためmigrationのみを実装している。

https://github.com/flutter/flutter/issues/146204

packages/flutter_tools/lib/src/macos/cocoapod_utils.dart

  • Swift PMが有効な場合にスキップされる
  • CocoaPodsを利用する場合には、Swift PMを利用しないモードとなり、キャッシュの更新判定からSwift PMのファイルを除く

packages/flutter_tools/lib/src/macos/cocoapods.dart

  • Swift PMが有効な場合にスキップされる
  • xcconfigIncludesPods プロパティの追加

packages/flutter_tools/lib/src/macos/swift_package_manager.dart

SwiftPackageManagerを定義している。
定義されているメソッドは次の通り。

  • Future<void> generatePluginsSwiftPackage
    • FlutterGeneratedPluginSwiftPackageを作り、flutterのSwift PM対応プラグインをまとめた構造を作る
    • _dependenciesForPluginsの中をざっとみると、Package.swiftを持つパスを探しているので、Swift PM対応プラグインを探していることが確認できる
    • コメントが適切に書いてあるので、目を通すと処理が理解しやすい
  • static void updateMinimumDeployment
    • SwiftPackageSupportedPlatformに設定されている、min SDKを更新
    • Podfileで(大体のケースで)書いていた処理が、Swift PMとして対応している印象

packages/flutter_tools/lib/src/macos/swift_packages.dart

DartのSwiftPackageオブジェクトを、.createSwiftPackageメソッドを呼び出すとPackage.swiftファイルとして書き出すクラス。
_singleIndent として ' 'を定義するなど、なるほどという工夫があちこちにある。

https://github.com/flutter/flutter/pull/146256/files#r1556534097

のやりとりにあるように、Swift PMはstaticな構造を作るほうが推奨とのこと。Privacy Manifestも上手く動いているとのことなので、開発中に気になることはほぼないと思われる。

koji-1009koji-1009

packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart

目玉その2。
プロジェクトをSwift PM対応版に更新する処理。1000行を超えているので一見読みにくいが、

  • void _migrateScheme
    • xcschemeファイル、つまりRunner.xcschemeのマイグレーション処理
    • xcode_backend.sh prepareの処理をschemeのPreActionsに追加している
      • この処理により、flutterプロジェクトの実行やビルド時にSwift PMの依存関係解決がなされる
      • xcode_backend.shはshell scriptである必要がある
  • void _migratePbxproj
    • project.pbxprojのマイグレーション処理
    • Xcodeが(flutterのプロジェクトで作る)Swift PMのファイルを認識できるように、必要なファイルをプロジェクトに追加する
  • 上記2メソッドが必要かどうか判断し、途中でExcptionが発生したら処理をリバートするのが Future<void> migrate
koji-1009koji-1009

確かに、これでSwift PM対応できそう。タイミング的に次のstable版に含まれないかもしれないが、先にpluginの対応を進めないと問題が生じるので、beta版には含まれるかも?
楽しみですね。