commands.py 72 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776
  1. """Extends setuptools with the ``build_apps`` and ``bdist_apps`` commands.
  2. See the :ref:`distribution` section of the programming manual for information
  3. on how to use these commands.
  4. """
  5. import os
  6. import plistlib
  7. import sys
  8. import subprocess
  9. import zipfile
  10. import re
  11. import shutil
  12. import stat
  13. import struct
  14. import string
  15. import tempfile
  16. import setuptools
  17. import distutils.log
  18. from . import FreezeTool
  19. from . import pefile
  20. from . import installers
  21. from .icon import Icon
  22. from ._dist_hooks import finalize_distribution_options
  23. import panda3d.core as p3d
  24. def _parse_list(input):
  25. if isinstance(input, str):
  26. input = input.strip().replace(',', '\n')
  27. if input:
  28. return [item.strip() for item in input.split('\n') if item.strip()]
  29. else:
  30. return []
  31. else:
  32. return input
  33. def _parse_dict(input):
  34. if isinstance(input, dict):
  35. return input
  36. d = {}
  37. for item in _parse_list(input):
  38. key, sep, value = item.partition('=')
  39. d[key.strip()] = value.strip()
  40. return d
  41. def _register_python_loaders():
  42. # We need this method so that we don't depend on direct.showbase.Loader.
  43. if getattr(_register_python_loaders, 'done', None):
  44. return
  45. _register_python_loaders.done = True
  46. from importlib.metadata import entry_points
  47. eps = entry_points()
  48. if isinstance(eps, dict): # Python 3.8 and 3.9
  49. loaders = eps.get('panda3d.loaders', ())
  50. else:
  51. loaders = eps.select(group='panda3d.loaders')
  52. registry = p3d.LoaderFileTypeRegistry.get_global_ptr()
  53. for entry_point in loaders:
  54. registry.register_deferred_type(entry_point)
  55. def _model_to_bam(_build_cmd, srcpath, dstpath):
  56. if dstpath.endswith('.gz') or dstpath.endswith('.pz'):
  57. dstpath = dstpath[:-3]
  58. dstpath = dstpath + '.bam'
  59. src_fn = p3d.Filename.from_os_specific(srcpath)
  60. dst_fn = p3d.Filename.from_os_specific(dstpath)
  61. _register_python_loaders()
  62. loader = p3d.Loader.get_global_ptr()
  63. options = p3d.LoaderOptions(p3d.LoaderOptions.LF_report_errors |
  64. p3d.LoaderOptions.LF_no_ram_cache)
  65. node = loader.load_sync(src_fn, options)
  66. if not node:
  67. raise IOError('Failed to load model: %s' % (srcpath))
  68. if not p3d.NodePath(node).write_bam_file(dst_fn):
  69. raise IOError('Failed to write .bam file: %s' % (dstpath))
  70. macosx_binary_magics = (
  71. b'\xFE\xED\xFA\xCE', b'\xCE\xFA\xED\xFE',
  72. b'\xFE\xED\xFA\xCF', b'\xCF\xFA\xED\xFE',
  73. b'\xCA\xFE\xBA\xBE', b'\xBE\xBA\xFE\xCA',
  74. b'\xCA\xFE\xBA\xBF', b'\xBF\xBA\xFE\xCA')
  75. # Some dependencies need data directories to be extracted. This dictionary maps
  76. # modules with data to extract. The values are lists of tuples of the form
  77. # (source_pattern, destination_pattern, flags). The flags is a set of strings.
  78. PACKAGE_DATA_DIRS = {
  79. 'matplotlib': [('matplotlib/mpl-data/*', 'mpl-data', {})],
  80. 'jsonschema': [('jsonschema/schemas/*', 'schemas', {})],
  81. 'cefpython3': [
  82. ('cefpython3/*.pak', '', {}),
  83. ('cefpython3/*.dat', '', {}),
  84. ('cefpython3/*.bin', '', {}),
  85. ('cefpython3/*.dll', '', {}),
  86. ('cefpython3/libcef.so', '', {}),
  87. ('cefpython3/LICENSE.txt', '', {}),
  88. ('cefpython3/License', '', {}),
  89. ('cefpython3/subprocess*', '', {'PKG_DATA_MAKE_EXECUTABLE'}),
  90. ('cefpython3/locals/*', 'locals', {}),
  91. ('cefpython3/Chromium Embedded Framework.framework/Resources', 'Chromium Embedded Framework.framework/Resources', {}),
  92. ('cefpython3/Chromium Embedded Framework.framework/Chromium Embedded Framework', '', {'PKG_DATA_MAKE_EXECUTABLE'}),
  93. ],
  94. 'pytz': [('pytz/zoneinfo/*', 'zoneinfo', ())],
  95. 'certifi': [('certifi/cacert.pem', '', {})],
  96. '_tkinter_ext': [('_tkinter_ext/tcl/**', 'tcl', {})],
  97. }
  98. # Some dependencies have extra directories that need to be scanned for DLLs.
  99. # This dictionary maps wheel basenames (ie. the part of the .whl basename
  100. # before the first hyphen) to a list of tuples, the first value being the
  101. # directory inside the wheel, the second being which wheel to look in (or
  102. # None to look in its own wheel).
  103. PACKAGE_LIB_DIRS = {
  104. 'scipy': [('scipy/extra-dll', None)],
  105. 'PyQt5': [('PyQt5/Qt5/bin', 'PyQt5_Qt5')],
  106. }
  107. SITE_PY = """
  108. import sys
  109. from _frozen_importlib import _imp, FrozenImporter
  110. sys.frozen = True
  111. if sys.platform == 'win32' and sys.version_info < (3, 10):
  112. # Make sure the preferred encoding is something we actually support.
  113. import _bootlocale
  114. enc = _bootlocale.getpreferredencoding().lower()
  115. if enc != 'utf-8' and not _imp.is_frozen('encodings.%s' % (enc)):
  116. def getpreferredencoding(do_setlocale=True):
  117. return 'mbcs'
  118. _bootlocale.getpreferredencoding = getpreferredencoding
  119. # Alter FrozenImporter to give a __file__ property to frozen modules.
  120. _find_spec = FrozenImporter.find_spec
  121. def find_spec(fullname, path=None, target=None):
  122. spec = _find_spec(fullname, path=path, target=target)
  123. if spec:
  124. spec.has_location = True
  125. spec.origin = sys.executable
  126. return spec
  127. def get_data(path):
  128. with open(path, 'rb') as fp:
  129. return fp.read()
  130. FrozenImporter.find_spec = find_spec
  131. FrozenImporter.get_data = get_data
  132. """
  133. SITE_PY_ANDROID = """
  134. import sys, os
  135. from _frozen_importlib import _imp, FrozenImporter
  136. from importlib import _bootstrap_external
  137. from importlib.abc import Loader, MetaPathFinder
  138. from importlib.machinery import ModuleSpec
  139. from io import RawIOBase, TextIOWrapper
  140. from android_log import write as android_log_write
  141. sys.frozen = True
  142. # Temporary hack for plyer to detect Android, see kivy/plyer#670
  143. os.environ['ANDROID_ARGUMENT'] = ''
  144. # Replace stdout/stderr with something that writes to the Android log.
  145. class AndroidLogStream:
  146. closed = False
  147. encoding = 'utf-8'
  148. def __init__(self, prio, tag):
  149. self.prio = prio
  150. self.tag = tag
  151. self.buffer = ''
  152. def isatty(self):
  153. return False
  154. def write(self, text):
  155. self.writelines(text.split('\\n'))
  156. def writelines(self, lines):
  157. num_lines = len(lines)
  158. if num_lines == 1:
  159. self.buffer += lines[0]
  160. elif num_lines > 1:
  161. android_log_write(self.prio, self.tag, self.buffer + lines[0])
  162. for line in lines[1:-1]:
  163. android_log_write(self.prio, self.tag, line)
  164. self.buffer = lines[-1]
  165. def flush(self):
  166. pass
  167. def seekable(self):
  168. return False
  169. def readable(self):
  170. return False
  171. def writable(self):
  172. return True
  173. sys.stdout = AndroidLogStream(2, 'Python')
  174. sys.stderr = AndroidLogStream(3, 'Python')
  175. # Alter FrozenImporter to give a __file__ property to frozen modules.
  176. _find_spec = FrozenImporter.find_spec
  177. def find_spec(fullname, path=None, target=None):
  178. spec = _find_spec(fullname, path=path, target=target)
  179. if spec:
  180. spec.has_location = True
  181. spec.origin = sys.executable
  182. return spec
  183. def get_data(path):
  184. with open(path, 'rb') as fp:
  185. return fp.read()
  186. FrozenImporter.find_spec = find_spec
  187. FrozenImporter.get_data = get_data
  188. class AndroidExtensionFinder(MetaPathFinder):
  189. @classmethod
  190. def find_spec(cls, fullname, path=None, target=None):
  191. soname = 'libpy.' + fullname + '.so'
  192. path = os.path.join(os.path.dirname(sys.executable), soname)
  193. if os.path.exists(path):
  194. loader = _bootstrap_external.ExtensionFileLoader(fullname, path)
  195. return ModuleSpec(fullname, loader, origin=path)
  196. sys.meta_path.append(AndroidExtensionFinder)
  197. """
  198. class build_apps(setuptools.Command):
  199. description = 'build Panda3D applications'
  200. user_options = [
  201. ('build-base=', None, 'directory to build applications in'),
  202. ('requirements-path=', None, 'path to requirements.txt file for pip'),
  203. ('platforms=', 'p', 'a list of platforms to build for'),
  204. ]
  205. default_file_handlers = {
  206. }
  207. def initialize_options(self):
  208. self.build_base = os.path.join(os.getcwd(), 'build')
  209. self.application_id = None
  210. self.android_abis = None
  211. self.android_debuggable = False
  212. self.android_version_code = 1
  213. self.android_min_sdk_version = 21
  214. self.android_max_sdk_version = None
  215. self.android_target_sdk_version = 30
  216. self.gui_apps = {}
  217. self.console_apps = {}
  218. self.macos_main_app = None
  219. self.rename_paths = {}
  220. self.include_patterns = []
  221. self.exclude_patterns = []
  222. self.include_modules = {}
  223. self.exclude_modules = {}
  224. self.icons = {}
  225. self.platforms = [
  226. 'manylinux2014_x86_64',
  227. 'macosx_10_9_x86_64',
  228. 'win_amd64',
  229. ]
  230. self.plugins = []
  231. self.embed_prc_data = True
  232. self.extra_prc_files = []
  233. self.extra_prc_data = ''
  234. self.default_prc_dir = None
  235. self.log_filename = None
  236. self.log_filename_strftime = True
  237. self.log_append = False
  238. self.prefer_discrete_gpu = False
  239. self.requirements_path = os.path.join(os.getcwd(), 'requirements.txt')
  240. self.strip_docstrings = True
  241. self.use_optimized_wheels = True
  242. self.optimized_wheel_index = ''
  243. self.pypi_extra_indexes = [
  244. 'https://archive.panda3d.org/thirdparty',
  245. ]
  246. self.file_handlers = {}
  247. self.bam_model_extensions = ['.egg', '.gltf', '.glb']
  248. self.exclude_dependencies = [
  249. # Windows
  250. 'kernel32.dll', 'user32.dll', 'wsock32.dll', 'ws2_32.dll',
  251. 'advapi32.dll', 'opengl32.dll', 'glu32.dll', 'gdi32.dll',
  252. 'shell32.dll', 'ntdll.dll', 'ws2help.dll', 'rpcrt4.dll',
  253. 'imm32.dll', 'ddraw.dll', 'shlwapi.dll', 'secur32.dll',
  254. 'dciman32.dll', 'comdlg32.dll', 'comctl32.dll', 'ole32.dll',
  255. 'oleaut32.dll', 'gdiplus.dll', 'winmm.dll', 'iphlpapi.dll',
  256. 'msvcrt.dll', 'kernelbase.dll', 'msimg32.dll', 'msacm32.dll',
  257. 'setupapi.dll', 'version.dll', 'userenv.dll', 'netapi32.dll',
  258. 'crypt32.dll', 'bcrypt.dll',
  259. # manylinux1/linux
  260. 'libdl.so.*', 'libstdc++.so.*', 'libm.so.*', 'libgcc_s.so.*',
  261. 'libpthread.so.*', 'libc.so.*', 'ld-linux-x86-64.so.*',
  262. 'libgl.so.*', 'libx11.so.*', 'libncursesw.so.*', 'libz.so.*',
  263. 'librt.so.*', 'libutil.so.*', 'libnsl.so.1', 'libXext.so.6',
  264. 'libXrender.so.1', 'libICE.so.6', 'libSM.so.6', 'libEGL.so.1',
  265. 'libOpenGL.so.0', 'libGLdispatch.so.0', 'libGLX.so.0',
  266. 'libgobject-2.0.so.0', 'libgthread-2.0.so.0', 'libglib-2.0.so.0',
  267. # macOS
  268. '/usr/lib/libc++.1.dylib',
  269. '/usr/lib/libstdc++.*.dylib',
  270. '/usr/lib/libz.*.dylib',
  271. '/usr/lib/libobjc.*.dylib',
  272. '/usr/lib/libSystem.*.dylib',
  273. '/usr/lib/libbz2.*.dylib',
  274. '/usr/lib/libedit.*.dylib',
  275. '/usr/lib/libffi.dylib',
  276. '/usr/lib/libauditd.0.dylib',
  277. '/usr/lib/libgermantok.dylib',
  278. '/usr/lib/liblangid.dylib',
  279. '/usr/lib/libarchive.2.dylib',
  280. '/usr/lib/libipsec.A.dylib',
  281. '/usr/lib/libpanel.5.4.dylib',
  282. '/usr/lib/libiodbc.2.1.18.dylib',
  283. '/usr/lib/libhunspell-1.2.0.0.0.dylib',
  284. '/usr/lib/libsqlite3.dylib',
  285. '/usr/lib/libpam.1.dylib',
  286. '/usr/lib/libtidy.A.dylib',
  287. '/usr/lib/libDHCPServer.A.dylib',
  288. '/usr/lib/libpam.2.dylib',
  289. '/usr/lib/libXplugin.1.dylib',
  290. '/usr/lib/libxslt.1.dylib',
  291. '/usr/lib/libiodbcinst.2.1.18.dylib',
  292. '/usr/lib/libBSDPClient.A.dylib',
  293. '/usr/lib/libsandbox.1.dylib',
  294. '/usr/lib/libform.5.4.dylib',
  295. '/usr/lib/libbsm.0.dylib',
  296. '/usr/lib/libMatch.1.dylib',
  297. '/usr/lib/libresolv.9.dylib',
  298. '/usr/lib/libcharset.1.dylib',
  299. '/usr/lib/libxml2.2.dylib',
  300. '/usr/lib/libiconv.2.dylib',
  301. '/usr/lib/libScreenReader.dylib',
  302. '/usr/lib/libdtrace.dylib',
  303. '/usr/lib/libicucore.A.dylib',
  304. '/usr/lib/libsasl2.2.dylib',
  305. '/usr/lib/libpcap.A.dylib',
  306. '/usr/lib/libexslt.0.dylib',
  307. '/usr/lib/libcurl.4.dylib',
  308. '/usr/lib/libncurses.5.4.dylib',
  309. '/usr/lib/libxar.1.dylib',
  310. '/usr/lib/libmenu.5.4.dylib',
  311. '/System/Library/**',
  312. # Android
  313. 'libc.so', 'libm.so', 'liblog.so', 'libdl.so', 'libandroid.so',
  314. 'libGLESv1_CM.so', 'libGLESv2.so', 'libjnigraphics.so', 'libEGL.so',
  315. 'libOpenSLES.so', 'libandroid.so', 'libOpenMAXAL.so', 'libz.so',
  316. ]
  317. self.package_data_dirs = {}
  318. self.hidden_imports = {}
  319. # We keep track of the zip files we've opened.
  320. self._zip_files = {}
  321. def _get_zip_file(self, path):
  322. if path in self._zip_files:
  323. return self._zip_files[path]
  324. zip = zipfile.ZipFile(path)
  325. self._zip_files[path] = zip
  326. return zip
  327. def finalize_options(self):
  328. # We need to massage the inputs a bit in case they came from a
  329. # setup.cfg file.
  330. self.gui_apps = _parse_dict(self.gui_apps)
  331. self.console_apps = _parse_dict(self.console_apps)
  332. self.rename_paths = _parse_dict(self.rename_paths)
  333. self.include_patterns = _parse_list(self.include_patterns)
  334. self.exclude_patterns = _parse_list(self.exclude_patterns)
  335. self.include_modules = {
  336. key: _parse_list(value)
  337. for key, value in _parse_dict(self.include_modules).items()
  338. }
  339. self.exclude_modules = {
  340. key: _parse_list(value)
  341. for key, value in _parse_dict(self.exclude_modules).items()
  342. }
  343. self.icons = _parse_dict(self.icons)
  344. self.platforms = _parse_list(self.platforms)
  345. self.plugins = _parse_list(self.plugins)
  346. self.extra_prc_files = _parse_list(self.extra_prc_files)
  347. self.hidden_imports = {
  348. key: _parse_list(value)
  349. for key, value in _parse_dict(self.hidden_imports).items()
  350. }
  351. if self.default_prc_dir is None:
  352. self.default_prc_dir = '<auto>etc' if not self.embed_prc_data else ''
  353. num_gui_apps = len(self.gui_apps)
  354. num_console_apps = len(self.console_apps)
  355. if not self.macos_main_app:
  356. if num_gui_apps > 1:
  357. assert False, 'macos_main_app must be defined if more than one gui_app is defined'
  358. elif num_gui_apps == 1:
  359. self.macos_main_app = list(self.gui_apps.keys())[0]
  360. use_pipenv = (
  361. 'Pipfile' in os.path.basename(self.requirements_path) or
  362. not os.path.exists(self.requirements_path) and os.path.exists('Pipfile')
  363. )
  364. if use_pipenv:
  365. reqspath = os.path.join(self.build_base, 'requirements.txt')
  366. with open(reqspath, 'w') as reqsfile:
  367. subprocess.check_call(['pipenv', 'lock', '--requirements'], stdout=reqsfile)
  368. self.requirements_path = reqspath
  369. if self.use_optimized_wheels:
  370. if not self.optimized_wheel_index:
  371. # Try to find an appropriate wheel index
  372. # Start with the release index
  373. self.optimized_wheel_index = 'https://archive.panda3d.org/simple/opt'
  374. # See if a buildbot build is being used
  375. with open(self.requirements_path) as reqsfile:
  376. reqsdata = reqsfile.read()
  377. matches = re.search(r'--extra-index-url (https*://archive.panda3d.org/.*\b)', reqsdata)
  378. if matches and matches.group(1):
  379. self.optimized_wheel_index = matches.group(1)
  380. if not matches.group(1).endswith('opt'):
  381. self.optimized_wheel_index += '/opt'
  382. assert self.optimized_wheel_index, 'An index for optimized wheels must be defined if use_optimized_wheels is set'
  383. assert os.path.exists(self.requirements_path), 'Requirements.txt path does not exist: {}'.format(self.requirements_path)
  384. assert num_gui_apps + num_console_apps != 0, 'Must specify at least one app in either gui_apps or console_apps'
  385. self.exclude_dependencies = [p3d.GlobPattern(i) for i in self.exclude_dependencies]
  386. for glob in self.exclude_dependencies:
  387. glob.case_sensitive = False
  388. # bam_model_extensions registers a 2bam handler for each given extension.
  389. # They can override a default handler, but not a custom handler.
  390. if self.bam_model_extensions:
  391. for ext in self.bam_model_extensions:
  392. ext = '.' + ext.lstrip('.')
  393. handler = self.file_handlers.get(ext)
  394. if handler != _model_to_bam:
  395. assert handler is None, \
  396. 'Extension {} occurs in both file_handlers and bam_model_extensions!'.format(ext)
  397. self.file_handlers[ext] = _model_to_bam
  398. tmp = self.default_file_handlers.copy()
  399. tmp.update(self.file_handlers)
  400. self.file_handlers = tmp
  401. tmp = PACKAGE_DATA_DIRS.copy()
  402. tmp.update(self.package_data_dirs)
  403. self.package_data_dirs = tmp
  404. # Default to all supported ABIs (for the given Android version).
  405. if self.android_max_sdk_version and self.android_max_sdk_version < 21:
  406. assert self.android_max_sdk_version >= 19, \
  407. 'Panda3D requires at least Android API level 19!'
  408. if self.android_abis:
  409. for abi in self.android_abis:
  410. assert abi not in ('mips64', 'x86_64', 'arm64-v8a'), \
  411. f'{abi} was not a valid Android ABI before Android 21!'
  412. else:
  413. self.android_abis = ['armeabi-v7a', 'x86']
  414. elif not self.android_abis:
  415. self.android_abis = ['arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86']
  416. supported_abis = 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
  417. unsupported_abis = set(self.android_abis) - set(supported_abis)
  418. if unsupported_abis:
  419. raise ValueError(f'Unrecognized value(s) for android_abis: {", ".join(unsupported_abis)}\n'
  420. f'Valid ABIs are: {", ".join(supported_abis)}')
  421. self.icon_objects = {}
  422. for app, iconpaths in self.icons.items():
  423. if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple):
  424. iconpaths = (iconpaths,)
  425. iconobj = Icon()
  426. for iconpath in iconpaths:
  427. iconobj.addImage(iconpath)
  428. iconobj.generateMissingImages()
  429. self.icon_objects[app] = iconobj
  430. def run(self):
  431. self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO)
  432. for platform in self.platforms:
  433. # Create the build directory, or ensure it is empty.
  434. build_dir = os.path.join(self.build_base, platform)
  435. if os.path.exists(build_dir):
  436. for entry in os.listdir(build_dir):
  437. path = os.path.join(build_dir, entry)
  438. if os.path.islink(path) or os.path.isfile(path):
  439. os.unlink(path)
  440. else:
  441. shutil.rmtree(path)
  442. else:
  443. os.makedirs(build_dir)
  444. if platform == 'android':
  445. # Make a multi-arch build for Android.
  446. data_dir = os.path.join(build_dir, 'assets')
  447. os.makedirs(data_dir, exist_ok=True)
  448. for abi in self.android_abis:
  449. lib_dir = os.path.join(build_dir, 'lib', abi)
  450. os.makedirs(lib_dir, exist_ok=True)
  451. suffix = None
  452. if abi == 'arm64-v8a':
  453. suffix = '_arm64'
  454. elif abi == 'armeabi-v7a':
  455. suffix = '_armv7a'
  456. elif abi == 'armeabi':
  457. suffix = '_arm'
  458. else: # e.g. x86, x86_64, mips, mips64
  459. suffix = '_' + abi.replace('-', '_')
  460. # We end up copying the data multiple times to the same
  461. # directory, but that's probably fine for now.
  462. self.build_binaries(platform + suffix, lib_dir, data_dir)
  463. # Write out the icons to the res directory.
  464. for appname, icon in self.icon_objects.items():
  465. if appname == '*' or (appname == self.macos_main_app and '*' not in self.icon_objects):
  466. # Conventional name for icon on Android.
  467. basename = 'ic_launcher.png'
  468. else:
  469. basename = f'ic_{appname}.png'
  470. res_dir = os.path.join(build_dir, 'res')
  471. icon.writeSize(48, os.path.join(res_dir, 'mipmap-mdpi-v4', basename))
  472. icon.writeSize(72, os.path.join(res_dir, 'mipmap-hdpi-v4', basename))
  473. icon.writeSize(96, os.path.join(res_dir, 'mipmap-xhdpi-v4', basename))
  474. icon.writeSize(144, os.path.join(res_dir, 'mipmap-xxhdpi-v4', basename))
  475. if icon.getLargestSize() >= 192:
  476. icon.writeSize(192, os.path.join(res_dir, 'mipmap-xxxhdpi-v4', basename))
  477. self.build_assets(platform, data_dir)
  478. # Generate an AndroidManifest.xml
  479. self.generate_android_manifest(os.path.join(build_dir, 'AndroidManifest.xml'))
  480. else:
  481. self.build_binaries(platform, build_dir, build_dir)
  482. self.build_assets(platform, build_dir)
  483. # Bundle into an .app on macOS
  484. if self.macos_main_app and 'macosx' in platform:
  485. self.bundle_macos_app(build_dir)
  486. def download_wheels(self, platform):
  487. """ Downloads wheels for the given platform using pip. This includes panda3d
  488. wheels. These are special wheels that are expected to contain a deploy_libs
  489. directory containing the Python runtime libraries, which will be added
  490. to sys.path."""
  491. import pip
  492. self.announce('Gathering wheels for platform: {}'.format(platform), distutils.log.INFO)
  493. whlcache = os.path.join(self.build_base, '__whl_cache__')
  494. pip_version = int(pip.__version__.split('.', 1)[0])
  495. if pip_version < 9:
  496. raise RuntimeError("pip 9.0 or greater is required, but found {}".format(pip.__version__))
  497. abi_tag = 'cp%d%d' % (sys.version_info[:2])
  498. if sys.version_info < (3, 8):
  499. abi_tag += 'm'
  500. whldir = os.path.join(whlcache, '_'.join((platform, abi_tag)))
  501. if not os.path.isdir(whldir):
  502. os.makedirs(whldir)
  503. # Remove any .zip files. These are built from a VCS and block for an
  504. # interactive prompt on subsequent downloads.
  505. if os.path.exists(whldir):
  506. for whl in os.listdir(whldir):
  507. if whl.endswith('.zip'):
  508. os.remove(os.path.join(whldir, whl))
  509. pip_args = [
  510. '--disable-pip-version-check',
  511. 'download',
  512. '-d', whldir,
  513. '-r', self.requirements_path,
  514. '--only-binary', ':all:',
  515. '--abi', abi_tag,
  516. '--platform', platform,
  517. ]
  518. if platform.startswith('linux_'):
  519. # Also accept manylinux.
  520. arch = platform[6:]
  521. pip_args += ['--platform', 'manylinux2014_' + arch]
  522. if self.use_optimized_wheels:
  523. pip_args += [
  524. '--extra-index-url', self.optimized_wheel_index
  525. ]
  526. for index in self.pypi_extra_indexes:
  527. pip_args += ['--extra-index-url', index]
  528. try:
  529. subprocess.check_call([sys.executable, '-m', 'pip'] + pip_args)
  530. except:
  531. # Display a more helpful message for these common issues.
  532. if platform.startswith('manylinux2010_') and sys.version_info >= (3, 11):
  533. new_platform = platform.replace('manylinux2010_', 'manylinux2014_')
  534. self.announce('This error likely occurs because {} is not a supported target as of Python 3.11.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR)
  535. elif platform.startswith('manylinux1_') and sys.version_info >= (3, 10):
  536. new_platform = platform.replace('manylinux1_', 'manylinux2014_')
  537. self.announce('This error likely occurs because {} is not a supported target as of Python 3.10.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR)
  538. elif platform.startswith('macosx_10_6_') and sys.version_info >= (3, 8):
  539. new_platform = platform.replace('macosx_10_6_', 'macosx_10_9_')
  540. self.announce('This error likely occurs because {} is not a supported target as of Python 3.8.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR)
  541. raise
  542. # Return a list of paths to the downloaded whls
  543. return [
  544. os.path.join(whldir, filename)
  545. for filename in os.listdir(whldir)
  546. if filename.endswith('.whl')
  547. ]
  548. def update_pe_resources(self, appname, runtime):
  549. """Update resources (e.g., icons) in windows PE file"""
  550. icon = self.icon_objects.get(
  551. appname,
  552. self.icon_objects.get('*', None),
  553. )
  554. if icon is not None or self.prefer_discrete_gpu:
  555. pef = pefile.PEFile()
  556. pef.open(runtime, 'r+')
  557. if icon is not None:
  558. pef.add_icon(icon)
  559. pef.add_resource_section()
  560. if self.prefer_discrete_gpu:
  561. if not pef.rename_export("SymbolPlaceholder___________________", "AmdPowerXpressRequestHighPerformance") or \
  562. not pef.rename_export("SymbolPlaceholder__", "NvOptimusEnablement"):
  563. self.warn("Failed to apply prefer_discrete_gpu, newer target Panda3D version may be required")
  564. pef.write_changes()
  565. pef.close()
  566. def bundle_macos_app(self, builddir):
  567. """Bundle built runtime into a .app for macOS"""
  568. appname = '{}.app'.format(self.macos_main_app)
  569. appdir = os.path.join(builddir, appname)
  570. contentsdir = os.path.join(appdir, 'Contents')
  571. macosdir = os.path.join(contentsdir, 'MacOS')
  572. fwdir = os.path.join(contentsdir, 'Frameworks')
  573. resdir = os.path.join(contentsdir, 'Resources')
  574. self.announce('Bundling macOS app into {}'.format(appdir), distutils.log.INFO)
  575. # Create initial directory structure
  576. os.makedirs(macosdir)
  577. os.makedirs(fwdir)
  578. os.makedirs(resdir)
  579. # Move files over
  580. for fname in os.listdir(builddir):
  581. src = os.path.join(builddir, fname)
  582. if appdir in src:
  583. continue
  584. if fname in self.gui_apps or self.console_apps:
  585. dst = macosdir
  586. elif os.path.isfile(src) and open(src, 'rb').read(4) in macosx_binary_magics:
  587. dst = fwdir
  588. else:
  589. dst = resdir
  590. shutil.move(src, dst)
  591. # Write out Info.plist
  592. plist = {
  593. 'CFBundleName': appname,
  594. 'CFBundleDisplayName': appname, #TODO use name from setup.py/cfg
  595. 'CFBundleIdentifier': '', #TODO
  596. 'CFBundleVersion': '0.0.0', #TODO get from setup.py
  597. 'CFBundlePackageType': 'APPL',
  598. 'CFBundleSignature': '', #TODO
  599. 'CFBundleExecutable': self.macos_main_app,
  600. 'NSHighResolutionCapable': 'True',
  601. }
  602. icon = self.icon_objects.get(
  603. self.macos_main_app,
  604. self.icon_objects.get('*', None)
  605. )
  606. if icon is not None:
  607. plist['CFBundleIconFile'] = 'iconfile'
  608. icon.makeICNS(os.path.join(resdir, 'iconfile.icns'))
  609. with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
  610. plistlib.dump(plist, f)
  611. def generate_android_manifest(self, path):
  612. import xml.etree.ElementTree as ET
  613. name = self.distribution.get_name()
  614. version = self.distribution.get_version()
  615. classifiers = self.distribution.get_classifiers()
  616. is_game = False
  617. for classifier in classifiers:
  618. if classifier == 'Topic :: Games/Entertainment' or classifier.startswith('Topic :: Games/Entertainment ::'):
  619. is_game = True
  620. manifest = ET.Element('manifest')
  621. manifest.set('xmlns:android', 'http://schemas.android.com/apk/res/android')
  622. manifest.set('package', self.application_id)
  623. manifest.set('android:versionCode', str(int(self.android_version_code)))
  624. manifest.set('android:versionName', version)
  625. manifest.set('android:installLocation', 'auto')
  626. uses_sdk = ET.SubElement(manifest, 'uses-sdk')
  627. uses_sdk.set('android:minSdkVersion', str(int(self.android_min_sdk_version)))
  628. uses_sdk.set('android:targetSdkVersion', str(int(self.android_target_sdk_version)))
  629. if self.android_max_sdk_version:
  630. uses_sdk.set('android:maxSdkVersion', str(int(self.android_max_sdk_version)))
  631. if 'pandagles2' in self.plugins:
  632. uses_feature = ET.SubElement(manifest, 'uses-feature')
  633. uses_feature.set('android:glEsVersion', '0x00020000')
  634. uses_feature.set('android:required', 'false' if 'pandagles' in self.plugins else 'true')
  635. if 'p3openal_audio' in self.plugins:
  636. uses_feature = ET.SubElement(manifest, 'uses-feature')
  637. uses_feature.set('android:name', 'android.hardware.audio.output')
  638. uses_feature.set('android:required', 'false')
  639. uses_feature = ET.SubElement(manifest, 'uses-feature')
  640. uses_feature.set('android:name', 'android.hardware.gamepad')
  641. uses_feature.set('android:required', 'false')
  642. application = ET.SubElement(manifest, 'application')
  643. application.set('android:label', name)
  644. application.set('android:isGame', ('false', 'true')[is_game])
  645. application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
  646. application.set('android:extractNativeLibs', 'true')
  647. app_icon = self.icon_objects.get('*', self.icon_objects.get(self.macos_main_app))
  648. if app_icon:
  649. application.set('android:icon', '@mipmap/ic_launcher')
  650. for appname in self.gui_apps:
  651. activity = ET.SubElement(application, 'activity')
  652. activity.set('android:name', 'org.panda3d.android.PythonActivity')
  653. activity.set('android:label', appname)
  654. activity.set('android:theme', '@android:style/Theme.NoTitleBar')
  655. activity.set('android:configChanges', 'orientation|keyboardHidden')
  656. activity.set('android:launchMode', 'singleInstance')
  657. act_icon = self.icon_objects.get(appname)
  658. if act_icon and act_icon is not app_icon:
  659. activity.set('android:icon', '@mipmap/ic_' + appname)
  660. meta_data = ET.SubElement(activity, 'meta-data')
  661. meta_data.set('android:name', 'android.app.lib_name')
  662. meta_data.set('android:value', appname)
  663. intent_filter = ET.SubElement(activity, 'intent-filter')
  664. ET.SubElement(intent_filter, 'action').set('android:name', 'android.intent.action.MAIN')
  665. ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LAUNCHER')
  666. ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LEANBACK_LAUNCHER')
  667. tree = ET.ElementTree(manifest)
  668. with open(path, 'wb') as fh:
  669. tree.write(fh, encoding='utf-8', xml_declaration=True)
  670. def build_binaries(self, platform, binary_dir, data_dir=None):
  671. """ Builds the binary data for the given platform. """
  672. use_wheels = True
  673. path = sys.path[:]
  674. p3dwhl = None
  675. wheelpaths = []
  676. has_tkinter_wheel = False
  677. if use_wheels:
  678. wheelpaths = self.download_wheels(platform)
  679. for whl in wheelpaths:
  680. if os.path.basename(whl).startswith('panda3d-'):
  681. p3dwhlfn = whl
  682. p3dwhl = self._get_zip_file(p3dwhlfn)
  683. break
  684. elif os.path.basename(whl).startswith('tkinter-'):
  685. has_tkinter_wheel = True
  686. else:
  687. raise RuntimeError("Missing panda3d wheel for platform: {}".format(platform))
  688. if self.use_optimized_wheels:
  689. # Check to see if we have an optimized wheel
  690. localtag = p3dwhlfn.split('+')[1].split('-')[0] if '+' in p3dwhlfn else ''
  691. if not localtag.endswith('opt'):
  692. self.announce(
  693. 'Could not find an optimized wheel (using index {}) for platform: {}'.format(self.optimized_wheel_index, platform),
  694. distutils.log.WARN
  695. )
  696. for whl in wheelpaths:
  697. if os.path.basename(whl).startswith('tkinter-'):
  698. has_tkinter_wheel = True
  699. break
  700. #whlfiles = {whl: self._get_zip_file(whl) for whl in wheelpaths}
  701. # Add whl files to the path so they are picked up by modulefinder
  702. for whl in wheelpaths:
  703. path.insert(0, whl)
  704. # Add deploy_libs from panda3d whl to the path
  705. path.insert(0, os.path.join(p3dwhlfn, 'deploy_libs'))
  706. self.announce('Building runtime for platform: {}'.format(platform), distutils.log.INFO)
  707. # Gather PRC data
  708. prcstring = ''
  709. if not use_wheels:
  710. dtool_fn = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name())
  711. libdir = os.path.dirname(dtool_fn.to_os_specific())
  712. etcdir = os.path.join(libdir, '..', 'etc')
  713. for fn in sorted(os.listdir(etcdir), reverse=True):
  714. if fn.lower().endswith('.prc'):
  715. with open(os.path.join(etcdir, fn)) as f:
  716. prcstring += f.read()
  717. else:
  718. for fn in sorted((i for i in p3dwhl.namelist() if i.endswith('.prc')), reverse=True):
  719. with p3dwhl.open(fn) as f:
  720. prcstring += f.read().decode('utf8')
  721. user_prcstring = self.extra_prc_data
  722. for fn in self.extra_prc_files:
  723. with open(fn) as f:
  724. user_prcstring += f.read()
  725. # Clenup PRC data
  726. check_plugins = [
  727. #TODO find a better way to get this list
  728. 'pandaegg',
  729. 'p3ffmpeg',
  730. 'p3ptloader',
  731. 'p3assimp',
  732. ]
  733. def parse_prc(prcstr, warn_on_missing_plugin):
  734. out = []
  735. for ln in prcstr.split('\n'):
  736. ln = ln.strip()
  737. useline = True
  738. if ln.startswith('#') or not ln:
  739. continue
  740. words = ln.split(None, 1)
  741. if not words:
  742. continue
  743. var = words[0]
  744. value = words[1] if len(words) > 1 else ''
  745. # Strip comment after value.
  746. c = value.find(' #')
  747. if c > 0:
  748. value = value[:c].rstrip()
  749. if var == 'model-cache-dir' and value:
  750. if platform.startswith('android'):
  751. # Ignore on Android, where the cache dir is fixed.
  752. continue
  753. value = value.replace('/panda3d', '/{}'.format(self.distribution.get_name()))
  754. if var == 'audio-library-name':
  755. # We have the default set to p3fmod_audio on macOS in 1.10,
  756. # but this can be unexpected as other platforms use OpenAL
  757. # by default. Switch it up if FMOD is not included.
  758. if value not in self.plugins and value == 'p3fmod_audio' and 'p3openal_audio' in self.plugins:
  759. self.warn("Missing audio plugin p3fmod_audio referenced in PRC data, replacing with p3openal_audio")
  760. value = 'p3openal_audio'
  761. if var == 'aux-display':
  762. # Silently remove aux-display lines for missing plugins.
  763. if value not in self.plugins:
  764. continue
  765. for plugin in check_plugins:
  766. if plugin in value and plugin not in self.plugins:
  767. useline = False
  768. if warn_on_missing_plugin:
  769. self.warn(
  770. "Missing plugin ({0}) referenced in user PRC data".format(plugin)
  771. )
  772. break
  773. if useline:
  774. if value:
  775. out.append(var + ' ' + value)
  776. else:
  777. out.append(var)
  778. return out
  779. prcexport = parse_prc(prcstring, 0) + parse_prc(user_prcstring, 1)
  780. # Export PRC data
  781. prcexport = '\n'.join(prcexport)
  782. if not self.embed_prc_data:
  783. prcdir = self.default_prc_dir.replace('<auto>', '')
  784. prcdir = os.path.join(binary_dir, prcdir)
  785. os.makedirs(prcdir)
  786. with open(os.path.join(prcdir, '00-panda3d.prc'), 'w') as f:
  787. f.write(prcexport)
  788. # Create runtimes
  789. freezer_extras = set()
  790. freezer_modules = set()
  791. ext_suffixes = set()
  792. def get_search_path_for(source_path):
  793. search_path = [os.path.dirname(source_path)]
  794. if use_wheels:
  795. search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
  796. # If the .whl containing this file has a .libs directory, add
  797. # it to the path. This is an auditwheel/numpy convention.
  798. if '.whl' + os.sep in source_path:
  799. whl, wf = source_path.split('.whl' + os.path.sep)
  800. whl += '.whl'
  801. rootdir = wf.split(os.path.sep, 1)[0]
  802. search_path.append(os.path.join(whl, rootdir, '.libs'))
  803. # Also look for eg. numpy.libs or Pillow.libs in the root
  804. whl_name = os.path.basename(whl).split('-', 1)[0]
  805. search_path.append(os.path.join(whl, whl_name + '.libs'))
  806. # Also look for more specific per-package cases, defined in
  807. # PACKAGE_LIB_DIRS at the top of this file.
  808. extra_dirs = PACKAGE_LIB_DIRS.get(whl_name, [])
  809. for extra_dir, search_in in extra_dirs:
  810. if not search_in:
  811. search_path.append(os.path.join(whl, extra_dir.replace('/', os.path.sep)))
  812. else:
  813. for whl2 in wheelpaths:
  814. if os.path.basename(whl2).startswith(search_in + '-'):
  815. search_path.append(os.path.join(whl2, extra_dir.replace('/', os.path.sep)))
  816. return search_path
  817. def create_runtime(platform, appname, mainscript, use_console):
  818. freezer = FreezeTool.Freezer(
  819. platform=platform,
  820. path=path,
  821. hiddenImports=self.hidden_imports,
  822. optimize=2 if self.strip_docstrings else 1
  823. )
  824. freezer.addModule('__main__', filename=mainscript)
  825. if platform.startswith('android'):
  826. freezer.addModule('site', filename='site.py', text=SITE_PY_ANDROID)
  827. else:
  828. freezer.addModule('site', filename='site.py', text=SITE_PY)
  829. for incmod in self.include_modules.get(appname, []) + self.include_modules.get('*', []):
  830. freezer.addModule(incmod)
  831. for exmod in self.exclude_modules.get(appname, []) + self.exclude_modules.get('*', []):
  832. freezer.excludeModule(exmod)
  833. freezer.done(addStartupModules=True)
  834. stub_name = 'deploy-stub'
  835. target_name = appname
  836. if platform.startswith('win') or 'macosx' in platform:
  837. if not use_console:
  838. stub_name = 'deploy-stubw'
  839. elif platform.startswith('android'):
  840. if not use_console:
  841. stub_name = 'libdeploy-stubw.so'
  842. target_name = 'lib' + target_name + '.so'
  843. if platform.startswith('win'):
  844. stub_name += '.exe'
  845. target_name += '.exe'
  846. if use_wheels:
  847. if stub_name.endswith('.so'):
  848. stub_file = p3dwhl.open('deploy_libs/{0}'.format(stub_name))
  849. else:
  850. stub_file = p3dwhl.open('panda3d_tools/{0}'.format(stub_name))
  851. else:
  852. dtool_path = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name()).to_os_specific()
  853. stub_path = os.path.join(os.path.dirname(dtool_path), '..', 'bin', stub_name)
  854. stub_file = open(stub_path, 'rb')
  855. # Do we need an icon? On Windows, we need to add this to the stub
  856. # before we add the blob.
  857. if 'win' in platform:
  858. temp_file = tempfile.NamedTemporaryFile(suffix='-icon.exe', delete=False)
  859. temp_file.write(stub_file.read())
  860. stub_file.close()
  861. temp_file.close()
  862. self.update_pe_resources(appname, temp_file.name)
  863. stub_file = open(temp_file.name, 'rb')
  864. else:
  865. temp_file = None
  866. use_strftime = self.log_filename_strftime
  867. if not self.log_filename or '%' not in self.log_filename:
  868. use_strftime = False
  869. target_path = os.path.join(binary_dir, target_name)
  870. freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
  871. 'prc_data': prcexport if self.embed_prc_data else None,
  872. 'default_prc_dir': self.default_prc_dir,
  873. 'prc_dir_envvars': None,
  874. 'prc_path_envvars': None,
  875. 'prc_patterns': None,
  876. 'prc_encrypted_patterns': None,
  877. 'prc_encryption_key': None,
  878. 'prc_executable_patterns': None,
  879. 'prc_executable_args_envvar': None,
  880. 'main_dir': None,
  881. 'log_filename': self.expand_path(self.log_filename, platform),
  882. }, self.log_append, use_strftime)
  883. stub_file.close()
  884. if temp_file:
  885. os.unlink(temp_file.name)
  886. # Copy the dependencies.
  887. search_path = [binary_dir]
  888. if use_wheels:
  889. search_path.append(os.path.join(p3dwhlfn, 'panda3d'))
  890. search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
  891. self.copy_dependencies(target_path, binary_dir, search_path, stub_name)
  892. freezer_extras.update(freezer.extras)
  893. freezer_modules.update(freezer.getAllModuleNames())
  894. for suffix in freezer.mf.suffixes:
  895. if suffix[2] == 3: # imp.C_EXTENSION:
  896. ext_suffixes.add(suffix[0])
  897. for appname, scriptname in self.gui_apps.items():
  898. create_runtime(platform, appname, scriptname, False)
  899. for appname, scriptname in self.console_apps.items():
  900. create_runtime(platform, appname, scriptname, True)
  901. # Warn if tkinter is used but hasn't been added to requirements.txt
  902. if not has_tkinter_wheel and '_tkinter' in freezer_modules:
  903. self.warn("Detected use of tkinter, but tkinter is not specified in requirements.txt!")
  904. # Copy extension modules
  905. whl_modules = {}
  906. if use_wheels:
  907. # Get the module libs
  908. for i in p3dwhl.namelist():
  909. if not i.startswith('deploy_libs/'):
  910. continue
  911. if not any(i.endswith(suffix) for suffix in ext_suffixes):
  912. continue
  913. if has_tkinter_wheel and i.startswith('deploy_libs/_tkinter.'):
  914. # Ignore this one, we have a separate tkinter package
  915. # nowadays that contains all the dependencies.
  916. continue
  917. base = os.path.basename(i)
  918. module, _, ext = base.partition('.')
  919. whl_modules[module] = i
  920. # Make sure to copy any builtins that have shared objects in the
  921. # deploy libs, assuming they are not already in freezer_extras.
  922. for mod, source_path in freezer_extras:
  923. freezer_modules.discard(mod)
  924. for mod in freezer_modules:
  925. if mod in whl_modules:
  926. freezer_extras.add((mod, None))
  927. # Copy over necessary plugins
  928. plugin_list = ['panda3d/lib{}'.format(i) for i in self.plugins]
  929. for lib in p3dwhl.namelist():
  930. plugname = lib.split('.', 1)[0]
  931. if plugname in plugin_list:
  932. source_path = os.path.join(p3dwhlfn, lib)
  933. target_path = os.path.join(binary_dir, os.path.basename(lib))
  934. search_path = [os.path.dirname(source_path)]
  935. self.copy_with_dependencies(source_path, target_path, search_path)
  936. # Copy any shared objects we need
  937. for module, source_path in freezer_extras:
  938. if source_path is not None:
  939. # Rename panda3d/core.pyd to panda3d.core.pyd
  940. source_path = os.path.normpath(source_path)
  941. basename = os.path.basename(source_path)
  942. if '.' in module:
  943. basename = module.rsplit('.', 1)[0] + '.' + basename
  944. # Remove python version string
  945. parts = basename.split('.')
  946. if len(parts) >= 3 and ('-' in parts[-2] or parts[-2] == 'abi' + str(sys.version_info[0])):
  947. parts = parts[:-2] + parts[-1:]
  948. basename = '.'.join(parts)
  949. # Was this not found in a wheel? Then we may have a problem,
  950. # since it may be for the current platform instead of the target
  951. # platform.
  952. if use_wheels:
  953. found_in_wheel = False
  954. for whl in wheelpaths:
  955. whl = os.path.normpath(whl)
  956. if source_path.lower().startswith(os.path.join(whl, '').lower()):
  957. found_in_wheel = True
  958. break
  959. if not found_in_wheel:
  960. self.warn('{} was not found in any downloaded wheel, is a dependency missing from requirements.txt?'.format(basename))
  961. else:
  962. # Builtin module, but might not be builtin in wheel libs, so double check
  963. if module in whl_modules:
  964. source_path = os.path.join(p3dwhlfn, whl_modules[module])
  965. basename = os.path.basename(source_path)
  966. #XXX should we remove python version string here too?
  967. else:
  968. continue
  969. if platform.startswith('android'):
  970. # Python modules on Android need a special prefix to be loadable
  971. # as a library.
  972. basename = 'libpy.' + basename
  973. # If this is a dynamic library, search for dependencies.
  974. target_path = os.path.join(binary_dir, basename)
  975. search_path = get_search_path_for(source_path)
  976. self.copy_with_dependencies(source_path, target_path, search_path)
  977. # Copy classes.dex on Android
  978. if use_wheels and platform.startswith('android'):
  979. self.copy(os.path.join(p3dwhlfn, 'deploy_libs', 'classes.dex'),
  980. os.path.join(binary_dir, '..', '..', 'classes.dex'))
  981. # Extract any other data files from dependency packages.
  982. if data_dir is None:
  983. return
  984. for module, datadesc in self.package_data_dirs.items():
  985. if module not in freezer_modules:
  986. continue
  987. self.announce('Copying data files for module: {}'.format(module), distutils.log.INFO)
  988. # OK, find out in which .whl this occurs.
  989. for whl in wheelpaths:
  990. whlfile = self._get_zip_file(whl)
  991. filenames = whlfile.namelist()
  992. for source_pattern, target_dir, flags in datadesc:
  993. srcglob = p3d.GlobPattern(source_pattern.lower())
  994. source_dir = os.path.dirname(source_pattern)
  995. # Relocate the target dir to the build directory.
  996. target_dir = target_dir.replace('/', os.sep)
  997. target_dir = os.path.join(data_dir, target_dir)
  998. for wf in filenames:
  999. if wf.endswith('/'):
  1000. # Skip directories.
  1001. continue
  1002. if wf.lower().startswith(source_dir.lower() + '/'):
  1003. if not srcglob.matches(wf.lower()):
  1004. continue
  1005. wf = wf.replace('/', os.sep)
  1006. relpath = wf[len(source_dir) + 1:]
  1007. source_path = os.path.join(whl, wf)
  1008. target_path = os.path.join(target_dir, relpath)
  1009. if 'PKG_DATA_MAKE_EXECUTABLE' in flags:
  1010. search_path = get_search_path_for(source_path)
  1011. self.copy_with_dependencies(source_path, target_path, search_path)
  1012. mode = os.stat(target_path).st_mode
  1013. mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
  1014. os.chmod(target_path, mode)
  1015. else:
  1016. self.copy(source_path, target_path)
  1017. def build_assets(self, platform, data_dir):
  1018. """ Builds the data files for the given platform. """
  1019. # Copy Game Files
  1020. self.announce('Copying assets for platform: {}'.format(platform), distutils.log.INFO)
  1021. ignore_copy_list = [
  1022. '**/__pycache__/**',
  1023. '**/*.pyc',
  1024. '**/*.py',
  1025. '{}/**'.format(self.build_base),
  1026. ]
  1027. ignore_copy_list += self.exclude_patterns
  1028. ignore_copy_list += self.extra_prc_files
  1029. ignore_copy_list = [p3d.GlobPattern(p3d.Filename.from_os_specific(i).get_fullpath()) for i in ignore_copy_list]
  1030. include_copy_list = [p3d.GlobPattern(i) for i in self.include_patterns]
  1031. def check_pattern(src, pattern_list):
  1032. # Normalize file paths across platforms
  1033. fn = p3d.Filename.from_os_specific(os.path.normpath(src))
  1034. path = fn.get_fullpath()
  1035. fn.make_absolute()
  1036. abspath = fn.get_fullpath()
  1037. for pattern in pattern_list:
  1038. # If the pattern is absolute, match against the absolute filename.
  1039. if pattern.pattern[0] == '/':
  1040. #print('check ignore: {} {} {}'.format(pattern, src, pattern.matches_file(abspath)))
  1041. if pattern.matches_file(abspath):
  1042. return True
  1043. else:
  1044. #print('check ignore: {} {} {}'.format(pattern, src, pattern.matches_file(path)))
  1045. if pattern.matches_file(path):
  1046. return True
  1047. return False
  1048. def check_file(fname):
  1049. return check_pattern(fname, include_copy_list) and \
  1050. not check_pattern(fname, ignore_copy_list)
  1051. def skip_directory(src):
  1052. # Provides a quick-out for directory checks. NOT recursive.
  1053. fn = p3d.Filename.from_os_specific(os.path.normpath(src))
  1054. path = fn.get_fullpath()
  1055. fn.make_absolute()
  1056. abspath = fn.get_fullpath()
  1057. for pattern in ignore_copy_list:
  1058. if not pattern.pattern.endswith('/*') and \
  1059. not pattern.pattern.endswith('/**'):
  1060. continue
  1061. pattern_dir = p3d.Filename(pattern.pattern).get_dirname()
  1062. if abspath.startswith(pattern_dir + '/'):
  1063. return True
  1064. if path.startswith(pattern_dir + '/'):
  1065. return True
  1066. return False
  1067. def copy_file(src, dst):
  1068. src = os.path.normpath(src)
  1069. dst = os.path.normpath(dst)
  1070. if not check_file(src):
  1071. self.announce('skipping file {}'.format(src))
  1072. return
  1073. dst_dir = os.path.dirname(dst)
  1074. if not os.path.exists(dst_dir):
  1075. os.makedirs(dst_dir)
  1076. ext = os.path.splitext(src)[1]
  1077. # If the file ends with .gz/.pz, we strip this off.
  1078. if ext in ('.gz', '.pz'):
  1079. ext = os.path.splitext(src[:-3])[1]
  1080. if not ext:
  1081. ext = os.path.basename(src)
  1082. if ext in self.file_handlers:
  1083. buildscript = self.file_handlers[ext]
  1084. self.announce('running {} on src ({})'.format(buildscript.__name__, src))
  1085. try:
  1086. dst = self.file_handlers[ext](self, src, dst)
  1087. except Exception as err:
  1088. self.announce('{}'.format(err), distutils.log.ERROR)
  1089. else:
  1090. self.announce('copying {0} -> {1}'.format(src, dst))
  1091. shutil.copyfile(src, dst)
  1092. def update_path(path):
  1093. normpath = p3d.Filename.from_os_specific(os.path.normpath(src)).c_str()
  1094. for inputpath, outputpath in self.rename_paths.items():
  1095. if normpath.startswith(inputpath):
  1096. normpath = normpath.replace(inputpath, outputpath, 1)
  1097. return p3d.Filename(normpath).to_os_specific()
  1098. rootdir = os.getcwd()
  1099. for dirname, subdirlist, filelist in os.walk(rootdir):
  1100. subdirlist.sort()
  1101. dirpath = os.path.relpath(dirname, rootdir)
  1102. if skip_directory(dirpath):
  1103. self.announce('skipping directory {}'.format(dirpath))
  1104. continue
  1105. for fname in filelist:
  1106. src = os.path.join(dirpath, fname)
  1107. dst = os.path.join(data_dir, update_path(src))
  1108. copy_file(src, dst)
  1109. def add_dependency(self, name, target_dir, search_path, referenced_by):
  1110. """ Searches for the given DLL on the search path. If it exists,
  1111. copies it to the target_dir. """
  1112. if os.path.exists(os.path.join(target_dir, name)):
  1113. # We've already added it earlier.
  1114. return
  1115. for dep in self.exclude_dependencies:
  1116. if dep.matches_file(name):
  1117. return
  1118. for dir in search_path:
  1119. source_path = os.path.join(dir, name)
  1120. if os.path.isfile(source_path):
  1121. target_path = os.path.join(target_dir, name)
  1122. self.copy_with_dependencies(source_path, target_path, search_path)
  1123. return
  1124. elif '.whl' in source_path:
  1125. # Check whether the file exists inside the wheel.
  1126. whl, wf = source_path.split('.whl' + os.path.sep)
  1127. whl += '.whl'
  1128. whlfile = self._get_zip_file(whl)
  1129. # Normalize the path separator
  1130. wf = os.path.normpath(wf).replace(os.path.sep, '/')
  1131. # Look case-insensitively.
  1132. namelist = whlfile.namelist()
  1133. namelist_lower = [file.lower() for file in namelist]
  1134. if wf.lower() in namelist_lower:
  1135. # We have a match. Change it to the correct case.
  1136. wf = namelist[namelist_lower.index(wf.lower())]
  1137. source_path = os.path.join(whl, wf)
  1138. target_path = os.path.join(target_dir, os.path.basename(wf))
  1139. self.copy_with_dependencies(source_path, target_path, search_path)
  1140. return
  1141. # If we didn't find it, look again, but case-insensitively.
  1142. name_lower = name.lower()
  1143. for dir in search_path:
  1144. if os.path.isdir(dir):
  1145. files = os.listdir(dir)
  1146. files_lower = [file.lower() for file in files]
  1147. if name_lower in files_lower:
  1148. name = files[files_lower.index(name_lower)]
  1149. source_path = os.path.join(dir, name)
  1150. target_path = os.path.join(target_dir, name)
  1151. self.copy_with_dependencies(source_path, target_path, search_path)
  1152. # Warn if we can't find it, but only once.
  1153. self.warn("could not find dependency {0} (referenced by {1})".format(name, referenced_by))
  1154. self.exclude_dependencies.append(p3d.GlobPattern(name.lower()))
  1155. def copy(self, source_path, target_path):
  1156. """ Copies source_path to target_path.
  1157. source_path may be located inside a .whl file. """
  1158. try:
  1159. self.announce('copying {0} -> {1}'.format(os.path.relpath(source_path, self.build_base), os.path.relpath(target_path, self.build_base)))
  1160. except ValueError:
  1161. # No relative path (e.g., files on different drives in Windows), just print absolute paths instead
  1162. self.announce('copying {0} -> {1}'.format(source_path, target_path))
  1163. # Make the directory if it does not yet exist.
  1164. target_dir = os.path.dirname(target_path)
  1165. if not os.path.isdir(target_dir):
  1166. os.makedirs(target_dir)
  1167. # Copy the file, and open it for analysis.
  1168. if '.whl' in source_path:
  1169. # This was found in a wheel, extract it
  1170. whl, wf = source_path.split('.whl' + os.path.sep)
  1171. whl += '.whl'
  1172. whlfile = self._get_zip_file(whl)
  1173. data = whlfile.read(wf.replace(os.path.sep, '/'))
  1174. with open(target_path, 'wb') as f:
  1175. f.write(data)
  1176. else:
  1177. # Regular file, copy it
  1178. shutil.copyfile(source_path, target_path)
  1179. def copy_with_dependencies(self, source_path, target_path, search_path):
  1180. """ Copies source_path to target_path. It also scans source_path for
  1181. any dependencies, which are located along the given search_path and
  1182. copied to the same directory as target_path.
  1183. source_path may be located inside a .whl file. """
  1184. self.copy(source_path, target_path)
  1185. source_dir = os.path.dirname(source_path)
  1186. target_dir = os.path.dirname(target_path)
  1187. base = os.path.basename(target_path)
  1188. if source_dir not in search_path:
  1189. search_path = search_path + [source_dir]
  1190. self.copy_dependencies(target_path, target_dir, search_path, base)
  1191. def copy_dependencies(self, target_path, target_dir, search_path, referenced_by):
  1192. """ Copies the dependencies of target_path into target_dir. """
  1193. fp = open(target_path, 'rb+')
  1194. # What kind of magic does the file contain?
  1195. deps = []
  1196. magic = fp.read(4)
  1197. if magic.startswith(b'MZ'):
  1198. # It's a Windows DLL or EXE file.
  1199. pe = pefile.PEFile()
  1200. pe.read(fp)
  1201. for lib in pe.imports:
  1202. deps.append(lib)
  1203. elif magic == b'\x7FELF':
  1204. # Elf magic. Used on (among others) Linux and FreeBSD.
  1205. deps = self._read_dependencies_elf(fp, target_dir, search_path)
  1206. elif magic in (b'\xCE\xFA\xED\xFE', b'\xCF\xFA\xED\xFE'):
  1207. # A Mach-O file, as used on macOS.
  1208. deps = self._read_dependencies_macho(fp, '<', flatten=True)
  1209. elif magic in (b'\xFE\xED\xFA\xCE', b'\xFE\xED\xFA\xCF'):
  1210. rel_dir = os.path.relpath(target_dir, os.path.dirname(target_path))
  1211. deps = self._read_dependencies_macho(fp, '>', flatten=True)
  1212. elif magic in (b'\xCA\xFE\xBA\xBE', b'\xBE\xBA\xFE\xCA'):
  1213. # A fat file, containing multiple Mach-O binaries. In the future,
  1214. # we may want to extract the one containing the architecture we
  1215. # are building for.
  1216. deps = self._read_dependencies_fat(fp, False, flatten=True)
  1217. elif magic in (b'\xCA\xFE\xBA\xBF', b'\xBF\xBA\xFE\xCA'):
  1218. # A 64-bit fat file.
  1219. deps = self._read_dependencies_fat(fp, True, flatten=True)
  1220. # If we discovered any dependencies, recursively add those.
  1221. for dep in deps:
  1222. self.add_dependency(dep, target_dir, search_path, referenced_by)
  1223. def _read_dependencies_elf(self, elf, origin, search_path):
  1224. """ Having read the first 4 bytes of the ELF file, fetches the
  1225. dependent libraries and returns those as a list. """
  1226. ident = elf.read(12)
  1227. # Make sure we read in the correct endianness and integer size
  1228. byte_order = "<>"[ord(ident[1:2]) - 1]
  1229. elf_class = ord(ident[0:1]) - 1 # 0 = 32-bits, 1 = 64-bits
  1230. header_struct = byte_order + ("HHIIIIIHHHHHH", "HHIQQQIHHHHHH")[elf_class]
  1231. section_struct = byte_order + ("4xI8xIII8xI", "4xI16xQQI12xQ")[elf_class]
  1232. dynamic_struct = byte_order + ("iI", "qQ")[elf_class]
  1233. type, machine, version, entry, phoff, shoff, flags, ehsize, phentsize, phnum, shentsize, shnum, shstrndx \
  1234. = struct.unpack(header_struct, elf.read(struct.calcsize(header_struct)))
  1235. dynamic_sections = []
  1236. string_tables = {}
  1237. # Seek to the section header table and find the .dynamic section.
  1238. elf.seek(shoff)
  1239. for i in range(shnum):
  1240. type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
  1241. if type == 6 and link != 0: # DYNAMIC type, links to string table
  1242. dynamic_sections.append((offset, size, link, entsize))
  1243. string_tables[link] = None
  1244. # Read the relevant string tables.
  1245. for idx in string_tables.keys():
  1246. elf.seek(shoff + idx * shentsize)
  1247. type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
  1248. if type != 3:
  1249. continue
  1250. elf.seek(offset)
  1251. string_tables[idx] = elf.read(size)
  1252. # Loop through the dynamic sections and rewrite it if it has an rpath/runpath.
  1253. needed = []
  1254. rpath = []
  1255. for offset, size, link, entsize in dynamic_sections:
  1256. elf.seek(offset)
  1257. data = elf.read(entsize)
  1258. tag, val = struct.unpack_from(dynamic_struct, data)
  1259. # Read tags until we find a NULL tag.
  1260. while tag != 0:
  1261. if tag == 1: # A NEEDED entry. Read it from the string table.
  1262. string = string_tables[link][val : string_tables[link].find(b'\0', val)]
  1263. needed.append(string.decode('utf-8'))
  1264. elif tag == 15 or tag == 29:
  1265. # An RPATH or RUNPATH entry.
  1266. string = string_tables[link][val : string_tables[link].find(b'\0', val)]
  1267. rpath += [
  1268. os.path.normpath(i.decode('utf-8').replace('$ORIGIN', origin))
  1269. for i in string.split(b':')
  1270. ]
  1271. data = elf.read(entsize)
  1272. tag, val = struct.unpack_from(dynamic_struct, data)
  1273. elf.close()
  1274. search_path += rpath
  1275. return needed
  1276. def _read_dependencies_macho(self, fp, endian, flatten=False):
  1277. """ Having read the first 4 bytes of the Mach-O file, fetches the
  1278. dependent libraries and returns those as a list.
  1279. If flatten is True, if the dependencies contain paths like
  1280. @loader_path/../.dylibs/libsomething.dylib, it will rewrite them to
  1281. instead contain @loader_path/libsomething.dylib if possible.
  1282. This requires the file pointer to be opened in rb+ mode. """
  1283. cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags = \
  1284. struct.unpack(endian + 'IIIIII', fp.read(24))
  1285. is_64bit = (cputype & 0x1000000) != 0
  1286. if is_64bit:
  1287. fp.read(4)
  1288. # After the header, we get a series of linker commands. We just
  1289. # iterate through them and gather up the LC_LOAD_DYLIB commands.
  1290. load_dylibs = []
  1291. for i in range(ncmds):
  1292. cmd, cmd_size = struct.unpack(endian + 'II', fp.read(8))
  1293. cmd_data = fp.read(cmd_size - 8)
  1294. cmd &= ~0x80000000
  1295. if cmd == 0x0c: # LC_LOAD_DYLIB
  1296. dylib = cmd_data[16:].decode('ascii').split('\x00', 1)[0]
  1297. orig = dylib
  1298. if dylib.startswith('@loader_path/../Frameworks/'):
  1299. dylib = dylib.replace('@loader_path/../Frameworks/', '')
  1300. elif dylib.startswith('@executable_path/../Frameworks/'):
  1301. dylib = dylib.replace('@executable_path/../Frameworks/', '')
  1302. else:
  1303. for prefix in ('@loader_path/', '@rpath/'):
  1304. if dylib.startswith(prefix):
  1305. dylib = dylib.replace(prefix, '')
  1306. # Do we need to flatten the relative reference?
  1307. if '/' in dylib and flatten:
  1308. new_dylib = prefix + os.path.basename(dylib)
  1309. str_size = len(cmd_data) - 16
  1310. if len(new_dylib) < str_size:
  1311. fp.seek(-str_size, os.SEEK_CUR)
  1312. fp.write(new_dylib.encode('ascii').ljust(str_size, b'\0'))
  1313. else:
  1314. self.warn('Unable to rewrite dependency {}'.format(orig))
  1315. load_dylibs.append(dylib)
  1316. return load_dylibs
  1317. def _read_dependencies_fat(self, fp, is_64bit, flatten=False):
  1318. num_fat, = struct.unpack('>I', fp.read(4))
  1319. # After the header we get a table of executables in this fat file,
  1320. # each one with a corresponding offset into the file.
  1321. offsets = []
  1322. for i in range(num_fat):
  1323. if is_64bit:
  1324. cputype, cpusubtype, offset, size, align = \
  1325. struct.unpack('>QQQQQ', fp.read(40))
  1326. else:
  1327. cputype, cpusubtype, offset, size, align = \
  1328. struct.unpack('>IIIII', fp.read(20))
  1329. offsets.append(offset)
  1330. # Go through each of the binaries in the fat file.
  1331. deps = []
  1332. for offset in offsets:
  1333. # Add 4, since it expects we've already read the magic.
  1334. fp.seek(offset)
  1335. magic = fp.read(4)
  1336. if magic in (b'\xCE\xFA\xED\xFE', b'\xCF\xFA\xED\xFE'):
  1337. endian = '<'
  1338. elif magic in (b'\xFE\xED\xFA\xCE', b'\xFE\xED\xFA\xCF'):
  1339. endian = '>'
  1340. else:
  1341. # Not a Mach-O file we can read.
  1342. continue
  1343. for dep in self._read_dependencies_macho(fp, endian, flatten=flatten):
  1344. if dep not in deps:
  1345. deps.append(dep)
  1346. return deps
  1347. def expand_path(self, path, platform):
  1348. "Substitutes variables in the given path string."
  1349. if path is None:
  1350. return None
  1351. t = string.Template(path)
  1352. if platform.startswith('win'):
  1353. return t.substitute(HOME='~', USER_APPDATA='~/AppData/Local')
  1354. elif platform.startswith('macosx'):
  1355. return t.substitute(HOME='~', USER_APPDATA='~/Documents')
  1356. else:
  1357. return t.substitute(HOME='~', USER_APPDATA='~/.local/share')
  1358. class bdist_apps(setuptools.Command):
  1359. DEFAULT_INSTALLERS = {
  1360. 'manylinux1_x86_64': ['gztar'],
  1361. 'manylinux1_i686': ['gztar'],
  1362. 'manylinux2010_x86_64': ['gztar'],
  1363. 'manylinux2010_i686': ['gztar'],
  1364. 'manylinux2014_x86_64': ['gztar'],
  1365. 'manylinux2014_i686': ['gztar'],
  1366. 'manylinux2014_aarch64': ['gztar'],
  1367. 'manylinux2014_armv7l': ['gztar'],
  1368. 'manylinux2014_ppc64': ['gztar'],
  1369. 'manylinux2014_ppc64le': ['gztar'],
  1370. 'manylinux2014_s390x': ['gztar'],
  1371. 'manylinux_2_24_x86_64': ['gztar'],
  1372. 'manylinux_2_24_i686': ['gztar'],
  1373. 'manylinux_2_24_aarch64': ['gztar'],
  1374. 'manylinux_2_24_armv7l': ['gztar'],
  1375. 'manylinux_2_24_ppc64': ['gztar'],
  1376. 'manylinux_2_24_ppc64le': ['gztar'],
  1377. 'manylinux_2_24_s390x': ['gztar'],
  1378. 'manylinux_2_28_x86_64': ['gztar'],
  1379. 'manylinux_2_28_aarch64': ['gztar'],
  1380. 'manylinux_2_28_ppc64le': ['gztar'],
  1381. 'manylinux_2_28_s390x': ['gztar'],
  1382. 'android': ['aab'],
  1383. # Everything else defaults to ['zip']
  1384. }
  1385. DEFAULT_INSTALLER_FUNCS = {
  1386. 'zip': installers.create_zip,
  1387. 'gztar': installers.create_gztar,
  1388. 'bztar': installers.create_bztar,
  1389. 'xztar': installers.create_xztar,
  1390. 'nsis': installers.create_nsis,
  1391. 'aab': installers.create_aab,
  1392. }
  1393. description = 'bundle built Panda3D applications into distributable forms'
  1394. user_options = build_apps.user_options + [
  1395. ('dist-dir=', 'd', 'directory to put final built distributions in'),
  1396. ('skip-build', None, 'skip rebuilding everything (for testing/debugging)'),
  1397. ]
  1398. def _build_apps_options(self):
  1399. return [opt[0].replace('-', '_').replace('=', '') for opt in build_apps.user_options]
  1400. def initialize_options(self):
  1401. self.installers = {}
  1402. self.dist_dir = os.path.join(os.getcwd(), 'dist')
  1403. self.skip_build = False
  1404. self.signing_certificate = None
  1405. self.signing_private_key = None
  1406. self.signing_passphrase = None
  1407. self.installer_functions = {}
  1408. self._current_platform = None
  1409. for opt in self._build_apps_options():
  1410. setattr(self, opt, None)
  1411. def finalize_options(self):
  1412. from importlib.metadata import entry_points
  1413. # We need to massage the inputs a bit in case they came from a
  1414. # setup.cfg file.
  1415. self.installers = {
  1416. key: _parse_list(value)
  1417. for key, value in _parse_dict(self.installers).items()
  1418. }
  1419. if self.signing_certificate:
  1420. assert self.signing_private_key, 'Missing signing_private_key'
  1421. self.signing_certificate = os.path.abspath(self.signing_certificate)
  1422. self.signing_private_key = os.path.abspath(self.signing_private_key)
  1423. eps = entry_points()
  1424. if isinstance(eps, dict): # Python 3.8 and 3.9
  1425. installer_eps = eps.get('panda3d.bdist_apps.installers', ())
  1426. else:
  1427. installer_eps = eps.select(group='panda3d.bdist_apps.installers')
  1428. tmp = self.DEFAULT_INSTALLER_FUNCS.copy()
  1429. tmp.update(self.installer_functions)
  1430. tmp.update({
  1431. entrypoint.name: entrypoint.load()
  1432. for entrypoint in installer_eps
  1433. })
  1434. self.installer_functions = tmp
  1435. def get_archive_basedir(self):
  1436. return self.distribution.get_name()
  1437. def get_current_platform(self):
  1438. return self._current_platform
  1439. def run(self):
  1440. build_cmd = self.distribution.get_command_obj('build_apps')
  1441. for opt in self._build_apps_options():
  1442. optval = getattr(self, opt)
  1443. if optval is not None:
  1444. setattr(build_cmd, opt, optval)
  1445. if not self.skip_build:
  1446. self.run_command('build_apps')
  1447. else:
  1448. build_cmd.finalize_options()
  1449. platforms = build_cmd.platforms
  1450. build_base = os.path.abspath(build_cmd.build_base)
  1451. if not os.path.exists(self.dist_dir):
  1452. os.makedirs(self.dist_dir)
  1453. os.chdir(self.dist_dir)
  1454. for platform in platforms:
  1455. build_dir = os.path.join(build_base, platform)
  1456. basename = '{}_{}'.format(self.distribution.get_fullname(), platform)
  1457. installers = self.installers.get(platform, self.DEFAULT_INSTALLERS.get(platform, ['zip']))
  1458. self._current_platform = platform
  1459. for installer in installers:
  1460. self.announce('\nBuilding {} for platform: {}'.format(installer, platform), distutils.log.INFO)
  1461. if installer not in self.installer_functions:
  1462. self.announce(
  1463. '\tUnknown installer: {}'.format(installer),
  1464. distutils.log.ERROR
  1465. )
  1466. continue
  1467. self.installer_functions[installer](self, basename, build_dir)