CMakeでiOS用のframeworkを作ってみる

先日カッコいいと書いたBlitzを、頑張ってiOS用にビルドしたという話です。

元々Blitzに限らずC/C++で書かれたライブラリがiOSに対応している事はまれで、あーiOSに対応してたら使いたいのになーというシーンはちょくちょくあります。なんですけど、.frameworkを作ろうとすると.xcodeprojを作ってどうのこうのという話になりがちで、普段Autotoolsでビルドしているソースツリーに特定のしかもプロプリエタリなIDEのプロジェクトファイルを追加するとか、醜すぎて誰もやりたくないわけです。

ところが最近OpenCVがiOS用の.frameworkのアーカイブを別途配布しているのを知りまして、どうやってビルドしてんのかなと見てみると、CMakeで一旦xcodeprojを吐いてから、xcodebuildという流れになっていて、それをPythonスクリプトで纏めて、ワンコマンドで.frameworkが構築出来るようになっていました。

まぁiOS用のCMakeLists.txtは必要だし、結局xcodebuild叩いてるやんとかはあるんですけど、プロジェクトファイルを管理せずに済んでいるので、ソースの構成が変わるたびにプロジェクトファイルを編集するという残念な作業からは解放されているので、なるほどなと思いました。

というわけで、真似してBlitzのソースツリーから.frameworkを作ってみようという話ですね。

で、とりあえず書いてみたシンプルなCMakeLists.txtが以下です。ビルドしてみて気づいたんですけど、Blitzのソースコードはglobals.cpp以外は全部ヘッダとして扱われていて、array.ccとかを単体でビルドしようとするとエラーになります。だからビルドも超速いし、ビルドファイルも極端にシンプルで済みます。Blitzマジで凄いですね。この先C++書くときは真っ先に参考にしたいなと思います。

cmake_minimum_required(VERSION 2.8)
project(blitz)

include_directories("${PROJECT_SOURCE_DIR}")

# variables
set(ARCH "i386" CACHE STRING "Target Architecture")
set(PLATFORM "iPhoneSimulator" CACHE STRING "Target Platform")

# path
set(DEV_ROOT "/Applications/Xcode.app/Contents/Developer")
file(GLOB IOS_SDKS "${DEV_ROOT}/Platforms/${PLATFORM}.platform/Developer/SDKs/*")
list(SORT IOS_SDKS)
list(REVERSE IOS_SDKS)
list(GET IOS_SDKS 0 SDK_ROOT)
set(CMAKE_C_COMPILER "${DEV_ROOT}/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang")
set(CMAKE_CXX_COMPILER "${DEV_ROOT}/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++")
set(CMAKE_AR "${DEV_ROOT}/Toolchains/XcodeDefault.xctoolchain/usr/bin/ar")
set(CMAKE_RANLIB "${DEV_ROOT}/Toolchains/XcodeDefault.xctoolchain/usr/bin/ranlib")

set(CMAKE_CXX_FLAGS "-DBZ_HAVE_NAMESPACES -arch ${ARCH} -isysroot ${SDK_ROOT}")
add_library(blitz STATIC src/globals.cpp)

このCMakeLists.txtを書いていて気づいたのですけど、Xcode.app内のコンパイラ指定するんやったら別に.xcodeproj吐かなくても良くね?と思いMakefileで試してみたら普通にビルド出来ました。以下のような感じです。

$ mkdir build
$ cd build
$ cmake .. -DARCH=armv7 -DPLATFORM=iPhoneOS
$ make
$ lipo -info libblitz.a
input file libblitz.a is not a fat file
Non-fat file: libblitz.a is architecture: armv7

さて、.framworkを作るのにはシミュレータ用にi386、実機用にarmv7でそれぞれビルドしたライブラリをlipoで連結する必要があります。
その辺りの処理と.frameworkのファイル構成を作るのをPythonスクリプトで纏めてみたのが以下です。かなりOpenCVで使われてるスクリプトをパクってます。

# -*- coding: utf-8 -*-

import os
import sys
import shutil
import glob

def build(srcroot, buildroot, target, arch):
    builddir = os.path.join(buildroot, target + '-' + arch)
    if os.path.isdir(builddir):
        shutil.rmtree(builddir)
        
    os.makedirs(builddir)
    currdir = os.getcwd()
    os.chdir(builddir)
    cmakeargs = ("-DARCH=%s " +
                 "-DPLATFORM=%s ") % (arch, target)
    os.system("cmake %s %s" % (cmakeargs, srcroot))
    os.system("make VERBOSE=1")
    os.chdir(currdir)
    

def build_framework(srcroot, dstroot):
    targets = ["iPhoneOS", "iPhoneOS", "iPhoneSimulator"]
    archs = ["armv7", "armv7s", "i386"]
    for i in range(len(targets)):
        build(srcroot, os.path.join(dstroot, "build"), targets[i], archs[i])
    put_framework_together(srcroot, dstroot)


def put_framework_together(srcroot, dstroot):
    targetlist = glob.glob(os.path.join(dstroot, "build", "*"))
    targetlist = [os.path.basename(t) for t in targetlist]

    # set the current dir to the dst root
    currdir = os.getcwd()
    framework_dir = dstroot + "/blitz.framework"
    if os.path.isdir(framework_dir):
        shutil.rmtree(framework_dir)
    os.makedirs(framework_dir)
    os.chdir(framework_dir)

    dstdir = "Versions/A"
    os.makedirs(dstdir + "/Resources")

    # copy headers
    shutil.copytree("../../blitz", dstdir + "/Headers")

    # make universal lib
    wlist = " ".join(["../build/" + t + "/libblitz.a" for t in targetlist])
    os.system("lipo -create " + wlist + " -o " + dstdir + "/blitz")

    # make symbolic links
    os.symlink("A", "Versions/Current")
    os.symlink("Versions/Current/Headers", "Headers")
    os.symlink("Versions/Current/Resources", "Resources")
    os.symlink("Versions/Current/blitz", "blitz")


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print "Usage:\n\t./build_framework.py <outputdir>\n\n"
        sys.exit(0)

    build_framework(os.path.abspath(os.path.dirname(sys.argv[0])),
                    os.path.abspath(sys.argv[1]))

このスクリプトを以下のようにして叩くと、output_dir内に.frameworkができます。

$ python build_framework.py output_dir
$ ls output_dir
blitz.framework build

PythonスクリプトもCMakeLists.txtも、一般化はしてないですが、プロジェクト毎にそう大きく変化するわけではないので、何かC/C++なライブラリをiOSで使いたくなった場合は、この方法で上手くいきそうな気がします。CMakeなかなか良いです。

Leave a Reply

Your email address will not be published. Required fields are marked *