Ticket #1190: bundle_builder.py

File bundle_builder.py, 26.6 KB (added by Tristan Croll, 7 years ago)

Further bug fixes

Line 
1# vim: set expandtab ts=4 sw=4:
2
3def distlib_hack(_func):
4 # This hack is needed because distlib and wheel do not yet
5 # agree on the metadata file name.
6 def hack(*args, **kw):
7 # import distutils.core
8 # distutils.core.DEBUG = True
9 # TODO: remove distlib monkey patch when the wheel package
10 # implements PEP 426's pydist.json
11 from distlib import metadata
12 save = metadata.METADATA_FILENAME
13 metadata.METADATA_FILENAME = "metadata.json"
14 try:
15 return _func(*args, **kw)
16 finally:
17 metadata.METADATA_FILENAME = save
18 return hack
19
20
21class BundleBuilder:
22
23 def __init__(self, logger, bundle_path=None):
24 import os, os.path
25 self.logger = logger
26 if bundle_path is None:
27 bundle_path = os.getcwd()
28 self.path = bundle_path
29 info_file = os.path.join(bundle_path, "bundle_info.xml")
30 if not os.path.exists(info_file):
31 raise IOError("Bundle info file %s is missing" % repr(info_file))
32 self._read_bundle_info(info_file)
33 self._make_paths()
34 self._make_setup_arguments()
35
36 @distlib_hack
37 def make_wheel(self, test=True, debug=False):
38 # HACK: distutils uses a cache to track created directories
39 # for a single setup() run. We want to run setup() multiple
40 # times which can remove/create the same directories.
41 # So we need to flush the cache before each run.
42 import distutils.dir_util
43 try:
44 distutils.dir_util._path_created.clear()
45 except AttributeError:
46 pass
47 import os.path
48 for lib in self.c_libraries:
49 self.datafiles[self.package].append(
50 lib.compile(self.logger, self.dependencies, debug=debug))
51 setup_args = ["--no-user-cfg", "build"]
52 if debug:
53 setup_args.append("--debug")
54 if test:
55 setup_args.append("test")
56 setup_args.extend(["bdist_wheel"])
57 built = self._run_setup(setup_args)
58 if not built or not os.path.exists(self.wheel_path):
59 raise RuntimeError("Building wheel failed")
60 else:
61 print("Distribution is in %s" % self.wheel_path)
62
63 @distlib_hack
64 def make_install(self, session, test=True, debug=False, user=None):
65 self.make_wheel(test=test, debug=debug)
66 from chimerax.core.commands import run
67 cmd = "toolshed install %r reinstall true" % self.wheel_path
68 if user is not None:
69 if user:
70 cmd += " user true"
71 else:
72 cmd += " user false"
73 run(session, cmd)
74
75 @distlib_hack
76 def make_clean(self):
77 import os.path
78 self._rmtree(os.path.join(self.path, "build"))
79 self._rmtree(os.path.join(self.path, "dist"))
80 self._rmtree(os.path.join(self.path, "src/__pycache__"))
81 self._rmtree(self.egg_info)
82
83 def dump(self):
84 for a in dir(self):
85 if a.startswith('_'):
86 continue
87 v = getattr(self, a)
88 if not callable(v):
89 print("%s: %s" % (a, repr(v)))
90
91 def _rmtree(self, path):
92 import shutil
93 shutil.rmtree(path, ignore_errors=True)
94
95 _mac_platforms = ["mac", "macos", "darwin"]
96 _windows_platforms = ["windows", "win32"]
97 _linux_platforms = ["linux"]
98
99 def _read_bundle_info(self, bundle_info):
100 # Setup platform variable so we can skip non-matching elements
101 import sys
102 if sys.platform == "darwin":
103 # Tested with macOS 10.12
104 self._platform_names = self._mac_platforms
105 elif sys.platform == "win32":
106 # Tested with Cygwin
107 self._platform_names = self._windows_platforms
108 else:
109 # Presumably Linux
110 # Tested with Ubuntu 16.04 LTS running in
111 # a singularity container on CentOS 7.3
112 self._platform_names = self._linux_platforms
113 # Read data from XML file
114 self._used_elements = set()
115 from xml.dom.minidom import parse
116 doc = parse(bundle_info)
117 bi = doc.documentElement
118 self._get_identifiers(bi)
119 self._get_categories(bi)
120 self._get_descriptions(bi)
121 self._get_datafiles(bi)
122 self._get_dependencies(bi)
123 self._get_c_modules(bi)
124 self._get_c_libraries(bi)
125 self._get_packages(bi)
126 self._get_classifiers(bi)
127 self._check_unused_elements(bi)
128
129 def _get_identifiers(self, bi):
130 self.name = bi.getAttribute("name")
131 self.version = bi.getAttribute("version")
132 self.package = bi.getAttribute("package")
133 self.min_session = bi.getAttribute("minSessionVersion")
134 self.max_session = bi.getAttribute("maxSessionVersion")
135 self.custom_init = bi.getAttribute("customInit")
136 self.pure_python = bi.getAttribute("purePython")
137
138 def _get_categories(self, bi):
139 self.categories = []
140 deps = self._get_singleton(bi, "Categories")
141 for e in self._get_elements(deps, "Category"):
142 self.categories.append(e.getAttribute("name"))
143
144 def _get_descriptions(self, bi):
145 self.author = self._get_singleton_text(bi, "Author")
146 self.email = self._get_singleton_text(bi, "Email")
147 self.url = self._get_singleton_text(bi, "URL")
148 self.synopsis = self._get_singleton_text(bi, "Synopsis")
149 self.description = self._get_singleton_text(bi, "Description")
150 try:
151 self.license = self._get_singleton_text(bi, "License")
152 except ValueError:
153 self.license = None
154
155 def _get_datafiles(self, bi):
156 self.datafiles = {}
157 for dfs in self._get_elements(bi, "DataFiles"):
158 pkg_name = dfs.getAttribute("package")
159 files = []
160 for e in self._get_elements(dfs, "DataFile"):
161 filename = self._get_element_text(e)
162 files.append(filename)
163 if files:
164 if not pkg_name:
165 pkg_name = self.package
166 self.datafiles[pkg_name] = files
167
168 def _get_dependencies(self, bi):
169 self.dependencies = []
170 try:
171 deps = self._get_singleton(bi, "Dependencies")
172 except ValueError:
173 # Dependencies is optional, although
174 # ChimeraXCore *should* always be present
175 return
176 for e in self._get_elements(deps, "Dependency"):
177 pkg = e.getAttribute("name")
178 ver = e.getAttribute("version")
179 self.dependencies.append("%s %s" % (pkg, ver))
180
181 def _get_c_modules(self, bi):
182 self.c_modules = []
183 for cm in self._get_elements(bi, "CModule"):
184 mod_name = cm.getAttribute("name")
185 try:
186 major = int(cm.getAttribute("major_version"))
187 except ValueError:
188 major = 0
189 try:
190 minor = int(cm.getAttribute("minor_version"))
191 except ValueError:
192 minor = 1
193 uses_numpy = cm.getAttribute("usesNumpy") == "true"
194 c = _CModule(mod_name, uses_numpy, major, minor)
195 self._add_c_options(c, cm)
196 self.c_modules.append(c)
197
198 def _get_c_libraries(self, bi):
199 self.c_libraries = []
200 for lib in self._get_elements(bi, "CLibrary"):
201 c = _CLibrary(lib.getAttribute("name"),
202 lib.getAttribute("usesNumpy") == "true",
203 lib.getAttribute("static") == "true")
204 self._add_c_options(c, lib)
205 self.c_libraries.append(c)
206
207 def _add_c_options(self, c, ce):
208 for e in self._get_elements(ce, "Requires"):
209 c.add_require(self._get_element_text(e))
210 for e in self._get_elements(ce, "SourceFile"):
211 c.add_source_file(self._get_element_text(e))
212 for e in self._get_elements(ce, "IncludeDir"):
213 c.add_include_dir(self._get_element_text(e))
214 for e in self._get_elements(ce, "Library"):
215 c.add_library(self._get_element_text(e))
216 for e in self._get_elements(ce, "LibraryDir"):
217 c.add_library_dir(self._get_element_text(e))
218 for e in self._get_elements(ce, "CompileArgument"):
219 c.add_compile_argument(self._get_element_text(e))
220 for e in self._get_elements(ce, "LinkArgument"):
221 c.add_link_argument(self._get_element_text(e))
222 for e in self._get_elements(ce, "Framework"):
223 c.add_framework(self._get_element_text(e))
224 for e in self._get_elements(ce, "FrameworkDir"):
225 c.add_framework_dir(self._get_element_text(e))
226 for e in self._get_elements(ce, "Define"):
227 edef = self._get_element_text(e).split("=")
228 if len(edef) > 2:
229 raise TypeError(
230 "Too many arguments for macro definition: {}".format(edef))
231 elif len(edef) == 1:
232 edef.append(None)
233 c.add_macro_define(*edef)
234 for e in self._get_elements(ce, "Undefine"):
235 c.add_macro_undef(self._get_element_text(e))
236
237 def _get_packages(self, bi):
238 self.packages = []
239 try:
240 pkgs = self._get_singleton(bi, "AdditionalPackages")
241 except ValueError:
242 # AdditionalPackages is optional
243 return
244 for pkg in self._get_elements(pkgs, "Package"):
245 pkg_name = pkg.getAttribute("name")
246 pkg_folder = pkg.getAttribute("folder")
247 self.packages.append((pkg_name, pkg_folder))
248
249 def _get_classifiers(self, bi):
250 self.python_classifiers = [
251 "Framework :: ChimeraX",
252 "Intended Audience :: Science/Research",
253 "Programming Language :: Python :: 3",
254 "Topic :: Scientific/Engineering :: Visualization",
255 "Topic :: Scientific/Engineering :: Chemistry",
256 "Topic :: Scientific/Engineering :: Bio-Informatics",
257 ]
258 cls = self._get_singleton(bi, "Classifiers")
259 for e in self._get_elements(cls, "PythonClassifier"):
260 self.python_classifiers.append(self._get_element_text(e))
261 self.chimerax_classifiers = [
262 ("ChimeraX :: Bundle :: " + ','.join(self.categories) +
263 " :: " + self.min_session + "," + self.max_session +
264 " :: " + self.package + " :: :: " + self.custom_init)
265 ]
266 for e in self._get_elements(cls, "ChimeraXClassifier"):
267 self.chimerax_classifiers.append(self._get_element_text(e))
268
269 def _is_pure_python(self):
270 return (not self.c_modules and not self.c_libraries
271 and self.pure_python != "false")
272
273 def _make_setup_arguments(self):
274 def add_argument(name, value):
275 if value:
276 self.setup_arguments[name] = value
277 self.setup_arguments = {"name": self.name,
278 "python_requires": ">= 3.6"}
279 add_argument("version", self.version)
280 add_argument("description", self.synopsis)
281 add_argument("long_description", self.description)
282 add_argument("author", self.author)
283 add_argument("author_email", self.email)
284 add_argument("url", self.url)
285 add_argument("install_requires", self.dependencies)
286 add_argument("license", self.license)
287 add_argument("package_data", self.datafiles)
288 # We cannot call find_packages unless we are already
289 # in the right directory, and that will not happen
290 # until run_setup. So we do the package stuff there.
291 ext_mods = [em for em in [cm.ext_mod(self.logger, self.package,
292 self.dependencies)
293 for cm in self.c_modules]
294 if em is not None]
295 if not self._is_pure_python():
296 import sys
297 if sys.platform == "darwin":
298 env = "Environment :: MacOS X :: Aqua",
299 op_sys = "Operating System :: MacOS :: MacOS X"
300 elif sys.platform == "win32":
301 env = "Environment :: Win32 (MS Windows)"
302 op_sys = "Operating System :: Microsoft :: Windows :: Windows 10"
303 else:
304 env = "Environment :: X11 Applications"
305 op_sys = "Operating System :: POSIX :: Linux"
306 platform_classifiers = [env, op_sys]
307 if not ext_mods:
308 # From https://stackoverflow.com/questions/35112511/pip-setup-py-bdist-wheel-no-longer-builds-forced-non-pure-wheels
309 from setuptools.dist import Distribution
310 class BinaryDistribution(Distribution):
311 def has_ext_modules(foo):
312 return True
313 self.setup_arguments["distclass"] = BinaryDistribution
314 else:
315 # pure Python
316 platform_classifiers = [
317 "Environment :: MacOS X :: Aqua",
318 "Environment :: Win32 (MS Windows)",
319 "Environment :: X11 Applications",
320 "Operating System :: MacOS :: MacOS X",
321 "Operating System :: Microsoft :: Windows :: Windows 10",
322 "Operating System :: POSIX :: Linux",
323 ]
324 self.python_classifiers.extend(platform_classifiers)
325 self.setup_arguments["ext_modules"] = ext_mods
326 self.setup_arguments["classifiers"] = (self.python_classifiers +
327 self.chimerax_classifiers)
328
329 def _make_package_arguments(self):
330 from setuptools import find_packages
331 def add_package(base_package, folder):
332 package_dir[base_package] = folder
333 packages.append(base_package)
334 packages.extend([base_package + "." + sub_pkg
335 for sub_pkg in find_packages(folder)])
336 package_dir = {}
337 packages = []
338 add_package(self.package, "src")
339 for name, folder in self.packages:
340 add_package(name, folder)
341 return package_dir, packages
342
343 def _make_paths(self):
344 import os.path
345 from .wheel_tag import tag
346 self.tag = tag(self._is_pure_python())
347 self.bundle_base_name = self.name.replace("ChimeraX-", "")
348 bundle_wheel_name = self.name.replace("-", "_")
349 wheel = "%s-%s-%s.whl" % (bundle_wheel_name, self.version, self.tag)
350 self.wheel_path = os.path.join(self.path, "dist", wheel)
351 self.egg_info = os.path.join(self.path, bundle_wheel_name + ".egg-info")
352
353 def _run_setup(self, cmd):
354 import os, sys, setuptools
355 cwd = os.getcwd()
356 save = sys.argv
357 try:
358 os.chdir(self.path)
359 kw = self.setup_arguments.copy()
360 kw["package_dir"], kw["packages"] = self._make_package_arguments()
361 sys.argv = ["setup.py"] + cmd
362 setuptools.setup(**kw)
363 return True
364 except:
365 import traceback
366 traceback.print_exc()
367 return False
368 finally:
369 sys.argv = save
370 os.chdir(cwd)
371
372 #
373 # Utility functions dealing with XML tree
374 #
375 def _get_elements(self, e, tag):
376 tagged_elements = e.getElementsByTagName(tag)
377 # Mark element as used even for non-applicable platform
378 self._used_elements.update(tagged_elements)
379 elements = []
380 for se in tagged_elements:
381 platform = se.getAttribute("platform")
382 if not platform or platform in self._platform_names:
383 elements.append(se)
384 return elements
385
386 def _get_element_text(self, e):
387 text = ""
388 for node in e.childNodes:
389 if node.nodeType == node.TEXT_NODE:
390 text += node.data
391 return text.strip()
392
393 def _get_singleton(self, bi, tag):
394 elements = bi.getElementsByTagName(tag)
395 self._used_elements.update(elements)
396 if len(elements) > 1:
397 raise ValueError("too many %s elements" % repr(tag))
398 elif len(elements) == 0:
399 raise ValueError("%s element is missing" % repr(tag))
400 return elements[0]
401
402 def _get_singleton_text(self, bi, tag):
403 return self._get_element_text(self._get_singleton(bi, tag))
404
405 def _check_unused_elements(self, bi):
406 for node in bi.childNodes:
407 if node.nodeType != node.ELEMENT_NODE:
408 continue
409 if node not in self._used_elements:
410 print("WARNING: unsupported element:", node.nodeName)
411
412import os
413class _CompiledCode:
414 def __init__(self, name, uses_numpy):
415 self.name = name
416 self.uses_numpy = uses_numpy
417 self.requires = []
418 self.source_files = []
419 self.frameworks = []
420 self.libraries = []
421 self.compile_arguments = []
422 self.link_arguments = []
423 self.include_dirs = []
424 self.library_dirs = []
425 self.framework_dirs = []
426 self.macros = []
427
428 def add_require(self, req):
429 self.requires.append(req)
430
431 def add_source_file(self, f):
432 self.source_files.append(f)
433
434 def add_include_dir(self, d):
435 self.include_dirs.append(d)
436
437 def add_library(self, l):
438 self.libraries.append(l)
439
440 def add_library_dir(self, d):
441 self.library_dirs.append(d)
442
443 def add_compile_argument(self, a):
444 self.compile_arguments.append(a)
445
446 def add_link_argument(self, a):
447 self.link_arguments.append(a)
448
449 def add_framework(self, f):
450 self.frameworks.append(f)
451
452 def add_framework_dir(self, d):
453 self.framework_dirs.append(d)
454
455 def add_macro_define(self, m, val):
456 # 2-tuple defines (set val to None to define without a value)
457 self.macros.append((m, val))
458
459 def add_macro_undef(self, m):
460 # 1-tuple of macro name undefines
461 self.macros.append((m,))
462
463 def _compile_options(self, logger, dependencies):
464 import sys, os.path
465 for req in self.requires:
466 if not os.path.exists(req):
467 raise ValueError("unused on this platform")
468 # platform-specific
469 # Assume Python executable is in ROOT/bin/python
470 # and make include directory be ROOT/include
471 root = os.path.dirname(os.path.dirname(sys.executable))
472 inc_dirs = [os.path.join(root, "include")]
473 lib_dirs = [os.path.join(root, "lib")]
474 if self.uses_numpy:
475 from numpy.distutils.misc_util import get_numpy_include_dirs
476 inc_dirs.extend(get_numpy_include_dirs())
477 if sys.platform == "darwin":
478 libraries = self.libraries
479 # Unfortunately, clang on macOS (for now) exits
480 # when receiving a -std=c++11 option when compiling
481 # a C (not C++) source file, which is why this value
482 # is named "cpp_flags" not "compile_flags"
483 cpp_flags = ["-std=c++11", "-stdlib=libc++"]
484 extra_link_args = ["-F" + d for d in self.framework_dirs]
485 for fw in self.frameworks:
486 extra_link_args.extend(["-framework", fw])
487 elif sys.platform == "win32":
488 libraries = []
489 for lib in self.libraries:
490 if lib.lower().endswith(".lib"):
491 # Strip the .lib since suffixes are handled automatically
492 libraries.append(lib[:-4])
493 else:
494 libraries.append("lib" + lib)
495 cpp_flags = []
496 extra_link_args = []
497 else:
498 libraries = self.libraries
499 cpp_flags = ["-std=c++11"]
500 extra_link_args = []
501 for req in self.requires:
502 if not os.path.exists(req):
503 return None
504 inc_dirs.extend(self.include_dirs)
505 lib_dirs.extend(self.library_dirs)
506 for dep in dependencies:
507 d_inc, d_lib = self._get_bundle_dirs(logger, dep)
508 if d_inc:
509 inc_dirs.append(d_inc)
510 if d_lib:
511 lib_dirs.append(d_lib)
512 extra_link_args.extend(self.link_arguments)
513 return inc_dirs, lib_dirs, self.macros, extra_link_args, libraries, cpp_flags
514
515 def _get_bundle_dirs(self, logger, dep):
516 from chimerax.core import toolshed
517 # It's either pulling unsupported class from pip or roll our own.
518 from pip.req.req_install import InstallRequirement
519 ir = InstallRequirement.from_line(dep)
520 if not ir.check_if_exists():
521 raise RuntimeError("unsatisfied dependency: %s" % dep)
522 ts = toolshed.get_toolshed()
523 bundle = ts.find_bundle(ir.name, logger)
524 if not bundle:
525 return None, None
526 inc = bundle.include_dir()
527 lib = bundle.library_dir()
528 return inc, lib
529
530
531class _CModule(_CompiledCode):
532
533 def __init__(self, name, uses_numpy, major, minor):
534 super().__init__(name, uses_numpy)
535 self.major = major
536 self.minor = minor
537
538 def ext_mod(self, logger, package, dependencies):
539 from setuptools import Extension
540 try:
541 (inc_dirs, lib_dirs, macros, extra_link_args,
542 libraries, cpp_flags) = self._compile_options(logger, dependencies)
543 macros.extend([("MAJOR_VERSION", self.major),
544 ("MINOR_VERSION", self.minor)])
545 except ValueError:
546 return None
547 import sys
548 if sys.platform == "linux":
549 extra_link_args.append("-Wl,-rpath,\$ORIGIN")
550 return Extension(package + '.' + self.name,
551 define_macros=macros,
552 extra_compile_args=cpp_flags+self.compile_arguments,
553 include_dirs=inc_dirs,
554 library_dirs=lib_dirs,
555 libraries=libraries,
556 extra_link_args=extra_link_args,
557 sources=self.source_files)
558
559
560class _CLibrary(_CompiledCode):
561
562 def __init__(self, name, uses_numpy, static):
563 super().__init__(name, uses_numpy)
564 self.static = static
565
566 def compile(self, logger, dependencies, debug=False):
567 import sys, os, os.path, distutils.ccompiler, distutils.sysconfig
568 import distutils.log
569 distutils.log.set_verbosity(1)
570 try:
571 (inc_dirs, lib_dirs, macros, extra_link_args,
572 libraries, cpp_flags) = self._compile_options(logger, dependencies)
573 except ValueError:
574 print("Error on {}".format(self.name))
575 return None
576 output_dir = "src"
577 compiler = distutils.ccompiler.new_compiler()
578 distutils.sysconfig.customize_compiler(compiler)
579 if inc_dirs:
580 compiler.set_include_dirs(inc_dirs)
581 if lib_dirs:
582 compiler.set_library_dirs(lib_dirs)
583 if libraries:
584 compiler.set_libraries(libraries)
585 compiler.add_include_dir(distutils.sysconfig.get_python_inc())
586 if sys.platform == "win32":
587 # Link library directory for Python on Windows
588 compiler.add_library_dir(os.path.join(sys.exec_prefix, 'libs'))
589 lib_name = "lib" + self.name
590 else:
591 lib_name = self.name
592 if not self.static:
593 macros.append(("DYNAMIC_LIBRARY", 1))
594 # compiler.define_macro("DYNAMIC_LIBRARY", 1)
595
596 from concurrent.futures import ThreadPoolExecutor
597 import os
598 results = []
599 with ThreadPoolExecutor(max_workers=os.cpu_count()-1) as executor:
600 # with ThreadPoolExecutor(max_workers=1) as executor:
601 for f in self.source_files:
602 l = compiler.detect_language(f)
603 if l == 'c':
604 preargs = []
605 elif l == 'c++':
606 preargs = cpp_flags
607 else:
608 raise RuntimeError('Unsupported language for {}'.format(f))
609 results.append(
610 executor.submit(
611 compiler.compile, [f],
612 extra_preargs=preargs+self.compile_arguments,
613 macros=macros, debug=debug))
614 #Ensure all have finished before continuing
615 for r in results:
616 r.result()
617 #compiler.compile(self.source_files, extra_preargs=cpp_flags, macros=macros, debug=debug)
618 objs = compiler.object_filenames(self.source_files)
619 compiler.mkpath(output_dir)
620 if self.static:
621 lib = compiler.library_filename(lib_name, lib_type="static")
622 compiler.create_static_lib(objs, lib_name, output_dir=output_dir,
623 debug=debug)
624 else:
625 if sys.platform == "darwin":
626 # On Mac, we only need the .dylib and it MUST be compiled
627 # with "-dynamiclib", not "-bundle". Hence the giant hack:
628 try:
629 n = compiler.linker_so.index("-bundle")
630 except ValueError:
631 pass
632 else:
633 compiler.linker_so[n] = "-dynamiclib"
634 lib = compiler.library_filename(lib_name, lib_type="dylib")
635 extra_link_args.append("-Wl,-install_name,@loader_path/%s" % lib)
636 compiler.link_shared_object(objs, lib, output_dir=output_dir,
637 extra_preargs=extra_link_args,
638 debug=debug)
639 elif sys.platform == "win32":
640 # On Windows, we need both .dll and .lib
641 link_lib = compiler.library_filename(lib_name, lib_type="static")
642 extra_link_args.append("/LIBPATH:%s" % link_lib)
643 lib = compiler.shared_object_filename(lib_name)
644 compiler.link_shared_object(objs, lib, output_dir=output_dir,
645 extra_preargs=extra_link_args,
646 debug=debug)
647 else:
648 # On Linux, we only need the .so
649 lib = compiler.library_filename(lib_name, lib_type="shared")
650 extra_link_args.append("-Wl,-rpath,\$ORIGIN")
651 compiler.link_shared_object(objs, lib, output_dir=output_dir,
652 extra_preargs=extra_link_args,
653 debug=debug)
654 return lib
655
656if __name__ == "__main__" or __name__.startswith("ChimeraX_sandbox"):
657 import sys
658 bb = BundleBuilder()
659 for cmd in sys.argv[1:]:
660 if cmd == "wheel":
661 bb.make_wheel()
662 elif cmd == "install":
663 try:
664 bb.make_install(session)
665 except NameError:
666 print("%s only works from ChimeraX, not Python" % repr(cmd))
667 elif cmd == "clean":
668 bb.make_clean()
669 elif cmd == "dump":
670 bb.dump()
671 else:
672 print("unknown command: %s" % repr(cmd))
673 raise SystemExit(0)