Skip to content

allright/make-fmwk

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

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

No packages published