UnityでiOSのアプリを作っていて困ることの一つに、iOSが提供するシステムフレームワークへのリンクをプロジェクトに追加するのが超面倒くさいという問題が挙げられます。UnityがiOSアプリを書きだした後、手動でXcode上からシステムフレームワークを追加してもいいのですが、これはとんでもなく面倒です。というわけで、以前こちらの記事でRubyのxcodeprojモジュールを利用して自動的にシステムフレームワークを追加する方法をご紹介しました。
http://akisute.com/2012/09/unity-postprocessbuildplayer-weak.html
今回はそのPostprocessBuildPlayerをさらに機能拡充しましたのでご紹介いたします。主な機能として、
- システムフレームワークへのリンクをプロジェクトに追加する
- dylib, framework両方に対応
- required, optional両方に対応
- 空のinfo.plistをプロジェクトに追加する
- ja, enに対応
- Unity 3時代に空のinfo.plistを追加しないとiOSが提供するUIが英語で表示される問題が合ったため追加
- Unity 4以上であれば修正されているかも
- ヘッダサーチパスをライブラリサーチパスからコピーして自動設定する
- Unity 3時代にビルドにこける事があったので追加
- Unity 4以上であれば修正されているかも
- ローンチイメージを自動設定する
- 容量の関係で極限まで圧縮したjpgをpngの代わりに使いたいということで追加
- 現在のXcode 5/iOS7向けの環境ではjpgを利用したDefault.pngは全く考慮されていない用に見えるので、使わないほうが無難だと思います
- main.mmの書き換え
- ここではSystem.Net.Socket.SocketがSIGPIPEを飛ばしてアプリ全体をクラッシュさせてしまうことがある問題を回避するためにsignalを捕まえたりしています
- 変更規模が大きいならわざわざここでやるよりUnity側の/Assets/Plugins/iOSにmain.mmを置くほうが良いかと思いますが、ちょっと書き換えるだけなら有用です
- AppController.mmの書き換え
- 変更規模が大きいならわざわざここでやるよりUnity側の/Assets/Plugins/iOSにAppController.mmを置くほうが良いかと思いますが、ちょっと書き換えるだけなら有用です
インストール方法は、
- まずソースコードを取ってきてPostprocessBuildPlayerという名前でUnityプロジェクトの/Assets/Editor/以下に配置します。
- 実行にはRubyとバージョン0.4.xのxcodeprojモジュールが必要になりますので、インストールします。より大きいバージョンのxcodeprojでは動作未確認ですので、0.4.x系を指定することをおすすめします。
sudo gem install xcodeproj --version '~>0.4.0'
ソースコードはこちらになります。MITライセンスです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# | |
# PostprocessBuildPlayer version 2 | |
# Tested on Ruby 1.8.7, Gem 1.3.6, and xcodeproj 0.4.1 | |
# Created by akisute (http://akisute.com) | |
# Licensed under The MIT License: http://opensource.org/licenses/mit-license.php | |
# | |
require 'rubygems' | |
# least require version, doesn't work in 0.3.X or less | |
# possibly work on higher version than 0.4.X, but not tested | |
gem "xcodeproj", "~>0.4.0" | |
require 'xcodeproj' | |
require 'pathname' | |
require 'fileutils' | |
# | |
# Define utility functions | |
# | |
def proj_add_system_framework(proj, name) # workaround for 0.4.0 bug of Project#add_system_framework | |
path = "System/Library/Frameworks/#{name}.framework" | |
framework_ref = proj.frameworks_group.new_file(path) | |
framework_ref.name = "#{name}.framework" | |
framework_ref.source_tree = 'SDKROOT' | |
framework_ref | |
end | |
def proj_add_dylib(proj, name) # added new function, because there's no way we can add dylibs in 0.4.0 | |
path = "usr/lib/#{name}.dylib" | |
framework_ref = proj.frameworks_group.new_file(path) | |
framework_ref.name = "#{name}.dylib" | |
framework_ref.source_tree = 'SDKROOT' | |
framework_ref | |
end | |
def add_system_frameworks_to_project(proj, framework_names, option=:required) | |
proj.targets.each do |target| | |
# if target.name == "Unity-iPhone-simulator" then | |
# next | |
# end | |
framework_names.each { |framework_name| | |
framework = proj_add_system_framework(proj, framework_name) # workaround for 0.4.0 bug of Project#add_system_framework | |
phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXFrameworksBuildPhase) } | |
ref = phase.add_file_reference(framework) | |
if option == :optional then | |
ref.settings = { "ATTRIBUTES" => ["Weak"] } | |
end | |
# ref = Xcodeproj::Project::PBXBuildFile.new(proj, nil) | |
# ref.file_ref = framework | |
# if option == :optional then | |
# ref.settings = { "ATTRIBUTES" => ["Weak"] } | |
# end | |
# phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXFrameworksBuildPhase) } | |
# phase.files << ref | |
puts "Added system framework: " + framework_name + " as " + option.id2name | |
} | |
end | |
end | |
def add_dylibs_to_project(proj, dylib_names, option=:required) | |
proj.targets.each do |target| | |
# if target.name == "Unity-iPhone-simulator" then | |
# next | |
# end | |
dylib_names.each { |dylib_name| | |
dylib = proj_add_dylib(proj, dylib_name) | |
phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXFrameworksBuildPhase) } | |
ref = phase.add_file_reference(dylib) | |
if option == :optional then | |
ref.settings = { "ATTRIBUTES" => ["Weak"] } | |
end | |
puts "Added dylib: " + dylib_name + " as " + option.id2name | |
} | |
end | |
end | |
def new_variant_group_for_group(group, name, sub_group_path=nil) | |
variant_group = group.project.new(Xcodeproj::Project::PBXVariantGroup) | |
variant_group.name = name | |
target = group.find_subpath(sub_group_path, true) | |
target.children << variant_group | |
variant_group | |
end | |
def add_infoplist_strings_to_project(proj, buildpath) | |
system("mkdir #{buildpath}/en.lproj") | |
system("touch #{buildpath}/en.lproj/InfoPlist.strings") | |
system("mkdir #{buildpath}/ja.lproj") | |
system("touch #{buildpath}/ja.lproj/InfoPlist.strings") | |
proj.targets.each do |target| | |
# if target.name == "Unity-iPhone-simulator" then | |
# next | |
# end | |
infoplist_ref = new_variant_group_for_group(proj.main_group, "InfoPlist.strings") | |
en_ref = infoplist_ref.new_file("en.lproj/InfoPlist.strings") | |
en_ref.name = "en" | |
ja_ref = infoplist_ref.new_file("ja.lproj/InfoPlist.strings") | |
ja_ref.name = "ja" | |
buildfile_ref = Xcodeproj::Project::PBXBuildFile.new(proj, nil) | |
buildfile_ref.file_ref = infoplist_ref | |
phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXResourcesBuildPhase) } | |
phase.files << buildfile_ref | |
puts "Added InfoPlist.strings" | |
end | |
end | |
def set_header_search_paths(proj) | |
proj.targets.each do |target| | |
# if target.name == "Unity-iPhone-simulator" then | |
# next | |
# end | |
target.build_configuration_list.build_configurations.each do |build_configuration| | |
header_search_paths = build_configuration.build_settings["LIBRARY_SEARCH_PATHS"] | |
build_configuration.build_settings["HEADER_SEARCH_PATHS"] = header_search_paths | |
puts "Added HEADER_SEARCH_PATHS: #{header_search_paths} to build configuration #{build_configuration.name}" | |
end | |
end | |
end | |
def set_launch_image(proj, unity_project_root_path, buildpath, option) | |
imagepath = "" | |
ext = "" | |
case option | |
when :png | |
imagepath = unity_project_root_path + "/Assets/Images" | |
ext = option.id2name | |
when :jpg, :jpeg | |
imagepath = unity_project_root_path + "/Assets/Images/jpg" | |
ext = option.id2name | |
system("/usr/libexec/PlistBuddy -c 'Add :UILaunchImageFile string Default.#{ext}' #{buildpath}/Info.plist") | |
proj.targets.each do |target| | |
# if target.name == "Unity-iPhone-simulator" then | |
# next | |
# end | |
resource_build_phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXResourcesBuildPhase) } | |
resource_build_phase.files.each do |file| | |
re = Regexp.new("^(Default.*?)\.png") | |
if re === file.file_ref.name then | |
file.file_ref.name = file.file_ref.name.gsub(re, "\\1.#{ext}") | |
end | |
if re === file.file_ref.path then | |
file.file_ref.path = file.file_ref.path.gsub(re, "\\1.#{ext}") | |
file.file_ref.last_known_file_type = "image.jpeg" | |
end | |
end | |
delete_files = [] | |
sources_build_phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXSourcesBuildPhase) } | |
sources_build_phase.files.each do |file| | |
re = Regexp.new("^(Default.*?)\.png") | |
if re === file.file_ref.name then | |
file.file_ref.name = file.file_ref.name.gsub(re, "\\1.#{ext}") | |
end | |
if re === file.file_ref.path then | |
file.file_ref.path = file.file_ref.path.gsub(re, "\\1.#{ext}") | |
file.file_ref.last_known_file_type = "image.jpeg" | |
resource_build_phase.files << file | |
delete_files << file | |
end | |
end | |
delete_files.each do |file| | |
sources_build_phase.files.delete(file) | |
end | |
end | |
end | |
FileUtils.cp("#{imagepath}/Default.#{ext}", "#{buildpath}/Default.#{ext}") | |
FileUtils.cp("#{imagepath}/Default@2x.#{ext}", "#{buildpath}/Default@2x.#{ext}") | |
FileUtils.cp("#{imagepath}/Default-568h@2x.#{ext}", "#{buildpath}/Default-568h@2x.#{ext}") | |
FileUtils.cp("#{imagepath}/Default-Landscape.#{ext}", "#{buildpath}/Default-Landscape.#{ext}") | |
FileUtils.cp("#{imagepath}/Default-Landscape@2x.#{ext}", "#{buildpath}/Default-Landscape@2x.#{ext}") | |
FileUtils.cp("#{imagepath}/Default-Portrait.#{ext}", "#{buildpath}/Default-Portrait.#{ext}") | |
FileUtils.cp("#{imagepath}/Default-Portrait@2x.#{ext}", "#{buildpath}/Default-Portrait@2x.#{ext}") | |
puts "Added Default images (#{ext}) from #{imagepath}" | |
end | |
# | |
# Define build directory path | |
# -> Will be suppried as argv if run by Unity | |
# -> Else, assume unity_project_root_path/build is a build directory | |
# | |
unity_project_root_path = File.expand_path(File.dirname($0)) + "/../.." | |
buildpath = (ARGV[0]) ? ARGV[0] : unity_project_root_path + "/build" | |
puts "PostprocessBuildPlayer running on build directory: " + buildpath | |
projpath = buildpath + "/Unity-iPhone.xcodeproj" | |
proj = Xcodeproj::Project.new(projpath) | |
############################################################################### | |
# Example Usages | |
############################################################################### | |
# | |
# Add System frameworks and dynamic libralies to build | |
# | |
add_system_frameworks_to_project(proj, ["StoreKit", "Security", "CoreText", "MessageUI"], :required) | |
add_system_frameworks_to_project(proj, ["Twitter", "Social"], :optional) | |
add_dylibs_to_project(proj, ["libsqlite3"], :required) | |
# | |
# Set HEADER_SEARCH_PATHS | |
# | |
set_header_search_paths(proj) | |
# | |
# Add UIRequiredDeviceCapabilities to Info.plist | |
# | |
system("/usr/libexec/PlistBuddy -c 'Add :UIRequiredDeviceCapabilities: string gyroscope' #{buildpath}/Info.plist") | |
# | |
# Update lproj files | |
# | |
add_infoplist_strings_to_project(proj, buildpath) | |
# | |
# Set Launch Image (Default.png) | |
# | |
launch_image_ext = :jpg | |
set_launch_image(proj, unity_project_root_path, buildpath, launch_image_ext) | |
# | |
# Save xcodeproj | |
# | |
proj.save_as(projpath) | |
# | |
# Rewrite main.mm | |
# Add uncaught exception handler for better debugging | |
# Ignores SIGPIPE which rises when application enters background while System.Net.Socket.Socket is open (There's no other way to shut this) | |
# | |
f_main_mm = open(buildpath + "/Classes/main.mm", "r") | |
text_main_mm = f_main_mm.read | |
f_main_mm.close | |
f_main_mm = open(buildpath + "/Classes/main.mm", "w") | |
text_main_mm = text_main_mm.sub( | |
'int main(int argc, char *argv[])', | |
'void uncaughtExceptionHandler(NSException *exception); | |
void uncaughtExceptionHandler(NSException *exception) | |
{ | |
NSLog(@"%@", exception); | |
NSLog(@"Stack Trace: %@", [exception callStackSymbols]); | |
} | |
int main/*substituted by PostprocessBuildPlayer*/(int argc, char *argv[])' | |
); | |
text_main_mm = text_main_mm.sub( | |
'UnityParseCommandLine(argc, argv);', | |
'signal(SIGPIPE, SIG_IGN); | |
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler); | |
UnityParseCommandLine/*substituted by PostprocessBuildPlayer*/(argc, argv);' | |
); | |
f_main_mm.write(text_main_mm) | |
f_main_mm.close | |
puts "Updated: main.mm" | |
# | |
# Rewrite AppController.mm | |
# Fixes UIScrollView problems by rewriting run loop modes | |
# https://github.com/keijiro/unity-ios-textview | |
# | |
# Also fixes problems when you use jpg images as a launch image | |
# | |
f_app_controller_mm = open(buildpath + "/Classes/AppController.mm", "r") | |
text_app_controller_mm = f_app_controller_mm.read | |
f_app_controller_mm.close | |
f_app_controller_mm = open(buildpath + "/Classes/AppController.mm", "w") | |
text_app_controller_mm = text_app_controller_mm.sub( | |
'while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, kInputProcessingTime, TRUE) == kCFRunLoopRunHandledSource)', | |
'while (CFRunLoopRunInMode(CFStringRef(UITrackingRunLoopMode), kInputProcessingTime, YES) == kCFRunLoopRunHandledSource)' | |
); | |
text_app_controller_mm = text_app_controller_mm.sub( | |
'[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];', | |
'[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];' | |
); | |
text_app_controller_mm = text_app_controller_mm.sub( | |
'return [NSString stringWithFormat:@"Default%s%s", orientSuffix, szSuffix];', | |
'return [NSString stringWithFormat:@"Default%s%s.' + launch_image_ext.id2name + '", orientSuffix, szSuffix];' | |
); | |
f_app_controller_mm.write(text_app_controller_mm) | |
f_app_controller_mm.close | |
puts "Updated: AppController.mm" | |
puts "PostprocessBuildPlayer completed." |
余談になりますが、最近ではRubyのxcodeprojを使うのではなく、Pythonのmod-pbxprojを使う方法もあるみたいです。@Seasons氏はこちらの方法を使われているそうです。Pythonのがいい!という方はいかがでしょうか。