Browse Source

bdist_apps: Support user-defined installers

Custom installers can be added by specifying a build function in
bdist_apps options:

'installer_functions': {
    'custom': custom_func
}

Installer functions must accept three arguments:

  * command - the bdist_apps setuptools command function (can be used to
        gather build information)
  * basename - name of the output file (minus extension)
  * build_dir - path to the directory containing the built application
Mitchell Stokes 5 years ago
parent
commit
d015b7e2f1
2 changed files with 199 additions and 165 deletions
  1. 26 165
      direct/src/dist/commands.py
  2. 173 0
      direct/src/dist/installers.py

+ 26 - 165
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):
-        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()
+        tmp = self.DEFAULT_INSTALLER_FUNCS.copy()
+        tmp.update(self.installer_functions)
+        self.installer_functions = tmp
 
-        # Create the .nsi installer script
-        nsifile = p3d.Filename(build_cmd.build_base, shortname + ".nsi")
-        nsifile.unlink()
-        nsi = open(nsifile.to_os_specific(), "w")
+    def get_archive_basedir(self):
+        return self.distribution.get_name()
 
-        # 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)

+ 173 - 0
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)
+