1#!/usr/bin/env python
2# coding: UTF-8
3
4'''This scirpt builds the seafile windows msi installer.
5
6Some notes:
7
81. The working directory is always the 'builddir'. 'os.chdir' is only called
9to change to the 'builddir'. We make use of the 'cwd' argument in
10'subprocess.Popen' to run a command in a specific directory.
11
122. When invoking commands like 'tar', we must convert the path to posix path with the function to_mingw_path. E.g., 'c:\\seafile' should be converted to '/c/seafile'.
13
14'''
15
16import sys
17
18####################
19### Requires Python 2.6+
20####################
21if sys.version_info[0] == 3:
22    print 'Python 3 not supported yet. Quit now.'
23    sys.exit(1)
24if sys.version_info[1] < 6:
25    print 'Python 2.6 or above is required. Quit now.'
26    sys.exit(1)
27
28import multiprocessing
29import os
30import glob
31import shutil
32import re
33import subprocess
34import optparse
35import atexit
36import csv
37import time
38
39error_exit = False
40####################
41### Global variables
42####################
43
44# command line configuartion
45conf = {}
46
47# The retry times when sign programs
48RETRY_COUNT = 3
49
50# key names in the conf dictionary.
51CONF_VERSION            = 'version'
52CONF_LIBSEARPC_VERSION  = 'libsearpc_version'
53CONF_SEAFILE_VERSION    = 'seafile_version'
54CONF_SEAFILE_CLIENT_VERSION  = 'seafile_client_version'
55CONF_SRCDIR             = 'srcdir'
56CONF_KEEP               = 'keep'
57CONF_BUILDDIR           = 'builddir'
58CONF_OUTPUTDIR          = 'outputdir'
59CONF_DEBUG              = 'debug'
60CONF_ONLY_CHINESE       = 'onlychinese'
61CONF_QT_ROOT            = 'qt_root'
62CONF_EXTRA_LIBS_DIR     = 'extra_libs_dir'
63CONF_QT5                = 'qt5'
64CONF_BRAND              = 'brand'
65CONF_CERTFILE           = 'certfile'
66CONF_NO_STRIP           = 'nostrip'
67
68####################
69### Common helper functions
70####################
71def to_mingw_path(path):
72    if len(path) < 2 or path[1] != ':' :
73        return path.replace('\\', '/')
74
75    drive = path[0]
76    return '/%s%s' % (drive.lower(), path[2:].replace('\\', '/'))
77
78def to_win_path(path):
79    if len(path) < 2 or path[1] == ':' :
80        return path.replace('/', '\\')
81
82    drive = path[1]
83    return '%s:%s' % (drive.lower(), path[2:].replace('/', '\\'))
84
85def highlight(content, is_error=False):
86    '''Add ANSI color to content to get it highlighted on terminal'''
87    dummy = is_error
88    return content
89    # if is_error:
90    #     return '\x1b[1;31m%s\x1b[m' % content
91    # else:
92    #     return '\x1b[1;32m%s\x1b[m' % content
93
94def info(msg):
95    print highlight('[INFO] ') + msg
96
97def find_in_path(prog):
98    '''Test whether prog exists in system path'''
99    dirs = os.environ['PATH'].split(';')
100    for d in dirs:
101        if d == '':
102            continue
103        path = os.path.join(d, prog)
104        if os.path.exists(path):
105            return path
106
107    return None
108
109def prepend_env_value(name, value, seperator=':'):
110    '''prepend a new value to a list'''
111    try:
112        current_value = os.environ[name]
113    except KeyError:
114        current_value = ''
115
116    new_value = value
117    if current_value:
118        new_value += seperator + current_value
119
120    os.environ[name] = new_value
121
122def error(msg=None, usage=None):
123    if msg:
124        print highlight('[ERROR] ') + msg
125    if usage:
126        print usage
127    sys.exit(1)
128
129def run_argv(argv, cwd=None, env=None, suppress_stdout=False, suppress_stderr=False):
130    '''Run a program and wait it to finish, and return its exit code. The
131    standard output of this program is supressed.
132
133    '''
134    with open(os.devnull, 'w') as devnull:
135        if suppress_stdout:
136            stdout = devnull
137        else:
138            stdout = sys.stdout
139
140        if suppress_stderr:
141            stderr = devnull
142        else:
143            stderr = sys.stderr
144
145        proc = subprocess.Popen(argv,
146                                cwd=cwd,
147                                stdout=stdout,
148                                stderr=stderr,
149                                env=env)
150        return proc.wait()
151
152def run(cmdline, cwd=None, env=None, suppress_stdout=False, suppress_stderr=False):
153    '''Like run_argv but specify a command line string instead of argv'''
154    info('running %s, cwd=%s' % (cmdline, cwd if cwd else os.getcwd()))
155    with open(os.devnull, 'w') as devnull:
156        if suppress_stdout:
157            stdout = devnull
158        else:
159            stdout = sys.stdout
160
161        if suppress_stderr:
162            stderr = devnull
163        else:
164            stderr = sys.stderr
165
166        proc = subprocess.Popen(cmdline,
167                                cwd=cwd,
168                                stdout=stdout,
169                                stderr=stderr,
170                                env=env,
171                                shell=True)
172        ret = proc.wait()
173        if 'depend' not in cmdline and ret != 0:
174            global error_exit
175            error_exit = True
176        return ret
177
178def must_mkdir(path):
179    '''Create a directory, exit on failure'''
180    try:
181        os.makedirs(path)
182    except OSError, e:
183        error('failed to create directory %s:%s' % (path, e))
184
185def must_copy(src, dst):
186    '''Copy src to dst, exit on failure'''
187    try:
188        shutil.copy(src, dst)
189    except Exception, e:
190        error('failed to copy %s to %s: %s' % (src, dst, e))
191
192def must_copytree(src, dst):
193    '''Copy dir src to dst, exit on failure'''
194    try:
195        shutil.copytree(src, dst)
196    except Exception, e:
197        error('failed to copy dir %s to %s: %s' % (src, dst, e))
198
199def must_move(src, dst):
200    '''Move src to dst, exit on failure'''
201    try:
202        shutil.move(src, dst)
203    except Exception, e:
204        error('failed to move %s to %s: %s' % (src, dst, e))
205
206class Project(object):
207    '''Base class for a project'''
208    # Probject name, i.e. libseaprc/seafile/seahub
209    name = ''
210
211    # A list of shell commands to configure/build the project
212    build_commands = []
213
214    def __init__(self):
215        self.prefix = os.path.join(conf[CONF_BUILDDIR], 'usr')
216        self.version = self.get_version()
217        self.src_tarball = os.path.join(conf[CONF_SRCDIR],
218                            '%s-%s.tar.gz' % (self.name, self.version))
219
220        # project dir, like <builddir>/seafile-1.2.2/
221        self.projdir = os.path.join(conf[CONF_BUILDDIR], '%s-%s' % (self.name, self.version))
222
223    def get_version(self):
224        # libsearpc can have different versions from seafile.
225        raise NotImplementedError
226
227    def get_source_commit_id(self):
228        '''By convetion, we record the commit id of the source code in the
229        file "<projdir>/latest_commit"
230
231        '''
232        latest_commit_file = os.path.join(self.projdir, 'latest_commit')
233        with open(latest_commit_file, 'r') as fp:
234            commit_id = fp.read().strip('\n\r\t ')
235
236        return commit_id
237
238    def append_cflags(self, macros):
239        cflags = ' '.join([ '-D%s=%s' % (k, macros[k]) for k in macros ])
240        prepend_env_value('CPPFLAGS',
241                          cflags,
242                          seperator=' ')
243
244    def uncompress(self):
245        '''Uncompress the source from the tarball'''
246        info('Uncompressing %s' % self.name)
247
248        tarball = to_mingw_path(self.src_tarball)
249        if run('tar xf %s' % tarball) != 0:
250            error('failed to uncompress source of %s' % self.name)
251
252    def before_build(self):
253        '''Hook method to do project-specific stuff before running build commands'''
254        pass
255
256    def build(self):
257        '''Build the source'''
258        self.before_build()
259        info('Building %s' % self.name)
260        dump_env()
261        for cmd in self.build_commands:
262            if run(cmd, cwd=self.projdir) != 0:
263                error('error when running command:\n\t%s\n' % cmd)
264
265def get_make_path():
266    return find_in_path('make.exe')
267
268def concurrent_make():
269    return '%s -j%s' % (get_make_path(), multiprocessing.cpu_count())
270
271class Libsearpc(Project):
272    name = 'libsearpc'
273
274    def __init__(self):
275        Project.__init__(self)
276        self.build_commands = [
277            'sh ./configure --prefix=%s --disable-compile-demo' % to_mingw_path(self.prefix),
278            concurrent_make(),
279            '%s install' % get_make_path(),
280        ]
281
282    def get_version(self):
283        return conf[CONF_LIBSEARPC_VERSION]
284
285class Seafile(Project):
286    name = 'seafile'
287    def __init__(self):
288        Project.__init__(self)
289        enable_breakpad = '--enable-breakpad'
290        self.build_commands = [
291            'sh ./configure %s --prefix=%s' % (enable_breakpad, to_mingw_path(self.prefix)),
292            concurrent_make(),
293            '%s install' % get_make_path(),
294        ]
295
296    def get_version(self):
297        return conf[CONF_SEAFILE_VERSION]
298
299    def before_build(self):
300        macros = {}
301        # SET SEAFILE_SOURCE_COMMIT_ID, so it can be printed in the log
302        macros['SEAFILE_SOURCE_COMMIT_ID'] = '\\"%s\\"' % self.get_source_commit_id()
303        self.append_cflags(macros)
304
305class SeafileClient(Project):
306    name = 'seafile-client'
307    def __init__(self):
308        Project.__init__(self)
309        ninja = find_in_path('ninja.exe')
310        seafile_prefix = Seafile().prefix
311        generator = 'Ninja' if ninja else 'MSYS Makefiles'
312        build_type = 'Debug' if conf[CONF_DEBUG] else 'Release'
313        flags = {
314            'BUILD_SPARKLE_SUPPORT': 'ON',
315            'USE_QT5': 'ON' if conf[CONF_QT5] else 'OFF',
316            'BUILD_SHIBBOLETH_SUPPORT': 'ON',
317            'CMAKE_BUILD_TYPE': build_type,
318            'CMAKE_INSTALL_PREFIX': to_mingw_path(self.prefix),
319            # ninja invokes cmd.exe which doesn't support msys/mingw path
320            # change the value but don't override CMAKE_EXE_LINKER_FLAGS,
321            # which is in use
322            'CMAKE_EXE_LINKER_FLAGS_%s' % build_type.upper(): '-L%s' % (os.path.join(seafile_prefix, 'lib') if ninja else to_mingw_path(os.path.join(seafile_prefix, 'lib'))),
323        }
324        flags_str = ' '.join(['-D%s=%s' % (k, v) for k, v in flags.iteritems()])
325        make = ninja or concurrent_make()
326        self.build_commands = [
327            'cmake -G "%s" %s .' % (generator, flags_str),
328            make,
329            '%s install' % make,
330            "bash extensions/build.sh",
331        ]
332
333    def get_version(self):
334        return conf[CONF_SEAFILE_CLIENT_VERSION]
335
336    def before_build(self):
337        shutil.copy(os.path.join(conf[CONF_EXTRA_LIBS_DIR], 'winsparkle.lib'), self.projdir)
338
339class SeafileShellExt(Project):
340    name = 'seafile-shell-ext'
341    def __init__(self):
342        Project.__init__(self)
343        self.build_commands = [
344            "bash extensions/build.sh",
345            "bash shellext-fix/build.sh",
346        ]
347
348    def get_version(self):
349        return conf[CONF_SEAFILE_CLIENT_VERSION]
350
351def check_targz_src(proj, version, srcdir):
352    src_tarball = os.path.join(srcdir, '%s-%s.tar.gz' % (proj, version))
353    if not os.path.exists(src_tarball):
354        error('%s not exists' % src_tarball)
355
356def validate_args(usage, options):
357    required_args = [
358        CONF_VERSION,
359        CONF_LIBSEARPC_VERSION,
360        CONF_SEAFILE_VERSION,
361        CONF_SEAFILE_CLIENT_VERSION,
362        CONF_SRCDIR,
363        CONF_QT_ROOT,
364        CONF_EXTRA_LIBS_DIR,
365    ]
366
367    # fist check required args
368    for optname in required_args:
369        if getattr(options, optname, None) == None:
370            error('%s must be specified' % optname, usage=usage)
371
372    def get_option(optname):
373        return getattr(options, optname)
374
375    # [ version ]
376    def check_project_version(version):
377        '''A valid version must be like 1.2.2, 1.3'''
378        if not re.match(r'^[0-9]+(\.[0-9]+)+$', version):
379            error('%s is not a valid version' % version, usage=usage)
380
381    version = get_option(CONF_VERSION)
382    libsearpc_version = get_option(CONF_LIBSEARPC_VERSION)
383    seafile_version = get_option(CONF_SEAFILE_VERSION)
384    seafile_client_version = get_option(CONF_SEAFILE_CLIENT_VERSION)
385    seafile_shell_ext_version = get_option(CONF_SEAFILE_CLIENT_VERSION)
386
387    check_project_version(version)
388    check_project_version(libsearpc_version)
389    check_project_version(seafile_version)
390    check_project_version(seafile_client_version)
391    check_project_version(seafile_shell_ext_version)
392
393    # [ srcdir ]
394    srcdir = to_win_path(get_option(CONF_SRCDIR))
395    check_targz_src('libsearpc', libsearpc_version, srcdir)
396    check_targz_src('seafile', seafile_version, srcdir)
397    check_targz_src('seafile-client', seafile_client_version, srcdir)
398    check_targz_src('seafile-shell-ext', seafile_shell_ext_version, srcdir)
399
400    # [ builddir ]
401    builddir = to_win_path(get_option(CONF_BUILDDIR))
402    if not os.path.exists(builddir):
403        error('%s does not exist' % builddir, usage=usage)
404
405    builddir = os.path.join(builddir, 'seafile-msi-build')
406
407    # [ outputdir ]
408    outputdir = to_win_path(get_option(CONF_OUTPUTDIR))
409    if not os.path.exists(outputdir):
410        error('outputdir %s does not exist' % outputdir, usage=usage)
411
412    # [ keep ]
413    keep = get_option(CONF_KEEP)
414
415    # [ no strip]
416    debug = get_option(CONF_DEBUG)
417
418    # [ no strip]
419    nostrip = get_option(CONF_NO_STRIP)
420
421    # [only chinese]
422    onlychinese = get_option(CONF_ONLY_CHINESE)
423
424    # [ qt root]
425    qt_root = get_option(CONF_QT_ROOT)
426    def check_qt_root(qt_root):
427        if not os.path.exists(os.path.join(qt_root, 'plugins')):
428            error('%s is not a valid qt root' % qt_root)
429    check_qt_root(qt_root)
430
431    # [ sparkle dir]
432    extra_libs_dir = get_option(CONF_EXTRA_LIBS_DIR)
433    def check_extra_libs_dir(extra_libs_dir):
434        for fn in ['winsparkle.lib']:
435            if not os.path.exists(os.path.join(extra_libs_dir, fn)):
436                error('%s is missing in %s' % (fn, extra_libs_dir))
437    check_extra_libs_dir(extra_libs_dir)
438
439    # [qt5]
440    qt5 = get_option(CONF_QT5)
441    brand = get_option(CONF_BRAND)
442    cert = get_option(CONF_CERTFILE)
443    if cert is not None:
444        if not os.path.exists(cert):
445            error('cert file "{}" does not exist'.format(cert))
446
447    conf[CONF_VERSION] = version
448    conf[CONF_LIBSEARPC_VERSION] = libsearpc_version
449    conf[CONF_SEAFILE_VERSION] = seafile_version
450    conf[CONF_SEAFILE_CLIENT_VERSION] = seafile_client_version
451
452    conf[CONF_BUILDDIR] = builddir
453    conf[CONF_SRCDIR] = srcdir
454    conf[CONF_OUTPUTDIR] = outputdir
455    conf[CONF_KEEP] = True
456    conf[CONF_DEBUG] = debug or nostrip
457    conf[CONF_NO_STRIP] = debug or nostrip
458    conf[CONF_ONLY_CHINESE] = onlychinese
459    conf[CONF_QT_ROOT] = qt_root
460    conf[CONF_EXTRA_LIBS_DIR] = extra_libs_dir
461    conf[CONF_QT5] = qt5
462    conf[CONF_BRAND] = brand
463    conf[CONF_CERTFILE] = cert
464
465    prepare_builddir(builddir)
466    show_build_info()
467
468def show_build_info():
469    '''Print all conf information. Confirm before continue.'''
470    info('------------------------------------------')
471    info('Seafile msi installer: BUILD INFO')
472    info('------------------------------------------')
473    info('seafile:                  %s' % conf[CONF_VERSION])
474    info('libsearpc:                %s' % conf[CONF_LIBSEARPC_VERSION])
475    info('seafile:                  %s' % conf[CONF_SEAFILE_VERSION])
476    info('seafile-client:           %s' % conf[CONF_SEAFILE_CLIENT_VERSION])
477    info('qt-root:                  %s' % conf[CONF_QT_ROOT])
478    info('builddir:                 %s' % conf[CONF_BUILDDIR])
479    info('outputdir:                %s' % conf[CONF_OUTPUTDIR])
480    info('source dir:               %s' % conf[CONF_SRCDIR])
481    info('debug:                    %s' % conf[CONF_DEBUG])
482    info('build english version:    %s' % (not conf[CONF_ONLY_CHINESE]))
483    info('clean on exit:            %s' % (not conf[CONF_KEEP]))
484    info('------------------------------------------')
485    info('press any key to continue ')
486    info('------------------------------------------')
487    dummy = raw_input()
488
489def prepare_builddir(builddir):
490    must_mkdir(builddir)
491
492    if not conf[CONF_KEEP]:
493        def remove_builddir():
494            '''Remove the builddir when exit'''
495            if not error_exit:
496                info('remove builddir before exit')
497                shutil.rmtree(builddir, ignore_errors=True)
498        atexit.register(remove_builddir)
499
500    os.chdir(builddir)
501
502def parse_args():
503    parser = optparse.OptionParser()
504    def long_opt(opt):
505        return '--' + opt
506
507    parser.add_option(long_opt(CONF_VERSION),
508                      dest=CONF_VERSION,
509                      nargs=1,
510                      help='the version to build. Must be digits delimited by dots, like 1.3.0')
511
512    parser.add_option(long_opt(CONF_LIBSEARPC_VERSION),
513                      dest=CONF_LIBSEARPC_VERSION,
514                      nargs=1,
515                      help='the version of libsearpc as specified in its "configure.ac". Must be digits delimited by dots, like 1.3.0')
516
517    parser.add_option(long_opt(CONF_SEAFILE_VERSION),
518                      dest=CONF_SEAFILE_VERSION,
519                      nargs=1,
520                      help='the version of seafile as specified in its "configure.ac". Must be digits delimited by dots, like 1.3.0')
521
522    parser.add_option(long_opt(CONF_SEAFILE_CLIENT_VERSION),
523                      dest=CONF_SEAFILE_CLIENT_VERSION,
524                      nargs=1,
525                      help='the version of seafile-client. Must be digits delimited by dots, like 1.3.0')
526
527    parser.add_option(long_opt(CONF_BUILDDIR),
528                      dest=CONF_BUILDDIR,
529                      nargs=1,
530                      help='the directory to build the source. Defaults to /c',
531                      default='c:\\')
532
533    parser.add_option(long_opt(CONF_OUTPUTDIR),
534                      dest=CONF_OUTPUTDIR,
535                      nargs=1,
536                      help='the output directory to put the generated server tarball. Defaults to the current directory.',
537                      default=os.getcwd())
538
539    parser.add_option(long_opt(CONF_SRCDIR),
540                      dest=CONF_SRCDIR,
541                      nargs=1,
542                      help='''Source tarballs must be placed in this directory.''')
543
544    parser.add_option(long_opt(CONF_QT_ROOT),
545                      dest=CONF_QT_ROOT,
546                      nargs=1,
547                      help='''qt root directory.''')
548
549    parser.add_option(long_opt(CONF_EXTRA_LIBS_DIR),
550                      dest=CONF_EXTRA_LIBS_DIR,
551                      nargs=1,
552                      help='''where we can find winsparkle.lib''')
553
554    parser.add_option(long_opt(CONF_KEEP),
555                      dest=CONF_KEEP,
556                      action='store_true',
557                      help='''keep the build directory after the script exits. By default, the script would delete the build directory at exit.''')
558
559    parser.add_option(long_opt(CONF_DEBUG),
560                      dest=CONF_DEBUG,
561                      action='store_true',
562                      help='''compile in debug mode''')
563
564    parser.add_option(long_opt(CONF_ONLY_CHINESE),
565                      dest=CONF_ONLY_CHINESE,
566                      action='store_true',
567                      help='''only build the Chinese version. By default both Chinese and English versions would be built.''')
568
569    parser.add_option(long_opt(CONF_QT5),
570                      dest=CONF_QT5,
571                      action='store_true',
572                      help='''build seafile client with qt5''')
573
574    parser.add_option(long_opt(CONF_BRAND),
575                      dest=CONF_BRAND,
576                      default='seafile',
577                      help='''brand name of the package''')
578
579    parser.add_option(long_opt(CONF_CERTFILE),
580                      nargs=1,
581                      default=None,
582                      dest=CONF_CERTFILE,
583                      help='''The cert for signing the executables and the installer.''')
584
585    parser.add_option(long_opt(CONF_NO_STRIP),
586                      dest=CONF_NO_STRIP,
587                      action='store_true',
588                      help='''do not strip the symbols.''')
589
590    usage = parser.format_help()
591    options, remain = parser.parse_args()
592    if remain:
593        error(usage=usage)
594
595    validate_args(usage, options)
596
597def setup_build_env():
598    '''Setup environment variables, such as export PATH=$BUILDDDIR/bin:$PATH'''
599    prefix = Seafile().prefix
600    prepend_env_value('CPPFLAGS',
601                     '-I%s' % to_mingw_path(os.path.join(prefix, 'include')),
602                     seperator=' ')
603
604    prepend_env_value('CPPFLAGS',
605                     '-DSEAFILE_CLIENT_VERSION=\\"%s\\"' % conf[CONF_VERSION],
606                     seperator=' ')
607
608    prepend_env_value('CPPFLAGS',
609                      '-g -fno-omit-frame-pointer',
610                      seperator=' ')
611    if conf[CONF_DEBUG]:
612        prepend_env_value('CPPFLAGS', '-O0', seperator=' ')
613
614    prepend_env_value('LDFLAGS',
615                     '-L%s' % to_mingw_path(os.path.join(prefix, 'lib')),
616                     seperator=' ')
617
618    prepend_env_value('PATH',
619                      os.path.join(prefix, 'bin'),
620                      seperator=';')
621
622    prepend_env_value('PKG_CONFIG_PATH',
623                      os.path.join(prefix, 'lib', 'pkgconfig'),
624                      seperator=';')
625                      # to_mingw_path(os.path.join(prefix, 'lib', 'pkgconfig')))
626
627    # specifiy the directory for wix temporary files
628    wix_temp_dir = os.path.join(conf[CONF_BUILDDIR], 'wix-temp')
629    os.environ['WIX_TEMP'] = wix_temp_dir
630
631    must_mkdir(wix_temp_dir)
632
633def dependency_walk(applet):
634    output = os.path.join(conf[CONF_BUILDDIR], 'depends.csv')
635    cmd = 'depends.exe -c -f 1 -oc %s %s' % (output, applet)
636
637    # See the manual of Dependency walker
638    if run(cmd) > 0x100:
639        error('failed to run dependency walker for %s' % applet)
640
641    if not os.path.exists(output):
642        error('failed to run dependency walker for %s' % applet)
643
644    shared_libs = parse_depends_csv(output)
645    return shared_libs
646
647def parse_depends_csv(path):
648    '''parse the output of dependency walker'''
649    libs = set()
650    our_libs = ['libsearpc', 'libseafile']
651    def should_ignore_lib(lib):
652        lib = lib.lower()
653        if not os.path.exists(lib):
654            return True
655
656        if lib.startswith('c:\\windows'):
657            return True
658
659        if lib.endswith('exe'):
660            return True
661
662        for name in our_libs:
663            if name in lib:
664                return True
665
666        return False
667
668    with open(path, 'r') as fp:
669        reader = csv.reader(fp)
670        for row in reader:
671            if len(row) < 2:
672                continue
673            lib = row[1]
674            if not should_ignore_lib(lib):
675                libs.add(lib)
676
677    return set(libs)
678
679def copy_shared_libs(exes):
680    '''Copy shared libs need by seafile-applet.exe, such as libsearpc,
681    libseafile, etc. First we use Dependency walker to analyse
682    seafile-applet.exe, and get an output file in csv format. Then we parse
683    the csv file to get the list of shared libs.
684
685    '''
686
687    shared_libs = set()
688    for exectuable in exes:
689        shared_libs.update(dependency_walk(exectuable))
690
691    pack_bin_dir = os.path.join(conf[CONF_BUILDDIR], 'pack', 'bin')
692    for lib in shared_libs:
693        must_copy(lib, pack_bin_dir)
694
695    if not any([os.path.basename(lib).lower().startswith('libssl') for lib in shared_libs]):
696        ssleay32 = find_in_path('ssleay32.dll')
697        must_copy(ssleay32, pack_bin_dir)
698
699def copy_dll_exe():
700    prefix = Seafile().prefix
701    destdir = os.path.join(conf[CONF_BUILDDIR], 'pack', 'bin')
702
703    filelist = [
704        os.path.join(prefix, 'bin', 'libsearpc-1.dll'),
705        os.path.join(prefix, 'bin', 'libseafile-0.dll'),
706        os.path.join(prefix, 'bin', 'seaf-daemon.exe'),
707        os.path.join(SeafileClient().projdir, 'seafile-applet.exe'),
708        os.path.join(SeafileShellExt().projdir, 'shellext-fix', 'shellext-fix.exe'),
709    ]
710
711    for name in filelist:
712        must_copy(name, destdir)
713
714    extdlls = [
715        os.path.join(SeafileShellExt().projdir, 'extensions', 'lib', 'seafile_ext.dll'),
716        os.path.join(SeafileShellExt().projdir, 'extensions', 'lib', 'seafile_ext64.dll'),
717    ]
718
719    customdir = os.path.join(conf[CONF_BUILDDIR], 'pack', 'custom')
720    for dll in extdlls:
721        must_copy(dll, customdir)
722
723    copy_shared_libs([ f for f in filelist if f.endswith('.exe') ])
724    copy_qt_plugins_imageformats()
725    copy_qt_plugins_platforms()
726    copy_qt_translations()
727
728def copy_qt_plugins_imageformats():
729    destdir = os.path.join(conf[CONF_BUILDDIR], 'pack', 'bin', 'imageformats')
730    must_mkdir(destdir)
731
732    qt_plugins_srcdir = os.path.join(conf[CONF_QT_ROOT], 'plugins', 'imageformats')
733
734    src = os.path.join(qt_plugins_srcdir, 'qico4.dll')
735    if conf[CONF_QT5]:
736        src = os.path.join(qt_plugins_srcdir, 'qico.dll')
737    must_copy(src, destdir)
738
739    src = os.path.join(qt_plugins_srcdir, 'qgif4.dll')
740    if conf[CONF_QT5]:
741        src = os.path.join(qt_plugins_srcdir, 'qgif.dll')
742    must_copy(src, destdir)
743
744    src = os.path.join(qt_plugins_srcdir, 'qjpeg.dll')
745    if conf[CONF_QT5]:
746        src = os.path.join(qt_plugins_srcdir, 'qjpeg.dll')
747    must_copy(src, destdir)
748
749def copy_qt_plugins_platforms():
750    if not conf[CONF_QT5]:
751        return
752
753    destdir = os.path.join(conf[CONF_BUILDDIR], 'pack', 'bin', 'platforms')
754    must_mkdir(destdir)
755
756    qt_plugins_srcdir = os.path.join(conf[CONF_QT_ROOT], 'plugins', 'platforms')
757
758    src = os.path.join(qt_plugins_srcdir, 'qwindows.dll')
759    must_copy(src, destdir)
760
761    src = os.path.join(qt_plugins_srcdir, 'qminimal.dll')
762    must_copy(src, destdir)
763
764def copy_qt_translations():
765    destdir = os.path.join(conf[CONF_BUILDDIR], 'pack', 'bin')
766
767    qt_translation_dir = os.path.join(conf[CONF_QT_ROOT], 'translations')
768
769    i18n_dir = os.path.join(SeafileClient().projdir, 'i18n')
770    qm_pattern = os.path.join(i18n_dir, 'seafile_*.qm')
771
772    qt_qms = set()
773    def add_lang(lang):
774        if not lang:
775            return
776        qt_qm = os.path.join(qt_translation_dir, 'qt_%s.qm' % lang)
777        if os.path.exists(qt_qm):
778            qt_qms.add(qt_qm)
779        elif '_' in lang:
780            add_lang(lang[:lang.index('_')])
781
782    for fn in glob.glob(qm_pattern):
783        name = os.path.basename(fn)
784        m = re.match(r'seafile_(.*)\.qm', name)
785        lang = m.group(1)
786        add_lang(lang)
787
788    for src in qt_qms:
789        must_copy(src, destdir)
790
791def prepare_msi():
792    pack_dir = os.path.join(conf[CONF_BUILDDIR], 'pack')
793
794    msi_dir = os.path.join(Seafile().projdir, 'msi')
795
796    # These files are in seafile-shell-ext because they're shared between seafile/seadrive
797    ext_wxi = os.path.join(SeafileShellExt().projdir, 'msi', 'ext.wxi')
798    must_copy(ext_wxi, msi_dir)
799    shell_wxs = os.path.join(SeafileShellExt().projdir, 'msi', 'shell.wxs')
800    must_copy(shell_wxs, msi_dir)
801
802    must_copytree(msi_dir, pack_dir)
803    must_mkdir(os.path.join(pack_dir, 'bin'))
804
805    if run('make', cwd=os.path.join(pack_dir, 'custom')) != 0:
806        error('Error when compiling seafile msi custom dlls')
807
808    copy_dll_exe()
809
810def sign_executables():
811    certfile = conf.get(CONF_CERTFILE)
812    if certfile is None:
813        info('exectuable signing is skipped since no cert is provided.')
814        return
815
816    pack_dir = os.path.join(conf[CONF_BUILDDIR], 'pack')
817    exectuables = glob.glob(os.path.join(pack_dir, 'bin', '*.exe'))
818    for exe in exectuables:
819        do_sign(certfile, exe)
820
821def sign_installers():
822    certfile = conf.get(CONF_CERTFILE)
823    if certfile is None:
824        info('msi signing is skipped since no cert is provided.')
825        return
826
827    pack_dir = os.path.join(conf[CONF_BUILDDIR], 'pack')
828    installers = glob.glob(os.path.join(pack_dir, '*.msi'))
829    for fn in installers:
830        name = conf[CONF_BRAND]
831        if name == 'seafile':
832            name = 'Seafile'
833        do_sign(certfile, fn, desc='{} Installer'.format(name))
834
835def do_sign(certfile, fn, desc=None):
836    certfile = to_win_path(certfile)
837    fn = to_win_path(fn)
838    info('signing file {} using cert "{}"'.format(fn, certfile))
839
840    if desc:
841        desc_flags = '-d "{}"'.format(desc)
842    else:
843        desc_flags = ''
844
845    # https://support.comodo.com/index.php?/Knowledgebase/Article/View/68/0/time-stamping-server
846    signcmd = 'signtool.exe sign -fd sha256 -t http://timestamp.comodoca.com -f {} {} {}'.format(certfile, desc_flags, fn)
847    i = 0
848    while i < RETRY_COUNT:
849        time.sleep(30)
850        ret = run(signcmd, cwd=os.path.dirname(fn))
851        if ret == 0:
852            break
853        i = i + 1
854        if i == RETRY_COUNT:
855            error('Failed to sign file "{}"'.format(fn))
856
857def strip_symbols():
858    bin_dir = os.path.join(conf[CONF_BUILDDIR], 'pack', 'bin')
859    def do_strip(fn, stripcmd='strip'):
860        run('%s "%s"' % (stripcmd, fn))
861        info('stripping: %s' % fn)
862
863    for dll in glob.glob(os.path.join(bin_dir, '*.dll')):
864        name = os.path.basename(dll).lower()
865        if 'qt' in name:
866            do_strip(dll)
867        if name == 'seafile_ext.dll':
868            do_strip(dll)
869        elif name == 'seafile_ext64.dll':
870            do_strip(dll, stripcmd='x86_64-w64-mingw32-strip')
871
872def edit_fragment_wxs():
873    '''In the main wxs file(seafile.wxs) we need to reference to the id of
874    seafile-applet.exe, which is listed in fragment.wxs. Since fragments.wxs is
875    auto generated, the id is sequentially generated, so we need to change the
876    id of seafile-applet.exe manually.
877
878    '''
879    file_path = os.path.join(conf[CONF_BUILDDIR], 'pack', 'fragment.wxs')
880    new_lines = []
881    with open(file_path, 'r') as fp:
882        for line in fp:
883            if 'seafile-applet.exe' in line:
884                # change the id of 'seafile-applet.exe' to 'seafileapplet.exe'
885                new_line = re.sub('file_bin_[\d]+', 'seafileapplet.exe', line)
886                new_lines.append(new_line)
887            else:
888                new_lines.append(line)
889
890    content = '\r\n'.join(new_lines)
891    with open(file_path, 'w') as fp:
892        fp.write(content)
893
894
895def generate_breakpad_symbols():
896    """
897    Generate seafile and seafile-gui breakpad symbols
898    :return: None
899    """
900    seafile_src = Seafile().projdir
901    seafile_gui_src = SeafileClient().projdir
902    generate_breakpad_symbols_script = os.path.join(seafile_src, 'scripts/breakpad.py')
903
904    # generate seafile the breakpad symbols
905    seafile_name = 'seaf-daemon.exe'
906    seafile_symbol_name = 'seaf-daemon.exe.sym-%s' % conf[CONF_VERSION]
907    seafile_symbol_output = os.path.join(seafile_src, seafile_symbol_name)
908
909    if run('python %s  --projectSrc %s --name %s --output %s'
910           % (generate_breakpad_symbols_script, seafile_src, seafile_name, seafile_symbol_output)) != 0:
911        error('Error when generating breakpad symbols')
912
913    # generate seafile gui breakpad symbols
914    seafile_gui_name = 'seafile-applet.exe'
915    seafile_gui_symbol_name = 'seafile-applet.exe.sym-%s' % conf[CONF_VERSION]
916    seafile_gui_symbol_output = os.path.join(seafile_gui_src, seafile_gui_symbol_name)
917
918    if run('python %s --projectSrc %s --name %s --output %s'
919            % (generate_breakpad_symbols_script, seafile_gui_src, seafile_gui_name, seafile_gui_symbol_output)) != 0:
920        error('Error when generating seafile gui client breakpad symbol')
921
922    # move symbols to output directory
923    dst_seafile_symbol_file = os.path.join(conf[CONF_OUTPUTDIR], seafile_symbol_name)
924    dst_seafile_gui_symbol_file = os.path.join(conf[CONF_OUTPUTDIR], seafile_gui_symbol_name)
925    must_copy(seafile_symbol_output, dst_seafile_symbol_file)
926    must_copy(seafile_gui_symbol_output, dst_seafile_gui_symbol_file)
927
928
929def build_msi():
930    prepare_msi()
931    generate_breakpad_symbols()
932    if conf[CONF_DEBUG] or conf[CONF_NO_STRIP]:
933        info('Would not strip exe/dll symbols since --debug or --nostrip is specified')
934    else:
935        strip_symbols()
936
937    # Only sign the exectuables after stripping symbols.
938    if need_sign():
939        sign_executables()
940
941    pack_dir = os.path.join(conf[CONF_BUILDDIR], 'pack')
942    if run('make fragment.wxs', cwd=pack_dir) != 0:
943        error('Error when make fragement.wxs')
944
945    edit_fragment_wxs()
946
947    if run('make', cwd=pack_dir) != 0:
948        error('Error when make seafile.msi')
949
950def build_english_msi():
951    '''The extra work to build the English msi.'''
952    pack_dir = os.path.join(conf[CONF_BUILDDIR], 'pack')
953
954    if run('make en', cwd=pack_dir) != 0:
955        error('Error when make seafile-en.msi')
956
957def build_german_msi():
958    '''The extra work to build the German msi.'''
959    pack_dir = os.path.join(conf[CONF_BUILDDIR], 'pack')
960
961    if run('make de', cwd=pack_dir) != 0:
962        error('Error when make seafile-de.msi')
963
964def move_msi():
965    pack_dir = os.path.join(conf[CONF_BUILDDIR], 'pack')
966    src_msi = os.path.join(pack_dir, 'seafile.msi')
967    brand = conf[CONF_BRAND]
968    dst_msi = os.path.join(conf[CONF_OUTPUTDIR], '%s-%s.msi' % (brand, conf[CONF_VERSION]))
969
970    # move msi to outputdir
971    must_copy(src_msi, dst_msi)
972
973    if not conf[CONF_ONLY_CHINESE]:
974        src_msi_en = os.path.join(pack_dir, 'seafile-en.msi')
975        dst_msi_en = os.path.join(conf[CONF_OUTPUTDIR], '%s-%s-en.msi' % (brand, conf[CONF_VERSION]))
976        must_copy(src_msi_en, dst_msi_en)
977
978    print '---------------------------------------------'
979    print 'The build is successfully. Output is:'
980    print '>>\t%s' % dst_msi
981    if not conf[CONF_ONLY_CHINESE]:
982        print '>>\t%s' % dst_msi_en
983        # print '>>\t%s' % dst_msi_de
984    print '---------------------------------------------'
985
986def check_tools():
987    tools = [
988        'Paraffin',
989        'candle',
990        'light',
991        'depends',
992    ]
993
994    for prog in tools:
995        if not find_in_path(prog + '.exe'):
996            error('%s not found' % prog)
997
998def dump_env():
999    print 'Dumping environment variables:'
1000    for k, v in os.environ.iteritems():
1001        print '%s: %s' % (k, v)
1002
1003def need_sign():
1004    return conf[CONF_BRAND].lower() == 'seafile'
1005
1006def main():
1007    dump_env()
1008    parse_args()
1009    setup_build_env()
1010    check_tools()
1011
1012    libsearpc = Libsearpc()
1013    seafile = Seafile()
1014    seafile_client = SeafileClient()
1015    seafile_shell_ext = SeafileShellExt()
1016
1017    libsearpc.uncompress()
1018    libsearpc.build()
1019
1020    seafile.uncompress()
1021    seafile.build()
1022
1023    seafile_client.uncompress()
1024    seafile_shell_ext.uncompress()
1025
1026    seafile_client.build()
1027    seafile_shell_ext.build()
1028
1029    build_msi()
1030    if not conf[CONF_ONLY_CHINESE]:
1031        build_english_msi()
1032        # build_german_msi()
1033
1034    if need_sign():
1035        sign_installers()
1036    move_msi()
1037
1038if __name__ == '__main__':
1039    main()
1040