「.NET MAUI(.NET 9、Mac Catalyst)」で dotnet publish により作成した .pkg をダブルクリックで実行したのに、/Applications にアプリが見当たらない――。この現象はパッケージの不備ではなく、macOS インストーラの“バンドル再配置(relocation)”の仕様やレシートの扱いが影響している可能性が高いです。この記事では原因の見極め方から再現検証、確実に /Applications に配置する実践手順までを、コピペで試せるコマンド付きで詳解します。
現象の整理(前提と再現条件)
読者の前提と近い構成で事象を整理します。
dotnet publish ChronoWiz/ChronoWiz.csproj \
-f net9.0-maccatalyst -c Release \
-p:MtouchLink=SdkOnly \
-p:CreatePackage=true \
-p:UseHardenedRuntime=true \
-o "/Users/eks/Downloads/"
- 上記の発行で
ChronoWiz-1.4.12.pkgが生成される。 pkgを実行しても/ApplicationsにChronoWiz.appが現れない。- 一方、発行元のフォルダ(例:
bin/Release/net9.0-maccatalyst/)にはChronoWiz.appが存在し、そこからは起動できる。
結論から:多くの場合「バンドル再配置(relocation)」が原因
macOS のパッケージは バンドル(.app)を“見つかった場所でアップデート”するという仕様を持ちます。pkgbuild --component <YourApp.app> 由来の コンポーネント・パッケージは既定で「再配置可能(relocatable)」です。この場合、インストーラはターゲットボリューム内を走査し、同一の CFBundleIdentifier を持つアプリがどこか(例:ビルド生成先の bin/Release/... や ~/Downloads)に存在すれば、そこを“インストール先”とみなして上書き更新します。つまり新規コピーが /Applications に現れず、「ビルド先の .app が入れ替わっているだけ」に見えるわけです。
この挙動は不具合ではなく仕様です。ユーザーが任意の場所に置いたアプリを尊重し、その場所で更新するのが bundle relocation の思想だからです。
ありがちな別要因(見落としがちな“インストールされたのに見えない”ケース)
- ユーザー用アプリケーションフォルダに入っている:管理者権限が無い状況やパッケージの属性により、
~/Applications(ユーザーのホーム配下)へ配置されることがあります。 - レシート(receipts)により“更新扱い”になっている:過去に同じ
identifierの.pkgをインストールしていると、更新として扱われ、ユーザーが気づきにくい場所にとどまることがあります。 - パッケージのペイロードに /Applications が入っていない:生成された
.pkgの中身が期待通りでないケース(例:構成の誤りでペイロードが空、あるいはパス指定のミス)。 - Gatekeeper や署名の問題で起動できず“無い”と誤認:配置自体はされているが、初回起動で弾かれて存在しないと勘違いしてしまうパターン。
原因と解法の対応表(要点だけ先取り)
| 症状 | 主な原因 | 確認コマンド | 推奨対処 |
|---|---|---|---|
| /Applications に現れない | Bundle relocation でビルド先の .app が更新された | mdfind "kMDItemCFBundleIdentifier == 'com.example.ChronoWiz'" | ビルド先の .app を一時移動/リネーム、または非リロケータブルな .pkg を作成 |
| どこにも見当たらない | ペイロードが想定と異なる/空 | pkgutil --payload-files ChronoWiz-1.4.12.pkg | head | ペイロードに /Applications/ChronoWiz.app が含まれるかを確認し、生成条件を見直す |
| ホーム配下に入った | ユーザーインストール(権限)/ relocatable | ls ~/Applications | grep ChronoWiz | 管理者で実行、または非リロケータブル化 |
| インストールが“更新扱い” | 過去の receipt が残存 | pkgutil --pkgs | grep ChronoWiz | pkgutil --forget <pkgid> でレシートを破棄して再検証 |
| 起動できない | 署名/Notarization/Gatekeeper | spctl --assess -vvv /Applications/ChronoWiz.app | 適切な署名と Hardened Runtime、必要に応じて Notarization を実施 |
まずは「どこに入ったか」を機械的に特定する
# バンドル ID で検索(Info.plist の CFBundleIdentifier)
/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
"/path/to/ChronoWiz.app/Contents/Info.plist"
mdfind "kMDItemCFBundleIdentifier == 'com.example.ChronoWiz'"
# 典型的な候補
ls /Applications | grep -i ChronoWiz
ls ~/Applications | grep -i ChronoWiz
ビルド先(例:bin/Release/net9.0-maccatalyst/)に残っている ChronoWiz.app の更新日時が .pkg 実行時刻と一致していれば、そこで“更新”が起きていると判断できます。
.pkg の中身とインストーラの動きを可視化する
ペイロードの確認
# ペイロードのファイル一覧をざっと確認
pkgutil --payload-files ChronoWiz-1.4.12.pkg | head -n 50
# 期待どおり /Applications/ChronoWiz.app 配下が含まれているか?
pkgutil --payload-files ChronoWiz-1.4.12.pkg | grep -E '^Applications/ChronoWiz.app'
より詳細に見る場合は展開して Bom を読むと構造がわかります。
mkdir -p /tmp/chrono-pkg
pkgutil --expand-full ChronoWiz-1.4.12.pkg /tmp/chrono-pkg
# Bill of Materials(BOM)からファイル・ディレクトリを抽出
lsbom -fls /tmp/chrono-pkg/*/Bom | head -n 100
インストールログで実行時の判断を追跡
# 詳細ログを有効にしてインストール
sudo installer -pkg "/Users/eks/Downloads/ChronoWiz-1.4.12.pkg" \
-target / -verboseR -dumplog
# あるいはシステムログを確認
tail -n 200 /var/log/install.log
ログ中に「relocated」「Existing bundle found」「updating existing bundle」等の記述があれば、bundle relocation が発動しています。
同一 Mac で“確実に /Applications に置く”検証手順
方法 A:既存の同一バンドルを一時的に退避する(最短)
- 発行先の
ChronoWiz.appを一時フォルダへ移動またはリネームします。mv bin/Release/net9.0-maccatalyst/ChronoWiz.app bin/Release/net9.0-maccatalyst/_ChronoWiz.app.bak .pkgを再実行し、/Applicationsを確認します。
「とにかく今この Mac で確認したい」時の一手です。欠点は手作業の煩雑さと、うっかり元に戻し忘れるリスク。
方法 B:ユーザーを切り替える/別 Mac で試す(確実)
新規ユーザー(クリーンなホーム)で .pkg を実行すると、同一バンドルが見つからず、既定の /Applications へ配置されやすくなります。
方法 C:非リロケータブルなテスト用 .pkg を自作して検証(本命)
dotnet publish の成果物から 再配置不可(BundleIsRelocatable=false)のテストパッケージを自前で作ると、常に /Applications へ展開されます。以下は最小例です。
# 1) コンポーネント挙動を制御する Plist を用意
cat > /tmp/component.plist <<'PLIST'
<?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>BundleIsRelocatable</key><false/>
<key>BundleIsVersionChecked</key><true/>
<key>BundleOverwriteAction</key><string>upgrade</string>
</dict>
</plist>
PLIST
# 2) MAUI の生成物から .pkg を作る(署名は割愛)
APP="/absolute/path/to/bin/Release/net9.0-maccatalyst/maccatalyst-x64/ChronoWiz.app"
pkgbuild
--component "$APP"
--install-location "/Applications"
--identifier "com.example.ChronoWiz"
--component-plist "/tmp/component.plist"
"/tmp/ChronoWiz-nonrelocatable.pkg"
この .pkg は既存の配置場所に関係なく /Applications に展開されるため、テスト観点で最も分かりやすいです。必要に応じて productbuild でディストリビューションパッケージに仕上げます。
レシート(receipts)の扱いと“クリーンな再インストール”
macOS は .pkg をインストールすると「レシート(インストール記録)」を保存します。過去のレシートがあると更新扱いになり、挙動が分かりにくくなります。検証時は以下で整頓します。
# パッケージ ID を特定(例:com.example.ChronoWiz)
pkgutil --pkgs | grep -i chrono
# レシートに登録されたファイル一覧を確認
pkgutil --files com.example.ChronoWiz | head -n 50
# レシートを“忘れる”(テスト時に有効)
sudo pkgutil --forget com.example.ChronoWiz
「ビルド先の .app が邪魔をする」を根本から回避する設計
- テスト専用のバンドル ID を使う:
com.example.ChronoWiz.devのように区別すると、製品版と衝突せず relocation も起きにくくなります。 - バージョンは毎回上げる:
CFBundleShortVersionStringとCFBundleVersionを確実に更新しておくと、アップデート検出が安定します。 - CI で“クリーンマシン”に配布して検証:ローカル環境の残骸(古い .app やレシート)に影響されません。
署名・Hardened Runtime・Notarization の整理(誤解しやすいポイント)
ここは混同が多い領域なので、役割を分けて押さえます。
| 目的 | どこで必要? | キーポイント |
|---|---|---|
| コード署名(codesign) | 配布前の .app/.pkg すべて | 適切な証明書(開発/配布)を用い、--options runtime 相当(Hardened Runtime)を有効化 |
| Notarization(公証) | ストア外配布(Developer ID) | notarytool submit で公証、成功後に stapler staple でチケット付与 |
| App Store Connect(Mac App Store) | ストア配布 | 公証は不要(Apple 側で審査)。証明書・プロビジョニングはストア用を使用し、Transporter/Xcode からアップロード |
dotnet publish で署名まで行う場合は、概ね以下のようなプロパティを併用します(値名は環境により異なります)。
dotnet publish ChronoWiz/ChronoWiz.csproj \
-f net9.0-maccatalyst -c Release \
-p:CreatePackage=true \
-p:UseHardenedRuntime=true \
-p:EnableCodeSigning=true \
-p:CodesignKey="Developer ID Application: Your Name (TEAMID)" \
-p:CodesignEntitlements="Platforms/MacCatalyst/Entitlements.plist" \
-o "/Users/eks/Downloads/"
ストア外配布では、生成後に次のとおり公証&ステープルします。
# 公証
xcrun notarytool submit "/Users/eks/Downloads/ChronoWiz-1.4.12.pkg" \
--apple-id "[email protected]" \
--team-id "TEAMID" \
--password "app-specific-password" \
--wait
# ステープル(チケットを貼る)
xcrun stapler staple "/Users/eks/Downloads/ChronoWiz-1.4.12.pkg"
起動可否は Gatekeeper の評価で確認できます。
spctl --assess -vvv "/Applications/ChronoWiz.app"
codesign -dv --verbose=4 "/Applications/ChronoWiz.app" 2>&1 | head -n 20
MAUI(Mac Catalyst)特有の実用メモ
- ランタイム識別子(RID):
maccatalyst-x64/maccatalyst-arm64を意識。ユニバーサルバイナリにする場合は lipo で結合、またはマルチターゲットの発行戦略を採る。 - リンク設定:
MtouchLink=SdkOnlyは一般的ですが、サードパーティ依存によりNoneを選ぶ必要がある場合も。不要コード除去が過剰だと起動後クラッシュして「インストールされていない」ように見えます。 - Info.plist の整合性:
CFBundleIdentifier、CFBundleShortVersionString、CFBundleVersion、LSMinimumSystemVersionをチェック。バージョンが逆行すると更新がスキップされます。 - アプリ名の衝突:
ChronoWiz.appという名前が同じでも、CFBundleIdentifierが異なれば別アプリ扱いになります。テストと本番で ID を分けるのが安全です。
「この .pkg は正しいのか?」を 5 分で確かめるチェックリスト
- 1. ペイロードに目的の .app が含まれている:
pkgutil --payload-files <pkg>でApplications/ChronoWiz.app/配下が見える。 - 2. 署名情報が付いている:
pkgutil --check-signature <pkg>の出力を確認。 - 3. 起動基本要件を満たす:
codesign -dvで Hardened Runtime、spctl --assessで Gatekeeper OK。 - 4. ログは正常:
installer -verboseR実行時にエラーがない。relocation の痕跡があるかも観察。 - 5. 重複配置の可能性を排除:
mdfindで同じバンドル ID を複数検出しない。
CI 向け:失敗しない一発検証スクリプト(雛形)
#!/usr/bin/env bash
set -euo pipefail
APP_ID="com.example.ChronoWiz"
PKG="/Users/eks/Downloads/ChronoWiz-1.4.12.pkg"
echo "[1/5] Payload sanity check"
pkgutil --payload-files "$PKG" | grep -q "^Applications/ChronoWiz.app"
|| { echo "payload NG"; exit 1; }
echo "[2/5] Forget previous receipts (if any)"
if pkgutil --pkgs | grep -q "$APP_ID"; then
sudo pkgutil --forget "$APP_ID" || true
fi
echo "[3/5] Remove previous app (if any)"
sudo rm -rf "/Applications/ChronoWiz.app" || true
rm -rf "$HOME/Applications/ChronoWiz.app" || true
echo "[4/5] Install with verbose logs"
sudo installer -pkg "$PKG" -target / -verboseR -dumplog
echo "[5/5] Verify presence"
test -d "/Applications/ChronoWiz.app" || {
echo "Installation did not end up in /Applications (relocation?)"; exit 1; }
echo "OK"
トラブル対処の実践ノウハウ
Gatekeeper で弾かれる/“壊れているため開けません”と出る
- まず署名を再確認:
codesign -dv --verbose=4 /Applications/ChronoWiz.app - ストア外配布なら公証:
notarytool submit→stapler staple。 - テスト目的で検証するだけなら、一時的に隔離属性を落として挙動を観察:
xattr -r -d com.apple.quarantine /Applications/ChronoWiz.app(配布物には非推奨)。
インストーラが「成功」と言うのに何も変わらない
/var/log/install.logの末尾 200 行を確認。既存バンドル発見→再配置の記録があれば、原因は relocation。- レシートで“更新先”を把握:
pkgutil --pkgs→pkgutil --files <pkgid>。
どうしても /Applications に入れたい(仕様より強制)
- 非リロケータブル化(本稿の「方法 C」)。
- 別バンドル ID(
.devなど)で衝突を断つ。
MAUI 開発フローへの落とし込み(再発防止)
- 発行:
dotnet publish(署名まで含めるならプロパティ指定)。 - パッケージ検査:
pkgutil --payload-files/--check-signature。 - ローカル検証:
- クリーンユーザーで実行、または非リロケータブル .pkg を使う。
- レシートと既存 .app を整理してから検証する。
- Gatekeeper/公証:ストア外のみ公証→ステープル。
- 配布:ストア配布は App Store Connect へ、ストア外はサイト配布などへ。
ケーススタディ:今回の ChronoWiz を 30 分で正常検証する手順
- ビルド先の
ChronoWiz.appを退避(または削除)する。 pkgutil --payload-files ChronoWiz-1.4.12.pkgでApplications/ChronoWiz.appの存在を確認。sudo installer -pkg ChronoWiz-1.4.12.pkg -target / -verboseRを実行。/ApplicationsにChronoWiz.appが現れることを確認。起動して Gatekeeper のダイアログ対応。- 必要なら
spctlとcodesignで診断、公証フローを回す。 - 包摂テストとして、非リロケータブル版
.pkgも作り、再現性を確保する。
よくある質問(FAQ)
Q. ビルド先にアプリがあると、なぜ /Applications にコピーされないのですか?
A. コンポーネントパッケージの既定動作(relocation)により、同じバンドル ID を持つアプリがどこであってもディスク上に見つかると、その場所を“インストール先”と見なして上書きされます。ユーザーの意図した場所を尊重するための仕様です。
Q. リロケーションを禁止するには?
A. pkgbuild --component-plist で BundleIsRelocatable=false を指定したパッケージを作ります(記事内の「方法 C」)。
Q. .pkg の設置先を確実に /Applications に固定したい
A. 非リロケータブル化に加え、必ず管理者権限でインストールしてください。また、テストでは過去のレシートを pkgutil --forget で消してから実行すると結果が読みやすくなります。
Q. App Store Connect に提出する .pkg と、ローカル検証用 .pkg は同じで良い?
A. 署名やプロビジョニングの要件が異なるため、提出用はストア向けの設定でビルドしてください。ローカル検証は非リロケータブルのテスト .pkg を併用すると挙動が明確です。
Q. “インストール成功”と出るのに起動しない
A. 署名/権限/Entitlements 由来のクラッシュや Gatekeeper の評価が原因のことが多いです。spctl --assess -vvv と Console.app のクラッシュログ、codesign -dv を併用して切り分けましょう。
まとめ
今回の「.pkg を実行しても /Applications に見当たらない」問題は、macOS の bundle relocation が発動し、ビルド先などに存在する同一バンドル ID のアプリがその場で更新されたことが主因である可能性が高いと分かりました。実運用では次の 3 点を徹底すれば迷いません。
- 場所の特定:
mdfindとinstall.logで“どこに入ったか”をまず確定。 - 非リロケータブル化 or 退避:テスト時はビルド先 .app をどかす、もしくは
BundleIsRelocatable=falseで固定。 - レシート整頓:
pkgutil --forgetで更新・再現性を安定化。
この 3 本柱を押さえておけば、App Store 提出前のローカル検証も、ストア外配布の QA も、短時間で安定して再現・確認できます。MAUI/.NET 9 の開発サイクルに是非組み込み、“インストールされたはずなのに無い”という時間泥棒をここで終わらせましょう。

コメント