ElectronアプリをMac AppStoreに登録する

## Electronアプリを作ったのでMac AppStore (MAS)に登録したい

Electronアプリを無事に作成できて

```shell
$ electron ./dist/main.js
```

みたいな感じで動かすことができました。これをMac AppStore(以下MAS)に登録したいと思います。
ググってみてもiOSアプリの登録方法は沢山ヒットするのですが、macOSアプリの登録方法はあまりヒットしません。検索時に"macos"とか"osx"とか入れても、iOSアプリの登録方法が大量に混ざってきて、なかなかmacOSアプリに関する情報に行きつけません。
ましてやこれがElectronアプリとなるとさらに必要な情報になるとさらに限られてきますし、Electronの最新バージョンに適合したものなのかどうかもよくわかりません。
macOSアプリのパッケージ情報もx64を前提にしたものが多く、M1 Mac発売後のmacOSアプリの標準であるUniversalバイナリに関する情報も少なく感じました。
私自身が今回なんとかUniversalバイナリのElectronアプリをMASに登録できたので、その情報を書いていきます。
方法はとんでもなくグチャグチャで、Electronについてよくご存知の方から見ればとんでもなくムダなことを沢山やっていると思いますが、**MASに登録できる**ことを最優先で行いました。
より良い方法をご存知の方はご教示いただければ幸いです。
**Xcode**や**Node.js**、**yarn**等の必要ソフトウェアはすでにインストールしていること、App Store Connectに該当macOSアプリのページが作成されていることを前提に以下の話を進めます。

#### Electronのバージョン

Electron関連のバージョンは以下の通りです。

```json:package.json(抜粋)
{
  ...
  "devDependencies": {
    "electron": "^16.0.7",
    "electron-builder": "^22.14.5",
    "electron-packager": "^15.4.0",
    ...
  },
  ...
}
```

#### まずはmacOS用のアイコンを作って適用する

macOS用のアイコンが無いとMASへの登録ができません。
プロジェクトディレクトリ配下にiconというディレクトリを作って以下のシェルを実行します。
シェルスクリプトはプロジェクトディレクトリ配下にshellsというディレクトリを作って収めます。
[こちら](https://gist.github.com/jimye/086d506ace7981cabd8c8c8434a5b369)を参考にさせていただきました。ありがとうございます。

```shell:shells/icon_create.sh
#!/bin/sh

input_filepath="あなたのiconイメージのPNGファイル(サイズ1024*1024)のiconディレクトリからの相対パス"
output_iconset_name="icon.iconset"

# 確実に　プロジェクトディレクトリ/icon に行けるように。
MY_DIR_NAME=`dirname $0`
SHELL_SCRIP_DIR=`cd $MY_DIR_NAME;pwd`
cd $SHELL_SCRIP_DIR
cd ../icon

# icon用PNGファイルを収めるディレクトリを作成
mkdir $output_iconset_name

sips -z 16 16     $input_filepath --out "${output_iconset_name}/icon_16x16.png"
sips -z 32 32     $input_filepath --out "${output_iconset_name}/icon_16x16@2x.png"
sips -z 32 32     $input_filepath --out "${output_iconset_name}/icon_32x32.png"
sips -z 64 64     $input_filepath --out "${output_iconset_name}/icon_32x32@2x.png"
sips -z 128 128   $input_filepath --out "${output_iconset_name}/icon_128x128.png"
sips -z 256 256   $input_filepath --out "${output_iconset_name}/icon_128x128@2x.png"
sips -z 256 256   $input_filepath --out "${output_iconset_name}/icon_256x256.png"
sips -z 512 512   $input_filepath --out "${output_iconset_name}/icon_256x256@2x.png"
sips -z 512 512   $input_filepath --out "${output_iconset_name}/icon_512x512.png"
sips -z 1024 1024 $input_filepath --out "${output_iconset_name}/icon_512x512@2x.png"

# icon.icns ファイルを作成
iconutil -c icns $output_iconset_name

# icon用PNGファイルを収めるディレクトリを削除
rm -R $output_iconset_name
# プロジェクトディレクトリ/icon/icon.icns が出来ました。
```

#### macOS用パッケージ定義をpackage.jsonに追加

package.jsonにmacOSパッケージを作るための定義を追加します。
パッケージを作成するターゲットディレクトリはプロジェクトディレクトリ配下にbuildという名前のディレクトリが作成されます。
なお、MAS登録に必要なファイル一式はプロジェクトディレクトリ配下にAppStoreというディレクトリを作ってそこに入れています。
以下の記述にある**com.example.yourelectron**はあなたのElectronアプリのAppIDに、**MASTEAMID**はApple Developer ProgramのチームIDにそれぞれ書き換えてください。
参考例のロケールはen(英語)とja(日本語)のみです。他のロケールが必要な場合は追加してください。

```json:package.json(抜粋)
{
  "name": "your_electron",
  "version": "1.0.0",
  ...
  "electronLanguagesInfoPlistStrings": {
    "en": {
      "CFBundleName": "Your Electron",
      "CFBundleDisplayName": "Your Electron"
    },
    "ja": {
      "CFBundleName": "あなたのElectron",
      "CFBundleDisplayName": "あなたのElectron"
    }
  },
  "build": {
    "productName": "Your-Electron",
    "appId": "com.example.yourelectron",
    ...
    "directories": {
      ...
      "output": "./build"
    },
    "files": [
      "./dist/**/*.js",
      ... (アプリが動作に必要なファイルを列挙してください)
    ],
    "mac": {
      "icon": "./icon/icon.icns",
      "category": "public.app-category.business",
      "bundleVersion": "6",
      "extendInfo": {
        "LSHasLocalizedDisplayName": true,
        "CFBundleDevelopmentRegion": "en",
        "CFBundleURLName": "com.example.yourelectron",
        "ElectronTeamID": "MASTEAMID"
      },
      "identity": "YOUR-MAS-COMPANY-NAME (MASTEAMID)",
      "provisioningProfile": "./AppStore/your_electron_distribution.provisionprofile",
      "electronLanguages": [
        "en",
        "ja"
      ],
      "target": [
        "dmg",
        "mas"
      ]
    },
    "mas": {
      "icon": "./icon/icon.icns",
      "category": "public.app-category.business",
      "bundleVersion": "6",
      "extendInfo": {
        "LSHasLocalizedDisplayName": true,
        "CFBundleDevelopmentRegion": "en",
        "CFBundleURLName": "com.example.yourelectron",
        "ElectronTeamID": "MASTEAMID"
      },
      "identity": "YOUR-MAS-COMPANY-NAME (MASTEAMID)",
      "entitlements": "./AppStore/parent.plist",
      "entitlementsInherit": "./AppStore/child.plist",
      "entitlementsLoginHelper": "./AppStore/login_helper.plist",
      "provisioningProfile": "./AppStore/your_electron_distribution.provisionprofile",
      "type": "distribution",
      "hardenedRuntime": true,
      "gatekeeperAssess": false,
      "electronLanguages": [
        "en",
        "ja"
      ],
      "target": [
        "zip"
      ]
    },
    "afterPack": "./shells/buildAfterPack.js",
    "extraMetadata": {
      "main": "./dist/main.js"
    }
  }
  ...
}
```

package.jsonの内容について説明します。

- **electronLanguagesInfoPlistStrings**はmacOSの各ロケールに合わせて書き換えられる、バンドル名(CFBundleName)やバンドル表示名(CFBundleDisplayName)の定義です。
 
- **build**内にビルドとパッケージについて必要な情報を記述します。
    - **productName**はこれから作成される*.appのファイル名となります。
    - **appId**にはあなたのAppIDを指定してください。
    - **directories**にはビルドプロセスに必要なディレクトリパス情報が書かれます。ここでは出力ディレクトリとしてbuildが指定されています。
    - **files**にはパッケージ作成に必要なファイル一覧を列挙してください。jsファイルやhtmlファイル、iconsファイル等です。
    - **mac**にはmacOS用のアプリケーションパッケージ情報を記載します。
        - **icon**には先程作成したアイコンファイルへの相対パスを記述します。
        - **category**にはあなたのアプリのカテゴリーを記述してください。[こちら](https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype)からご自身のアプリに合ったものをお選びください。
        - **bundleVersion**にはInfo.plistに書かれるCFBundleVersionの値を設定してください。この部分は各アプリを後述するTransporterでアップロードする場合に重複が無いように設定しないとなりません。
        - **extendInfo**はInfo.plistに実際に追加・更新するキーと値のセットです。ここでは以下の4つについて説明します。必要なInfo.plistの値が他にありましたら、追加してください。
            - **LSHasLocalizedDisplayName** ローカライズ情報が存在することを指定する場合はtrueにセットします。ここの例ではCFBundleNameとCFBundleDisplayNameがそれに該当します。
            - **CFBundleDevelopmentRegion** デフォルトロケールを設定します。ここではen(英語)です。
            - **CFBundleURLName** Info.plistにCFBundleURLNameを追加します。ご自身のアプリのAppIDを指定してください。
            - **ElectronTeamID** ElectronではApple Developer ProgramのチームIDを指定しないとならないようです。
        - **identity** Apple Developer ProgramのCertificateの名前を設定します。electron-packagerはApple DistributionのCertificateをキーチェーンアクセスから探し出すようです。
        - **provisioningProfile** Apple Distributionと該当AppIDで作成されたMac AppStore Distributionのprofileをダウンロードして該当パスにコピーしてください。
        - **electronLanguages** ローカライズするロケール一覧を列挙してください。通常はelectronLanguagesInfoPlistStringsのロケール一覧と同じで良いと思います。この指定を忘れるとAppStoreに大量の言語をサポートしているかのように表示されますし、英語・日本語以外のロケールで英語のバンドル表示名が使用されなくなります。
        - **target** ここではインストーラのディスクイメージとMac App Store用パッケージを作成することを指定しています。
    - **mas**にはmacのtargetの"mas"によって作成されるアプリケーションパッケージ情報記載します。記述内容もmacに似ています。先程からしきりにMASと言っているのはここに由来しています。macと違う部分のみ説明します。
        - **entitlements** Electronアプリに埋め込むentitlementの一覧を指定します。
        - **entitlementsInherit** Electron Helperアプリに埋め込むentitlementの一覧を指定します。
        - **entitlementsLoginHelper** Electron Login Helperアプリに埋め込むentitlementの一覧を指定します。
        - **type** ここではdistributionを指定しました。他にも色々なタイプがあるようですが、説明は割愛いたします。
        - **hardenedRuntime** MASタイプのビルドにはこれをtrueに指定しないとならないようなことを書いてあったような気がします。間違っていたらご指摘ください。
        - **gatekeeperAssess** macOSのgatekeeperへのアクセスを行うかどうかの指定のようです。ここではfalseを指定しました。
        - **target** ここではzipを指定しましたが、特にzipファイルが作成されることはないみたいです。
    - **afterPack**にはパッケージプロセス終了後に動作させるスクリプトを指定します。
    - **extraMetadata**にはエントリーポイントの記述を行います。

かなりの不要な情報が設定されているのではないかと思われますが、わからないなりに色々と調べた結果を記述しました。

3つのエンタイトルメントの内容を示します。

```xml:AppStore/parent.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>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-only</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.debugger</key>
    <true/>
    <key>com.apple.security.application-groups</key>
    <array>
      <string>MASTEAMID.com.example.yourelectron</string>
    </array>
  </dict>
</plist>
```

**com.apple.security.app-sandbox**はMASでの配布には必ず必要な属性です。
**com.apple.security.network.client**はアプリがネットワーク機能を使う時に必要なものです。
**com.apple.security.files.user-selected.read-only**はファイルオープンダイアログ等を使ってファイルを読む時に必要となります。ファイルライトダイアログを使ってファイルを書く場合は**com.apple.security.files.user-selected.read-write**属性が代わりに必要となります。
その他の属性はElectronアプリにとりあえず必要、とだけ思っておいてください。実はよく理解できていないので必要無いのかもしれません。

```xml:AppStore/child.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>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.inherit</key>
    <true/>
  </dict>
</plist>
```

```xml:AppStore/login_helper.plist
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.inherit</key>
    <true/>
  </dict>
</plist>
```

上記2つのエンタイトルメント属性もとりあえず必要とだけ思っておいてください。

以下に./shells/buildAfterPack.jsの内容です。
このJavaScriptがCFBundleNameとCFBundleDisplayNameのローカライズを行います。
[こちらのページ](https://github.com/electron-userland/electron-builder/issues/4630)を参考にさせていただきました。ありがとうございます。
他の属性のローカライズが必要な場合はpackagejsonのelectronLanguagesInfoPlistStringsに値を追加してください。

```javascript:shells/buildAfterPack.js
// @see https://github.com/electron-userland/electron-builder/issues/4630
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs');

exports.default = async context => {
  const { electronPlatformName, appOutDir } = context;
  if (electronPlatformName !== 'darwin' && electronPlatformName !== 'mas') {
    return;
  }
  const {
    productFilename,
    info: {
      _metadata: { electronLanguagesInfoPlistStrings },
    },
  } = context.packager.appInfo;

  const resPath = `${appOutDir}/${productFilename}.app/Contents/Resources/`;
  console.log(
    '\n> Create a language package Sta based on the package.json configuration item "electronLanguagesInfoPlistStrings" \n',
    '\n>  electronLanguagesInfoPlistStrings:\n',
    electronLanguagesInfoPlistStrings,
    '\n\n',
    '>  ResourcesPath:',
    resPath,
  );

  // Create APP language pack file.
  return await Promise.all(
    Object.keys(electronLanguagesInfoPlistStrings).map(langKey => {
      const infoPlistStrPath = `${langKey}.lproj/InfoPlist.strings`;
      let infos = '';
      const langItem = electronLanguagesInfoPlistStrings[langKey];
      Object.keys(langItem).forEach(infoKey => {
        infos += `"${infoKey}" = "${langItem[infoKey]}";\n`;
      });
      return new Promise(resolve => {
        fs.writeFile(`${resPath}${infoPlistStrPath}`, infos, err => {
          resolve();
          if (err) throw err;
          console.log(`>  “{ResourcesPath}/${infoPlistStrPath}” Created.`);
        });
      });
    }),
  );
};
```

#### Mac Installer DistributionのCertifaicateを作ってキーチェーンにインストール

Mac Installer DistributionのCertifaicateがまだ無い場合は作ってください。
キーチェーンアクセスの証明書アシスタントの認証局に証明書を要求を選んで、証明書を作ります。
注意すべき点は**鍵ペア情報を指定**することです。
![cert_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31696/dd2b327f-e836-f7e4-4ba6-d47292f05f26.png)
鍵のサイズとアルゴリズムは表示の通りで大丈夫でした。
![cert_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31696/c897e6d6-afe2-dd01-907d-481abc909c84.png)
鍵ペア情報が無いとelectron-packagerはキーチェーンアクセスからCertificateを探し出せないみたいです。

これをキーチェーンアクセスにインストールすると"3rd Party Mac Developer Installer: XXX (XXX)"みたいな名前になります。なぜ3rd Partyなのかわからないのですが、そういう名前になるとだけ憶えておいてください。

#### パッケージしましょう

ビルドしてプロジェクトディレクトリ配下に正しくdistが作成された前提で話を進めます。

pakage.jsonにパッケージコマンドを起動するための記述を追加します。

```json:package.json(抜粋)
{
  ...
  "scripts": {
    "start": "electron ./dist/main.js",
    ...
    "package:mac": "electron-builder --mac --universal"
  }
}
```

**package:mac**の中身がnpmによって起動されるパッケージの正体です。
アーキテクチャは**--x64**ではなく**--universal**です。

そしてターミナルから以下のコマンドを実行してください。

```shell
$ npm run package:mac
```

成功するとbuildディレクトリに以下のようなファイルが作成されます。

```
builder-debug.yml
builder-effective-config.yaml
mac-universal/Your-Electron.app
mas-universal/Your-Electron-1.0.0-universal.pkg
mas-universal/Your-Electron.app
Your-Electron-1.0.0-universal.dmg
Your-Electron-1.0.0-universal.dmg.blockmap
```

#### これだけではダメみたいです
上記のmas-universal/Your-Electron-1.0.0-universal.pkgをTransporterでアップロードすればAppStoreがバイナリを受け取って貰えそうに思うのですが、ダメだったようです。
どうもcodesignというプロセスを経ないとならないようで、以下のようなシェルスクリプトを作りました。

```shell:shells/codesign.sh
#!/bin/bash

# 必ずプロジェクトパスに行けるように
MY_DIR_NAME=`dirname $0`
SHELL_SCRIP_DIR=`cd $MY_DIR_NAME;pwd`
cd $SHELL_SCRIP_DIR
cd ..

APP="Your-Electron"
ARCH="mas-universal"

# *.appのパス
APP_PATH="build/$ARCH/$APP.app"
# framewarkパス
FRAMEWORKS_PATH="$APP_PATH/Contents/Frameworks"

# codesignするcertificate
APP_KEY="Apple Distribution: YOUR-MAS-COMPANY-NAME (MASTEAMID)"

# エンタイトルメント用のplistファイル
PARENT_PLIST="AppStore/parent.plist"
CHILD_PLIST="AppStore/child.plist"
LOGINHELPER_PLIST="AppStore/login_helper.plist"

codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Electron Framework"

codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libEGL.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libswiftshader_libEGL.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libvk_swiftshader.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libGLESv2.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libswiftshader_libGLESv2.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib"

codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Libraries/libEGL.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Libraries/libswiftshader_libEGL.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Libraries/libvk_swiftshader.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Libraries/libGLESv2.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Libraries/libswiftshader_libGLESv2.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Libraries/libffmpeg.dylib"

codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/Contents/MacOS/$APP Helper"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/"

codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/Contents/MacOS/$APP Login Helper"
codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/"
codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$FRAMEWORKS_PATH/$APP Helper (GPU).app/Contents/MacOS/$APP Helper (GPU)"
codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$FRAMEWORKS_PATH/$APP Helper (Plugin).app/Contents/MacOS/$APP Helper (Plugin)"
codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$FRAMEWORKS_PATH/$APP Helper (Renderer).app/Contents/MacOS/$APP Helper (Renderer)"

codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$APP_PATH/Contents/MacOS/$APP"

codesign -s "$APP_KEY" -f --entitlements "$PARENT_PLIST" "$APP_PATH"
```

アプリ本体だけでなく、色んなヘルパー類もcodesignしないとならないみたいです。結構たいへんですね。
さらにこれをproductbuildします。

```shell:shells/productbuild.sh
#!/bin/bash

# 必ずプロジェクトパスに行けるように
MY_DIR_NAME=`dirname $0`
SHELL_SCRIP_DIR=`cd $MY_DIR_NAME;pwd`
cd $SHELL_SCRIP_DIR
cd ..

APP="Your-Electron"
ARCH="mas-universal"

# *.appファイル
BUILD_DIR="build/$ARCH"
APP_NAME="$APP.app"
APP_PATH="$BUILD_DIR/$APP_NAME"
# *.pkgファイル
RESULT_PATH="$BUILD_DIR/$APP-mac_store.pkg"

# インストーラcertificate
INSTALLER_KEY="3rd Party Mac Developer Installer: YOUR-MAS-COMPANY-NAME (MASTEAMID)"

rm -f "$RESULT_PATH"
productbuild --component "$APP_PATH" /Applications --sign "$INSTALLER_KEY" "$RESULT_PATH"
```

上記2つのシェルスクリプトを順番に実行すると

```
./build/mas-universal/Your-Electron-mac_store.pkg
```

というファイルができます。
これをTransporterを使用してAppStoreにアップロードしてください。

#### あとはApp Store Connectで編集・審査

あとはApp Store Connectで必要な情報を編集し審査を請求するだけです。
頑張ってmacOSアプリをElectronで作成して公開してください。
もしうまく行かない部分があれば書き込みしてください。お役に立てるかどうかはわかりませんが、なるべくお手伝いできればと思っております。