diff options
author | PJ Eby <distutils-sig@python.org> | 2005-09-17 01:13:02 +0000 |
---|---|---|
committer | PJ Eby <distutils-sig@python.org> | 2005-09-17 01:13:02 +0000 |
commit | 673ac23e93f64a287c16a0d0ea45ba9ab9379d2d (patch) | |
tree | b9aa9ca6d2049ccb783d15d7aa1d72826655e399 | |
parent | baad93e3fb9e3275fec745eb0383a212e6042dbe (diff) | |
download | external_python_setuptools-673ac23e93f64a287c16a0d0ea45ba9ab9379d2d.tar.gz external_python_setuptools-673ac23e93f64a287c16a0d0ea45ba9ab9379d2d.tar.bz2 external_python_setuptools-673ac23e93f64a287c16a0d0ea45ba9ab9379d2d.zip |
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
-rwxr-xr-x | EasyInstall.txt | 39 | ||||
-rwxr-xr-x | easy_install.py | 3 | ||||
-rwxr-xr-x | launcher.c | 123 | ||||
-rwxr-xr-x | setup.py | 51 | ||||
-rwxr-xr-x | setuptools.egg-info/entry_points.txt | 4 | ||||
-rwxr-xr-x | setuptools.txt | 96 | ||||
-rwxr-xr-x | setuptools/command/develop.py | 10 | ||||
-rwxr-xr-x | setuptools/command/easy_install.py | 157 | ||||
-rwxr-xr-x | setuptools/launcher.exe | bin | 0 -> 5120 bytes |
9 files changed, 397 insertions, 86 deletions
diff --git a/EasyInstall.txt b/EasyInstall.txt index b3145fee..bcbeafd7 100755 --- a/EasyInstall.txt +++ b/EasyInstall.txt @@ -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. diff --git a/easy_install.py b/easy_install.py index 4b204c47..bc2270c0 100755 --- a/easy_install.py +++ b/easy_install.py @@ -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:]) diff --git a/launcher.c b/launcher.c new file mode 100755 index 00000000..5896805f --- /dev/null +++ b/launcher.c @@ -0,0 +1,123 @@ +/* + 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'], ) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setuptools.egg-info/entry_points.txt b/setuptools.egg-info/entry_points.txt index 235330f4..37a81910 100755 --- a/setuptools.egg-info/entry_points.txt +++ b/setuptools.egg-info/entry_points.txt @@ -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 diff --git a/setuptools.txt b/setuptools.txt index 854da77a..b7166ae4 100755 --- a/setuptools.txt +++ b/setuptools.txt @@ -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. diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py index 1eb8bf6b..24875467 100755 --- a/setuptools/command/develop.py +++ b/setuptools/command/develop.py @@ -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): - - - - - diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 23e80343..ac7fe5a4 100755 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -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) diff --git a/setuptools/launcher.exe b/setuptools/launcher.exe Binary files differnew file mode 100755 index 00000000..f2f5dd5c --- /dev/null +++ b/setuptools/launcher.exe |