Explorar el Código

Merge pull request #191 from takluyver/cmds-relative-shebang

Construct command exes with relative shebangs
Thomas Kluyver hace 5 años
padre
commit
abed750d8c
Se han modificado 6 ficheros con 48 adiciones y 118 borrados
  1. 0 1
      nsist/__init__.py
  2. 0 40
      nsist/_assemble_launchers.py
  3. 25 12
      nsist/commands.py
  4. 0 1
      nsist/pyapp.nsi
  5. 22 63
      nsist/tests/test_commands.py
  6. 1 1
      pyproject.toml

+ 0 - 1
nsist/__init__.py

@@ -356,7 +356,6 @@ if __name__ == '__main__':
         prepare_bin_directory(command_dir, self.commands, bitness=self.py_bitness)
         self.install_dirs.append((command_dir.name, '$INSTDIR'))
         self.extra_files.append((pjoin(_PKGDIR, '_system_path.py'), '$INSTDIR'))
-        self.extra_files.append((pjoin(_PKGDIR, '_assemble_launchers.py'), '$INSTDIR'))
 
     def copytree_ignore_callback(self, directory, files):
         """This is being called back by our shutil.copytree call to implement the

+ 0 - 40
nsist/_assemble_launchers.py

@@ -1,40 +0,0 @@
-"""This is run during installation to assemble command-line exe launchers
-
-Each launcher contains: exe base + shebang + zipped Python code
-"""
-import glob
-import re
-import os
-import sys
-
-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)
-        f.write(b_append)
-
-def main(argv=None):
-    if argv is None:
-        argv = sys.argv
-    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), 'rb') as f:
-            b_launcher = f.read()
-
-        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()

+ 25 - 12
nsist/commands.py

@@ -31,13 +31,23 @@ def find_exe(bitness=32, console=True):
     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, True), str(target / 'launcher_exe.dat'))
-    shutil.copy(find_exe(bitness, False), str(target / 'launcher_noconsole_exe.dat'))
-
     for name, command in commands.items():
+        exe_path = target / (name + '.exe')
+        console = command.get('console', True)
+
+        # 1. Get the base launcher exe from distlib
+        with open(find_exe(bitness, console=console), 'rb') as f:
+            launcher_b = f.read()
+
+        # 2. Shebang: Python executable to run with
+        # shebangs relative to launcher location, according to
+        # https://bitbucket.org/vinay.sajip/simple_launcher/wiki/Launching%20an%20interpreter%20in%20a%20location%20relative%20to%20the%20launcher%20executable
+        if console:
+            shebang = b"#!<launcher_dir>\\..\\Python\\python.exe\r\n"
+        else:
+            shebang = b"#!<launcher_dir>\\..\\Python\\pythonw.exe\r\n"
+
+        # 3. The script to run, inside a zip file
         specified_preamble = command.get('extra_preamble', None)
         if isinstance(specified_preamble, str):
             # Filename
@@ -53,10 +63,13 @@ def prepare_bin_directory(target, commands, bitness=32):
             extra_preamble=extra_preamble.read().rstrip(),
         )
 
-        if command.get('console', True):
-            append = '-append.zip'
-        else:
-            append = '-append-noconsole.zip'
-
-        with ZipFile(str(target / (name + append)), 'w') as zf:
+        zip_bio = io.BytesIO()
+        with ZipFile(zip_bio, 'w') as zf:
             zf.writestr('__main__.py', script.encode('utf-8'))
+
+        # Put the pieces together
+        with exe_path.open('wb') as f:
+            f.write(launcher_b)
+            f.write(shebang)
+            f.write(zip_bio.getvalue())
+

+ 0 - 1
nsist/pyapp.nsi

@@ -115,7 +115,6 @@ 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" [[ python ]] "$INSTDIR\bin"'
 
     StrCmp $MultiUser.InstallMode CurrentUser 0 AddSysPathSystem
       ; Add to PATH for current user

+ 22 - 63
nsist/tests/test_commands.py

@@ -2,100 +2,59 @@ import io
 from testpath import assert_isfile, assert_not_path_exists
 from zipfile import ZipFile
 
-from nsist import commands, _assemble_launchers
+from nsist import commands
 
-def test_prepare_bin_dir(tmpdir):
+def test_prepare_bin_dir(tmp_path):
     cmds = {
         'acommand': {
             'entry_point': 'somemod:somefunc',
             'extra_preamble': io.StringIO(u'import extra')
         }
     }
-    commands.prepare_bin_directory(tmpdir, cmds)
+    commands.prepare_bin_directory(tmp_path, 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')
+    exe_file = str(tmp_path / '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)
+    assert_isfile(exe_file)
 
-    with open(launcher_file, 'rb') as lf:
+    with open(commands.find_exe(console=True), '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
+        assert b_launcher[:2] == b'MZ'  # Sanity check
 
-    _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:
+    with open(exe_file, 'rb') as ef:
         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
+        assert b_exe[len(b_launcher):].startswith(b"#!<launcher_dir>\\..\\Python\\python.exe\r\n")
 
     with ZipFile(exe_file) as zf:
         assert zf.testzip() is None
-        assert zf.read('__main__.py').decode('utf-8') == script_contents
+        script_contents = zf.read('__main__.py').decode('utf-8')
+    assert 'import extra' in script_contents
+    assert 'somefunc()' in script_contents
 
-def test_prepare_bin_dir_noconsole(tmpdir):
+def test_prepare_bin_dir_noconsole(tmp_path):
     cmds = {
         'acommand': {
             'entry_point': 'somemod:somefunc',
             'console': False
         }
     }
-    commands.prepare_bin_directory(tmpdir, cmds)
+    commands.prepare_bin_directory(tmp_path, 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')
+    exe_file = str(tmp_path / '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)
+    assert_isfile(exe_file)
 
-    with open(launcher_file, 'rb') as lf:
-        assert lf.read(2) == b'MZ'
-    with open(launcher_noconsole_file, 'rb') as lf:
+    with open(commands.find_exe(console=False), '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
+        assert b_launcher[:2] == b'MZ'  # Sanity check
 
-    _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:
+    with open(exe_file, 'rb') as ef:
         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
+        assert b_exe[len(b_launcher):].startswith(b"#!<launcher_dir>\\..\\Python\\pythonw.exe\r\n")
 
     with ZipFile(exe_file) as zf:
         assert zf.testzip() is None
-        assert zf.read('__main__.py').decode('utf-8') == script_contents
+        script_contents = zf.read('__main__.py').decode('utf-8')
+    assert 'somefunc()' in script_contents

+ 1 - 1
pyproject.toml

@@ -15,7 +15,7 @@ requires = [
     "requests_download",
     "jinja2",
     "yarg",
-    "distlib"
+    "distlib >=0.3"
 ]
 classifiers = [
     "License :: OSI Approved :: MIT License",