makewheel.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. """
  2. Generates a wheel (.whl) file from the output of makepanda.
  3. """
  4. from __future__ import print_function, unicode_literals
  5. import distutils.util
  6. import json
  7. import sys
  8. import os
  9. from os.path import join
  10. import zipfile
  11. import hashlib
  12. import tempfile
  13. import subprocess
  14. import configparser
  15. from distutils.sysconfig import get_config_var
  16. from optparse import OptionParser
  17. from base64 import urlsafe_b64encode
  18. cfg_parser = None
  19. def get_metadata_value(key):
  20. global cfg_parser
  21. if not cfg_parser:
  22. # Parse the metadata from the setup.cfg file.
  23. cfg_parser = configparser.ConfigParser()
  24. path = os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')
  25. assert cfg_parser.read(path), "Could not read setup.cfg file."
  26. value = cfg_parser.get('metadata', key)
  27. if key == 'classifiers':
  28. value = value.strip().split('\n')
  29. return value
  30. def get_host():
  31. """Returns the host platform, ie. the one we're compiling on."""
  32. if sys.platform == 'win32' or sys.platform == 'cygwin':
  33. # sys.platform is win32 on 64-bits Windows as well.
  34. return 'windows'
  35. elif sys.platform == 'darwin':
  36. return 'darwin'
  37. elif sys.platform.startswith('linux'):
  38. try:
  39. # Python seems to offer no built-in way to check this.
  40. osname = subprocess.check_output(["uname", "-o"])
  41. if osname.strip().lower() == b'android':
  42. return 'android'
  43. else:
  44. return 'linux'
  45. except:
  46. return 'linux'
  47. elif sys.platform.startswith('freebsd'):
  48. return 'freebsd'
  49. else:
  50. exit('Unrecognized sys.platform: %s' % (sys.platform))
  51. def locate_binary(binary):
  52. """
  53. Searches the system PATH for the binary.
  54. :param binary: Name of the binary to locate.
  55. :return: The full path to the binary, or None if not found.
  56. """
  57. if os.path.isfile(binary):
  58. return binary
  59. if "PATH" not in os.environ or os.environ["PATH"] == "":
  60. p = os.defpath
  61. else:
  62. p = os.environ["PATH"]
  63. pathList = p.split(os.pathsep)
  64. suffixes = ['']
  65. if get_host() == 'windows':
  66. if not binary.lower().endswith('.exe') and not binary.lower().endswith('.bat'):
  67. # Append .exe if necessary
  68. suffixes = ['.exe', '.bat']
  69. # On Windows the current directory is always implicitly
  70. # searched before anything else on PATH.
  71. pathList = ['.'] + pathList
  72. for path in pathList:
  73. binpath = os.path.join(os.path.expanduser(path), binary)
  74. for suffix in suffixes:
  75. if os.access(binpath + suffix, os.X_OK):
  76. return os.path.abspath(os.path.realpath(binpath + suffix))
  77. return None
  78. def get_abi_tag():
  79. soabi = get_config_var('SOABI')
  80. if soabi and soabi.startswith('cpython-'):
  81. return 'cp' + soabi.split('-')[1]
  82. elif soabi:
  83. return soabi.replace('.', '_').replace('-', '_')
  84. soabi = 'cp%d%d' % (sys.version_info[:2])
  85. if sys.version_info >= (3, 8):
  86. return soabi
  87. debug_flag = get_config_var('Py_DEBUG')
  88. if (debug_flag is None and hasattr(sys, 'gettotalrefcount')) or debug_flag:
  89. soabi += 'd'
  90. return soabi
  91. def is_exe_file(path):
  92. return os.path.isfile(path) and path.lower().endswith('.exe')
  93. def is_elf_file(path):
  94. base = os.path.basename(path)
  95. return os.path.isfile(path) and '.' not in base and \
  96. open(path, 'rb').read(4) == b'\x7FELF'
  97. def is_macho_or_fat_file(path):
  98. base = os.path.basename(path)
  99. return os.path.isfile(path) and '.' not in base and \
  100. open(path, 'rb').read(4) in (b'\xFE\xED\xFA\xCE', b'\xCE\xFA\xED\xFE',
  101. b'\xFE\xED\xFA\xCF', b'\xCF\xFA\xED\xFE',
  102. b'\xCA\xFE\xBA\xBE', b'\xBE\xBA\xFE\xCA',
  103. b'\xCA\xFE\xBA\xBF', b'\xBF\xBA\xFE\xCA')
  104. def is_fat_file(path):
  105. return os.path.isfile(path) and \
  106. open(path, 'rb').read(4) in (b'\xCA\xFE\xBA\xBE', b'\xBE\xBA\xFE\xCA',
  107. b'\xCA\xFE\xBA\xBF', b'\xBF\xBA\xFE\xCA')
  108. def get_python_ext_module_dir():
  109. import _ctypes
  110. return os.path.dirname(_ctypes.__file__)
  111. if sys.platform in ('win32', 'cygwin'):
  112. is_executable = is_exe_file
  113. elif sys.platform == 'darwin':
  114. is_executable = is_macho_or_fat_file
  115. else:
  116. is_executable = is_elf_file
  117. class TargetInfo:
  118. """
  119. Holds information about the system the wheel is being prepared for.
  120. """
  121. def __init__(self,
  122. platform_tag=None,
  123. soabi='',
  124. python_version=None,
  125. python_root=sys.exec_prefix,
  126. sys_platform=get_host(),
  127. static_panda=False):
  128. """
  129. With no arguments, it will be assumed that the target is the same as the
  130. host (which will be most cases).
  131. :param platform_string: The platform tag used in the wheel filename.
  132. :param soabi: Value of SOABI from Python's makefile. This isn't really
  133. a thing with Python 2.
  134. :param python_version: Version of Python we're bundling. Will be
  135. inferred from SOABI or the system's python version if not
  136. explicitly set.
  137. :param python_root: Root of the Python installation containing the
  138. libraries to be bundled in deploy_libs.
  139. :param sys_platform:
  140. """
  141. if platform_tag:
  142. self.platform_tag = platform_tag
  143. else:
  144. self.platform_tag = distutils.util.get_platform()
  145. if (self.platform_tag.startswith("linux-")
  146. and os.path.isfile("/lib/libc-2.5.so")
  147. and os.path.isdir("/opt/python")):
  148. self.platform_tag = self.platform_tag.replace("linux",
  149. "manylinux1")
  150. self.abi_flags = None
  151. self.soabi = soabi
  152. self.python_version = python_version
  153. self.python_root = python_root
  154. self.sys_platform = sys_platform
  155. self.static_panda = static_panda
  156. self.platform_tag = self.platform_tag.replace('-', '_').replace('.', '_')
  157. if not self.python_version:
  158. if self.soabi:
  159. self.python_version = tuple(
  160. int(i) for i in self.soabi.split('-')[1].rstrip('mdu')
  161. )
  162. print("Inferring Python version %s from provided soabi."
  163. % str(self.python_version))
  164. else:
  165. self.python_version = sys.version_info[:2]
  166. self.soabi = get_config_var('SOABI')
  167. print("Inferring Python version %s from system version."
  168. % str(self.python_version))
  169. elif type(self.python_version) is str:
  170. self.abi_flags = self.python_version.lstrip('0123456789')
  171. self.python_version = tuple(int(i) for i in self.python_version.rstrip('mdu'))
  172. @property
  173. def python_ext_module_dir(self):
  174. return os.path.join(self.python_root,
  175. 'lib/python{}.{}/lib-dynload'
  176. .format(*self.python_version))
  177. @property
  178. def python_tag(self):
  179. return "cp{0}{1}".format(*self.python_version)
  180. @property
  181. def abi_tag(self):
  182. if sys.version_info >= (3, 0) and self.soabi:
  183. if self.soabi.startswith('cpython-'):
  184. return 'cp' + self.soabi.split('-')[1]
  185. return self.soabi.replace('.', '_').replace('-', '_')
  186. abi_tag = self.python_tag
  187. if self.abi_flags:
  188. return abi_tag + self.abi_flags
  189. debug_flag = get_config_var('Py_DEBUG')
  190. if (debug_flag is None and hasattr(sys,
  191. 'gettotalrefcount')) or debug_flag:
  192. abi_tag += 'd'
  193. malloc_flag = get_config_var('WITH_PYMALLOC')
  194. if malloc_flag is None or malloc_flag:
  195. abi_tag += 'm'
  196. if sys.version_info < (3, 3):
  197. usize = get_config_var('Py_UNICODE_SIZE')
  198. if (usize is None and sys.maxunicode == 0x10ffff) or usize == 4:
  199. abi_tag += 'u'
  200. return abi_tag
  201. @property
  202. def extension_suffix(self):
  203. if 'ios' in self.platform_tag:
  204. return '.a' if self.static_panda else '.so'
  205. if self.soabi != '':
  206. ext = '.pyd' if self.sys_platform == 'win32' else '.so'
  207. return '.' + self.soabi + ext
  208. import _imp
  209. return _imp.extension_suffixes()[0]
  210. # Other global parameters
  211. # PY_VERSION = "cp{0}{1}".format(*sys.version_info)
  212. # ABI_TAG = get_abi_tag()
  213. EXCLUDE_EXT = [".pyc", ".pyo", ".N", ".prebuilt", ".xcf", ".plist", ".vcproj", ".sln"]
  214. # Plug-ins to install.
  215. PLUGIN_LIBS = ["pandagl", "pandagles", "pandagles2", "pandadx9", "p3tinydisplay", "p3ptloader", "p3assimp", "p3ffmpeg", "p3openal_audio", "p3fmod_audio"]
  216. # Libraries included in manylinux ABI that should be ignored. See PEP 513/571/599.
  217. MANYLINUX_LIBS = [
  218. "libgcc_s.so.1", "libstdc++.so.6", "libm.so.6", "libdl.so.2", "librt.so.1",
  219. "libcrypt.so.1", "libc.so.6", "libnsl.so.1", "libutil.so.1",
  220. "libpthread.so.0", "libresolv.so.2", "libX11.so.6", "libXext.so.6",
  221. "libXrender.so.1", "libICE.so.6", "libSM.so.6", "libGL.so.1",
  222. "libgobject-2.0.so.0", "libgthread-2.0.so.0", "libglib-2.0.so.0",
  223. # These are not mentioned in manylinux1 spec but should nonetheless always
  224. # be excluded.
  225. "linux-vdso.so.1", "linux-gate.so.1", "ld-linux.so.2", "libdrm.so.2",
  226. ]
  227. # Binaries to never scan for dependencies on non-Windows systems.
  228. IGNORE_UNIX_DEPS_OF = [
  229. "panda3d_tools/pstats",
  230. ]
  231. WHEEL_DATA = """Wheel-Version: 1.0
  232. Generator: makepanda
  233. Root-Is-Purelib: false
  234. Tag: {0}-{1}-{2}
  235. """
  236. PROJECT_URLS = dict([line.split('=', 1) for line in get_metadata_value('project_urls').strip().splitlines()])
  237. METADATA = {
  238. "license": get_metadata_value('license'),
  239. "name": get_metadata_value('name'),
  240. "metadata_version": "2.0",
  241. "generator": "makepanda",
  242. "summary": get_metadata_value('description'),
  243. "extensions": {
  244. "python.details": {
  245. "project_urls": dict(PROJECT_URLS, Home=get_metadata_value('url')),
  246. "document_names": {
  247. "license": "LICENSE.txt"
  248. },
  249. "contacts": [
  250. {
  251. "role": "author",
  252. "name": get_metadata_value('author'),
  253. "email": get_metadata_value('author_email'),
  254. }
  255. ]
  256. }
  257. },
  258. "classifiers": get_metadata_value('classifiers'),
  259. }
  260. DESCRIPTION = """
  261. The Panda3D free 3D game engine
  262. ===============================
  263. Panda3D is a powerful 3D engine written in C++, with a complete set of Python
  264. bindings. Unlike other engines, these bindings are automatically generated,
  265. meaning that they are always up-to-date and complete: all functions of the
  266. engine can be controlled from Python. All major Panda3D applications have been
  267. written in Python, this is the intended way of using the engine.
  268. Panda3D now supports automatic shader generation, which now means you can use
  269. normal maps, gloss maps, glow maps, HDR, cartoon shading, and the like without
  270. having to write any shaders.
  271. Panda3D is a modern engine supporting advanced features such as shaders,
  272. stencil, and render-to-texture. Panda3D is unusual in that it emphasizes a
  273. short learning curve, rapid development, and extreme stability and robustness.
  274. Panda3D is free software that runs under Windows, Linux, or macOS.
  275. The Panda3D team is very concerned with making the engine accessible to new
  276. users. We provide a detailed manual, a complete API reference, and a large
  277. collection of sample programs to help you get started. We have active forums,
  278. with many helpful users, and the developers are regularly online to answer
  279. questions.
  280. """
  281. PANDA3D_TOOLS_INIT = """import os, sys
  282. import panda3d
  283. dir = os.path.dirname(panda3d.__file__)
  284. del panda3d
  285. if sys.platform in ('win32', 'cygwin'):
  286. path_var = 'PATH'
  287. if hasattr(os, 'add_dll_directory'):
  288. os.add_dll_directory(dir)
  289. elif sys.platform == 'darwin':
  290. path_var = 'DYLD_LIBRARY_PATH'
  291. else:
  292. path_var = 'LD_LIBRARY_PATH'
  293. if not os.environ.get(path_var):
  294. os.environ[path_var] = dir
  295. else:
  296. os.environ[path_var] = dir + os.pathsep + os.environ[path_var]
  297. del os, sys, path_var, dir
  298. def _exec_tool(tool):
  299. import os, sys
  300. from subprocess import Popen
  301. tools_dir = os.path.dirname(__file__)
  302. handle = Popen(sys.argv, executable=os.path.join(tools_dir, tool))
  303. try:
  304. try:
  305. return handle.wait()
  306. except KeyboardInterrupt:
  307. # Give the program a chance to handle the signal gracefully.
  308. return handle.wait()
  309. except:
  310. handle.kill()
  311. handle.wait()
  312. raise
  313. # Register all the executables in this directory as global functions.
  314. {0}
  315. """
  316. def parse_dependencies_windows(data):
  317. """ Parses the given output from dumpbin /dependents to determine the list
  318. of dll's this executable file depends on. """
  319. lines = data.splitlines()
  320. li = 0
  321. while li < len(lines):
  322. line = lines[li]
  323. li += 1
  324. if line.find(' has the following dependencies') != -1:
  325. break
  326. if li < len(lines):
  327. line = lines[li]
  328. if line.strip() == '':
  329. # Skip a blank line.
  330. li += 1
  331. # Now we're finding filenames, until the next blank line.
  332. filenames = []
  333. while li < len(lines):
  334. line = lines[li]
  335. li += 1
  336. line = line.strip()
  337. if line == '':
  338. # We're done.
  339. return filenames
  340. filenames.append(line)
  341. # At least we got some data.
  342. return filenames
  343. def parse_dependencies_unix(data):
  344. """ Parses the given output from otool -XL or ldd to determine the list of
  345. libraries this executable file depends on. """
  346. lines = data.splitlines()
  347. filenames = []
  348. for l in lines:
  349. l = l.strip()
  350. if l != "statically linked":
  351. filenames.append(l.split(' ', 1)[0])
  352. return filenames
  353. def scan_dependencies(pathname, target_info):
  354. """ Checks the named file for DLL dependencies, and adds any appropriate
  355. dependencies found into pluginDependencies and dependentFiles. """
  356. if target_info.sys_platform in ("darwin", "ios"):
  357. command = ['otool', '-XL', pathname]
  358. elif target_info.sys_platform in ("win32", "cygwin"):
  359. command = ['dumpbin', '/dependents', pathname]
  360. else:
  361. command = ['ldd', pathname]
  362. process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
  363. output, unused_err = process.communicate()
  364. retcode = process.poll()
  365. if retcode:
  366. raise subprocess.CalledProcessError(retcode, command[0], output=output)
  367. filenames = None
  368. if target_info.sys_platform in ("win32", "cygwin"):
  369. filenames = parse_dependencies_windows(output)
  370. else:
  371. filenames = parse_dependencies_unix(output)
  372. if filenames is None:
  373. sys.exit("Unable to determine dependencies from %s" % (pathname))
  374. if target_info.sys_platform in ("darwin", "ios") and len(filenames) > 0:
  375. # Filter out the library ID.
  376. if os.path.basename(filenames[0]).split('.', 1)[0] == os.path.basename(pathname).split('.', 1)[0]:
  377. del filenames[0]
  378. return filenames
  379. class WheelFile(object):
  380. def __init__(self, name, version, target_info):
  381. self.name = name
  382. self.version = version
  383. self.target_info = target_info
  384. wheel_name = "{0}-{1}-{2}-{3}-{4}.whl".format(
  385. name, version, target_info.python_tag, target_info.abi_tag, target_info.platform_tag)
  386. print("Writing %s" % (wheel_name))
  387. self.zip_file = zipfile.ZipFile(wheel_name, 'w', zipfile.ZIP_DEFLATED)
  388. self.records = []
  389. # Used to locate dependency libraries.
  390. self.lib_path = []
  391. self.dep_paths = {}
  392. self.ignore_deps = set()
  393. def consider_add_dependency(self, target_path, dep, search_path=None):
  394. """Considers adding a dependency library.
  395. Returns the target_path if it was added, which may be different from
  396. target_path if it was already added earlier, or None if it wasn't."""
  397. if dep in self.dep_paths:
  398. # Already considered this.
  399. return self.dep_paths[dep]
  400. self.dep_paths[dep] = None
  401. if dep in self.ignore_deps or dep.lower().startswith("python") or os.path.basename(dep).startswith("libpython"):
  402. # Don't include the Python library, or any other explicit ignore.
  403. if verbose:
  404. print("Ignoring {0} (explicitly ignored)".format(dep))
  405. return
  406. if target_info.sys_platform in ("darwin", "ios") and dep.endswith(".so"):
  407. # Temporary hack for 1.9, which had link deps on modules.
  408. return
  409. if target_info.sys_platform in ("darwin", "ios") and dep.startswith("/System/"):
  410. return
  411. if dep.startswith('/'):
  412. source_path = dep
  413. else:
  414. source_path = None
  415. if search_path is None:
  416. search_path = self.lib_path
  417. for lib_dir in search_path:
  418. # Ignore static stuff.
  419. path = os.path.join(lib_dir, dep)
  420. if os.path.isfile(path):
  421. source_path = os.path.normpath(path)
  422. break
  423. if not source_path:
  424. # Couldn't find library in the panda3d lib dir.
  425. if verbose:
  426. print("Ignoring {0} (not in search path)".format(dep))
  427. return
  428. self.dep_paths[dep] = target_path
  429. self.write_file(target_path, source_path)
  430. return target_path
  431. def write_file(self, target_path, source_path):
  432. """Adds the given file to the .whl file."""
  433. orig_source_path = source_path
  434. # If this is a .so file, we should set the rpath appropriately.
  435. temp = None
  436. basename, ext = os.path.splitext(source_path)
  437. if ext in ('.so', '.dylib') or '.so.' in os.path.basename(source_path) or \
  438. (not ext and is_executable(source_path)):
  439. # Scan Unix dependencies.
  440. if target_path not in IGNORE_UNIX_DEPS_OF:
  441. deps = scan_dependencies(source_path, target_info)
  442. else:
  443. deps = []
  444. suffix = ''
  445. if '.so' in os.path.basename(source_path):
  446. suffix = '.so'
  447. elif ext == '.dylib':
  448. suffix = '.dylib'
  449. temp = tempfile.NamedTemporaryFile(suffix=suffix, prefix='whl', delete=False)
  450. # On macOS, if no fat wheel was requested, extract the right architecture.
  451. if target_info.sys_platform == "darwin" and is_fat_file(source_path) \
  452. and not target_info.platform_tag.endswith("_intel") \
  453. and "_fat" not in target_info.platform_tag:
  454. if target_info.platform_tag.endswith("_x86_64"):
  455. arch = 'x86_64'
  456. else:
  457. arch = target_info.platform_tag.split('_')[-1]
  458. subprocess.call(['lipo', source_path, '-extract', arch, '-output', temp.name])
  459. else:
  460. # Otherwise, just copy it over.
  461. temp.write(open(source_path, 'rb').read())
  462. temp.close()
  463. os.chmod(temp.name, os.stat(temp.name).st_mode | 0o711)
  464. # Now add dependencies. On macOS, fix @loader_path references.
  465. if target_info.sys_platform in ("darwin", "ios"):
  466. if source_path.endswith('deploy-stubw'):
  467. deps_path = '@executable_path/../Frameworks'
  468. else:
  469. deps_path = '@loader_path'
  470. remove_signature = False
  471. loader_path = [os.path.dirname(source_path)]
  472. for dep in deps:
  473. if dep.endswith('/Python'):
  474. # If this references the Python framework, change it
  475. # to reference libpython instead.
  476. new_dep = deps_path + '/libpython{0}.{1}.dylib'.format(*target_info.python_version)
  477. elif '@loader_path' in dep:
  478. dep_path = dep.replace('@loader_path', '.')
  479. target_dep = os.path.dirname(target_path) + '/' + os.path.basename(dep)
  480. target_dep = self.consider_add_dependency(target_dep, dep_path, loader_path)
  481. if not target_dep:
  482. # It won't be included, so no use adjusting the path.
  483. continue
  484. new_dep = os.path.join(deps_path, os.path.relpath(target_dep, os.path.dirname(target_path)))
  485. elif '@rpath' in dep:
  486. # Unlike makepanda, CMake uses @rpath instead of
  487. # @loader_path. This means we can just search for the
  488. # dependencies like normal.
  489. dep_path = dep.replace('@rpath', '.')
  490. target_dep = os.path.dirname(target_path) + '/' + os.path.basename(dep)
  491. self.consider_add_dependency(target_dep, dep_path)
  492. continue
  493. elif dep.startswith('/Library/Frameworks/Python.framework/'):
  494. # Add this dependency if it's in the Python directory.
  495. target_dep = os.path.dirname(target_path) + '/' + os.path.basename(dep)
  496. target_dep = self.consider_add_dependency(target_dep, dep, loader_path)
  497. if not target_dep:
  498. # It won't be included, so no use adjusting the path.
  499. continue
  500. new_dep = os.path.join(deps_path, os.path.relpath(target_dep, os.path.dirname(target_path)))
  501. else:
  502. if '/' in dep:
  503. if verbose:
  504. print("Ignoring dependency %s" % (dep))
  505. continue
  506. subprocess.call(["install_name_tool", "-change", dep, new_dep, temp.name])
  507. remove_signature = True
  508. # Remove the codesign signature if we modified the library.
  509. if remove_signature:
  510. subprocess.call(["codesign", "--remove-signature", temp.name])
  511. else:
  512. # On other unixes, we just add dependencies normally.
  513. for dep in deps:
  514. # Only include dependencies with relative path, for now.
  515. if '/' not in dep:
  516. target_dep = os.path.dirname(target_path) + '/' + dep
  517. self.consider_add_dependency(target_dep, dep)
  518. subprocess.call(["strip", "-s", temp.name])
  519. subprocess.call(["patchelf", "--set-rpath", "$ORIGIN", temp.name])
  520. source_path = temp.name
  521. ext = ext.lower()
  522. if ext in ('.dll', '.pyd', '.exe'):
  523. # Scan and add Win32 dependencies.
  524. for dep in scan_dependencies(source_path, target_info):
  525. target_dep = os.path.dirname(target_path) + '/' + dep
  526. self.consider_add_dependency(target_dep, dep)
  527. # Calculate the SHA-256 hash and size.
  528. sha = hashlib.sha256()
  529. fp = open(source_path, 'rb')
  530. size = 0
  531. data = fp.read(1024 * 1024)
  532. while data:
  533. size += len(data)
  534. sha.update(data)
  535. data = fp.read(1024 * 1024)
  536. fp.close()
  537. # Save it in PEP-0376 format for writing out later.
  538. digest = urlsafe_b64encode(sha.digest()).decode('ascii')
  539. digest = digest.rstrip('=')
  540. self.records.append("{0},sha256={1},{2}\n".format(target_path, digest, size))
  541. if verbose:
  542. print("Adding {0} from {1}".format(target_path, orig_source_path))
  543. self.zip_file.write(source_path, target_path)
  544. #if temp:
  545. # os.unlink(temp.name)
  546. def write_file_data(self, target_path, source_data):
  547. """Adds the given file from a string."""
  548. sha = hashlib.sha256()
  549. sha.update(source_data.encode())
  550. digest = urlsafe_b64encode(sha.digest()).decode('ascii')
  551. digest = digest.rstrip('=')
  552. self.records.append("{0},sha256={1},{2}\n".format(target_path, digest, len(source_data)))
  553. if verbose:
  554. print("Adding %s from data" % target_path)
  555. self.zip_file.writestr(target_path, source_data)
  556. def write_directory(self, target_dir, source_dir):
  557. """Adds the given directory recursively to the .whl file."""
  558. for root, dirs, files in os.walk(source_dir):
  559. for file in files:
  560. if os.path.splitext(file)[1] in EXCLUDE_EXT:
  561. continue
  562. source_path = os.path.join(root, file)
  563. target_path = os.path.join(target_dir, os.path.relpath(source_path, source_dir))
  564. target_path = target_path.replace('\\', '/')
  565. self.write_file(target_path, source_path)
  566. def close(self):
  567. # Write the RECORD file.
  568. record_file = "{0}-{1}.dist-info/RECORD".format(self.name, self.version)
  569. self.records.append(record_file + ",,\n")
  570. self.zip_file.writestr(record_file, "".join(self.records))
  571. self.zip_file.close()
  572. def makewheel(version, output_dir, target_info):
  573. if target_info.sys_platform not in ("win32", "darwin", "ios") and not target_info.sys_platform.startswith("cygwin"):
  574. if not locate_binary("patchelf"):
  575. raise Exception("patchelf is required when building a Linux wheel.")
  576. if sys.version_info < (3, 6):
  577. raise Exception("Python 3.6 is required to produce a wheel.")
  578. platform = target_info.platform_tag
  579. if platform is None:
  580. # Determine the platform from the build.
  581. platform_dat = os.path.join(output_dir, 'tmp', 'platform.dat')
  582. if os.path.isfile(platform_dat):
  583. platform = open(platform_dat, 'r').read().strip()
  584. else:
  585. print("Could not find platform.dat in build directory")
  586. platform = get_platform()
  587. if platform.startswith("linux-") and os.path.isdir("/opt/python"):
  588. # Is this manylinux?
  589. if os.path.isfile("/lib/libc-2.5.so") or os.path.isfile("/lib64/libc-2.5.so"):
  590. platform = platform.replace("linux", "manylinux1")
  591. elif os.path.isfile("/lib/libc-2.12.so") or os.path.isfile("/lib64/libc-2.12.so"):
  592. platform = platform.replace("linux", "manylinux2010")
  593. elif os.path.isfile("/lib/libc-2.17.so") or os.path.isfile("/lib64/libc-2.17.so"):
  594. platform = platform.replace("linux", "manylinux2014")
  595. platform = platform.replace('-', '_').replace('.', '_')
  596. # Global filepaths
  597. panda3d_dir = join(output_dir, "panda3d")
  598. pandac_dir = join(output_dir, "pandac")
  599. direct_dir = join(output_dir, "direct")
  600. models_dir = join(output_dir, "models")
  601. etc_dir = join(output_dir, "etc")
  602. bin_dir = join(output_dir, "bin")
  603. if target_info.sys_platform == "win32":
  604. libs_dir = join(output_dir, "bin")
  605. else:
  606. libs_dir = join(output_dir, "lib")
  607. ext_mod_dir = get_python_ext_module_dir()
  608. license_src = "LICENSE"
  609. readme_src = "README.md"
  610. # Update relevant METADATA entries
  611. METADATA['version'] = version
  612. # Build out the metadata
  613. details = METADATA["extensions"]["python.details"]
  614. homepage = details["project_urls"]["Home"]
  615. author = details["contacts"][0]["name"]
  616. email = details["contacts"][0]["email"]
  617. metadata = ''.join([
  618. "Metadata-Version: {metadata_version}\n" \
  619. "Name: {name}\n" \
  620. "Version: {version}\n" \
  621. "Summary: {summary}\n" \
  622. "License: {license}\n".format(**METADATA),
  623. "Home-page: {0}\n".format(homepage),
  624. ] + ["Project-URL: {0}, {1}\n".format(*url) for url in PROJECT_URLS.items()] + [
  625. "Author: {0}\n".format(author),
  626. "Author-email: {0}\n".format(email),
  627. "Platform: {0}\n".format(target_info.platform_tag),
  628. ] + ["Classifier: {0}\n".format(c) for c in METADATA['classifiers']])
  629. metadata += '\n' + DESCRIPTION.strip() + '\n'
  630. # Zip it up and name it the right thing
  631. whl = WheelFile('panda3d', version, target_info)
  632. whl.lib_path = [libs_dir]
  633. if sys.platform == "win32":
  634. whl.lib_path.append(ext_mod_dir)
  635. if target_info.platform_tag.startswith("manylinux"):
  636. # On manylinux1, we pick up all libraries except for the ones specified
  637. # by the manylinux1 ABI.
  638. whl.lib_path.append("/usr/local/lib")
  639. if target_info.platform_tag.endswith("_x86_64"):
  640. whl.lib_path += ["/lib64", "/usr/lib64"]
  641. else:
  642. whl.lib_path += ["/lib", "/usr/lib"]
  643. whl.ignore_deps.update(MANYLINUX_LIBS)
  644. # Add libpython for deployment.
  645. if sys.platform in ('win32', 'cygwin'):
  646. pylib_name = 'python{0}{1}.dll'.format(*sys.version_info)
  647. pylib_path = os.path.join(get_config_var('BINDIR'), pylib_name)
  648. elif sys.platform == 'darwin':
  649. pylib_name = 'libpython{0}.{1}.dylib'.format(*sys.version_info)
  650. pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
  651. else:
  652. pylib_name = get_config_var('LDLIBRARY')
  653. pylib_arch = get_config_var('MULTIARCH')
  654. libdir = get_config_var('LIBDIR')
  655. if pylib_arch and os.path.exists(os.path.join(libdir, pylib_arch, pylib_name)):
  656. pylib_path = os.path.join(libdir, pylib_arch, pylib_name)
  657. else:
  658. pylib_path = os.path.join(libdir, pylib_name)
  659. # If Python was linked statically, we don't need to include this.
  660. if not pylib_name.endswith('.a'):
  661. whl.write_file('deploy_libs/' + pylib_name, pylib_path)
  662. # Add the trees with Python modules.
  663. whl.write_directory('direct', direct_dir)
  664. # Write the panda3d tree. We use a custom empty __init__ since the
  665. # default one adds the bin directory to the PATH, which we don't have.
  666. p3d_init = """"Python bindings for the Panda3D libraries"
  667. __version__ = '{0}'
  668. """.format(version)
  669. if (2, 7) in target_info.python_version:
  670. p3d_init += """
  671. if __debug__:
  672. if 1 / 2 == 0:
  673. raise ImportError(\"Python 2 is not supported.\")
  674. """
  675. whl.write_file_data('panda3d/__init__.py', p3d_init)
  676. ext_suffix = target_info.extension_suffix
  677. module_dir = libs_dir if target_info.static_panda else panda3d_dir
  678. for file in os.listdir(module_dir):
  679. if file == '__init__.py':
  680. pass
  681. elif file.endswith('.py') or (file.endswith(ext_suffix) and '.' not in file[:-len(ext_suffix)]) or file.startswith('libpy'):
  682. source_path = os.path.join(module_dir, file)
  683. if file.endswith('.pyd') and target_info.sys_platform == 'cygwin':
  684. # Rename it to .dll for cygwin Python to be able to load it.
  685. target_path = 'panda3d/' + os.path.splitext(file)[0] + '.dll'
  686. else:
  687. target_path = 'panda3d/' + file
  688. whl.write_file(target_path, source_path)
  689. # And copy the extension modules from the Python installation into the
  690. # deploy_libs directory, for use by deploy-ng.
  691. if target_info.static_panda:
  692. ext_suffix = '.lib' if target_info.sys_platform in ('win32', 'cygwin') else '.a'
  693. else:
  694. ext_suffix = '.pyd' if target_info.sys_platform in ('win32', 'cygwin') else '.so'
  695. ext_mod_dir = target_info.python_ext_module_dir
  696. if not 'ios' in target_info.platform_tag:
  697. for file in os.listdir(ext_mod_dir):
  698. if file.endswith(ext_suffix):
  699. source_path = os.path.join(ext_mod_dir, file)
  700. if file.endswith('.pyd') and target_info.sys_platform == 'cygwin':
  701. # Rename it to .dll for cygwin Python to be able to load it.
  702. target_path = 'deploy_libs/' + os.path.splitext(file)[0] + '.dll'
  703. else:
  704. target_path = 'deploy_libs/' + file
  705. whl.write_file(target_path, source_path)
  706. # Add plug-ins.
  707. for lib in PLUGIN_LIBS:
  708. plugin_name = 'lib' + lib
  709. if target_info.sys_platform in ('win32', 'cygwin'):
  710. plugin_name += '.lib' if target_info.static_panda else '.dll'
  711. elif target_info.sys_platform in ('darwin', 'ios'):
  712. plugin_name += '.a' if target_info.static_panda else '.dylib'
  713. else:
  714. plugin_name += '.a' if target_info.static_panda else '.so'
  715. plugin_path = os.path.join(libs_dir, plugin_name)
  716. if os.path.isfile(plugin_path):
  717. whl.write_file('panda3d/' + plugin_name, plugin_path)
  718. # Add the .data directory, containing additional files.
  719. data_dir = 'panda3d-{0}.data'.format(version)
  720. #whl.write_directory(data_dir + '/data/etc', etc_dir)
  721. #whl.write_directory(data_dir + '/data/models', models_dir)
  722. # Actually, let's not. That seems to install the files to the strangest
  723. # places in the user's filesystem. Let's instead put them in panda3d.
  724. whl.write_directory('panda3d/etc', etc_dir)
  725. whl.write_directory('panda3d/models', models_dir)
  726. # Add the pandac tree for backward compatibility.
  727. for file in os.listdir(pandac_dir):
  728. if file.endswith('.py'):
  729. whl.write_file('pandac/' + file, os.path.join(pandac_dir, file))
  730. # Let's also add the interrogate databases.
  731. input_dir = os.path.join(pandac_dir, 'input')
  732. if os.path.isdir(input_dir):
  733. for file in os.listdir(input_dir):
  734. if file.endswith('.in'):
  735. whl.write_file('pandac/input/' + file, os.path.join(input_dir, file))
  736. # Add a panda3d-tools directory containing the executables.
  737. entry_points = '[console_scripts]\n'
  738. entry_points += 'eggcacher = direct.directscripts.eggcacher:main\n'
  739. entry_points += 'pfreeze = direct.dist.pfreeze:main\n'
  740. tools_init = ''
  741. for file in os.listdir(bin_dir):
  742. basename = os.path.splitext(file)[0]
  743. if basename in ('eggcacher', 'packpanda'):
  744. continue
  745. source_path = os.path.join(bin_dir, file)
  746. if is_executable(source_path):
  747. # Put the .exe files inside the panda3d-tools directory.
  748. whl.write_file('panda3d_tools/' + file, source_path)
  749. if basename.endswith('_bin'):
  750. # These tools won't be invoked by the user directly.
  751. continue
  752. # Tell pip to create a wrapper script.
  753. funcname = basename.replace('-', '_')
  754. entry_points += '{0} = panda3d_tools:{1}\n'.format(basename, funcname)
  755. tools_init += '{0} = lambda: _exec_tool({1!r})\n'.format(funcname, file)
  756. entry_points += '[distutils.commands]\n'
  757. entry_points += 'build_apps = direct.dist.commands:build_apps\n'
  758. entry_points += 'bdist_apps = direct.dist.commands:bdist_apps\n'
  759. whl.write_file_data('panda3d_tools/__init__.py', PANDA3D_TOOLS_INIT.format(tools_init))
  760. # Add the dist-info directory last.
  761. info_dir = 'panda3d-{0}.dist-info'.format(version)
  762. whl.write_file_data(info_dir + '/entry_points.txt', entry_points)
  763. whl.write_file_data(info_dir + '/metadata.json', json.dumps(METADATA, indent=4, separators=(',', ': ')))
  764. whl.write_file_data(info_dir + '/METADATA', metadata)
  765. whl.write_file_data(info_dir + '/WHEEL', WHEEL_DATA.format(target_info.python_tag, target_info.abi_tag, target_info.platform_tag))
  766. whl.write_file(info_dir + '/LICENSE.txt', license_src)
  767. whl.write_file(info_dir + '/README.md', readme_src)
  768. whl.write_file_data(info_dir + '/top_level.txt', 'direct\npanda3d\npandac\npanda3d_tools\n')
  769. whl.close()
  770. if __name__ == "__main__":
  771. version = get_metadata_value('version')
  772. parser = OptionParser()
  773. parser.add_option('', '--version', dest = 'version', help = 'Panda3D version number (default: %s)' % (version), default = version)
  774. parser.add_option('', '--outputdir', dest = 'outputdir', help = 'Makepanda\'s output directory (default: built)', default = 'built')
  775. parser.add_option('', '--verbose', dest = 'verbose', help = 'Enable verbose output', action = 'store_true', default = False)
  776. parser.add_option('', '--platform', dest = 'platform_tag', help = 'Override platform tag', default = None)
  777. parser.add_option('', '--soabi', dest = 'soabi', help = 'SOABI, used for extension suffixes.')
  778. parser.add_option('', '--pyver', dest = 'python_version', help = 'Custom Python version we\'re making the wheel for.')
  779. parser.add_option('', '--pyroot', dest = 'python_root', help = 'Custom root of Python installation.', default = sys.exec_prefix)
  780. parser.add_option('', '--sysplatform', dest = 'sys_platform', help = 'Output of "sys.platform" on the target', default = get_host())
  781. parser.add_option('', '--static', dest = 'static_panda', help = 'If the Panda libraries we\'re packaging are statically linked', action = 'store_true', default = False)
  782. (options, args) = parser.parse_args()
  783. ti_opts = {key: options.__dict__[key] for key in ('platform_tag', 'soabi', 'python_version', 'python_root', 'sys_platform', 'static_panda')}
  784. target_info = TargetInfo(**ti_opts)
  785. global verbose
  786. verbose = options.verbose
  787. makewheel(options.version, options.outputdir, target_info)