diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index c74bc2f446..4f38c5642a 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -24,6 +24,7 @@ import distutils.log from . import FreezeTool from . import pefile +from . import installers from .icon import Icon import panda3d.core as p3d @@ -1313,6 +1314,14 @@ class bdist_apps(setuptools.Command): # 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, + } + description = 'bundle built Panda3D applications into distributable forms' user_options = build_apps.user_options + [ ('dist-dir=', 'd', 'directory to put final built distributions in'), @@ -1326,6 +1335,8 @@ class bdist_apps(setuptools.Command): self.installers = {} self.dist_dir = os.path.join(os.getcwd(), 'dist') self.skip_build = False + self.installer_functions = {} + self._current_platform = None for opt in self._build_apps_options(): setattr(self, opt, None) @@ -1337,145 +1348,15 @@ class bdist_apps(setuptools.Command): for key, value in _parse_dict(self.installers).items() } - def _get_archive_basedir(self): + tmp = self.DEFAULT_INSTALLER_FUNCS.copy() + tmp.update(self.installer_functions) + self.installer_functions = tmp + + def get_archive_basedir(self): return self.distribution.get_name() - 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): - for name in sorted(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)) - - 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()) - - def tarfilter(tarinfo): - if tarinfo.isdir() or os.path.basename(tarinfo.name) in binary_names: - tarinfo.mode = 0o755 - else: - tarinfo.mode = 0o644 - return tarinfo - - with tarfile.open('{}.tar.{}'.format(basename, tar_compression), 'w|{}'.format(tar_compression)) as tf: - tf.add(build_dir, base_dir, filter=tarfilter) - - 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 - ] - - 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' % os.path.join(self.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): - 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) + def get_current_platform(self): + return self._current_platform def run(self): build_cmd = self.distribution.get_command_obj('build_apps') @@ -1497,35 +1378,15 @@ class bdist_apps(setuptools.Command): 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 - 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) + self.installer_functions[installer](self, basename, build_dir) diff --git a/direct/src/dist/installers.py b/direct/src/dist/installers.py new file mode 100644 index 0000000000..3d84fd4306 --- /dev/null +++ b/direct/src/dist/installers.py @@ -0,0 +1,173 @@ +import distutils.log +import os +import subprocess +import sys +import tarfile +import zipfile + +import panda3d.core as p3d + +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): + for name in sorted(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)) + + +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()) + + def tarfilter(tarinfo): + if tarinfo.isdir() or os.path.basename(tarinfo.name) in binary_names: + tarinfo.mode = 0o755 + else: + tarinfo.mode = 0o644 + return tarinfo + + with tarfile.open('{}.tar.{}'.format(basename, tar_compression), 'w|{}'.format(tar_compression)) as tf: + tf.add(build_dir, base_dir, filter=tarfilter) + + +def create_gztar(command, basename, build_dir): + return create_tarball(command, basename, build_dir, 'gz') + + +def create_bztar(command, basename, build_dir): + return create_tarball(command, basename, build_dir, 'bz2') + + +def create_xztar(command, basename, build_dir): + return create_tarball(command, basename, build_dir, 'xz') + + +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): + 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) +