forked from defagos/make-fmwk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathREADME
500 lines (428 loc) · 29.4 KB
/
README
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
************************************************************
* 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.