| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776 |
- """Extends setuptools with the ``build_apps`` and ``bdist_apps`` commands.
- See the :ref:`distribution` section of the programming manual for information
- on how to use these commands.
- """
- import os
- import plistlib
- import sys
- import subprocess
- import zipfile
- import re
- import shutil
- import stat
- import struct
- import string
- import tempfile
- import setuptools
- import distutils.log
- from . import FreezeTool
- from . import pefile
- from . import installers
- from .icon import Icon
- from ._dist_hooks import finalize_distribution_options
- import panda3d.core as p3d
- def _parse_list(input):
- if isinstance(input, str):
- input = input.strip().replace(',', '\n')
- if input:
- return [item.strip() for item in input.split('\n') if item.strip()]
- else:
- return []
- else:
- return input
- def _parse_dict(input):
- if isinstance(input, dict):
- return input
- d = {}
- for item in _parse_list(input):
- key, sep, value = item.partition('=')
- d[key.strip()] = value.strip()
- return d
- def _register_python_loaders():
- # We need this method so that we don't depend on direct.showbase.Loader.
- if getattr(_register_python_loaders, 'done', None):
- return
- _register_python_loaders.done = True
- from importlib.metadata import entry_points
- eps = entry_points()
- if isinstance(eps, dict): # Python 3.8 and 3.9
- loaders = eps.get('panda3d.loaders', ())
- else:
- loaders = eps.select(group='panda3d.loaders')
- registry = p3d.LoaderFileTypeRegistry.get_global_ptr()
- for entry_point in loaders:
- registry.register_deferred_type(entry_point)
- def _model_to_bam(_build_cmd, srcpath, dstpath):
- if dstpath.endswith('.gz') or dstpath.endswith('.pz'):
- dstpath = dstpath[:-3]
- dstpath = dstpath + '.bam'
- src_fn = p3d.Filename.from_os_specific(srcpath)
- dst_fn = p3d.Filename.from_os_specific(dstpath)
- _register_python_loaders()
- loader = p3d.Loader.get_global_ptr()
- options = p3d.LoaderOptions(p3d.LoaderOptions.LF_report_errors |
- p3d.LoaderOptions.LF_no_ram_cache)
- node = loader.load_sync(src_fn, options)
- if not node:
- raise IOError('Failed to load model: %s' % (srcpath))
- if not p3d.NodePath(node).write_bam_file(dst_fn):
- raise IOError('Failed to write .bam file: %s' % (dstpath))
- macosx_binary_magics = (
- b'\xFE\xED\xFA\xCE', b'\xCE\xFA\xED\xFE',
- b'\xFE\xED\xFA\xCF', b'\xCF\xFA\xED\xFE',
- b'\xCA\xFE\xBA\xBE', b'\xBE\xBA\xFE\xCA',
- b'\xCA\xFE\xBA\xBF', b'\xBF\xBA\xFE\xCA')
- # Some dependencies need data directories to be extracted. This dictionary maps
- # modules with data to extract. The values are lists of tuples of the form
- # (source_pattern, destination_pattern, flags). The flags is a set of strings.
- PACKAGE_DATA_DIRS = {
- 'matplotlib': [('matplotlib/mpl-data/*', 'mpl-data', {})],
- 'jsonschema': [('jsonschema/schemas/*', 'schemas', {})],
- 'cefpython3': [
- ('cefpython3/*.pak', '', {}),
- ('cefpython3/*.dat', '', {}),
- ('cefpython3/*.bin', '', {}),
- ('cefpython3/*.dll', '', {}),
- ('cefpython3/libcef.so', '', {}),
- ('cefpython3/LICENSE.txt', '', {}),
- ('cefpython3/License', '', {}),
- ('cefpython3/subprocess*', '', {'PKG_DATA_MAKE_EXECUTABLE'}),
- ('cefpython3/locals/*', 'locals', {}),
- ('cefpython3/Chromium Embedded Framework.framework/Resources', 'Chromium Embedded Framework.framework/Resources', {}),
- ('cefpython3/Chromium Embedded Framework.framework/Chromium Embedded Framework', '', {'PKG_DATA_MAKE_EXECUTABLE'}),
- ],
- 'pytz': [('pytz/zoneinfo/*', 'zoneinfo', ())],
- 'certifi': [('certifi/cacert.pem', '', {})],
- '_tkinter_ext': [('_tkinter_ext/tcl/**', 'tcl', {})],
- }
- # Some dependencies have extra directories that need to be scanned for DLLs.
- # This dictionary maps wheel basenames (ie. the part of the .whl basename
- # before the first hyphen) to a list of tuples, the first value being the
- # directory inside the wheel, the second being which wheel to look in (or
- # None to look in its own wheel).
- PACKAGE_LIB_DIRS = {
- 'scipy': [('scipy/extra-dll', None)],
- 'PyQt5': [('PyQt5/Qt5/bin', 'PyQt5_Qt5')],
- }
- SITE_PY = """
- import sys
- from _frozen_importlib import _imp, FrozenImporter
- sys.frozen = True
- if sys.platform == 'win32' and sys.version_info < (3, 10):
- # Make sure the preferred encoding is something we actually support.
- import _bootlocale
- enc = _bootlocale.getpreferredencoding().lower()
- if enc != 'utf-8' and not _imp.is_frozen('encodings.%s' % (enc)):
- def getpreferredencoding(do_setlocale=True):
- return 'mbcs'
- _bootlocale.getpreferredencoding = getpreferredencoding
- # Alter FrozenImporter to give a __file__ property to frozen modules.
- _find_spec = FrozenImporter.find_spec
- def find_spec(fullname, path=None, target=None):
- spec = _find_spec(fullname, path=path, target=target)
- if spec:
- spec.has_location = True
- spec.origin = sys.executable
- return spec
- def get_data(path):
- with open(path, 'rb') as fp:
- return fp.read()
- FrozenImporter.find_spec = find_spec
- FrozenImporter.get_data = get_data
- """
- SITE_PY_ANDROID = """
- import sys, os
- from _frozen_importlib import _imp, FrozenImporter
- from importlib import _bootstrap_external
- from importlib.abc import Loader, MetaPathFinder
- from importlib.machinery import ModuleSpec
- from io import RawIOBase, TextIOWrapper
- from android_log import write as android_log_write
- sys.frozen = True
- # Temporary hack for plyer to detect Android, see kivy/plyer#670
- os.environ['ANDROID_ARGUMENT'] = ''
- # Replace stdout/stderr with something that writes to the Android log.
- class AndroidLogStream:
- closed = False
- encoding = 'utf-8'
- def __init__(self, prio, tag):
- self.prio = prio
- self.tag = tag
- self.buffer = ''
- def isatty(self):
- return False
- def write(self, text):
- self.writelines(text.split('\\n'))
- def writelines(self, lines):
- num_lines = len(lines)
- if num_lines == 1:
- self.buffer += lines[0]
- elif num_lines > 1:
- android_log_write(self.prio, self.tag, self.buffer + lines[0])
- for line in lines[1:-1]:
- android_log_write(self.prio, self.tag, line)
- self.buffer = lines[-1]
- def flush(self):
- pass
- def seekable(self):
- return False
- def readable(self):
- return False
- def writable(self):
- return True
- sys.stdout = AndroidLogStream(2, 'Python')
- sys.stderr = AndroidLogStream(3, 'Python')
- # Alter FrozenImporter to give a __file__ property to frozen modules.
- _find_spec = FrozenImporter.find_spec
- def find_spec(fullname, path=None, target=None):
- spec = _find_spec(fullname, path=path, target=target)
- if spec:
- spec.has_location = True
- spec.origin = sys.executable
- return spec
- def get_data(path):
- with open(path, 'rb') as fp:
- return fp.read()
- FrozenImporter.find_spec = find_spec
- FrozenImporter.get_data = get_data
- class AndroidExtensionFinder(MetaPathFinder):
- @classmethod
- def find_spec(cls, fullname, path=None, target=None):
- soname = 'libpy.' + fullname + '.so'
- path = os.path.join(os.path.dirname(sys.executable), soname)
- if os.path.exists(path):
- loader = _bootstrap_external.ExtensionFileLoader(fullname, path)
- return ModuleSpec(fullname, loader, origin=path)
- sys.meta_path.append(AndroidExtensionFinder)
- """
- class build_apps(setuptools.Command):
- description = 'build Panda3D applications'
- user_options = [
- ('build-base=', None, 'directory to build applications in'),
- ('requirements-path=', None, 'path to requirements.txt file for pip'),
- ('platforms=', 'p', 'a list of platforms to build for'),
- ]
- default_file_handlers = {
- }
- def initialize_options(self):
- self.build_base = os.path.join(os.getcwd(), 'build')
- self.application_id = None
- self.android_abis = None
- self.android_debuggable = False
- self.android_version_code = 1
- self.android_min_sdk_version = 21
- self.android_max_sdk_version = None
- self.android_target_sdk_version = 30
- self.gui_apps = {}
- self.console_apps = {}
- self.macos_main_app = None
- self.rename_paths = {}
- self.include_patterns = []
- self.exclude_patterns = []
- self.include_modules = {}
- self.exclude_modules = {}
- self.icons = {}
- self.platforms = [
- 'manylinux2014_x86_64',
- 'macosx_10_9_x86_64',
- 'win_amd64',
- ]
- self.plugins = []
- self.embed_prc_data = True
- self.extra_prc_files = []
- self.extra_prc_data = ''
- self.default_prc_dir = None
- self.log_filename = None
- self.log_filename_strftime = True
- self.log_append = False
- self.prefer_discrete_gpu = False
- self.requirements_path = os.path.join(os.getcwd(), 'requirements.txt')
- self.strip_docstrings = True
- self.use_optimized_wheels = True
- self.optimized_wheel_index = ''
- self.pypi_extra_indexes = [
- 'https://archive.panda3d.org/thirdparty',
- ]
- self.file_handlers = {}
- self.bam_model_extensions = ['.egg', '.gltf', '.glb']
- self.exclude_dependencies = [
- # Windows
- 'kernel32.dll', 'user32.dll', 'wsock32.dll', 'ws2_32.dll',
- 'advapi32.dll', 'opengl32.dll', 'glu32.dll', 'gdi32.dll',
- 'shell32.dll', 'ntdll.dll', 'ws2help.dll', 'rpcrt4.dll',
- 'imm32.dll', 'ddraw.dll', 'shlwapi.dll', 'secur32.dll',
- 'dciman32.dll', 'comdlg32.dll', 'comctl32.dll', 'ole32.dll',
- 'oleaut32.dll', 'gdiplus.dll', 'winmm.dll', 'iphlpapi.dll',
- 'msvcrt.dll', 'kernelbase.dll', 'msimg32.dll', 'msacm32.dll',
- 'setupapi.dll', 'version.dll', 'userenv.dll', 'netapi32.dll',
- 'crypt32.dll', 'bcrypt.dll',
- # manylinux1/linux
- 'libdl.so.*', 'libstdc++.so.*', 'libm.so.*', 'libgcc_s.so.*',
- 'libpthread.so.*', 'libc.so.*', 'ld-linux-x86-64.so.*',
- 'libgl.so.*', 'libx11.so.*', 'libncursesw.so.*', 'libz.so.*',
- 'librt.so.*', 'libutil.so.*', 'libnsl.so.1', 'libXext.so.6',
- 'libXrender.so.1', 'libICE.so.6', 'libSM.so.6', 'libEGL.so.1',
- 'libOpenGL.so.0', 'libGLdispatch.so.0', 'libGLX.so.0',
- 'libgobject-2.0.so.0', 'libgthread-2.0.so.0', 'libglib-2.0.so.0',
- # macOS
- '/usr/lib/libc++.1.dylib',
- '/usr/lib/libstdc++.*.dylib',
- '/usr/lib/libz.*.dylib',
- '/usr/lib/libobjc.*.dylib',
- '/usr/lib/libSystem.*.dylib',
- '/usr/lib/libbz2.*.dylib',
- '/usr/lib/libedit.*.dylib',
- '/usr/lib/libffi.dylib',
- '/usr/lib/libauditd.0.dylib',
- '/usr/lib/libgermantok.dylib',
- '/usr/lib/liblangid.dylib',
- '/usr/lib/libarchive.2.dylib',
- '/usr/lib/libipsec.A.dylib',
- '/usr/lib/libpanel.5.4.dylib',
- '/usr/lib/libiodbc.2.1.18.dylib',
- '/usr/lib/libhunspell-1.2.0.0.0.dylib',
- '/usr/lib/libsqlite3.dylib',
- '/usr/lib/libpam.1.dylib',
- '/usr/lib/libtidy.A.dylib',
- '/usr/lib/libDHCPServer.A.dylib',
- '/usr/lib/libpam.2.dylib',
- '/usr/lib/libXplugin.1.dylib',
- '/usr/lib/libxslt.1.dylib',
- '/usr/lib/libiodbcinst.2.1.18.dylib',
- '/usr/lib/libBSDPClient.A.dylib',
- '/usr/lib/libsandbox.1.dylib',
- '/usr/lib/libform.5.4.dylib',
- '/usr/lib/libbsm.0.dylib',
- '/usr/lib/libMatch.1.dylib',
- '/usr/lib/libresolv.9.dylib',
- '/usr/lib/libcharset.1.dylib',
- '/usr/lib/libxml2.2.dylib',
- '/usr/lib/libiconv.2.dylib',
- '/usr/lib/libScreenReader.dylib',
- '/usr/lib/libdtrace.dylib',
- '/usr/lib/libicucore.A.dylib',
- '/usr/lib/libsasl2.2.dylib',
- '/usr/lib/libpcap.A.dylib',
- '/usr/lib/libexslt.0.dylib',
- '/usr/lib/libcurl.4.dylib',
- '/usr/lib/libncurses.5.4.dylib',
- '/usr/lib/libxar.1.dylib',
- '/usr/lib/libmenu.5.4.dylib',
- '/System/Library/**',
- # Android
- 'libc.so', 'libm.so', 'liblog.so', 'libdl.so', 'libandroid.so',
- 'libGLESv1_CM.so', 'libGLESv2.so', 'libjnigraphics.so', 'libEGL.so',
- 'libOpenSLES.so', 'libandroid.so', 'libOpenMAXAL.so', 'libz.so',
- ]
- self.package_data_dirs = {}
- self.hidden_imports = {}
- # We keep track of the zip files we've opened.
- self._zip_files = {}
- def _get_zip_file(self, path):
- if path in self._zip_files:
- return self._zip_files[path]
- zip = zipfile.ZipFile(path)
- self._zip_files[path] = zip
- return zip
- def finalize_options(self):
- # We need to massage the inputs a bit in case they came from a
- # setup.cfg file.
- self.gui_apps = _parse_dict(self.gui_apps)
- self.console_apps = _parse_dict(self.console_apps)
- self.rename_paths = _parse_dict(self.rename_paths)
- self.include_patterns = _parse_list(self.include_patterns)
- self.exclude_patterns = _parse_list(self.exclude_patterns)
- self.include_modules = {
- key: _parse_list(value)
- for key, value in _parse_dict(self.include_modules).items()
- }
- self.exclude_modules = {
- key: _parse_list(value)
- for key, value in _parse_dict(self.exclude_modules).items()
- }
- self.icons = _parse_dict(self.icons)
- self.platforms = _parse_list(self.platforms)
- self.plugins = _parse_list(self.plugins)
- self.extra_prc_files = _parse_list(self.extra_prc_files)
- self.hidden_imports = {
- key: _parse_list(value)
- for key, value in _parse_dict(self.hidden_imports).items()
- }
- if self.default_prc_dir is None:
- self.default_prc_dir = '<auto>etc' if not self.embed_prc_data else ''
- num_gui_apps = len(self.gui_apps)
- num_console_apps = len(self.console_apps)
- if not self.macos_main_app:
- if num_gui_apps > 1:
- assert False, 'macos_main_app must be defined if more than one gui_app is defined'
- elif num_gui_apps == 1:
- self.macos_main_app = list(self.gui_apps.keys())[0]
- use_pipenv = (
- 'Pipfile' in os.path.basename(self.requirements_path) or
- not os.path.exists(self.requirements_path) and os.path.exists('Pipfile')
- )
- if use_pipenv:
- reqspath = os.path.join(self.build_base, 'requirements.txt')
- with open(reqspath, 'w') as reqsfile:
- subprocess.check_call(['pipenv', 'lock', '--requirements'], stdout=reqsfile)
- self.requirements_path = reqspath
- if self.use_optimized_wheels:
- if not self.optimized_wheel_index:
- # Try to find an appropriate wheel index
- # Start with the release index
- self.optimized_wheel_index = 'https://archive.panda3d.org/simple/opt'
- # See if a buildbot build is being used
- with open(self.requirements_path) as reqsfile:
- reqsdata = reqsfile.read()
- matches = re.search(r'--extra-index-url (https*://archive.panda3d.org/.*\b)', reqsdata)
- if matches and matches.group(1):
- self.optimized_wheel_index = matches.group(1)
- if not matches.group(1).endswith('opt'):
- self.optimized_wheel_index += '/opt'
- assert self.optimized_wheel_index, 'An index for optimized wheels must be defined if use_optimized_wheels is set'
- assert os.path.exists(self.requirements_path), 'Requirements.txt path does not exist: {}'.format(self.requirements_path)
- assert num_gui_apps + num_console_apps != 0, 'Must specify at least one app in either gui_apps or console_apps'
- self.exclude_dependencies = [p3d.GlobPattern(i) for i in self.exclude_dependencies]
- for glob in self.exclude_dependencies:
- glob.case_sensitive = False
- # bam_model_extensions registers a 2bam handler for each given extension.
- # They can override a default handler, but not a custom handler.
- if self.bam_model_extensions:
- for ext in self.bam_model_extensions:
- ext = '.' + ext.lstrip('.')
- handler = self.file_handlers.get(ext)
- if handler != _model_to_bam:
- assert handler is None, \
- 'Extension {} occurs in both file_handlers and bam_model_extensions!'.format(ext)
- self.file_handlers[ext] = _model_to_bam
- tmp = self.default_file_handlers.copy()
- tmp.update(self.file_handlers)
- self.file_handlers = tmp
- tmp = PACKAGE_DATA_DIRS.copy()
- tmp.update(self.package_data_dirs)
- self.package_data_dirs = tmp
- # Default to all supported ABIs (for the given Android version).
- if self.android_max_sdk_version and self.android_max_sdk_version < 21:
- assert self.android_max_sdk_version >= 19, \
- 'Panda3D requires at least Android API level 19!'
- if self.android_abis:
- for abi in self.android_abis:
- assert abi not in ('mips64', 'x86_64', 'arm64-v8a'), \
- f'{abi} was not a valid Android ABI before Android 21!'
- else:
- self.android_abis = ['armeabi-v7a', 'x86']
- elif not self.android_abis:
- self.android_abis = ['arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86']
- supported_abis = 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
- unsupported_abis = set(self.android_abis) - set(supported_abis)
- if unsupported_abis:
- raise ValueError(f'Unrecognized value(s) for android_abis: {", ".join(unsupported_abis)}\n'
- f'Valid ABIs are: {", ".join(supported_abis)}')
- self.icon_objects = {}
- for app, iconpaths in self.icons.items():
- if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple):
- iconpaths = (iconpaths,)
- iconobj = Icon()
- for iconpath in iconpaths:
- iconobj.addImage(iconpath)
- iconobj.generateMissingImages()
- self.icon_objects[app] = iconobj
- def run(self):
- self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO)
- for platform in self.platforms:
- # Create the build directory, or ensure it is empty.
- build_dir = os.path.join(self.build_base, platform)
- if os.path.exists(build_dir):
- for entry in os.listdir(build_dir):
- path = os.path.join(build_dir, entry)
- if os.path.islink(path) or os.path.isfile(path):
- os.unlink(path)
- else:
- shutil.rmtree(path)
- else:
- os.makedirs(build_dir)
- if platform == 'android':
- # Make a multi-arch build for Android.
- data_dir = os.path.join(build_dir, 'assets')
- os.makedirs(data_dir, exist_ok=True)
- for abi in self.android_abis:
- lib_dir = os.path.join(build_dir, 'lib', abi)
- os.makedirs(lib_dir, exist_ok=True)
- suffix = None
- if abi == 'arm64-v8a':
- suffix = '_arm64'
- elif abi == 'armeabi-v7a':
- suffix = '_armv7a'
- elif abi == 'armeabi':
- suffix = '_arm'
- else: # e.g. x86, x86_64, mips, mips64
- suffix = '_' + abi.replace('-', '_')
- # We end up copying the data multiple times to the same
- # directory, but that's probably fine for now.
- self.build_binaries(platform + suffix, lib_dir, data_dir)
- # Write out the icons to the res directory.
- for appname, icon in self.icon_objects.items():
- if appname == '*' or (appname == self.macos_main_app and '*' not in self.icon_objects):
- # Conventional name for icon on Android.
- basename = 'ic_launcher.png'
- else:
- basename = f'ic_{appname}.png'
- res_dir = os.path.join(build_dir, 'res')
- icon.writeSize(48, os.path.join(res_dir, 'mipmap-mdpi-v4', basename))
- icon.writeSize(72, os.path.join(res_dir, 'mipmap-hdpi-v4', basename))
- icon.writeSize(96, os.path.join(res_dir, 'mipmap-xhdpi-v4', basename))
- icon.writeSize(144, os.path.join(res_dir, 'mipmap-xxhdpi-v4', basename))
- if icon.getLargestSize() >= 192:
- icon.writeSize(192, os.path.join(res_dir, 'mipmap-xxxhdpi-v4', basename))
- self.build_assets(platform, data_dir)
- # Generate an AndroidManifest.xml
- self.generate_android_manifest(os.path.join(build_dir, 'AndroidManifest.xml'))
- else:
- self.build_binaries(platform, build_dir, build_dir)
- self.build_assets(platform, build_dir)
- # Bundle into an .app on macOS
- if self.macos_main_app and 'macosx' in platform:
- self.bundle_macos_app(build_dir)
- def download_wheels(self, platform):
- """ Downloads wheels for the given platform using pip. This includes panda3d
- wheels. These are special wheels that are expected to contain a deploy_libs
- directory containing the Python runtime libraries, which will be added
- to sys.path."""
- import pip
- self.announce('Gathering wheels for platform: {}'.format(platform), distutils.log.INFO)
- whlcache = os.path.join(self.build_base, '__whl_cache__')
- pip_version = int(pip.__version__.split('.', 1)[0])
- if pip_version < 9:
- raise RuntimeError("pip 9.0 or greater is required, but found {}".format(pip.__version__))
- abi_tag = 'cp%d%d' % (sys.version_info[:2])
- if sys.version_info < (3, 8):
- abi_tag += 'm'
- whldir = os.path.join(whlcache, '_'.join((platform, abi_tag)))
- if not os.path.isdir(whldir):
- os.makedirs(whldir)
- # Remove any .zip files. These are built from a VCS and block for an
- # interactive prompt on subsequent downloads.
- if os.path.exists(whldir):
- for whl in os.listdir(whldir):
- if whl.endswith('.zip'):
- os.remove(os.path.join(whldir, whl))
- pip_args = [
- '--disable-pip-version-check',
- 'download',
- '-d', whldir,
- '-r', self.requirements_path,
- '--only-binary', ':all:',
- '--abi', abi_tag,
- '--platform', platform,
- ]
- if platform.startswith('linux_'):
- # Also accept manylinux.
- arch = platform[6:]
- pip_args += ['--platform', 'manylinux2014_' + arch]
- if self.use_optimized_wheels:
- pip_args += [
- '--extra-index-url', self.optimized_wheel_index
- ]
- for index in self.pypi_extra_indexes:
- pip_args += ['--extra-index-url', index]
- try:
- subprocess.check_call([sys.executable, '-m', 'pip'] + pip_args)
- except:
- # Display a more helpful message for these common issues.
- if platform.startswith('manylinux2010_') and sys.version_info >= (3, 11):
- new_platform = platform.replace('manylinux2010_', 'manylinux2014_')
- 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)
- elif platform.startswith('manylinux1_') and sys.version_info >= (3, 10):
- new_platform = platform.replace('manylinux1_', 'manylinux2014_')
- 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)
- elif platform.startswith('macosx_10_6_') and sys.version_info >= (3, 8):
- new_platform = platform.replace('macosx_10_6_', 'macosx_10_9_')
- 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)
- raise
- # Return a list of paths to the downloaded whls
- return [
- os.path.join(whldir, filename)
- for filename in os.listdir(whldir)
- if filename.endswith('.whl')
- ]
- def update_pe_resources(self, appname, runtime):
- """Update resources (e.g., icons) in windows PE file"""
- icon = self.icon_objects.get(
- appname,
- self.icon_objects.get('*', None),
- )
- if icon is not None or self.prefer_discrete_gpu:
- pef = pefile.PEFile()
- pef.open(runtime, 'r+')
- if icon is not None:
- pef.add_icon(icon)
- pef.add_resource_section()
- if self.prefer_discrete_gpu:
- if not pef.rename_export("SymbolPlaceholder___________________", "AmdPowerXpressRequestHighPerformance") or \
- not pef.rename_export("SymbolPlaceholder__", "NvOptimusEnablement"):
- self.warn("Failed to apply prefer_discrete_gpu, newer target Panda3D version may be required")
- pef.write_changes()
- pef.close()
- def bundle_macos_app(self, builddir):
- """Bundle built runtime into a .app for macOS"""
- appname = '{}.app'.format(self.macos_main_app)
- appdir = os.path.join(builddir, appname)
- contentsdir = os.path.join(appdir, 'Contents')
- macosdir = os.path.join(contentsdir, 'MacOS')
- fwdir = os.path.join(contentsdir, 'Frameworks')
- resdir = os.path.join(contentsdir, 'Resources')
- self.announce('Bundling macOS app into {}'.format(appdir), distutils.log.INFO)
- # Create initial directory structure
- os.makedirs(macosdir)
- os.makedirs(fwdir)
- os.makedirs(resdir)
- # Move files over
- for fname in os.listdir(builddir):
- src = os.path.join(builddir, fname)
- if appdir in src:
- continue
- if fname in self.gui_apps or self.console_apps:
- dst = macosdir
- elif os.path.isfile(src) and open(src, 'rb').read(4) in macosx_binary_magics:
- dst = fwdir
- else:
- dst = resdir
- shutil.move(src, dst)
- # Write out Info.plist
- plist = {
- 'CFBundleName': appname,
- 'CFBundleDisplayName': appname, #TODO use name from setup.py/cfg
- 'CFBundleIdentifier': '', #TODO
- 'CFBundleVersion': '0.0.0', #TODO get from setup.py
- 'CFBundlePackageType': 'APPL',
- 'CFBundleSignature': '', #TODO
- 'CFBundleExecutable': self.macos_main_app,
- 'NSHighResolutionCapable': 'True',
- }
- icon = self.icon_objects.get(
- self.macos_main_app,
- self.icon_objects.get('*', None)
- )
- if icon is not None:
- plist['CFBundleIconFile'] = 'iconfile'
- icon.makeICNS(os.path.join(resdir, 'iconfile.icns'))
- with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
- plistlib.dump(plist, f)
- def generate_android_manifest(self, path):
- import xml.etree.ElementTree as ET
- name = self.distribution.get_name()
- version = self.distribution.get_version()
- classifiers = self.distribution.get_classifiers()
- is_game = False
- for classifier in classifiers:
- if classifier == 'Topic :: Games/Entertainment' or classifier.startswith('Topic :: Games/Entertainment ::'):
- is_game = True
- manifest = ET.Element('manifest')
- manifest.set('xmlns:android', 'http://schemas.android.com/apk/res/android')
- manifest.set('package', self.application_id)
- manifest.set('android:versionCode', str(int(self.android_version_code)))
- manifest.set('android:versionName', version)
- manifest.set('android:installLocation', 'auto')
- uses_sdk = ET.SubElement(manifest, 'uses-sdk')
- uses_sdk.set('android:minSdkVersion', str(int(self.android_min_sdk_version)))
- uses_sdk.set('android:targetSdkVersion', str(int(self.android_target_sdk_version)))
- if self.android_max_sdk_version:
- uses_sdk.set('android:maxSdkVersion', str(int(self.android_max_sdk_version)))
- if 'pandagles2' in self.plugins:
- uses_feature = ET.SubElement(manifest, 'uses-feature')
- uses_feature.set('android:glEsVersion', '0x00020000')
- uses_feature.set('android:required', 'false' if 'pandagles' in self.plugins else 'true')
- if 'p3openal_audio' in self.plugins:
- uses_feature = ET.SubElement(manifest, 'uses-feature')
- uses_feature.set('android:name', 'android.hardware.audio.output')
- uses_feature.set('android:required', 'false')
- uses_feature = ET.SubElement(manifest, 'uses-feature')
- uses_feature.set('android:name', 'android.hardware.gamepad')
- uses_feature.set('android:required', 'false')
- application = ET.SubElement(manifest, 'application')
- application.set('android:label', name)
- application.set('android:isGame', ('false', 'true')[is_game])
- application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
- application.set('android:extractNativeLibs', 'true')
- app_icon = self.icon_objects.get('*', self.icon_objects.get(self.macos_main_app))
- if app_icon:
- application.set('android:icon', '@mipmap/ic_launcher')
- for appname in self.gui_apps:
- activity = ET.SubElement(application, 'activity')
- activity.set('android:name', 'org.panda3d.android.PythonActivity')
- activity.set('android:label', appname)
- activity.set('android:theme', '@android:style/Theme.NoTitleBar')
- activity.set('android:configChanges', 'orientation|keyboardHidden')
- activity.set('android:launchMode', 'singleInstance')
- act_icon = self.icon_objects.get(appname)
- if act_icon and act_icon is not app_icon:
- activity.set('android:icon', '@mipmap/ic_' + appname)
- meta_data = ET.SubElement(activity, 'meta-data')
- meta_data.set('android:name', 'android.app.lib_name')
- meta_data.set('android:value', appname)
- intent_filter = ET.SubElement(activity, 'intent-filter')
- ET.SubElement(intent_filter, 'action').set('android:name', 'android.intent.action.MAIN')
- ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LAUNCHER')
- ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LEANBACK_LAUNCHER')
- tree = ET.ElementTree(manifest)
- with open(path, 'wb') as fh:
- tree.write(fh, encoding='utf-8', xml_declaration=True)
- def build_binaries(self, platform, binary_dir, data_dir=None):
- """ Builds the binary data for the given platform. """
- use_wheels = True
- path = sys.path[:]
- p3dwhl = None
- wheelpaths = []
- has_tkinter_wheel = False
- if use_wheels:
- wheelpaths = self.download_wheels(platform)
- for whl in wheelpaths:
- if os.path.basename(whl).startswith('panda3d-'):
- p3dwhlfn = whl
- p3dwhl = self._get_zip_file(p3dwhlfn)
- break
- elif os.path.basename(whl).startswith('tkinter-'):
- has_tkinter_wheel = True
- else:
- raise RuntimeError("Missing panda3d wheel for platform: {}".format(platform))
- if self.use_optimized_wheels:
- # Check to see if we have an optimized wheel
- localtag = p3dwhlfn.split('+')[1].split('-')[0] if '+' in p3dwhlfn else ''
- if not localtag.endswith('opt'):
- self.announce(
- 'Could not find an optimized wheel (using index {}) for platform: {}'.format(self.optimized_wheel_index, platform),
- distutils.log.WARN
- )
- for whl in wheelpaths:
- if os.path.basename(whl).startswith('tkinter-'):
- has_tkinter_wheel = True
- break
- #whlfiles = {whl: self._get_zip_file(whl) for whl in wheelpaths}
- # Add whl files to the path so they are picked up by modulefinder
- for whl in wheelpaths:
- path.insert(0, whl)
- # Add deploy_libs from panda3d whl to the path
- path.insert(0, os.path.join(p3dwhlfn, 'deploy_libs'))
- self.announce('Building runtime for platform: {}'.format(platform), distutils.log.INFO)
- # Gather PRC data
- prcstring = ''
- if not use_wheels:
- dtool_fn = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name())
- libdir = os.path.dirname(dtool_fn.to_os_specific())
- etcdir = os.path.join(libdir, '..', 'etc')
- for fn in sorted(os.listdir(etcdir), reverse=True):
- if fn.lower().endswith('.prc'):
- with open(os.path.join(etcdir, fn)) as f:
- prcstring += f.read()
- else:
- for fn in sorted((i for i in p3dwhl.namelist() if i.endswith('.prc')), reverse=True):
- with p3dwhl.open(fn) as f:
- prcstring += f.read().decode('utf8')
- user_prcstring = self.extra_prc_data
- for fn in self.extra_prc_files:
- with open(fn) as f:
- user_prcstring += f.read()
- # Clenup PRC data
- check_plugins = [
- #TODO find a better way to get this list
- 'pandaegg',
- 'p3ffmpeg',
- 'p3ptloader',
- 'p3assimp',
- ]
- def parse_prc(prcstr, warn_on_missing_plugin):
- out = []
- for ln in prcstr.split('\n'):
- ln = ln.strip()
- useline = True
- if ln.startswith('#') or not ln:
- continue
- words = ln.split(None, 1)
- if not words:
- continue
- var = words[0]
- value = words[1] if len(words) > 1 else ''
- # Strip comment after value.
- c = value.find(' #')
- if c > 0:
- value = value[:c].rstrip()
- if var == 'model-cache-dir' and value:
- if platform.startswith('android'):
- # Ignore on Android, where the cache dir is fixed.
- continue
- value = value.replace('/panda3d', '/{}'.format(self.distribution.get_name()))
- if var == 'audio-library-name':
- # We have the default set to p3fmod_audio on macOS in 1.10,
- # but this can be unexpected as other platforms use OpenAL
- # by default. Switch it up if FMOD is not included.
- if value not in self.plugins and value == 'p3fmod_audio' and 'p3openal_audio' in self.plugins:
- self.warn("Missing audio plugin p3fmod_audio referenced in PRC data, replacing with p3openal_audio")
- value = 'p3openal_audio'
- if var == 'aux-display':
- # Silently remove aux-display lines for missing plugins.
- if value not in self.plugins:
- continue
- for plugin in check_plugins:
- if plugin in value and plugin not in self.plugins:
- useline = False
- if warn_on_missing_plugin:
- self.warn(
- "Missing plugin ({0}) referenced in user PRC data".format(plugin)
- )
- break
- if useline:
- if value:
- out.append(var + ' ' + value)
- else:
- out.append(var)
- return out
- prcexport = parse_prc(prcstring, 0) + parse_prc(user_prcstring, 1)
- # Export PRC data
- prcexport = '\n'.join(prcexport)
- if not self.embed_prc_data:
- prcdir = self.default_prc_dir.replace('<auto>', '')
- prcdir = os.path.join(binary_dir, prcdir)
- os.makedirs(prcdir)
- with open(os.path.join(prcdir, '00-panda3d.prc'), 'w') as f:
- f.write(prcexport)
- # Create runtimes
- freezer_extras = set()
- freezer_modules = set()
- ext_suffixes = set()
- def get_search_path_for(source_path):
- search_path = [os.path.dirname(source_path)]
- if use_wheels:
- search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
- # If the .whl containing this file has a .libs directory, add
- # it to the path. This is an auditwheel/numpy convention.
- if '.whl' + os.sep in source_path:
- whl, wf = source_path.split('.whl' + os.path.sep)
- whl += '.whl'
- rootdir = wf.split(os.path.sep, 1)[0]
- search_path.append(os.path.join(whl, rootdir, '.libs'))
- # Also look for eg. numpy.libs or Pillow.libs in the root
- whl_name = os.path.basename(whl).split('-', 1)[0]
- search_path.append(os.path.join(whl, whl_name + '.libs'))
- # Also look for more specific per-package cases, defined in
- # PACKAGE_LIB_DIRS at the top of this file.
- extra_dirs = PACKAGE_LIB_DIRS.get(whl_name, [])
- for extra_dir, search_in in extra_dirs:
- if not search_in:
- search_path.append(os.path.join(whl, extra_dir.replace('/', os.path.sep)))
- else:
- for whl2 in wheelpaths:
- if os.path.basename(whl2).startswith(search_in + '-'):
- search_path.append(os.path.join(whl2, extra_dir.replace('/', os.path.sep)))
- return search_path
- def create_runtime(platform, appname, mainscript, use_console):
- freezer = FreezeTool.Freezer(
- platform=platform,
- path=path,
- hiddenImports=self.hidden_imports,
- optimize=2 if self.strip_docstrings else 1
- )
- freezer.addModule('__main__', filename=mainscript)
- if platform.startswith('android'):
- freezer.addModule('site', filename='site.py', text=SITE_PY_ANDROID)
- else:
- freezer.addModule('site', filename='site.py', text=SITE_PY)
- for incmod in self.include_modules.get(appname, []) + self.include_modules.get('*', []):
- freezer.addModule(incmod)
- for exmod in self.exclude_modules.get(appname, []) + self.exclude_modules.get('*', []):
- freezer.excludeModule(exmod)
- freezer.done(addStartupModules=True)
- stub_name = 'deploy-stub'
- target_name = appname
- if platform.startswith('win') or 'macosx' in platform:
- if not use_console:
- stub_name = 'deploy-stubw'
- elif platform.startswith('android'):
- if not use_console:
- stub_name = 'libdeploy-stubw.so'
- target_name = 'lib' + target_name + '.so'
- if platform.startswith('win'):
- stub_name += '.exe'
- target_name += '.exe'
- if use_wheels:
- if stub_name.endswith('.so'):
- stub_file = p3dwhl.open('deploy_libs/{0}'.format(stub_name))
- else:
- stub_file = p3dwhl.open('panda3d_tools/{0}'.format(stub_name))
- else:
- dtool_path = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name()).to_os_specific()
- stub_path = os.path.join(os.path.dirname(dtool_path), '..', 'bin', stub_name)
- stub_file = open(stub_path, 'rb')
- # Do we need an icon? On Windows, we need to add this to the stub
- # before we add the blob.
- if 'win' in platform:
- temp_file = tempfile.NamedTemporaryFile(suffix='-icon.exe', delete=False)
- temp_file.write(stub_file.read())
- stub_file.close()
- temp_file.close()
- self.update_pe_resources(appname, temp_file.name)
- stub_file = open(temp_file.name, 'rb')
- else:
- temp_file = None
- use_strftime = self.log_filename_strftime
- if not self.log_filename or '%' not in self.log_filename:
- use_strftime = False
- target_path = os.path.join(binary_dir, target_name)
- freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
- 'prc_data': prcexport if self.embed_prc_data else None,
- 'default_prc_dir': self.default_prc_dir,
- 'prc_dir_envvars': None,
- 'prc_path_envvars': None,
- 'prc_patterns': None,
- 'prc_encrypted_patterns': None,
- 'prc_encryption_key': None,
- 'prc_executable_patterns': None,
- 'prc_executable_args_envvar': None,
- 'main_dir': None,
- 'log_filename': self.expand_path(self.log_filename, platform),
- }, self.log_append, use_strftime)
- stub_file.close()
- if temp_file:
- os.unlink(temp_file.name)
- # Copy the dependencies.
- search_path = [binary_dir]
- if use_wheels:
- search_path.append(os.path.join(p3dwhlfn, 'panda3d'))
- search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
- self.copy_dependencies(target_path, binary_dir, search_path, stub_name)
- freezer_extras.update(freezer.extras)
- freezer_modules.update(freezer.getAllModuleNames())
- for suffix in freezer.mf.suffixes:
- if suffix[2] == 3: # imp.C_EXTENSION:
- ext_suffixes.add(suffix[0])
- for appname, scriptname in self.gui_apps.items():
- create_runtime(platform, appname, scriptname, False)
- for appname, scriptname in self.console_apps.items():
- create_runtime(platform, appname, scriptname, True)
- # Warn if tkinter is used but hasn't been added to requirements.txt
- if not has_tkinter_wheel and '_tkinter' in freezer_modules:
- self.warn("Detected use of tkinter, but tkinter is not specified in requirements.txt!")
- # Copy extension modules
- whl_modules = {}
- if use_wheels:
- # Get the module libs
- for i in p3dwhl.namelist():
- if not i.startswith('deploy_libs/'):
- continue
- if not any(i.endswith(suffix) for suffix in ext_suffixes):
- continue
- if has_tkinter_wheel and i.startswith('deploy_libs/_tkinter.'):
- # Ignore this one, we have a separate tkinter package
- # nowadays that contains all the dependencies.
- continue
- base = os.path.basename(i)
- module, _, ext = base.partition('.')
- whl_modules[module] = i
- # Make sure to copy any builtins that have shared objects in the
- # deploy libs, assuming they are not already in freezer_extras.
- for mod, source_path in freezer_extras:
- freezer_modules.discard(mod)
- for mod in freezer_modules:
- if mod in whl_modules:
- freezer_extras.add((mod, None))
- # Copy over necessary plugins
- plugin_list = ['panda3d/lib{}'.format(i) for i in self.plugins]
- for lib in p3dwhl.namelist():
- plugname = lib.split('.', 1)[0]
- if plugname in plugin_list:
- source_path = os.path.join(p3dwhlfn, lib)
- target_path = os.path.join(binary_dir, os.path.basename(lib))
- search_path = [os.path.dirname(source_path)]
- self.copy_with_dependencies(source_path, target_path, search_path)
- # Copy any shared objects we need
- for module, source_path in freezer_extras:
- if source_path is not None:
- # Rename panda3d/core.pyd to panda3d.core.pyd
- source_path = os.path.normpath(source_path)
- basename = os.path.basename(source_path)
- if '.' in module:
- basename = module.rsplit('.', 1)[0] + '.' + basename
- # Remove python version string
- parts = basename.split('.')
- if len(parts) >= 3 and ('-' in parts[-2] or parts[-2] == 'abi' + str(sys.version_info[0])):
- parts = parts[:-2] + parts[-1:]
- basename = '.'.join(parts)
- # Was this not found in a wheel? Then we may have a problem,
- # since it may be for the current platform instead of the target
- # platform.
- if use_wheels:
- found_in_wheel = False
- for whl in wheelpaths:
- whl = os.path.normpath(whl)
- if source_path.lower().startswith(os.path.join(whl, '').lower()):
- found_in_wheel = True
- break
- if not found_in_wheel:
- self.warn('{} was not found in any downloaded wheel, is a dependency missing from requirements.txt?'.format(basename))
- else:
- # Builtin module, but might not be builtin in wheel libs, so double check
- if module in whl_modules:
- source_path = os.path.join(p3dwhlfn, whl_modules[module])
- basename = os.path.basename(source_path)
- #XXX should we remove python version string here too?
- else:
- continue
- if platform.startswith('android'):
- # Python modules on Android need a special prefix to be loadable
- # as a library.
- basename = 'libpy.' + basename
- # If this is a dynamic library, search for dependencies.
- target_path = os.path.join(binary_dir, basename)
- search_path = get_search_path_for(source_path)
- self.copy_with_dependencies(source_path, target_path, search_path)
- # Copy classes.dex on Android
- if use_wheels and platform.startswith('android'):
- self.copy(os.path.join(p3dwhlfn, 'deploy_libs', 'classes.dex'),
- os.path.join(binary_dir, '..', '..', 'classes.dex'))
- # Extract any other data files from dependency packages.
- if data_dir is None:
- return
- for module, datadesc in self.package_data_dirs.items():
- if module not in freezer_modules:
- continue
- self.announce('Copying data files for module: {}'.format(module), distutils.log.INFO)
- # OK, find out in which .whl this occurs.
- for whl in wheelpaths:
- whlfile = self._get_zip_file(whl)
- filenames = whlfile.namelist()
- for source_pattern, target_dir, flags in datadesc:
- srcglob = p3d.GlobPattern(source_pattern.lower())
- source_dir = os.path.dirname(source_pattern)
- # Relocate the target dir to the build directory.
- target_dir = target_dir.replace('/', os.sep)
- target_dir = os.path.join(data_dir, target_dir)
- for wf in filenames:
- if wf.endswith('/'):
- # Skip directories.
- continue
- if wf.lower().startswith(source_dir.lower() + '/'):
- if not srcglob.matches(wf.lower()):
- continue
- wf = wf.replace('/', os.sep)
- relpath = wf[len(source_dir) + 1:]
- source_path = os.path.join(whl, wf)
- target_path = os.path.join(target_dir, relpath)
- if 'PKG_DATA_MAKE_EXECUTABLE' in flags:
- search_path = get_search_path_for(source_path)
- self.copy_with_dependencies(source_path, target_path, search_path)
- mode = os.stat(target_path).st_mode
- mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
- os.chmod(target_path, mode)
- else:
- self.copy(source_path, target_path)
- def build_assets(self, platform, data_dir):
- """ Builds the data files for the given platform. """
- # Copy Game Files
- self.announce('Copying assets for platform: {}'.format(platform), distutils.log.INFO)
- ignore_copy_list = [
- '**/__pycache__/**',
- '**/*.pyc',
- '**/*.py',
- '{}/**'.format(self.build_base),
- ]
- ignore_copy_list += self.exclude_patterns
- ignore_copy_list += self.extra_prc_files
- ignore_copy_list = [p3d.GlobPattern(p3d.Filename.from_os_specific(i).get_fullpath()) for i in ignore_copy_list]
- include_copy_list = [p3d.GlobPattern(i) for i in self.include_patterns]
- def check_pattern(src, pattern_list):
- # Normalize file paths across platforms
- fn = p3d.Filename.from_os_specific(os.path.normpath(src))
- path = fn.get_fullpath()
- fn.make_absolute()
- abspath = fn.get_fullpath()
- for pattern in pattern_list:
- # If the pattern is absolute, match against the absolute filename.
- if pattern.pattern[0] == '/':
- #print('check ignore: {} {} {}'.format(pattern, src, pattern.matches_file(abspath)))
- if pattern.matches_file(abspath):
- return True
- else:
- #print('check ignore: {} {} {}'.format(pattern, src, pattern.matches_file(path)))
- if pattern.matches_file(path):
- return True
- return False
- def check_file(fname):
- return check_pattern(fname, include_copy_list) and \
- not check_pattern(fname, ignore_copy_list)
- def skip_directory(src):
- # Provides a quick-out for directory checks. NOT recursive.
- fn = p3d.Filename.from_os_specific(os.path.normpath(src))
- path = fn.get_fullpath()
- fn.make_absolute()
- abspath = fn.get_fullpath()
- for pattern in ignore_copy_list:
- if not pattern.pattern.endswith('/*') and \
- not pattern.pattern.endswith('/**'):
- continue
- pattern_dir = p3d.Filename(pattern.pattern).get_dirname()
- if abspath.startswith(pattern_dir + '/'):
- return True
- if path.startswith(pattern_dir + '/'):
- return True
- return False
- def copy_file(src, dst):
- src = os.path.normpath(src)
- dst = os.path.normpath(dst)
- if not check_file(src):
- self.announce('skipping file {}'.format(src))
- return
- dst_dir = os.path.dirname(dst)
- if not os.path.exists(dst_dir):
- os.makedirs(dst_dir)
- ext = os.path.splitext(src)[1]
- # If the file ends with .gz/.pz, we strip this off.
- if ext in ('.gz', '.pz'):
- ext = os.path.splitext(src[:-3])[1]
- if not ext:
- ext = os.path.basename(src)
- if ext in self.file_handlers:
- buildscript = self.file_handlers[ext]
- self.announce('running {} on src ({})'.format(buildscript.__name__, src))
- try:
- dst = self.file_handlers[ext](self, src, dst)
- except Exception as err:
- self.announce('{}'.format(err), distutils.log.ERROR)
- else:
- self.announce('copying {0} -> {1}'.format(src, dst))
- shutil.copyfile(src, dst)
- def update_path(path):
- normpath = p3d.Filename.from_os_specific(os.path.normpath(src)).c_str()
- for inputpath, outputpath in self.rename_paths.items():
- if normpath.startswith(inputpath):
- normpath = normpath.replace(inputpath, outputpath, 1)
- return p3d.Filename(normpath).to_os_specific()
- rootdir = os.getcwd()
- for dirname, subdirlist, filelist in os.walk(rootdir):
- subdirlist.sort()
- dirpath = os.path.relpath(dirname, rootdir)
- if skip_directory(dirpath):
- self.announce('skipping directory {}'.format(dirpath))
- continue
- for fname in filelist:
- src = os.path.join(dirpath, fname)
- dst = os.path.join(data_dir, update_path(src))
- copy_file(src, dst)
- def add_dependency(self, name, target_dir, search_path, referenced_by):
- """ Searches for the given DLL on the search path. If it exists,
- copies it to the target_dir. """
- if os.path.exists(os.path.join(target_dir, name)):
- # We've already added it earlier.
- return
- for dep in self.exclude_dependencies:
- if dep.matches_file(name):
- return
- for dir in search_path:
- source_path = os.path.join(dir, name)
- if os.path.isfile(source_path):
- target_path = os.path.join(target_dir, name)
- self.copy_with_dependencies(source_path, target_path, search_path)
- return
- elif '.whl' in source_path:
- # Check whether the file exists inside the wheel.
- whl, wf = source_path.split('.whl' + os.path.sep)
- whl += '.whl'
- whlfile = self._get_zip_file(whl)
- # Normalize the path separator
- wf = os.path.normpath(wf).replace(os.path.sep, '/')
- # Look case-insensitively.
- namelist = whlfile.namelist()
- namelist_lower = [file.lower() for file in namelist]
- if wf.lower() in namelist_lower:
- # We have a match. Change it to the correct case.
- wf = namelist[namelist_lower.index(wf.lower())]
- source_path = os.path.join(whl, wf)
- target_path = os.path.join(target_dir, os.path.basename(wf))
- self.copy_with_dependencies(source_path, target_path, search_path)
- return
- # If we didn't find it, look again, but case-insensitively.
- name_lower = name.lower()
- for dir in search_path:
- if os.path.isdir(dir):
- files = os.listdir(dir)
- files_lower = [file.lower() for file in files]
- if name_lower in files_lower:
- name = files[files_lower.index(name_lower)]
- source_path = os.path.join(dir, name)
- target_path = os.path.join(target_dir, name)
- self.copy_with_dependencies(source_path, target_path, search_path)
- # Warn if we can't find it, but only once.
- self.warn("could not find dependency {0} (referenced by {1})".format(name, referenced_by))
- self.exclude_dependencies.append(p3d.GlobPattern(name.lower()))
- def copy(self, source_path, target_path):
- """ Copies source_path to target_path.
- source_path may be located inside a .whl file. """
- try:
- self.announce('copying {0} -> {1}'.format(os.path.relpath(source_path, self.build_base), os.path.relpath(target_path, self.build_base)))
- except ValueError:
- # No relative path (e.g., files on different drives in Windows), just print absolute paths instead
- self.announce('copying {0} -> {1}'.format(source_path, target_path))
- # Make the directory if it does not yet exist.
- target_dir = os.path.dirname(target_path)
- if not os.path.isdir(target_dir):
- os.makedirs(target_dir)
- # Copy the file, and open it for analysis.
- if '.whl' in source_path:
- # This was found in a wheel, extract it
- whl, wf = source_path.split('.whl' + os.path.sep)
- whl += '.whl'
- whlfile = self._get_zip_file(whl)
- data = whlfile.read(wf.replace(os.path.sep, '/'))
- with open(target_path, 'wb') as f:
- f.write(data)
- else:
- # Regular file, copy it
- shutil.copyfile(source_path, target_path)
- def copy_with_dependencies(self, source_path, target_path, search_path):
- """ Copies source_path to target_path. It also scans source_path for
- any dependencies, which are located along the given search_path and
- copied to the same directory as target_path.
- source_path may be located inside a .whl file. """
- self.copy(source_path, target_path)
- source_dir = os.path.dirname(source_path)
- target_dir = os.path.dirname(target_path)
- base = os.path.basename(target_path)
- if source_dir not in search_path:
- search_path = search_path + [source_dir]
- self.copy_dependencies(target_path, target_dir, search_path, base)
- def copy_dependencies(self, target_path, target_dir, search_path, referenced_by):
- """ Copies the dependencies of target_path into target_dir. """
- fp = open(target_path, 'rb+')
- # What kind of magic does the file contain?
- deps = []
- magic = fp.read(4)
- if magic.startswith(b'MZ'):
- # It's a Windows DLL or EXE file.
- pe = pefile.PEFile()
- pe.read(fp)
- for lib in pe.imports:
- deps.append(lib)
- elif magic == b'\x7FELF':
- # Elf magic. Used on (among others) Linux and FreeBSD.
- deps = self._read_dependencies_elf(fp, target_dir, search_path)
- elif magic in (b'\xCE\xFA\xED\xFE', b'\xCF\xFA\xED\xFE'):
- # A Mach-O file, as used on macOS.
- deps = self._read_dependencies_macho(fp, '<', flatten=True)
- elif magic in (b'\xFE\xED\xFA\xCE', b'\xFE\xED\xFA\xCF'):
- rel_dir = os.path.relpath(target_dir, os.path.dirname(target_path))
- deps = self._read_dependencies_macho(fp, '>', flatten=True)
- elif magic in (b'\xCA\xFE\xBA\xBE', b'\xBE\xBA\xFE\xCA'):
- # A fat file, containing multiple Mach-O binaries. In the future,
- # we may want to extract the one containing the architecture we
- # are building for.
- deps = self._read_dependencies_fat(fp, False, flatten=True)
- elif magic in (b'\xCA\xFE\xBA\xBF', b'\xBF\xBA\xFE\xCA'):
- # A 64-bit fat file.
- deps = self._read_dependencies_fat(fp, True, flatten=True)
- # If we discovered any dependencies, recursively add those.
- for dep in deps:
- self.add_dependency(dep, target_dir, search_path, referenced_by)
- def _read_dependencies_elf(self, elf, origin, search_path):
- """ Having read the first 4 bytes of the ELF file, fetches the
- dependent libraries and returns those as a list. """
- ident = elf.read(12)
- # Make sure we read in the correct endianness and integer size
- byte_order = "<>"[ord(ident[1:2]) - 1]
- elf_class = ord(ident[0:1]) - 1 # 0 = 32-bits, 1 = 64-bits
- header_struct = byte_order + ("HHIIIIIHHHHHH", "HHIQQQIHHHHHH")[elf_class]
- section_struct = byte_order + ("4xI8xIII8xI", "4xI16xQQI12xQ")[elf_class]
- dynamic_struct = byte_order + ("iI", "qQ")[elf_class]
- type, machine, version, entry, phoff, shoff, flags, ehsize, phentsize, phnum, shentsize, shnum, shstrndx \
- = struct.unpack(header_struct, elf.read(struct.calcsize(header_struct)))
- dynamic_sections = []
- string_tables = {}
- # Seek to the section header table and find the .dynamic section.
- elf.seek(shoff)
- for i in range(shnum):
- type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
- if type == 6 and link != 0: # DYNAMIC type, links to string table
- dynamic_sections.append((offset, size, link, entsize))
- string_tables[link] = None
- # Read the relevant string tables.
- for idx in string_tables.keys():
- elf.seek(shoff + idx * shentsize)
- type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
- if type != 3:
- continue
- elf.seek(offset)
- string_tables[idx] = elf.read(size)
- # Loop through the dynamic sections and rewrite it if it has an rpath/runpath.
- needed = []
- rpath = []
- for offset, size, link, entsize in dynamic_sections:
- elf.seek(offset)
- data = elf.read(entsize)
- tag, val = struct.unpack_from(dynamic_struct, data)
- # Read tags until we find a NULL tag.
- while tag != 0:
- if tag == 1: # A NEEDED entry. Read it from the string table.
- string = string_tables[link][val : string_tables[link].find(b'\0', val)]
- needed.append(string.decode('utf-8'))
- elif tag == 15 or tag == 29:
- # An RPATH or RUNPATH entry.
- string = string_tables[link][val : string_tables[link].find(b'\0', val)]
- rpath += [
- os.path.normpath(i.decode('utf-8').replace('$ORIGIN', origin))
- for i in string.split(b':')
- ]
- data = elf.read(entsize)
- tag, val = struct.unpack_from(dynamic_struct, data)
- elf.close()
- search_path += rpath
- return needed
- def _read_dependencies_macho(self, fp, endian, flatten=False):
- """ Having read the first 4 bytes of the Mach-O file, fetches the
- dependent libraries and returns those as a list.
- If flatten is True, if the dependencies contain paths like
- @loader_path/../.dylibs/libsomething.dylib, it will rewrite them to
- instead contain @loader_path/libsomething.dylib if possible.
- This requires the file pointer to be opened in rb+ mode. """
- cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags = \
- struct.unpack(endian + 'IIIIII', fp.read(24))
- is_64bit = (cputype & 0x1000000) != 0
- if is_64bit:
- fp.read(4)
- # After the header, we get a series of linker commands. We just
- # iterate through them and gather up the LC_LOAD_DYLIB commands.
- load_dylibs = []
- for i in range(ncmds):
- cmd, cmd_size = struct.unpack(endian + 'II', fp.read(8))
- cmd_data = fp.read(cmd_size - 8)
- cmd &= ~0x80000000
- if cmd == 0x0c: # LC_LOAD_DYLIB
- dylib = cmd_data[16:].decode('ascii').split('\x00', 1)[0]
- orig = dylib
- if dylib.startswith('@loader_path/../Frameworks/'):
- dylib = dylib.replace('@loader_path/../Frameworks/', '')
- elif dylib.startswith('@executable_path/../Frameworks/'):
- dylib = dylib.replace('@executable_path/../Frameworks/', '')
- else:
- for prefix in ('@loader_path/', '@rpath/'):
- if dylib.startswith(prefix):
- dylib = dylib.replace(prefix, '')
- # Do we need to flatten the relative reference?
- if '/' in dylib and flatten:
- new_dylib = prefix + os.path.basename(dylib)
- str_size = len(cmd_data) - 16
- if len(new_dylib) < str_size:
- fp.seek(-str_size, os.SEEK_CUR)
- fp.write(new_dylib.encode('ascii').ljust(str_size, b'\0'))
- else:
- self.warn('Unable to rewrite dependency {}'.format(orig))
- load_dylibs.append(dylib)
- return load_dylibs
- def _read_dependencies_fat(self, fp, is_64bit, flatten=False):
- num_fat, = struct.unpack('>I', fp.read(4))
- # After the header we get a table of executables in this fat file,
- # each one with a corresponding offset into the file.
- offsets = []
- for i in range(num_fat):
- if is_64bit:
- cputype, cpusubtype, offset, size, align = \
- struct.unpack('>QQQQQ', fp.read(40))
- else:
- cputype, cpusubtype, offset, size, align = \
- struct.unpack('>IIIII', fp.read(20))
- offsets.append(offset)
- # Go through each of the binaries in the fat file.
- deps = []
- for offset in offsets:
- # Add 4, since it expects we've already read the magic.
- fp.seek(offset)
- magic = fp.read(4)
- if magic in (b'\xCE\xFA\xED\xFE', b'\xCF\xFA\xED\xFE'):
- endian = '<'
- elif magic in (b'\xFE\xED\xFA\xCE', b'\xFE\xED\xFA\xCF'):
- endian = '>'
- else:
- # Not a Mach-O file we can read.
- continue
- for dep in self._read_dependencies_macho(fp, endian, flatten=flatten):
- if dep not in deps:
- deps.append(dep)
- return deps
- def expand_path(self, path, platform):
- "Substitutes variables in the given path string."
- if path is None:
- return None
- t = string.Template(path)
- if platform.startswith('win'):
- return t.substitute(HOME='~', USER_APPDATA='~/AppData/Local')
- elif platform.startswith('macosx'):
- return t.substitute(HOME='~', USER_APPDATA='~/Documents')
- else:
- return t.substitute(HOME='~', USER_APPDATA='~/.local/share')
- class bdist_apps(setuptools.Command):
- DEFAULT_INSTALLERS = {
- 'manylinux1_x86_64': ['gztar'],
- 'manylinux1_i686': ['gztar'],
- 'manylinux2010_x86_64': ['gztar'],
- 'manylinux2010_i686': ['gztar'],
- 'manylinux2014_x86_64': ['gztar'],
- 'manylinux2014_i686': ['gztar'],
- 'manylinux2014_aarch64': ['gztar'],
- 'manylinux2014_armv7l': ['gztar'],
- 'manylinux2014_ppc64': ['gztar'],
- 'manylinux2014_ppc64le': ['gztar'],
- 'manylinux2014_s390x': ['gztar'],
- 'manylinux_2_24_x86_64': ['gztar'],
- 'manylinux_2_24_i686': ['gztar'],
- 'manylinux_2_24_aarch64': ['gztar'],
- 'manylinux_2_24_armv7l': ['gztar'],
- 'manylinux_2_24_ppc64': ['gztar'],
- 'manylinux_2_24_ppc64le': ['gztar'],
- 'manylinux_2_24_s390x': ['gztar'],
- 'manylinux_2_28_x86_64': ['gztar'],
- 'manylinux_2_28_aarch64': ['gztar'],
- 'manylinux_2_28_ppc64le': ['gztar'],
- 'manylinux_2_28_s390x': ['gztar'],
- 'android': ['aab'],
- # Everything else defaults to ['zip']
- }
- DEFAULT_INSTALLER_FUNCS = {
- 'zip': installers.create_zip,
- 'gztar': installers.create_gztar,
- 'bztar': installers.create_bztar,
- 'xztar': installers.create_xztar,
- 'nsis': installers.create_nsis,
- 'aab': installers.create_aab,
- }
- description = 'bundle built Panda3D applications into distributable forms'
- user_options = build_apps.user_options + [
- ('dist-dir=', 'd', 'directory to put final built distributions in'),
- ('skip-build', None, 'skip rebuilding everything (for testing/debugging)'),
- ]
- def _build_apps_options(self):
- return [opt[0].replace('-', '_').replace('=', '') for opt in build_apps.user_options]
- def initialize_options(self):
- self.installers = {}
- self.dist_dir = os.path.join(os.getcwd(), 'dist')
- self.skip_build = False
- self.signing_certificate = None
- self.signing_private_key = None
- self.signing_passphrase = None
- self.installer_functions = {}
- self._current_platform = None
- for opt in self._build_apps_options():
- setattr(self, opt, None)
- def finalize_options(self):
- from importlib.metadata import entry_points
- # We need to massage the inputs a bit in case they came from a
- # setup.cfg file.
- self.installers = {
- key: _parse_list(value)
- for key, value in _parse_dict(self.installers).items()
- }
- if self.signing_certificate:
- assert self.signing_private_key, 'Missing signing_private_key'
- self.signing_certificate = os.path.abspath(self.signing_certificate)
- self.signing_private_key = os.path.abspath(self.signing_private_key)
- eps = entry_points()
- if isinstance(eps, dict): # Python 3.8 and 3.9
- installer_eps = eps.get('panda3d.bdist_apps.installers', ())
- else:
- installer_eps = eps.select(group='panda3d.bdist_apps.installers')
- tmp = self.DEFAULT_INSTALLER_FUNCS.copy()
- tmp.update(self.installer_functions)
- tmp.update({
- entrypoint.name: entrypoint.load()
- for entrypoint in installer_eps
- })
- self.installer_functions = tmp
- def get_archive_basedir(self):
- return self.distribution.get_name()
- def get_current_platform(self):
- return self._current_platform
- def run(self):
- build_cmd = self.distribution.get_command_obj('build_apps')
- for opt in self._build_apps_options():
- optval = getattr(self, opt)
- if optval is not None:
- setattr(build_cmd, opt, optval)
- if not self.skip_build:
- self.run_command('build_apps')
- else:
- build_cmd.finalize_options()
- platforms = build_cmd.platforms
- build_base = os.path.abspath(build_cmd.build_base)
- if not os.path.exists(self.dist_dir):
- os.makedirs(self.dist_dir)
- os.chdir(self.dist_dir)
- for platform in platforms:
- build_dir = os.path.join(build_base, platform)
- basename = '{}_{}'.format(self.distribution.get_fullname(), platform)
- installers = self.installers.get(platform, self.DEFAULT_INSTALLERS.get(platform, ['zip']))
- self._current_platform = platform
- for installer in installers:
- self.announce('\nBuilding {} for platform: {}'.format(installer, platform), distutils.log.INFO)
- if installer not in self.installer_functions:
- self.announce(
- '\tUnknown installer: {}'.format(installer),
- distutils.log.ERROR
- )
- continue
- self.installer_functions[installer](self, basename, build_dir)
|