ad-hoc用IPAパッケージを再ビルドしないで修正してみた

業務的に同一のソースコードからBundle IdentifierやAppアイコン、スプラッシュイメージが異なるだけのIPAパッケージをいくつも作る必要があった。
今までは差分ファイルを切り替えるシェルスクリプトと自動ビルドさせるシェルスクリプトを組み合わせてパッケージの数だけ再ビルドを行っていた。
これがソースコードに変更がないのであれば既存のIPAパッケージを元に、再ビルド無しにパッケージを作ることが可能だというのでやってみた。

やりたいこと

既存のIPAパッケージを元に、

  • Appアイコン、スプラッシュイメージの差し替え
  • Appタイトルの変更(Sample1 -> Sample2)
  • Bundle Identifierの変更(com.myapp.sample2 -> com.myapp.sample2)
  • 別のプロビジョニングプロファイルに切り替える
  • 別の署名に切り替える
必要な物
  • Xcode
  • IPAパッケージ
  • codesignコマンド(Mac OS付属)
  • 別のプロビジョニングプロファイルのファイル(myprovision2.mobileprovision)
  • 別のCode Signing Identityの名前(iPhone Distribution: MyDistribution2)
  • 別のAppアイコンとスプラッシュイメージ
手順

1.適当なフォルダにIPAパッケージをunzipする
以下の様なフォルダ構造で解凍される

Payload
└Sample.app
  ├Info.plst
  ├Default.png
  └(略)
iTunesMetadata.plist

2.xcodeでPayload/Sample.app/Info.plistファイルを編集して保存
3.Payload/Sample.app内のAppアイコン、スプラッシュイメージを差し替える
4.myprovision2.mobileprovisionファイルをembedded.mobileprovisionという名前でPayload/Sample.app以下に上書きコピー
5.xcodeでiTunesMetadata.plistを編集してInfo.plistと合わせておく

6.ターミナルを起動して以下のコマンドを実行

export EMBEDDED_PROFILE_NAME=embedded.mobileprovision
export CODESIGN_ALLOCATE="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/codesign_allocate"
codesign -f -s -vv "iPhone Distribution: MyDistribution2" -i "com.myapp.sample2" ./Payload/Sample.app

環境変数EMBEDDED_PROFILE_NAMEとCODESIGN_ALLOCATEはcodesignが必要とする


7.PayloadフォルダとiTunesMetadata.plistをまとめてzip
8.zipしたファイルの拡張子ipaに変更


その他
  • Xcodeはv5.x使ってます
  • Xamarin.iOSでビルドしたパッケージでも可能
  • パッケージ内のInfo.plistはビルド時に付加情報が書き込まれてあるので、ビルドを通していないInfo.plistを上書きコピーなどして使ってはいけない。もし使ってしまった場合はインストールに失敗するパッケージになる(codesignコマンドでエラーはでない)。
  • Info.plistファイルは元々バイナリ形式になっているが、XML形式でも問題はおきない。
  • 差し替える画像(PNG)は最適化してなくても問題はおきない。
  • 言語リソース等も差し替え可能。

iOS7.1から企業内アプリ配布にSSLが必須となったので自己証明書でなんとかしてみた

突然社内へのiOSアプリ配信が動かなくなったと連絡を受けた。
原因はこれ
iOS7.1からplistファイルの配布にhttpsが必須になっていた。
しかし社内向けのしかもテスト用の配信システムににわざわざ予算を掛けてまでSSL証明書など買っていられないので自己証明書でなんとかならないかやってみた。

IISで作る自己証明書はiOSと互換性がない

配信サーバーがIISであることから、まずIISの自己証明書作成機能をつかってみた。opensslコマンドでpfx->pem->cerなど形式を変換しつつiOSに自己証明書をインストールしてみた。しかし相変わらずアプリの配信ができない。よくよく調べてみるとIISで作る自己証明書はiOSと互換性がないので使えないとのことだった。

MacのopensslでCAから作ってみる

結局ネットでみつけたここのTip #5、自己CAから自己証明書を発行して~の手順でうまくいった。
Macのopensslコマンドで各種証明書を生成。それらをIISiOSデバイスに取り込んだ。

手順

1.自己CAのための秘密鍵を生成

openssl genrsa -out myserverCA.key 2048


2.自己CA証明書の生成(有効期限730日)

openssl req -x509 -new -key myserverCA.key -out myserverCA.cer -days 730 -subj /CN="Myserver Custom CA"

できあがるmyserverCA.cerが自己CA証明書になる。


3.自己証明書のための秘密鍵を生成

openssl genrsa -out myservercert1.key 2048


4.自己証明書を生成するためのリクエストを生成

openssl req -new -out myservercert1.req -key myservercert1.key -subj /CN=myserver.example.com

元々http://myserver.example.com/iOShogehoge...というURLだったのでCNはmyserver.example.comとなる。


5.自己証明書を生成するためのリクエストを承認して自己証明書を生成(有効期限365日)

openssl x509 -req -in myservercert1.req -out myservercert1.cer -CAkey myserverCA.key -CA myserverCA.cer -days 365 -CAcreateserial -CAserial serial

生成されたmyservercert1.cerが自己証明書になる。


6.IISで読み込むためにcer形式をpfx形式に変換

openssl pkcs12 -export -out myserver.selfsigned.pfx -inkey myservercert1.key -in myservercert1.cer

7.myserverCA.cerとmyserver.selfsigned.pfxをIISのサーバーにコピー

8.myserverCA.cerをダブルクリックで証明書ストアにインポート

9.IIS管理コンソールのツリーからサーバー名を選択してサーバー証明書ビューからmyserver.selfsigned.pfxをインポート
インポートした証明書は名前欄が空白になってしまった。生成時のオプション不足?

10.IIS管理コンソールのツリーからWebサイトを選択してバインドメニューからhttpsを追加
この時先程インポートした証明書を使うようにする。

11.iPhone構成ユーティリティでmyserverCA.cerを含んだ構成プロファイルを生成し署名ありでエクスポート
ファイル名はmyserver-ssl.mobileconfigにした。

12.エクスポートしたmyserver-ssl.mobileconfigをIISにコピーしてhttpでアクセスできるようにハイパーリンクなどを追加する

13.IISMIMEタイプに拡張子".mobileconfig"、種類"application/x-apple-aspen-config"を追加

14.plistファイルへのリンクのhttpをhttpsに変更する

15.デバイスのSafariからmyserver-ssl.mobileconfigへのリンクを開いてデバイスに自己CA証明書をインストール
この時証明されていない旨の表示がされるがインストールはできる。



これでiOS7.1でも警告無くアプリがインストール出来るようになった。
しかしこの方法にはデメリットがあって問題の無かったiOS7.0.x以前でも自己証明書のインストールが必要になってしまった。javascriptでごにょごにょすれば回避できるのだろうが面倒くさいのそこまではしなかった。

ステータスバーのバッテリーアイコン付近の色情報を取得してみた

ステータスバーのバッテリーアイコン付近の色情報(RGBA)を取得する必要があって、そのときに書いたコード。
処理的にはスクリーンキャプチャしたCGImageから対象の領域を1pxにトリミングして、そこからRGBA情報を取得するというもの。

using System;
using System.Drawing;
using MonoTouch.UIKit;
using MonoTouch.CoreGraphics;



CGImage screenImage;
CGImage captureImage;

screenImage = CGImage.ScreenImage;
float width = screenImage.Width;
float height = screenImage.Height;

switch (UIDevice.CurrentDevice.Orientation)
{
  case UIDeviceOrientation.LandscapeLeft:
    captureImage = screenImage.WithImageInRect(new RectangleF(width - 4, height - 4, 1, 1));
    break;
  case UIDeviceOrientation.PortraitUpsideDown:
    captureImage = screenImage.WithImageInRect(new RectangleF(4, height - 4, 1, 1));
    break;
  case UIDeviceOrientation.LandscapeRight:
    captureImage = screenImage.WithImageInRect(new RectangleF(4, 4, 1, 1));
    break;
  default:
    captureImage = screenImage.WithImageInRect(new RectangleF(width - 4, 4, 1, 1));
    break;
}

CGDataProvider dataProv = captureImage.DataProvider;

byte r;
byte g;
byte b;
byte a;
unsafe
{
  IntPtr intPtr = dataProv.CopyData().Bytes;
  byte* byteData = (byte*)intPtr;
  r = byteData[0];
  g = byteData[1];
  b = byteData[2];
  a = byteData[3];
  Console.WriteLine("r={0}, b={1}, b={2}, a={3}", r, g, b, a);
}

画像データのビットマップ情報に直接アクセスできるメソッドやプロパティはないようで、unsafeなコードでポインタ上から直接RGBAの情報にアクセスしなければならなかった。

Comparer<object>.Default.Compareメソッドでbyte配列の比較ができない

以下のコードが.NETでは動作するがXamarin.iOSでは動作しない。

byte[] v1 = new byte[]{0,1};

int ret = Comparer<object>.Default.Compare(v1, v1);  // <-ここで例外。メッセージは"does not implement right interface"

バグ?仕様?

追記 2014/02/12 10:21

サンプルコードが間違っていたので修正

追記 2014/02/13 10:17

最新のMono Frameworkのソースコードは修正されていて例外にはならない模様。しかしながらこの修正がいつXamarin.iOSに取り込まれるのかは不明。

DispatchQueue内でNSRunLoop.Current.RunUntilを呼び出すコードが動かなくなってしまった その2

前回の記事の動かなくなったコードをObjective-Cで記述してみた。Xcodeはv5.0.2を使用。

bool alertFinished;

- (void)hoge1:(id)sender {
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    dispatch_sync(dispatch_get_main_queue(), ^{
      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Title"
      message:@"Test"
      delegate:self
      cancelButtonTitle:@"Cancel"
      otherButtonTitles:@"OK", nil];

      [alert show];

      alertFinished = NO;
      while (alertFinished == NO) {
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5f]];
      }
    });
  });
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
  alertFinished = YES;
}

結果、嫌な予感はしていたがXamarin.iOSと同じ症状に。
仕様が変わったんだろうか。原因は分かったけど対処方法が全く不明。困った。


追記 2014/02/10 18:19
ひょっとしてここに答えがあるでは。

DispatchQueue内でNSRunLoop.Current.RunUntilを呼び出すコードが動かなくなってしまった

元々は本家Objective-Cでも使われているテクニックで、非同期処理を同期処理にするためにNSRunLoop.Current.RunUntilを使うテクニックがある。
これをDispatchQueueからDispatchAsync経由で呼び出している箇所があるのだが、

DispatchQueue.GetGlobalQueue(DispatchQueuePriority.Low).DispatchAsync(delegate {
  DispatchQueue.MainQueue.DispatchAsync(delegate () {
    UIAlertView alert = new UIAlertView();

    bool dismiss = false;
    alert.Message = "TEST";
    alert.AddButton("OK");
    alert.Dismissed += delegate {
      dismiss = true;
    };
    alert.Show();
    while (!dismiss) {
      NSRunLoop.Current.RunUntil(NSDate.FromTimeIntervalSinceNow(0.5));
    }
  });
});

※本当はもっと複雑なのだが省略するとこのようなコードになる。

最近、このコードがおかしな挙動を起こすようになってしまった。
具体的にはUIAlertViewは表示されるもののタップ操作にまったく反応しない。なので画面上の操作が全体的に無視されているような状態になる。
デバッグでNSRunLoop.Current.RunUntilの所でBreakできるのでいちおうUIスレッドは固まったりしていない模様。

原因は不明。以前に比べるとXamarin.iOSXcodeもバージョンアップしているのでどっちに原因があるのかもよくわからない。
そもそも動作していたのが奇跡だったのだろうか。


追記 2014/02/12 12:32
解決出来た。そして"動かなくなった"のではなかった。"そもそも動いていなかった"の間違いだった。XcodeとXamarin.iOSのバージョンはまったく関係がなかった。

mtouchコマンドがインターネットアクセスを要求してくる

Xamarin.iOSのビルドをシェルスクリプトで定期的に行っているのだが、ある時から突然動作しなくなってしまった。
シェルスクリプトといっても単純でXamarin.iOSのmdtoolコマンドにソリューションを読み込ませてビルドさせているだけのもの。
状況としてはこのmdtoolのプロセスが標準エラーも標準出力もなにも出力しなくなり、かといってゾンビになっているわけでもないよくわからない状態で停止してしまうというもの。
調べてみるとmdtoolがWEBアクセスをしていて、しかもこの接続が何度も再試行されていた。

mdtool    32999 xxxxx   13u  IPv4 0x0000000000000000      0t0  TCP xxxxx.xxx.xxx:52983->store.xamarin.com:https (SYN_SENT)

使用していたXamarin.iOSがv7.0.5.2(Beta)だったのでリリース版v7.0.5.2に入れ直してみたところ止まることはなくなったものの、ビルド自体が非常に遅くなってしまった。相変わらずWEBアクセスはしている(今度はmtouchコマンド)ので数回の再試行で諦めてくれるもののタイムアウトするまで処理が止まってしまうのだろうか。

該当PCはインターネットアクセスするためにプロキシが必要だが、業務上インターネットアクセスは禁止されているのでプロキシは無効になっている。とりあえずプロキシを有効にすることで問題が回避できた。

以前からこうだったのかは不明。
インターネットアクセスできなければビルドできないなんて作りが悪すぎると思うのだが。