commands.py 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457
  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 collections
  6. import os
  7. import plistlib
  8. import sys
  9. import subprocess
  10. import zipfile
  11. import re
  12. import shutil
  13. import stat
  14. import struct
  15. import imp
  16. import string
  17. import time
  18. import tempfile
  19. import setuptools
  20. import distutils.log
  21. from . import FreezeTool
  22. from . import pefile
  23. from .icon import Icon
  24. import panda3d.core as p3d
  25. if sys.version_info < (3, 0):
  26. # Warn the user. They might be using Python 2 by accident.
  27. print("=================================================================")
  28. print("WARNING: You are using Python 2, which has reached the end of its")
  29. print("WARNING: life as of January 1, 2020. Please upgrade to Python 3.")
  30. print("=================================================================")
  31. sys.stdout.flush()
  32. time.sleep(4.0)
  33. def _parse_list(input):
  34. if isinstance(input, str):
  35. input = input.strip().replace(',', '\n')
  36. if input:
  37. return [item.strip() for item in input.split('\n') if item.strip()]
  38. else:
  39. return []
  40. else:
  41. return input
  42. def _parse_dict(input):
  43. if isinstance(input, dict):
  44. return input
  45. d = {}
  46. for item in _parse_list(input):
  47. key, sep, value = item.partition('=')
  48. d[key.strip()] = value.strip()
  49. return d
  50. def egg2bam(_build_cmd, srcpath, dstpath):
  51. if dstpath.endswith('.gz') or dstpath.endswith('.pz'):
  52. dstpath = dstpath[:-3]
  53. dstpath = dstpath + '.bam'
  54. try:
  55. subprocess.check_call([
  56. 'egg2bam',
  57. '-o', dstpath,
  58. '-pd', os.path.dirname(os.path.abspath(srcpath)),
  59. '-ps', 'rel',
  60. srcpath
  61. ])
  62. except FileNotFoundError:
  63. raise RuntimeError('egg2bam failed: egg2bam was not found in the PATH')
  64. except (subprocess.CalledProcessError, OSError) as err:
  65. raise RuntimeError('egg2bam failed: {}'.format(err))
  66. return dstpath
  67. macosx_binary_magics = (
  68. b'\xFE\xED\xFA\xCE', b'\xCE\xFA\xED\xFE',
  69. b'\xFE\xED\xFA\xCF', b'\xCF\xFA\xED\xFE',
  70. b'\xCA\xFE\xBA\xBE', b'\xBE\xBA\xFE\xCA',
  71. b'\xCA\xFE\xBA\xBF', b'\xBF\xBA\xFE\xCA')
  72. # Some dependencies need data directories to be extracted. This dictionary maps
  73. # modules with data to extract. The values are lists of tuples of the form
  74. # (source_pattern, destination_pattern, flags). The flags is a set of strings.
  75. PACKAGE_DATA_DIRS = {
  76. 'matplotlib': [('matplotlib/mpl-data/*', 'mpl-data', {})],
  77. 'jsonschema': [('jsonschema/schemas/*', 'schemas', {})],
  78. 'cefpython3': [
  79. ('cefpython3/*.pak', '', {}),
  80. ('cefpython3/*.dat', '', {}),
  81. ('cefpython3/*.bin', '', {}),
  82. ('cefpython3/*.dll', '', {}),
  83. ('cefpython3/libcef.so', '', {}),
  84. ('cefpython3/LICENSE.txt', '', {}),
  85. ('cefpython3/License', '', {}),
  86. ('cefpython3/subprocess*', '', {'PKG_DATA_MAKE_EXECUTABLE'}),
  87. ('cefpython3/locals/*', 'locals', {}),
  88. ('cefpython3/Chromium Embedded Framework.framework/Resources', 'Chromium Embedded Framework.framework/Resources', {}),
  89. ('cefpython3/Chromium Embedded Framework.framework/Chromium Embedded Framework', '', {'PKG_DATA_MAKE_EXECUTABLE'}),
  90. ],
  91. }
  92. # Some dependencies have extra directories that need to be scanned for DLLs.
  93. # This dictionary maps wheel basenames (ie. the part of the .whl basename
  94. # before the first hyphen) to a list of directories inside the .whl.
  95. PACKAGE_LIB_DIRS = {
  96. 'scipy': ['scipy/extra-dll'],
  97. }
  98. SITE_PY = u"""
  99. import sys
  100. from _frozen_importlib import _imp, FrozenImporter
  101. sys.frozen = True
  102. if sys.platform == 'win32':
  103. # Make sure the preferred encoding is something we actually support.
  104. import _bootlocale
  105. enc = _bootlocale.getpreferredencoding().lower()
  106. if enc != 'utf-8' and not _imp.is_frozen('encodings.%s' % (enc)):
  107. def getpreferredencoding(do_setlocale=True):
  108. return 'mbcs'
  109. _bootlocale.getpreferredencoding = getpreferredencoding
  110. # Alter FrozenImporter to give a __file__ property to frozen modules.
  111. _find_spec = FrozenImporter.find_spec
  112. def find_spec(fullname, path=None, target=None):
  113. spec = _find_spec(fullname, path=path, target=target)
  114. if spec:
  115. spec.has_location = True
  116. spec.origin = sys.executable
  117. return spec
  118. def get_data(path):
  119. with open(path, 'rb') as fp:
  120. return fp.read()
  121. FrozenImporter.find_spec = find_spec
  122. FrozenImporter.get_data = get_data
  123. # Set the TCL_LIBRARY directory to the location of the Tcl/Tk/Tix files.
  124. import os
  125. tcl_dir = os.path.join(os.path.dirname(sys.executable), 'tcl')
  126. if os.path.isdir(tcl_dir):
  127. for dir in os.listdir(tcl_dir):
  128. sub_dir = os.path.join(tcl_dir, dir)
  129. if os.path.isdir(sub_dir):
  130. if dir.startswith('tcl'):
  131. os.environ['TCL_LIBRARY'] = sub_dir
  132. if dir.startswith('tk'):
  133. os.environ['TK_LIBRARY'] = sub_dir
  134. if dir.startswith('tix'):
  135. os.environ['TIX_LIBRARY'] = sub_dir
  136. del os
  137. """
  138. class build_apps(setuptools.Command):
  139. description = 'build Panda3D applications'
  140. user_options = [
  141. ('build-base=', None, 'directory to build applications in'),
  142. ('requirements-path=', None, 'path to requirements.txt file for pip'),
  143. ('platforms=', 'p', 'a list of platforms to build for'),
  144. ]
  145. default_file_handlers = {
  146. '.egg': egg2bam,
  147. }
  148. def initialize_options(self):
  149. self.build_base = os.path.join(os.getcwd(), 'build')
  150. self.gui_apps = {}
  151. self.console_apps = {}
  152. self.macos_main_app = None
  153. self.rename_paths = {}
  154. self.include_patterns = []
  155. self.exclude_patterns = []
  156. self.include_modules = {}
  157. self.exclude_modules = {}
  158. self.icons = {}
  159. self.platforms = [
  160. 'manylinux1_x86_64',
  161. 'macosx_10_9_x86_64',
  162. 'win_amd64',
  163. ]
  164. self.plugins = []
  165. self.embed_prc_data = True
  166. self.extra_prc_files = []
  167. self.extra_prc_data = ''
  168. self.default_prc_dir = None
  169. self.log_filename = None
  170. self.log_append = False
  171. self.requirements_path = os.path.join(os.getcwd(), 'requirements.txt')
  172. self.use_optimized_wheels = True
  173. self.optimized_wheel_index = ''
  174. self.pypi_extra_indexes = [
  175. 'https://archive.panda3d.org/thirdparty',
  176. ]
  177. self.file_handlers = {}
  178. self.exclude_dependencies = [
  179. # Windows
  180. 'kernel32.dll', 'user32.dll', 'wsock32.dll', 'ws2_32.dll',
  181. 'advapi32.dll', 'opengl32.dll', 'glu32.dll', 'gdi32.dll',
  182. 'shell32.dll', 'ntdll.dll', 'ws2help.dll', 'rpcrt4.dll',
  183. 'imm32.dll', 'ddraw.dll', 'shlwapi.dll', 'secur32.dll',
  184. 'dciman32.dll', 'comdlg32.dll', 'comctl32.dll', 'ole32.dll',
  185. 'oleaut32.dll', 'gdiplus.dll', 'winmm.dll', 'iphlpapi.dll',
  186. 'msvcrt.dll', 'kernelbase.dll', 'msimg32.dll', 'msacm32.dll',
  187. 'setupapi.dll', 'version.dll',
  188. # manylinux1/linux
  189. 'libdl.so.*', 'libstdc++.so.*', 'libm.so.*', 'libgcc_s.so.*',
  190. 'libpthread.so.*', 'libc.so.*', 'ld-linux-x86-64.so.*',
  191. 'libgl.so.*', 'libx11.so.*', 'libreadline.so.*', 'libncursesw.so.*',
  192. 'libbz2.so.*', 'libz.so.*', 'liblzma.so.*', 'librt.so.*', 'libutil.so.*',
  193. # macOS
  194. '/usr/lib/libc++.1.dylib',
  195. '/usr/lib/libstdc++.*.dylib',
  196. '/usr/lib/libz.*.dylib',
  197. '/usr/lib/libobjc.*.dylib',
  198. '/usr/lib/libSystem.*.dylib',
  199. '/usr/lib/libbz2.*.dylib',
  200. '/usr/lib/libedit.*.dylib',
  201. '/System/Library/**',
  202. ]
  203. if sys.version_info >= (3, 5):
  204. # Python 3.5+ requires at least Windows Vista to run anyway, so we
  205. # shouldn't warn about DLLs that are shipped with Vista.
  206. self.exclude_dependencies += ['bcrypt.dll']
  207. self.package_data_dirs = {}
  208. # We keep track of the zip files we've opened.
  209. self._zip_files = {}
  210. def _get_zip_file(self, path):
  211. if path in self._zip_files:
  212. return self._zip_files[path]
  213. zip = zipfile.ZipFile(path)
  214. self._zip_files[path] = zip
  215. return zip
  216. def finalize_options(self):
  217. # We need to massage the inputs a bit in case they came from a
  218. # setup.cfg file.
  219. self.gui_apps = _parse_dict(self.gui_apps)
  220. self.console_apps = _parse_dict(self.console_apps)
  221. self.rename_paths = _parse_dict(self.rename_paths)
  222. self.include_patterns = _parse_list(self.include_patterns)
  223. self.exclude_patterns = _parse_list(self.exclude_patterns)
  224. self.include_modules = {
  225. key: _parse_list(value)
  226. for key, value in _parse_dict(self.include_modules).items()
  227. }
  228. self.exclude_modules = {
  229. key: _parse_list(value)
  230. for key, value in _parse_dict(self.exclude_modules).items()
  231. }
  232. self.icons = _parse_dict(self.icons)
  233. self.platforms = _parse_list(self.platforms)
  234. self.plugins = _parse_list(self.plugins)
  235. self.extra_prc_files = _parse_list(self.extra_prc_files)
  236. if self.default_prc_dir is None:
  237. self.default_prc_dir = '<auto>etc' if not self.embed_prc_data else ''
  238. num_gui_apps = len(self.gui_apps)
  239. num_console_apps = len(self.console_apps)
  240. if not self.macos_main_app:
  241. if num_gui_apps > 1:
  242. assert False, 'macos_main_app must be defined if more than one gui_app is defined'
  243. elif num_gui_apps == 1:
  244. self.macos_main_app = list(self.gui_apps.keys())[0]
  245. use_pipenv = (
  246. 'Pipfile' in os.path.basename(self.requirements_path) or
  247. not os.path.exists(self.requirements_path) and os.path.exists('Pipfile')
  248. )
  249. if use_pipenv:
  250. reqspath = os.path.join(self.build_base, 'requirements.txt')
  251. with open(reqspath, 'w') as reqsfile:
  252. subprocess.check_call(['pipenv', 'lock', '--requirements'], stdout=reqsfile)
  253. self.requirements_path = reqspath
  254. if self.use_optimized_wheels:
  255. if not self.optimized_wheel_index:
  256. # Try to find an appropriate wheel index
  257. # Start with the release index
  258. self.optimized_wheel_index = 'https://archive.panda3d.org/simple/opt'
  259. # See if a buildbot build is being used
  260. with open(self.requirements_path) as reqsfile:
  261. reqsdata = reqsfile.read()
  262. matches = re.search(r'--extra-index-url (https*://archive.panda3d.org/.*\b)', reqsdata)
  263. if matches and matches.group(1):
  264. self.optimized_wheel_index = matches.group(1)
  265. if not matches.group(1).endswith('opt'):
  266. self.optimized_wheel_index += '/opt'
  267. assert self.optimized_wheel_index, 'An index for optimized wheels must be defined if use_optimized_wheels is set'
  268. assert os.path.exists(self.requirements_path), 'Requirements.txt path does not exist: {}'.format(self.requirements_path)
  269. assert num_gui_apps + num_console_apps != 0, 'Must specify at least one app in either gui_apps or console_apps'
  270. self.exclude_dependencies = [p3d.GlobPattern(i) for i in self.exclude_dependencies]
  271. for glob in self.exclude_dependencies:
  272. glob.case_sensitive = False
  273. tmp = self.default_file_handlers.copy()
  274. tmp.update(self.file_handlers)
  275. self.file_handlers = tmp
  276. tmp = PACKAGE_DATA_DIRS.copy()
  277. tmp.update(self.package_data_dirs)
  278. self.package_data_dirs = tmp
  279. self.icon_objects = {}
  280. for app, iconpaths in self.icons.items():
  281. if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple):
  282. iconpaths = (iconpaths,)
  283. iconobj = Icon()
  284. for iconpath in iconpaths:
  285. iconobj.addImage(iconpath)
  286. iconobj.generateMissingImages()
  287. self.icon_objects[app] = iconobj
  288. def run(self):
  289. self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO)
  290. for platform in self.platforms:
  291. self.build_runtimes(platform, True)
  292. def download_wheels(self, platform):
  293. """ Downloads wheels for the given platform using pip. This includes panda3d
  294. wheels. These are special wheels that are expected to contain a deploy_libs
  295. directory containing the Python runtime libraries, which will be added
  296. to sys.path."""
  297. import pip
  298. self.announce('Gathering wheels for platform: {}'.format(platform), distutils.log.INFO)
  299. whlcache = os.path.join(self.build_base, '__whl_cache__')
  300. pip_version = int(pip.__version__.split('.')[0])
  301. if pip_version < 9:
  302. raise RuntimeError("pip 9.0 or greater is required, but found {}".format(pip.__version__))
  303. abi_tag = 'cp%d%d' % (sys.version_info[:2])
  304. if sys.version_info < (3, 8):
  305. abi_tag += 'm'
  306. whldir = os.path.join(whlcache, '_'.join((platform, abi_tag)))
  307. os.makedirs(whldir, exist_ok=True)
  308. # Remove any .zip files. These are built from a VCS and block for an
  309. # interactive prompt on subsequent downloads.
  310. if os.path.exists(whldir):
  311. for whl in os.listdir(whldir):
  312. if whl.endswith('.zip'):
  313. os.remove(os.path.join(whldir, whl))
  314. pip_args = [
  315. '--disable-pip-version-check',
  316. 'download',
  317. '-d', whldir,
  318. '-r', self.requirements_path,
  319. '--only-binary', ':all:',
  320. '--platform', platform,
  321. '--abi', abi_tag,
  322. ]
  323. if self.use_optimized_wheels:
  324. pip_args += [
  325. '--extra-index-url', self.optimized_wheel_index
  326. ]
  327. for index in self.pypi_extra_indexes:
  328. pip_args += ['--extra-index-url', index]
  329. subprocess.check_call([sys.executable, '-m', 'pip'] + pip_args)
  330. # Return a list of paths to the downloaded whls
  331. return [
  332. os.path.join(whldir, filename)
  333. for filename in os.listdir(whldir)
  334. if filename.endswith('.whl')
  335. ]
  336. def update_pe_resources(self, appname, runtime):
  337. """Update resources (e.g., icons) in windows PE file"""
  338. icon = self.icon_objects.get(
  339. appname,
  340. self.icon_objects.get('*', None),
  341. )
  342. if icon is not None:
  343. pef = pefile.PEFile()
  344. pef.open(runtime, 'r+')
  345. pef.add_icon(icon)
  346. pef.add_resource_section()
  347. pef.write_changes()
  348. pef.close()
  349. def bundle_macos_app(self, builddir):
  350. """Bundle built runtime into a .app for macOS"""
  351. appname = '{}.app'.format(self.macos_main_app)
  352. appdir = os.path.join(builddir, appname)
  353. contentsdir = os.path.join(appdir, 'Contents')
  354. macosdir = os.path.join(contentsdir, 'MacOS')
  355. fwdir = os.path.join(contentsdir, 'Frameworks')
  356. resdir = os.path.join(contentsdir, 'Resources')
  357. self.announce('Bundling macOS app into {}'.format(appdir), distutils.log.INFO)
  358. # Create initial directory structure
  359. os.makedirs(macosdir)
  360. os.makedirs(fwdir)
  361. os.makedirs(resdir)
  362. # Move files over
  363. for fname in os.listdir(builddir):
  364. src = os.path.join(builddir, fname)
  365. if appdir in src:
  366. continue
  367. if fname in self.gui_apps or self.console_apps:
  368. dst = macosdir
  369. elif os.path.isfile(src) and open(src, 'rb').read(4) in macosx_binary_magics:
  370. dst = fwdir
  371. else:
  372. dst = resdir
  373. shutil.move(src, dst)
  374. # Write out Info.plist
  375. plist = {
  376. 'CFBundleName': appname,
  377. 'CFBundleDisplayName': appname, #TODO use name from setup.py/cfg
  378. 'CFBundleIdentifier': '', #TODO
  379. 'CFBundleVersion': '0.0.0', #TODO get from setup.py
  380. 'CFBundlePackageType': 'APPL',
  381. 'CFBundleSignature': '', #TODO
  382. 'CFBundleExecutable': self.macos_main_app,
  383. }
  384. icon = self.icon_objects.get(
  385. self.macos_main_app,
  386. self.icon_objects.get('*', None)
  387. )
  388. if icon is not None:
  389. plist['CFBundleIconFile'] = 'iconfile'
  390. icon.makeICNS(os.path.join(resdir, 'iconfile.icns'))
  391. with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
  392. if hasattr(plistlib, 'dump'):
  393. plistlib.dump(plist, f)
  394. else:
  395. plistlib.writePlist(plist, f)
  396. def build_runtimes(self, platform, use_wheels):
  397. """ Builds the distributions for the given platform. """
  398. builddir = os.path.join(self.build_base, platform)
  399. if os.path.exists(builddir):
  400. shutil.rmtree(builddir)
  401. os.makedirs(builddir)
  402. path = sys.path[:]
  403. p3dwhl = None
  404. if use_wheels:
  405. wheelpaths = self.download_wheels(platform)
  406. for whl in wheelpaths:
  407. if os.path.basename(whl).startswith('panda3d-'):
  408. p3dwhlfn = whl
  409. p3dwhl = self._get_zip_file(p3dwhlfn)
  410. break
  411. else:
  412. raise RuntimeError("Missing panda3d wheel for platform: {}".format(platform))
  413. if self.use_optimized_wheels:
  414. # Check to see if we have an optimized wheel
  415. localtag = p3dwhlfn.split('+')[1].split('-')[0] if '+' in p3dwhlfn else ''
  416. if not localtag.endswith('opt'):
  417. self.announce(
  418. 'Could not find an optimized wheel (using index {}) for platform: {}'.format(self.optimized_wheel_index, platform),
  419. distutils.log.WARN
  420. )
  421. #whlfiles = {whl: self._get_zip_file(whl) for whl in wheelpaths}
  422. # Add whl files to the path so they are picked up by modulefinder
  423. for whl in wheelpaths:
  424. path.insert(0, whl)
  425. # Add deploy_libs from panda3d whl to the path
  426. path.insert(0, os.path.join(p3dwhlfn, 'deploy_libs'))
  427. self.announce('Building runtime for platform: {}'.format(platform), distutils.log.INFO)
  428. # Gather PRC data
  429. prcstring = ''
  430. if not use_wheels:
  431. dtool_fn = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name())
  432. libdir = os.path.dirname(dtool_fn.to_os_specific())
  433. etcdir = os.path.join(libdir, '..', 'etc')
  434. etcfiles = os.listdir(etcdir)
  435. etcfiles.sort(reverse=True)
  436. for fn in etcfiles:
  437. if fn.lower().endswith('.prc'):
  438. with open(os.path.join(etcdir, fn)) as f:
  439. prcstring += f.read()
  440. else:
  441. etcfiles = [i for i in p3dwhl.namelist() if i.endswith('.prc')]
  442. etcfiles.sort(reverse=True)
  443. for fn in etcfiles:
  444. with p3dwhl.open(fn) as f:
  445. prcstring += f.read().decode('utf8')
  446. user_prcstring = self.extra_prc_data
  447. for fn in self.extra_prc_files:
  448. with open(fn) as f:
  449. user_prcstring += f.read()
  450. # Clenup PRC data
  451. check_plugins = [
  452. #TODO find a better way to get this list
  453. 'pandaegg',
  454. 'p3ffmpeg',
  455. 'p3ptloader',
  456. 'p3assimp',
  457. ]
  458. def parse_prc(prcstr, warn_on_missing_plugin):
  459. out = []
  460. for ln in prcstr.split('\n'):
  461. ln = ln.strip()
  462. useline = True
  463. if ln.startswith('#') or not ln:
  464. continue
  465. words = ln.split(None, 1)
  466. if not words:
  467. continue
  468. var = words[0]
  469. value = words[1] if len(words) > 1 else ''
  470. # Strip comment after value.
  471. c = value.find(' #')
  472. if c > 0:
  473. value = value[:c].rstrip()
  474. if var == 'model-cache-dir' and value:
  475. value = value.replace('/panda3d', '/{}'.format(self.distribution.get_name()))
  476. if var == 'audio-library-name':
  477. # We have the default set to p3fmod_audio on macOS in 1.10,
  478. # but this can be unexpected as other platforms use OpenAL
  479. # by default. Switch it up if FMOD is not included.
  480. if value not in self.plugins and value == 'p3fmod_audio' and 'p3openal_audio' in self.plugins:
  481. self.warn("Missing audio plugin p3fmod_audio referenced in PRC data, replacing with p3openal_audio")
  482. for plugin in check_plugins:
  483. if plugin in value and plugin not in self.plugins:
  484. useline = False
  485. if warn_on_missing_plugin:
  486. self.warn(
  487. "Missing plugin ({0}) referenced in user PRC data".format(plugin)
  488. )
  489. break
  490. if useline:
  491. if value:
  492. out.append(var + ' ' + value)
  493. else:
  494. out.append(var)
  495. return out
  496. prcexport = parse_prc(prcstring, 0) + parse_prc(user_prcstring, 1)
  497. # Export PRC data
  498. prcexport = '\n'.join(prcexport)
  499. if not self.embed_prc_data:
  500. prcdir = self.default_prc_dir.replace('<auto>', '')
  501. prcdir = os.path.join(builddir, prcdir)
  502. os.makedirs(prcdir)
  503. with open (os.path.join(prcdir, '00-panda3d.prc'), 'w') as f:
  504. f.write(prcexport)
  505. # Create runtimes
  506. freezer_extras = set()
  507. freezer_modules = set()
  508. freezer_modpaths = set()
  509. ext_suffixes = set()
  510. def get_search_path_for(source_path):
  511. search_path = [os.path.dirname(source_path)]
  512. if use_wheels:
  513. search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
  514. # If the .whl containing this file has a .libs directory, add
  515. # it to the path. This is an auditwheel/numpy convention.
  516. if '.whl' + os.sep in source_path:
  517. whl, wf = source_path.split('.whl' + os.path.sep)
  518. whl += '.whl'
  519. rootdir = wf.split(os.path.sep, 1)[0]
  520. search_path.append(os.path.join(whl, rootdir, '.libs'))
  521. # Also look for eg. numpy.libs or Pillow.libs in the root
  522. whl_name = os.path.basename(whl).split('-', 1)[0]
  523. search_path.append(os.path.join(whl, whl_name + '.libs'))
  524. # Also look for more specific per-package cases, defined in
  525. # PACKAGE_LIB_DIRS at the top of this file.
  526. extra_dirs = PACKAGE_LIB_DIRS.get(whl_name, [])
  527. for extra_dir in extra_dirs:
  528. search_path.append(os.path.join(whl, extra_dir.replace('/', os.path.sep)))
  529. return search_path
  530. def create_runtime(appname, mainscript, use_console):
  531. freezer = FreezeTool.Freezer(platform=platform, path=path)
  532. freezer.addModule('__main__', filename=mainscript)
  533. freezer.addModule('site', filename='site.py', text=SITE_PY)
  534. for incmod in self.include_modules.get(appname, []) + self.include_modules.get('*', []):
  535. freezer.addModule(incmod)
  536. for exmod in self.exclude_modules.get(appname, []) + self.exclude_modules.get('*', []):
  537. freezer.excludeModule(exmod)
  538. freezer.done(addStartupModules=True)
  539. target_path = os.path.join(builddir, appname)
  540. stub_name = 'deploy-stub'
  541. if platform.startswith('win') or 'macosx' in platform:
  542. if not use_console:
  543. stub_name = 'deploy-stubw'
  544. if platform.startswith('win'):
  545. stub_name += '.exe'
  546. target_path += '.exe'
  547. if use_wheels:
  548. stub_file = p3dwhl.open('panda3d_tools/{0}'.format(stub_name))
  549. else:
  550. dtool_path = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name()).to_os_specific()
  551. stub_path = os.path.join(os.path.dirname(dtool_path), '..', 'bin', stub_name)
  552. stub_file = open(stub_path, 'rb')
  553. # Do we need an icon? On Windows, we need to add this to the stub
  554. # before we add the blob.
  555. if 'win' in platform:
  556. temp_file = tempfile.NamedTemporaryFile(suffix='-icon.exe', delete=False)
  557. temp_file.write(stub_file.read())
  558. stub_file.close()
  559. temp_file.close()
  560. self.update_pe_resources(appname, temp_file.name)
  561. stub_file = open(temp_file.name, 'rb')
  562. else:
  563. temp_file = None
  564. freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
  565. 'prc_data': prcexport if self.embed_prc_data else None,
  566. 'default_prc_dir': self.default_prc_dir,
  567. 'prc_dir_envvars': None,
  568. 'prc_path_envvars': None,
  569. 'prc_patterns': None,
  570. 'prc_encrypted_patterns': None,
  571. 'prc_encryption_key': None,
  572. 'prc_executable_patterns': None,
  573. 'prc_executable_args_envvar': None,
  574. 'main_dir': None,
  575. 'log_filename': self.expand_path(self.log_filename, platform),
  576. }, self.log_append)
  577. stub_file.close()
  578. if temp_file:
  579. os.unlink(temp_file.name)
  580. # Copy the dependencies.
  581. search_path = [builddir]
  582. if use_wheels:
  583. search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
  584. self.copy_dependencies(target_path, builddir, search_path, stub_name)
  585. freezer_extras.update(freezer.extras)
  586. freezer_modules.update(freezer.getAllModuleNames())
  587. freezer_modpaths.update({
  588. mod[1].filename.to_os_specific()
  589. for mod in freezer.getModuleDefs() if mod[1].filename
  590. })
  591. for suffix in freezer.moduleSuffixes:
  592. if suffix[2] == imp.C_EXTENSION:
  593. ext_suffixes.add(suffix[0])
  594. for appname, scriptname in self.gui_apps.items():
  595. create_runtime(appname, scriptname, False)
  596. for appname, scriptname in self.console_apps.items():
  597. create_runtime(appname, scriptname, True)
  598. # Copy extension modules
  599. whl_modules = []
  600. whl_modules_ext = ''
  601. if use_wheels:
  602. # Get the module libs
  603. whl_modules = []
  604. for i in p3dwhl.namelist():
  605. if not i.startswith('deploy_libs/'):
  606. continue
  607. if not any(i.endswith(suffix) for suffix in ext_suffixes):
  608. continue
  609. base = os.path.basename(i)
  610. module, _, ext = base.partition('.')
  611. whl_modules.append(module)
  612. whl_modules_ext = ext
  613. # Make sure to copy any builtins that have shared objects in the
  614. # deploy libs, assuming they are not already in freezer_extras.
  615. for mod, source_path in freezer_extras:
  616. freezer_modules.discard(mod)
  617. for mod in freezer_modules:
  618. if mod in whl_modules:
  619. freezer_extras.add((mod, None))
  620. # Copy over necessary plugins
  621. plugin_list = ['panda3d/lib{}'.format(i) for i in self.plugins]
  622. for lib in p3dwhl.namelist():
  623. plugname = lib.split('.', 1)[0]
  624. if plugname in plugin_list:
  625. source_path = os.path.join(p3dwhlfn, lib)
  626. target_path = os.path.join(builddir, os.path.basename(lib))
  627. search_path = [os.path.dirname(source_path)]
  628. self.copy_with_dependencies(source_path, target_path, search_path)
  629. # Copy any shared objects we need
  630. for module, source_path in freezer_extras:
  631. if source_path is not None:
  632. # Rename panda3d/core.pyd to panda3d.core.pyd
  633. basename = os.path.basename(source_path)
  634. if '.' in module:
  635. basename = module.rsplit('.', 1)[0] + '.' + basename
  636. # Remove python version string
  637. parts = basename.split('.')
  638. if len(parts) >= 3 and '-' in parts[-2]:
  639. parts = parts[:-2] + parts[-1:]
  640. basename = '.'.join(parts)
  641. else:
  642. # Builtin module, but might not be builtin in wheel libs, so double check
  643. if module in whl_modules:
  644. source_path = os.path.join(p3dwhlfn, 'deploy_libs/{}.{}'.format(module, whl_modules_ext))#'{0}/deploy_libs/{1}.{2}'.format(p3dwhlfn, module, whl_modules_ext)
  645. basename = os.path.basename(source_path)
  646. #XXX should we remove python version string here too?
  647. else:
  648. continue
  649. # If this is a dynamic library, search for dependencies.
  650. target_path = os.path.join(builddir, basename)
  651. search_path = get_search_path_for(source_path)
  652. self.copy_with_dependencies(source_path, target_path, search_path)
  653. # Copy over the tcl directory.
  654. #TODO: get this to work on non-Windows platforms.
  655. if sys.platform == "win32" and platform.startswith('win'):
  656. tcl_dir = os.path.join(sys.prefix, 'tcl')
  657. if os.path.isdir(tcl_dir) and 'tkinter' in freezer_modules:
  658. self.announce('Copying Tcl files', distutils.log.INFO)
  659. os.makedirs(os.path.join(builddir, 'tcl'))
  660. for dir in os.listdir(tcl_dir):
  661. sub_dir = os.path.join(tcl_dir, dir)
  662. if os.path.isdir(sub_dir):
  663. target_dir = os.path.join(builddir, 'tcl', dir)
  664. self.announce('copying {0} -> {1}'.format(sub_dir, target_dir))
  665. shutil.copytree(sub_dir, target_dir)
  666. # Extract any other data files from dependency packages.
  667. for module, datadesc in self.package_data_dirs.items():
  668. if module not in freezer_modules:
  669. continue
  670. self.announce('Copying data files for module: {}'.format(module), distutils.log.INFO)
  671. # OK, find out in which .whl this occurs.
  672. for whl in wheelpaths:
  673. whlfile = self._get_zip_file(whl)
  674. filenames = whlfile.namelist()
  675. for source_pattern, target_dir, flags in datadesc:
  676. srcglob = p3d.GlobPattern(source_pattern.lower())
  677. source_dir = os.path.dirname(source_pattern)
  678. # Relocate the target dir to the build directory.
  679. target_dir = target_dir.replace('/', os.sep)
  680. target_dir = os.path.join(builddir, target_dir)
  681. for wf in filenames:
  682. if wf.lower().startswith(source_dir.lower() + '/'):
  683. if not srcglob.matches(wf.lower()):
  684. continue
  685. wf = wf.replace('/', os.sep)
  686. relpath = wf[len(source_dir) + 1:]
  687. source_path = os.path.join(whl, wf)
  688. target_path = os.path.join(target_dir, relpath)
  689. if 'PKG_DATA_MAKE_EXECUTABLE' in flags:
  690. search_path = get_search_path_for(source_path)
  691. self.copy_with_dependencies(source_path, target_path, search_path)
  692. mode = os.stat(target_path).st_mode
  693. mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
  694. os.chmod(target_path, mode)
  695. else:
  696. self.copy(source_path, target_path)
  697. # Copy Game Files
  698. self.announce('Copying game files for platform: {}'.format(platform), distutils.log.INFO)
  699. ignore_copy_list = [
  700. '**/__pycache__/**',
  701. '**/*.pyc',
  702. '{}/**'.format(self.build_base),
  703. ]
  704. ignore_copy_list += self.exclude_patterns
  705. ignore_copy_list += freezer_modpaths
  706. ignore_copy_list += self.extra_prc_files
  707. ignore_copy_list = [p3d.GlobPattern(p3d.Filename.from_os_specific(i).get_fullpath()) for i in ignore_copy_list]
  708. include_copy_list = [p3d.GlobPattern(i) for i in self.include_patterns]
  709. def check_pattern(src, pattern_list):
  710. # Normalize file paths across platforms
  711. fn = p3d.Filename.from_os_specific(os.path.normpath(src))
  712. path = fn.get_fullpath()
  713. fn.make_absolute()
  714. abspath = fn.get_fullpath()
  715. for pattern in pattern_list:
  716. # If the pattern is absolute, match against the absolute filename.
  717. if pattern.pattern[0] == '/':
  718. #print('check ignore: {} {} {}'.format(pattern, src, pattern.matches_file(abspath)))
  719. if pattern.matches_file(abspath):
  720. return True
  721. else:
  722. #print('check ignore: {} {} {}'.format(pattern, src, pattern.matches_file(path)))
  723. if pattern.matches_file(path):
  724. return True
  725. return False
  726. def check_file(fname):
  727. return check_pattern(fname, include_copy_list) and \
  728. not check_pattern(fname, ignore_copy_list)
  729. def copy_file(src, dst):
  730. src = os.path.normpath(src)
  731. dst = os.path.normpath(dst)
  732. if not check_file(src):
  733. self.announce('skipping file {}'.format(src))
  734. return
  735. dst_dir = os.path.dirname(dst)
  736. if not os.path.exists(dst_dir):
  737. os.makedirs(dst_dir)
  738. ext = os.path.splitext(src)[1]
  739. # If the file ends with .gz/.pz, we strip this off.
  740. if ext in ('.gz', '.pz'):
  741. ext = os.path.splitext(src[:-3])[1]
  742. if not ext:
  743. ext = os.path.basename(src)
  744. if ext in self.file_handlers:
  745. buildscript = self.file_handlers[ext]
  746. self.announce('running {} on src ({})'.format(buildscript.__name__, src))
  747. try:
  748. dst = self.file_handlers[ext](self, src, dst)
  749. except Exception as err:
  750. self.announce('{}'.format(err), distutils.log.ERROR)
  751. else:
  752. self.announce('copying {0} -> {1}'.format(src, dst))
  753. shutil.copyfile(src, dst)
  754. def update_path(path):
  755. normpath = p3d.Filename.from_os_specific(os.path.normpath(src)).c_str()
  756. for inputpath, outputpath in self.rename_paths.items():
  757. if normpath.startswith(inputpath):
  758. normpath = normpath.replace(inputpath, outputpath, 1)
  759. return p3d.Filename(normpath).to_os_specific()
  760. rootdir = os.getcwd()
  761. for dirname, subdirlist, filelist in os.walk(rootdir):
  762. dirpath = os.path.relpath(dirname, rootdir)
  763. for fname in filelist:
  764. src = os.path.join(dirpath, fname)
  765. dst = os.path.join(builddir, update_path(src))
  766. copy_file(src, dst)
  767. # Bundle into an .app on macOS
  768. if self.macos_main_app and 'macosx' in platform:
  769. self.bundle_macos_app(builddir)
  770. def add_dependency(self, name, target_dir, search_path, referenced_by):
  771. """ Searches for the given DLL on the search path. If it exists,
  772. copies it to the target_dir. """
  773. if os.path.exists(os.path.join(target_dir, name)):
  774. # We've already added it earlier.
  775. return
  776. for dep in self.exclude_dependencies:
  777. if dep.matches_file(name):
  778. return
  779. for dir in search_path:
  780. source_path = os.path.join(dir, name)
  781. if os.path.isfile(source_path):
  782. target_path = os.path.join(target_dir, name)
  783. self.copy_with_dependencies(source_path, target_path, search_path)
  784. return
  785. elif '.whl' in source_path:
  786. # Check whether the file exists inside the wheel.
  787. whl, wf = source_path.split('.whl' + os.path.sep)
  788. whl += '.whl'
  789. whlfile = self._get_zip_file(whl)
  790. # Normalize the path separator
  791. wf = os.path.normpath(wf).replace(os.path.sep, '/')
  792. # Look case-insensitively.
  793. namelist = whlfile.namelist()
  794. namelist_lower = [file.lower() for file in namelist]
  795. if wf.lower() in namelist_lower:
  796. # We have a match. Change it to the correct case.
  797. wf = namelist[namelist_lower.index(wf.lower())]
  798. source_path = os.path.join(whl, wf)
  799. target_path = os.path.join(target_dir, os.path.basename(wf))
  800. self.copy_with_dependencies(source_path, target_path, search_path)
  801. return
  802. # If we didn't find it, look again, but case-insensitively.
  803. name_lower = name.lower()
  804. for dir in search_path:
  805. if os.path.isdir(dir):
  806. files = os.listdir(dir)
  807. files_lower = [file.lower() for file in files]
  808. if name_lower in files_lower:
  809. name = files[files_lower.index(name_lower)]
  810. source_path = os.path.join(dir, name)
  811. target_path = os.path.join(target_dir, name)
  812. self.copy_with_dependencies(source_path, target_path, search_path)
  813. # Warn if we can't find it, but only once.
  814. self.warn("could not find dependency {0} (referenced by {1})".format(name, referenced_by))
  815. self.exclude_dependencies.append(p3d.GlobPattern(name.lower()))
  816. def copy(self, source_path, target_path):
  817. """ Copies source_path to target_path.
  818. source_path may be located inside a .whl file. """
  819. try:
  820. self.announce('copying {0} -> {1}'.format(os.path.relpath(source_path, self.build_base), os.path.relpath(target_path, self.build_base)))
  821. except ValueError:
  822. # No relative path (e.g., files on different drives in Windows), just print absolute paths instead
  823. self.announce('copying {0} -> {1}'.format(source_path, target_path))
  824. # Make the directory if it does not yet exist.
  825. target_dir = os.path.dirname(target_path)
  826. if not os.path.isdir(target_dir):
  827. os.makedirs(target_dir)
  828. # Copy the file, and open it for analysis.
  829. if '.whl' in source_path:
  830. # This was found in a wheel, extract it
  831. whl, wf = source_path.split('.whl' + os.path.sep)
  832. whl += '.whl'
  833. whlfile = self._get_zip_file(whl)
  834. data = whlfile.read(wf.replace(os.path.sep, '/'))
  835. with open(target_path, 'wb') as f:
  836. f.write(data)
  837. else:
  838. # Regular file, copy it
  839. shutil.copyfile(source_path, target_path)
  840. def copy_with_dependencies(self, source_path, target_path, search_path):
  841. """ Copies source_path to target_path. It also scans source_path for
  842. any dependencies, which are located along the given search_path and
  843. copied to the same directory as target_path.
  844. source_path may be located inside a .whl file. """
  845. self.copy(source_path, target_path)
  846. source_dir = os.path.dirname(source_path)
  847. target_dir = os.path.dirname(target_path)
  848. base = os.path.basename(target_path)
  849. self.copy_dependencies(target_path, target_dir, search_path + [source_dir], base)
  850. def copy_dependencies(self, target_path, target_dir, search_path, referenced_by):
  851. """ Copies the dependencies of target_path into target_dir. """
  852. fp = open(target_path, 'rb+')
  853. # What kind of magic does the file contain?
  854. deps = []
  855. magic = fp.read(4)
  856. if magic.startswith(b'MZ'):
  857. # It's a Windows DLL or EXE file.
  858. pe = pefile.PEFile()
  859. pe.read(fp)
  860. for lib in pe.imports:
  861. if not lib.lower().startswith('api-ms-win-'):
  862. deps.append(lib)
  863. elif magic == b'\x7FELF':
  864. # Elf magic. Used on (among others) Linux and FreeBSD.
  865. deps = self._read_dependencies_elf(fp, target_dir, search_path)
  866. elif magic in (b'\xCE\xFA\xED\xFE', b'\xCF\xFA\xED\xFE'):
  867. # A Mach-O file, as used on macOS.
  868. deps = self._read_dependencies_macho(fp, '<', flatten=True)
  869. elif magic in (b'\xFE\xED\xFA\xCE', b'\xFE\xED\xFA\xCF'):
  870. rel_dir = os.path.relpath(target_dir, os.path.dirname(target_path))
  871. deps = self._read_dependencies_macho(fp, '>', flatten=True)
  872. elif magic in (b'\xCA\xFE\xBA\xBE', b'\xBE\xBA\xFE\xCA'):
  873. # A fat file, containing multiple Mach-O binaries. In the future,
  874. # we may want to extract the one containing the architecture we
  875. # are building for.
  876. deps = self._read_dependencies_fat(fp, False, flatten=True)
  877. elif magic in (b'\xCA\xFE\xBA\xBF', b'\xBF\xBA\xFE\xCA'):
  878. # A 64-bit fat file.
  879. deps = self._read_dependencies_fat(fp, True, flatten=True)
  880. # If we discovered any dependencies, recursively add those.
  881. for dep in deps:
  882. self.add_dependency(dep, target_dir, search_path, referenced_by)
  883. def _read_dependencies_elf(self, elf, origin, search_path):
  884. """ Having read the first 4 bytes of the ELF file, fetches the
  885. dependent libraries and returns those as a list. """
  886. ident = elf.read(12)
  887. # Make sure we read in the correct endianness and integer size
  888. byte_order = "<>"[ord(ident[1:2]) - 1]
  889. elf_class = ord(ident[0:1]) - 1 # 0 = 32-bits, 1 = 64-bits
  890. header_struct = byte_order + ("HHIIIIIHHHHHH", "HHIQQQIHHHHHH")[elf_class]
  891. section_struct = byte_order + ("4xI8xIII8xI", "4xI16xQQI12xQ")[elf_class]
  892. dynamic_struct = byte_order + ("iI", "qQ")[elf_class]
  893. type, machine, version, entry, phoff, shoff, flags, ehsize, phentsize, phnum, shentsize, shnum, shstrndx \
  894. = struct.unpack(header_struct, elf.read(struct.calcsize(header_struct)))
  895. dynamic_sections = []
  896. string_tables = {}
  897. # Seek to the section header table and find the .dynamic section.
  898. elf.seek(shoff)
  899. for i in range(shnum):
  900. type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
  901. if type == 6 and link != 0: # DYNAMIC type, links to string table
  902. dynamic_sections.append((offset, size, link, entsize))
  903. string_tables[link] = None
  904. # Read the relevant string tables.
  905. for idx in string_tables.keys():
  906. elf.seek(shoff + idx * shentsize)
  907. type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
  908. if type != 3: continue
  909. elf.seek(offset)
  910. string_tables[idx] = elf.read(size)
  911. # Loop through the dynamic sections and rewrite it if it has an rpath/runpath.
  912. needed = []
  913. rpath = []
  914. for offset, size, link, entsize in dynamic_sections:
  915. elf.seek(offset)
  916. data = elf.read(entsize)
  917. tag, val = struct.unpack_from(dynamic_struct, data)
  918. # Read tags until we find a NULL tag.
  919. while tag != 0:
  920. if tag == 1: # A NEEDED entry. Read it from the string table.
  921. string = string_tables[link][val : string_tables[link].find(b'\0', val)]
  922. needed.append(string.decode('utf-8'))
  923. elif tag == 15 or tag == 29:
  924. # An RPATH or RUNPATH entry.
  925. string = string_tables[link][val : string_tables[link].find(b'\0', val)]
  926. rpath += [
  927. os.path.normpath(i.decode('utf-8').replace('$ORIGIN', origin))
  928. for i in string.split(b':')
  929. ]
  930. data = elf.read(entsize)
  931. tag, val = struct.unpack_from(dynamic_struct, data)
  932. elf.close()
  933. search_path += rpath
  934. return needed
  935. def _read_dependencies_macho(self, fp, endian, flatten=False):
  936. """ Having read the first 4 bytes of the Mach-O file, fetches the
  937. dependent libraries and returns those as a list.
  938. If flatten is True, if the dependencies contain paths like
  939. @loader_path/../.dylibs/libsomething.dylib, it will rewrite them to
  940. instead contain @loader_path/libsomething.dylib if possible.
  941. This requires the file pointer to be opened in rb+ mode. """
  942. cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags = \
  943. struct.unpack(endian + 'IIIIII', fp.read(24))
  944. is_64bit = (cputype & 0x1000000) != 0
  945. if is_64bit:
  946. fp.read(4)
  947. # After the header, we get a series of linker commands. We just
  948. # iterate through them and gather up the LC_LOAD_DYLIB commands.
  949. load_dylibs = []
  950. for i in range(ncmds):
  951. cmd, cmd_size = struct.unpack(endian + 'II', fp.read(8))
  952. cmd_data = fp.read(cmd_size - 8)
  953. cmd &= ~0x80000000
  954. if cmd == 0x0c: # LC_LOAD_DYLIB
  955. dylib = cmd_data[16:].decode('ascii').split('\x00', 1)[0]
  956. orig = dylib
  957. if dylib.startswith('@loader_path/../Frameworks/'):
  958. dylib = dylib.replace('@loader_path/../Frameworks/', '')
  959. elif dylib.startswith('@executable_path/../Frameworks/'):
  960. dylib = dylib.replace('@executable_path/../Frameworks/', '')
  961. else:
  962. for prefix in ('@loader_path/', '@rpath/'):
  963. if dylib.startswith(prefix):
  964. dylib = dylib.replace(prefix, '')
  965. # Do we need to flatten the relative reference?
  966. if '/' in dylib and flatten:
  967. new_dylib = prefix + os.path.basename(dylib)
  968. str_size = len(cmd_data) - 16
  969. if len(new_dylib) < str_size:
  970. fp.seek(-str_size, os.SEEK_CUR)
  971. fp.write(new_dylib.encode('ascii').ljust(str_size, b'\0'))
  972. else:
  973. self.warn('Unable to rewrite dependency {}'.format(orig))
  974. load_dylibs.append(dylib)
  975. return load_dylibs
  976. def _read_dependencies_fat(self, fp, is_64bit, flatten=False):
  977. num_fat, = struct.unpack('>I', fp.read(4))
  978. # After the header we get a table of executables in this fat file,
  979. # each one with a corresponding offset into the file.
  980. offsets = []
  981. for i in range(num_fat):
  982. if is_64bit:
  983. cputype, cpusubtype, offset, size, align = \
  984. struct.unpack('>QQQQQ', fp.read(40))
  985. else:
  986. cputype, cpusubtype, offset, size, align = \
  987. struct.unpack('>IIIII', fp.read(20))
  988. offsets.append(offset)
  989. # Go through each of the binaries in the fat file.
  990. deps = []
  991. for offset in offsets:
  992. # Add 4, since it expects we've already read the magic.
  993. fp.seek(offset)
  994. magic = fp.read(4)
  995. if magic in (b'\xCE\xFA\xED\xFE', b'\xCF\xFA\xED\xFE'):
  996. endian = '<'
  997. elif magic in (b'\xFE\xED\xFA\xCE', b'\xFE\xED\xFA\xCF'):
  998. endian = '>'
  999. else:
  1000. # Not a Mach-O file we can read.
  1001. continue
  1002. for dep in self._read_dependencies_macho(fp, endian, flatten=flatten):
  1003. if dep not in deps:
  1004. deps.append(dep)
  1005. return deps
  1006. def expand_path(self, path, platform):
  1007. "Substitutes variables in the given path string."
  1008. if path is None:
  1009. return None
  1010. t = string.Template(path)
  1011. if platform.startswith('win'):
  1012. return t.substitute(HOME='~', USER_APPDATA='~/AppData/Local')
  1013. elif platform.startswith('macosx'):
  1014. return t.substitute(HOME='~', USER_APPDATA='~/Documents')
  1015. else:
  1016. return t.substitute(HOME='~', USER_APPDATA='~/.local/share')
  1017. class bdist_apps(setuptools.Command):
  1018. DEFAULT_INSTALLERS = {
  1019. 'manylinux1_x86_64': ['gztar'],
  1020. 'manylinux1_i686': ['gztar'],
  1021. # Everything else defaults to ['zip']
  1022. }
  1023. description = 'bundle built Panda3D applications into distributable forms'
  1024. user_options = build_apps.user_options + [
  1025. ('dist-dir=', 'd', 'directory to put final built distributions in'),
  1026. ('skip-build', None, 'skip rebuilding everything (for testing/debugging)'),
  1027. ]
  1028. def _build_apps_options(self):
  1029. return [opt[0].replace('-', '_').replace('=', '') for opt in build_apps.user_options]
  1030. def initialize_options(self):
  1031. self.installers = {}
  1032. self.dist_dir = os.path.join(os.getcwd(), 'dist')
  1033. self.skip_build = False
  1034. for opt in self._build_apps_options():
  1035. setattr(self, opt, None)
  1036. def finalize_options(self):
  1037. # We need to massage the inputs a bit in case they came from a
  1038. # setup.cfg file.
  1039. self.installers = {
  1040. key: _parse_list(value)
  1041. for key, value in _parse_dict(self.installers).items()
  1042. }
  1043. def _get_archive_basedir(self):
  1044. return self.distribution.get_name()
  1045. def create_zip(self, basename, build_dir):
  1046. import zipfile
  1047. base_dir = self._get_archive_basedir()
  1048. with zipfile.ZipFile(basename+'.zip', 'w', compression=zipfile.ZIP_DEFLATED) as zf:
  1049. zf.write(build_dir, base_dir)
  1050. for dirpath, dirnames, filenames in os.walk(build_dir):
  1051. for name in sorted(dirnames):
  1052. path = os.path.normpath(os.path.join(dirpath, name))
  1053. zf.write(path, path.replace(build_dir, base_dir, 1))
  1054. for name in filenames:
  1055. path = os.path.normpath(os.path.join(dirpath, name))
  1056. if os.path.isfile(path):
  1057. zf.write(path, path.replace(build_dir, base_dir, 1))
  1058. def create_tarball(self, basename, build_dir, tar_compression):
  1059. import tarfile
  1060. base_dir = self._get_archive_basedir()
  1061. build_cmd = self.get_finalized_command('build_apps')
  1062. binary_names = list(build_cmd.console_apps.keys()) + list(build_cmd.gui_apps.keys())
  1063. def tarfilter(tarinfo):
  1064. if tarinfo.isdir() or os.path.basename(tarinfo.name) in binary_names:
  1065. tarinfo.mode = 0o755
  1066. else:
  1067. tarinfo.mode = 0o644
  1068. return tarinfo
  1069. with tarfile.open('{}.tar.{}'.format(basename, tar_compression), 'w|{}'.format(tar_compression)) as tf:
  1070. tf.add(build_dir, base_dir, filter=tarfilter)
  1071. def create_nsis(self, basename, build_dir, is_64bit):
  1072. # Get a list of build applications
  1073. build_cmd = self.get_finalized_command('build_apps')
  1074. apps = build_cmd.gui_apps.copy()
  1075. apps.update(build_cmd.console_apps)
  1076. apps = [
  1077. '{}.exe'.format(i)
  1078. for i in apps
  1079. ]
  1080. fullname = self.distribution.get_fullname()
  1081. shortname = self.distribution.get_name()
  1082. # Create the .nsi installer script
  1083. nsifile = p3d.Filename(build_cmd.build_base, shortname + ".nsi")
  1084. nsifile.unlink()
  1085. nsi = open(nsifile.to_os_specific(), "w")
  1086. # Some global info
  1087. nsi.write('Name "%s"\n' % shortname)
  1088. nsi.write('OutFile "%s"\n' % (fullname+'.exe'))
  1089. if is_64bit:
  1090. nsi.write('InstallDir "$PROGRAMFILES64\\%s"\n' % shortname)
  1091. else:
  1092. nsi.write('InstallDir "$PROGRAMFILES\\%s"\n' % shortname)
  1093. nsi.write('SetCompress auto\n')
  1094. nsi.write('SetCompressor lzma\n')
  1095. nsi.write('ShowInstDetails nevershow\n')
  1096. nsi.write('ShowUninstDetails nevershow\n')
  1097. nsi.write('InstType "Typical"\n')
  1098. # Tell Vista that we require admin rights
  1099. nsi.write('RequestExecutionLevel admin\n')
  1100. nsi.write('\n')
  1101. # TODO offer run and desktop shortcut after we figure out how to deal
  1102. # with multiple apps
  1103. nsi.write('!include "MUI2.nsh"\n')
  1104. nsi.write('!define MUI_ABORTWARNING\n')
  1105. nsi.write('\n')
  1106. nsi.write('Var StartMenuFolder\n')
  1107. nsi.write('!insertmacro MUI_PAGE_WELCOME\n')
  1108. # TODO license file
  1109. nsi.write('!insertmacro MUI_PAGE_DIRECTORY\n')
  1110. nsi.write('!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder\n')
  1111. nsi.write('!insertmacro MUI_PAGE_INSTFILES\n')
  1112. nsi.write('!insertmacro MUI_PAGE_FINISH\n')
  1113. nsi.write('!insertmacro MUI_UNPAGE_WELCOME\n')
  1114. nsi.write('!insertmacro MUI_UNPAGE_CONFIRM\n')
  1115. nsi.write('!insertmacro MUI_UNPAGE_INSTFILES\n')
  1116. nsi.write('!insertmacro MUI_UNPAGE_FINISH\n')
  1117. nsi.write('!insertmacro MUI_LANGUAGE "English"\n')
  1118. # This section defines the installer.
  1119. nsi.write('Section "" SecCore\n')
  1120. nsi.write(' SetOutPath "$INSTDIR"\n')
  1121. curdir = ""
  1122. nsi_dir = p3d.Filename.fromOsSpecific(build_cmd.build_base)
  1123. build_root_dir = p3d.Filename.fromOsSpecific(build_dir)
  1124. for root, dirs, files in os.walk(build_dir):
  1125. for name in files:
  1126. basefile = p3d.Filename.fromOsSpecific(os.path.join(root, name))
  1127. file = p3d.Filename(basefile)
  1128. file.makeAbsolute()
  1129. file.makeRelativeTo(nsi_dir)
  1130. outdir = p3d.Filename(basefile)
  1131. outdir.makeAbsolute()
  1132. outdir.makeRelativeTo(build_root_dir)
  1133. outdir = outdir.getDirname().replace('/', '\\')
  1134. if curdir != outdir:
  1135. nsi.write(' SetOutPath "$INSTDIR\\%s"\n' % outdir)
  1136. curdir = outdir
  1137. nsi.write(' File "%s"\n' % (file.toOsSpecific()))
  1138. nsi.write(' SetOutPath "$INSTDIR"\n')
  1139. nsi.write(' WriteUninstaller "$INSTDIR\\Uninstall.exe"\n')
  1140. nsi.write(' ; Start menu items\n')
  1141. nsi.write(' !insertmacro MUI_STARTMENU_WRITE_BEGIN Application\n')
  1142. nsi.write(' CreateDirectory "$SMPROGRAMS\\$StartMenuFolder"\n')
  1143. for app in apps:
  1144. nsi.write(' CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\%s.lnk" "$INSTDIR\\%s"\n' % (shortname, app))
  1145. nsi.write(' CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\Uninstall.lnk" "$INSTDIR\\Uninstall.exe"\n')
  1146. nsi.write(' !insertmacro MUI_STARTMENU_WRITE_END\n')
  1147. nsi.write('SectionEnd\n')
  1148. # This section defines the uninstaller.
  1149. nsi.write('Section Uninstall\n')
  1150. nsi.write(' RMDir /r "$INSTDIR"\n')
  1151. nsi.write(' ; Desktop icon\n')
  1152. nsi.write(' Delete "$DESKTOP\\%s.lnk"\n' % shortname)
  1153. nsi.write(' ; Start menu items\n')
  1154. nsi.write(' !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder\n')
  1155. nsi.write(' RMDir /r "$SMPROGRAMS\\$StartMenuFolder"\n')
  1156. nsi.write('SectionEnd\n')
  1157. nsi.close()
  1158. cmd = ['makensis']
  1159. for flag in ["V2"]:
  1160. cmd.append(
  1161. '{}{}'.format('/' if sys.platform.startswith('win') else '-', flag)
  1162. )
  1163. cmd.append(nsifile.to_os_specific())
  1164. subprocess.check_call(cmd)
  1165. def run(self):
  1166. build_cmd = self.distribution.get_command_obj('build_apps')
  1167. for opt in self._build_apps_options():
  1168. optval = getattr(self, opt)
  1169. if optval is not None:
  1170. setattr(build_cmd, opt, optval)
  1171. build_cmd.finalize_options()
  1172. if not self.skip_build:
  1173. self.run_command('build_apps')
  1174. platforms = build_cmd.platforms
  1175. build_base = os.path.abspath(build_cmd.build_base)
  1176. if not os.path.exists(self.dist_dir):
  1177. os.makedirs(self.dist_dir)
  1178. os.chdir(self.dist_dir)
  1179. for platform in platforms:
  1180. build_dir = os.path.join(build_base, platform)
  1181. basename = '{}_{}'.format(self.distribution.get_fullname(), platform)
  1182. installers = self.installers.get(platform, self.DEFAULT_INSTALLERS.get(platform, ['zip']))
  1183. for installer in installers:
  1184. self.announce('\nBuilding {} for platform: {}'.format(installer, platform), distutils.log.INFO)
  1185. if installer == 'zip':
  1186. self.create_zip(basename, build_dir)
  1187. elif installer in ('gztar', 'bztar', 'xztar'):
  1188. compress = installer.replace('tar', '')
  1189. if compress == 'bz':
  1190. compress = 'bz2'
  1191. self.create_tarball(basename, build_dir, compress)
  1192. elif installer == 'nsis':
  1193. if not platform.startswith('win'):
  1194. self.announce(
  1195. '\tNSIS installer not supported for platform: {}'.format(platform),
  1196. distutils.log.ERROR
  1197. )
  1198. continue
  1199. try:
  1200. subprocess.call(['makensis', '--version'])
  1201. except OSError:
  1202. self.announce(
  1203. '\tCould not find makensis tool that is required to build NSIS installers',
  1204. distutils.log.ERROR
  1205. )
  1206. # continue
  1207. is_64bit = platform == 'win_amd64'
  1208. self.create_nsis(basename, build_dir, is_64bit)
  1209. else:
  1210. self.announce('\tUnknown installer: {}'.format(installer), distutils.log.ERROR)