1
0

__init__.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. """Build NSIS installers for Python applications.
  2. """
  3. import logging
  4. import os
  5. import shutil
  6. from subprocess import check_output, call
  7. import sys
  8. from urllib.request import urlretrieve
  9. from .copymodules import copy_modules
  10. from .nsiswriter import NSISFileWriter
  11. pjoin = os.path.join
  12. logger = logging.getLogger(__name__)
  13. _PKGDIR = os.path.abspath(os.path.dirname(__file__))
  14. DEFAULT_PY_VERSION = '3.3.2'
  15. DEFAULT_BUILD_DIR = pjoin('build', 'nsis')
  16. DEFAULT_NSI_TEMPLATE = pjoin(_PKGDIR, 'template.nsi')
  17. DEFAULT_ICON = pjoin(_PKGDIR, 'glossyorb.ico')
  18. if os.name == 'nt' and sys.maxsize == (2**63)-1:
  19. DEFAULT_BITNESS = 64
  20. else:
  21. DEFAULT_BITNESS = 32
  22. def fetch_python(version=DEFAULT_PY_VERSION, bitness=DEFAULT_BITNESS,
  23. destination=DEFAULT_BUILD_DIR):
  24. """Fetch the MSI for the specified version of Python.
  25. It will be placed in the destination directory, and validated using GPG
  26. if possible.
  27. """
  28. arch_tag = '.amd64' if (bitness==64) else ''
  29. url = 'http://python.org/ftp/python/{0}/python-{0}{1}.msi'.format(version, arch_tag)
  30. target = pjoin(destination, 'python-{0}{1}.msi'.format(version, arch_tag))
  31. if os.path.isfile(target):
  32. logger.info('Python MSI already in build directory.')
  33. return
  34. logger.info('Downloading Python MSI...')
  35. urlretrieve(url, target)
  36. urlretrieve(url+'.asc', target+'.asc')
  37. try:
  38. keys_file = os.path.join(_PKGDIR, 'python-pubkeys.txt')
  39. check_output(['gpg', '--import', keys_file])
  40. check_output(['gpg', '--verify', target+'.asc'])
  41. except FileNotFoundError:
  42. logger.warn("GPG not available - could not check signature of {0}".format(target))
  43. SCRIPT_TEMPLATE = """#!python{qualifier}
  44. import sys
  45. sys.path.insert(0, 'pkgs')
  46. from {module} import {func}
  47. {func}()
  48. """
  49. def write_script(entrypt, python_version, bitness, target):
  50. """Write a launcher script from a 'module:function' entry point
  51. python_version and bitness are used to write an appropriate shebang line
  52. for the PEP 397 Windows launcher.
  53. """
  54. qualifier = '.'.join(python_version.split('.')[:2])
  55. if bitness == 32:
  56. qualifier += '-32'
  57. module, func = entrypt.split(":")
  58. with open(target, 'w') as f:
  59. f.write(SCRIPT_TEMPLATE.format(qualifier=qualifier, module=module, func=func))
  60. def copy_extra_files(filelist, build_dir):
  61. """Copy a list of files into the build directory.
  62. Returns a list of 2-tuples: the filename without any path coomponents,
  63. and a boolean that is True if the file is a directory.
  64. """
  65. results = [] # name, is_directory
  66. for file in filelist:
  67. file = file.rstrip('/\\')
  68. basename = os.path.basename(file)
  69. if os.path.isdir(file):
  70. target_name = pjoin(build_dir, basename)
  71. if os.path.isdir(target_name):
  72. shutil.rmtree(target_name)
  73. elif os.path.exists(target_name):
  74. os.unlink(target_name)
  75. shutil.copytree(file, target_name)
  76. results.append((basename, True))
  77. else:
  78. shutil.copy2(file, build_dir)
  79. results.append((basename, False))
  80. return results
  81. def make_installer_name(appname, version):
  82. """Generate the filename of the installer exe
  83. e.g. My_App_1.0.exe
  84. """
  85. s = appname + '_' + version + '.exe'
  86. return s.replace(' ', '_')
  87. def run_nsis(nsi_file):
  88. """Runs makensis using the specified .nsi file
  89. Returns the exit code.
  90. """
  91. try:
  92. return call(['makensis', nsi_file])
  93. except FileNotFoundError:
  94. print("makensis was not found. Install NSIS and try again.")
  95. print("http://nsis.sourceforge.net/Download")
  96. return 1
  97. def all_steps(appname, version, script=None, entry_point=None, icon=DEFAULT_ICON, console=False,
  98. packages=None, extra_files=None, py_version=DEFAULT_PY_VERSION,
  99. py_bitness=DEFAULT_BITNESS, build_dir=DEFAULT_BUILD_DIR,
  100. installer_name=None, nsi_template=DEFAULT_NSI_TEMPLATE):
  101. """Run all the steps to build an installer.
  102. For details of the parameters, see the documentation for the config file
  103. options.
  104. """
  105. installer_name = installer_name or make_installer_name(appname, version)
  106. os.makedirs(build_dir, exist_ok=True)
  107. fetch_python(version=py_version, bitness=py_bitness, destination=build_dir)
  108. if entry_point is not None:
  109. if script is not None:
  110. raise ValueError('Both script and entry_point were specified.')
  111. script = 'launch.py'
  112. write_script(entry_point, py_version, py_bitness, pjoin(build_dir, script))
  113. elif script is not None:
  114. shutil.copy2(script, build_dir)
  115. else:
  116. raise ValueError('Neither script nor entry_point was specified.')
  117. shutil.copy2(icon, build_dir)
  118. # Packages
  119. build_pkg_dir = pjoin(build_dir, 'pkgs')
  120. if os.path.isdir(build_pkg_dir):
  121. shutil.rmtree(build_pkg_dir)
  122. if os.path.isdir('pynsist_pkgs'):
  123. shutil.copytree('pynsist_pkgs', build_pkg_dir)
  124. else:
  125. os.mkdir(build_pkg_dir)
  126. copy_modules(packages or [], build_pkg_dir)
  127. nsis_writer = NSISFileWriter(nsi_template,
  128. definitions = {'PRODUCT_NAME': appname,
  129. 'PRODUCT_VERSION': version,
  130. 'PY_VERSION': py_version,
  131. 'SCRIPT': os.path.basename(script),
  132. 'PRODUCT_ICON': os.path.basename(icon),
  133. 'INSTALLER_NAME': installer_name,
  134. 'ARCH_TAG': '.amd64' if (py_bitness==64) else '',
  135. 'PY_EXE': 'py' if console else 'pyw',
  136. }
  137. )
  138. # Extra files
  139. nsis_writer.extra_files = copy_extra_files(extra_files or [], build_dir)
  140. nsi_file = pjoin(build_dir, 'installer.nsi')
  141. nsis_writer.write(nsi_file)
  142. exitcode = run_nsis(nsi_file)
  143. if not exitcode:
  144. logger.info('Installer written to %s', pjoin(build_dir, installer_name))
  145. def main(argv=None):
  146. """Make an installer from the command line.
  147. This parses command line arguments and a config file, and calls
  148. :func:`all_steps` with the extracted information.
  149. """
  150. logger.setLevel(logging.INFO)
  151. logger.addHandler(logging.StreamHandler())
  152. import argparse
  153. argp = argparse.ArgumentParser(prog='pynsist')
  154. argp.add_argument('config_file')
  155. options = argp.parse_args(argv)
  156. dirname, config_file = os.path.split(options.config_file)
  157. if dirname:
  158. os.chdir(dirname)
  159. import configparser
  160. cfg = configparser.ConfigParser()
  161. cfg.read(config_file)
  162. appcfg = cfg['Application']
  163. all_steps(
  164. appname = appcfg['name'],
  165. version = appcfg['version'],
  166. script = appcfg.get('script', fallback=None),
  167. entry_point = appcfg.get('entry_point', fallback=None),
  168. icon = appcfg.get('icon', DEFAULT_ICON),
  169. console = appcfg.getboolean('console', fallback=False),
  170. packages = cfg.get('Include', 'packages', fallback='').splitlines(),
  171. extra_files = cfg.get('Include', 'files', fallback='').splitlines(),
  172. py_version = cfg.get('Python', 'version', fallback=DEFAULT_PY_VERSION),
  173. py_bitness = cfg.getint('Python', 'bitness', fallback=DEFAULT_BITNESS),
  174. build_dir = cfg.get('Build', 'directory', fallback=DEFAULT_BUILD_DIR),
  175. installer_name = cfg.get('Build', 'installer_name', fallback=None),
  176. nsi_template = cfg.get('Build', 'nsi_template', fallback=DEFAULT_NSI_TEMPLATE),
  177. )