Flutterの "Add Swift Package Manager as new opt-in feature for iOS and macOS #146256 " を読む
を読んで、FlutterでSwift PMがどのように利用されるのかを理解したい。
dev/bots/analyze.dart
は気にしなくてOK。 Package.swift
ファイルに対しては、ファイル内にライセンスが書いてあるかどうかのチェックをスキップする、という処理になっているだけ。
Package.swift
はライセンス(ApatchとかMITとか)をまず書かないので、この処理はそのまま読めば良い。
packages/integration_test
は既存のCocoaPods向けテストを、Swift PMでもテストできるよう、階層をいじっているだけ。
CocoaPodsでは /Classes/
配下にコードを置いていたが、Swift PMでは /Sources/
以下にコードを置くことになる。また、/Package.swift
がSwift PM用に追加されているので、このswiftファイルをCocoaPodsで扱うソースコードから外す狙いもある(ハズ)。
packages/flutter_tools
が本丸。packages/flutter_tools/bin
とpackages/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
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
に定義されている。
_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_ios
やdebug_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,
);
なので、fileSytem
とplatform
、project
が引数に追加された。この引数はv3.19.0には存在しないので、今回のPRで追加された処理になっている(ハズ)。
packages/flutter_tools/lib/src/commands/clean.dart
deleteFile(flutterProject.ios.flutterPluginSwiftPackageDirectory);
と deleteFile(flutterProject.macos.flutterPluginSwiftPackageDirectory);
なので、clean時に削除されることがわかる。
CocoaPodsの場合、 /ios/Pods
は flutter clean
コマンドで削除されない。このため、いい点としては flutter clean
でライブラリのキャッシュもリフレッシュされる。他方、flutter clean
後に都度Swift PMの依存関係解決が行われるので、想定するよりも時間がかかるかもしれない。
追加コミットで packages/flutter_tools/lib/src/commands/clean.dart
の変更が削除され、packages/flutter_tools/lib/src/commands/build_ios_framework.dart
とpackages/flutter_tools/lib/src/commands/build_macos_framework.dart
が追加された。
clean.dart
の処理が戻ったコミット。opt-inなので、利用しないケースで影響が出ないようにしたのかな。
ios-frameworkとmacos-frameworkを生成するときには、CocoaPodsを利用するように変更されたコミット。
このフラグが立ってる状態でSwift PMを利用する処理に入ると、 Swift Package Manager does not yet support this command.
とワーニング表示されるので、flutterのプラグインとしてiOSやmacOS向けのxcframeworkを作る処理がうまく動かないのかもしれない。
packages/flutter_tools/bin/macos_assemble.sh
後述の xcode_backend.dart
のmacos版の様子。
macos向けのビルドはわからないので、スキップ。
packages/flutter_tools/bin/podhelper.rb
「Swift PMが有効であれば、CocoaPodsを走らせなくてもいいのではないか?」という気もするけれど、そうとも言い切れないケース対応なのかな? という雰囲気。
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の適切なバージョンを解決するのはどっち……? 状態になってしまう。正解は開発者が頑張るしかないので、この運用はあまりうまくいかないはず。
そう考えると
- Swift PMが有効、そしてライブラリが
Package.swift
を持っているケース - ライブラリが
Package.swift
を持っているが、~.podspec
を持っていないケース
を考慮しているのは、Swift PMへの移行期を見た対応なのかなと。印象です。
packages/flutter_tools/bin/xcode_backend.dart
ファイルをぱっと見てもなんのこっちゃ、となったのでPRを遡ってみた。どうやら、もともとbashで書かれていた処理を、dartにしたものらしい。
なので、たぶんiOSやmacOS向けのビルドに必要なステップが定義されている。そうみてみると、このPRでの変更点は大きく分けて2つ。
- スクリプトに
prepare
コマンドが追加された -
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
で発見。
このステップは、ログに ' ├─Building App.xcframework...',
を書き出す様子。見た覚えがない気がする(flutter build ios
だと出ている……?)ので、関連issueを見てみる。
issueの内容から察するに、おそらくiOS向けに通常のアプリを作っている場合には、利用したことがないのではないかなと。Swift PMの導入によって、ログ出力に(当然)変更があるので、この辺りに注目してみてもいいかも。
build
コマンドを見ていくと、最後に次のログを出力している。
streamOutput('done');
streamOutput(' └─Compiling, linking and signing...');
echo('Project $projectPath built and packaged successfully.');
なので、見慣れたアプリのビルド処理の様子。
release_ios_bundle_flutter_assets
で呼び出される処理を見ていくと、次の箇所であることがわかる。
継承元のクラスが、非常に気になる名前をしているので見てみる。
ということで、ここでbundleしているのはdSYMの様子。確かにSwift PMで落としてきたコードをコンパイルしたら、そのdSYMをセットしないとマズイですね。
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
は、beta
とstable
のチャンネルごとにデフォルト値を設定するための機構らしい。まだ開発中なので、master
チャンネルでのみデフォルト有効、ってことですね。
packages/flutter_tools/lib/src/flutter_manifest.dart
pabspec.yaml
のflutter:
で設定されているオプションを見ている様子。
プロジェクトごとに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);
}
}
差分を見ると、forceCocoaPodsOnly
はchanged
の判定に影響を与えることになる。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? pluginSwiftPackageManifestPath
とString? 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
のリファクタリングがされたような印象です。
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
packages/flutter_tools/lib/src/macos/build_macos.dart
iOSのビルドと同じようにmigrationとtarget sdk versionを更新する……のだが、次のissueのためmigrationのみを実装している。
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
として ' '
を定義するなど、なるほどという工夫があちこちにある。
のやりとりにあるように、Swift PMはstaticな構造を作るほうが推奨とのこと。Privacy Manifestも上手く動いているとのことなので、開発中に気になることはほぼないと思われる。
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
確かに、これでSwift PM対応できそう。タイミング的に次のstable版に含まれないかもしれないが、先にpluginの対応を進めないと問題が生じるので、beta版には含まれるかも?
楽しみですね。