1#!/usr/bin/env python
2
3import os, sys, subprocess, argparse, shutil, glob, re, multiprocessing
4import logging as log
5
6SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
7
8class Fail(Exception):
9    def __init__(self, text=None):
10        self.t = text
11    def __str__(self):
12        return "ERROR" if self.t is None else self.t
13
14def execute(cmd, shell=False):
15    try:
16        log.info("Executing: %s" % cmd)
17        env = os.environ.copy()
18        env['VERBOSE'] = '1'
19        retcode = subprocess.call(cmd, shell=shell, env=env)
20        if retcode < 0:
21            raise Fail("Child was terminated by signal: %s" % -retcode)
22        elif retcode > 0:
23            raise Fail("Child returned: %s" % retcode)
24    except OSError as e:
25        raise Fail("Execution failed: %d / %s" % (e.errno, e.strerror))
26
27def rm_one(d):
28    d = os.path.abspath(d)
29    if os.path.exists(d):
30        if os.path.isdir(d):
31            log.info("Removing dir: %s", d)
32            shutil.rmtree(d)
33        elif os.path.isfile(d):
34            log.info("Removing file: %s", d)
35            os.remove(d)
36
37def check_dir(d, create=False, clean=False):
38    d = os.path.abspath(d)
39    log.info("Check dir %s (create: %s, clean: %s)", d, create, clean)
40    if os.path.exists(d):
41        if not os.path.isdir(d):
42            raise Fail("Not a directory: %s" % d)
43        if clean:
44            for x in glob.glob(os.path.join(d, "*")):
45                rm_one(x)
46    else:
47        if create:
48            os.makedirs(d)
49    return d
50
51def check_file(d):
52    d = os.path.abspath(d)
53    if os.path.exists(d):
54        if os.path.isfile(d):
55            return True
56        else:
57            return False
58    return False
59
60def find_file(name, path):
61    for root, dirs, files in os.walk(path):
62        if name in files:
63            return os.path.join(root, name)
64
65class Builder:
66    def __init__(self, options):
67        self.options = options
68        self.build_dir = check_dir(options.build_dir, create=True)
69        self.opencv_dir = check_dir(options.opencv_dir)
70        self.emscripten_dir = check_dir(options.emscripten_dir)
71
72    def get_toolchain_file(self):
73        return os.path.join(self.emscripten_dir, "cmake", "Modules", "Platform", "Emscripten.cmake")
74
75    def clean_build_dir(self):
76        for d in ["CMakeCache.txt", "CMakeFiles/", "bin/", "libs/", "lib/", "modules"]:
77            rm_one(d)
78
79    def get_cmake_cmd(self):
80        cmd = [
81            "cmake",
82            "-DPYTHON_DEFAULT_EXECUTABLE=%s" % sys.executable,
83               "-DENABLE_PIC=FALSE", # To workaround emscripten upstream backend issue https://github.com/emscripten-core/emscripten/issues/8761
84               "-DCMAKE_BUILD_TYPE=Release",
85               "-DCMAKE_TOOLCHAIN_FILE='%s'" % self.get_toolchain_file(),
86               "-DCPU_BASELINE=''",
87               "-DCPU_DISPATCH=''",
88               "-DCV_TRACE=OFF",
89               "-DBUILD_SHARED_LIBS=OFF",
90               "-DWITH_1394=OFF",
91               "-DWITH_ADE=OFF",
92               "-DWITH_VTK=OFF",
93               "-DWITH_EIGEN=OFF",
94               "-DWITH_FFMPEG=OFF",
95               "-DWITH_GSTREAMER=OFF",
96               "-DWITH_GTK=OFF",
97               "-DWITH_GTK_2_X=OFF",
98               "-DWITH_IPP=OFF",
99               "-DWITH_JASPER=OFF",
100               "-DWITH_JPEG=OFF",
101               "-DWITH_WEBP=OFF",
102               "-DWITH_OPENEXR=OFF",
103               "-DWITH_OPENGL=OFF",
104               "-DWITH_OPENVX=OFF",
105               "-DWITH_OPENNI=OFF",
106               "-DWITH_OPENNI2=OFF",
107               "-DWITH_PNG=OFF",
108               "-DWITH_TBB=OFF",
109               "-DWITH_TIFF=OFF",
110               "-DWITH_V4L=OFF",
111               "-DWITH_OPENCL=OFF",
112               "-DWITH_OPENCL_SVM=OFF",
113               "-DWITH_OPENCLAMDFFT=OFF",
114               "-DWITH_OPENCLAMDBLAS=OFF",
115               "-DWITH_GPHOTO2=OFF",
116               "-DWITH_LAPACK=OFF",
117               "-DWITH_ITT=OFF",
118               "-DWITH_QUIRC=ON",
119               "-DBUILD_ZLIB=ON",
120               "-DBUILD_opencv_apps=OFF",
121               "-DBUILD_opencv_calib3d=ON",
122               "-DBUILD_opencv_dnn=ON",
123               "-DBUILD_opencv_features2d=ON",
124               "-DBUILD_opencv_flann=ON",  # No bindings provided. This module is used as a dependency for other modules.
125               "-DBUILD_opencv_gapi=OFF",
126               "-DBUILD_opencv_ml=OFF",
127               "-DBUILD_opencv_photo=ON",
128               "-DBUILD_opencv_imgcodecs=OFF",
129               "-DBUILD_opencv_shape=OFF",
130               "-DBUILD_opencv_videoio=OFF",
131               "-DBUILD_opencv_videostab=OFF",
132               "-DBUILD_opencv_highgui=OFF",
133               "-DBUILD_opencv_superres=OFF",
134               "-DBUILD_opencv_stitching=OFF",
135               "-DBUILD_opencv_java=OFF",
136               "-DBUILD_opencv_js=ON",
137               "-DBUILD_opencv_python2=OFF",
138               "-DBUILD_opencv_python3=OFF",
139               "-DBUILD_EXAMPLES=OFF",
140               "-DBUILD_PACKAGE=OFF",
141               "-DBUILD_TESTS=OFF",
142               "-DBUILD_PERF_TESTS=OFF"]
143        if self.options.cmake_option:
144            cmd += self.options.cmake_option
145        if self.options.build_doc:
146            cmd.append("-DBUILD_DOCS=ON")
147        else:
148            cmd.append("-DBUILD_DOCS=OFF")
149
150        if self.options.threads:
151            cmd.append("-DWITH_PTHREADS_PF=ON")
152        else:
153            cmd.append("-DWITH_PTHREADS_PF=OFF")
154
155        if self.options.simd:
156            cmd.append("-DCV_ENABLE_INTRINSICS=ON")
157        else:
158            cmd.append("-DCV_ENABLE_INTRINSICS=OFF")
159
160        if self.options.build_wasm_intrin_test:
161            cmd.append("-DBUILD_WASM_INTRIN_TESTS=ON")
162        else:
163            cmd.append("-DBUILD_WASM_INTRIN_TESTS=OFF")
164
165        flags = self.get_build_flags()
166        if flags:
167            cmd += ["-DCMAKE_C_FLAGS='%s'" % flags,
168                    "-DCMAKE_CXX_FLAGS='%s'" % flags]
169        return cmd
170
171    def get_build_flags(self):
172        flags = ""
173        if self.options.build_wasm:
174            flags += "-s WASM=1 "
175        elif self.options.disable_wasm:
176            flags += "-s WASM=0 "
177        if self.options.threads:
178            flags += "-s USE_PTHREADS=1 -s PTHREAD_POOL_SIZE=4 "
179        else:
180            flags += "-s USE_PTHREADS=0 "
181        if self.options.enable_exception:
182            flags += "-s DISABLE_EXCEPTION_CATCHING=0 "
183        if self.options.simd:
184            flags += "-msimd128 "
185        if self.options.build_flags:
186            flags += self.options.build_flags
187        return flags
188
189    def config(self):
190        cmd = self.get_cmake_cmd()
191        cmd.append(self.opencv_dir)
192        execute(cmd)
193
194    def build_opencvjs(self):
195        execute(["make", "-j", str(multiprocessing.cpu_count()), "opencv.js"])
196
197    def build_test(self):
198        execute(["make", "-j", str(multiprocessing.cpu_count()), "opencv_js_test"])
199
200    def build_perf(self):
201        execute(["make", "-j", str(multiprocessing.cpu_count()), "opencv_js_perf"])
202
203    def build_doc(self):
204        execute(["make", "-j", str(multiprocessing.cpu_count()), "doxygen"])
205
206    def build_loader(self):
207        execute(["make", "-j", str(multiprocessing.cpu_count()), "opencv_js_loader"])
208
209
210#===================================================================================================
211
212if __name__ == "__main__":
213    log.basicConfig(format='%(message)s', level=log.DEBUG)
214
215    opencv_dir = os.path.abspath(os.path.join(SCRIPT_DIR, '../..'))
216    emscripten_dir = None
217    if "EMSCRIPTEN" in os.environ:
218        emscripten_dir = os.environ["EMSCRIPTEN"]
219    else:
220        log.warning("EMSCRIPTEN environment variable is not available. Please properly activate Emscripten SDK and consider using 'emcmake' launcher")
221
222    parser = argparse.ArgumentParser(description='Build OpenCV.js by Emscripten')
223    parser.add_argument("build_dir", help="Building directory (and output)")
224    parser.add_argument('--opencv_dir', default=opencv_dir, help='Opencv source directory (default is "../.." relative to script location)')
225    parser.add_argument('--emscripten_dir', default=emscripten_dir, help="Path to Emscripten to use for build (deprecated in favor of 'emcmake' launcher)")
226    parser.add_argument('--build_wasm', action="store_true", help="Build OpenCV.js in WebAssembly format")
227    parser.add_argument('--disable_wasm', action="store_true", help="Build OpenCV.js in Asm.js format")
228    parser.add_argument('--threads', action="store_true", help="Build OpenCV.js with threads optimization")
229    parser.add_argument('--simd', action="store_true", help="Build OpenCV.js with SIMD optimization")
230    parser.add_argument('--build_test', action="store_true", help="Build tests")
231    parser.add_argument('--build_perf', action="store_true", help="Build performance tests")
232    parser.add_argument('--build_doc', action="store_true", help="Build tutorials")
233    parser.add_argument('--build_loader', action="store_true", help="Build OpenCV.js loader")
234    parser.add_argument('--clean_build_dir', action="store_true", help="Clean build dir")
235    parser.add_argument('--skip_config', action="store_true", help="Skip cmake config")
236    parser.add_argument('--config_only', action="store_true", help="Only do cmake config")
237    parser.add_argument('--enable_exception', action="store_true", help="Enable exception handling")
238    # Use flag --cmake option="-D...=ON" only for one argument, if you would add more changes write new cmake_option flags
239    parser.add_argument('--cmake_option', action='append', help="Append CMake options")
240    # Use flag --build_flags="-s USE_PTHREADS=0 -Os" for one and more arguments as in the example
241    parser.add_argument('--build_flags', help="Append Emscripten build options")
242    parser.add_argument('--build_wasm_intrin_test', default=False, action="store_true", help="Build WASM intrin tests")
243    # Write a path to modify file like argument of this flag
244    parser.add_argument('--config', default=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'opencv_js.config.py'),
245                        help="Specify configuration file with own list of exported into JS functions")
246
247    args = parser.parse_args()
248
249    log.debug("Args: %s", args)
250
251    os.environ["OPENCV_JS_WHITELIST"] = os.path.abspath(args.config)
252
253    if 'EMMAKEN_JUST_CONFIGURE' in os.environ:
254        del os.environ['EMMAKEN_JUST_CONFIGURE']  # avoid linker errors with NODERAWFS message then using 'emcmake' launcher
255
256    if args.emscripten_dir is None:
257        log.error("Cannot get Emscripten path, please use 'emcmake' launcher or specify it either by EMSCRIPTEN environment variable or --emscripten_dir option.")
258        sys.exit(-1)
259
260    builder = Builder(args)
261
262    os.chdir(builder.build_dir)
263
264    if args.clean_build_dir:
265        log.info("=====")
266        log.info("===== Clean build dir %s", builder.build_dir)
267        log.info("=====")
268        builder.clean_build_dir()
269
270    if not args.skip_config:
271        target = "default target"
272        if args.build_wasm:
273            target = "wasm"
274        elif args.disable_wasm:
275            target = "asm.js"
276        log.info("=====")
277        log.info("===== Config OpenCV.js build for %s" % target)
278        log.info("=====")
279        builder.config()
280
281    if args.config_only:
282        sys.exit(0)
283
284    log.info("=====")
285    log.info("===== Building OpenCV.js")
286    log.info("=====")
287    builder.build_opencvjs()
288
289    if args.build_test:
290        log.info("=====")
291        log.info("===== Building OpenCV.js tests")
292        log.info("=====")
293        builder.build_test()
294
295    if args.build_perf:
296        log.info("=====")
297        log.info("===== Building OpenCV.js performance tests")
298        log.info("=====")
299        builder.build_perf()
300
301    if args.build_doc:
302        log.info("=====")
303        log.info("===== Building OpenCV.js tutorials")
304        log.info("=====")
305        builder.build_doc()
306
307    if args.build_loader:
308        log.info("=====")
309        log.info("===== Building OpenCV.js loader")
310        log.info("=====")
311        builder.build_loader()
312
313    log.info("=====")
314    log.info("===== Build finished")
315    log.info("=====")
316
317    opencvjs_path = os.path.join(builder.build_dir, "bin", "opencv.js")
318    if check_file(opencvjs_path):
319        log.info("OpenCV.js location: %s", opencvjs_path)
320
321    if args.build_test:
322        opencvjs_test_path = os.path.join(builder.build_dir, "bin", "tests.html")
323        if check_file(opencvjs_test_path):
324            log.info("OpenCV.js tests location: %s", opencvjs_test_path)
325
326    if args.build_perf:
327        opencvjs_perf_path = os.path.join(builder.build_dir, "bin", "perf")
328        opencvjs_perf_base_path = os.path.join(builder.build_dir, "bin", "perf", "base.js")
329        if check_file(opencvjs_perf_base_path):
330            log.info("OpenCV.js performance tests location: %s", opencvjs_perf_path)
331
332    if args.build_doc:
333        opencvjs_tutorial_path = find_file("tutorial_js_root.html", os.path.join(builder.build_dir, "doc", "doxygen", "html"))
334        if check_file(opencvjs_tutorial_path):
335            log.info("OpenCV.js tutorials location: %s", opencvjs_tutorial_path)
336
337    if args.build_loader:
338        opencvjs_loader_path = os.path.join(builder.build_dir, "bin", "loader.js")
339        if check_file(opencvjs_loader_path):
340            log.info("OpenCV.js loader location: %s", opencvjs_loader_path)
341