Skip to content

Instantly share code, notes, and snippets.

@akisute
Created November 27, 2013 04:59
Show Gist options
  • Save akisute/7670871 to your computer and use it in GitHub Desktop.
Save akisute/7670871 to your computer and use it in GitHub Desktop.
PostprocessBuildPlayer for Unity, Written in Ruby.
#!/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."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment