Unity3D build postprocessors
Fortunately, Unity gives us a powerful tool for this: Postprocessor scripts. These are scripts with a static function tagged with the PostProcessBuild attribute, and they run after creating the XCode project. This function receives a Build Target and our project’s path. Something like this:
1 2 3 4 5 6 7 8 9 10 | using UnityEditor.Callbacks; public class BeatDefenseBuildPostProcessor { [PostProcessBuild] public static void OnPostprocessBuild (UnityEditor.BuildTarget buildTarget, string path) { Debug.Log ("Running BeatDefense Postprocessor"); } } |
This script will only show a console log when Unity finishes creating the XCode project, so let’s get into more interesting possibilities. Let me use a real example of how I use this tool in BeatDefense.
Using Unity’s XCode API
We’ll be using the Unity3D XCode API for all of this. XCode’s projects are no more than plists, so we can use any tool that allows us to edit them, but this toolkit is simpler. This is how you open the project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class BeatDefenseBuildPostProcessor { private static PBXProject _project; private static string _path; private static string _projectPath; private static void OpenProject() { _projectPath = _path + "/Unity-iPhone.xcodeproj/project.pbxproj"; _project = new PBXProject (); _project.ReadFromFile (_projectPath); _target = _project.TargetGuidByName ("Unity-iPhone"); } private static void CloseProject() { File.WriteAllText (_projectPath, _project.WriteToString ()); } [PostProcessBuild] public static void OnPostprocessBuild (UnityEditor.BuildTarget buildTarget, string path) { _path = path; OpenProject(); CloseProject(); } } |
As we can see, opening a project is as easy as sending the path to our project.pbxproj. Once we have it, we get the target. Finally, when we’re done editing, we must write again the project on the original path. This is why we’re storing our project path in the class.
Adding a Framework
This is probably the most repeated task in an iOS project, and there’s two variants:
Internal Frameworks
Internal frameworks are those that are part of the iOS SDK. Here, we don’t need to worry about the path, because the SDK knows it already. We only have to link it, and it’s really easy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private static void AddFramework(string framework) { if(_project.HasFramework(framework)) return; _project.AddFrameworkToProject (_target, framework, false); } private static void AddFrameworks() { AddFramework("CoreData.framework"); AddFramework("MediaPlayer.framework"); AddFramework("Security.framework"); AddFramework("libxml2.2.dylib"); } [PostProcessBuild] public static void OnPostprocessBuild (UnityEditor.BuildTarget buildTarget, string path) { _path = path; OpenProject(); AddFrameworks(); CloseProject(); } |
As we can see, we only have to send the Framework’s name (with extension, so we can use things like libxml2.2.dylib) to our AddFramework method. Remember that, like almost everything else we’re going to do here, it must be placed between OpenProject and CloseProject.
External frameworks
These are a bit more difficult, because we need the physical file to link them. My personal way to deal with this is creating a folder at the same level than the Assets folder (not inside, because we don’t need to import them) and then make our script copy them to the right place and link them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | private static void CopyAndReplaceDirectory (string srcPath, string dstPath) { if (Directory.Exists (dstPath)) Directory.Delete (dstPath); if (File.Exists (dstPath)) File.Delete (dstPath); Directory.CreateDirectory (dstPath); foreach (var file in Directory.GetFiles(srcPath)) File.Copy (file, Path.Combine (dstPath, Path.GetFileName (file))); foreach (var dir in Directory.GetDirectories(srcPath)) CopyAndReplaceDirectory (dir, Path.Combine (dstPath, Path.GetFileName (dir))); } private static void AddFramework(string framework) { if(_project.HasFramework(framework)) return; _project.AddFrameworkToProject (_target, framework, false); } private static void AddExternalFramework(string framework) { var unityPath = "/../iOSFrameworks/" + framework; var fullUnityPath = Application.dataPath + unityPath; var frameworkPath = "Frameworks/" + framework; var fullFrameworkPath = Path.Combine(_path, frameworkPath); CopyAndReplaceDirectory (fullUnityPath, fullFrameworkPath); var frameworkFileGuid = _project.AddFile (frameworkPath, frameworkPath, PBXSourceTree.Source); _project.AddFileToBuild (_target, frameworkFileGuid); AddFramework(framework); } private static void AddFrameworks() { AddFramework("CoreData.framework"); AddFramework("MediaPlayer.framework"); AddFramework("Security.framework"); AddFramework("libxml2.2.dylib"); } [PostProcessBuild] public static void OnPostprocessBuild (UnityEditor.BuildTarget buildTarget, string path) { _path = path; OpenProject(); AddFrameworks(); CloseProject(); } |
The CopyAndReplaceDirectory recursively copies the content of the folder given through srcPath to dstPath, replacing duplicate files if necessary. AddExternalFramework copies the content of iOSFrameworks to the Frameworks folder of the XCode project. Next, we add a reference to the file in the project, because having it in the project folder is not enough. The file needs an identifier (GUID) to be able to link it.
Once all of this is done, the framework is recognized by the project and we can add it with AddFramework as if it was part of the SDK.
Adding GameKit
If we’re going to use GameKit functions in our project such as GameCenter to keep scores, we must add it to the required device capabilities. This is done by adding a value to an array inside info.plist. We can do this anywhere because is a separate file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private static void AddGameKitCapability() { string infoPlistPath = _path + "/Info.plist"; var plistParser = new PlistDocument (); plistParser.ReadFromFile (infoPlistPath); plistParser.root ["UIRequiredDeviceCapabilities"].AsArray ().AddString ("gamekit"); plistParser.WriteToFile (infoPlistPath); } [PostProcessBuild] public static void OnPostprocessBuild (UnityEditor.BuildTarget buildTarget, string path) { _path = path; AddGameKitCapability(); OpenProject(); CloseProject(); } |
Linker flags
This is another simple but important task. We can use this, for example, to add the -ObjC flag.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private static void SetBuildProperties() { _project.SetBuildProperty (_target, "FRAMEWORK_SEARCH_PATHS", "$(inherited)"); _project.AddBuildProperty (_target, "FRAMEWORK_SEARCH_PATHS", "$(PROJECT_DIR)/Frameworks"); _project.AddBuildProperty (_target, "OTHER_LDFLAGS", "-ObjC"); _project.AddBuildProperty (_target, "OTHER_LDFLAGS", "-fobjc-arc"); } [PostProcessBuild] public static void OnPostprocessBuild (UnityEditor.BuildTarget buildTarget, string path) { _path = path; OpenProject(); SetBuildProperties(); CloseProject(); } |
We also add the folder were we copy the frameworks to the search path, so the linker can find them. We’re also adding the -fobjc-arc flag, because the Facebook framework requires it.
Disabling ARC on a single file
Of course, we can’t have ARC enabled everywhere, because that would be too easy, and any file that manages memory on its own must be compiled without this flag. This, I admit, has a dirtier solution:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | private static void DisableArcOnFile(string guid) { _project.RemoveFileFromBuild(_target, guid); _project.AddFileToBuildWithFlags(_target, guid, "-fno-objc-arc"); } private static void DisableArcOnFileByProjectPath(string file) { var guid = _project.FindFileGuidByProjectPath(file); DisableArcOnFile(guid); } private static void DisableArcOnFileByRealPath(string file) { var guid = _project.FindFileGuidByRealPath(file); if(guid == null) { guid = _project.AddFile(file, file); } DisableArcOnFile(guid); } private static void DisableArcOnFiles() { DisableArcOnFileByProjectPath("Libraries/Plugins/iOS/GADUObjectCache.m"); DisableArcOnFileByProjectPath("Libraries/Plugins/iOS/GADUInterstitial.m"); DisableArcOnFileByProjectPath("Libraries/Plugins/iOS/GADURequest.m"); DisableArcOnFileByProjectPath("Libraries/Plugins/iOS/GADUInterface.m"); DisableArcOnFileByProjectPath("Libraries/Plugins/iOS/GADUBanner.m"); DisableArcOnFileByRealPath(Application.dataPath + "/Facebook/Editor/iOS/FbUnityInterface.mm"); } [PostProcessBuild] public static void OnPostprocessBuild (UnityEditor.BuildTarget buildTarget, string path) { _path = path; OpenProject(); DisableArcOnFiles(); CloseProject(); } |