1
0
Эх сурвалжийг харах

Merge pull request #179 from bastimeyer/feature/commands-console

Implement `console` command section property
Thomas Kluyver 6 жил өмнө
parent
commit
b066fabf3d

+ 6 - 0
doc/cfgfile.rst

@@ -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

+ 19 - 12
nsist/_assemble_launchers.py

@@ -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()

+ 11 - 4
nsist/commands.py

@@ -25,15 +25,17 @@ if __name__ == '__main__':
     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)
@@ -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 - 0
nsist/configreader.py

@@ -95,6 +95,7 @@ CONFIG_VALIDATORS = {
     ]),
     'Command': SectionValidator([
         ('entry_point', True),
+        ('console', False),
         ('extra_preamble', False),
     ])
 }
@@ -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' %

+ 1 - 1
nsist/pyapp.nsi

@@ -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

+ 27 - 0
nsist/tests/data_files/valid_config_with_commands.cfg

@@ -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

+ 77 - 5
nsist/tests/test_commands.py

@@ -4,16 +4,32 @@ from zipfile import ZipFile
 
 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
@@ -21,9 +37,65 @@ def test_prepare_bin_dir(tmpdir):
     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 - 0
nsist/tests/test_configuration_validator.py

@@ -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)