Merge pull request #6602 from mensinda/depBoost

boost: System dependency rewrite
pull/6678/head
Jussi Pakkanen 4 years ago committed by GitHub
commit 8bf937b012
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/lint_mypy.yml
  2. 8
      .github/workflows/os_comp.yml
  3. 3
      azure-pipelines.yml
  4. 4
      ci/azure-steps.yml
  5. 2
      ci/ciimage/eoan/Dockerfile
  6. 1407
      mesonbuild/dependencies/boost.py
  7. 40
      run_project_tests.py
  8. 42
      test cases/frameworks/1 boost/meson.build
  9. 1
      test cases/frameworks/1 boost/meson_options.txt
  10. 19
      test cases/frameworks/1 boost/test_matrix.json
  11. 1
      test cases/frameworks/1 boost/unit_test.cpp
  12. 371
      tools/boost_names.py

@ -30,4 +30,4 @@ jobs:
with:
python-version: '3.x'
- run: python -m pip install mypy
- run: mypy --follow-imports=skip mesonbuild/mtest.py mesonbuild/minit.py mesonbuild/mintro.py mesonbuild/msetup.py mesonbuild/wrap tools/ mesonbuild/modules/fs.py mesonbuild/dependencies/mpi.py mesonbuild/dependencies/hdf5.py mesonbuild/compilers/mixins/intel.py
- run: mypy --follow-imports=skip mesonbuild/mtest.py mesonbuild/minit.py mesonbuild/mintro.py mesonbuild/msetup.py mesonbuild/wrap tools/ mesonbuild/modules/fs.py mesonbuild/dependencies/boost.py mesonbuild/dependencies/mpi.py mesonbuild/dependencies/hdf5.py mesonbuild/compilers/mixins/intel.py

@ -11,9 +11,7 @@ jobs:
- name: Install Dependencies
run: |
sudo apt update -yq
sudo apt install -yq --no-install-recommends python3-setuptools python3-pip g++ gfortran gobjc gobjc++ zlib1g-dev python-dev python3-dev libboost-all-dev
- name: Remove GitHub boost version
run: sudo rm -rf /usr/local/share/boost
sudo apt install -yq --no-install-recommends python3-setuptools python3-pip g++ gfortran gobjc gobjc++ zlib1g-dev python-dev python3-dev
- name: Install ninja-build tool
uses: seanmiddleditch/gha-setup-ninja@v1
- name: Python version
@ -21,7 +19,7 @@ jobs:
- name: Ninja version
run: ninja --version
- name: Run tests
run: python3 run_tests.py
run: LD_LIBRARY_PATH=/usr/local/share/boost/1.69.0/lib/:$LD_LIBRARY_PATH python3 run_tests.py
env:
CI: '1'
XENIAL: '1'
@ -48,6 +46,7 @@ jobs:
env:
CI: '1'
SKIP_SCIENTIFIC: '1'
SKIP_STATIC_BOOST: '1'
opensuse:
name: OpenSUSE
@ -60,3 +59,4 @@ jobs:
env:
CI: '1'
SKIP_SCIENTIFIC: '1'
SKIP_STATIC_BOOST: '1'

@ -105,7 +105,10 @@ jobs:
- script: |
set BOOST_ROOT=
set PATH=%CYGWIN_ROOT%\bin;%SYSTEMROOT%\system32
set SKIP_STATIC_BOOST=1
env.exe -- python3 run_tests.py --backend=ninja
# Cygwin's static boost installation is broken (some static library
# variants such as boost_thread are not present)
displayName: Run Tests
- task: CopyFiles@2
condition: not(canceled())

@ -49,8 +49,8 @@ steps:
# install boost (except for clang-cl)
if ($env:arch -eq 'x86') { $boost_bitness = '32' } else { $boost_bitness = '64' }
if ($env:compiler -eq 'msvc2017') {
$boost_version = '1.64.0' ; $boost_abi_tag = '14.1'
if ($env:compiler -eq 'msvc2017' -Or $env:compiler -eq 'msvc2019' -Or $env:compiler -eq 'clang-cl') {
$boost_version = '1.72.0' ; $boost_abi_tag = '14.1'
}
if ($boost_version) {
$boost_filename = $boost_version.Replace('.', '_')

@ -24,7 +24,7 @@ RUN sed -i '/^#\sdeb-src /s/^#//' "/etc/apt/sources.list" \
&& eatmydata apt-get -y install libgcrypt20-dev \
&& eatmydata apt-get -y install libgpgme-dev \
&& eatmydata apt-get -y install libhdf5-dev \
&& eatmydata apt-get -y install libboost-python-dev \
&& eatmydata apt-get -y install libboost-python-dev libboost-regex-dev \
&& eatmydata apt-get -y install libblocksruntime-dev \
&& eatmydata apt-get -y install libperl-dev \
&& eatmydata apt-get -y install liblapack-dev libscalapack-mpi-dev \

File diff suppressed because it is too large Load Diff

@ -136,6 +136,7 @@ do_debug = under_ci or print_debug
no_meson_log_msg = 'No meson-log.txt found.'
system_compiler = None
compiler_id_map = {} # type: T.Dict[str, str]
class StopException(Exception):
def __init__(self):
@ -544,12 +545,24 @@ def gather_tests(testdir: Path) -> T.Iterator[TestDef]:
assert "val" in i
skip = False
# Add an empty matrix entry
if i['val'] is None:
tmp_opts += [(None, False)]
continue
# Skip the matrix entry if environment variable is present
if 'skip_on_env' in i:
for env in i['skip_on_env']:
if env in os.environ:
skip = True
# Only run the test if all compiler ID's match
if 'compilers' in i:
for lang, id_list in i['compilers'].items():
if lang not in compiler_id_map or compiler_id_map[lang] not in id_list:
skip = True
break
tmp_opts += [('{}={}'.format(key, i['val']), skip)]
if opt_list:
@ -561,9 +574,27 @@ def gather_tests(testdir: Path) -> T.Iterator[TestDef]:
else:
opt_list = [[x] for x in tmp_opts]
# Exclude specific configurations
if 'exclude' in matrix:
assert isinstance(matrix['exclude'], list)
new_opt_list = [] # type: T.List[T.List[T.Tuple[str, bool]]]
for i in opt_list:
exclude = False
opt_names = [x[0] for x in i]
for j in matrix['exclude']:
ex_list = ['{}={}'.format(k, v) for k, v in j.items()]
if all([x in opt_names for x in ex_list]):
exclude = True
break
if not exclude:
new_opt_list += [i]
opt_list = new_opt_list
for i in opt_list:
name = ' '.join([x[0] for x in i])
opts = ['-D' + x[0] for x in i]
name = ' '.join([x[0] for x in i if x[0] is not None])
opts = ['-D' + x[0] for x in i if x[0] is not None]
skip = any([x[1] for x in i])
all_tests += [TestDef(t.path, name, opts, skip)]
@ -991,7 +1022,7 @@ def check_meson_commands_work(options):
def detect_system_compiler(options):
global system_compiler
global system_compiler, compiler_id_map
with AutoDeletedDir(tempfile.mkdtemp(prefix='b ', dir='.')) as build_dir:
fake_opts = get_fake_options('/')
@ -1002,7 +1033,8 @@ def detect_system_compiler(options):
for lang in sorted(compilers.all_languages):
try:
comp = env.compiler_from_language(lang, MachineChoice.HOST)
details = '%s %s' % (' '.join(comp.get_exelist()), comp.get_version_string())
details = '{} {} [{}]'.format(' '.join(comp.get_exelist()), comp.get_version_string(), comp.get_id())
compiler_id_map[lang] = comp.get_id()
except mesonlib.MesonException:
comp = None
details = 'not found'

@ -1,31 +1,25 @@
# this test requires the following on Ubuntu: libboost-{system,python,log,thread,test}-dev
project('boosttest', 'cpp',
default_options : ['cpp_std=c++11'])
default_options : ['cpp_std=c++14'])
add_project_arguments(['-DBOOST_LOG_DYN_LINK'],
language : 'cpp'
)
s = get_option('static')
dep = dependency('boost', required: false)
dep = dependency('boost', static: s, required: false)
if not dep.found()
error('MESON_SKIP_TEST boost not found.')
endif
compiler = meson.get_compiler('cpp')
if compiler.has_argument('-permissive')
# boost 1.64, the version we test against, doesn't work with -permissive
add_project_arguments('-permissive', language: 'cpp')
endif
# We want to have multiple separate configurations of Boost
# within one project. The need to be independent of each other.
# Use one without a library dependency and one with it.
linkdep = dependency('boost', modules : ['thread', 'system', 'test'])
staticdep = dependency('boost', modules : ['thread', 'system'], static : true)
testdep = dependency('boost', modules : ['unit_test_framework'])
nomoddep = dependency('boost')
extralibdep = dependency('boost', modules : ['thread', 'system', 'log_setup', 'log'])
linkdep = dependency('boost', static: s, modules : ['thread', 'system'])
testdep = dependency('boost', static: s, modules : ['unit_test_framework'])
nomoddep = dependency('boost', static: s)
extralibdep = dependency('boost', static: s, modules : ['thread', 'system', 'date_time', 'log_setup', 'log', 'filesystem', 'regex'])
notfound = dependency('boost', static: s, modules : ['this_should_not_exist_on_any_systen'], required: false)
assert(not notfound.found())
pymod = import('python')
python2 = pymod.find_installation('python2', required: host_machine.system() == 'linux', disabler: true)
@ -34,28 +28,28 @@ python2dep = python2.dependency(required: host_machine.system() == 'linux', embe
python3dep = python3.dependency(required: host_machine.system() == 'linux', embed: true, disabler: true)
# compile python 2/3 modules only if we found a corresponding python version
if(python2dep.found() and host_machine.system() == 'linux')
if(python2dep.found() and host_machine.system() == 'linux' and not s)
if(dep.version().version_compare('>=1.67'))
# if we have a new version of boost, we need to construct the module name based
# on the installed version of python (and hope that they match the version boost
# was compiled against)
py2version_string = ''.join(python2dep.version().split('.'))
bpython2dep = dependency('boost', modules : ['python' + py2version_string], required: false, disabler: true)
bpython2dep = dependency('boost', static: s, modules : ['python' + py2version_string], required: false, disabler: true)
else
# if we have an older version of boost, we need to use the old module names
bpython2dep = dependency('boost', modules : ['python'], required: false, disabler: true)
bpython2dep = dependency('boost', static: s, modules : ['python'], required: false, disabler: true)
endif
else
python2dep = disabler()
bpython2dep = disabler()
endif
if(python3dep.found() and host_machine.system() == 'linux')
if(python3dep.found() and host_machine.system() == 'linux' and not s)
if(dep.version().version_compare('>=1.67'))
py3version_string = ''.join(python3dep.version().split('.'))
bpython3dep = dependency('boost', modules : ['python' + py3version_string], required: false, disabler: true)
bpython3dep = dependency('boost', static: s, modules : ['python' + py3version_string], required: false, disabler: true)
else
bpython3dep = dependency('boost', modules : ['python3'], required: false, disabler: true)
bpython3dep = dependency('boost', static: s, modules : ['python3'], required: false, disabler: true)
endif
else
python3dep = disabler()
@ -63,7 +57,6 @@ else
endif
linkexe = executable('linkedexe', 'linkexe.cc', dependencies : linkdep)
staticexe = executable('staticlinkedexe', 'linkexe.cc', dependencies : staticdep)
unitexe = executable('utf', 'unit_test.cpp', dependencies: testdep)
nomodexe = executable('nomod', 'nomod.cpp', dependencies : nomoddep)
extralibexe = executable('extralibexe', 'extralib.cpp', dependencies : extralibdep)
@ -73,7 +66,6 @@ python2module = shared_library('python2_module', ['python_module.cpp'], dependen
python3module = shared_library('python3_module', ['python_module.cpp'], dependencies: [python3dep, bpython3dep], name_prefix: '', cpp_args: ['-DMOD_NAME=python3_module'])
test('Boost linktest', linkexe)
test('Boost statictest', staticexe)
test('Boost UTF test', unitexe)
test('Boost nomod', nomodexe)
test('Boost extralib test', extralibexe)
@ -87,4 +79,4 @@ test('Boost Python3', python3interpreter, args: ['./test_python_module.py', meso
subdir('partial_dep')
# check we can apply a version constraint
dependency('boost', version: '>=@0@'.format(dep.version()))
dependency('boost', static: s, version: '>=@0@'.format(dep.version()))

@ -0,0 +1 @@
option('static', type: 'boolean', value: false)

@ -0,0 +1,19 @@
{
"options": {
"static": [
{ "val": "true", "skip_on_env": [ "SKIP_STATIC_BOOST" ] },
{ "val": "false" }
],
"b_vscrt": [
{ "val": null },
{ "val": "md", "compilers": { "cpp": [ "msvc" ] } },
{ "val": "mdd", "compilers": { "cpp": [ "msvc" ] } },
{ "val": "mt", "compilers": { "cpp": [ "msvc" ] } },
{ "val": "mtd", "compilers": { "cpp": [ "msvc" ] } }
]
},
"exclude": [
{ "static": "false", "b_vscrt": "mt" },
{ "static": "false", "b_vscrt": "mtd" }
]
}

@ -1,4 +1,3 @@
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE "MesonTest"
#define BOOST_TEST_MAIN
#include <boost/test/unit_test.hpp>

@ -24,164 +24,249 @@ boost/$ path/to/meson/tools/boost_names.py >> path/to/meson/dependencies/misc.py
"""
import sys
import os
import collections
import pprint
import json
import re
import textwrap
import functools
import typing as T
from pathlib import Path
lib_dir = Path('libs')
jamroot = Path('Jamroot')
not_modules = ['config', 'disjoint_sets', 'headers']
export_modules = False
@functools.total_ordering
class BoostLibrary():
def __init__(self, name: str, shared: T.List[str], static: T.List[str], single: T.List[str], multi: T.List[str]):
self.name = name
self.shared = shared
self.static = static
self.single = single
self.multi = multi
def __lt__(self, other: T.Any) -> T.Union[bool, 'NotImplemented']:
if isinstance(other, BoostLibrary):
return self.name < other.name
return NotImplemented
def __eq__(self, other: T.Any) -> T.Union[bool, 'NotImplemented']:
if isinstance(other, BoostLibrary):
return self.name == other.name
elif isinstance(other, str):
return self.name == other
return NotImplemented
def __hash__(self) -> int:
return hash(self.name)
@functools.total_ordering
class BoostModule():
def __init__(self, name: str, key: str, desc: str, libs: T.List[BoostLibrary]):
self.name = name
self.key = key
self.desc = desc
self.libs = libs
def __lt__(self, other: T.Any) -> T.Union[bool, 'NotImplemented']:
if isinstance(other, BoostModule):
return self.key < other.key
return NotImplemented
Module = collections.namedtuple('Module', ['dirname', 'name', 'libnames'])
Module.__repr__ = lambda self: str((self.dirname, self.name, self.libnames)) # type: ignore
LIBS = 'libs'
manual_map = {
'callable_traits': 'Call Traits',
'crc': 'CRC',
'dll': 'DLL',
'gil': 'GIL',
'graph_parallel': 'GraphParallel',
'icl': 'ICL',
'io': 'IO State Savers',
'msm': 'Meta State Machine',
'mpi': 'MPI',
'mpl': 'MPL',
'multi_array': 'Multi-Array',
'multi_index': 'Multi-Index',
'numeric': 'Numeric Conversion',
'ptr_container': 'Pointer Container',
'poly_collection': 'PolyCollection',
'qvm': 'QVM',
'throw_exception': 'ThrowException',
'tti': 'TTI',
'vmd': 'VMD',
}
extra = [
Module('utility', 'Compressed Pair', []),
Module('core', 'Enable If', []),
Module('functional', 'Functional/Factory', []),
Module('functional', 'Functional/Forward', []),
Module('functional', 'Functional/Hash', []),
Module('functional', 'Functional/Overloaded Function', []),
Module('utility', 'Identity Type', []),
Module('utility', 'In Place Factory, Typed In Place Factory', []),
Module('numeric', 'Interval', []),
Module('math', 'Math Common Factor', []),
Module('math', 'Math Octonion', []),
Module('math', 'Math Quaternion', []),
Module('math', 'Math/Special Functions', []),
Module('math', 'Math/Statistical Distributions', []),
Module('bind', 'Member Function', []),
Module('algorithm', 'Min-Max', []),
Module('numeric', 'Odeint', []),
Module('utility', 'Operators', []),
Module('core', 'Ref', []),
Module('utility', 'Result Of', []),
Module('algorithm', 'String Algo', []),
Module('core', 'Swap', []),
Module('', 'Tribool', []),
Module('numeric', 'uBLAS', []),
Module('utility', 'Value Initialized', []),
]
# Cannot find the following modules in the documentation of boost
not_modules = ['beast', 'logic', 'mp11', 'winapi']
def eprint(message):
print(message, file=sys.stderr)
def get_library_names(jamfile):
libs = []
with open(jamfile) as jamfh:
jam = jamfh.read()
res = re.finditer(r'^lib[\s]+([A-Za-z0-9_]+)([^;]*);', jam, re.MULTILINE | re.DOTALL)
for matches in res:
if ':' in matches.group(2):
libs.append(matches.group(1))
res = re.finditer(r'^boost-lib[\s]+([A-Za-z0-9_]+)([^;]*);', jam, re.MULTILINE | re.DOTALL)
for matches in res:
if ':' in matches.group(2):
libs.append('boost_{}'.format(matches.group(1)))
return libs
def exists(modules, module):
return len([x for x in modules if x.dirname == module.dirname]) != 0
def get_boost_version() -> T.Optional[str]:
raw = jamroot.read_text()
m = re.search(r'BOOST_VERSION\s*:\s*([0-9\.]+)\s*;', raw)
if m:
return m.group(1)
return None
def get_modules(init=extra):
modules = init
for directory in os.listdir(LIBS):
if not os.path.isdir(os.path.join(LIBS, directory)):
def get_libraries(jamfile: Path) -> T.List[BoostLibrary]:
# Extract libraries from the boost Jamfiles. This includes:
# - library name
# - compiler flags
libs: T.List[BoostLibrary] = []
raw = jamfile.read_text()
raw = re.sub(r'#.*\n', '\n', raw) # Remove comments
raw = re.sub(r'\s+', ' ', raw) # Force single space
raw = re.sub(r'}', ';', raw) # Cheat code blocks by converting } to ;
cmds = raw.split(';') # Commands always terminate with a ; (I hope)
cmds = [x.strip() for x in cmds] # Some cleanup
# "Parse" the relevant sections
for i in cmds:
parts = i.split(' ')
parts = [x for x in parts if x not in ['', ':']]
if not parts:
continue
if directory in not_modules:
# Parese libraries
if parts[0] in ['lib', 'boost-lib']:
assert len(parts) >= 2
# Get and check the library name
lname = parts[1]
if parts[0] == 'boost-lib':
lname = f'boost_{lname}'
if not lname.startswith('boost_'):
continue
# Get shared / static defines
shared: T.List[str] = []
static: T.List[str] = []
single: T.List[str] = []
multi: T.List[str] = []
for j in parts:
m1 = re.match(r'<link>shared:<define>(.*)', j)
m2 = re.match(r'<link>static:<define>(.*)', j)
m3 = re.match(r'<threading>single:<define>(.*)', j)
m4 = re.match(r'<threading>multi:<define>(.*)', j)
if m1:
shared += [m1.group(1)]
if m2:
static += [m2.group(1)]
if m3:
single += [m3.group(1)]
if m4:
multi += [m4.group(1)]
shared = [f'-D{x}' for x in shared]
static = [f'-D{x}' for x in static]
libs += [BoostLibrary(lname, shared, static, single, multi)]
return libs
def process_lib_dir(ldir: Path) -> T.List[BoostModule]:
meta_file = ldir / 'meta' / 'libraries.json'
bjam_file = ldir / 'build' / 'Jamfile.v2'
if not meta_file.exists():
print(f'WARNING: Meta file {meta_file} does not exist')
return []
# Extract libs
libs: T.List[BoostLibrary] = []
if bjam_file.exists():
libs = get_libraries(bjam_file)
# Extract metadata
data = json.loads(meta_file.read_text())
if not isinstance(data, list):
data = [data]
modules: T.List[BoostModule] = []
for i in data:
modules += [BoostModule(i['name'], i['key'], i['description'], libs)]
return modules
def get_modules() -> T.List[BoostModule]:
modules: T.List[BoostModule] = []
for i in lib_dir.iterdir():
if not i.is_dir() or i.name in not_modules:
continue
jamfile = os.path.join(LIBS, directory, 'build', 'Jamfile.v2')
if os.path.isfile(jamfile):
libs = get_library_names(jamfile)
else:
libs = []
if directory in manual_map.keys():
modname = manual_map[directory]
# numeric has sub libs
subdirs = i / 'sublibs'
metadir = i / 'meta'
if subdirs.exists() and not metadir.exists():
for j in i.iterdir():
if not j.is_dir():
continue
modules += process_lib_dir(j)
else:
modname = directory.replace('_', ' ').title()
modules.append(Module(directory, modname, libs))
modules += process_lib_dir(i)
return modules
def get_modules_2():
modules = []
# The python module uses an older build system format and is not easily parseable.
# We add the python module libraries manually.
modules.append(Module('python', 'Python', ['boost_python', 'boost_python3', 'boost_numpy', 'boost_numpy3']))
for (root, _, files) in os.walk(LIBS):
for f in files:
if f == "libraries.json":
projectdir = os.path.dirname(root)
jamfile = os.path.join(projectdir, 'build', 'Jamfile.v2')
if os.path.isfile(jamfile):
libs = get_library_names(jamfile)
else:
libs = []
# Get metadata for module
jsonfile = os.path.join(root, f)
with open(jsonfile) as jsonfh:
boost_modules = json.loads(jsonfh.read())
if(isinstance(boost_modules, dict)):
boost_modules = [boost_modules]
for boost_module in boost_modules:
modules.append(Module(boost_module['key'], boost_module['name'], libs))
# Some subprojects do not have meta directory with json file. Find those
jsonless_modules = [x for x in get_modules([]) if not exists(modules, x)]
for module in jsonless_modules:
eprint("WARNING: {} does not have meta/libraries.json. Will guess pretty name '{}'".format(module.dirname, module.name))
modules.extend(jsonless_modules)
return modules
def main() -> int:
if not lib_dir.is_dir() or not jamroot.exists():
print("ERROR: script must be run in boost source directory")
return 1
vers = get_boost_version()
modules = get_modules()
modules = sorted(modules)
libraries = [x for y in modules for x in y.libs]
libraries = sorted(set(libraries))
print(textwrap.dedent(f'''\
#### ---- BEGIN GENERATED ---- ####
# #
# Generated with tools/boost_names.py:
# - boost version: {vers}
# - modules found: {len(modules)}
# - libraries found: {len(libraries)}
#
class BoostLibrary():
def __init__(self, name: str, shared: T.List[str], static: T.List[str], single: T.List[str], multi: T.List[str]):
self.name = name
self.shared = shared
self.static = static
self.single = single
self.multi = multi
class BoostModule():
def __init__(self, name: str, key: str, desc: str, libs: T.List[str]):
self.name = name
self.key = key
self.desc = desc
self.libs = libs
# dict of all know libraries with additional compile options
boost_libraries = {{\
'''))
for i in libraries:
print(textwrap.indent(textwrap.dedent(f"""\
'{i.name}': BoostLibrary(
name='{i.name}',
shared={i.shared},
static={i.static},
single={i.single},
multi={i.multi},
),\
"""), ' '))
if export_modules:
print(textwrap.dedent(f'''\
}}
def main(args):
if not os.path.isdir(LIBS):
eprint("ERROR: script must be run in boost source directory")
# dict of all modules with metadata
boost_modules = {{\
'''))
# It will pick jsonless algorithm if 1 is given as argument
impl = 0
if len(args) > 1:
if args[1] == '1':
impl = 1
for mod in modules:
desc_excaped = re.sub(r"'", "\\'", mod.desc)
print(textwrap.indent(textwrap.dedent(f"""\
'{mod.key}': BoostModule(
name='{mod.name}',
key='{mod.key}',
desc='{desc_excaped}',
libs={[x.name for x in mod.libs]},
),\
"""), ' '))
if impl == 1:
modules = get_modules()
else:
modules = get_modules_2()
print(textwrap.dedent(f'''\
}}
sorted_modules = sorted(modules, key=lambda module: module.name.lower())
sorted_modules = [x[2] for x in sorted_modules if x[2]]
sorted_modules = sum(sorted_modules, [])
sorted_modules = [x for x in sorted_modules if x.startswith('boost')]
# #
#### ---- END GENERATED ---- ####\
'''))
pp = pprint.PrettyPrinter()
pp.pprint(sorted_modules)
return 0
if __name__ == '__main__':
main(sys.argv)
sys.exit(main())

Loading…
Cancel
Save