fix compilation error for linux builds due to missing iconv support
[wkhtmltopdf:wkhtmltopdf.git] / scripts / build.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2014 wkhtmltopdf authors
4 #
5 # This file is part of wkhtmltopdf.
6 #
7 # wkhtmltopdf is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Lesser General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # wkhtmltopdf is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public License
18 # along with wkhtmltopdf.  If not, see <http:#www.gnu.org/licenses/>.
19
20 # --------------------------------------------------------------- CONFIGURATION
21
22 QT_CONFIG = {
23     'common' : [
24         '-opensource',
25         '-confirm-license',
26         '-fast',
27         '-release',
28         '-static',
29         '-graphicssystem raster',
30         '-webkit',
31         '-exceptions',              # required by XmlPatterns
32         '-xmlpatterns',             # required for TOC support
33         '-system-zlib',
34         '-system-libpng',
35         '-system-libjpeg',
36         '-no-libmng',
37         '-no-libtiff',
38         '-no-accessibility',
39         '-no-stl',
40         '-no-qt3support',
41         '-no-phonon',
42         '-no-phonon-backend',
43         '-no-opengl',
44         '-no-declarative',
45         '-no-script',
46         '-no-scripttools',
47         '-no-sql-ibase',
48         '-no-sql-mysql',
49         '-no-sql-odbc',
50         '-no-sql-psql',
51         '-no-sql-sqlite',
52         '-no-sql-sqlite2',
53         '-no-mmx',
54         '-no-3dnow',
55         '-no-sse',
56         '-no-sse2',
57         '-no-multimedia',
58         '-nomake demos',
59         '-nomake docs',
60         '-nomake examples',
61         '-nomake tools',
62         '-nomake tests',
63         '-nomake translations'
64     ],
65
66     'msvc': [
67         '-mp',
68         '-qt-style-windows',
69         '-qt-style-cleanlooks',
70         '-no-style-windowsxp',
71         '-no-style-windowsvista',
72         '-no-style-plastique',
73         '-no-style-motif',
74         '-no-style-cde',
75         '-openssl-linked'           # static linkage for OpenSSL
76     ],
77
78     'posix': [
79         '-silent',                  # perform a silent build
80         '-xrender',                 # xrender support is required
81         '-largefile',
82         '-iconv',                   # iconv support is required for text codecs
83         '-openssl',                 # load OpenSSL binaries at runtime
84         '-no-rpath',
85         '-no-dbus',
86         '-no-nis',
87         '-no-cups',
88         '-no-pch',
89         '-no-gtkstyle',
90         '-no-nas-sound',
91         '-no-sm',
92         '-no-xshape',
93         '-no-xinerama',
94         '-no-xcursor',
95         '-no-xfixes',
96         '-no-xrandr',
97         '-no-mitshm',
98         '-no-xinput',
99         '-no-xkb',
100         '-no-glib',
101         '-no-gstreamer',
102         '-D ENABLE_VIDEO=0',        # required as otherwise gstreamer gets linked in
103         '-no-openvg',
104         '-no-xsync',
105         '-no-audio-backend',
106         '-no-sse3',
107         '-no-ssse3',
108         '-no-sse4.1',
109         '-no-sse4.2',
110         '-no-avx',
111         '-no-neon'
112     ],
113
114     'mingw-w64-cross' : [
115         '-silent',                  # perform a silent build
116         '-openssl-linked',          # static linkage for OpenSSL
117         '-no-reduce-exports',
118         '-no-rpath',
119         '-xplatform win32-g++-4.6'
120     ],
121
122     'osx': [
123         '-no-framework',
124         '-no-dwarf2',
125         '-xrender',                 # xrender support is required
126         '-openssl',                 # load OpenSSL binaries at runtime
127         '-largefile',
128         '-no-rpath',
129         'remove:-system-libpng',
130         'remove:-system-libjpeg',
131         '-qt-libpng',
132         '-qt-libjpeg'
133     ]
134 }
135
136 BUILDERS = {
137     'source-tarball':        'source_tarball',
138     'msvc2008-win32':        'msvc',
139     'msvc2008-win64':        'msvc',
140     'msvc2010-win32':        'msvc',
141     'msvc2010-win64':        'msvc',
142     'msvc2012-win32':        'msvc',
143     'msvc2012-win64':        'msvc',
144     'msvc2013-win32':        'msvc',
145     'msvc2013-win64':        'msvc',
146     'msvc-winsdk71-win32':   'msvc_winsdk71',
147     'msvc-winsdk71-win64':   'msvc_winsdk71',
148     'setup-mingw-w64':       'setup_mingw64',
149     'setup-schroot-centos5': 'setup_schroot',
150     'setup-schroot-centos6': 'setup_schroot',
151     'setup-schroot-wheezy':  'setup_schroot',
152     'setup-schroot-trusty':  'setup_schroot',
153     'setup-schroot-precise': 'setup_schroot',
154     'update-all-schroots':   'update_schroot',
155     'centos5-i386':          'linux_schroot',
156     'centos5-amd64':         'linux_schroot',
157     'centos6-i386':          'linux_schroot',
158     'centos6-amd64':         'linux_schroot',
159     'wheezy-i386':           'linux_schroot',
160     'wheezy-amd64':          'linux_schroot',
161     'trusty-i386':           'linux_schroot',
162     'trusty-amd64':          'linux_schroot',
163     'precise-i386':          'linux_schroot',
164     'precise-amd64':         'linux_schroot',
165     'mingw-w64-cross-win32': 'mingw64_cross',
166     'mingw-w64-cross-win64': 'mingw64_cross',
167     'posix-local':           'posix_local',
168     'osx-cocoa-x86-64':      'osx',
169     'osx-carbon-i386':       'osx'
170 }
171
172 CHROOT_SETUP  = {
173     'wheezy': [
174         ('debootstrap', 'wheezy', 'http://ftp.us.debian.org/debian/'),
175         ('write_file', 'etc/apt/sources.list', """
176 deb http://ftp.debian.org/debian/ wheezy         main contrib non-free
177 deb http://ftp.debian.org/debian/ wheezy-updates main contrib non-free
178 deb http://security.debian.org/   wheezy/updates main contrib non-free"""),
179         ('shell', 'apt-get update'),
180         ('shell', 'apt-get dist-upgrade --assume-yes'),
181         ('shell', 'apt-get install --assume-yes xz-utils libssl-dev libpng-dev libjpeg8-dev zlib1g-dev rubygems'),
182         ('shell', 'apt-get install --assume-yes libfontconfig1-dev libfreetype6-dev libx11-dev libxext-dev libxrender-dev'),
183         ('shell', 'gem install fpm ronn --no-ri --no-rdoc'),
184         ('write_file', 'update.sh', 'apt-get update\napt-get dist-upgrade --assume-yes\n'),
185         ('schroot_conf', 'Debian Wheezy')
186     ],
187
188     'trusty': [
189         ('debootstrap', 'trusty', 'http://archive.ubuntu.com/ubuntu/'),
190         ('write_file', 'etc/apt/sources.list', """
191 deb http://archive.ubuntu.com/ubuntu/ trusty          main restricted universe multiverse
192 deb http://archive.ubuntu.com/ubuntu/ trusty-updates  main restricted universe multiverse
193 deb http://archive.ubuntu.com/ubuntu/ trusty-security main restricted universe multiverse"""),
194         ('shell', 'apt-get update'),
195         ('shell', 'apt-get dist-upgrade --assume-yes'),
196         ('shell', 'apt-get install --assume-yes xz-utils libssl-dev libpng-dev libjpeg-turbo8-dev zlib1g-dev ruby-dev'),
197         ('shell', 'apt-get install --assume-yes libfontconfig1-dev libfreetype6-dev libx11-dev libxext-dev libxrender-dev'),
198         ('shell', 'gem install fpm ronn --no-ri --no-rdoc'),
199         ('write_file', 'update.sh', 'apt-get update\napt-get dist-upgrade --assume-yes\n'),
200         ('schroot_conf', 'Ubuntu Trusty')
201     ],
202
203     'precise': [
204         ('debootstrap', 'precise', 'http://archive.ubuntu.com/ubuntu/'),
205         ('write_file', 'etc/apt/sources.list', """
206 deb http://archive.ubuntu.com/ubuntu/ precise          main restricted universe multiverse
207 deb http://archive.ubuntu.com/ubuntu/ precise-updates  main restricted universe multiverse
208 deb http://archive.ubuntu.com/ubuntu/ precise-security main restricted universe multiverse"""),
209         ('shell', 'apt-get update'),
210         ('shell', 'apt-get dist-upgrade --assume-yes'),
211         ('shell', 'apt-get install --assume-yes xz-utils libssl-dev libpng-dev libjpeg8-dev zlib1g-dev rubygems'),
212         ('shell', 'apt-get install --assume-yes libfontconfig1-dev libfreetype6-dev libx11-dev libxext-dev libxrender-dev'),
213         ('shell', 'gem install fpm ronn --no-ri --no-rdoc'),
214         ('write_file', 'update.sh', 'apt-get update\napt-get dist-upgrade --assume-yes\n'),
215         ('schroot_conf', 'Ubuntu Precise')
216     ],
217
218     'centos5': [
219         ('rinse', 'centos-5'),
220         ('download_file', 'http://centos.karan.org/el5/ruby187/kbs-el5-ruby187.repo', 'etc/yum.repos.d'),
221         ('download_file', 'http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm', 'tmp'),
222         ('shell', 'rpm -i /tmp/epel-release-5-4.noarch.rpm'),
223         ('shell', 'yum update -y'),
224         ('append_file:amd64', 'etc/yum.conf', 'exclude = *.i?86\n'),
225         ('shell', 'yum install -y gcc gcc-c++ make diffutils perl xz ruby-devel rubygems rpm-build libffi-devel'),
226         ('shell', 'yum install -y openssl-devel libX11-devel libXrender-devel libXext-devel fontconfig-devel freetype-devel libjpeg-devel libpng-devel zlib-devel'),
227         ('shell', 'gem install fpm ronn --no-ri --no-rdoc'),
228         ('write_file', 'update.sh', 'yum update -y\n'),
229         ('schroot_conf', 'CentOS 5')
230     ],
231
232     'centos6': [
233         ('rinse', 'centos-6'),
234         ('shell', 'yum update -y'),
235         ('append_file:amd64', 'etc/yum.conf', 'exclude = *.i?86\n'),
236         ('shell', 'yum install -y gcc gcc-c++ make diffutils perl tar xz ruby-devel rubygems rpm-build libffi-devel'),
237         ('shell', 'yum install -y openssl-devel libX11-devel libXrender-devel libXext-devel fontconfig-devel freetype-devel libjpeg-devel libpng-devel zlib-devel'),
238         ('shell', 'gem install fpm ronn --no-ri --no-rdoc'),
239         ('write_file', 'update.sh', 'yum update -y\n'),
240         ('schroot_conf', 'CentOS 6')
241     ]
242 }
243
244 DEPENDENT_LIBS = {
245     'openssl': {
246         'order' : 1,
247         'url'   : 'http://www.openssl.org/source/openssl-1.0.1h.tar.gz',
248         'sha1'  : 'b2239599c8bf8f7fc48590a55205c26abe560bf8',
249         'build' : {
250             'msvc*-win32*': {
251                 'result': ['include/openssl/ssl.h', 'lib/ssleay32.lib', 'lib/libeay32.lib'],
252                 'commands': [
253                     'perl Configure --openssldir=%(destdir)s VC-WIN32 no-asm',
254                     'ms\\do_ms.bat',
255                     'nmake /f ms\\nt.mak install'],
256             },
257             'msvc*-win64*': {
258                 'result': ['include/openssl/ssl.h', 'lib/ssleay32.lib', 'lib/libeay32.lib'],
259                 'commands': [
260                     'perl Configure --openssldir=%(destdir)s VC-WIN64A',
261                     'ms\\do_win64a.bat',
262                     'nmake /f ms\\nt.mak install']
263             },
264             'mingw-w64-cross-win*': {
265                 'result': ['include/openssl/ssl.h', 'lib/libssl.a', 'lib/libcrypto.a'],
266                 'commands': [
267                     'perl Configure --openssldir=%(destdir)s --cross-compile-prefix=%(mingw-w64)s- no-shared no-asm mingw64',
268                     'make',
269                     'make install_sw']
270             }
271         }
272     },
273
274     'zlib': {
275         'order' : 2,
276         'url'   : 'http://downloads.sourceforge.net/libpng/zlib-1.2.8.tar.gz',
277         'sha1'  : 'a4d316c404ff54ca545ea71a27af7dbc29817088',
278         'build' : {
279             'msvc*': {
280                 'result': {
281                     'include/zlib.h' : 'zlib.h',
282                     'include/zconf.h': 'zconf.h',
283                     'lib/zdll.lib'   : 'zlib.lib'
284                 },
285                 'replace':  [('win32/Makefile.msc', '-MD', '-MT')],
286                 'commands': ['nmake /f win32/Makefile.msc zlib.lib']
287             },
288             'mingw-w64-cross-win*': {
289                 'result': {
290                     'include/zlib.h' : 'zlib.h',
291                     'include/zconf.h': 'zconf.h',
292                     'lib/libz.a'     : 'libz.a'
293                 },
294                 'replace':  [('win32/Makefile.gcc', 'PREFIX =', 'PREFIX = %(mingw-w64)s-')],
295                 'commands': ['make -f win32/Makefile.gcc']
296             }
297         }
298     },
299
300     'libpng': {
301         'order' : 3,
302         'url' : 'http://downloads.sourceforge.net/libpng/libpng-1.5.18.tar.gz',
303         'sha1': '7ddf6865aa70d2d79faf65ebc611919a0b573654',
304         'build' : {
305             'msvc*': {
306                 'result': {
307                     'include/png.h'       : 'png.h',
308                     'include/pngconf.h'   : 'pngconf.h',
309                     'include/pnglibconf.h': 'pnglibconf.h',
310                     'lib/libpng.lib'      : 'libpng.lib'
311                 },
312                 'replace': [
313                     ('scripts/makefile.vcwin32', '-MD', '-MT'),
314                     ('scripts/makefile.vcwin32', '-I..\\zlib', '-I..\\deplibs\\include'),
315                     ('scripts/makefile.vcwin32', '..\\zlib\\zlib.lib', '..\\deplibs\\lib\\zdll.lib')],
316                 'commands': ['nmake /f scripts/makefile.vcwin32 libpng.lib']
317             },
318             'mingw-w64-cross-win*': {
319                 'result': {
320                     'include/png.h'       : 'png.h',
321                     'include/pngconf.h'   : 'pngconf.h',
322                     'include/pnglibconf.h': 'pnglibconf.h',
323                     'lib/libpng.a'        : 'libpng.a'
324                 },
325                 'replace': [
326                     ('scripts/makefile.gcc', 'ZLIBINC = ../zlib', 'ZLIBINC = %(destdir)s/include'),
327                     ('scripts/makefile.gcc', 'ZLIBLIB = ../zlib', 'ZLIBLIB = %(destdir)s/lib'),
328                     ('scripts/makefile.gcc', 'CC = gcc', 'CC = %(mingw-w64)s-gcc'),
329                     ('scripts/makefile.gcc', 'AR_RC = ar', 'AR_RC = %(mingw-w64)s-ar'),
330                     ('scripts/makefile.gcc', 'RANLIB = ranlib', 'RANLIB = %(mingw-w64)s-ranlib')],
331                 'commands': ['make -f scripts/makefile.gcc libpng.a']
332             }
333         }
334     },
335
336     'libjpeg': {
337         'order' : 4,
338         'url' : 'http://ijg.org/files/jpegsrc.v9a.tar.gz',
339         'sha1': 'd65ed6f88d318f7380a3a5f75d578744e732daca',
340         'build' : {
341             'msvc*': {
342                 'result': {
343                     'include/jpeglib.h' : 'jpeglib.h',
344                     'include/jmorecfg.h': 'jmorecfg.h',
345                     'include/jerror.h'  : 'jerror.h',
346                     'include/jconfig.h' : 'jconfig.h',
347                     'lib/libjpeg.lib'   : 'libjpeg.lib'
348                 },
349                 'replace':  [('makefile.vc', '!include <win32.mak>', ''),
350                              ('makefile.vc', '$(cc)', 'cl'),
351                              ('makefile.vc', '$(cflags) $(cdebug) $(cvars)', '-c -nologo -D_CRT_SECURE_NO_DEPRECATE -MT -O2 -W3')],
352                 'commands': [
353                     'copy /y jconfig.vc jconfig.h',
354                     'nmake /f makefile.vc libjpeg.lib']
355             },
356             'mingw-w64-cross-win*': {
357                 'result': ['include/jpeglib.h', 'include/jmorecfg.h', 'include/jerror.h', 'include/jconfig.h', 'lib/libjpeg.a'],
358                 'commands': [
359                     './configure --host=%(mingw-w64)s --disable-shared --prefix=%(destdir)s',
360                     'make install']
361             }
362         }
363     }
364 }
365
366 EXCLUDE_SRC_TARBALL = [
367     'qt/config.profiles*',
368     'qt/demos*',
369     'qt/dist*',
370     'qt/doc*',
371     'qt/examples*',
372     'qt/imports*',
373     'qt/templates*',
374     'qt/tests*',
375     'qt/translations*',
376     'qt/util*',
377     'qt/lib/fonts*',
378     'qt/src/3rdparty/*ChangeLog*',
379     'qt/src/3rdparty/ce-compat*',
380     'qt/src/3rdparty/clucene*',
381     'qt/src/3rdparty/fonts*',
382     'qt/src/3rdparty/freetype*',
383     'qt/src/3rdparty/javascriptcore*',
384     'qt/src/3rdparty/libgq*',
385     'qt/src/3rdparty/libmng*',
386     'qt/src/3rdparty/libtiff*',
387     'qt/src/3rdparty/patches*',
388     'qt/src/3rdparty/phonon*',
389     'qt/src/3rdparty/pixman*',
390     'qt/src/3rdparty/powervr*',
391     'qt/src/3rdparty/ptmalloc*',
392     'qt/src/3rdparty/s60*',
393     'qt/src/3rdparty/wayland*'
394 ]
395
396 # --------------------------------------------------------------- HELPERS
397
398 import os, sys, platform, subprocess, shutil, re, fnmatch, multiprocessing, urllib, hashlib, tarfile
399
400 from os.path import exists
401
402 CPU_COUNT = max(2, multiprocessing.cpu_count()-1)   # leave one CPU free
403
404 def rchop(s, e):
405     if s.endswith(e):
406         return s[:-len(e)]
407     return s
408
409 def message(msg):
410     sys.stdout.write(msg)
411     sys.stdout.flush()
412
413 def error(msg):
414     message(msg+'\n')
415     sys.exit(1)
416
417 def shell(cmd):
418     ret = os.system(cmd)
419     if ret != 0:
420         error("command failed: exit code %d" % ret)
421
422 def get_output(*cmd):
423     try:
424         return subprocess.check_output(cmd, stderr=subprocess.STDOUT).strip()
425     except:
426         return None
427
428 def rmdir(path):
429     if exists(path):
430         if platform.system() == 'Windows':
431             shell('attrib -R %s\* /S' % path)
432         shutil.rmtree(path)
433
434 def mkdir_p(path):
435     if not exists(path):
436         os.makedirs(path)
437
438 def get_version(basedir):
439     mkdir_p(basedir)
440     text = open(os.path.join(basedir, '..', 'VERSION'), 'r').read()
441     if '-' not in text:
442         return (text, text)
443     version = text[:text.index('-')]
444     os.chdir(os.path.join(basedir, '..'))
445     hash = get_output('git', 'rev-parse', '--short', 'HEAD')
446     if not hash:
447         return (text, version)
448     return ('%s-%s' % (version, hash), version)
449
450 def qt_config(key, *opts):
451     input, output = [], []
452     input.extend(QT_CONFIG['common'])
453     input.extend(QT_CONFIG[key])
454     input.extend(opts)
455     cfg = os.environ.get('WKHTMLTOX_QT_CONFIG')
456     if cfg:
457         input.extend(cfg.split())
458     for arg in input:
459         if not arg.startswith('remove:-'):
460             output.append(arg)
461         elif arg[1+arg.index(':'):] in output:
462             output.remove(arg[1+arg.index(':'):])
463     return ' '.join(output)
464
465 def download_file(url, sha1, dir):
466     name = url.split('/')[-1]
467     loc  = os.path.join(dir, name)
468     if os.path.exists(loc):
469         hash = hashlib.sha1(open(loc, 'rb').read()).hexdigest()
470         if hash == sha1:
471             return loc
472         os.remove(loc)
473         message('Checksum mismatch for %s, re-downloading.\n' % name)
474     def hook(cnt, bs, total):
475         pct = int(cnt*bs*100/total)
476         message("\rDownloading: %s [%d%%]" % (name, pct))
477     urllib.urlretrieve(url, loc, reporthook=hook)
478     message("\r")
479     hash = hashlib.sha1(open(loc, 'rb').read()).hexdigest()
480     if hash != sha1:
481         os.remove(loc)
482         error('Checksum mismatch for %s, aborting.' % name)
483     message("\rDownloaded: %s [checksum OK]\n" % name)
484     return loc
485
486 def download_tarball(url, sha1, dir, name):
487     loc = download_file(url, sha1, dir)
488     tar = tarfile.open(loc)
489     sub = tar.getnames()[0]
490     if '/' in sub:
491         sub = sub[:sub.index('/')]
492     src = os.path.join(dir, sub)
493     tgt = os.path.join(dir, name)
494     rmdir(src)
495     tar.extractall(dir)
496     rmdir(tgt)
497     os.rename(src, tgt)
498     return tgt
499
500 def _is_compiled(dst, loc):
501     present = True
502     for name in loc['result']:
503         present = present and exists(os.path.join(dst, name))
504     return present
505
506 def build_deplibs(config, basedir):
507     mkdir_p(os.path.join(basedir, config))
508
509     dstdir = os.path.join(basedir, config, 'deplibs')
510     vars   = {'destdir': dstdir, 'mingw-w64': MINGW_W64_PREFIX.get(rchop(config, '-dbg'), '')}
511     for lib in sorted(DEPENDENT_LIBS, key=lambda x: DEPENDENT_LIBS[x]['order']):
512         cfg = None
513         for key in DEPENDENT_LIBS[lib]['build']:
514             if fnmatch.fnmatch(config, key):
515                 cfg = key
516
517         if not cfg or _is_compiled(dstdir, DEPENDENT_LIBS[lib]['build'][cfg]):
518             continue
519
520         build_cfg = DEPENDENT_LIBS[lib]['build'][cfg]
521         message('========== building: %s\n' % lib)
522         srcdir = download_tarball(DEPENDENT_LIBS[lib]['url'], DEPENDENT_LIBS[lib]['sha1'],
523                                   basedir, os.path.join(config, lib))
524
525         for location, source, target in build_cfg.get('replace', []):
526             data = open(os.path.join(srcdir, location), 'r').read()
527             open(os.path.join(srcdir, location), 'w').write(data.replace(source, target % vars))
528
529         os.chdir(srcdir)
530         for command in build_cfg['commands']:
531             shell(command % vars)
532         if not type(build_cfg['result']) is list:
533             for target in build_cfg['result']:
534                 mkdir_p(os.path.dirname(os.path.join(dstdir, target)))
535                 shutil.copy(build_cfg['result'][target], os.path.join(dstdir, target))
536         os.chdir(dstdir)
537         if not _is_compiled(dstdir, build_cfg):
538             error("Unable to compile %s for your system, aborting." % lib)
539
540         rmdir(srcdir)
541
542 def check_running_on_debian():
543     if not sys.platform.startswith('linux') or not exists('/etc/apt/sources.list'):
544         error('This can only be run on a Debian/Ubuntu distribution, aborting.')
545
546     if os.geteuid() != 0:
547         error('This script must be run as root.')
548
549     if platform.architecture()[0] == '64bit' and 'amd64' not in ARCH:
550         ARCH.insert(0, 'amd64')
551
552 PACKAGE_NAME = re.compile(r'ii\s+(.+?)\s+.*')
553 def install_packages(*names):
554     lines = get_output('dpkg-query', '--list').split('\n')
555     avail = [PACKAGE_NAME.match(line).group(1) for line in lines if PACKAGE_NAME.match(line)]
556     inst  = [name for name in names if name in avail]
557
558     if len(inst) != len(names):
559         shell('apt-get update')
560         shell('apt-get install --assume-yes %s' % (' '.join(names)))
561
562 # --------------------------------------------------------------- Linux chroot
563
564 ARCH = ['i386']
565
566 def check_setup_schroot(config):
567     check_running_on_debian()
568     login = os.environ.get('SUDO_USER') or get_output('logname')
569     if not login or login == 'root':
570         error('Unable to determine the login for which schroot access is to be given.')
571
572 def build_setup_schroot(config, basedir):
573     install_packages('git', 'debootstrap', 'schroot', 'rinse', 'debian-archive-keyring')
574     os.environ['HOME'] = '/tmp' # workaround bug in gem when home directory doesn't exist
575
576     login  = os.environ.get('SUDO_USER') or get_output('logname')
577     chroot = config[1+config.rindex('-'):]
578     for arch in ARCH:
579         message('******************* %s-%s\n' % (chroot, arch))
580         base_dir = os.environ.get('WKHTMLTOX_CHROOT') or '/var/chroot'
581         root_dir = os.path.join(base_dir, 'wkhtmltopdf-%s-%s' % (chroot, arch))
582         rmdir(root_dir)
583         mkdir_p(root_dir)
584         for command in CHROOT_SETUP[chroot]:
585             # handle architecture-specific commands
586             name = command[0]
587             if ':' in name:
588                 if name[1+name.rindex(':'):] != arch:
589                     continue
590                 else:
591                     name = name[:name.rindex(':')]
592
593             # handle commands
594             if name == 'debootstrap':
595                 shell('debootstrap --arch=%(arch)s --variant=buildd %(distro)s %(dir)s %(url)s' % {
596                     'arch': arch, 'dir': root_dir, 'distro': command[1], 'url': command[2] })
597             elif name == 'rinse':
598                 cmd = (arch == 'i386' and 'linux32 rinse' or 'rinse')
599                 shell('%s --arch %s --distribution %s --directory %s' % (cmd, arch, command[1], root_dir))
600             elif name == 'shell':
601                 cmd = (arch == 'i386' and 'linux32 chroot' or 'chroot')
602                 shell('%s %s %s' % (cmd, root_dir, command[1]))
603             elif name == 'write_file':
604                 open(os.path.join(root_dir, command[1]), 'w').write(command[2].strip())
605             elif name == 'append_file':
606                 open(os.path.join(root_dir, command[1]), 'a').write(command[2].strip())
607             elif name == 'download_file':
608                 name = command[1].split('/')[-1]
609                 loc  = os.path.join(root_dir, command[2], name)
610                 if exists(loc): os.remove(loc)
611                 def hook(cnt, bs, total):
612                     pct = int(cnt*bs*100/total)
613                     message("\rDownloading: %s [%d%%]" % (name, pct))
614                 urllib.urlretrieve(command[1], loc, reporthook=hook)
615                 message("\rDownloaded: %s%s\n" % (name, ' '*10))
616             elif name == 'schroot_conf':
617                 cfg = open('/etc/schroot/chroot.d/wkhtmltopdf-%s-%s' % (chroot, arch), 'w')
618                 cfg.write('[wkhtmltopdf-%s-%s]\n' % (chroot, arch))
619                 cfg.write('type=directory\ndirectory=%s/\n' % root_dir)
620                 cfg.write('description=%s %s for wkhtmltopdf\n' % (command[1], arch))
621                 cfg.write('users=%s\nroot-users=root\n' % login)
622                 if arch == 'i386' and 'amd64' in ARCH:
623                     cfg.write('personality=linux32\n')
624                 cfg.close()
625
626 def check_update_schroot(config):
627     check_running_on_debian()
628     if not get_output('schroot', '--list'):
629         error('Unable to determine the list of available schroots.')
630
631 def build_update_schroot(config, basedir):
632     for name in get_output('schroot', '--list').split('\n'):
633         message('******************* %s\n' % name[name.index('wkhtmltopdf-'):])
634         shell('schroot -c %s -- /bin/bash /update.sh' % name[name.index('wkhtmltopdf-'):])
635
636 def check_setup_mingw64(config):
637     check_running_on_debian()
638
639 def build_setup_mingw64(config, basedir):
640     install_packages('build-essential', 'mingw-w64', 'nsis')
641
642 def check_source_tarball(config):
643     if not get_output('git', 'rev-parse', '--short', 'HEAD'):
644         error("This can only be run inside a git checkout.")
645
646     if not exists(os.path.join(os.getcwd(), 'qt', '.git')):
647         error("Please initialize and download the Qt submodule before running this.")
648
649 def _filter_tar(info):
650     name = info.name[1+info.name.index('/'):]
651     if name.endswith('.git') or [p for p in EXCLUDE_SRC_TARBALL if fnmatch.fnmatch(name, p)]:
652         return None
653
654     info.uid   = info.gid   = 1000
655     info.uname = info.gname = 'wkhtmltopdf'
656     return info
657
658 def build_source_tarball(config, basedir):
659     version, simple_version = get_version(basedir)
660     root_dir = os.path.realpath(os.path.join(basedir, '..'))
661     os.chdir(os.path.join(root_dir, 'qt'))
662     shell('git clean -fdx')
663     shell('git reset --hard HEAD')
664     os.chdir(root_dir)
665     shell('git clean -fdx')
666     shell('git reset --hard HEAD')
667     shell('git submodule update')
668     with tarfile.open('wkhtmltox-%s.tar.bz2' % version, 'w:bz2') as tar:
669         tar.add('.', 'wkhtmltox-%s/' % version, filter=_filter_tar)
670
671 # --------------------------------------------------------------- MSVC (2008-2013)
672
673 MSVC_LOCATION = {
674     'msvc2008': 'VS90COMNTOOLS',
675     'msvc2010': 'VS100COMNTOOLS',
676     'msvc2012': 'VS110COMNTOOLS',
677     'msvc2013': 'VS120COMNTOOLS'
678 }
679
680 def check_msvc(config):
681     version, arch = rchop(config, '-dbg').split('-')
682     env_var = MSVC_LOCATION[version]
683     if not env_var in os.environ:
684         error("%s does not seem to be installed." % version)
685
686     vcdir = os.path.join(os.environ[env_var], '..', '..', 'VC')
687     if not exists(os.path.join(vcdir, 'vcvarsall.bat')):
688         error("%s: unable to find vcvarsall.bat" % version)
689
690     if arch == 'win32' and not exists(os.path.join(vcdir, 'bin', 'cl.exe')):
691         error("%s: unable to find the x86 compiler" % version)
692
693     if arch == 'win64' and not exists(os.path.join(vcdir, 'bin', 'amd64', 'cl.exe')) \
694                        and not exists(os.path.join(vcdir, 'bin', 'x86_amd64', 'cl.exe')):
695         error("%s: unable to find the amd64 compiler" % version)
696
697 def build_msvc(config, basedir):
698     msvc, arch = rchop(config, '-dbg').split('-')
699     vcdir = os.path.join(os.environ[MSVC_LOCATION[msvc]], '..', '..', 'VC')
700     vcarg = 'x86'
701     if arch == 'win64':
702         if exists(os.path.join(vcdir, 'bin', 'amd64', 'cl.exe')):
703             vcarg = 'amd64'
704         else:
705             vcarg = 'x86_amd64'
706
707     python = sys.executable
708     process = subprocess.Popen('("%s" %s>nul)&&"%s" -c "import os, sys; sys.stdout.write(repr(dict(os.environ)))"' % (
709         os.path.join(vcdir, 'vcvarsall.bat'), vcarg, python), stdout=subprocess.PIPE, shell=True)
710     stdout, _ = process.communicate()
711     exitcode = process.wait()
712     if exitcode != 0:
713         error("%s: unable to initialize the environment" % msvc)
714
715     os.environ.update(eval(stdout.strip()))
716
717     build_msvc_common(config, basedir)
718
719 # --------------------------------------------------------------- MSVC via Windows SDK 7.1
720
721 def check_msvc_winsdk71(config):
722     for pfile in ['ProgramFiles(x86)', 'ProgramFiles']:
723         if pfile in os.environ and exists(os.path.join(os.environ[pfile], 'Microsoft SDKs', 'Windows', 'v7.1', 'Bin', 'SetEnv.cmd')):
724             return
725     error("Unable to detect the location of Windows SDK 7.1")
726
727 def build_msvc_winsdk71(config, basedir):
728     arch = config[config.rindex('-'):]
729     setenv = None
730     for pfile in ['ProgramFiles(x86)', 'ProgramFiles']:
731         if not pfile in os.environ:
732             continue
733         setenv = os.path.join(os.environ[pfile], 'Microsoft SDKs', 'Windows', 'v7.1', 'Bin', 'SetEnv.cmd')
734
735     mode = debug and '/Debug' or '/Release'
736     if arch == 'win64':
737         args = '/2008 /x64 %s' % mode
738     else:
739         args = '/2008 /x86 %s' % mode
740
741     python = sys.executable
742     process = subprocess.Popen('("%s" %s>nul)&&"%s" -c "import os, sys; sys.stdout.write(repr(dict(os.environ)))"' % (
743         setenv, args, python), stdout=subprocess.PIPE, shell=True)
744     stdout, _ = process.communicate()
745     exitcode = process.wait()
746     if exitcode != 0:
747         error("unable to initialize the environment for Windows SDK 7.1")
748
749     os.environ.update(eval(stdout.strip()))
750
751     build_msvc_common(config, basedir)
752
753 def build_msvc_common(config, basedir):
754     version, simple_version = get_version(basedir)
755     build_deplibs(config, basedir)
756
757     libdir = os.path.join(basedir, config, 'deplibs')
758     qtdir  = os.path.join(basedir, config, 'qt')
759     mkdir_p(qtdir)
760
761     configure_args = qt_config('msvc',
762         '-I %s\\include' % libdir,
763         '-L %s\\lib' % libdir,
764         'OPENSSL_LIBS="-L%s\\\\lib -lssleay32 -llibeay32 -lUser32 -lAdvapi32 -lGdi32 -lCrypt32"' % libdir.replace('\\', '\\\\'))
765
766     os.chdir(qtdir)
767     if not exists('is_configured'):
768         shell('%s\\..\\qt\\configure.exe %s' % (basedir, configure_args))
769         open('is_configured', 'w').write('')
770     shell('nmake')
771
772     appdir = os.path.join(basedir, config, 'app')
773     mkdir_p(appdir)
774     os.chdir(appdir)
775     rmdir('bin')
776     mkdir_p('bin')
777
778     os.environ['WKHTMLTOX_VERSION'] = version
779
780     shell('%s\\bin\\qmake %s\\..\\wkhtmltopdf.pro' % (qtdir, basedir))
781     shell('nmake')
782
783     found = False
784     for pfile in ['ProgramFiles(x86)', 'ProgramFiles']:
785         if not pfile in os.environ or not exists(os.path.join(os.environ[pfile], 'NSIS', 'makensis.exe')):
786             continue
787         found = True
788
789         makensis = os.path.join(os.environ[pfile], 'NSIS', 'makensis.exe')
790         os.chdir(os.path.join(basedir, '..'))
791         shell('"%s" /DVERSION=%s /DSIMPLE_VERSION=%s /DTARGET=%s wkhtmltox.nsi' % \
792                 (makensis, version, simple_version, config))
793
794     if not found:
795         message("\n\nCould not build installer as NSIS was not found.\n")
796
797 # ------------------------------------------------ MinGW-W64 Cross Environment
798
799 MINGW_W64_PREFIX = {
800     'mingw-w64-cross-win32' : 'i686-w64-mingw32',
801     'mingw-w64-cross-win64' : 'x86_64-w64-mingw32',
802 }
803
804 def check_mingw64_cross(config):
805     shell('%s-gcc --version' % MINGW_W64_PREFIX[rchop(config, '-dbg')])
806
807 def build_mingw64_cross(config, basedir):
808     version, simple_version = get_version(basedir)
809     build_deplibs(config, basedir)
810
811     libdir = os.path.join(basedir, config, 'deplibs')
812     qtdir  = os.path.join(basedir, config, 'qt')
813
814     configure_args = qt_config('mingw-w64-cross',
815         '--prefix=%s'   % qtdir,
816         '-I %s/include' % libdir,
817         '-L %s/lib'     % libdir,
818         '-device-option CROSS_COMPILE=%s-' % MINGW_W64_PREFIX[rchop(config, '-dbg')])
819
820     os.environ['OPENSSL_LIBS'] = '-lssl -lcrypto -L %s/lib -lws2_32 -lgdi32 -lcrypt32' % libdir
821
822     mkdir_p(qtdir)
823     os.chdir(qtdir)
824
825     if not exists('is_configured'):
826         for var in ['CFLAGS', 'CXXFLAGS']:
827             os.environ[var] = '-w'
828         shell('%s/../qt/configure %s' % (basedir, configure_args))
829         shell('touch is_configured')
830     shell('make -j%d' % CPU_COUNT)
831
832     appdir = os.path.join(basedir, config, 'app')
833     mkdir_p(appdir)
834     os.chdir(appdir)
835     shell('rm -f bin/*')
836
837     # set up cross compiling prefix correctly
838     os.environ['WKHTMLTOX_VERSION'] = version
839     shell('%s/bin/qmake -set CROSS_COMPILE %s-' % (qtdir, MINGW_W64_PREFIX[rchop(config, '-dbg')]))
840     shell('%s/bin/qmake -spec win32-g++-4.6 %s/../wkhtmltopdf.pro' % (qtdir, basedir))
841     shell('make')
842     shutil.copy('bin/libwkhtmltox0.a', 'bin/wkhtmltox.lib')
843
844     os.chdir(os.path.join(basedir, '..'))
845     shell('makensis -DVERSION=%s -DSIMPLE_VERSION=%s -DTARGET=%s wkhtmltox.nsi' % \
846             (version, simple_version, config))
847
848 # -------------------------------------------------- Linux schroot environment
849
850 def check_linux_schroot(config):
851     shell('schroot -c wkhtmltopdf-%s -- gcc --version' % rchop(config, '-dbg'))
852
853 def build_linux_schroot(config, basedir):
854     version, simple_version = get_version(basedir)
855
856     dir    = os.path.join(basedir, config)
857     script = os.path.join(dir, 'build.sh')
858     dist   = os.path.join(dir, 'wkhtmltox-%s' % version)
859
860     mkdir_p(dir)
861     rmdir(dist)
862
863     configure_args = qt_config('posix', '--prefix=%s' % os.path.join(dir, 'qt'))
864
865     lines = ['#!/bin/bash']
866     lines.append('# start of autogenerated build script')
867     lines.append('mkdir -p app qt')
868     lines.append('cd qt')
869     if config == 'centos5-i386':
870         lines.append('export CFLAGS="-march=i486 -w"')
871         lines.append('export CXXFLAGS="-march=i486 -w"')
872     else:
873         for var in ['CFLAGS', 'CXXFLAGS']:
874             lines.append('export %s="-w"' % var)
875     lines.append('if [ ! -f is_configured ]; then')
876     lines.append('  ../../../qt/configure %s || exit 1' % configure_args)
877     lines.append('  touch is_configured')
878     lines.append('fi')
879     lines.append('if ! make -j%d -q; then\n  make -j%d || exit 1\nfi' % (CPU_COUNT, CPU_COUNT))
880     lines.append('cd ../app')
881     lines.append('rm -f bin/*')
882     lines.append('export WKHTMLTOX_VERSION=%s' % version)
883     lines.append('../qt/bin/qmake ../../../wkhtmltopdf.pro')
884     lines.append('make install INSTALL_ROOT=%s || exit 1' % dist)
885     lines.append('cd ..')
886     lines.append('tar -c -v -f ../wkhtmltox-%s_linux-%s.tar wkhtmltox-%s/' % (version, config, version))
887     lines.append('xz --compress --force --verbose -9 ../wkhtmltox-%s_linux-%s.tar' % (version, config))
888     lines.append('# end of build script')
889
890     open(script, 'w').write('\n'.join(lines))
891     os.chdir(dir)
892     shell('chmod +x build.sh')
893     shell('schroot -c wkhtmltopdf-%s -- ./build.sh' % rchop(config, '-dbg'))
894
895
896 # -------------------------------------------------- POSIX local environment
897
898 def check_posix_local(config):
899     pass
900
901 def build_posix_local(config, basedir):
902     version, simple_version = get_version(basedir)
903
904     app    = os.path.join(basedir, config, 'app')
905     qtdir  = os.path.join(basedir, config, 'qt')
906     dist   = os.path.join(basedir, config, 'wkhtmltox-%s' % version)
907     make   = get_output('which gmake') and 'gmake' or 'make'
908
909     mkdir_p(qt)
910     mkdir_p(app)
911
912     rmdir(dist)
913     mkdir_p(os.path.join(dist, 'bin'))
914     mkdir_p(os.path.join(dist, 'include', 'wkhtmltox'))
915     mkdir_p(os.path.join(dist, 'lib'))
916
917     os.chdir(qt)
918     if not exists('is_configured'):
919         shell('../../../qt/configure %s' % qt_config('posix', '--prefix=%s' % qtdir))
920         shell('touch is_configured')
921
922     if subprocess.call([make, '-j%d' % CPU_COUNT]):
923         shell('%s -j%d' % (make, CPU_COUNT))
924
925     os.chdir(app)
926     shell('rm -f bin/*')
927     os.environ['WKHTMLTOX_VERSION'] = version
928     shell('../qt/bin/qmake ../../../wkhtmltopdf.pro')
929     shell('%s -j%d' % (make, CPU_COUNT))
930     shell('cp bin/wkhtmlto* ../wkhtmltox-%s/bin' % version)
931     shell('cp -P bin/libwkhtmltox*.so.* ../wkhtmltox-%s/lib' % version)
932     shell('cp ../../../include/wkhtmltox/*.h ../wkhtmltox-%s/include/wkhtmltox' % version)
933     shell('cp ../../../include/wkhtmltox/dll*.inc ../wkhtmltox-%s/include/wkhtmltox' % version)
934
935     os.chdir(basedir)
936     shell('tar -c -v -f ../wkhtmltox-%s_local-%s.tar wkhtmltox-%s/' % (version, platform.node(), version))
937     shell('xz --compress --force --verbose -9 ../wkhtmltox-%s_local-%s.tar' % (version, platform.node()))
938
939 # --------------------------------------------------------------- OS X
940
941 def check_osx(config):
942     if not platform.system() == 'Darwin' or not platform.mac_ver()[0]:
943         error('This can only be run on a OS X system!')
944
945     if not get_output('xcode-select', '--print-path'):
946         error('Xcode is not installed, aborting.')
947
948 def build_osx(config, basedir):
949     version, simple_version = get_version(basedir)
950
951     osxver    = platform.mac_ver()[0][:platform.mac_ver()[0].rindex('.')]
952     framework = config.split('-')[1]
953     if osxver == '10.6':
954         osxcfg = '-%s -platform macx-g++42' % framework
955     else:
956         osxcfg = '-%s -platform unsupported/macx-clang' % framework
957
958     flags = ''
959     if framework == 'carbon' and osxver != '10.6':
960         for item in ['CFLAGS', 'CXXFLAGS']:
961             flags += '"QMAKE_%s += %s" ' % (item, '-fvisibility=hidden -fvisibility-inlines-hidden')
962
963     qt     = os.path.join(basedir, config, 'qt')
964     app    = os.path.join(basedir, config, 'app')
965     dist   = os.path.join(basedir, config, 'wkhtmltox-%s' % version)
966
967     mkdir_p(qt)
968     mkdir_p(app)
969
970     rmdir(dist)
971     mkdir_p(os.path.join(dist, 'bin'))
972     mkdir_p(os.path.join(dist, 'include', 'wkhtmltox'))
973     mkdir_p(os.path.join(dist, 'lib'))
974
975     os.chdir(qt)
976     if not exists('is_configured'):
977         shell('../../../qt/configure %s' % qt_config('osx', '--prefix=%s' % qt, osxcfg))
978         shell('touch is_configured')
979
980     shell('make -j%d' % CPU_COUNT)
981
982     os.chdir(app)
983     shell('rm -f bin/*')
984     os.environ['WKHTMLTOX_VERSION'] = version
985     shell('../qt/bin/qmake %s ../../../wkhtmltopdf.pro' % flags)
986     shell('make -j%d' % CPU_COUNT)
987
988     if osxver not in ['10.6', '10.7']:
989         for item in ['wkhtmltoimage', 'wkhtmltopdf', 'libwkhtmltox.%s.dylib' % simple_version]:
990             shell(' '.join([
991                 'install_name_tool', '-change',
992                 '/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText',
993                 '/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/CoreText.framework/CoreText',
994                 'bin/'+item]))
995
996     shell('cp bin/wkhtmlto* ../wkhtmltox-%s/bin' % version)
997     shell('cp -P bin/libwkhtmltox*.dylib* ../wkhtmltox-%s/lib' % version)
998     shell('cp ../../../include/wkhtmltox/*.h ../wkhtmltox-%s/include/wkhtmltox' % version)
999     shell('cp ../../../include/wkhtmltox/dll*.inc ../wkhtmltox-%s/include/wkhtmltox' % version)
1000
1001     os.chdir(os.path.join(basedir, config))
1002     shell('tar -c -v -f ../wkhtmltox-%s_%s.tar wkhtmltox-%s/' % (version, config, version))
1003     shell('xz --compress --force --verbose -9 ../wkhtmltox-%s_%s.tar' % (version, config))
1004
1005 # --------------------------------------------------------------- command line
1006
1007 def usage(exit_code=2):
1008     message("Usage: scripts/build.py <target> [-clean] [-debug]\n\nThe supported targets are:\n")
1009     opts = list(BUILDERS.keys())
1010     opts.sort()
1011     for opt in opts:
1012         message('* %s\n' % opt)
1013     sys.exit(exit_code)
1014
1015 def main():
1016     rootdir = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
1017     basedir = os.path.join(rootdir, 'static-build')
1018     if len(sys.argv) == 1:
1019         usage(0)
1020
1021     config = sys.argv[1]
1022     if config not in BUILDERS:
1023         usage()
1024
1025     for arg in sys.argv[2:]:
1026         if not arg in ['-clean', '-debug']:
1027             usage()
1028
1029     final_config = config
1030     if '-debug' in sys.argv[2:]:
1031         final_config += '-dbg'
1032         QT_CONFIG['common'].extend(['remove:-release', 'remove:-webkit', '-debug', '-webkit-debug'])
1033
1034     if '-clean' in sys.argv[2:]:
1035         rmdir(os.path.join(basedir, config))
1036
1037     os.chdir(rootdir)
1038     globals()['check_%s' % BUILDERS[config]](final_config)
1039     globals()['build_%s' % BUILDERS[config]](final_config, basedir)
1040
1041 if __name__ == '__main__':
1042     main()