Use all available cpu cores in iOS/OSX build procedure
[platform/upstream/opencv.git] / platforms / ios / build_framework.py
1 #!/usr/bin/env python
2 """
3 The script builds OpenCV.framework for iOS.
4 The built framework is universal, it can be used to build app and run it on either iOS simulator or real device.
5
6 Usage:
7     ./build_framework.py <outputdir>
8
9 By cmake conventions (and especially if you work with OpenCV repository),
10 the output dir should not be a subdirectory of OpenCV source tree.
11
12 Script will create <outputdir>, if it's missing, and a few its subdirectories:
13
14     <outputdir>
15         build/
16             iPhoneOS-*/
17                [cmake-generated build tree for an iOS device target]
18             iPhoneSimulator-*/
19                [cmake-generated build tree for iOS simulator]
20         opencv2.framework/
21             [the framework content]
22
23 The script should handle minor OpenCV updates efficiently
24 - it does not recompile the library from scratch each time.
25 However, opencv2.framework directory is erased and recreated on each run.
26
27 Adding --dynamic parameter will build opencv2.framework as App Store dynamic framework. Only iOS 8+ versions are supported.
28 """
29
30 from __future__ import print_function
31 import glob, re, os, os.path, shutil, string, sys, argparse, traceback, multiprocessing
32 from subprocess import check_call, check_output, CalledProcessError
33
34 def execute(cmd, cwd = None):
35     print("Executing: %s in %s" % (cmd, cwd), file=sys.stderr)
36     retcode = check_call(cmd, cwd = cwd)
37     if retcode != 0:
38         raise Exception("Child returned:", retcode)
39
40 def getXCodeMajor():
41     ret = check_output(["xcodebuild", "-version"])
42     m = re.match(r'XCode\s+(\d)\..*', ret, flags=re.IGNORECASE)
43     if m:
44         return int(m.group(1))
45     return 0
46
47 class Builder:
48     def __init__(self, opencv, contrib, dynamic, bitcodedisabled, exclude, targets):
49         self.opencv = os.path.abspath(opencv)
50         self.contrib = None
51         if contrib:
52             modpath = os.path.join(contrib, "modules")
53             if os.path.isdir(modpath):
54                 self.contrib = os.path.abspath(modpath)
55             else:
56                 print("Note: contrib repository is bad - modules subfolder not found", file=sys.stderr)
57         self.dynamic = dynamic
58         self.bitcodedisabled = bitcodedisabled
59         self.exclude = exclude
60         self.targets = targets
61
62     def getBD(self, parent, t):
63
64         if len(t[0]) == 1:
65             res = os.path.join(parent, 'build-%s-%s' % (t[0][0].lower(), t[1].lower()))
66         else:
67             res = os.path.join(parent, 'build-%s' % t[1].lower())
68
69         if not os.path.isdir(res):
70             os.makedirs(res)
71         return os.path.abspath(res)
72
73     def _build(self, outdir):
74         outdir = os.path.abspath(outdir)
75         if not os.path.isdir(outdir):
76             os.makedirs(outdir)
77         mainWD = os.path.join(outdir, "build")
78         dirs = []
79
80         xcode_ver = getXCodeMajor()
81
82         if self.dynamic:
83             alltargets = self.targets
84         else:
85             # if we are building a static library, we must build each architecture separately
86             alltargets = []
87
88             for t in self.targets:
89                 for at in t[0]:
90                     current = ( [at], t[1] )
91
92                     alltargets.append(current)
93
94         for t in alltargets:
95             mainBD = self.getBD(mainWD, t)
96             dirs.append(mainBD)
97
98             cmake_flags = []
99             if self.contrib:
100                 cmake_flags.append("-DOPENCV_EXTRA_MODULES_PATH=%s" % self.contrib)
101             if xcode_ver >= 7 and t[1] == 'iPhoneOS' and self.bitcodedisabled == False:
102                 cmake_flags.append("-DCMAKE_C_FLAGS=-fembed-bitcode")
103                 cmake_flags.append("-DCMAKE_CXX_FLAGS=-fembed-bitcode")
104             self.buildOne(t[0], t[1], mainBD, cmake_flags)
105
106             if self.dynamic == False:
107                 self.mergeLibs(mainBD)
108         self.makeFramework(outdir, dirs)
109
110     def build(self, outdir):
111         try:
112             self._build(outdir)
113         except Exception as e:
114             print("="*60, file=sys.stderr)
115             print("ERROR: %s" % e, file=sys.stderr)
116             print("="*60, file=sys.stderr)
117             traceback.print_exc(file=sys.stderr)
118             sys.exit(1)
119
120     def getToolchain(self, arch, target):
121         return None
122
123     def getCMakeArgs(self, arch, target):
124
125         args = [
126             "cmake",
127             "-GXcode",
128             "-DAPPLE_FRAMEWORK=ON",
129             "-DCMAKE_INSTALL_PREFIX=install",
130             "-DCMAKE_BUILD_TYPE=Release",
131         ] + ([
132             "-DBUILD_SHARED_LIBS=ON",
133             "-DCMAKE_MACOSX_BUNDLE=ON",
134             "-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=NO",
135         ] if self.dynamic else [])
136
137         if len(self.exclude) > 0:
138             args += ["-DBUILD_opencv_world=OFF"] if not self.dynamic else []
139             args += ["-DBUILD_opencv_%s=OFF" % m for m in self.exclude]
140
141         return args
142
143     def getBuildCommand(self, archs, target):
144
145         buildcmd = [
146             "xcodebuild",
147         ]
148
149         if self.dynamic:
150             buildcmd += [
151                 "IPHONEOS_DEPLOYMENT_TARGET=8.0",
152                 "ONLY_ACTIVE_ARCH=NO",
153             ]
154
155             for arch in archs:
156                 buildcmd.append("-arch")
157                 buildcmd.append(arch.lower())
158         else:
159             arch = ";".join(archs)
160             buildcmd += [
161                 "IPHONEOS_DEPLOYMENT_TARGET=6.0",
162                 "ARCHS=%s" % arch,
163             ]
164
165         buildcmd += [
166                 "-sdk", target.lower(),
167                 "-configuration", "Release",
168                 "-parallelizeTargets",
169                 "-jobs", multiprocessing.cpu_count(),
170             ] + (["-target","ALL_BUILD"] if self.dynamic else [])
171
172         return buildcmd
173
174     def getInfoPlist(self, builddirs):
175         return os.path.join(builddirs[0], "ios", "Info.plist")
176
177     def buildOne(self, arch, target, builddir, cmakeargs = []):
178         # Run cmake
179         toolchain = self.getToolchain(arch, target)
180         cmakecmd = self.getCMakeArgs(arch, target) + \
181             (["-DCMAKE_TOOLCHAIN_FILE=%s" % toolchain] if toolchain is not None else [])
182         if target.lower().startswith("iphoneos"):
183             cmakecmd.append("-DENABLE_NEON=ON")
184         cmakecmd.append(self.opencv)
185         cmakecmd.extend(cmakeargs)
186         execute(cmakecmd, cwd = builddir)
187
188         # Clean and build
189         clean_dir = os.path.join(builddir, "install")
190         if os.path.isdir(clean_dir):
191             shutil.rmtree(clean_dir)
192         buildcmd = self.getBuildCommand(arch, target)
193         execute(buildcmd + ["-target", "ALL_BUILD", "build"], cwd = builddir)
194         execute(["cmake", "-P", "cmake_install.cmake"], cwd = builddir)
195
196     def mergeLibs(self, builddir):
197         res = os.path.join(builddir, "lib", "Release", "libopencv_merged.a")
198         libs = glob.glob(os.path.join(builddir, "install", "lib", "*.a"))
199         libs3 = glob.glob(os.path.join(builddir, "install", "share", "OpenCV", "3rdparty", "lib", "*.a"))
200         print("Merging libraries:\n\t%s" % "\n\t".join(libs + libs3), file=sys.stderr)
201         execute(["libtool", "-static", "-o", res] + libs + libs3)
202
203     def makeFramework(self, outdir, builddirs):
204         name = "opencv2"
205
206         # set the current dir to the dst root
207         framework_dir = os.path.join(outdir, "%s.framework" % name)
208         if os.path.isdir(framework_dir):
209             shutil.rmtree(framework_dir)
210         os.makedirs(framework_dir)
211
212         if self.dynamic:
213             dstdir = framework_dir
214             libname = "opencv2.framework/opencv2"
215         else:
216             dstdir = os.path.join(framework_dir, "Versions", "A")
217             libname = "libopencv_merged.a"
218
219         # copy headers from one of build folders
220         shutil.copytree(os.path.join(builddirs[0], "install", "include", "opencv2"), os.path.join(dstdir, "Headers"))
221
222         # make universal static lib
223         libs = [os.path.join(d, "lib", "Release", libname) for d in builddirs]
224         lipocmd = ["lipo", "-create"]
225         lipocmd.extend(libs)
226         lipocmd.extend(["-o", os.path.join(dstdir, name)])
227         print("Creating universal library from:\n\t%s" % "\n\t".join(libs), file=sys.stderr)
228         execute(lipocmd)
229
230         # dynamic framework has different structure, just copy the Plist directly
231         if self.dynamic:
232             resdir = dstdir
233             shutil.copyfile(self.getInfoPlist(builddirs), os.path.join(resdir, "Info.plist"))
234         else:
235             # copy Info.plist
236             resdir = os.path.join(dstdir, "Resources")
237             os.makedirs(resdir)
238             shutil.copyfile(self.getInfoPlist(builddirs), os.path.join(resdir, "Info.plist"))
239
240             # make symbolic links
241             links = [
242                 (["A"], ["Versions", "Current"]),
243                 (["Versions", "Current", "Headers"], ["Headers"]),
244                 (["Versions", "Current", "Resources"], ["Resources"]),
245                 (["Versions", "Current", name], [name])
246             ]
247             for l in links:
248                 s = os.path.join(*l[0])
249                 d = os.path.join(framework_dir, *l[1])
250                 os.symlink(s, d)
251
252 class iOSBuilder(Builder):
253
254     def getToolchain(self, arch, target):
255         toolchain = os.path.join(self.opencv, "platforms", "ios", "cmake", "Toolchains", "Toolchain-%s_Xcode.cmake" % target)
256         return toolchain
257
258     def getCMakeArgs(self, arch, target):
259         arch = ";".join(arch)
260
261         args = Builder.getCMakeArgs(self, arch, target)
262         args = args + [
263             '-DIOS_ARCH=%s' % arch
264         ]
265         return args
266
267
268 if __name__ == "__main__":
269     folder = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "../.."))
270     parser = argparse.ArgumentParser(description='The script builds OpenCV.framework for iOS.')
271     parser.add_argument('out', metavar='OUTDIR', help='folder to put built framework')
272     parser.add_argument('--opencv', metavar='DIR', default=folder, help='folder with opencv repository (default is "../.." relative to script location)')
273     parser.add_argument('--contrib', metavar='DIR', default=None, help='folder with opencv_contrib repository (default is "None" - build only main framework)')
274     parser.add_argument('--without', metavar='MODULE', default=[], action='append', help='OpenCV modules to exclude from the framework')
275     parser.add_argument('--dynamic', default=False, action='store_true', help='build dynamic framework (default is "False" - builds static framework)')
276     parser.add_argument('--disable-bitcode', default=False, dest='bitcodedisabled', action='store_true', help='disable bitcode (enabled by default)')
277     args = parser.parse_args()
278
279     b = iOSBuilder(args.opencv, args.contrib, args.dynamic, args.bitcodedisabled, args.without,
280         [
281             (["armv7", "arm64"], "iPhoneOS"),
282         ] if os.environ.get('BUILD_PRECOMMIT', None) else
283         [
284             (["armv7", "armv7s", "arm64"], "iPhoneOS"),
285             (["i386", "x86_64"], "iPhoneSimulator"),
286         ])
287     b.build(args.out)