524 lines
20 KiB
Plaintext
524 lines
20 KiB
Plaintext
|
#!/usr/bin/env python3
|
||
|
|
||
|
# Script to extract information from image manifests
|
||
|
#
|
||
|
# Copyright (C) 2018 Intel Corporation
|
||
|
# Copyright (C) 2021 Wind River Systems, Inc.
|
||
|
#
|
||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||
|
#
|
||
|
|
||
|
import sys
|
||
|
import os
|
||
|
import argparse
|
||
|
import logging
|
||
|
import json
|
||
|
import shutil
|
||
|
import tempfile
|
||
|
import tarfile
|
||
|
from collections import OrderedDict
|
||
|
|
||
|
scripts_path = os.path.dirname(__file__)
|
||
|
lib_path = scripts_path + '/../lib'
|
||
|
sys.path = sys.path + [lib_path]
|
||
|
|
||
|
import scriptutils
|
||
|
logger = scriptutils.logger_create(os.path.basename(__file__))
|
||
|
|
||
|
import argparse_oe
|
||
|
import scriptpath
|
||
|
bitbakepath = scriptpath.add_bitbake_lib_path()
|
||
|
if not bitbakepath:
|
||
|
logger.error("Unable to find bitbake by searching parent directory of this script or PATH")
|
||
|
sys.exit(1)
|
||
|
logger.debug('Using standard bitbake path %s' % bitbakepath)
|
||
|
scriptpath.add_oe_lib_path()
|
||
|
|
||
|
import bb.tinfoil
|
||
|
import bb.utils
|
||
|
import oe.utils
|
||
|
import oe.recipeutils
|
||
|
|
||
|
def get_pkg_list(manifest):
|
||
|
pkglist = []
|
||
|
with open(manifest, 'r') as f:
|
||
|
for line in f:
|
||
|
linesplit = line.split()
|
||
|
if len(linesplit) == 3:
|
||
|
# manifest file
|
||
|
pkglist.append(linesplit[0])
|
||
|
elif len(linesplit) == 1:
|
||
|
# build dependency file
|
||
|
pkglist.append(linesplit[0])
|
||
|
return sorted(pkglist)
|
||
|
|
||
|
def list_packages(args):
|
||
|
pkglist = get_pkg_list(args.manifest)
|
||
|
for pkg in pkglist:
|
||
|
print('%s' % pkg)
|
||
|
|
||
|
def pkg2recipe(tinfoil, pkg):
|
||
|
if "-native" in pkg:
|
||
|
logger.info('skipping %s' % pkg)
|
||
|
return None
|
||
|
|
||
|
pkgdata_dir = tinfoil.config_data.getVar('PKGDATA_DIR')
|
||
|
pkgdatafile = os.path.join(pkgdata_dir, 'runtime-reverse', pkg)
|
||
|
logger.debug('pkgdatafile %s' % pkgdatafile)
|
||
|
try:
|
||
|
f = open(pkgdatafile, 'r')
|
||
|
for line in f:
|
||
|
if line.startswith('PN:'):
|
||
|
recipe = line.split(':', 1)[1].strip()
|
||
|
return recipe
|
||
|
except Exception:
|
||
|
logger.warning('%s is missing' % pkgdatafile)
|
||
|
return None
|
||
|
|
||
|
def get_recipe_list(manifest, tinfoil):
|
||
|
pkglist = get_pkg_list(manifest)
|
||
|
recipelist = []
|
||
|
for pkg in pkglist:
|
||
|
recipe = pkg2recipe(tinfoil,pkg)
|
||
|
if recipe:
|
||
|
if not recipe in recipelist:
|
||
|
recipelist.append(recipe)
|
||
|
|
||
|
return sorted(recipelist)
|
||
|
|
||
|
def list_recipes(args):
|
||
|
import bb.tinfoil
|
||
|
with bb.tinfoil.Tinfoil() as tinfoil:
|
||
|
tinfoil.logger.setLevel(logger.getEffectiveLevel())
|
||
|
tinfoil.prepare(config_only=True)
|
||
|
recipelist = get_recipe_list(args.manifest, tinfoil)
|
||
|
for recipe in sorted(recipelist):
|
||
|
print('%s' % recipe)
|
||
|
|
||
|
def list_layers(args):
|
||
|
|
||
|
def find_git_repo(pth):
|
||
|
checkpth = pth
|
||
|
while checkpth != os.sep:
|
||
|
if os.path.exists(os.path.join(checkpth, '.git')):
|
||
|
return checkpth
|
||
|
checkpth = os.path.dirname(checkpth)
|
||
|
return None
|
||
|
|
||
|
def get_git_remote_branch(repodir):
|
||
|
try:
|
||
|
stdout, _ = bb.process.run(['git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], cwd=repodir)
|
||
|
except bb.process.ExecutionError as e:
|
||
|
stdout = None
|
||
|
if stdout:
|
||
|
return stdout.strip()
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def get_git_head_commit(repodir):
|
||
|
try:
|
||
|
stdout, _ = bb.process.run(['git', 'rev-parse', 'HEAD'], cwd=repodir)
|
||
|
except bb.process.ExecutionError as e:
|
||
|
stdout = None
|
||
|
if stdout:
|
||
|
return stdout.strip()
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def get_git_repo_url(repodir, remote='origin'):
|
||
|
import bb.process
|
||
|
# Try to get upstream repo location from origin remote
|
||
|
try:
|
||
|
stdout, _ = bb.process.run(['git', 'remote', '-v'], cwd=repodir)
|
||
|
except bb.process.ExecutionError as e:
|
||
|
stdout = None
|
||
|
if stdout:
|
||
|
for line in stdout.splitlines():
|
||
|
splitline = line.split()
|
||
|
if len(splitline) > 1:
|
||
|
if splitline[0] == remote and scriptutils.is_src_url(splitline[1]):
|
||
|
return splitline[1]
|
||
|
return None
|
||
|
|
||
|
with bb.tinfoil.Tinfoil() as tinfoil:
|
||
|
tinfoil.logger.setLevel(logger.getEffectiveLevel())
|
||
|
tinfoil.prepare(config_only=False)
|
||
|
layers = OrderedDict()
|
||
|
for layerdir in tinfoil.config_data.getVar('BBLAYERS').split():
|
||
|
layerdata = OrderedDict()
|
||
|
layername = os.path.basename(layerdir)
|
||
|
logger.debug('layername %s, layerdir %s' % (layername, layerdir))
|
||
|
if layername in layers:
|
||
|
logger.warning('layername %s is not unique in configuration' % layername)
|
||
|
layername = os.path.basename(os.path.dirname(layerdir)) + '_' + os.path.basename(layerdir)
|
||
|
logger.debug('trying layername %s' % layername)
|
||
|
if layername in layers:
|
||
|
logger.error('Layer name %s is not unique in configuration' % layername)
|
||
|
sys.exit(2)
|
||
|
repodir = find_git_repo(layerdir)
|
||
|
if repodir:
|
||
|
remotebranch = get_git_remote_branch(repodir)
|
||
|
remote = 'origin'
|
||
|
if remotebranch and '/' in remotebranch:
|
||
|
rbsplit = remotebranch.split('/', 1)
|
||
|
layerdata['actual_branch'] = rbsplit[1]
|
||
|
remote = rbsplit[0]
|
||
|
layerdata['vcs_url'] = get_git_repo_url(repodir, remote)
|
||
|
if os.path.abspath(repodir) != os.path.abspath(layerdir):
|
||
|
layerdata['vcs_subdir'] = os.path.relpath(layerdir, repodir)
|
||
|
commit = get_git_head_commit(repodir)
|
||
|
if commit:
|
||
|
layerdata['vcs_commit'] = commit
|
||
|
layers[layername] = layerdata
|
||
|
|
||
|
json.dump(layers, args.output, indent=2)
|
||
|
|
||
|
def get_recipe(args):
|
||
|
with bb.tinfoil.Tinfoil() as tinfoil:
|
||
|
tinfoil.logger.setLevel(logger.getEffectiveLevel())
|
||
|
tinfoil.prepare(config_only=True)
|
||
|
|
||
|
recipe = pkg2recipe(tinfoil, args.package)
|
||
|
print(' %s package provided by %s' % (args.package, recipe))
|
||
|
|
||
|
def pkg_dependencies(args):
|
||
|
def get_recipe_info(tinfoil, recipe):
|
||
|
try:
|
||
|
info = tinfoil.get_recipe_info(recipe)
|
||
|
except Exception:
|
||
|
logger.error('Failed to get recipe info for: %s' % recipe)
|
||
|
sys.exit(1)
|
||
|
if not info:
|
||
|
logger.warning('No recipe info found for: %s' % recipe)
|
||
|
sys.exit(1)
|
||
|
append_files = tinfoil.get_file_appends(info.fn)
|
||
|
appends = True
|
||
|
data = tinfoil.parse_recipe_file(info.fn, appends, append_files)
|
||
|
data.pn = info.pn
|
||
|
data.pv = info.pv
|
||
|
return data
|
||
|
|
||
|
def find_dependencies(tinfoil, assume_provided, recipe_info, packages, rn, order):
|
||
|
spaces = ' ' * order
|
||
|
data = recipe_info[rn]
|
||
|
if args.native:
|
||
|
logger.debug('%s- %s' % (spaces, data.pn))
|
||
|
elif "-native" not in data.pn:
|
||
|
if "cross" not in data.pn:
|
||
|
logger.debug('%s- %s' % (spaces, data.pn))
|
||
|
|
||
|
depends = []
|
||
|
for dep in data.depends:
|
||
|
if dep not in assume_provided:
|
||
|
depends.append(dep)
|
||
|
|
||
|
# First find all dependencies not in package list.
|
||
|
for dep in depends:
|
||
|
if dep not in packages:
|
||
|
packages.append(dep)
|
||
|
dep_data = get_recipe_info(tinfoil, dep)
|
||
|
# Do this once now to reduce the number of bitbake calls.
|
||
|
dep_data.depends = dep_data.getVar('DEPENDS').split()
|
||
|
recipe_info[dep] = dep_data
|
||
|
|
||
|
# Then recursively analyze all of the dependencies for the current recipe.
|
||
|
for dep in depends:
|
||
|
find_dependencies(tinfoil, assume_provided, recipe_info, packages, dep, order + 1)
|
||
|
|
||
|
with bb.tinfoil.Tinfoil() as tinfoil:
|
||
|
tinfoil.logger.setLevel(logger.getEffectiveLevel())
|
||
|
tinfoil.prepare()
|
||
|
|
||
|
assume_provided = tinfoil.config_data.getVar('ASSUME_PROVIDED').split()
|
||
|
logger.debug('assumed provided:')
|
||
|
for ap in sorted(assume_provided):
|
||
|
logger.debug(' - %s' % ap)
|
||
|
|
||
|
recipe = pkg2recipe(tinfoil, args.package)
|
||
|
data = get_recipe_info(tinfoil, recipe)
|
||
|
data.depends = []
|
||
|
depends = data.getVar('DEPENDS').split()
|
||
|
for dep in depends:
|
||
|
if dep not in assume_provided:
|
||
|
data.depends.append(dep)
|
||
|
|
||
|
recipe_info = dict([(recipe, data)])
|
||
|
packages = []
|
||
|
find_dependencies(tinfoil, assume_provided, recipe_info, packages, recipe, order=1)
|
||
|
|
||
|
print('\nThe following packages are required to build %s' % recipe)
|
||
|
for p in sorted(packages):
|
||
|
data = recipe_info[p]
|
||
|
if "-native" not in data.pn:
|
||
|
if "cross" not in data.pn:
|
||
|
print(" %s (%s)" % (data.pn,p))
|
||
|
|
||
|
if args.native:
|
||
|
print('\nThe following native packages are required to build %s' % recipe)
|
||
|
for p in sorted(packages):
|
||
|
data = recipe_info[p]
|
||
|
if "-native" in data.pn:
|
||
|
print(" %s(%s)" % (data.pn,p))
|
||
|
if "cross" in data.pn:
|
||
|
print(" %s(%s)" % (data.pn,p))
|
||
|
|
||
|
def default_config():
|
||
|
vlist = OrderedDict()
|
||
|
vlist['PV'] = 'yes'
|
||
|
vlist['SUMMARY'] = 'no'
|
||
|
vlist['DESCRIPTION'] = 'no'
|
||
|
vlist['SECTION'] = 'no'
|
||
|
vlist['LICENSE'] = 'yes'
|
||
|
vlist['HOMEPAGE'] = 'no'
|
||
|
vlist['BUGTRACKER'] = 'no'
|
||
|
vlist['PROVIDES'] = 'no'
|
||
|
vlist['BBCLASSEXTEND'] = 'no'
|
||
|
vlist['DEPENDS'] = 'no'
|
||
|
vlist['PACKAGECONFIG'] = 'no'
|
||
|
vlist['SRC_URI'] = 'yes'
|
||
|
vlist['SRCREV'] = 'yes'
|
||
|
vlist['EXTRA_OECONF'] = 'no'
|
||
|
vlist['EXTRA_OESCONS'] = 'no'
|
||
|
vlist['EXTRA_OECMAKE'] = 'no'
|
||
|
vlist['EXTRA_OEMESON'] = 'no'
|
||
|
|
||
|
clist = OrderedDict()
|
||
|
clist['variables'] = vlist
|
||
|
clist['filepath'] = 'no'
|
||
|
clist['sha256sum'] = 'no'
|
||
|
clist['layerdir'] = 'no'
|
||
|
clist['layer'] = 'no'
|
||
|
clist['inherits'] = 'no'
|
||
|
clist['source_urls'] = 'no'
|
||
|
clist['packageconfig_opts'] = 'no'
|
||
|
clist['patches'] = 'no'
|
||
|
clist['packagedir'] = 'no'
|
||
|
return clist
|
||
|
|
||
|
def dump_config(args):
|
||
|
config = default_config()
|
||
|
f = open('default_config.json', 'w')
|
||
|
json.dump(config, f, indent=2)
|
||
|
logger.info('Default config list dumped to default_config.json')
|
||
|
|
||
|
def export_manifest_info(args):
|
||
|
|
||
|
def handle_value(value):
|
||
|
if value:
|
||
|
return oe.utils.squashspaces(value)
|
||
|
else:
|
||
|
return value
|
||
|
|
||
|
if args.config:
|
||
|
logger.debug('config: %s' % args.config)
|
||
|
f = open(args.config, 'r')
|
||
|
config = json.load(f, object_pairs_hook=OrderedDict)
|
||
|
else:
|
||
|
config = default_config()
|
||
|
if logger.isEnabledFor(logging.DEBUG):
|
||
|
print('Configuration:')
|
||
|
json.dump(config, sys.stdout, indent=2)
|
||
|
print('')
|
||
|
|
||
|
tmpoutdir = tempfile.mkdtemp(prefix=os.path.basename(__file__)+'-')
|
||
|
logger.debug('tmp dir: %s' % tmpoutdir)
|
||
|
|
||
|
# export manifest
|
||
|
shutil.copy2(args.manifest,os.path.join(tmpoutdir, "manifest"))
|
||
|
|
||
|
with bb.tinfoil.Tinfoil(tracking=True) as tinfoil:
|
||
|
tinfoil.logger.setLevel(logger.getEffectiveLevel())
|
||
|
tinfoil.prepare(config_only=False)
|
||
|
|
||
|
pkglist = get_pkg_list(args.manifest)
|
||
|
# export pkg list
|
||
|
f = open(os.path.join(tmpoutdir, "pkgs"), 'w')
|
||
|
for pkg in pkglist:
|
||
|
f.write('%s\n' % pkg)
|
||
|
f.close()
|
||
|
|
||
|
recipelist = []
|
||
|
for pkg in pkglist:
|
||
|
recipe = pkg2recipe(tinfoil,pkg)
|
||
|
if recipe:
|
||
|
if not recipe in recipelist:
|
||
|
recipelist.append(recipe)
|
||
|
recipelist.sort()
|
||
|
# export recipe list
|
||
|
f = open(os.path.join(tmpoutdir, "recipes"), 'w')
|
||
|
for recipe in recipelist:
|
||
|
f.write('%s\n' % recipe)
|
||
|
f.close()
|
||
|
|
||
|
try:
|
||
|
rvalues = OrderedDict()
|
||
|
for pn in sorted(recipelist):
|
||
|
logger.debug('Package: %s' % pn)
|
||
|
rd = tinfoil.parse_recipe(pn)
|
||
|
|
||
|
rvalues[pn] = OrderedDict()
|
||
|
|
||
|
for varname in config['variables']:
|
||
|
if config['variables'][varname] == 'yes':
|
||
|
rvalues[pn][varname] = handle_value(rd.getVar(varname))
|
||
|
|
||
|
fpth = rd.getVar('FILE')
|
||
|
layerdir = oe.recipeutils.find_layerdir(fpth)
|
||
|
if config['filepath'] == 'yes':
|
||
|
rvalues[pn]['filepath'] = os.path.relpath(fpth, layerdir)
|
||
|
if config['sha256sum'] == 'yes':
|
||
|
rvalues[pn]['sha256sum'] = bb.utils.sha256_file(fpth)
|
||
|
|
||
|
if config['layerdir'] == 'yes':
|
||
|
rvalues[pn]['layerdir'] = layerdir
|
||
|
|
||
|
if config['layer'] == 'yes':
|
||
|
rvalues[pn]['layer'] = os.path.basename(layerdir)
|
||
|
|
||
|
if config['inherits'] == 'yes':
|
||
|
gr = set(tinfoil.config_data.getVar("__inherit_cache") or [])
|
||
|
lr = set(rd.getVar("__inherit_cache") or [])
|
||
|
rvalues[pn]['inherits'] = sorted({os.path.splitext(os.path.basename(r))[0] for r in lr if r not in gr})
|
||
|
|
||
|
if config['source_urls'] == 'yes':
|
||
|
rvalues[pn]['source_urls'] = []
|
||
|
for url in (rd.getVar('SRC_URI') or '').split():
|
||
|
if not url.startswith('file://'):
|
||
|
url = url.split(';')[0]
|
||
|
rvalues[pn]['source_urls'].append(url)
|
||
|
|
||
|
if config['packageconfig_opts'] == 'yes':
|
||
|
rvalues[pn]['packageconfig_opts'] = OrderedDict()
|
||
|
for key in rd.getVarFlags('PACKAGECONFIG').keys():
|
||
|
if key == 'doc':
|
||
|
continue
|
||
|
rvalues[pn]['packageconfig_opts'][key] = rd.getVarFlag('PACKAGECONFIG', key)
|
||
|
|
||
|
if config['patches'] == 'yes':
|
||
|
patches = oe.recipeutils.get_recipe_patches(rd)
|
||
|
rvalues[pn]['patches'] = []
|
||
|
if patches:
|
||
|
recipeoutdir = os.path.join(tmpoutdir, pn, 'patches')
|
||
|
bb.utils.mkdirhier(recipeoutdir)
|
||
|
for patch in patches:
|
||
|
# Patches may be in other layers too
|
||
|
patchlayerdir = oe.recipeutils.find_layerdir(patch)
|
||
|
# patchlayerdir will be None for remote patches, which we ignore
|
||
|
# (since currently they are considered as part of sources)
|
||
|
if patchlayerdir:
|
||
|
rvalues[pn]['patches'].append((os.path.basename(patchlayerdir), os.path.relpath(patch, patchlayerdir)))
|
||
|
shutil.copy(patch, recipeoutdir)
|
||
|
|
||
|
if config['packagedir'] == 'yes':
|
||
|
pn_dir = os.path.join(tmpoutdir, pn)
|
||
|
bb.utils.mkdirhier(pn_dir)
|
||
|
f = open(os.path.join(pn_dir, 'recipe.json'), 'w')
|
||
|
json.dump(rvalues[pn], f, indent=2)
|
||
|
f.close()
|
||
|
|
||
|
with open(os.path.join(tmpoutdir, 'recipes.json'), 'w') as f:
|
||
|
json.dump(rvalues, f, indent=2)
|
||
|
|
||
|
if args.output:
|
||
|
outname = os.path.basename(args.output)
|
||
|
else:
|
||
|
outname = os.path.splitext(os.path.basename(args.manifest))[0]
|
||
|
if outname.endswith('.tar.gz'):
|
||
|
outname = outname[:-7]
|
||
|
elif outname.endswith('.tgz'):
|
||
|
outname = outname[:-4]
|
||
|
|
||
|
tarfn = outname
|
||
|
if tarfn.endswith(os.sep):
|
||
|
tarfn = tarfn[:-1]
|
||
|
if not tarfn.endswith(('.tar.gz', '.tgz')):
|
||
|
tarfn += '.tar.gz'
|
||
|
with open(tarfn, 'wb') as f:
|
||
|
with tarfile.open(None, "w:gz", f) as tar:
|
||
|
tar.add(tmpoutdir, outname)
|
||
|
finally:
|
||
|
shutil.rmtree(tmpoutdir)
|
||
|
|
||
|
|
||
|
def main():
|
||
|
parser = argparse_oe.ArgumentParser(description="Image manifest utility",
|
||
|
epilog="Use %(prog)s <subcommand> --help to get help on a specific command")
|
||
|
parser.add_argument('-d', '--debug', help='Enable debug output', action='store_true')
|
||
|
parser.add_argument('-q', '--quiet', help='Print only errors', action='store_true')
|
||
|
subparsers = parser.add_subparsers(dest="subparser_name", title='subcommands', metavar='<subcommand>')
|
||
|
subparsers.required = True
|
||
|
|
||
|
# get recipe info
|
||
|
parser_get_recipes = subparsers.add_parser('recipe-info',
|
||
|
help='Get recipe info',
|
||
|
description='Get recipe information for a package')
|
||
|
parser_get_recipes.add_argument('package', help='Package name')
|
||
|
parser_get_recipes.set_defaults(func=get_recipe)
|
||
|
|
||
|
# list runtime dependencies
|
||
|
parser_pkg_dep = subparsers.add_parser('list-depends',
|
||
|
help='List dependencies',
|
||
|
description='List dependencies required to build the package')
|
||
|
parser_pkg_dep.add_argument('--native', help='also print native and cross packages', action='store_true')
|
||
|
parser_pkg_dep.add_argument('package', help='Package name')
|
||
|
parser_pkg_dep.set_defaults(func=pkg_dependencies)
|
||
|
|
||
|
# list recipes
|
||
|
parser_recipes = subparsers.add_parser('list-recipes',
|
||
|
help='List recipes producing packages within an image',
|
||
|
description='Lists recipes producing the packages that went into an image, using the manifest and pkgdata')
|
||
|
parser_recipes.add_argument('manifest', help='Manifest file')
|
||
|
parser_recipes.set_defaults(func=list_recipes)
|
||
|
|
||
|
# list packages
|
||
|
parser_packages = subparsers.add_parser('list-packages',
|
||
|
help='List packages within an image',
|
||
|
description='Lists packages that went into an image, using the manifest')
|
||
|
parser_packages.add_argument('manifest', help='Manifest file')
|
||
|
parser_packages.set_defaults(func=list_packages)
|
||
|
|
||
|
# list layers
|
||
|
parser_layers = subparsers.add_parser('list-layers',
|
||
|
help='List included layers',
|
||
|
description='Lists included layers')
|
||
|
parser_layers.add_argument('-o', '--output', help='Output file - defaults to stdout if not specified',
|
||
|
default=sys.stdout, type=argparse.FileType('w'))
|
||
|
parser_layers.set_defaults(func=list_layers)
|
||
|
|
||
|
# dump default configuration file
|
||
|
parser_dconfig = subparsers.add_parser('dump-config',
|
||
|
help='Dump default config',
|
||
|
description='Dump default config to default_config.json')
|
||
|
parser_dconfig.set_defaults(func=dump_config)
|
||
|
|
||
|
# export recipe info for packages in manifest
|
||
|
parser_export = subparsers.add_parser('manifest-info',
|
||
|
help='Export recipe info for a manifest',
|
||
|
description='Export recipe information using the manifest')
|
||
|
parser_export.add_argument('-c', '--config', help='load config from json file')
|
||
|
parser_export.add_argument('-o', '--output', help='Output file (tarball) - defaults to manifest name if not specified')
|
||
|
parser_export.add_argument('manifest', help='Manifest file')
|
||
|
parser_export.set_defaults(func=export_manifest_info)
|
||
|
|
||
|
args = parser.parse_args()
|
||
|
|
||
|
if args.debug:
|
||
|
logger.setLevel(logging.DEBUG)
|
||
|
logger.debug("Debug Enabled")
|
||
|
elif args.quiet:
|
||
|
logger.setLevel(logging.ERROR)
|
||
|
|
||
|
ret = args.func(args)
|
||
|
|
||
|
return ret
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
try:
|
||
|
ret = main()
|
||
|
except Exception:
|
||
|
ret = 1
|
||
|
import traceback
|
||
|
traceback.print_exc()
|
||
|
sys.exit(ret)
|