Skip to content

Commit

Permalink
Implement console command section property
Browse files Browse the repository at this point in the history
  • Loading branch information
bastimeyer committed Feb 23, 2019
1 parent ca4c2ce commit 91f39ef
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 22 deletions.
6 changes: 6 additions & 0 deletions doc/cfgfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ variable.
As with shortcuts, this specifies the Python function to call, in the format
``module:function``.

.. describe:: console (optional)

If ``true`` (default), the command will be using the ``py`` launcher, which
opens a console for the process. If ``false``, it will use the ``pyw``
launcher, which doesn't create a console.

.. describe:: extra_preamble (optional)

As for shortcuts, a file containing extra code to run before importing the
Expand Down
31 changes: 19 additions & 12 deletions nsist/_assemble_launchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,38 @@
Each launcher contains: exe base + shebang + zipped Python code
"""
import glob
import re
import os
import sys

b_shebang = '#!"{}"\r\n'.format(sys.executable).encode('utf-8')

def assemble_exe(path, b_launcher):
exe_path = path[:-len('-append.zip')] + '.exe'
shebang = '#!"{executable}{suffix}.exe"\r\n'
launchers = [('launcher_exe.dat', '-append.zip', ''),
('launcher_noconsole_exe.dat', '-append-noconsole.zip', 'w')]

def assemble_exe(exe_path, b_launcher, b_shebang, b_append):
with open(exe_path, 'wb') as f:
f.write(b_launcher)
f.write(b_shebang)

with open(path, 'rb') as f2:
f.write(f2.read())
f.write(b_append)

def main(argv=None):
if argv is None:
argv = sys.argv
target_dir = argv[1]
executable = argv[1]
target_dir = argv[2]

executable = re.sub(r'\.exe$', '', executable)

for launcher, append, suffix in launchers:
b_shebang = shebang.format(executable=executable, suffix=suffix).encode('utf-8')

with open(os.path.join(target_dir, 'launcher_exe.dat'), 'rb') as f:
b_launcher = f.read()
with open(os.path.join(target_dir, launcher), 'rb') as f:
b_launcher = f.read()

for path in glob.glob(os.path.join(target_dir, '*-append.zip')):
assemble_exe(path, b_launcher)
for path in glob.glob(os.path.join(target_dir, '*' + append)):
with open(path, 'rb') as f:
b_append = f.read()
assemble_exe(path[:-len(append)] + '.exe', b_launcher, b_shebang, b_append)

if __name__ == '__main__':
main()
15 changes: 11 additions & 4 deletions nsist/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@
sys.exit({func}())
"""

def find_exe(bitness=32):
def find_exe(bitness=32, console=True):
distlib_dir = osp.dirname(distlib.scripts.__file__)
return osp.join(distlib_dir, 't%d.exe' % bitness)
name = 't' if console else 'w'
return osp.join(distlib_dir, '{name}{bitness}.exe'.format(name=name, bitness=bitness))

def prepare_bin_directory(target, commands, bitness=32):
# Give the base launcher a .dat extension so it doesn't show up as an
# executable command itself. During the installation it will be copied to
# each launcher name, and the necessary data appended to it.
shutil.copy(find_exe(bitness), str(target / 'launcher_exe.dat'))
shutil.copy(find_exe(bitness, True), str(target / 'launcher_exe.dat'))
shutil.copy(find_exe(bitness, False), str(target / 'launcher_noconsole_exe.dat'))

for name, command in commands.items():
specified_preamble = command.get('extra_preamble', None)
Expand All @@ -51,5 +53,10 @@ def prepare_bin_directory(target, commands, bitness=32):
extra_preamble=extra_preamble.read().rstrip(),
)

with ZipFile(str(target / (name + '-append.zip')), 'w') as zf:
if command.get('console', True):
append = '-append.zip'
else:
append = '-append-noconsole.zip'

with ZipFile(str(target / (name + append)), 'w') as zf:
zf.writestr('__main__.py', script.encode('utf-8'))
2 changes: 2 additions & 0 deletions nsist/configreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def _check_invalid_keys(self, section_name, section):
]),
'Command': SectionValidator([
('entry_point', True),
('console', False),
('extra_preamble', False),
])
}
Expand Down Expand Up @@ -200,6 +201,7 @@ def read_commands_config(cfg):
if section.startswith("Command "):
name = section[len("Command "):]
commands[name] = cc = dict(cfg[section])
cc['console'] = cfg[section].getboolean('console', fallback=True)
if ('extra_preamble' in cc) and \
not os.path.isfile(cc['extra_preamble']):
raise InvalidConfig('extra_preamble file %r does not exist' %
Expand Down
2 changes: 1 addition & 1 deletion nsist/pyapp.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Section "!${PRODUCT_NAME}" sec_app
[% block install_commands %]
[% if has_commands %]
DetailPrint "Setting up command-line launchers..."
nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_assemble_launchers.py" "$INSTDIR\bin"'
nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_assemble_launchers.py" [[ python ]] "$INSTDIR\bin"'

StrCmp $MultiUser.InstallMode CurrentUser 0 AddSysPathSystem
; Add to PATH for current user
Expand Down
27 changes: 27 additions & 0 deletions nsist/tests/data_files/valid_config_with_commands.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[Application]
name=My App
version=1.0
# How to launch the app - this calls the 'main' function from the 'myapp' package:
entry_point=myapp:main
icon=myapp.ico

[Python]
version=3.4.0

[Include]
# Importable packages that your application requires, one per line
packages = requests
bs4
html5lib

# Other files and folders that should be installed
files = LICENSE
data_files/

[Command foo]
entry_point=foo:foo
extra_preamble=/foo

[Command bar]
entry_point=bar:bar
console=false
82 changes: 77 additions & 5 deletions nsist/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,98 @@

from nsist import commands, _assemble_launchers

cmds = {'acommand': {'entry_point': 'somemod:somefunc',
'extra_preamble': io.StringIO(u'import extra')}}

def test_prepare_bin_dir(tmpdir):
cmds = {
'acommand': {
'entry_point': 'somemod:somefunc',
'extra_preamble': io.StringIO(u'import extra')
}
}
commands.prepare_bin_directory(tmpdir, cmds)

launcher_file = str(tmpdir / 'launcher_exe.dat')
launcher_noconsole_file = str(tmpdir / 'launcher_noconsole_exe.dat')
zip_file = str(tmpdir / 'acommand-append.zip')
zip_file_invalid = str(tmpdir / 'acommand-append-noconsole.zip')
exe_file = str(tmpdir / 'acommand.exe')

assert_isfile(launcher_file)
assert_isfile(launcher_noconsole_file)
assert_isfile(zip_file)
assert_not_path_exists(exe_file) # Created by _assemble_launchers
assert_not_path_exists(zip_file_invalid)
assert_not_path_exists(exe_file)

with open(launcher_file, 'rb') as lf:
b_launcher = lf.read()
assert b_launcher[:2] == b'MZ'
with open(launcher_noconsole_file, 'rb') as lf:
assert lf.read(2) == b'MZ'

with ZipFile(zip_file) as zf:
assert zf.testzip() is None
script_contents = zf.read('__main__.py').decode('utf-8')
assert 'import extra' in script_contents
assert 'somefunc()' in script_contents

_assemble_launchers.main(['_assemble_launchers.py', str(tmpdir)])
_assemble_launchers.main(['_assemble_launchers.py', 'C:\\path\\to\\python', str(tmpdir)])

assert_isfile(exe_file)

with open(exe_file, 'rb') as ef, open(zip_file, 'rb') as zf:
b_exe = ef.read()
b_zip = zf.read()
assert b_exe[:len(b_launcher)] == b_launcher
assert b_exe[len(b_launcher):-len(b_zip)].decode('utf-8') == '#!"C:\\path\\to\\python.exe"\r\n'
assert b_exe[-len(b_zip):] == b_zip

with ZipFile(exe_file) as zf:
assert zf.testzip() is None
assert zf.read('__main__.py').decode('utf-8') == script_contents

def test_prepare_bin_dir_noconsole(tmpdir):
cmds = {
'acommand': {
'entry_point': 'somemod:somefunc',
'console': False
}
}
commands.prepare_bin_directory(tmpdir, cmds)

launcher_file = str(tmpdir / 'launcher_exe.dat')
launcher_noconsole_file = str(tmpdir / 'launcher_noconsole_exe.dat')
zip_file = str(tmpdir / 'acommand-append-noconsole.zip')
zip_file_invalid = str(tmpdir / 'acommand-append.zip')
exe_file = str(tmpdir / 'acommand.exe')

assert_isfile(launcher_file)
assert_isfile(launcher_noconsole_file)
assert_isfile(zip_file)
assert_not_path_exists(zip_file_invalid)
assert_not_path_exists(exe_file)

with open(launcher_file, 'rb') as lf:
assert lf.read(2) == b'MZ'
with open(launcher_noconsole_file, 'rb') as lf:
b_launcher = lf.read()
assert b_launcher[:2] == b'MZ'

with ZipFile(zip_file) as zf:
assert zf.testzip() is None
script_contents = zf.read('__main__.py').decode('utf-8')
assert 'import extra' not in script_contents
assert 'somefunc()' in script_contents

_assemble_launchers.main(['_assemble_launchers.py', 'C:\\custom\\python.exe', str(tmpdir)])

assert_isfile(exe_file)

with open(exe_file, 'rb') as ef, open(zip_file, 'rb') as zf:
b_exe = ef.read()
b_zip = zf.read()
assert b_exe[:len(b_launcher)] == b_launcher
assert b_exe[len(b_launcher):-len(b_zip)].decode('utf-8') == '#!"C:\\custom\\pythonw.exe"\r\n'
assert b_exe[-len(b_zip):] == b_zip

with ZipFile(exe_file) as zf:
assert zf.testzip() is None
assert zf.read('__main__.py').decode('utf-8') == script_contents
4 changes: 4 additions & 0 deletions nsist/tests/test_configuration_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def test_valid_config_with_shortcut():
configfile = os.path.join(DATA_FILES, 'valid_config_with_shortcut.cfg')
configreader.read_and_validate(configfile)

def test_valid_config_with_commands():
configfile = os.path.join(DATA_FILES, 'valid_config_with_commands.cfg')
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)
Expand Down

0 comments on commit 91f39ef

Please sign in to comment.