Ticket #1256: bundle_builder.py

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

Alternate bundle_builder.py

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