Skip to content

Commit

Permalink
BF: make windows script wrappers more robust
Browse files Browse the repository at this point in the history
The script wrappers were hard-coding the python path at the time of the
creation of the binary such as an exe or an egg.  This path is likely to be
wrong after the binary is installed somewhere.  Use some minor bat file
programming to pull the Python command out of the script directly, rather like
the ``cli.exe`` trick used by setuptools.
  • Loading branch information
matthew-brett committed Jan 31, 2013
1 parent a6db930 commit 80c283b
Showing 1 changed file with 41 additions and 42 deletions.
83 changes: 41 additions & 42 deletions setup_helpers.py
Expand Up @@ -16,11 +16,10 @@
# Standard library imports
import sys
import os
from os.path import join as pjoin, dirname, isfile
from os.path import join as pjoin, dirname, splitext, split as psplit
import zipfile
import warnings
import shutil
import re
from distutils.cmd import Command
from distutils.command.clean import clean
from distutils.command.install_scripts import install_scripts
Expand Down Expand Up @@ -299,69 +298,69 @@ def generate_a_pyrex_source(self, base, ext_name, source, extension):
(CYTHON_MIN_VERSION, source))
return target_file

EXE_MATCH_RE = re.compile('^#!.*python[0-9.]*', re.IGNORECASE)
BAT_TEMPLATE = \
r"""@echo off
set mypath=%~dp0
set pyscript="%mypath%{FNAME}"
set /p line1=<%pyscript%
if "%line1:~0,2%" == "#!" (goto :goodstart)
echo First line of %pyscript% does not start with "#!"
exit /b 1
:goodstart
set py_exe=%line1:~2%
call %py_exe% %pyscript% %*
"""

class install_scripts_nipy(install_scripts):
""" Make scripts executable on Windows
Scripts are bare file names without extension on Unix, fitting (for example)
Debian rules. They identify as python scripts with the usual ``#!`` first
line. This doesn't work on Windows. So, on Windows only we:
* Add a ``.py`` extension to the ``bare_script_name`` to select interpeter
by extension
* Add a ``.bat`` wrapper of name ``bare_script_name.bat`` to call
``bare_script_name.py`` using the python interpreter.
line. This doesn't work on Windows. So, on Windows only we ad a ``.bat``
wrapper of name ``bare_script_name.bat`` to call ``bare_script_name``
using the python interpreter from the #! first line of the script.
Notes
-----
The idea for this routine came from an idea in Twisted, re-used in IPython
An alternative to this method would be to use the ``distribute`` /
``setuptools`` ``cli.exe`` method, where we copy a pre-compiled Windows
executable ``cli.exe`` as ``bare_script_name.exe``, and the script as
``bare_script_name-script.py``. The executable file ``cli.exe`` (copied as
``bare_script_name.exe``) analyzes its own name, and then calls
``bare_script_name-script.py`` with the Python interpreter named in the top
line of the script file. But, ``.bat`` wrappers as here seem to work on all
current versions of Windows, are simpler to understand, and don't require
putting pre-compiled code into the repository.
``setuptools`` ``cli.exe`` method. We could do this by defining console
entry points for the scripts, and moving the script code into the libary.
setuptools then copies a pre-compiled Windows executable ``cli.exe`` as
``bare_script_name.exe``, and the script as ``bare_script_name-script.py``.
The executable file ``cli.exe`` (copied as ``bare_script_name.exe``)
analyzes its own name, and then calls ``bare_script_name-script.py`` with
the Python interpreter named in the top line of the script file. Doing this
induces a run-time dependency on setuptools because the generated python
script files import ``pkg_resources``.
See git://github.com/matthew-brett/myscripter.git for some experiments to
show what setuptools and distutils are doing.
"""
def run(self):
install_scripts.run(self)
if not os.name == "nt":
return
for file in self.get_outputs():
# rename script to .py so it can be executed on windows
if file.endswith(".py"):
# Only do this processing on the bare scripts, not .py scripts.
continue
log.info("Adding .py extension to %s", file)
py_file = file + '.py'
if self.dry_run:
continue
if isfile(py_file):
os.unlink(py_file)
os.rename(file, py_file)
for filepath in self.get_outputs():
# If we can find an executable name in the #! top line of the script
# file, make .bat wrapper for .py script, using this executable
# name. The original name has been replaced by one specific to the
# platform during the build_script distutils step.
with open(py_file, 'rt') as fobj:
# file, make .bat wrapper for script.
with open(filepath, 'rt') as fobj:
first_line = fobj.readline()
exe_match = EXE_MATCH_RE.match(first_line)
if exe_match is None:
if not (first_line.startswith('#!') and
'python' in first_line.lower()):
log.info("No #!python executable found, skipping .bat "
"wrapper")
continue
exe_stuff = first_line[2:].strip()
log.info("Making .bat wrapper for %s with executable %s",
py_file, exe_stuff)
bat_file = file + '.bat'
if isfile(bat_file):
os.unlink(bat_file)
pth, fname = psplit(filepath)
froot, ext = splitext(fname)
bat_file = pjoin(pth, froot + '.bat')
bat_contents = BAT_TEMPLATE.replace('{FNAME}', fname)
log.info("Making %s wrapper for %s" % (bat_file, filepath))
if self.dry_run:
continue
with open(bat_file, 'wt') as fobj:
fobj.write('@"%s" "%s"' % (exe_stuff, py_file) + " %*\n")
fobj.write(bat_contents)


# The command classes for distutils, used by setup.py
Expand Down

0 comments on commit 80c283b

Please sign in to comment.