Commit 673ac23e authored by PJ Eby's avatar PJ Eby

Added support to solve the infamous "we want .py on Windows, no

extension elsewhere" problem, while also bypassing the need for PATHEXT
on Windows, and in fact the need to even write script files at all, for
any platform.  Instead, you define "entry points" in your setup script,
in this case the names of the scripts you want (without extensions) and
the functions that should be imported and run to implement the scripts.
Setuptools will then generate platform-appropriate script files at
install time, including an .exe wrapper when installing on Windows.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4041246
parent baad93e3
......@@ -46,12 +46,8 @@ You may receive a message telling you about an obsolete version of
setuptools being present; if so, you must be sure to delete it entirely, along
with the old ``pkg_resources`` module if it's present on ``sys.path``.
An ``easy_install.py`` script will be installed in the normal location for
Python scripts on your platform. In the examples below, you'll need to replace
references to ``easy_install`` with the correct invocation to run
``easy_install.py`` on your system. If you have Python 2.4 or better, you can
also use ``python -m easy_install``, which will have the same effect, but which
may be easier for you to type.
An ``easy_install`` script will be installed in the normal location for
Python scripts on your platform.
(Note: the ``ez_setup.py`` script accepts the same `Command-Line Options`_ and
`Configuration Files`_ as ``easy_install`` itself, so you can use them to
......@@ -61,6 +57,28 @@ this may make it impossible for scripts installed with EasyInstall to access
it afterwards.)
Windows Installation
~~~~~~~~~~~~~~~~~~~~
On Windows, an ``easy_install.exe`` launcher will also be installed, so that
you can just type ``easy_install`` as long as it's on your ``PATH``. If typing
``easy_install`` at the command prompt doesn't work, check to make sure your
``PATH`` includes the appropriate ``C:\\Python2X\\Scripts`` directory. On
most current versions of Windows, you can change the ``PATH`` by right-clicking
"My Computer", choosing "Properties" and selecting the "Advanced" tab, then
clicking the "Environment Variables" button. ``PATH`` will be in the "System
Variables" section, and you will probably need to reboot for the change to take
effect. Be sure to add a ``;`` after the last item on ``PATH`` before adding
the scripts directory to it.
Note that instead of changing your ``PATH`` to include the Python scripts
directory, you can also retarget the installtion location for scripts so they
go on a directory that's already on the ``PATH``. For more information see the
sections below on `Command-Line Options`_ and `Configuration Files`_. You
can pass command line options (such as ``--script-dir``) to ``ez_setup.py`` to
control where ``easy_install.exe`` will be installed.
Downloading and Installing a Package
------------------------------------
......@@ -758,6 +776,15 @@ Known Issues
in Exemaker. So, don't use Exemaker to wrap ``easy_install.py``, or at any
rate don't expect it to work with all packages.
0.6a2
* EasyInstall can now install "console_scripts" defined by packages that use
``setuptools`` and define appropriate entry points. On Windows, console
scripts get an ``.exe`` wrapper so you can just type their name. On other
platforms, the scripts are installed without a file extension.
* Using ``python -m easy_install`` is now DEPRECATED, since an
``easy_install`` wrapper is now available on all platforms.
0.6a1
* EasyInstall now does MD5 validation of downloads from PyPI, or from any link
that has an "#md5=..." trailer with a 32-digit lowercase hex md5 digest.
......
......@@ -15,5 +15,8 @@ import sys
from setuptools.command.easy_install import *
if __name__ == '__main__':
print >>sys.stderr, "NOTE: python -m easy_install is deprecated."
print >>sys.stderr, "Please use the 'easy_install' command instead."
print >>sys.stderr
main(sys.argv[1:])
/*
Setuptools Script Launcher for Windows
This is a stub executable for Windows that functions somewhat like
Effbot's "exemaker", in that it runs a script with the same name but
a .py extension, using information from a #! line. It differs in that
it spawns the actual Python executable, rather than attempting to
hook into the Python DLL. This means that the script will run with
sys.executable set to the Python executable, where exemaker ends up with
sys.executable pointing to itself. (Which means it won't work if you try
to run another Python process using sys.executable.)
To build/rebuild with mingw32, do this in the setuptools project directory:
gcc -mno-cygwin -O -s -o setuptools/launcher.exe launcher.c
It links to msvcrt.dll, but this shouldn't be a problem since it doesn't
actually run Python in the same process. Note that using 'exec' instead
of 'spawn' doesn't work, because on Windows this leads to the Python
executable running in the *background*, attached to the same console
window, meaning you get a command prompt back *before* Python even finishes
starting. So, we have to use spawnv() and wait for Python to exit before
continuing. :(
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include "windows.h"
int fail(char *format, char *data) {
/* Print error message to stderr and return 1 */
fprintf(stderr, format, data);
return 1;
}
int main(int argc, char **argv) {
char python[256]; /* python executable's filename*/
char script[256]; /* the script's filename */
HINSTANCE hPython; /* DLL handle for python executable */
int scriptf; /* file descriptor for script file */
char **newargs; /* argument array for exec */
char *ptr, *end; /* working pointers for string manipulation */
/* compute script name from our .exe name*/
GetModuleFileName(NULL, script, sizeof(script));
end = script + strlen(script);
while( end>script && *end != '.')
*end-- = '\0';
strcat(script, "py");
/* figure out the target python executable */
scriptf = open(script, O_RDONLY);
if (scriptf == -1) {
return fail("Cannot open %s\n", script);
}
end = python + read(scriptf, python, sizeof(python));
close(scriptf);
ptr = python-1;
while(++ptr < end && *ptr && *ptr!='\n' && *ptr!='\r') {
if (*ptr=='/')
*ptr='\\'; /* convert slashes to avoid LoadLibrary crashes... */
}
*ptr = '\0';
while (ptr>python && isspace(*ptr)) *ptr-- = '\0'; /* strip trailing sp */
if (strncmp(python, "#!", 2)) {
/* default to python.exe if no #! header */
strcpy(python, "#!python.exe");
}
/* At this point, the python buffer contains "#!pythonfilename" */
/* Using spawnv() can fail strangely if you e.g. find the Cygwin
Python, so we'll make sure Windows can find and load it */
hPython = LoadLibraryEx(python+2, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
if (!hPython) {
return fail("Cannot find Python executable %s\n", python+2);
}
/* And we'll use the absolute filename for spawnv */
GetModuleFileName(hPython, python, sizeof(python));
/* printf("Python executable: %s\n", python); */
/* Argument array needs to be argc+1 for args, plus 1 for null sentinel */
newargs = (char **)calloc(argc+2, sizeof(char *));
newargs[0] = python;
newargs[1] = script;
memcpy(newargs+2, argv+1, (argc-1)*sizeof(char *));
newargs[argc+1] = NULL;
/* printf("args 0: %s\nargs 1: %s\n", newargs[0], newargs[1]); */
return spawnv(P_WAIT, newargs[0], (const char * const *)(newargs));
}
......@@ -34,12 +34,12 @@ setup(
keywords = "CPAN PyPI distutils eggs package management",
url = "http://peak.telecommunity.com/DevCenter/setuptools",
test_suite = 'setuptools.tests.test_suite',
packages = find_packages(),
package_data = {'setuptools': ['launcher.exe']},
py_modules = ['pkg_resources', 'easy_install'],
scripts = ['easy_install.py'],
zip_safe = False, # We want 'python -m easy_install' to work :(
zip_safe = False, # We want 'python -m easy_install' to work, for now :(
entry_points = {
"distutils.commands" : [
"%(cmd)s = setuptools.command.%(cmd)s:%(cmd)s" % locals()
......@@ -63,9 +63,9 @@ setup(
"top_level.txt = setuptools.command.egg_info:write_toplevel_names",
"depends.txt = setuptools.command.egg_info:warn_depends_obsolete",
],
"console_scripts":
["easy_install = setuptools.command.easy_install:main"],
},
# uncomment for testing
# setup_requires = ['setuptools>=0.6a0'],
classifiers = [f.strip() for f in """
Development Status :: 3 - Alpha
......@@ -78,5 +78,46 @@ setup(
Topic :: System :: Archiving :: Packaging
Topic :: System :: Systems Administration
Topic :: Utilities""".splitlines() if f.strip()]
# uncomment for testing
# setup_requires = ['setuptools>=0.6a0'],
)
......@@ -16,11 +16,15 @@ namespace_packages.txt = setuptools.command.egg_info:write_arg
entry_points.txt = setuptools.command.egg_info:write_entries
depends.txt = setuptools.command.egg_info:warn_depends_obsolete
[console_scripts]
easy_install = setuptools.command.easy_install:main
[distutils.commands]
bdist_rpm = setuptools.command.bdist_rpm:bdist_rpm
rotate = setuptools.command.rotate:rotate
develop = setuptools.command.develop:develop
setopt = setuptools.command.setopt:setopt
build_py = setuptools.command.build_py:build_py
saveopts = setuptools.command.saveopts:saveopts
egg_info = setuptools.command.egg_info:egg_info
upload = setuptools.command.upload:upload
......
......@@ -199,7 +199,8 @@ unless you need the associated ``setuptools`` feature.
defining the entry points. Entry points are used to support dynamic
discovery of services or plugins provided by a project. See `Dynamic
Discovery of Services and Plugins`_ for details and examples of the format
of this argument.
of this argument. In addition, this keyword is used to support `Automatic
Script Creation`_.
``extras_require``
A dictionary mapping names of "extras" (optional features of your project)
......@@ -295,6 +296,49 @@ remember to modify your setup script whenever your project grows additional
top-level packages or subpackages.
Automatic Script Creation
=========================
Packaging and installing scripts can be a bit awkward with the distutils. For
one thing, there's no easy way to have a script's filename match local
conventions on both Windows and POSIX platforms. For another, you often have
to create a separate file just for the "main" script, when your actual "main"
is a function in a module somewhere. And even in Python 2.4, using the ``-m``
option only works for actual ``.py`` files that aren't installed in a package.
``setuptools`` fixes all of these problems by automatically generating scripts
for you with the correct extension, and on Windows it will even create an
``.exe`` file so that users don't have to change their ``PATHEXT`` settings.
The way to use this feature is to define "entry points" in your setup script
that indicate what function the generated script should import and run. For
example, to create two scripts called ``foo`` and ``bar``, you might do
something like this::
setup(
# other arguments here...
entry_points = {
'console_scripts': [
'foo = my_package.some_module:main_func',
'bar = other_module:some_func',
]
}
)
When this project is installed on non-Windows platforms (using "setup.py
install", "setup.py develop", or by using EasyInstall), a pair of ``foo`` and
``bar`` scripts will be installed that import ``main_func`` and ``some_func``
from the specified modules. The functions you specify are called with no
arguments, and their return value is passed to ``sys.exit()``, so you can
return an errorlevel or message to print to stderr.
You may define as many "console script" entry points as you like, and each one
can optionally specify "extras" that it depends on, and that will be added to
``sys.path`` when the script is run. For more information on "extras", see
section below on `Declaring Extras`_. For more information on "entry points"
in general, see the section below on `Dynamic Discovery of Services and
Plugins`_.
Declaring Dependencies
======================
......@@ -350,6 +394,9 @@ development work on it. (See `"Development Mode"`_ below for more details on
using ``setup.py develop``.)
.. _Declaring Extras:
Declaring "Extras" (optional features with their own dependencies)
------------------------------------------------------------------
......@@ -372,7 +419,33 @@ For example, let's say that Project A offers optional PDF and reST support::
}
)
And that project B needs project A, *with* PDF support::
As you can see, the ``extras_require`` argument takes a dictionary mapping
names of "extra" features, to strings or lists of strings describing those
features' requirements. These requirements will *not* be automatically
installed unless another package depends on them (directly or indirectly) by
including the desired "extras" in square brackets after the associated project
name. (Or if the extras were listed in a requirement spec on the EasyInstall
command line.)
Extras can be used by a project's `entry points`_ to specify dynamic
dependencies. For example, if Project A includes a "rst2pdf" script, it might
declare it like this, so that the "PDF" requirements are only resolved if the
"rst2pdf" script is run::
setup(
name="Project-A",
...
entry_points = {
'console_scripts':
['rst2pdf = project_a.tools.pdfgen [PDF]'],
['rst2html = project_a.tools.htmlgen'],
# more script entry points ...
}
)
Projects can also use another project's extras when specifying dependencies.
For example, if project B needs "project A" with PDF support installed, it
might declare the dependency like this::
setup(
name="Project-B",
......@@ -389,19 +462,11 @@ no longer needs ReportLab, or if it ends up needing other dependencies besides
ReportLab in order to provide PDF support, Project B's setup information does
not need to change, but the right packages will still be installed if needed.
As you can see, the ``extras_require`` argument takes a dictionary mapping
names of "extra" features, to strings or lists of strings describing those
features' requirements. These requirements will *not* be automatically
installed unless another package depends on them (directly or indirectly) by
including the desired "extras" in square brackets after the associated project
name. (Or if the extras were listed in a requirement spec on the EasyInstall
command line.)
Note, by the way, that if a project ends up not needing any other packages to
support a feature, it should keep an empty requirements list for that feature
in its ``extras_require`` argument, so that packages depending on that feature
don't break (due to an invalid feature name). For example, if Project A above
builds in PDF support and no longer needs ReportLab, it should change its
builds in PDF support and no longer needs ReportLab, it could change its
setup to this::
setup(
......@@ -417,7 +482,6 @@ so that Package B doesn't have to remove the ``[PDF]`` from its requirement
specifier.
Including Data Files
====================
......@@ -576,6 +640,8 @@ Extensible Applications and Frameworks
======================================
.. _Entry Points:
Dynamic Discovery of Services and Plugins
-----------------------------------------
......@@ -1776,6 +1842,12 @@ XXX
Release Notes/Change History
----------------------------
0.6a2
* Added ``console_scripts`` entry point group to allow installing scripts
without the need to create separate script files. On Windows, console
scripts get an ``.exe`` wrapper so you can just type their name. On other
platforms, the scripts are written without a file extension.
0.6a1
* Added support for building "old-style" RPMs that don't install an egg for
the target package, using a ``--no-egg`` option.
......
......@@ -101,6 +101,11 @@ class develop(easy_install):
return easy_install.install_egg_scripts(self,dist)
# create wrapper scripts in the script dir, pointing to dist.scripts
# new-style...
self.install_console_scripts(dist)
# ...and old-style
for script_name in self.distribution.scripts or []:
script_path = os.path.abspath(convert_path(script_name))
script_name = os.path.basename(script_path)
......@@ -116,8 +121,3 @@ class develop(easy_install):
......@@ -244,6 +244,19 @@ class easy_install(Command):
def install_egg_scripts(self, dist):
"""Write all the scripts for `dist`, unless scripts are excluded"""
self.install_console_scripts(dist)
if self.exclude_scripts or not dist.metadata_isdir('scripts'):
return
for script_name in dist.metadata_listdir('scripts'):
self.install_script(
dist, script_name,
dist.get_metadata('scripts/'+script_name).replace('\r','\n')
)
def add_output(self, path):
if os.path.isdir(path):
for base, dirs, files in os.walk(path):
......@@ -272,19 +285,6 @@ class easy_install(Command):
def easy_install(self, spec, deps=False):
tmpdir = tempfile.mkdtemp(prefix="easy_install-")
download = None
......@@ -408,16 +408,6 @@ class easy_install(Command):
)
def install_egg_scripts(self, dist):
if self.exclude_scripts or not dist.metadata_isdir('scripts'):
return
for script_name in dist.metadata_listdir('scripts'):
self.install_script(
dist, script_name,
dist.get_metadata('scripts/'+script_name).replace('\r','\n')
)
def should_unzip(self, dist):
if self.zip_ok is not None:
return not self.zip_ok
......@@ -449,23 +439,63 @@ class easy_install(Command):
ensure_directory(dst); shutil.move(setup_base, dst)
return dst
def install_console_scripts(self, dist):
"""Write new-style console scripts, unless excluded"""
if self.exclude_scripts:
return
spec = str(dist.as_requirement())
group = 'console_scripts'
for name,ep in dist.get_entry_map(group).items():
script_text = get_script_header("") + (
"# EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r\n"
"import sys\n"
"from pkg_resources import load_entry_point\n"
"\n"
"sys.exit(\n"
" load_entry_point(%(spec)r, %(group)r, %(name)r)()\n"
")\n"
) % locals()
if sys.platform=='win32':
# On Windows, add a .py extension and an .exe launcher
self.write_script(name+'.py', script_text)
self.write_script(
name+'.exe', resource_string('setuptools','launcher.exe'),
'b' # write in binary mode
)
else:
# On other platforms, we assume the right thing to do is to
# write the stub with no extension.
self.write_script(name, script_text)
def install_script(self, dist, script_name, script_text, dev_path=None):
log.info("Installing %s script to %s", script_name,self.script_dir)
target = os.path.join(self.script_dir, script_name)
first, rest = script_text.split('\n',1)
from distutils.command.build_scripts import first_line_re
match = first_line_re.match(first)
options = ''
if match:
options = match.group(1) or ''
if options:
options = ' '+options
"""Generate a legacy script wrapper and install it"""
spec = str(dist.as_requirement())
executable = os.path.normpath(sys.executable)
if dev_path:
script_text = (
"#!%(executable)s%(options)s\n"
script_text = get_script_header(script_text) + (
"# EASY-INSTALL-DEV-SCRIPT: %(spec)r,%(script_name)r\n"
"from pkg_resources import require; require(%(spec)r)\n"
"del require\n"
......@@ -473,23 +503,34 @@ class easy_install(Command):
"execfile(__file__)\n"
) % locals()
else:
script_text = (
"#!%(executable)s%(options)s\n"
script_text = get_script_header(script_text) + (
"#!python\n"
"# EASY-INSTALL-SCRIPT: %(spec)r,%(script_name)r\n"
"import pkg_resources\n"
"pkg_resources.run_script(%(spec)r, %(script_name)r)\n"
) % locals()
self.write_script(script_name, script_text)
def write_script(self, script_name, contents, mode="t"):
"""Write an executable file to the scripts directory"""
log.info("Installing %s script to %s", script_name, self.script_dir)
target = os.path.join(self.script_dir, script_name)
self.add_output(target)
if not self.dry_run:
ensure_directory(target)
f = open(target,"w")
f.write(script_text)
f = open(target,"w"+mode)
f.write(contents)
f.close()
try:
os.chmod(target,0755)
except (AttributeError, os.error):
pass
def install_eggs(self, spec, dist_filename, tmpdir):
# .egg dirs or files are already built, so just return them
if dist_filename.lower().endswith('.egg'):
......@@ -1118,26 +1159,26 @@ class PthDistributions(Environment):
Environment.remove(self,dist)
def main(argv, **kw):
from setuptools import setup
setup(script_args = ['-q','easy_install', '-v']+argv, **kw)
def get_script_header(script_text):
"""Create a #! line, getting options (if any) from script_text"""
from distutils.command.build_scripts import first_line_re
first, rest = (script_text+'\n').split('\n',1)
match = first_line_re.match(first)
options = ''
if match:
script_text = rest
options = match.group(1) or ''
if options:
options = ' '+options
executable = os.path.normpath(sys.executable)
return "#!%(executable)s%(options)s\n" % locals()
def main(argv=None, **kw):
from setuptools import setup
if argv is None:
argv = sys.argv[1:]
setup(script_args = ['-q','easy_install', '-v']+argv, **kw)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment