Source code for direct.dist.installers

import distutils.log
import os
import subprocess
import sys
import tarfile
import zipfile
import struct

import panda3d.core as p3d

[docs]def create_zip(command, basename, build_dir): base_dir = command.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(command, basename, build_dir, tar_compression): base_dir = command.get_archive_basedir() build_cmd = command.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_gztar(command, basename, build_dir): return create_tarball(command, basename, build_dir, 'gz')
[docs]def create_bztar(command, basename, build_dir): return create_tarball(command, basename, build_dir, 'bz2')
[docs]def create_xztar(command, basename, build_dir): return create_tarball(command, basename, build_dir, 'xz')
[docs]def create_nsis(command, basename, build_dir): platform = command.get_current_platform() if not platform.startswith('win'): command.announce( '\tNSIS installer not supported for platform: {}'.format(platform), distutils.log.ERROR ) return try: subprocess.call(['makensis', '--version']) except OSError: command.announce( '\tCould not find makensis tool that is required to build NSIS installers', distutils.log.ERROR ) return is_64bit = platform == 'win_amd64' # Get a list of build applications build_cmd = command.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 ] shortname = command.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' % os.path.join(command.dist_dir, basename+'.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 create_aab(command, basename, build_dir): """Create an Android App Bundle. This is a newer format that replaces Android's .apk format for uploads to the Play Store. Unlike .apk files, it does not rely on a proprietary signing scheme or an undocumented binary XML format (protobuf is used instead), so it is easier to create without requiring external tools. If desired, it is possible to install bundletool and use it to convert an .aab into an .apk. """ from ._android import AndroidManifest, AbiAlias, BundleConfig, NativeLibraries, ResourceTable bundle_fn = p3d.Filename.from_os_specific(command.dist_dir) / (basename + '.aab') build_dir_fn = p3d.Filename.from_os_specific(build_dir) # Convert the AndroidManifest.xml file to a protobuf-encoded version of it. axml = AndroidManifest() with open(os.path.join(build_dir, 'AndroidManifest.xml'), 'rb') as fh: axml.parse_xml(fh.read()) # We use our own zip implementation, which can create the correct # alignment and signature needed by Android automatically. bundle_fn.unlink() bundle = p3d.ZipArchive() if not bundle.open_read_write(bundle_fn): command.announce( f'\tUnable to open {bundle_fn} for writing', distutils.log.ERROR) return config = BundleConfig() config.bundletool.version = '1.1.0' config.optimizations.splits_config.Clear() config.optimizations.uncompress_native_libraries.enabled = False bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9) resources = ResourceTable() package = resources.package.add() package.package_id.id = 0x7f for attrib in axml.root.element.attribute: if attrib.name == 'package': package.package_name = attrib.value # Were there any icons referenced in the AndroidManifest.xml? for type_i, type_name in enumerate(axml.resource_types): res_type = package.type.add() res_type.name = type_name res_type.type_id.id = type_i + 1 for entry_id, res_name in enumerate(axml.resources[type_name]): entry = res_type.entry.add() entry.entry_id.id = entry_id entry.name = res_name for density, tag in (160, 'mdpi'), (240, 'hdpi'), (320, 'xhdpi'), (480, 'xxhdpi'), (640, 'xxxhdpi'): path = f'res/mipmap-{tag}-v4/{res_name}.png' if (build_dir_fn / path).exists(): bundle.add_subfile('base/' + path, build_dir_fn / path, 0) config_value = entry.config_value.add() config_value.config.density = density config_value.value.item.file.path = path bundle.add_subfile('base/resources.pb', p3d.StringStream(resources.SerializeToString()), 9) native = NativeLibraries() for abi in os.listdir(os.path.join(build_dir, 'lib')): native_dir = native.directory.add() native_dir.path = 'lib/' + abi native_dir.targeting.abi.alias = getattr(AbiAlias, abi.upper().replace('-', '_')) bundle.add_subfile('base/native.pb', p3d.StringStream(native.SerializeToString()), 9) bundle.add_subfile('base/manifest/AndroidManifest.xml', p3d.StringStream(axml.dumps()), 9) # Add the classes.dex. bundle.add_subfile(f'base/dex/classes.dex', build_dir_fn / 'classes.dex', 9) # Add libraries, compressed. for abi in os.listdir(os.path.join(build_dir, 'lib')): abi_dir = os.path.join(build_dir, 'lib', abi) for lib in os.listdir(abi_dir): if lib.startswith('lib') and lib.endswith('.so'): bundle.add_subfile(f'base/lib/{abi}/{lib}', build_dir_fn / 'lib' / abi / lib, 9) # Add assets, compressed. assets_dir = os.path.join(build_dir, 'assets') for dirpath, dirnames, filenames in os.walk(assets_dir): rel_dirpath = os.path.relpath(dirpath, build_dir).replace('\\', '/') dirnames.sort() filenames.sort() for name in filenames: fn = p3d.Filename.from_os_specific(dirpath) / name if fn.is_regular_file(): bundle.add_subfile(f'base/{rel_dirpath}/{name}', fn, 9) # Finally, generate the manifest file / signature, if a signing certificate # has been specified. if command.signing_certificate: password = command.signing_passphrase or '' if not password and 'ENCRYPTED' in open(command.signing_private_key).read(): # It appears to be encrypted, and we don't have a passphrase, so we # must request it on the command-line. from getpass import getpass password = getpass(f'Enter pass phrase for private key: ') if not bundle.add_jar_signature( p3d.Filename.from_os_specific(command.signing_certificate), p3d.Filename.from_os_specific(command.signing_private_key), password): command.announce( f'\tFailed to sign {bundle_fn}.', distutils.log.ERROR) bundle.close()