Source code for direct.dist.commands

"""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.
"""

from __future__ import print_function

import os
import plistlib
import sys
import subprocess
import zipfile
import re
import shutil
import stat
import struct
import string
import time
import tempfile

import setuptools
import distutils.log

from . import FreezeTool
from . import pefile
from .icon import Icon
from ._dist_hooks import finalize_distribution_options
import panda3d.core as p3d


if 'basestring' not in globals():
    basestring = str


if sys.version_info < (3, 0):
    # Python 3 defines these subtypes of IOError, but Python 2 doesn't.
    FileNotFoundError = IOError

    # Warn the user.  They might be using Python 2 by accident.
    print("=================================================================")
    print("WARNING: You are using Python 2, which has reached the end of its")
    print("WARNING: life as of January 1, 2020.  Please upgrade to Python 3.")
    print("=================================================================")
    sys.stdout.flush()
    time.sleep(4.0)


def _parse_list(input):
    if isinstance(input, basestring):
        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

    try:
        import pkg_resources
    except ImportError:
        return

    registry = p3d.LoaderFileTypeRegistry.getGlobalPtr()

    for entry_point in pkg_resources.iter_entry_points('panda3d.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)
    dst_fn.set_binary()

    _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))

    stream = p3d.OFileStream()
    if not dst_fn.open_write(stream):
        raise IOError('Failed to open .bam file for writing: %s' % (dstpath))

    # We pass it the source filename here so that texture files are made
    # relative to the original pathname and don't point from the destination
    # back into the source directory.
    dout = p3d.DatagramOutputFile()
    if not dout.open(stream, src_fn) or not dout.write_header("pbj\0\n\r"):
        raise IOError('Failed to write to .bam file: %s' % (dstpath))

    writer = p3d.BamWriter(dout)
    writer.root_node = node
    writer.init()
    if _build_cmd.bam_embed_textures:
        writer.set_file_texture_mode(p3d.BamEnums.BTM_rawdata)
    else:
        writer.set_file_texture_mode(p3d.BamEnums.BTM_relative)
    writer.write_object(node)
    writer.flush()
    writer = None
    dout.close()
    dout = None
    stream.close()


[docs]def egg2bam(_build_cmd, srcpath, dstpath): if dstpath.endswith('.gz') or dstpath.endswith('.pz'): dstpath = dstpath[:-3] dstpath = dstpath + '.bam' try: subprocess.check_call([ 'egg2bam', '-o', dstpath, '-pd', os.path.dirname(os.path.abspath(srcpath)), '-ps', 'rel', srcpath ]) except FileNotFoundError: raise RuntimeError('egg2bam failed: egg2bam was not found in the PATH') except (subprocess.CalledProcessError, OSError) as err: raise RuntimeError('egg2bam failed: {}'.format(err)) return 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 for Python 2. SITE_PY2 = u""" import sys sys.frozen = True # Override __import__ to set __file__ for frozen modules. prev_import = __import__ def __import__(*args, **kwargs): mod = prev_import(*args, **kwargs) if mod: mod.__file__ = sys.executable return mod # Add our custom __import__ version to the global scope, as well as a builtin # definition for __file__ so that it is available in the module itself. import __builtin__ __builtin__.__import__ = __import__ __builtin__.__file__ = sys.executable del __builtin__ """ # site.py for Python 3. SITE_PY3 = u""" 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 """ # This addendum is only needed for legacy tkinter handling, since the new # tkinter package already contains this logic. SITE_PY_TKINTER_ADDENDUM = """ # Set the TCL_LIBRARY directory to the location of the Tcl/Tk/Tix files. import os tcl_dir = os.path.join(os.path.dirname(sys.executable), 'tcl') if os.path.isdir(tcl_dir): for dir in os.listdir(tcl_dir): sub_dir = os.path.join(tcl_dir, dir) if os.path.isdir(sub_dir): if dir.startswith('tcl') and os.path.isfile(os.path.join(sub_dir, 'init.tcl')): os.environ['TCL_LIBRARY'] = sub_dir if dir.startswith('tk'): os.environ['TK_LIBRARY'] = sub_dir if dir.startswith('tix'): os.environ['TIX_LIBRARY'] = sub_dir del os """ SITE_PY = SITE_PY3 if sys.version_info >= (3,) else SITE_PY2
[docs]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 = { '.egg': egg2bam, }
[docs] def initialize_options(self): self.build_base = os.path.join(os.getcwd(), 'build') 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 = [ 'manylinux1_x86_64', 'macosx_10_6_x86_64', 'win_amd64', ] if sys.version_info >= (3, 11): # manylinux2010 is not offered for Python 3.11 anymore self.platforms[0] = 'manylinux2014_x86_64' elif sys.version_info >= (3, 10): # manylinux1 is not offered for Python 3.10 anymore self.platforms[0] = 'manylinux2010_x86_64' if sys.version_info >= (3, 13): # This version of Python is only available for 10.13+. self.platforms[1] = 'macosx_10_13_x86_64' elif sys.version_info >= (3, 8): # This version of Python is only available for 10.9+. self.platforms[1] = 'macosx_10_9_x86_64' 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 = False 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 = [] self.bam_embed_textures = False 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', # manylinux1/linux 'libdl.so.*', 'libstdc++.so.*', 'libm.so.*', 'libgcc_s.so.*', 'libpthread.so.*', 'libc.so.*', 'ld-linux-x86-64.so.*', 'ld-linux-aarch64.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/**', ] if sys.version_info >= (3, 5): # Python 3.5+ requires at least Windows Vista to run anyway, so we # shouldn't warn about DLLs that are shipped with Vista. self.exclude_dependencies += ['bcrypt.dll'] self.package_data_dirs = {} # 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
[docs] 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) 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 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
[docs] def run(self): self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO) for platform in self.platforms: self.build_runtimes(platform, True)
[docs] 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('.')[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' # For these distributions, we need to append 'u' on Linux if abi_tag in ('cp26m', 'cp27m', 'cp32m') and not platform.startswith('win') and not platform.startswith('macosx'): abi_tag += 'u' 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:] if sys.version_info >= (3, 11): pip_args += ['--platform', 'manylinux2014_' + arch] elif sys.version_info >= (3, 10): pip_args += ['--platform', 'manylinux2010_' + arch] else: pip_args += ['--platform', 'manylinux1_' + 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('macosx_10_9_') and sys.version_info >= (3, 13): new_platform = platform.replace('macosx_10_9_', 'macosx_10_13_') self.announce('This error likely occurs because {} is not a supported target as of Python 3.13.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR) elif 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): if sys.version_info >= (3, 13): new_platform = platform.replace('macosx_10_6_', 'macosx_10_13_') else: 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') ]
[docs] 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()
[docs] 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, } 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: if hasattr(plistlib, 'dump'): plistlib.dump(plist, f) else: plistlib.writePlist(plist, f)
[docs] def build_runtimes(self, platform, use_wheels): """ Builds the distributions for the given platform. """ builddir = os.path.join(self.build_base, platform) if os.path.exists(builddir): shutil.rmtree(builddir) os.makedirs(builddir) 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') etcfiles = os.listdir(etcdir) etcfiles.sort(reverse=True) for fn in etcfiles: if fn.lower().endswith('.prc'): with open(os.path.join(etcdir, fn)) as f: prcstring += f.read() else: etcfiles = [i for i in p3dwhl.namelist() if i.endswith('.prc')] etcfiles.sort(reverse=True) for fn in etcfiles: 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: 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(builddir, 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() freezer_modpaths = 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(appname, mainscript, use_console): site_py = SITE_PY if not has_tkinter_wheel: # Legacy handling for Tcl data files site_py += SITE_PY_TKINTER_ADDENDUM optimize = 2 if self.strip_docstrings else 1 freezer = FreezeTool.Freezer(platform=platform, path=path, optimize=optimize) freezer.addModule('__main__', filename=mainscript) 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) target_path = os.path.join(builddir, appname) stub_name = 'deploy-stub' if platform.startswith('win') or 'macosx' in platform: if not use_console: stub_name = 'deploy-stubw' if platform.startswith('win'): stub_name += '.exe' target_path += '.exe' if use_wheels: 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 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, self.log_filename_strftime) stub_file.close() if temp_file: os.unlink(temp_file.name) # Copy the dependencies. search_path = [builddir] if use_wheels: search_path.append(os.path.join(p3dwhlfn, 'deploy_libs')) self.copy_dependencies(target_path, builddir, search_path, stub_name) freezer_extras.update(freezer.extras) freezer_modules.update(freezer.getAllModuleNames()) freezer_modpaths.update({ mod[1].filename.to_os_specific() for mod in freezer.getModuleDefs() if mod[1].filename }) for suffix in freezer.moduleSuffixes: if suffix[2] == 3: # imp.C_EXTENSION: ext_suffixes.add(suffix[0]) for appname, scriptname in self.gui_apps.items(): create_runtime(appname, scriptname, False) for appname, scriptname in self.console_apps.items(): create_runtime(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: # The on-windows-for-windows case is handled as legacy below if sys.platform != "win32" or not platform.startswith('win'): self.warn("Detected use of tkinter, but tkinter is not specified in requirements.txt!") # Copy extension modules whl_modules = [] whl_modules_ext = '' if use_wheels: # Get the module libs whl_modules = [] 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.append(module) whl_modules_ext = ext # 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(builddir, 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 if sys.version_info >= (3, 0): 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, 'deploy_libs/{}.{}'.format(module, whl_modules_ext))#'{0}/deploy_libs/{1}.{2}'.format(p3dwhlfn, module, whl_modules_ext) basename = os.path.basename(source_path) #XXX should we remove python version string here too? else: continue # If this is a dynamic library, search for dependencies. target_path = os.path.join(builddir, basename) search_path = get_search_path_for(source_path) self.copy_with_dependencies(source_path, target_path, search_path) # Copy over the tcl directory. # This is legacy, we nowadays recommend the separate tkinter wheel. if sys.platform == "win32" and platform.startswith('win') and not has_tkinter_wheel: tcl_dir = os.path.join(sys.prefix, 'tcl') tkinter_name = 'tkinter' if sys.version_info >= (3, 0) else 'Tkinter' if os.path.isdir(tcl_dir) and tkinter_name in freezer_modules: self.announce('Copying Tcl files', distutils.log.INFO) os.makedirs(os.path.join(builddir, 'tcl')) for dir in os.listdir(tcl_dir): sub_dir = os.path.join(tcl_dir, dir) if os.path.isdir(sub_dir): target_dir = os.path.join(builddir, 'tcl', dir) self.announce('copying {0} -> {1}'.format(sub_dir, target_dir)) shutil.copytree(sub_dir, target_dir) # Extract any other data files from dependency packages. 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(builddir, 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) # Copy Game Files self.announce('Copying game files for platform: {}'.format(platform), distutils.log.INFO) ignore_copy_list = [ '**/__pycache__/**', '**/*.pyc', '{}/**'.format(self.build_base), ] ignore_copy_list += self.exclude_patterns ignore_copy_list += freezer_modpaths 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(builddir, update_path(src)) copy_file(src, dst) # Bundle into an .app on macOS if self.macos_main_app and 'macosx' in platform: self.bundle_macos_app(builddir)
[docs] 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()))
[docs] 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)
[docs] 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)
[docs] 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
[docs] 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')
[docs]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'], # Everything else defaults to ['zip'] } 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]
[docs] def initialize_options(self): self.installers = {} self.dist_dir = os.path.join(os.getcwd(), 'dist') self.skip_build = False for opt in self._build_apps_options(): setattr(self, opt, None)
[docs] def finalize_options(self): # 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() }
def _get_archive_basedir(self): return self.distribution.get_name()
[docs] def create_zip(self, basename, build_dir): import zipfile base_dir = self._get_archive_basedir() with zipfile.ZipFile(basename+'.zip', 'w', compression=zipfile.ZIP_DEFLATED) as zf: zf.write(build_dir, base_dir) for dirpath, dirnames, filenames in os.walk(build_dir): dirnames.sort() for name in dirnames: path = os.path.normpath(os.path.join(dirpath, name)) zf.write(path, path.replace(build_dir, base_dir, 1)) for name in filenames: path = os.path.normpath(os.path.join(dirpath, name)) if os.path.isfile(path): zf.write(path, path.replace(build_dir, base_dir, 1))
[docs] def create_tarball(self, basename, build_dir, tar_compression): import tarfile base_dir = self._get_archive_basedir() build_cmd = self.get_finalized_command('build_apps') binary_names = list(build_cmd.console_apps.keys()) + list(build_cmd.gui_apps.keys()) source_date = os.environ.get('SOURCE_DATE_EPOCH', '').strip() if source_date: max_mtime = int(source_date) else: max_mtime = None def tarfilter(tarinfo): if tarinfo.isdir() or os.path.basename(tarinfo.name) in binary_names: tarinfo.mode = 0o755 else: tarinfo.mode = 0o644 # This isn't interesting information to retain for distribution. tarinfo.uid = 0 tarinfo.gid = 0 tarinfo.uname = "" tarinfo.gname = "" if max_mtime is not None and tarinfo.mtime >= max_mtime: tarinfo.mtime = max_mtime return tarinfo filename = '{}.tar.{}'.format(basename, tar_compression) with tarfile.open(filename, 'w|{}'.format(tar_compression)) as tf: tf.add(build_dir, base_dir, filter=tarfilter) if tar_compression == 'gz' and max_mtime is not None: # Python provides no elegant way to overwrite the gzip timestamp. with open(filename, 'r+b') as fp: fp.seek(4) fp.write(struct.pack("<L", max_mtime))
[docs] def create_nsis(self, basename, build_dir, is_64bit): # Get a list of build applications build_cmd = self.get_finalized_command('build_apps') apps = build_cmd.gui_apps.copy() apps.update(build_cmd.console_apps) apps = [ '{}.exe'.format(i) for i in apps ] fullname = self.distribution.get_fullname() shortname = self.distribution.get_name() # Create the .nsi installer script nsifile = p3d.Filename(build_cmd.build_base, shortname + ".nsi") nsifile.unlink() nsi = open(nsifile.to_os_specific(), "w") # Some global info nsi.write('Name "%s"\n' % shortname) nsi.write('OutFile "%s"\n' % (fullname+'.exe')) if is_64bit: nsi.write('InstallDir "$PROGRAMFILES64\\%s"\n' % shortname) else: nsi.write('InstallDir "$PROGRAMFILES\\%s"\n' % shortname) nsi.write('SetCompress auto\n') nsi.write('SetCompressor lzma\n') nsi.write('ShowInstDetails nevershow\n') nsi.write('ShowUninstDetails nevershow\n') nsi.write('InstType "Typical"\n') # Tell Vista that we require admin rights nsi.write('RequestExecutionLevel admin\n') nsi.write('\n') # TODO offer run and desktop shortcut after we figure out how to deal # with multiple apps nsi.write('!include "MUI2.nsh"\n') nsi.write('!define MUI_ABORTWARNING\n') nsi.write('\n') nsi.write('Var StartMenuFolder\n') nsi.write('!insertmacro MUI_PAGE_WELCOME\n') # TODO license file nsi.write('!insertmacro MUI_PAGE_DIRECTORY\n') nsi.write('!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder\n') nsi.write('!insertmacro MUI_PAGE_INSTFILES\n') nsi.write('!insertmacro MUI_PAGE_FINISH\n') nsi.write('!insertmacro MUI_UNPAGE_WELCOME\n') nsi.write('!insertmacro MUI_UNPAGE_CONFIRM\n') nsi.write('!insertmacro MUI_UNPAGE_INSTFILES\n') nsi.write('!insertmacro MUI_UNPAGE_FINISH\n') nsi.write('!insertmacro MUI_LANGUAGE "English"\n') # This section defines the installer. nsi.write('Section "" SecCore\n') nsi.write(' SetOutPath "$INSTDIR"\n') curdir = "" nsi_dir = p3d.Filename.fromOsSpecific(build_cmd.build_base) build_root_dir = p3d.Filename.fromOsSpecific(build_dir) for root, dirs, files in os.walk(build_dir): dirs.sort() for name in files: basefile = p3d.Filename.fromOsSpecific(os.path.join(root, name)) file = p3d.Filename(basefile) file.makeAbsolute() file.makeRelativeTo(nsi_dir) outdir = p3d.Filename(basefile) outdir.makeAbsolute() outdir.makeRelativeTo(build_root_dir) outdir = outdir.getDirname().replace('/', '\\') if curdir != outdir: nsi.write(' SetOutPath "$INSTDIR\\%s"\n' % outdir) curdir = outdir nsi.write(' File "%s"\n' % (file.toOsSpecific())) nsi.write(' SetOutPath "$INSTDIR"\n') nsi.write(' WriteUninstaller "$INSTDIR\\Uninstall.exe"\n') nsi.write(' ; Start menu items\n') nsi.write(' !insertmacro MUI_STARTMENU_WRITE_BEGIN Application\n') nsi.write(' CreateDirectory "$SMPROGRAMS\\$StartMenuFolder"\n') for app in apps: nsi.write(' CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\%s.lnk" "$INSTDIR\\%s"\n' % (shortname, app)) nsi.write(' CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\Uninstall.lnk" "$INSTDIR\\Uninstall.exe"\n') nsi.write(' !insertmacro MUI_STARTMENU_WRITE_END\n') nsi.write('SectionEnd\n') # This section defines the uninstaller. nsi.write('Section Uninstall\n') nsi.write(' RMDir /r "$INSTDIR"\n') nsi.write(' ; Desktop icon\n') nsi.write(' Delete "$DESKTOP\\%s.lnk"\n' % shortname) nsi.write(' ; Start menu items\n') nsi.write(' !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder\n') nsi.write(' RMDir /r "$SMPROGRAMS\\$StartMenuFolder"\n') nsi.write('SectionEnd\n') nsi.close() cmd = ['makensis'] for flag in ["V2"]: cmd.append( '{}{}'.format('/' if sys.platform.startswith('win') else '-', flag) ) cmd.append(nsifile.to_os_specific()) subprocess.check_call(cmd)
[docs] 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) build_cmd.finalize_options() if not self.skip_build: self.run_command('build_apps') 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'])) for installer in installers: self.announce('\nBuilding {} for platform: {}'.format(installer, platform), distutils.log.INFO) if installer == 'zip': self.create_zip(basename, build_dir) elif installer in ('gztar', 'bztar', 'xztar'): compress = installer.replace('tar', '') if compress == 'bz': compress = 'bz2' self.create_tarball(basename, build_dir, compress) elif installer == 'nsis': if not platform.startswith('win'): self.announce( '\tNSIS installer not supported for platform: {}'.format(platform), distutils.log.ERROR ) continue try: subprocess.call(['makensis', '--version']) except OSError: self.announce( '\tCould not find makensis tool that is required to build NSIS installers', distutils.log.ERROR ) # continue is_64bit = platform == 'win_amd64' self.create_nsis(basename, build_dir, is_64bit) else: self.announce('\tUnknown installer: {}'.format(installer), distutils.log.ERROR)