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