#!/usr/bin/env python # coding: UTF-8 '''This script builds the seafile server tarball. Some notes: 1. The working directory is always the 'builddir'. 'os.chdir' is only called to change to the 'builddir'. We make use of the 'cwd' argument in 'subprocess.Popen' to run a command in a specific directory. 2. django/djangorestframework/djblets/gunicorn/flup must be easy_install-ed to a directory before running this script. That directory is passed in as the '--thirdpartdir' arguments. ''' import sys import os import glob import subprocess import tempfile import shutil import re import subprocess import optparse import atexit import platform #################### ### Global variables #################### # command line configuartion conf = {} # key names in the conf dictionary. CONF_VERSION = 'version' CONF_SEAFILE_VERSION = 'seafile_version' CONF_LIBSEARPC_VERSION = 'libsearpc_version' CONF_SRCDIR = 'srcdir' CONF_KEEP = 'keep' CONF_BUILDDIR = 'builddir' CONF_OUTPUTDIR = 'outputdir' CONF_THIRDPARTDIR = 'thirdpartdir' CONF_NO_STRIP = 'nostrip' CONF_ENABLE_S3 = 's3' CONF_YES = 'yes' CONF_JOBS = 'jobs' CONF_MYSQL_CONFIG = 'mysql_config' #################### ### Common helper functions #################### def highlight(content, is_error=False): '''Add ANSI color to content to get it highlighted on terminal''' if is_error: return '\x1b[1;31m%s\x1b[m' % content else: return '\x1b[1;32m%s\x1b[m' % content def info(msg): print(highlight('[INFO] ') + msg) def find_in_path(prog): '''Find a file in system path''' dirs = os.environ['PATH'].split(':') for d in dirs: if d == '': continue path = os.path.join(d, prog) if os.path.exists(path): return path return None def error(msg=None, usage=None): if msg: print(highlight('[ERROR] ') + msg) if usage: print(usage) sys.exit(1) def run_argv(argv, cwd=None, env=None, suppress_stdout=False, suppress_stderr=False): '''Run a program and wait for it to finish, and return its exit code. The standard output of this program is supressed. ''' with open(os.devnull, 'w') as devnull: if suppress_stdout: stdout = devnull else: stdout = sys.stdout if suppress_stderr: stderr = devnull else: stderr = sys.stderr proc = subprocess.Popen(argv, cwd=cwd, stdout=stdout, stderr=stderr, env=env) return proc.wait() def run(cmdline, cwd=None, env=None, suppress_stdout=False, suppress_stderr=False): '''Like run_argv but specify a command line string instead of argv''' with open(os.devnull, 'w') as devnull: if suppress_stdout: stdout = devnull else: stdout = sys.stdout if suppress_stderr: stderr = devnull else: stderr = sys.stderr proc = subprocess.Popen(cmdline, cwd=cwd, stdout=stdout, stderr=stderr, env=env, shell=True) return proc.wait() def must_mkdir(path): '''Create a directory, exit on failure''' try: os.mkdir(path) except OSError as e: error('failed to create directory %s:%s' % (path, e)) def must_copy(src, dst): '''Copy src to dst, exit on failure''' try: shutil.copy(src, dst) except Exception as e: error('failed to copy %s to %s: %s' % (src, dst, e)) class Project(object): '''Base class for a project''' # Project name, i.e. libseaprc/seafile/seahub name = '' # A list of shell commands to configure/build the project build_commands = [] def __init__(self): # the path to pass to --prefix=/ self.prefix = os.path.join(conf[CONF_BUILDDIR], 'seafile-server', 'seafile') self.version = self.get_version() self.src_tarball = os.path.join(conf[CONF_SRCDIR], '%s-%s.tar.gz' % (self.name, self.version)) # project dir, like /seafile-1.2.2/ self.projdir = os.path.join(conf[CONF_BUILDDIR], '%s-%s' % (self.name, self.version)) def get_version(self): # libsearpc can have different versions from seafile. raise NotImplementedError def uncompress(self): '''Uncompress the source from the tarball''' info('Uncompressing %s' % self.name) if run('tar xf %s' % self.src_tarball) < 0: error('failed to uncompress source of %s' % self.name) def build(self): '''Build the source''' info('Building %s' % self.name) for cmd in self.build_commands: if run(cmd, cwd=self.projdir) != 0: error('error when running command:\n\t%s\n' % cmd) class Libsearpc(Project): name = 'libsearpc' def __init__(self): Project.__init__(self) self.build_commands = [ './configure --prefix=%s' % self.prefix, 'make -j%s' % conf[CONF_JOBS], 'make install' ] def get_version(self): return conf[CONF_LIBSEARPC_VERSION] class Seafile(Project): name = 'seafile' def __init__(self): Project.__init__(self) s3_support = '' if conf[CONF_ENABLE_S3]: s3_support = '--enable-s3' configure_command = './configure --prefix=%s %s --enable-ldap' % (self.prefix, s3_support) if conf[CONF_MYSQL_CONFIG]: configure_command += ' --with-mysql=%s' % conf[CONF_MYSQL_CONFIG] self.build_commands = [ configure_command, 'make -j%s' % conf[CONF_JOBS], 'make install' ] def get_version(self): return conf[CONF_SEAFILE_VERSION] class Seahub(Project): name = 'seahub' def __init__(self): Project.__init__(self) # nothing to do for seahub self.build_commands = [ ] def get_version(self): return conf[CONF_SEAFILE_VERSION] def build(self): self.write_version_to_settings_py() Project.build(self) def write_version_to_settings_py(self): '''Write the version of current seafile server to seahub''' settings_py = os.path.join(self.projdir, 'seahub', 'settings.py') line = '\nSEAFILE_VERSION = "%s"\n' % conf[CONF_VERSION] with open(settings_py, 'a+') as fp: fp.write(line) def check_seahub_thirdpart(thirdpartdir): '''The ${thirdpartdir} must have django/djblets/gunicorn pre-installed. So we can copy it to seahub/thirdpart ''' thirdpart_libs = [ 'Django', # 'Djblets', 'gunicorn', #'flup', 'chardet', 'python_dateutil', #'django_picklefield', #'django_constance', # 'SQLAlchemy', # 'python_daemon', # 'lockfile', # 'six', ] def check_thirdpart_lib(name): name += '*' if not glob.glob(os.path.join(thirdpartdir, name)): error('%s not found in %s' % (name, thirdpartdir)) for lib in thirdpart_libs: check_thirdpart_lib(lib) def check_targz_src(proj, version, srcdir): src_tarball = os.path.join(srcdir, '%s-%s.tar.gz' % (proj, version)) if not os.path.exists(src_tarball): error('%s not exists' % src_tarball) def check_targz_src_no_version(proj, srcdir): src_tarball = os.path.join(srcdir, '%s.tar.gz' % proj) if not os.path.exists(src_tarball): error('%s not exists' % src_tarball) def check_pdf2htmlEX(): pdf2htmlEX_executable = find_in_path('pdf2htmlEX') if pdf2htmlEX_executable is None: error('pdf2htmlEX not found') def validate_args(usage, options): required_args = [ CONF_VERSION, CONF_LIBSEARPC_VERSION, CONF_SEAFILE_VERSION, CONF_SRCDIR, CONF_THIRDPARTDIR, ] # fist check required args for optname in required_args: if getattr(options, optname, None) == None: error('%s must be specified' % optname, usage=usage) def get_option(optname): return getattr(options, optname) # [ version ] def check_project_version(version): '''A valid version must be like 1.2.2, 1.3''' if not re.match('^[0-9]+(\.([0-9])+)+$', version): error('%s is not a valid version' % version, usage=usage) version = get_option(CONF_VERSION) seafile_version = get_option(CONF_SEAFILE_VERSION) libsearpc_version = get_option(CONF_LIBSEARPC_VERSION) check_project_version(version) check_project_version(libsearpc_version) check_project_version(seafile_version) # [ srcdir ] srcdir = get_option(CONF_SRCDIR) check_targz_src('libsearpc', libsearpc_version, srcdir) check_targz_src('seafile', seafile_version, srcdir) check_targz_src('seahub', seafile_version, srcdir) check_targz_src_no_version('seafdav', srcdir) check_targz_src_no_version('seafobj', srcdir) # check_pdf2htmlEX() # [ builddir ] builddir = get_option(CONF_BUILDDIR) if not os.path.exists(builddir): error('%s does not exist' % builddir, usage=usage) builddir = os.path.join(builddir, 'seafile-server-build') # [ thirdpartdir ] thirdpartdir = get_option(CONF_THIRDPARTDIR) check_seahub_thirdpart(thirdpartdir) # [ outputdir ] outputdir = get_option(CONF_OUTPUTDIR) if outputdir: if not os.path.exists(outputdir): error('outputdir %s does not exist' % outputdir, usage=usage) else: outputdir = os.getcwd() # [ yes ] yes = get_option(CONF_YES) # [ jobs ] jobs = get_option(CONF_JOBS) # [ keep ] keep = get_option(CONF_KEEP) # [ no strip] nostrip = get_option(CONF_NO_STRIP) # [ s3 ] s3 = get_option(CONF_ENABLE_S3) mysql_config_path = get_option(CONF_MYSQL_CONFIG) conf[CONF_VERSION] = version conf[CONF_LIBSEARPC_VERSION] = libsearpc_version conf[CONF_SEAFILE_VERSION] = seafile_version conf[CONF_BUILDDIR] = builddir conf[CONF_SRCDIR] = srcdir conf[CONF_OUTPUTDIR] = outputdir conf[CONF_KEEP] = keep conf[CONF_THIRDPARTDIR] = thirdpartdir conf[CONF_NO_STRIP] = nostrip conf[CONF_ENABLE_S3] = s3 conf[CONF_YES] = yes conf[CONF_JOBS] = jobs conf[CONF_MYSQL_CONFIG] = mysql_config_path prepare_builddir(builddir) show_build_info() def show_build_info(): '''Print all conf information. Confirm before continue.''' info('------------------------------------------') info('Seafile server %s: BUILD INFO' % conf[CONF_VERSION]) info('------------------------------------------') info('seafile: %s' % conf[CONF_SEAFILE_VERSION]) info('libsearpc: %s' % conf[CONF_LIBSEARPC_VERSION]) info('builddir: %s' % conf[CONF_BUILDDIR]) info('outputdir: %s' % conf[CONF_OUTPUTDIR]) info('source dir: %s' % conf[CONF_SRCDIR]) info('strip symbols: %s' % (not conf[CONF_NO_STRIP])) info('s3 support: %s' % (conf[CONF_ENABLE_S3])) info('clean on exit: %s' % (not conf[CONF_KEEP])) if conf[CONF_YES]: return info('------------------------------------------') info('press any key to continue ') info('------------------------------------------') input() def prepare_builddir(builddir): must_mkdir(builddir) if not conf[CONF_KEEP]: def remove_builddir(): '''Remove the builddir when exit''' info('remove builddir before exit') shutil.rmtree(builddir, ignore_errors=True) atexit.register(remove_builddir) os.chdir(builddir) must_mkdir(os.path.join(builddir, 'seafile-server')) must_mkdir(os.path.join(builddir, 'seafile-server', 'seafile')) def parse_args(): parser = optparse.OptionParser() def long_opt(opt): return '--' + opt parser.add_option(long_opt(CONF_YES), dest=CONF_YES, action='store_true') parser.add_option(long_opt(CONF_JOBS), dest=CONF_JOBS, default=2, type=int) parser.add_option(long_opt(CONF_THIRDPARTDIR), dest=CONF_THIRDPARTDIR, nargs=1, help='where to find the thirdpart libs for seahub') parser.add_option(long_opt(CONF_VERSION), dest=CONF_VERSION, nargs=1, help='the version to build. Must be digits delimited by dots, like 1.3.0') parser.add_option(long_opt(CONF_SEAFILE_VERSION), dest=CONF_SEAFILE_VERSION, nargs=1, help='the version of seafile as specified in its "configure.ac". Must be digits delimited by dots, like 1.3.0') parser.add_option(long_opt(CONF_LIBSEARPC_VERSION), dest=CONF_LIBSEARPC_VERSION, nargs=1, help='the version of libsearpc as specified in its "configure.ac". Must be digits delimited by dots, like 1.3.0') parser.add_option(long_opt(CONF_BUILDDIR), dest=CONF_BUILDDIR, nargs=1, help='the directory to build the source. Defaults to /tmp', default=tempfile.gettempdir()) parser.add_option(long_opt(CONF_OUTPUTDIR), dest=CONF_OUTPUTDIR, nargs=1, help='the output directory to put the generated server tarball. Defaults to the current directory.', default=os.getcwd()) parser.add_option(long_opt(CONF_SRCDIR), dest=CONF_SRCDIR, nargs=1, help='''Source tarballs must be placed in this directory.''') parser.add_option(long_opt(CONF_KEEP), dest=CONF_KEEP, action='store_true', help='''keep the build directory after the script exits. By default, the script would delete the build directory at exit.''') parser.add_option(long_opt(CONF_NO_STRIP), dest=CONF_NO_STRIP, action='store_true', help='''do not strip debug symbols''') parser.add_option(long_opt(CONF_ENABLE_S3), dest=CONF_ENABLE_S3, action='store_true', help='''enable amazon s3 support''') parser.add_option(long_opt(CONF_MYSQL_CONFIG), dest=CONF_MYSQL_CONFIG, nargs=1, help='''Absolute path to mysql_config or mariadb_config program.''') usage = parser.format_help() options, remain = parser.parse_args() if remain: error(usage=usage) validate_args(usage, options) def setup_build_env(): '''Setup environment variables, such as export PATH=$BUILDDDIR/bin:$PATH''' prefix = os.path.join(conf[CONF_BUILDDIR], 'seafile-server', 'seafile') def prepend_env_value(name, value, seperator=':'): '''append a new value to a list''' try: current_value = os.environ[name] except KeyError: current_value = '' new_value = value if current_value: new_value += seperator + current_value os.environ[name] = new_value prepend_env_value('CPPFLAGS', '-I%s' % os.path.join(prefix, 'include'), seperator=' ') prepend_env_value('CPPFLAGS', '-DLIBICONV_PLUG', seperator=' ') if conf[CONF_NO_STRIP]: prepend_env_value('CPPFLAGS', '-g -O0', seperator=' ') prepend_env_value('CFLAGS', '-g -O0', seperator=' ') prepend_env_value('LDFLAGS', '-L%s' % os.path.join(prefix, 'lib'), seperator=' ') prepend_env_value('LDFLAGS', '-L%s' % os.path.join(prefix, 'lib64'), seperator=' ') prepend_env_value('PATH', os.path.join(prefix, 'bin')) prepend_env_value('PKG_CONFIG_PATH', os.path.join(prefix, 'lib', 'pkgconfig')) prepend_env_value('PKG_CONFIG_PATH', os.path.join(prefix, 'lib64', 'pkgconfig')) def copy_user_manuals(): builddir = conf[CONF_BUILDDIR] # src_pattern = os.path.join(builddir, Seafile().projdir, 'doc', '*.doc') src_pattern = os.path.join(builddir, Seafile().projdir, 'doc', 'seafile-tutorial.doc') dst_dir = os.path.join(builddir, 'seafile-server', 'seafile', 'docs') must_mkdir(dst_dir) for path in glob.glob(src_pattern): must_copy(path, dst_dir) def copy_seafdav(): dst_dir = os.path.join(conf[CONF_BUILDDIR], 'seafile-server', 'seahub', 'thirdpart') tarball = os.path.join(conf[CONF_SRCDIR], 'seafdav.tar.gz') if run('tar xf %s -C %s' % (tarball, dst_dir)) != 0: error('failed to uncompress %s' % tarball) dst_dir = os.path.join(conf[CONF_BUILDDIR], 'seafile-server', 'seahub', 'thirdpart') tarball = os.path.join(conf[CONF_SRCDIR], 'seafobj.tar.gz') if run('tar xf %s -C %s' % (tarball, dst_dir)) != 0: error('failed to uncompress %s' % tarball) def copy_scripts_and_libs(): '''Copy server release scripts and shared libs, as well as seahub thirdpart libs ''' builddir = conf[CONF_BUILDDIR] scripts_srcdir = os.path.join(builddir, Seafile().projdir, 'scripts') serverdir = os.path.join(builddir, 'seafile-server') must_copy(os.path.join(scripts_srcdir, 'setup-seafile.sh'), serverdir) must_copy(os.path.join(scripts_srcdir, 'setup-seafile-mysql.sh'), serverdir) must_copy(os.path.join(scripts_srcdir, 'setup-seafile-mysql.py'), serverdir) must_copy(os.path.join(scripts_srcdir, 'seafile.sh'), serverdir) must_copy(os.path.join(scripts_srcdir, 'seahub.sh'), serverdir) must_copy(os.path.join(scripts_srcdir, 'reset-admin.sh'), serverdir) must_copy(os.path.join(scripts_srcdir, 'seaf-fuse.sh'), serverdir) must_copy(os.path.join(scripts_srcdir, 'check_init_admin.py'), serverdir) must_copy(os.path.join(scripts_srcdir, 'seaf-gc.sh'), serverdir) must_copy(os.path.join(scripts_srcdir, 'seaf-fsck.sh'), serverdir) # copy update scripts update_scriptsdir = os.path.join(scripts_srcdir, 'upgrade') dst_update_scriptsdir = os.path.join(serverdir, 'upgrade') try: shutil.copytree(update_scriptsdir, dst_update_scriptsdir) except Exception as e: error('failed to copy upgrade scripts: %s' % e) # copy sql scripts sql_scriptsdir = os.path.join(scripts_srcdir, 'sql') dst_sql_scriptsdir = os.path.join(serverdir, 'sql') try: shutil.copytree(sql_scriptsdir, dst_sql_scriptsdir) except Exception as e: error('failed to copy sql scripts: %s' % e) # copy runtime/seahub.conf runtimedir = os.path.join(serverdir, 'runtime') must_mkdir(runtimedir) must_copy(os.path.join(scripts_srcdir, 'seahub.conf'), runtimedir) # move seahub to seafile-server/seahub src_seahubdir = Seahub().projdir dst_seahubdir = os.path.join(serverdir, 'seahub') try: shutil.move(src_seahubdir, dst_seahubdir) except Exception as e: error('failed to move seahub to seafile-server/seahub: %s' % e) # copy seahub thirdpart libs seahub_thirdpart = os.path.join(dst_seahubdir, 'thirdpart') copy_seahub_thirdpart_libs(seahub_thirdpart) copy_seafdav() # copy_pdf2htmlex() # copy shared c libs copy_shared_libs() copy_user_manuals() def copy_pdf2htmlex(): '''Copy pdf2htmlEX exectuable and its dependent libs''' pdf2htmlEX_executable = find_in_path('pdf2htmlEX') libs = get_dependent_libs(pdf2htmlEX_executable) builddir = conf[CONF_BUILDDIR] dst_lib_dir = os.path.join(builddir, 'seafile-server', 'seafile', 'lib') dst_bin_dir = os.path.join(builddir, 'seafile-server', 'seafile', 'bin') for lib in libs: dst_file = os.path.join(dst_lib_dir, os.path.basename(lib)) if os.path.exists(dst_file): continue info('Copying %s' % lib) must_copy(lib, dst_lib_dir) must_copy(pdf2htmlEX_executable, dst_bin_dir) def get_dependent_libs(executable): syslibs = ['libsearpc', 'libseafile', 'libpthread.so', 'libc.so', 'libm.so', 'librt.so', 'libdl.so', 'libselinux.so', 'libresolv.so' ] def is_syslib(lib): for syslib in syslibs: if syslib in lib: return True return False ldd_output = subprocess.getoutput('ldd %s' % executable) ret = set() for line in ldd_output.splitlines(): tokens = line.split() if len(tokens) != 4: continue if is_syslib(tokens[0]): continue ret.add(tokens[2]) return ret def copy_shared_libs(): '''copy shared c libs, such as libevent, glib, libmysqlclient''' builddir = conf[CONF_BUILDDIR] dst_dir = os.path.join(builddir, 'seafile-server', 'seafile', 'lib') seafile_path = os.path.join(builddir, 'seafile-server', 'seafile', 'bin', 'seaf-server') seaf_fuse_path = os.path.join(builddir, 'seafile-server', 'seafile', 'bin', 'seaf-fuse') libs = set() libs.update(get_dependent_libs(seafile_path)) libs.update(get_dependent_libs(seaf_fuse_path)) for lib in libs: dst_file = os.path.join(dst_dir, os.path.basename(lib)) if os.path.exists(dst_file): continue info('Copying %s' % lib) shutil.copy(lib, dst_dir) def copy_seahub_thirdpart_libs(seahub_thirdpart): '''copy python third-party libraries from ${thirdpartdir} to seahub/thirdpart ''' src = conf[CONF_THIRDPARTDIR] dst = seahub_thirdpart try: for name in os.listdir(src): src_path = os.path.join(src, name) target_path = os.path.join(dst, name) if os.path.isdir(src_path): shutil.copytree(src_path, target_path) else: shutil.copy(src_path, target_path) except Exception as e: error('failed to copy seahub thirdpart libs: %s' % e) def strip_symbols(): def do_strip(fn): run('chmod u+w %s' % fn) info('stripping: %s' % fn) run('strip "%s"' % fn) def remove_static_lib(fn): info('removing: %s' % fn) os.remove(fn) for parent, dnames, fnames in os.walk('seafile-server/seafile'): dummy = dnames # avoid pylint 'unused' warning for fname in fnames: fn = os.path.join(parent, fname) if os.path.isdir(fn): continue if fn.endswith(".a") or fn.endswith(".la"): remove_static_lib(fn) continue if os.path.islink(fn): continue finfo = subprocess.getoutput('file "%s"' % fn) if 'not stripped' in finfo: do_strip(fn) def create_tarball(tarball_name): '''call tar command to generate a tarball''' version = conf[CONF_VERSION] serverdir = 'seafile-server' versioned_serverdir = 'seafile-server-' + version # move seafile-server to seafile-server-${version} try: shutil.move(serverdir, versioned_serverdir) except Exception as e: error('failed to move %s to %s: %s' % (serverdir, versioned_serverdir, e)) ignored_patterns = [ # common ignored files '*.pyc', '*~', '*#', # seahub os.path.join(versioned_serverdir, 'seahub', '.git*'), os.path.join(versioned_serverdir, 'seahub', 'media', 'flexpaper*'), os.path.join(versioned_serverdir, 'seahub', 'avatar', 'testdata*'), # seafile os.path.join(versioned_serverdir, 'seafile', 'share*'), os.path.join(versioned_serverdir, 'seafile', 'include*'), os.path.join(versioned_serverdir, 'seafile', 'lib', 'pkgconfig*'), os.path.join(versioned_serverdir, 'seafile', 'lib64', 'pkgconfig*'), os.path.join(versioned_serverdir, 'seafile', 'bin', 'searpc-codegen.py'), os.path.join(versioned_serverdir, 'seafile', 'bin', 'seafile-admin'), os.path.join(versioned_serverdir, 'seafile', 'bin', 'seafile'), ] excludes_list = [ '--exclude=%s' % pattern for pattern in ignored_patterns ] excludes = ' '.join(excludes_list) tar_cmd = 'tar czf %(tarball_name)s %(versioned_serverdir)s %(excludes)s' \ % dict(tarball_name=tarball_name, versioned_serverdir=versioned_serverdir, excludes=excludes) if run(tar_cmd) < 0: error('failed to generate the tarball') def gen_tarball(): # strip symbols of libraries to reduce size if not conf[CONF_NO_STRIP]: try: strip_symbols() except Exception as e: error('failed to strip symbols: %s' % e) # determine the output name # 64-bit: seafile-server_1.2.2_x86-64.tar.gz # 32-bit: seafile-server_1.2.2_i386.tar.gz version = conf[CONF_VERSION] arch = os.uname()[-1].replace('_', '-') if 'arm' in platform.machine(): arch = 'pi' elif arch != 'x86-64': arch = 'i386' dbg = '' if conf[CONF_NO_STRIP]: dbg = '.dbg' tarball_name = 'seafile-server_%(version)s_%(arch)s%(dbg)s.tar.gz' \ % dict(version=version, arch=arch, dbg=dbg) dst_tarball = os.path.join(conf[CONF_OUTPUTDIR], tarball_name) # generate the tarball try: create_tarball(tarball_name) except Exception as e: error('failed to generate tarball: %s' % e) # move tarball to outputdir try: shutil.copy(tarball_name, dst_tarball) except Exception as e: error('failed to copy %s to %s: %s' % (tarball_name, dst_tarball, e)) print('---------------------------------------------') print('The build is successful. Output is:\t%s' % dst_tarball) print('---------------------------------------------') def main(): parse_args() setup_build_env() libsearpc = Libsearpc() seafile = Seafile() seahub = Seahub() libsearpc.uncompress() libsearpc.build() seafile.uncompress() seafile.build() seahub.uncompress() seahub.build() copy_scripts_and_libs() gen_tarball() if __name__ == '__main__': main()