forked from defagos/make-fmwk
-
Notifications
You must be signed in to change notification settings - Fork 0
allright/make-fmwk
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
************************************************************ * Shell script for creating Objective-C frameworks for iOS * ************************************************************ Written by Samuel Défago, inspired by Pete Goodliffe's article published on accu.org: http://accu.org/index.php/articles/1594 1) Introduction ------------ Xcode offers a framework creation project template for MacOS applications, but no such template is provided for iOS. One of the reasons is probably that a framework is an NSBundle, of which no other instance than the main bundle can exist on iOS. This stems from the fact that an NSBundle must contain executable code, otherwise it cannot be loaded (and therefore its resources cannot be accessed). Since iOS applications run in sandboxes, they are not allowed to use shared libraries, and therefore bundles cannot be loaded. Thus, for applications running on iOS, there is no way to load something like a framework (in the Xcode sense). No corresponding template is therefore provided. But still one should have a way of reusing library code. And in fact there is one, since Xcode provides a static library project template, with which static library files can be created. But working with them has some drawbacks: - you can only link with one .a at once. How do you manage multiple architectures each with its .a file? - header files for the library must be provided separately and included into the client project - resources must also be provided separately Usually, to avoid these drawbacks, projects directly import the source code of a library into their own source code (either as a link, but often files are directly cloned as well). But this does not work well either: - there is a strong likelihood that the programmer is tempted to change the library source code directly within her project - the number of source files to be compiled increases. Often, programmers just delete those source files they are not interested in, but this is not really convenient (moreover, when the library is updated, the set of source code files required may change) - frameworks whose source code is private cannot be used this way Though there is no way to build a framework around a static library using an Xcode template, it is still possible to package binaries, headers and resources for easier reuse. This is just what make-fmwk is made for. 2) How the script works and why it works that way ---------------------------------------------- When a standard .framework directory (created using the Xcode template for MacOS) is added to Xcode, two things happen: - Xcode looks for a dynamic library file located at the root of this directory, and bearing the same name as the .framework - Xcode also looks for a "Headers" directory containing the headers defining the framework public interface. These can then be imported by clients using #import <framework_name/header_name.h> More precisely, the structure of a MacOS framework is made of symbolic links and directories to handle various versions of a library within the same .framework directory. Refer to the MacOS framework programming guide for more information. Frameworks satisfying these requirements can be added to an Xcode project with a single click. Such .frameworks can also embbed resources since the .framework (whether it is deployed system-wide or copied for private use in the application bundle) is a true NSBundle. The binary file located at the root of the .framework directory does not need to be diretctly executable, though. It can also be a universal binary file, created by the lipo command which brings together binaries compiled for different architectures. The linker then just figures out which .a it needs when a project is compiled, and extracts it from the universal binary file. Therefore, it is possible to create "fake" frameworks wrapping a static library. Though these frameworks are not frameworks in the Xcode sense, Xcode will happily deal with them and discover their content. Note that since static frameworks are not true frameworks (wrapping dynamic libraries in the MacOS sense), we do not need to create the whole directory structure and symbolic links needed to support different versions. Only one version will always be available, creating such a structure would be overkill. Based on this knowledge, the make-fwmk script creates a "static" .framework (i.e. containing a static library) with the internal structure expected by Xcode. Such .frameworks could then be added using a simple drag and drop onto an Xcode project. But this does not deal with resources: As said above, static frameworks cannot be NSBundles, therefore they cannot embed resources (even if we add them to a copy file target task, they will be copied into the application bundle but never loaded at runtime; resources packed within them will never be accessible). To solve this problem, the .framework containing the static library is itself embedded into a .staticframework directory, which contains a directory for resources (and maybe other useful files we might need). This is this .staticframework file which is then added to an Xcode project (read further since adding a .staticframework file requires to be careful). The .staticframework being added to the project directly, the files it contains will be copied at the root level of the final application bundle when the application is assembled. In an ideal world we would have created a directory for each framework resource files to reside in (so that resources belonging to different libraries do not overlap), but this does not work well because some methods (e.g. [UIImage imageWithName:] or [UIViewController initWithNibName:bundle:] can only look at the root level of a bundle. Having all resources merged in the same bundle root directory means that we must strive to avoid name conflicts. A reasonable way to minimize the probability of conflicts is to prefix each resource of a library with the library name (and e.g. and underscore). The script will therefore display warnings if resources do not meet this requirement when a .staticframework is assembled. The last issue to discuss is how .staticframeworks must be added to Xcode. Simply dragging and dropping a .staticframework into a project works, but if we want the library to remain outside the project using it (which should be the case since both projects are independent), we must add a reference to it, not copy its files. This means that the path which will be stored into the .pbxproj will depend on the machine on which the framework was added to the project. This becomes a nightmare when several people (and maybe a continuous integration tool) use the same project. Several solutions exist: - store the reference to the .staticframework using a path relative to the project, and apply a convention like "all computers should have libraries two levels higher than projects in the directory hierarchy", so that relative paths are always correct - after getting a project, apply a tool to fix all .staticframework paths in the .pbxproj - be smarter :-) The first solution is ugly and dfficult to maintain. The second solution highly depends on the .pbxproj format and might break as it is changed. So we have no choice but to be smart. The goal is to only store paths relative to the project in the .pbxproj. But we cannot apply a convention, so those paths must point somewhere within the project directory itself. We cannot copy the framework files into the project directory, the solution is therefore to store symbolic links instead. These symbolic links are not committed to the source code repository and are generated using a script (link-fmwk) just after a project has been checked out on a computer. The script must only know for which frameworks it must generate symbolic links and where the frameworks are located on this specific machine. One last important remark: In your projects, always avoid absolute paths (except if pointing at system directories). Use the cmd-I shortcut when a directory is selected in Xcode to check that a path is relative to the project (should be the default). This way anybody will be able to checkout your project and use it immediately. 3) How to create a static framework -------------------------------- Creating a static framework is easy: a) Using Xcode, create a static library project for iOS. b) Add files as you normally do. You can create any physical / logical structure you want. Whether you are creating a new project or migrating reusable code from an application into a library: - prefix all resource files (including localization files) with the name of the library and an underscore - init view controllers using initWithNibName:bundle: (with nil as bundle since no other bundle than the main bundle can be used on iOS), not simply using init (which loads a xib bearing the same name as the view controller class) - when accessing localized resources, use NSLocalizedStringFromTable instead of NSLocalizedString c) Create a text file listing all headers building the framework public interface, and which you will usually store in the project root directory d) Run make-fmwk.sh from the project root directory to create the bundle. Usually you should choose to put all .staticframeworks generated into a single "repository" directory (by default ~/StaticFrameworks) In some cases additional measures are needed to be able to link against the static framework, see section 7). 4) How to use a static framework ----------------------------- Using a static framework is also easy: a) Using Xcode, create an iOS application, or open an existing one b) Create a text file listing all frameworks you need, usually in the project root directory c) Locate where the .staticframework files you need are located, or create a repository to store them (by default the same repository as make-fwmk.sh will be used). If you need to compile those frameworks first, please refer to 3) c) Run link-fmwk.sh from your project root directory. By default link-fmwk.sh looks under the ~/StaticFrameworks directory, but you can change this behavior if needed. The script generates symbolic links in a StaticFrameworks project subdirectory. If your project is stored within a source code repository, add this directory to the locations to be ignored (SVN ignore, .gitignore, etc.) d) Within Xcode, add the symbolic links, either using a drag and drop or a right-click on your project explorer tree. Add them as references only, do not copy them e) You can include the framework main header in your project precompiled header file, or just include the headers you need where you need them (always using #import < >) f) Remove references to those .staticframework localized resources you do not need in your project (e.g. you might use a framework including localized resources for english, french, german and italian, but your application only targets english and german). Otherwise you will end up having a partially localized application for the non-needed languages (in our example, french and italian) If a static framework has been updated, you might encounter errors when compiling a project using it. This happens in the following cases: - a resource has been added to or removed from the static framework - a source file has been added to or removed from the static framework (only if the source code has been packed into the framework with the -s option) In both cases the result is that the project tree in Xcode is not in sync anymore. If files have disappeared your project will not compile anymore, if new files have been added your project will compile but probably not link. To fix the project tree, simply remove the framework and add it again using the existing symbolic link. In all other cases where the framework has been updated, compilation against the new version should work flawlessly. Remark: ------- When removing a framework, a dangling link is left in the .pbxproj file and yields warnings when compiling the project. To avoid this issue (and to avoid having to manually fix the .pbxproj file), the link-fmwk.sh script takes care of cleaning up dead link for you. 5) Working on a static framework and a project using it simultaneously ------------------------------------------------------------------- Sometimes you are just updating a static framework when working on a project using it. After having updated the library code, you just need to run make-fwmk.sh again before compiling your project again. If you need to debug your library code while executing your application, you can use the -s option of make-fmwk to pack the source code into the static framework. This option should of course only be used for in-house development. 6) Working with static framework versions -------------------------------------- It is strongly advised to tag frameworks using the -u option. Projects using static frameworks can then specify which framework version they are using since (by default) the version number is appended to the .staticframework name. This way you ensure better traceability of which tagged version of a library a project was linked with. 7) Linkage considerations ---------------------- Due to the highly dynamic nature of the Objective-C language, any method defined in a library might be called, explicitly or in hidden ways (e.g. by using objc_msgSend). Unlike C / C++, we would therefore expect the linker not to perform any dead-code stripping for Objective-C static libraries. In some cases, though, the linker still drops code it considers to be unused. Such code can still be referenced from an application, though, and I ran into the following issues: a) Categories defined for objects not in the library: If such categories are defined "alone" in a source file, the linker will not load the corresponding code, and you will get an "Unrecognized selector" exception at runtime. This problem can also arise even if the category is not alone, provided the linker has no other reason to link with the object file it is contained in. For more information, refer to the following article: http://developer.apple.com/library/mac/#qa/qa2006/qa1490.html b) When using library objects in Interface Builder, you might get an "Unknown class <class> in Interface Builder file" error in the console at runtime. If the library class inherits from an existing UIKit class, your application will not crash, but you will not get the new functionality your class implements, leading to incorrect behavior. The article mentioned above gives a solution to this problem: Double-click your client application target under Xcode, and add the -ObjC flag to the "Other linker flags" setting (Remark: this setting also exists when double-clicking on your project under Xcode, but it only works when set on a target). For categories the -all_load flag must also be added, as explained in the article. Adding linker flags works but is far from being optimal, though: - it leads to unnecessarily larger executable sizes - it affects all libraries which a client application is linked against - it has to be set manually for each client project - it has to be documented when you distribute a library, and you can expect users to forget or not set these flags correctly. Moreover, users can easily set parameters incorrectly for some but not all of their targets, which can lead to unpleasant debugging nights As discussed above, failure to set the linker flags properly will lead to crashes (if you are lucky) or to incorrect behaviors difficult to debug. It would therefore be nice if linking could be forced by the library itself without any additional client project configuration. There is a solution: Fooling the linker into thinking an object file must be loaded when linking. This is made possible by the fact that if the linker requires something into a file, it will link all of it, even if the code in the remaining of the file is not directly required. And since it suffices to call a method (even if this method is never actually itself called!) to force linking of the object file it resides in, the solution is to add code which will be always called to each file which must always be taken into account when linking. To achieve this result, this script proceeds as follows: a) The script reads a file as input (bootstrap.txt by default), which lists all source files for which linking must be forced. b) Each of these source files is then appended a dummy class (whose name comprises the name of the file to avoid clashes). Both the definitions and the declarations are added to the source files in order to avoid additional header files. A backup of the original source file is made, and the dummy class is appended to the end of the file so that the original file numbers are kept intact. The dummy class itself does nothing more than expose an empty class method. c) The library is compiled with the modified source code files, then the original files are restored. d) A bootstrap source file is created, which repeats the dummy class definitions (since we have no header files for them; class definition consistency is no issue here since we control the whole process). A dummy function is added to call the class method for all dummy classes. This file is saved into the static framework package as is. When a static framework is then added to a project, the bootstrap code gets compiled as well (thus the term "bootstrap" I introduced). Even if the dummy function it contains is not used, the linker will happily load all dummy classes since their class method is called, which effectively loads the modules they reside in, yielding the desire effect. Remark: When the source code is bundled into the .staticframework, no bootstrapping is needed. Since the whole source code is available, the linking will not be as aggressive as it is when linking to a static library. In such cases the bootstrapping file will be ignored, even if provided. 8) Troubleshooting --------------- a) 'I get an “Unknown class <class> in Interface Builder file" error in the console at runtime' or 'I get a "selector not recognized" exception when calling a category method stemming from a static library' If your static framework contains the source code, you probably have updated your project and you need to remove / add the framework again, see section 4). Otherwise this means that some of the source files require forced linkage. If you have access to the framework code, update the boostrap definitions and build the framework again. Otherwise add the -ObjC (and maybe -all_load) flag to your project target(s) and start the build again. 9) Version history --------------- 1.0 September 2010 Initial release 1.1 October 2010 Convention over configuration philosophy. Easier to use 1.2 October 2010 Ability to force link for specific files. Other minor improvements 10) Planned changes --------------- The 2.0 version should be even easier to use: A single command to link and build, and a single pom.xml-like configuration file with everything properly setup (version, repositories, etc.). It would definitely make sense to use Maven (and we could then benefit from artifact repositories as well), but I don't know if it would be hard to achieve or not. This remains to be investigated. This single pom.xml-like file should also be used to flag files which must not be copied as resources into the framework (e.g. readme files, compilation scripts, etc.). Currently this is not possible, any file which is not source code will be copied into the Resources folder. Some projects alter the default output directory for binary products (e.g. CorePlot), which means that the lipo command call fails (the script assumes a specific directory name and cannot find the binaries). Fix. 11) Example ------- This script is supplied with an example of a library (PrefixLibrary) for use by a client application (PrefixClientApp). This example conforms to the prefix convention for resources (thus its name) and shows how public headers are defined, how resources are accessed (localized strings, xib files and images) and how linkage can be forced for a category and a UILabel subclass. To compile the library using the Release configuration, the one expected by the PrefixClientApp project, open a terminal in the PrefixLibrary directory, and enter /path/to/the/script/make-fmwk.sh Release This saves the .staticframework package into the default ~/StaticFrameworks repository. Feel free to experiment with script parameters if you want. To compile the client application, you must first create the symbolic links pointing to the PrefixLibrary static framework. Switch to the PrefixClientApp folder, and enter /path/to/the/script/link-fmwk.sh This generates a StaticFrameworks directory into the PrefixClientApp directory. Then open the PrefixClientApp project using Xcode. Compile and run, you are done! 12) Known issues ------------ In general, with a default Xcode static library project, the name of the .a file matches the one of the .xcodeproj itself. An exception to this rule is when a project name contains hyphens (-). In such cases those are replaced by underscores (_) to obtain the name of the .a file. In such cases the script will not work since it assumes that the library name is the same as the project name when locating the .a files for the lipo command. The same issue affects projects for which the output file name has been changed and does not match the one of the .xcodeproj anymore. In such cases, you will have to rename the output file in your project file. This should hopefully be fixed soon. It is also rather inconvenient to have to remove non-needed localized resources after they have been added to a project (see 4)). Finally, you might encounter linker issues when using some static frameworks. Those can currently (and sadly) only be solved by editing your client project settings to fix the linker behavior (within Xcode, double-click the project, and under the "Build" tab search for the "Other Linker Flags" setting). Most notably: - if the static framework uses libxml internally, you need either to add -lxml2 to your project "Other Linker Flags" setting, or to add libxml2.dylib to your project frameworks, otherwise you will get unresolved symbols. You will also need to add $(SDKROOT)/usr/include/libxml2 to your project Include search path if one of the libxml headers is included from a framework header file - if the static framework was created by compiling C++ files (.mm most probably), the client project cannot know it must link against the C++ runtime, and you will get unresolved symbols. This is fixed by adding -lstdc++ to your project "Other Linker Flags" setting In all cases, your static framework should document which settings need to be tweaked for the client project. If Maven were used, this could probably be made automatically, so that you would never have to change project settings to use a .staticframework. 13) Possible improvements --------------------- Currently, if a static framework requires a system framework or library (e.g. CFNetwork, MapKit, libxml, etc.), then these still need to be manually added to the client project using it. Similarly, if some settings are required (e.g. include path for libxml), those have to be set manually. There should be a way to automate such tasks by editing the .pbxproj directly. This feature is a must-have IMHO, but this will not be implemented until everything is implemented as a Maven plugin (see 10)) 14) Real-life example: Building a .staticframework for ASIHTTPRequest ----------------------------------------------------------------- ASIHTTPRequest is a quite popular project, in v1.8 at the time of this writing. As always, the setup instructions are "copy the library files into your project, tune some settings, and you are done". Though the procedure is not as straightforward as it should be (the ASIHTTPRequest creator has not created any make-fmwk ready-to-build project), we can still pack this library into a .staticframework. Follow the instructions below: a) Checkout make-fmwk.sh (the discussion here assumes you are using at least v1.2) b) Checkout the ASIHTTPRequest source code from Github: git clone https://github.com/pokeb/asi-http-request.git c) No .pbxproj exists for the library (there are some sample projects. but these are standalone test applications). You must therefore create the missing static library project using Xcode. A good name choice is asi_http_request, to avoid current issues with make-fmwk.sh v1.8 and hyphens in project names (see section 12)) d) Copy the library .m and .h files into some subfolder of your static library project. For ASIHTTPRequest v1.8, those are: ASIAuthenticationDialog.h ASIAuthenticationDialog.m ASICacheDelegate.h ASIDataCompressor.h ASIDataCompressor.m ASIDataDecompressor.h ASIDataDecompressor.m ASIDownloadCache.h ASIDownloadCache.m ASIFormDataRequest.h ASIFormDataRequest.m ASIHTTPRequest.h ASIHTTPRequest.m ASIHTTPRequestConfig.h ASIHTTPRequestDelegate.h ASIInputStream.h ASIInputStream.m ASINetworkQueue.h ASINetworkQueue.m ASIProgressDelegate.h ASIWebPageRequest.h ASIWebPageRequest.m Reachability.h Reachability.m Open Xcode and add these files to the static library project. Also add the following frameworks and libraries to this project: *CFNetwork* *SystemConfiguration* *MobileCoreServices* *CoreGraphics* *UIKit* libxml2 (do not forget to add the $(SDKROOT)/usr/include/libxml2 to your project Include search path; since this is not a framework, headers are not bundled with it) libz For all frameworks between * *, add the corresponding global header <framework_name/framework_name.h> to the .pch file. Now build the library from within Xcode. Everything will compile. e) Create a publicHeaders.txt file listing all public header files (in fact all .h files from the above list), and save it within the static library main project directory. f) Create the .staticframework by opening a terminal and running the following commands from the static library main project directory (here we tag this version as 1.8): make-fmwk.sh -u 1.8 Release make-fmwk.sh -u 1.8 -s Debug (here I decided to put the whole source code into the Debug package for debugging purposes) If targeting iOS 3.2, we need to avoid compiling the ASIHTTPRequest iOS4 features by specifying the SDK version explicitly (otherwise linking with an iOS3.2 project will fail): make-fmwk.sh -u 1.8 -k 3.2 Release make-fmwk.sh -u 1.8 -k 3.2 -s Debug The resulting .staticframework is saved into your main static framework repository (~/StaticFrameworks). g) You are done. The .staticframework can now be added to any client project requiring it. In your client project main directory, create a frameworks.txt file listing the frameworks you need (e.g. asi_http_request-1.8-Release). Then issue the link-fmwk.sh command to create the symbolic link to the .staticframework. Open the client project using Xcode and add the symbolic link to your project. To get the project to compile and link properly, you will (sadly) have to include the framework dependencies of ASIHTTPRequest itself: CFNetwork SystemConfiguration MobileCoreServices CoreGraphics UIKit (should already be included) libxml2 (also add $(SDKROOT)/usr/include/libxml2 to your project Include search path) libz (Remark: instead of adding libxml2 to your project frameworks, you can add -lxml2 to your "Other Linker Flags" project setting) The make-fmwk command has taken care of including all headers in the .staticframework global include file, you therefore only have to add this header file to your .pch. Now build your project, everything should compile and link. If you get unresolved __Block_object_dispose calls, this means that your ASIHTTPRequest framework was compiled for iOS4, and you are linking to an iOS3 project (most probably an iPad project). Run the make-fmwk.sh command again with the -k 3.2 switch (see above), then build your project again. If Maven were used, none of this would hopefully be necessary. Maven would take care of dependency transitivity for you (fixing the .pbxproj to add frameworks automagically). It would also take care of the libxml2 header path, and everything would be reduced to a single command :-) 15) Adapters -------- Since this tool is not mainstream (hopefully it will soon be ;-) ), some projects cannot be used as is with the make-fmwk.sh command. For some projets I find helpful, I will provide adapters which checkout the original source code and create a project that make-fmwk.sh will be happy to deal with. Those are found under the adapters directory, and you simply need to run the generate.sh script to checkout the code, create the project and build the .staticframeworks (which are saved in the default framework repository). Be warned that those scripts are quite rough and not necessarily cleverly written. If Maven were used, those would simply be replaced by a pom.xml file.
About
Tool for creating Objective-C frameworks for iOS (iPhone and iPad) around static libraries
Resources
Stars
Watchers
Forks
Packages 0
No packages published