Skip to content

Commit

Permalink
Merge pull request #113 from Siecje/config
Browse files Browse the repository at this point in the history
Allowing config values to start with a newline
  • Loading branch information
takluyver committed Mar 6, 2017
2 parents 9cd4cd7 + c9505a3 commit 9b176df
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 52 deletions.
66 changes: 25 additions & 41 deletions nsist/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
else:
winreg = None

from .configreader import get_installer_builder_args
from .commands import prepare_bin_directory
from .copymodules import copy_modules
from .nsiswriter import NSISFileWriter
Expand Down Expand Up @@ -64,11 +65,11 @@ def __str__(self):

class InstallerBuilder(object):
"""Controls building an installer. This includes three main steps:
1. Arranging the necessary files in the build directory.
2. Filling out the template NSI file to control NSIS.
3. Running ``makensis`` to build the installer.
:param str appname: Application name
:param str version: Application version
:param dict shortcuts: Dictionary keyed by shortcut name, containing
Expand Down Expand Up @@ -151,12 +152,12 @@ def __init__(self, appname, version, shortcuts, publisher=None,
self.nsi_template = 'pyapp_installpy.nsi'

self.nsi_file = pjoin(self.build_dir, 'installer.nsi')

# To be filled later
self.install_files = []
self.install_dirs = []
self.msvcrt_files = []

_py_version_pattern = re.compile(r'\d\.\d+\.\d+$')

@property
Expand All @@ -166,7 +167,7 @@ def py_version_tuple(self):

def make_installer_name(self):
"""Generate the filename of the installer exe
e.g. My_App_1.0.exe
"""
s = self.appname + '_' + self.version + '.exe'
Expand All @@ -192,7 +193,7 @@ def _python_download_url_filename(self):

def fetch_python(self):
"""Fetch the MSI for the specified version of Python.
It will be placed in the build directory.
"""
url, filename = self._python_download_url_filename()
Expand Down Expand Up @@ -243,7 +244,7 @@ def prepare_msvcrt(self):

def fetch_pylauncher(self):
"""Fetch the MSI for PyLauncher (required for Python2.x).
It will be placed in the build directory.
"""
arch_tag = '.amd64' if (self.py_bitness == 64) else ''
Expand Down Expand Up @@ -288,10 +289,10 @@ def excepthook(etype, value, tb):
from {module} import {func}
{func}()
"""

def write_script(self, entrypt, target, extra_preamble=''):
"""Write a launcher script from a 'module:function' entry point
py_version and py_bitness are used to write an appropriate shebang line
for the PEP 397 Windows launcher.
"""
Expand All @@ -306,11 +307,11 @@ def write_script(self, entrypt, target, extra_preamble=''):

def prepare_shortcuts(self):
"""Prepare shortcut files in the build directory.
If entry_point is specified, write the script. If script is specified,
copy to the build directory. Prepare target and parameters for these
shortcuts.
Also copies shortcut icons
"""
files = set()
Expand Down Expand Up @@ -346,12 +347,12 @@ def prepare_shortcuts(self):
shutil.copy2(sc['icon'], self.build_dir)
sc['icon'] = os.path.basename(sc['icon'])
files.add(sc['icon'])

self.install_files.extend([(f, '$INSTDIR') for f in files])

def prepare_packages(self):
"""Move requested packages into the build directory.
If a pynsist_pkgs directory exists, it is copied into the build
directory as pkgs/ . Any packages not already there are found on
sys.path and copied in.
Expand Down Expand Up @@ -447,7 +448,7 @@ def write_nsi(self):

def run_nsis(self):
"""Runs makensis using the specified .nsi file
Returns the exit code.
"""
try:
Expand Down Expand Up @@ -480,15 +481,15 @@ def run(self, makensis=True):
self.fetch_python()
if self.py_version < '3.3':
self.fetch_pylauncher()

self.prepare_shortcuts()

if self.commands:
self.prepare_commands()

# Packages
self.prepare_packages()

# Extra files
self.copy_extra_files()

Expand All @@ -502,21 +503,21 @@ def run(self, makensis=True):

def main(argv=None):
"""Make an installer from the command line.
This parses command line arguments and a config file, and calls
:func:`all_steps` with the extracted information.
"""
logger.setLevel(logging.INFO)
logger.handlers = [logging.StreamHandler()]

import argparse
argp = argparse.ArgumentParser(prog='pynsist')
argp.add_argument('config_file')
argp.add_argument('--no-makensis', action='store_true',
help='Prepare files and folders, stop before calling makensis. For debugging.'
)
options = argp.parse_args(argv)

dirname, config_file = os.path.split(options.config_file)
if dirname:
os.chdir(dirname)
Expand All @@ -530,28 +531,11 @@ def main(argv=None):
logger.error('Error parsing configuration file:')
logger.error(str(e))
sys.exit(1)
appcfg = cfg['Application']

args = get_installer_builder_args(cfg)

try:
InstallerBuilder(
appname = appcfg['name'],
version = appcfg['version'],
publisher = appcfg.get('publisher', None),
icon = appcfg.get('icon', DEFAULT_ICON),
shortcuts = shortcuts,
commands=commands,
packages = cfg.get('Include', 'packages', fallback='').splitlines(),
pypi_wheel_reqs = cfg.get('Include', 'pypi_wheels', fallback='').splitlines(),
extra_files = configreader.read_extra_files(cfg),
py_version = cfg.get('Python', 'version', fallback=DEFAULT_PY_VERSION),
py_bitness = cfg.getint('Python', 'bitness', fallback=DEFAULT_BITNESS),
py_format = cfg.get('Python', 'format', fallback=None),
inc_msvcrt = cfg.getboolean('Python', 'include_msvcrt', fallback=True),
build_dir = cfg.get('Build', 'directory', fallback=DEFAULT_BUILD_DIR),
installer_name = cfg.get('Build', 'installer_name', fallback=None),
nsi_template = cfg.get('Build', 'nsi_template', fallback=None),
exclude = cfg.get('Include', 'exclude', fallback='').splitlines(),
).run(makensis=(not options.no_makensis))
InstallerBuilder(**args).run(makensis=(not options.no_makensis))
except InputError as e:
logger.error("Error in config values:")
logger.error(str(e))
Expand Down
47 changes: 40 additions & 7 deletions nsist/configreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ def __init__(self, keys):
key is mandatory
"""
self.keys = keys

def check(self, config, section_name):
"""
validates the section, if this is the correct validator for it
returns True if this is the correct validator for this section
raises InvalidConfig if something inside the section is wrong
"""
self._check_mandatory_fields(section_name, config[section_name])
Expand All @@ -33,7 +33,7 @@ def _check_mandatory_fields(self, section_name, key):
section_name,
key_name)
raise InvalidConfig(err_msg)

def _check_invalid_keys(self, section_name, section):
for key in section:
key_name = str(key)
Expand Down Expand Up @@ -116,12 +116,12 @@ def read_and_validate(config_file):
"be one of these: {1}").format(
section,
', '.join(['"%s"' % n for n in valid_section_names]))
raise InvalidConfig(err_msg)
raise InvalidConfig(err_msg)
return config

def read_extra_files(cfg):
"""Read the list of extra files from the config file.
Returns a list of 2-tuples: (file, destination_directory), which can be
passed as the ``extra_files`` parameter to :class:`nsist.InstallerBuilder`.
"""
Expand All @@ -138,10 +138,10 @@ def read_extra_files(cfg):

def read_shortcuts_config(cfg):
"""Read and verify the shortcut definitions from the config file.
There is one shortcut per 'Shortcut <name>' section, and one for the
Application section.
Returns a dict of dicts with the fields from the shortcut sections.
The optional 'icon' and 'console' fields will be filled with their
default values if not supplied.
Expand Down Expand Up @@ -201,3 +201,36 @@ def read_commands_config(cfg):
cc['extra_preamble'])

return commands


def get_installer_builder_args(config):
from . import (DEFAULT_BITNESS,
DEFAULT_BUILD_DIR,
DEFAULT_ICON,
DEFAULT_PY_VERSION)
def get_boolean(s):
if s.lower() in ('1', 'yes', 'true', 'on'):
return True
if s.lower() in ('0', 'no', 'false', 'off'):
return False
raise ValueError('ValueError: Not a boolean: {}'.format(s))

appcfg = config['Application']
args = {}
args['appname'] = appcfg['name'].strip()
args['version'] = appcfg['version'].strip()
args['publisher'] = appcfg['publisher'].strip() if 'publisher' in appcfg else None
args['icon'] = appcfg.get('icon', DEFAULT_ICON).strip()
args['packages'] = config.get('Include', 'packages', fallback='').strip().splitlines()
args['pypi_wheel_reqs'] = config.get('Include', 'pypi_wheels', fallback='').strip().splitlines()
args['extra_files'] = read_extra_files(config)
args['py_version'] = config.get('Python', 'version', fallback=DEFAULT_PY_VERSION).strip()
args['py_bitness'] = config.getint('Python', 'bitness', fallback=DEFAULT_BITNESS)
args['py_format'] = config.get('Python', 'format').strip() if 'format' in config['Python'] else None
inc_msvcrt = config.get('Python', 'include_msvcrt', fallback='True')
args['inc_msvcrt'] = get_boolean(inc_msvcrt.strip())
args['build_dir'] = config.get('Build', 'directory', fallback=DEFAULT_BUILD_DIR).strip()
args['installer_name'] = config.get('Build', 'installer_name') if 'installer_name' in config['Build'] else None
args['nsi_template'] = config.get('Build', 'nsi_template').strip() if 'nsi_template' in config['Build'] else None
args['exclude'] = config.get('Include', 'exclude', fallback='').strip().splitlines()
return args
41 changes: 41 additions & 0 deletions nsist/tests/data_files/valid_config_value_newline.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[Application]
name=
My App
version=
1.0
publisher=
Test
entry_point=
myapp:main
icon=
myapp.ico

[Python]
version=
3.6.0
bitness=
64
format=
bundled
include_msvcrt =
True

[Build]
directory=
build/
nsi_template=
template.nsi

[Include]
packages =
requests
bs4
pypi_wheels=
html5lib
exclude=
something

# Other files and folders that should be installed
files =
LICENSE
data_files/
62 changes: 58 additions & 4 deletions nsist/tests/test_configuration_validator.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,71 @@
from nose.tools import *
import configparser
import os

from nose.tools import *

from .. import configreader
import configparser


DATA_FILES = os.path.join(os.path.dirname(__file__), 'data_files')

def test_valid_config():
configfile = os.path.join(DATA_FILES, 'valid_config.cfg')
configreader.read_and_validate(configfile)
config = configreader.read_and_validate(configfile)
assert config.has_section('Application')

def test_valid_config_with_shortcut():
configfile = os.path.join(DATA_FILES, 'valid_config_with_shortcut.cfg')
configreader.read_and_validate(configfile)
config = configreader.read_and_validate(configfile)

def test_valid_config_with_values_starting_on_new_line():
configfile = os.path.join(DATA_FILES, 'valid_config_value_newline.cfg')
config = configreader.read_and_validate(configfile)
sections = ('Application', 'Python', 'Include', 'Build')
assert len(config.sections()) == len(sections)
for section in sections:
assert section in config
assert config.has_section(section)

assert config.get('Application', 'name') == '\nMy App'
assert config.get('Application', 'version') == '\n1.0'
assert config.get('Application', 'publisher') == '\nTest'
assert config.get('Application', 'entry_point') == '\nmyapp:main'
assert config.get('Application', 'icon') == '\nmyapp.ico'

assert config.get('Python', 'version') == '\n3.6.0'
assert config.get('Python', 'bitness') == '\n64'
assert config.get('Python', 'format') == '\nbundled'
assert config.get('Python', 'include_msvcrt') == '\nTrue'

assert config.get('Build', 'directory') == '\nbuild/'
assert config.get('Build', 'nsi_template') == '\ntemplate.nsi'

assert config.get('Include', 'packages') == '\nrequests\nbs4'
assert config.get('Include', 'pypi_wheels') == '\nhtml5lib'
assert config.get('Include', 'exclude') == '\nsomething'
assert config.get('Include', 'files') == '\nLICENSE\ndata_files/'

args = configreader.get_installer_builder_args(config)
assert args['appname'] == 'My App'
assert args['version'] == '1.0'
assert args['publisher'] == 'Test'
# assert args['entry_point'] == '\nmyapp:main'
assert args['icon'] == 'myapp.ico'

assert args['py_version'] == '3.6.0'
assert args['py_bitness'] == 64
assert args['py_format'] == 'bundled'
assert args['inc_msvcrt'] == True

assert args['build_dir'] == 'build/'
assert args['nsi_template'] == 'template.nsi'

assert args['packages'] == ['requests', 'bs4']
assert args['pypi_wheel_reqs'] == ['html5lib']
assert args['exclude'] == ['something']
assert args['extra_files'] == [('', '$INSTDIR'),
('LICENSE', '$INSTDIR'),
('data_files/', '$INSTDIR')]

@raises(configreader.InvalidConfig)
def test_invalid_config_keys():
Expand Down

0 comments on commit 9b176df

Please sign in to comment.