iアプリのビルド作業をRakeで自動化する

くるくる3Dデモ docomo版で、とうとうiアプリにも手を出してしまいました。これから正式版を用意する上で、ProGuardを使った最適化もしておきたいところですが、オープンアプリの開発環境(Sun Java Wireless Toolkit for CLDC)と違って、iアプリの開発環境はProGuardなどのツールに対応していません。

ここで、前からブックマークしてあったブログ記事を参考にする時がやってきました。

重力ブログ - iアプリのサイズダウン作業をantで自動化する

なるほどなるほど……が、しかし。よく考えると、

自分の環境にはAntが導入されていない(使ったこともない)

それにAntのビルドファイルは読みにくい、書きにくい

どうせ非標準のツールが必要になるのなら、Antでなくても好きなものを使えばいいのでは

そして自分の環境にはすでにRakeが導入されている

これだ!

というわけで、やってみました。Rakeもそんなに詳しいわけではないので手こずりましたが、元の記事でやっているのと同等のことは大体できるようにしました。

さらに、せっかくRakeでやるのにそれだけでは面白くないので、少し改良しました。

  • jamファイルのテンプレートを作ったり、別途バッチファイルを用意したりする必要はありません。
  • プロジェクト名や、ProGuardでリネームしないクラス名を明示的に指定する必要はありません(自動的に取得します)。そのため、一度Rakefileを環境に合わせて調整すれば、他のプロジェクトにそのまま使い回せます。

RakefileにはRubyのコードがそのまま書けるので、やろうと思えばソースのプリプロセスももっと凝ったこともできるでしょう。

iアプリゲーム開発テキストブック―903i/703i対応には、リソースファイル群を1ファイルにまとめてjarの圧縮率を稼ぐ方法が載っていたりしますが、そういう作業も全部Rubyで書いてしまえば、かなり便利になると思います。

使い方:

  1. Ruby、Rake、ProGuard、7-Zipを導入しておく(もしかすると、Rubyは半角スペースを含まないディレクトリにインストールするほうが安全かもしれません)
  2. java,javacコマンドのあるディレクトリ(C:\Program Files\Java\jdk1.6.0_07\binなど)と、ruby,rakeコマンドのあるディレクトリ(C:\Program Files\ruby-1.8\binなど)にパスを通しておく
  3. 普通にiアプリ開発ツールでアプリを作成(元になるjamファイルができている必要あり)
  4. 以下のコードをRakefileという名前でiアプリプロジェクトのディレクトリに保存
  5. 環境に合わせてパスなどを書き換え
  6. コマンドラインでプロジェクトのディレクトリに移ってrakeと打てば、pkg/下にjamとjarができる

デフォルトでは全工程を実行しますが、Rakeのターゲットを指定すれば途中まで実行することもできます。

  • rake preprocess # プリプロセスまで
  • rake compile # コンパイルまで
  • rake obfuscate # ProGuardによる難読化/最適化まで
  • rake preverify # 事前検証まで

その他、

  • rake clean # 中間生成物を削除
  • rake clobber # すべての生成物を削除

です。

Javaソースの中で、//#ifdef DEBUG 〜 //#endif で囲まれた行はプリプロセスでコメントアウトされます。

なお、差分ビルドはしていません。毎回、全ソースファイルを対象とします。

######## iアプリ ビルド用Rakefile

require 'rake/clean'


### ↓↓↓環境に合わせて書き換える↓↓↓

# DoJaのターゲットプロファイル
DOJA_PROFILE = "DoJa-4.0"
# iアプリ開発ツールのインストール先
DOJA_DIR = "C:/iDKDoJa5.1"
# ProGuardのパス
PROGUARD_PATH = "C:/WTK2.5.1/bin/proguard.jar"
# 7-Zipのパス
SEVENZIP_PATH = "\"C:/Program Files/7-Zip/7z.exe\""

# コンパイル対象のソース
source_files = FileList["src/*.java"]
source_files.exclude("**/Test*.java")  # 除外したいファイルがあればここへ
# jarに含めるリソース
res_files = FileList["res/*"]

# jam、jarを生成するディレクトリ
PACKAGE_DIR = "pkg"  # 念のためbinを上書きしないようにしていますが、binを指定しても動きます。

### ↑↑↑環境に合わせて書き換える↑↑↑


DOJA_LIB_DIR = "#{DOJA_DIR}/lib/profile/#{DOJA_PROFILE}"
DOJA_CLASSPATH = "#{DOJA_LIB_DIR}/classes.zip;#{DOJA_LIB_DIR}/doja_classes.zip"
PREVERIFY_PATH = "#{DOJA_DIR}/bin/preverify.exe"

PREPROCESSED_DIR = "preprocessed"
COMPILED_DIR = "compiled"
OBFUSCATED_DIR = "obfuscated"
PREVERIFIED_DIR = "preverified"
PACKAGE_TMP_DIR = "pkg_tmp"

orig_jam = nil
jam_info = nil
new_jam_path = nil
new_jar_path = nil

CLEAN.include(PREPROCESSED_DIR, COMPILED_DIR, OBFUSCATED_DIR, PREVERIFIED_DIR, PACKAGE_TMP_DIR)
if PACKAGE_DIR != "bin"  # binディレクトリは削除対象にしない
  CLOBBER.include(PACKAGE_DIR)
end

directory PREPROCESSED_DIR
directory COMPILED_DIR
directory OBFUSCATED_DIR
directory PREVERIFIED_DIR
directory PACKAGE_TMP_DIR
directory PACKAGE_DIR

desc "準備 (内部用)"
task :prepare => [] do
  # オリジナルのjamファイルを読み込む
  # 最初に見つかったjamファイルを採用。普通は1個なので大丈夫なはず。
  orig_jam_path = FileList["bin/*.jam"][0]
  if orig_jam_path.nil?
    puts "jam file not found!"
    exit
  end
  orig_jam = IO.readlines(orig_jam_path)

  # jamファイルからHashを作る
  jam_info = Hash.new
  orig_jam.each do |line|
    /^(\S+) = (.*)$/ =~ line
    jam_info[Regexp.last_match(1)] = Regexp.last_match(2)
  end

  # これから作るjamとjarのパス
  new_jam_path = orig_jam_path.gsub(/bin/, PACKAGE_DIR)
  new_jar_path = new_jam_path.ext("jar")
end

desc "プリプロセス"
task :preprocess => [:prepare, PREPROCESSED_DIR] do
  rm Dir.glob("#{PREPROCESSED_DIR}/*")
  source_files.each do |path|
    source = IO.readlines(path)
    prepro_source = Array.new
    skip = false
### ↓↓↓環境に合わせて書き換える↓↓↓
    # この辺りで好きなようにソースを加工できます。
    source.each do |line|
      # //#ifdef DEBUG 〜 //#endif で囲まれた行をコメントアウトする
      if /^\s*\/\/\s*#ifdef\s+DEBUG/ =~ line
        skip = true
      elsif /^\s*\/\/\s*#endif/ =~ line
        skip = false
      else
        line.gsub!(/^/, "//") if skip
      end
        prepro_source.push(line)
    end
### ↑↑↑環境に合わせて書き換える↑↑↑
    prepro_path = path.gsub(/^src/, PREPROCESSED_DIR)
    File.open(prepro_path, "w") do |file|
      file.write(prepro_source)
    end
  end
end

desc "コンパイル"
task :compile => [:preprocess, COMPILED_DIR] do
  rm Dir.glob("#{COMPILED_DIR}/*")
  sh "javac -bootclasspath #{DOJA_CLASSPATH} -source 1.4 -target 1.4 -g:none -d #{COMPILED_DIR} #{PREPROCESSED_DIR}/*.java"
end

desc "難読化/最適化"
task :obfuscate => [:prepare, :compile, OBFUSCATED_DIR] do
  rm Dir.glob("#{OBFUSCATED_DIR}/*")
  sh "java -jar #{PROGUARD_PATH} -injars #{COMPILED_DIR} -outjars #{OBFUSCATED_DIR} -libraryjars #{DOJA_CLASSPATH} -keep public class #{jam_info['AppClass']}"
end

desc "事前検証"
task :preverify => [:obfuscate, PREVERIFIED_DIR] do
  rm Dir.glob("#{PREVERIFIED_DIR}/*")
  sh "#{PREVERIFY_PATH} -classpath #{DOJA_CLASSPATH} -d #{PREVERIFIED_DIR} #{OBFUSCATED_DIR}"
end

desc "jar作成"
task :jar => [:preverify, PACKAGE_TMP_DIR, PACKAGE_DIR] do
  rm Dir.glob("#{PACKAGE_TMP_DIR}/*")
  rm_f new_jar_path
  cp FileList["#{PREVERIFIED_DIR}/*"], PACKAGE_TMP_DIR, {:preserve => true}
  cp res_files, PACKAGE_TMP_DIR, {:preserve => true}
  cd PACKAGE_TMP_DIR do
    sh "#{SEVENZIP_PATH} a -tzip -mx=9 -mfb=128 ../#{new_jar_path} *"
  end
end

desc "jam作成"
task :jam => [:jar, PACKAGE_DIR] do
  # 現在時刻とjarファイルサイズを得る
  curr_time = Time.now.strftime("%a, %d %b %Y %H:%M:%S")
  jar_size = File.size(new_jar_path)

  # 新しくjamファイルを作る
  new_jam = Array.new
  orig_jam.each do |line|
    new_line = line.dup
    new_line.gsub!(/(LastModified = ).*$/, "\\1#{curr_time}")
    new_line.gsub!(/(AppSize = ).*$/, "\\1#{jar_size}")
    new_jam.push(new_line)
  end

  # jamファイルを書き出す
  File.open(new_jam_path, "w") do |file|
    file.write(new_jam)
  end
end

task :default => [:jam, :jar]