Inno Setupでsigntool.exeを使ってコードサイニング証明をつける


Inno Setupとはフリーのカスタマイズ可能なインストーラー作成ツール

拙作のアタッシェケースでは、インストーラーとしてInno Setupを使用しています。フリーでありながら、さまざまなオプションによるカスタマイズが可能で、究極はPascalでスクリプトを書いて走らせることもできます。

その中で、できあがったインストーラーにコードサイニング証明書を付けるのは、バッチファイルなどで処理していますが、Inno Setup で作成したインストーラー内に含まれる「アンインストーラー」にも証明書を付けてやらねばなりません。


昔の Inno Setup では、インストーラーとして固められたバイナリから、無理くり抜き出してコードサイニング証明書を付加していたようですが、現在のバージョンではそのオプションがあり、容易に証明書を付けることができます。

バッチファイルからの signtool.exe は無理筋っぽくて、Inno Setup の IDE から登録するのが無難

ただし、Inno Setup で作成したインストーラー内の「アンインストーラー」に証明書を付けるには、Innno Setupの設定ファイル(*.iss)をバッチファイルから Inno Setup コンパイラ(ISCC.exe)に喰わせることで、証明書を付加することはできないようです。

そこも一箇所にまとめて自動化するには、Node.js のモジュールを入れれば、近いことができそうです。

node-innosetup-compiler
https://github.com/felicienfrancois/node-innosetup-compiler

おとなしくバッチファイルで処理する場合には、signtool.exe の引数込みのコマンドを Inno Setup の IDE 上から登録することで、処理することができるようになります。

IDE から signtool.exe の処理を追加する

登録するには、Inno Setup の IDE メニュー「Tools」→「Configure sign tools…」を選択します。

すると、Sign Tool を選択するためのウィンドウが出てきますので、「Add」を押して、Signtool.exe を登録していきましょう。

まず、登録する signtool.exe の内容を示す「変数名」を入力します。ここは、設定ファイル(*.iss)で必要になってきますが、分かりやすい適当な名前で良いでしょう。ここでは、仮に「MySignTool」という名前にしました。

名前を入力すると、続いてその内容を入力するウィンドウが表示されます。前のウィンドウと似てますので少し注意が必要です。

そこに、

$qC:\Program Files\Microsoft SDKs\Windows\v7.0A\bin\signtool.exe$q sign /v /a /n $qMitsuhiro Hibara$q /t http://timestamp.comodoca.com/authenticode $f


といった感じに、signtool.exe のコマンドライン実行形式を丸ごと入力してOKボタンを押します。ここに書き込まれる、signtool.exe の書式などの扱いについては後述します。

リストに、MySignTool=$qC:\Program Files\Microsoft....と表示されていると思います。

ここで、「OK」ボタンを押せば、signtool.exe のコマンドライン実行形式は、先ほど名前を付けた「MySignTool」変数に入り、Inno Setup の設定ファイル(*.iss)で使うことができます。

次に、設定ファイル(*.iss)にある、「Setup」セクションに、以下の二つを追加します。

issSignedUninstaller=yes
SignTool=MySignTool

これで設定は完了です。

ちなみに、上で例として使われている Inno Setup の設定ファイルは、GitHubで見ることができます。

https://github.com/hibara/AttacheCase3/blob/master/installer/AttacheCase.iss

Inno Setup で使う signtool.exe コマンドオプションの書式

さて、Inno Setup の変数として登録した signtool.exe ですが、コマンドオプションなどの書式詳細は以下を参考にすると良いでしょう。

SignTool.exe (署名ツール) – Microsoft
https://docs.microsoft.com/ja-jp/dotnet/framework/tools/signtool-exe

あと、書式内で$qや、$fといった記号が用いられていますが、こちらは、Innno Setup のパラメーターオプションです。

ちなみに、ここで使われている、$q は、クォートを示し、$f は、Innno Setup が与えるファイルパスなどが入ります。

$qC:\Program Files\Microsoft SDKs\Windows\v7.0A\bin\signtool.exe$q sign /v /a /n $qMitsuhiro Hibara$q /t http://timestamp.comodoca.com/authenticode $f

前述した signtool.exe のコマンドライン書式を振り返って観ましょう。コマンドオプション詳細については、以下のページにあります。

SignTool.exe (署名ツール) – Microsoft
https://docs.microsoft.com/ja-jp/dotnet/framework/tools/signtool-exe

ですが、ここで使われているオプションについては、補足しておきます。

  • /v コマンドライン処理いた歳に、コマンドが正常に実行したか、失敗したかにかかわらず、詳細出力と警告メッセージが表示されます。
  • /n コードサイニング証明書をつける人の名前(法人なら組織名を指定します。
    /n "Mitsuhiro Hibara"
  • /t タイム スタンプ サーバーの URL を指定します。これは、コードサイニング証明書を取得した場所によって指定がちがうため、入手先のヘルプページか何かを参考にされると良いでしょう。
  • /a 最適な署名証明書を自動的に選択します。もっとも有効期間が長い証明書が自動で選択されます。個人でアプリケーションを配布されている方などは、複数コードサイニング証明書を持っているとは思えませんし、一つで管理しているのなら開発環境のあるPC内にインストールして、このオプションを使うのが、もっとも簡単かと思います。

もしも複数のコードサイニング証明書を運用したいとき

もしも複数の証明書を運用したい場合は、Inno Setup の「Configure Sign Tools…」メニューから、複数の signtool.exe の書式を「変数」として登録できるので、ここで切り替えても良いでしょう。

なお、以下のように、直接、コードサイニング証明書ファイル(*.pfx)を指定してコードサイニングすることも可能です。

signtool.exe sign /f $qM:\cert\SectigoLimited.pfx$q /p $qpasswordpassword$q /n $qMitsuhiro Hibara$q /t http://timestamp.comodoca.com/authenticode $f

/f オプションで、pfxファイルへのパスを指定して、/pオプションで、その pfxファイルを作成した際に入力いたパスワード文字列を指定しても、アンインストーラーに署名をすることが可能になります。

Windows7 以降に対応する

拙作のアタッシェケースは、まだ XP, Vista などに対応しているため、タイムスタンプサーバーで使用されるダイジェストアルゴリズムは、セキュリティ的に脆弱性が指摘されている、SHA-1を使っておりますが、もし Wndiow7 以降のアプリケーションにコードサイニング証明書を付けるのなら、SHA-2(SHA-256)を使用する方が無難でしょう。

signtool.exe sign /a /fd SHA256 /v /tr http://timestamp.comodoca.com /td sha256 $f

以上です。


nuget-icon

.NET FrameworkでDLLファイル(国際化対応リソースを含む)をexeにマージする


前のバージョンまで、「アタッシェケース」ではインストールを行うと、以下のプログラムフォルダーには、本体と並んで、DLLファイルがいくつか並んでいました。

AttacheCase.exe
AtcSetup.exe
Microsoft.WindowsAPICodePack.dll
Microsoft.WindowsAPICodePack.Shell.dll
ja-JP\AttacheCase.resources.dll

このファイル構成は、あくまで開発者側の都合であって、ユーザーにとっては良く分からないでしょう。

実行ファイルを別の場所へ移動させて使いたいとき、このDLLファイルも同時に移動させなくてはいけないのか?と迷ったり、煩わしいと思ったりするかもしれません。

実際、付属DLLファイルによっては、実行ファイルのあるフォルダーに存在しないと、正常に動かないものもあり、ユーザーにとっては、ツールのポータビリティが低く、やや使いづらいのではないかと考えました。

一つの実行ファイルに、すべての付属ファイルをマージする

というわけで、章題。これを目標にしてみました。そこで考えられる方策をいつくかご紹介したいと思います。

ILMerge(ILRepack)

もっとも手軽で定番なのは、「ILMerge」でしょうか。

Microsoft謹製のツールです。リンク先にあるツールをダウンロードしてインストールすると、使えるようになります。いろいろなオプションがあって、それに従って設定すれば、指定の実行ファイルにDLLファイルをマージできるようになります。

コマンドの例としては、以下のような感じです。

"C:\Program Files\Microsoft\ILMerge\ILMerge" /log:ilmerge.log /ndebug /targetplatform:v4,"C:\Windows\Microsoft.NET\Framework\v4.0.30319" /out:AttacheCaseTemp.exe AttacheCase.exe Microsoft.WindowsAPICodePack.dll Microsoft.WindowsAPICodePack.Shell.dll ja-JP\AttacheCase.resources.dll

ところが、これですと、言語ファイルである、ja-JP\AttacheCase.resources.dllが残ってしまいます。どうやら、国際化対応リソースのDLLは「サテライトアセンブリ」といって、マージのときに除外されるみたいです。

ちなみに「ILMerge」は、Microsoftのプロプライエタリなツールですが、これをオープンソースにして、機能を拡張したものも存在します。「ILRepack」です。

これも、ILMergeと基本的に使い方は同じです。たとえば、以下のようにします。

"..\tools\ILRepack.exe" /log:ILRepack.log /ndebug /targetplatform:v4,"C:\Windows\Microsoft.NET\Framework\v4.0.30319" /out:"C:\Users\mhibara\Desktop\AttacheCase.exe" AttacheCase.exe Microsoft.WindowsAPICodePack.dll Microsoft.WindowsAPICodePack.Shell.dll ja-JP\AttacheCase.resources.dll

ただ、これも、肝心の言語ファイルがマージされません。

NuGet

では、より簡単で、すべてのDLLファイルを実行ファイル(exe)にマージできないか調べて見ると、Visual Studio 2012以降では標準で付属しているツールの「NuGet」で、あるパッケージ群をインストールすれば可能になるようです。

まず、NuGetとは何か? マイクロソフト公式のウェブサイトにも説明はありますが、以下のウィキペディアの説明の方が簡潔ですので、そちらを参照なさってください。

NuGet
https://ja.m.wikipedia.org/wiki/NuGet

NuGetは、基本的にVS全体にインストールするというよりも、プロジェクト毎にインストールするようになっています。ですので、以下の画面では、「アタッシェケース」のソリューション(その中に各プロジェクト)が開いている状態で、NuGetパッケージ管理画面を開こうとしています。

 

Fody

次に「参照」から「fody」を検索して、Fodyを見つけたら、NuGetからパッケージを必要とするプロジェクトにチェックを入れ、そこへ「インストール」します。

Fodyとは以下のサイトにも説明がある通り、「Extensible tool for weaving .net assemblies(.NET Frameworkにアセンブリを組み込むための拡張ツール)」です。

Fody
https://github.com/Fody/Fody/

ただ、これだけではまだ不完全です。さらに、次にあるFodyのアドインをインストールする必要があります。

Fody.Costura

再びNuGetで「Fody Costura」を検索してインストールします。

Costuraとは、プロジェクトで使うリソースをアプリケーション本体に埋め込むためのツールです。

Costura is an add-in for Fody
https://github.com/Fody/Costura/

しかし、インストールしただけでは正常に動作しません。その後に、設定ファイルを所定の場所に置かなくてはなりません。ファイル名は以下の通り、決まっています。

FodyWeavers.xml

というファイルを作り、プロジェクトファイルがあるディレクトリに配置しなくてはなりません。

そのXMLファイルの中身ですが、最低限の設定であれば、以下の内容だけでO.K.です。ちなみに、公式GitHubのウェブサイトを見れば、埋め込みを除外するDLLを指定することができたり、様々なオプションが用意されているので、カスタマイズする場合はそちらを参考になさってください。なお、XMLファイルの文字エンコーディングはUTF-8なのでご注意を。



  

これにより、
Microsoft.WindowsAPICodePack.dll
Microsoft.WindowsAPICodePack.Shell.dll
の二つが「AttacheCase.exe」埋め込まれて、以下のようにファイルが出力されるかと思います。

 

Resource.Embedder

しかし、これでも、まだ国際化対応リソース(言語ファイル)DLL、
ja-JP\AttacheCase.resources.dll が統合されておりません。

そこで、もう一つNuGetパッケージをインストールします。「Resource.Embedder」です。

Resource.Embedder
https://github.com/MarcStan/Resource.Embedder

インストールは、以下の通りです。

これにより、ビルドを行うと、すべてのDLLリソースが「AttacheCase.exe」に埋め込まれて、以下のように単一で出力されます。

 

ただし、.NET Framework 4.0 だと古いバージョンを使う

Fody パッケージ自体は、.NET Frameworkのバージョン依存関係はありませんが、Fodyのプラグインである Costura の最新版の方には、

.NET Framework 4.6
Fody (>= 3.2.6)

といった依存関係があります。

ですので、アタッシェケースは、一応WindowsXP上でも動作するように、.NET Frameworkは、“4.0” でビルドするという縛りを設けているため

Fody ver.2.5.0
Costura.Fody ver.1.6.2

と、あえて古いバージョンのパッケージをそれぞれ使っています。

以上です。

「@nifty光」ロゴ

光コラボレーションの勧誘電話からの契約でミスった話


先だって自宅直通電話に株式会社CLという会社から、「いまご加入の@niftyから乗り換えれば、通信量がお安くなります。」という営業電話がかかってきました。

ただ、僕はパソコン通信時代からNifty Serveを使っていたということもあって、メアドを変えたくないということと、だいぶ前に電力自由化されたことから@niftyでも「@niftyでんき」が始まり、その勧誘もあって、入会していたという経緯があります。ちょっとその段階では@niftyからの乗り換えたくはしたくありませんでした。

しかし、その営業電話では「いま現在ご加入の@niftyを変更することなく、メールアドレスも変わらず、毎月の通信料が安くなります」と勧めてくる。それじゃあ、変えてみようかな、と軽い気持ちで切り替えました。「ただ、後ほど届く資料を必ずご覧になってください」と、「それで契約内容誤解していたなどあれば初期契約解除(クーリングオフ)できます」と、強く念を押されたので「ん?」と思いつつも、その資料が届くのを待ちました。

けっきょく4,5日して届いた資料に細かく目を通して見たら、驚きました。まず、自分が「光コラボレーション(光コラボ)」が始まっているのを知らなかったこと。以下が公式、というか、回線業者(NTT)による光コラボ提供会社の紹介一覧です。かなり多くの企業がやっていることが分かります。

NTT東日本
NTT西日本(※西日本の方が若干詳しく載っています)

光コラボとは、一言で説明してしまえば、電力の自由化ならぬ、光回線の自由化みたいなものです。今までは、以下の図の左にあるとおり、回線事業者(NTT)とインターネット回線の使用を契約し、その上でインターネットプロバイダーをどこにするか、それぞれ決めることになっていました。なので、僕の場合は、「NTT東日本」+「@nify」ということになります。

上の右側の図は、光コラボの図です。僕はてっきり、そのときは左側の状態になっているのかと思っていました。

しかしよく調べてみると、実は何年か前に@niftyから直接、「@nifty光にしてみませんか?」という勧誘があり、実はそれが光コラボということを知らずに契約してしまっていました。

そのため、現状は以下右の図のようになっているということです。

そこへ新たな光コラボ事業者からの営業電話勧誘があり、光コラボのことなんて全然知らなかったこともあり、その状態で、勢いから、契約してしまったというわけです。「毎月たったの950円になります!」と言われれば、安い!と思うわけですよ。これまで毎月4,000円近く払っていたのですから。

しかし、届いた書類を見ると、契約サービス名が「CL接続」とある。この光コラボ事業者の株式会社CLの「サービス」のページを見ると、その他にも「CLひかり」とある。「ひかり」でもない「接続?」そこで「んんんっ?」となる。そしてもう一度、「CL接続」サービスの説明をよく見ると、、、

な、なんだってー!! これじゃあ、光コラボどころかプロバイダーを変更するだけじゃないですか。

とはいえ、たしかに営業担当の人が、「@niftyのサービスはそのままお使いいただけます」って言っていたのはウソじゃない。@niftyを解約せずに、かつプロバイダーをもう一個別のものを加入させようとしたのだから。つまり、意味の無いプロバイダーの追加契約。

しかも、僕はすでに「@nifty光」サービス(光コラボ)へ切り替えてあるので、プロバイダーを解約すると、違約金が既存の@niftyの方で発生します。

いわゆる電力自由化のように、電力供給事業者の契約を破棄しても、自動的に東京電力(既存の電力会社)に切り替わるということもなく、光コラボでは、前のサービスの契約を破棄する前提で新たに契約してね、ということらしい。

おいおい・・・そもそも最初に「@niftyのメアドを変えたくないので」と伝えたはずなんですけども・・・

ざわ、ざわ・・・よくよく株式会社CLのウェブサイトを見てみると、仮にもプロバイダー事業者なのに、SSL接続に対応していないっていうのも微妙に不安を煽ります。

これはヤバい・・・さっそく解約しなくては・・・

実はこの契約では月々の料金が格安になり、最大二ヶ月の利用料金無料特典まで付いている代わりに、契約期間中での解約違約金の高さがエグい。

37ヶ月(約3年)縛りの、
契約月から12ヶ月以内の解約: 22,500円
契約月から24ヶ月以内の解約: 15,000円
契約月から36ヶ月以内の解約: 7,500円

これについては、電話での口頭で説明を受けていたので、三年縛りでも毎月安くなるなら、と納得はしていましたが、契約内容を改めて見たら、内容は僕の想定外になっているし、これは余計にマズいぞとなって焦りました。

とはいえ、契約には、初期契約解除(クーリングオフ)の条項も入っていて、資料到着後8日間以内(本書記載の「ご契約日」から11日以内)に違約金無しの解約ができると書いてあったので、さっそくこの会社のサポート窓口に電話。

サポートにはなかなか繋がらなくて「混んでいますのでもうしばらく経ってからお掛け直しください」とメッセージが流れ、何度か先方から遮断されました。まさか・・・サポートは初めから繋がらなくて、契約解除できないんじゃないか・・・詐欺か・・・そうドキドキしていたとき、何度目かの電話で、なんとかサポートスタッフの方に繋がりました。

意外だったのは、繋がってすぐに「お客さま番号」を聞かれ、「どうして解約されたのか、よろしければ理由を」との質問のみで、あっさり解約することができました。これはかなり良心的でした。

今回はたまたま、まともな事業者だったので、解約できましたが、ネットで調べて見ると、あこぎな事業者もけっこういるそうですから、契約の際には十分気を付けましょう。電話での勧誘による契約には特に注意が必要です。けっこう互いの合意内容に齟齬があったりします。

光コラボレーション自体、わるいものではなくて、きちんと光コラボ事業者を選べば、かなりお得になります。docomoや、au、SoftBankなども光コラボ事業をやっていて、ケータイがどこかのキャリアで持っているのなら、さらにお安くなったりと、メリットも十分ありそうです。

まあ、勧誘を受ける前に、自分で光コラボ事業者を検討し、納得して選び、先に申し込むのがベターだと思いました。なお、「光コラボ」と検索すれば、山ほど紹介ページが出てくると思うので、そちらを参考にしながら自分にピッタリのプランを選べば良いでしょう。

iTunes Connect icon

[Electron][MacOS]Your application still accesses the following location(s):でリジェクト



追記:2017/06/18に大幅修正を加えました。

Electronで絶賛、開発中の「OutlineText」ですが、バージョンアップ版を審査に提出したところ、タイトルのようなメッセージとともにリジェクトが。

正確には、

2. 4 Performance: Hardware Compatibility (macOS)
Guideline 2.4.5(i) – Performance

Your application still accesses the following location(s):

‘/Applications/OutlineText.app/Contents/Resources/app/locales/en.json.tmp’

The majority of developers encountering this issue are opening files in Read/Write mode instead of Read-Only mode, in which case it should be changed to Read-Only.

これを初めてもらったとき、「そうか、たしかに自分で自分のappにアクセスしてしまったっらダメだな」と思って、OutlineText.app内のディレクトリにはロックをかけるような仕組みを作り直して、審査に再提出。

ところが、またリジェクト。理由は、判で押したように上記のメッセージで返ってきました。いろいろ試行錯誤した結果、4回連続でリジェクトを頂戴するという羽目に(審査チームももう少しヒントをくれても良いのに・・・)。

そこまでやって、ふと思い当たったのは、ひょっとすると、Entitlementsの指定が誤っているのではないか?ということで、先の記事中では、Electron でビルドするときは、シェルスクリプトでやっていて、以下のように設定しておりました。

Electornは、単体アプリではなくヘルパーアプリも一緒に動くので、codesignは本体同様にすべてに行ってあげないといけません。でないと、審査以前にアプリをアップロードするときに、アップローダーに怒られます。

この辺りは、「Electron アプリを Mac App Store に登録する手順」が参考になるでしょう。

<?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.files.user-selected.read-only</key>
    <true/>
    <key>com.apple.security.application-groups</key>
    <string>ABCD9EFGHI.org.hibara.outlinetext</string>
  </dict>
</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>
FRAMEWORKS_PATH="$APP_PATH/Contents/Frameworks"

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/libffmpeg.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libnode.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 "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper EH.app/Contents/MacOS/$APP Helper EH"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper EH.app/"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper NP.app/Contents/MacOS/$APP Helper NP"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper NP.app/"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$APP_PATH/Contents/MacOS/$APP"
codesign -s "$APP_KEY" -f --entitlements "$PARENT_PLIST" "$APP_PATH"

ここで、問題になっていたのは、parent.plist 9行目のEntitlementsに、com.apple.security.files.user-selected.read-write が指定されていること。

<?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.files.user-selected.read-only</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    <key>com.apple.security.application-groups</key>
    <string>ABCD9EFGHI.org.hibara.outlinetext</string>
  </dict>
</plist>

このせいでいくらい対策を講じても、アクセスは可能になっていたというわけかと納得。そこで、以下のようにcom.apple.security.files.user-selected.read-onlyだけの記述にして審査へ再提出したら、ようやく審査に通りました。

<?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.files.user-selected.read-only</key>
    <true/>
    <key>com.apple.security.application-groups</key>
    <string>ABCD9EFGHI.org.hibara.outlinetext</string>
  </dict>
</plist>

ところが、いったんは審査を通ったものの、今度は保存ダイアログボックスが出ないというバグが発生・・・(これは審査チームも見逃したようです)。

考えてもみれば、そりゃそうですよね。read-onlyなのですから、保存ダイアログが出ないのは当たり前です。

で、結局は以下のように、指定を元に戻して再提出しました。具体的には、Entitlementsの指定で、read-onlyを、read-writeに書き換えています。

<?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.files.user-selected.read-write</key>
    <true/>
    <key>com.apple.security.application-groups</key>
    <string>ABCD9EFGHI.org.hibara.outlinetext</string>
  </dict>
</plist>

これでまた一から振り出しに戻り、先のテンプレ文とリジェクトをいただくことに。。。

そこで業を煮やした僕は、審査チームに「アピール」を送信してみることにしました。

Writes in the location are restricted and be open for read-only already. Please tell me the detailed occurrence procedure.

画像も添付しました。

read-only

ちゃんと「読み取り専用」として開いているぞ、と。

すると、二日後に以下のようなメッセージが届きました。

Hello,

Thank you for your inquiry. To clarify, the app is opening files in Read/Write mode instead of Read-Only mode, in which case it should be changed to Read-Only.
The file being accessed is:
‘/Applications/OutlineText.app/Contents/Resources/app/locales/en.json.tmp’

これを読んで、ふと、思い当たる節がありました。ひょっとして、ファイルの読み書きのところで、意図せずread-writeで読み込んでいる箇所があるのではないか?と。

実際、ありました。

保存するとき(ファイルに書き込むとき)、前に保存したディレクトリが存在しているかどうかチェックをして、なければデフォルトは「書類」フォルダを指定するというコードにしていました。参考にしたのは、「Check synchronously if file/directory exists in Node.js – Stack Overflow」です。

let dirPath = config.get('InitialDirPath');
try {
  // Query the entry
  let stats = fs.lstatSync(dirPath);
  // Is it a directory?
  if (stats.isDirectory()) {
	// Yes it is
  }
}
catch (e) {
  dirPath = app.getPath('documents');
}

ひょっとすると、fs.lstatSyncが、read-writeでアクセスしに行っているのではないか?と思い(Node.jsの公式ページでは確認できなかったのですが)、Stack Overflowのページをもう一度確認してみると、回答が大幅に書き換えられていて、以下が推奨となっていました。

var fs = require('fs');
if (fs.existsSync(path)) {
    // Do something
}

そこで、この部分を上記で書き直して、再度Appleに提出。程なくして、「Ready for sale」の返答があり、ようやくバージョンアップ版がApp Storeに並ぶことになりました。

やれやれ・・・一件落着です。

ちなみに、Appleの審査チームのアピールですが、質問形式で送ると、即座にテンプレで「サポートか、フォーラムに投げてね」的な回答が返ってきますが、「再現の手順」を教えてくれ、といった具体的な質問に対しては、ヒントになるくらいの回答が得られました。アピールの仕方にも、少し工夫が必要ですね。

 

Electron-icon

Electronでメニューへ動的にチェックを入れる



ElectronでMacOSアプリ作っています。

当サイトで「Open source & Free software」などと、標榜しておきながら、MacOSの方で有料アプリとして作って公開しています。(→気になる方はコチラからどうぞ)。

言い訳がましいですが、Appleのディベロッパーアカウントは1万円/年ほどかかっており、フリーで公開中のWindowsアプリのコードサイニング証明書更新など6万円/年と、すでに出銭の方が多いのではないかという状況でして。どうか忖度、願います。

そんなことはさておき、表題の件。

意外とサンプルが見つからなかったんですよね。

アプリ起動時のメニュー生成は、サンプルもいくつかみつかり、比較的簡単です。

let template = [
    {
      label: i18n.__('View'),
      submenu: [
        {
          label: i18n.__('Encoding'),
          id: 'Encoding',
          submenu: [
            {
              label: 'UTF-8',
              type: 'checkbox',
              checked: true
            },
            {
              label: 'EUC-JP',
              type: 'checkbox',
              checked: false
            },
            {
              label: 'Shift_JIS',
              type: 'checkbox',
              checked: false
            }
          ]
        }
      }
    }

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);

JSON形式でテンプレートを作っておいて、それをアプリケーションメニューとしてセットするだけです。

ただ、これですと、セットした後にメニュー内容を変更するにはどうすれば良いのしょうか? たとえば、以下のような場合です。

sample-menu

チェックボックスの変更です。たとえば、上記の例でいえば、エンコーディングの種類の変更を行いたい(その選択したメニューにチェックを入れ直したい)場合です。

これはもう構築したメニュー全体から、変更したい該当のメニューを探し出すしかありません。そこで、前述のソースコードの7行目に注目してほしいのですが、メニューアイテムの属性値に「id」を追加しています。これを頼りに検索する関数をつくります。

function getSubMenuItem(subMenuItems, id) {
  if (subMenuItems) {
    for (let i = 0; i < subMenuItems.length; i++) {
      if (subMenuItems[i].id === id) {
        return subMenuItems[i];
      }
      else if (subMenuItems[i].submenu) {
        let found = getSubMenuItem(subMenuItems[i].submenu.items, id);
        if (found) return found;
      }
    }
  }
}

const menu = Menu.getApplicationMenu();
let encodingMenu = getSubMenuItem(menu.items, 'Encoding');

これにより、変数encodingMenuには、「エンコーディング」以下のサブメニューオブジェクトが入ります。

これも関数にまとめてしまいましょう。

function clickEncodingMenu(item){
  const menu = Menu.getApplicationMenu();
  let encodingMenu = getSubMenuItem(menu.items, 'Encoding');

  // 一度、全チェックを外す
  for(let i = 0; i < encodingMenu.submenu.items.length; i++){
    encodingMenu.submenu.items[i].checked = false;
  }
  // 選択したエンコーディングをチェック
  item.checked = true;
}

ここで注意したいのが、6行目のループで使われているsubmenu.itemsです。ここ、submenuだけにしがちなので、ご注意ください。

まとめるとこうなります。

function getSubMenuItem(subMenuItems, id) {
  if (subMenuItems) {
    for (let i = 0; i < subMenuItems.length; i++) {
      if (subMenuItems[i].id === id) {
        return subMenuItems[i];
      }
      else if (subMenuItems[i].submenu) {
        let found = getSubMenuItem(subMenuItems[i].submenu.items, id);
        if (found) return found;
      }
    }
  }
}

let template = [
{
  label: i18n.__('View'),
  submenu: [
	{
	  label: i18n.__('Encoding'),
	  id: 'Encoding',
	  submenu: [
		{
		  label: 'UTF-8',
		  type: 'checkbox',
		  checked: true,
		  click: function (item) {
			clickEncodingMenu(item);
		  }
		},
		{
		  label: 'EUC-JP',
		  type: 'checkbox',
		  checked: false,
		  click: function (item) {
			clickEncodingMenu(item);
		  }
		},
		{
		  label: 'Shift_JIS',
		  type: 'checkbox',
		  checked: false,
		  click: function (item) {
			clickEncodingMenu(item);
		  }
		}
	  ]
	}
  }
}
const menu = Menu.getApplicationMenu();
let encodingMenu = getSubMenuItem(menu.items, 'Encoding');

本来ならよくあるように、’id’だけでズバッと該当のメニューを指定して変更できるのがベターなのですが、現状ではその方法がないので、メニュー内を探索して特定するしかなさそうですね。

 

s