Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 1 | """Miscellaneous utility functions.""" |
| 2 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 3 | import os |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 4 | import re |
Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 5 | import csv |
Éric Araujo | a29e4f6 | 2011-10-08 04:09:15 +0200 | [diff] [blame] | 6 | import imp |
Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 7 | import sys |
| 8 | import errno |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 9 | import codecs |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 10 | import shutil |
| 11 | import string |
Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 12 | import hashlib |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 13 | import posixpath |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 14 | import subprocess |
Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 15 | import sysconfig |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 16 | from glob import iglob as std_iglob |
| 17 | from fnmatch import fnmatchcase |
| 18 | from inspect import getsource |
| 19 | from configparser import RawConfigParser |
| 20 | |
| 21 | from packaging import logger |
| 22 | from packaging.errors import (PackagingPlatformError, PackagingFileError, |
Éric Araujo | 8808015 | 2011-11-03 05:08:28 +0100 | [diff] [blame] | 23 | PackagingExecError, InstallationException, |
| 24 | PackagingInternalError) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 25 | |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 26 | __all__ = [ |
| 27 | # file dependencies |
| 28 | 'newer', 'newer_group', |
| 29 | # helpers for commands (dry-run system) |
| 30 | 'execute', 'write_file', |
| 31 | # spawning programs |
| 32 | 'find_executable', 'spawn', |
| 33 | # path manipulation |
| 34 | 'convert_path', 'change_root', |
| 35 | # 2to3 conversion |
| 36 | 'Mixin2to3', 'run_2to3', |
| 37 | # packaging compatibility helpers |
| 38 | 'cfg_to_args', 'generate_setup_py', |
| 39 | 'egginfo_to_distinfo', |
| 40 | 'get_install_method', |
| 41 | # misc |
| 42 | 'ask', 'check_environ', 'encode_multipart', 'resolve_name', |
| 43 | # querying for information TODO move to sysconfig |
| 44 | 'get_compiler_versions', 'get_platform', 'set_platform', |
| 45 | # configuration TODO move to packaging.config |
| 46 | 'get_pypirc_path', 'read_pypirc', 'generate_pypirc', |
| 47 | 'strtobool', 'split_multiline', |
| 48 | ] |
| 49 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 50 | _PLATFORM = None |
| 51 | _DEFAULT_INSTALLER = 'packaging' |
| 52 | |
| 53 | |
| 54 | def newer(source, target): |
| 55 | """Tell if the target is newer than the source. |
| 56 | |
| 57 | Returns true if 'source' exists and is more recently modified than |
| 58 | 'target', or if 'source' exists and 'target' doesn't. |
| 59 | |
| 60 | Returns false if both exist and 'target' is the same age or younger |
| 61 | than 'source'. Raise PackagingFileError if 'source' does not exist. |
| 62 | |
| 63 | Note that this test is not very accurate: files created in the same second |
| 64 | will have the same "age". |
| 65 | """ |
| 66 | if not os.path.exists(source): |
| 67 | raise PackagingFileError("file '%s' does not exist" % |
| 68 | os.path.abspath(source)) |
| 69 | if not os.path.exists(target): |
| 70 | return True |
| 71 | |
| 72 | return os.stat(source).st_mtime > os.stat(target).st_mtime |
| 73 | |
| 74 | |
| 75 | def get_platform(): |
| 76 | """Return a string that identifies the current platform. |
| 77 | |
| 78 | By default, will return the value returned by sysconfig.get_platform(), |
| 79 | but it can be changed by calling set_platform(). |
| 80 | """ |
| 81 | global _PLATFORM |
| 82 | if _PLATFORM is None: |
| 83 | _PLATFORM = sysconfig.get_platform() |
| 84 | return _PLATFORM |
| 85 | |
| 86 | |
| 87 | def set_platform(identifier): |
| 88 | """Set the platform string identifier returned by get_platform(). |
| 89 | |
| 90 | Note that this change doesn't impact the value returned by |
| 91 | sysconfig.get_platform(); it is local to packaging. |
| 92 | """ |
| 93 | global _PLATFORM |
| 94 | _PLATFORM = identifier |
| 95 | |
| 96 | |
| 97 | def convert_path(pathname): |
| 98 | """Return 'pathname' as a name that will work on the native filesystem. |
| 99 | |
| 100 | The path is split on '/' and put back together again using the current |
| 101 | directory separator. Needed because filenames in the setup script are |
| 102 | always supplied in Unix style, and have to be converted to the local |
| 103 | convention before we can actually use them in the filesystem. Raises |
| 104 | ValueError on non-Unix-ish systems if 'pathname' either starts or |
| 105 | ends with a slash. |
| 106 | """ |
| 107 | if os.sep == '/': |
| 108 | return pathname |
| 109 | if not pathname: |
| 110 | return pathname |
| 111 | if pathname[0] == '/': |
| 112 | raise ValueError("path '%s' cannot be absolute" % pathname) |
| 113 | if pathname[-1] == '/': |
| 114 | raise ValueError("path '%s' cannot end with '/'" % pathname) |
| 115 | |
| 116 | paths = pathname.split('/') |
| 117 | while os.curdir in paths: |
| 118 | paths.remove(os.curdir) |
| 119 | if not paths: |
| 120 | return os.curdir |
| 121 | return os.path.join(*paths) |
| 122 | |
| 123 | |
| 124 | def change_root(new_root, pathname): |
| 125 | """Return 'pathname' with 'new_root' prepended. |
| 126 | |
| 127 | If 'pathname' is relative, this is equivalent to |
| 128 | os.path.join(new_root,pathname). Otherwise, it requires making 'pathname' |
| 129 | relative and then joining the two, which is tricky on DOS/Windows. |
| 130 | """ |
| 131 | if os.name == 'posix': |
| 132 | if not os.path.isabs(pathname): |
| 133 | return os.path.join(new_root, pathname) |
| 134 | else: |
| 135 | return os.path.join(new_root, pathname[1:]) |
| 136 | |
| 137 | elif os.name == 'nt': |
| 138 | drive, path = os.path.splitdrive(pathname) |
| 139 | if path[0] == '\\': |
| 140 | path = path[1:] |
| 141 | return os.path.join(new_root, path) |
| 142 | |
| 143 | elif os.name == 'os2': |
| 144 | drive, path = os.path.splitdrive(pathname) |
| 145 | if path[0] == os.sep: |
| 146 | path = path[1:] |
| 147 | return os.path.join(new_root, path) |
| 148 | |
| 149 | else: |
| 150 | raise PackagingPlatformError("nothing known about " |
| 151 | "platform '%s'" % os.name) |
| 152 | |
| 153 | _environ_checked = False |
| 154 | |
| 155 | |
| 156 | def check_environ(): |
| 157 | """Ensure that 'os.environ' has all the environment variables needed. |
| 158 | |
| 159 | We guarantee that users can use in config files, command-line options, |
| 160 | etc. Currently this includes: |
| 161 | HOME - user's home directory (Unix only) |
| 162 | PLAT - description of the current platform, including hardware |
| 163 | and OS (see 'get_platform()') |
| 164 | """ |
| 165 | global _environ_checked |
| 166 | if _environ_checked: |
| 167 | return |
| 168 | |
| 169 | if os.name == 'posix' and 'HOME' not in os.environ: |
| 170 | import pwd |
| 171 | os.environ['HOME'] = pwd.getpwuid(os.getuid())[5] |
| 172 | |
| 173 | if 'PLAT' not in os.environ: |
| 174 | os.environ['PLAT'] = sysconfig.get_platform() |
| 175 | |
| 176 | _environ_checked = True |
| 177 | |
| 178 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 179 | # Needed by 'split_quoted()' |
| 180 | _wordchars_re = _squote_re = _dquote_re = None |
| 181 | |
| 182 | |
| 183 | def _init_regex(): |
| 184 | global _wordchars_re, _squote_re, _dquote_re |
| 185 | _wordchars_re = re.compile(r'[^\\\'\"%s ]*' % string.whitespace) |
| 186 | _squote_re = re.compile(r"'(?:[^'\\]|\\.)*'") |
| 187 | _dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"') |
| 188 | |
| 189 | |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 190 | # TODO replace with shlex.split after testing |
| 191 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 192 | def split_quoted(s): |
| 193 | """Split a string up according to Unix shell-like rules for quotes and |
| 194 | backslashes. |
| 195 | |
| 196 | In short: words are delimited by spaces, as long as those |
| 197 | spaces are not escaped by a backslash, or inside a quoted string. |
| 198 | Single and double quotes are equivalent, and the quote characters can |
| 199 | be backslash-escaped. The backslash is stripped from any two-character |
| 200 | escape sequence, leaving only the escaped character. The quote |
| 201 | characters are stripped from any quoted string. Returns a list of |
| 202 | words. |
| 203 | """ |
| 204 | # This is a nice algorithm for splitting up a single string, since it |
| 205 | # doesn't require character-by-character examination. It was a little |
| 206 | # bit of a brain-bender to get it working right, though... |
| 207 | if _wordchars_re is None: |
| 208 | _init_regex() |
| 209 | |
| 210 | s = s.strip() |
| 211 | words = [] |
| 212 | pos = 0 |
| 213 | |
| 214 | while s: |
| 215 | m = _wordchars_re.match(s, pos) |
| 216 | end = m.end() |
| 217 | if end == len(s): |
| 218 | words.append(s[:end]) |
| 219 | break |
| 220 | |
| 221 | if s[end] in string.whitespace: # unescaped, unquoted whitespace: now |
| 222 | words.append(s[:end]) # we definitely have a word delimiter |
| 223 | s = s[end:].lstrip() |
| 224 | pos = 0 |
| 225 | |
| 226 | elif s[end] == '\\': # preserve whatever is being escaped; |
| 227 | # will become part of the current word |
| 228 | s = s[:end] + s[end + 1:] |
| 229 | pos = end + 1 |
| 230 | |
| 231 | else: |
| 232 | if s[end] == "'": # slurp singly-quoted string |
| 233 | m = _squote_re.match(s, end) |
| 234 | elif s[end] == '"': # slurp doubly-quoted string |
| 235 | m = _dquote_re.match(s, end) |
| 236 | else: |
| 237 | raise RuntimeError("this can't happen " |
| 238 | "(bad char '%c')" % s[end]) |
| 239 | |
| 240 | if m is None: |
| 241 | raise ValueError("bad string (mismatched %s quotes?)" % s[end]) |
| 242 | |
| 243 | beg, end = m.span() |
| 244 | s = s[:beg] + s[beg + 1:end - 1] + s[end:] |
| 245 | pos = m.end() - 2 |
| 246 | |
| 247 | if pos >= len(s): |
| 248 | words.append(s) |
| 249 | break |
| 250 | |
| 251 | return words |
| 252 | |
| 253 | |
Éric Araujo | 1c1d9a5 | 2011-06-10 23:26:31 +0200 | [diff] [blame] | 254 | def split_multiline(value): |
| 255 | """Split a multiline string into a list, excluding blank lines.""" |
| 256 | |
| 257 | return [element for element in |
| 258 | (line.strip() for line in value.split('\n')) |
| 259 | if element] |
| 260 | |
| 261 | |
Éric Araujo | 4d15546 | 2011-11-15 11:43:20 +0100 | [diff] [blame] | 262 | def execute(func, args, msg=None, dry_run=False): |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 263 | """Perform some action that affects the outside world. |
| 264 | |
| 265 | Some actions (e.g. writing to the filesystem) are special because |
| 266 | they are disabled by the 'dry_run' flag. This method takes care of all |
| 267 | that bureaucracy for you; all you have to do is supply the |
| 268 | function to call and an argument tuple for it (to embody the |
| 269 | "external action" being performed), and an optional message to |
| 270 | print. |
| 271 | """ |
| 272 | if msg is None: |
| 273 | msg = "%s%r" % (func.__name__, args) |
| 274 | if msg[-2:] == ',)': # correct for singleton tuple |
| 275 | msg = msg[0:-2] + ')' |
| 276 | |
| 277 | logger.info(msg) |
| 278 | if not dry_run: |
| 279 | func(*args) |
| 280 | |
| 281 | |
| 282 | def strtobool(val): |
Éric Araujo | d5d831b | 2011-06-06 01:13:48 +0200 | [diff] [blame] | 283 | """Convert a string representation of truth to a boolean. |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 284 | |
| 285 | True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values |
| 286 | are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if |
| 287 | 'val' is anything else. |
| 288 | """ |
| 289 | val = val.lower() |
| 290 | if val in ('y', 'yes', 't', 'true', 'on', '1'): |
| 291 | return True |
| 292 | elif val in ('n', 'no', 'f', 'false', 'off', '0'): |
| 293 | return False |
| 294 | else: |
| 295 | raise ValueError("invalid truth value %r" % (val,)) |
| 296 | |
| 297 | |
| 298 | def byte_compile(py_files, optimize=0, force=False, prefix=None, |
Éric Araujo | f836162 | 2011-11-14 18:10:19 +0100 | [diff] [blame] | 299 | base_dir=None, dry_run=False, direct=None): |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 300 | """Byte-compile a collection of Python source files to either .pyc |
Éric Araujo | a29e4f6 | 2011-10-08 04:09:15 +0200 | [diff] [blame] | 301 | or .pyo files in a __pycache__ subdirectory. |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 302 | |
| 303 | 'py_files' is a list of files to compile; any files that don't end in |
| 304 | ".py" are silently skipped. 'optimize' must be one of the following: |
| 305 | 0 - don't optimize (generate .pyc) |
| 306 | 1 - normal optimization (like "python -O") |
| 307 | 2 - extra optimization (like "python -OO") |
Éric Araujo | f836162 | 2011-11-14 18:10:19 +0100 | [diff] [blame] | 308 | This function is independent from the running Python's -O or -B options; |
| 309 | it is fully controlled by the parameters passed in. |
| 310 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 311 | If 'force' is true, all files are recompiled regardless of |
| 312 | timestamps. |
| 313 | |
| 314 | The source filename encoded in each bytecode file defaults to the |
| 315 | filenames listed in 'py_files'; you can modify these with 'prefix' and |
| 316 | 'basedir'. 'prefix' is a string that will be stripped off of each |
| 317 | source filename, and 'base_dir' is a directory name that will be |
| 318 | prepended (after 'prefix' is stripped). You can supply either or both |
| 319 | (or neither) of 'prefix' and 'base_dir', as you wish. |
| 320 | |
| 321 | If 'dry_run' is true, doesn't actually do anything that would |
| 322 | affect the filesystem. |
| 323 | |
| 324 | Byte-compilation is either done directly in this interpreter process |
| 325 | with the standard py_compile module, or indirectly by writing a |
| 326 | temporary script and executing it. Normally, you should let |
| 327 | 'byte_compile()' figure out to use direct compilation or not (see |
| 328 | the source for details). The 'direct' flag is used by the script |
| 329 | generated in indirect mode; unless you know what you're doing, leave |
| 330 | it set to None. |
Éric Araujo | 8808015 | 2011-11-03 05:08:28 +0100 | [diff] [blame] | 331 | """ |
Éric Araujo | f836162 | 2011-11-14 18:10:19 +0100 | [diff] [blame] | 332 | # FIXME use compileall + remove direct/indirect shenanigans |
| 333 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 334 | # First, if the caller didn't force us into direct or indirect mode, |
| 335 | # figure out which mode we should be in. We take a conservative |
| 336 | # approach: choose direct mode *only* if the current interpreter is |
| 337 | # in debug mode and optimize is 0. If we're not in debug mode (-O |
| 338 | # or -OO), we don't know which level of optimization this |
| 339 | # interpreter is running with, so we can't do direct |
| 340 | # byte-compilation and be certain that it's the right thing. Thus, |
| 341 | # always compile indirectly if the current interpreter is in either |
| 342 | # optimize mode, or if either optimization level was requested by |
| 343 | # the caller. |
| 344 | if direct is None: |
| 345 | direct = (__debug__ and optimize == 0) |
| 346 | |
| 347 | # "Indirect" byte-compilation: write a temporary script and then |
| 348 | # run it with the appropriate flags. |
| 349 | if not direct: |
| 350 | from tempfile import mkstemp |
Éric Araujo | 7724a6c | 2011-09-17 03:31:51 +0200 | [diff] [blame] | 351 | # XXX use something better than mkstemp |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 352 | script_fd, script_name = mkstemp(".py") |
Éric Araujo | 7724a6c | 2011-09-17 03:31:51 +0200 | [diff] [blame] | 353 | os.close(script_fd) |
| 354 | script_fd = None |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 355 | logger.info("writing byte-compilation script '%s'", script_name) |
| 356 | if not dry_run: |
| 357 | if script_fd is not None: |
Victor Stinner | 9cf6d13 | 2011-05-19 21:42:47 +0200 | [diff] [blame] | 358 | script = os.fdopen(script_fd, "w", encoding='utf-8') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 359 | else: |
Victor Stinner | 9cf6d13 | 2011-05-19 21:42:47 +0200 | [diff] [blame] | 360 | script = open(script_name, "w", encoding='utf-8') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 361 | |
Victor Stinner | 21a9c74 | 2011-05-19 15:51:27 +0200 | [diff] [blame] | 362 | with script: |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 363 | script.write("""\ |
| 364 | from packaging.util import byte_compile |
| 365 | files = [ |
| 366 | """) |
| 367 | |
| 368 | # XXX would be nice to write absolute filenames, just for |
| 369 | # safety's sake (script should be more robust in the face of |
| 370 | # chdir'ing before running it). But this requires abspath'ing |
| 371 | # 'prefix' as well, and that breaks the hack in build_lib's |
| 372 | # 'byte_compile()' method that carefully tacks on a trailing |
| 373 | # slash (os.sep really) to make sure the prefix here is "just |
| 374 | # right". This whole prefix business is rather delicate -- the |
| 375 | # problem is that it's really a directory, but I'm treating it |
| 376 | # as a dumb string, so trailing slashes and so forth matter. |
| 377 | |
| 378 | #py_files = map(os.path.abspath, py_files) |
| 379 | #if prefix: |
| 380 | # prefix = os.path.abspath(prefix) |
| 381 | |
| 382 | script.write(",\n".join(map(repr, py_files)) + "]\n") |
| 383 | script.write(""" |
| 384 | byte_compile(files, optimize=%r, force=%r, |
| 385 | prefix=%r, base_dir=%r, |
Éric Araujo | f836162 | 2011-11-14 18:10:19 +0100 | [diff] [blame] | 386 | dry_run=False, |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 387 | direct=True) |
Éric Araujo | f836162 | 2011-11-14 18:10:19 +0100 | [diff] [blame] | 388 | """ % (optimize, force, prefix, base_dir)) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 389 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 390 | cmd = [sys.executable, script_name] |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 391 | |
Éric Araujo | 088025f | 2011-06-04 18:45:40 +0200 | [diff] [blame] | 392 | env = os.environ.copy() |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 393 | env['PYTHONPATH'] = os.path.pathsep.join(sys.path) |
| 394 | try: |
| 395 | spawn(cmd, env=env) |
| 396 | finally: |
| 397 | execute(os.remove, (script_name,), "removing %s" % script_name, |
| 398 | dry_run=dry_run) |
| 399 | |
| 400 | # "Direct" byte-compilation: use the py_compile module to compile |
| 401 | # right here, right now. Note that the script generated in indirect |
| 402 | # mode simply calls 'byte_compile()' in direct mode, a weird sort of |
| 403 | # cross-process recursion. Hey, it works! |
| 404 | else: |
| 405 | from py_compile import compile |
| 406 | |
| 407 | for file in py_files: |
| 408 | if file[-3:] != ".py": |
| 409 | # This lets us be lazy and not filter filenames in |
| 410 | # the "install_lib" command. |
| 411 | continue |
| 412 | |
| 413 | # Terminology from the py_compile module: |
| 414 | # cfile - byte-compiled file |
| 415 | # dfile - purported source filename (same as 'file' by default) |
Éric Araujo | f836162 | 2011-11-14 18:10:19 +0100 | [diff] [blame] | 416 | # The second argument to cache_from_source forces the extension to |
| 417 | # be .pyc (if true) or .pyo (if false); without it, the extension |
| 418 | # would depend on the calling Python's -O option |
| 419 | cfile = imp.cache_from_source(file, not optimize) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 420 | dfile = file |
Éric Araujo | 8808015 | 2011-11-03 05:08:28 +0100 | [diff] [blame] | 421 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 422 | if prefix: |
| 423 | if file[:len(prefix)] != prefix: |
| 424 | raise ValueError("invalid prefix: filename %r doesn't " |
| 425 | "start with %r" % (file, prefix)) |
| 426 | dfile = dfile[len(prefix):] |
| 427 | if base_dir: |
| 428 | dfile = os.path.join(base_dir, dfile) |
| 429 | |
| 430 | cfile_base = os.path.basename(cfile) |
| 431 | if direct: |
| 432 | if force or newer(file, cfile): |
| 433 | logger.info("byte-compiling %s to %s", file, cfile_base) |
| 434 | if not dry_run: |
| 435 | compile(file, cfile, dfile) |
| 436 | else: |
| 437 | logger.debug("skipping byte-compilation of %s to %s", |
| 438 | file, cfile_base) |
| 439 | |
| 440 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 441 | _RE_VERSION = re.compile('(\d+\.\d+(\.\d+)*)') |
| 442 | _MAC_OS_X_LD_VERSION = re.compile('^@\(#\)PROGRAM:ld ' |
| 443 | 'PROJECT:ld64-((\d+)(\.\d+)*)') |
| 444 | |
| 445 | |
| 446 | def _find_ld_version(): |
| 447 | """Find the ld version. The version scheme differs under Mac OS X.""" |
| 448 | if sys.platform == 'darwin': |
| 449 | return _find_exe_version('ld -v', _MAC_OS_X_LD_VERSION) |
| 450 | else: |
| 451 | return _find_exe_version('ld -v') |
| 452 | |
| 453 | |
| 454 | def _find_exe_version(cmd, pattern=_RE_VERSION): |
| 455 | """Find the version of an executable by running `cmd` in the shell. |
| 456 | |
| 457 | `pattern` is a compiled regular expression. If not provided, defaults |
| 458 | to _RE_VERSION. If the command is not found, or the output does not |
| 459 | match the mattern, returns None. |
| 460 | """ |
| 461 | from subprocess import Popen, PIPE |
| 462 | executable = cmd.split()[0] |
| 463 | if find_executable(executable) is None: |
| 464 | return None |
| 465 | pipe = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) |
| 466 | try: |
Victor Stinner | 9904b22 | 2011-05-21 02:20:36 +0200 | [diff] [blame] | 467 | stdout, stderr = pipe.communicate() |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 468 | finally: |
| 469 | pipe.stdout.close() |
| 470 | pipe.stderr.close() |
| 471 | # some commands like ld under MacOS X, will give the |
| 472 | # output in the stderr, rather than stdout. |
| 473 | if stdout != '': |
| 474 | out_string = stdout |
| 475 | else: |
| 476 | out_string = stderr |
| 477 | |
| 478 | result = pattern.search(out_string) |
| 479 | if result is None: |
| 480 | return None |
| 481 | return result.group(1) |
| 482 | |
| 483 | |
| 484 | def get_compiler_versions(): |
| 485 | """Return a tuple providing the versions of gcc, ld and dllwrap |
| 486 | |
| 487 | For each command, if a command is not found, None is returned. |
| 488 | Otherwise a string with the version is returned. |
| 489 | """ |
| 490 | gcc = _find_exe_version('gcc -dumpversion') |
| 491 | ld = _find_ld_version() |
| 492 | dllwrap = _find_exe_version('dllwrap --version') |
| 493 | return gcc, ld, dllwrap |
| 494 | |
| 495 | |
| 496 | def newer_group(sources, target, missing='error'): |
| 497 | """Return true if 'target' is out-of-date with respect to any file |
| 498 | listed in 'sources'. |
| 499 | |
| 500 | In other words, if 'target' exists and is newer |
| 501 | than every file in 'sources', return false; otherwise return true. |
| 502 | 'missing' controls what we do when a source file is missing; the |
| 503 | default ("error") is to blow up with an OSError from inside 'stat()'; |
| 504 | if it is "ignore", we silently drop any missing source files; if it is |
| 505 | "newer", any missing source files make us assume that 'target' is |
| 506 | out-of-date (this is handy in "dry-run" mode: it'll make you pretend to |
| 507 | carry out commands that wouldn't work because inputs are missing, but |
| 508 | that doesn't matter because you're not actually going to run the |
| 509 | commands). |
| 510 | """ |
| 511 | # If the target doesn't even exist, then it's definitely out-of-date. |
| 512 | if not os.path.exists(target): |
| 513 | return True |
| 514 | |
| 515 | # Otherwise we have to find out the hard way: if *any* source file |
| 516 | # is more recent than 'target', then 'target' is out-of-date and |
| 517 | # we can immediately return true. If we fall through to the end |
| 518 | # of the loop, then 'target' is up-to-date and we return false. |
| 519 | target_mtime = os.stat(target).st_mtime |
| 520 | |
| 521 | for source in sources: |
| 522 | if not os.path.exists(source): |
| 523 | if missing == 'error': # blow up when we stat() the file |
| 524 | pass |
| 525 | elif missing == 'ignore': # missing source dropped from |
| 526 | continue # target's dependency list |
| 527 | elif missing == 'newer': # missing source means target is |
| 528 | return True # out-of-date |
| 529 | |
| 530 | if os.stat(source).st_mtime > target_mtime: |
| 531 | return True |
| 532 | |
| 533 | return False |
| 534 | |
| 535 | |
| 536 | def write_file(filename, contents): |
| 537 | """Create *filename* and write *contents* to it. |
| 538 | |
| 539 | *contents* is a sequence of strings without line terminators. |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 540 | |
| 541 | This functions is not intended to replace the usual with open + write |
| 542 | idiom in all cases, only with Command.execute, which runs depending on |
| 543 | the dry_run argument and also logs its arguments). |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 544 | """ |
| 545 | with open(filename, "w") as f: |
| 546 | for line in contents: |
| 547 | f.write(line + "\n") |
| 548 | |
| 549 | |
| 550 | def _is_package(path): |
Éric Araujo | 1c1d9a5 | 2011-06-10 23:26:31 +0200 | [diff] [blame] | 551 | return os.path.isdir(path) and os.path.isfile( |
| 552 | os.path.join(path, '__init__.py')) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 553 | |
| 554 | |
| 555 | # Code taken from the pip project |
| 556 | def _is_archive_file(name): |
| 557 | archives = ('.zip', '.tar.gz', '.tar.bz2', '.tgz', '.tar') |
| 558 | ext = splitext(name)[1].lower() |
Éric Araujo | 1c1d9a5 | 2011-06-10 23:26:31 +0200 | [diff] [blame] | 559 | return ext in archives |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 560 | |
| 561 | |
| 562 | def _under(path, root): |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 563 | # XXX use os.path |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 564 | path = path.split(os.sep) |
| 565 | root = root.split(os.sep) |
| 566 | if len(root) > len(path): |
| 567 | return False |
| 568 | for pos, part in enumerate(root): |
| 569 | if path[pos] != part: |
| 570 | return False |
| 571 | return True |
| 572 | |
| 573 | |
| 574 | def _package_name(root_path, path): |
| 575 | # Return a dotted package name, given a subpath |
| 576 | if not _under(path, root_path): |
| 577 | raise ValueError('"%s" is not a subpath of "%s"' % (path, root_path)) |
| 578 | return path[len(root_path) + 1:].replace(os.sep, '.') |
| 579 | |
| 580 | |
| 581 | def find_packages(paths=(os.curdir,), exclude=()): |
| 582 | """Return a list all Python packages found recursively within |
| 583 | directories 'paths' |
| 584 | |
| 585 | 'paths' should be supplied as a sequence of "cross-platform" |
| 586 | (i.e. URL-style) path; it will be converted to the appropriate local |
| 587 | path syntax. |
| 588 | |
| 589 | 'exclude' is a sequence of package names to exclude; '*' can be used as |
| 590 | a wildcard in the names, such that 'foo.*' will exclude all subpackages |
| 591 | of 'foo' (but not 'foo' itself). |
| 592 | """ |
| 593 | packages = [] |
| 594 | discarded = [] |
| 595 | |
| 596 | def _discarded(path): |
| 597 | for discard in discarded: |
| 598 | if _under(path, discard): |
| 599 | return True |
| 600 | return False |
| 601 | |
| 602 | for path in paths: |
| 603 | path = convert_path(path) |
| 604 | for root, dirs, files in os.walk(path): |
| 605 | for dir_ in dirs: |
| 606 | fullpath = os.path.join(root, dir_) |
| 607 | if _discarded(fullpath): |
| 608 | continue |
| 609 | # we work only with Python packages |
| 610 | if not _is_package(fullpath): |
| 611 | discarded.append(fullpath) |
| 612 | continue |
| 613 | # see if it's excluded |
| 614 | excluded = False |
| 615 | package_name = _package_name(path, fullpath) |
| 616 | for pattern in exclude: |
| 617 | if fnmatchcase(package_name, pattern): |
| 618 | excluded = True |
| 619 | break |
| 620 | if excluded: |
| 621 | continue |
| 622 | |
| 623 | # adding it to the list |
| 624 | packages.append(package_name) |
| 625 | return packages |
| 626 | |
| 627 | |
| 628 | def resolve_name(name): |
| 629 | """Resolve a name like ``module.object`` to an object and return it. |
| 630 | |
Éric Araujo | 8ccd18f | 2011-10-19 06:46:13 +0200 | [diff] [blame] | 631 | This functions supports packages and attributes without depth limitation: |
| 632 | ``package.package.module.class.class.function.attr`` is valid input. |
| 633 | However, looking up builtins is not directly supported: use |
| 634 | ``builtins.name``. |
| 635 | |
| 636 | Raises ImportError if importing the module fails or if one requested |
| 637 | attribute is not found. |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 638 | """ |
Éric Araujo | 8ccd18f | 2011-10-19 06:46:13 +0200 | [diff] [blame] | 639 | if '.' not in name: |
| 640 | # shortcut |
| 641 | __import__(name) |
| 642 | return sys.modules[name] |
| 643 | |
| 644 | # FIXME clean up this code! |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 645 | parts = name.split('.') |
| 646 | cursor = len(parts) |
| 647 | module_name = parts[:cursor] |
Éric Araujo | 8ccd18f | 2011-10-19 06:46:13 +0200 | [diff] [blame] | 648 | ret = '' |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 649 | |
| 650 | while cursor > 0: |
| 651 | try: |
| 652 | ret = __import__('.'.join(module_name)) |
| 653 | break |
| 654 | except ImportError: |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 655 | cursor -= 1 |
| 656 | module_name = parts[:cursor] |
Éric Araujo | 8ccd18f | 2011-10-19 06:46:13 +0200 | [diff] [blame] | 657 | |
| 658 | if ret == '': |
| 659 | raise ImportError(parts[0]) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 660 | |
| 661 | for part in parts[1:]: |
| 662 | try: |
| 663 | ret = getattr(ret, part) |
| 664 | except AttributeError as exc: |
| 665 | raise ImportError(exc) |
| 666 | |
| 667 | return ret |
| 668 | |
| 669 | |
| 670 | def splitext(path): |
| 671 | """Like os.path.splitext, but take off .tar too""" |
| 672 | base, ext = posixpath.splitext(path) |
| 673 | if base.lower().endswith('.tar'): |
| 674 | ext = base[-4:] + ext |
| 675 | base = base[:-4] |
| 676 | return base, ext |
| 677 | |
| 678 | |
Ned Deily | fceb412 | 2011-06-28 20:04:24 -0700 | [diff] [blame] | 679 | if sys.platform == 'darwin': |
| 680 | _cfg_target = None |
| 681 | _cfg_target_split = None |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 682 | |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 683 | |
Éric Araujo | 4d15546 | 2011-11-15 11:43:20 +0100 | [diff] [blame] | 684 | def spawn(cmd, search_path=True, dry_run=False, env=None): |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 685 | """Run another program specified as a command list 'cmd' in a new process. |
| 686 | |
| 687 | 'cmd' is just the argument list for the new process, ie. |
| 688 | cmd[0] is the program to run and cmd[1:] are the rest of its arguments. |
| 689 | There is no way to run a program with a name different from that of its |
| 690 | executable. |
| 691 | |
| 692 | If 'search_path' is true (the default), the system's executable |
| 693 | search path will be used to find the program; otherwise, cmd[0] |
| 694 | must be the exact path to the executable. If 'dry_run' is true, |
| 695 | the command will not actually be run. |
| 696 | |
| 697 | If 'env' is given, it's a environment dictionary used for the execution |
| 698 | environment. |
| 699 | |
| 700 | Raise PackagingExecError if running the program fails in any way; just |
| 701 | return on success. |
| 702 | """ |
Éric Araujo | 6280606 | 2011-06-11 09:46:07 +0200 | [diff] [blame] | 703 | logger.debug('spawn: running %r', cmd) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 704 | if dry_run: |
Éric Araujo | 29f6297 | 2011-08-04 17:17:07 +0200 | [diff] [blame] | 705 | logger.debug('dry run, no process actually spawned') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 706 | return |
Ned Deily | fceb412 | 2011-06-28 20:04:24 -0700 | [diff] [blame] | 707 | if sys.platform == 'darwin': |
| 708 | global _cfg_target, _cfg_target_split |
| 709 | if _cfg_target is None: |
| 710 | _cfg_target = sysconfig.get_config_var( |
| 711 | 'MACOSX_DEPLOYMENT_TARGET') or '' |
| 712 | if _cfg_target: |
| 713 | _cfg_target_split = [int(x) for x in _cfg_target.split('.')] |
| 714 | if _cfg_target: |
| 715 | # ensure that the deployment target of build process is not less |
| 716 | # than that used when the interpreter was built. This ensures |
| 717 | # extension modules are built with correct compatibility values |
| 718 | env = env or os.environ |
| 719 | cur_target = env.get('MACOSX_DEPLOYMENT_TARGET', _cfg_target) |
| 720 | if _cfg_target_split > [int(x) for x in cur_target.split('.')]: |
| 721 | my_msg = ('$MACOSX_DEPLOYMENT_TARGET mismatch: ' |
| 722 | 'now "%s" but "%s" during configure' |
| 723 | % (cur_target, _cfg_target)) |
| 724 | raise PackagingPlatformError(my_msg) |
| 725 | env = dict(env, MACOSX_DEPLOYMENT_TARGET=cur_target) |
| 726 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 727 | exit_status = subprocess.call(cmd, env=env) |
| 728 | if exit_status != 0: |
Éric Araujo | 6280606 | 2011-06-11 09:46:07 +0200 | [diff] [blame] | 729 | msg = "command %r failed with exit status %d" |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 730 | raise PackagingExecError(msg % (cmd, exit_status)) |
| 731 | |
| 732 | |
| 733 | def find_executable(executable, path=None): |
| 734 | """Try to find 'executable' in the directories listed in 'path'. |
| 735 | |
| 736 | *path* is a string listing directories separated by 'os.pathsep' and |
| 737 | defaults to os.environ['PATH']. Returns the complete filename or None |
| 738 | if not found. |
| 739 | """ |
| 740 | if path is None: |
| 741 | path = os.environ['PATH'] |
| 742 | paths = path.split(os.pathsep) |
| 743 | base, ext = os.path.splitext(executable) |
| 744 | |
| 745 | if (sys.platform == 'win32' or os.name == 'os2') and (ext != '.exe'): |
| 746 | executable = executable + '.exe' |
| 747 | |
| 748 | if not os.path.isfile(executable): |
| 749 | for p in paths: |
| 750 | f = os.path.join(p, executable) |
| 751 | if os.path.isfile(f): |
| 752 | # the file exists, we have a shot at spawn working |
| 753 | return f |
| 754 | return None |
| 755 | else: |
| 756 | return executable |
| 757 | |
| 758 | |
| 759 | DEFAULT_REPOSITORY = 'http://pypi.python.org/pypi' |
| 760 | DEFAULT_REALM = 'pypi' |
| 761 | DEFAULT_PYPIRC = """\ |
| 762 | [distutils] |
| 763 | index-servers = |
| 764 | pypi |
| 765 | |
| 766 | [pypi] |
| 767 | username:%s |
| 768 | password:%s |
| 769 | """ |
| 770 | |
| 771 | |
| 772 | def get_pypirc_path(): |
| 773 | """Return path to pypirc config file.""" |
| 774 | return os.path.join(os.path.expanduser('~'), '.pypirc') |
| 775 | |
| 776 | |
| 777 | def generate_pypirc(username, password): |
| 778 | """Create a default .pypirc file.""" |
| 779 | rc = get_pypirc_path() |
| 780 | with open(rc, 'w') as f: |
| 781 | f.write(DEFAULT_PYPIRC % (username, password)) |
| 782 | try: |
| 783 | os.chmod(rc, 0o600) |
| 784 | except OSError: |
| 785 | # should do something better here |
| 786 | pass |
| 787 | |
| 788 | |
| 789 | def read_pypirc(repository=DEFAULT_REPOSITORY, realm=DEFAULT_REALM): |
| 790 | """Read the .pypirc file.""" |
| 791 | rc = get_pypirc_path() |
| 792 | if os.path.exists(rc): |
| 793 | config = RawConfigParser() |
| 794 | config.read(rc) |
| 795 | sections = config.sections() |
| 796 | if 'distutils' in sections: |
| 797 | # let's get the list of servers |
| 798 | index_servers = config.get('distutils', 'index-servers') |
| 799 | _servers = [server.strip() for server in |
| 800 | index_servers.split('\n') |
| 801 | if server.strip() != ''] |
| 802 | if _servers == []: |
| 803 | # nothing set, let's try to get the default pypi |
| 804 | if 'pypi' in sections: |
| 805 | _servers = ['pypi'] |
| 806 | else: |
| 807 | # the file is not properly defined, returning |
| 808 | # an empty dict |
| 809 | return {} |
| 810 | for server in _servers: |
| 811 | current = {'server': server} |
| 812 | current['username'] = config.get(server, 'username') |
| 813 | |
| 814 | # optional params |
| 815 | for key, default in (('repository', DEFAULT_REPOSITORY), |
| 816 | ('realm', DEFAULT_REALM), |
| 817 | ('password', None)): |
| 818 | if config.has_option(server, key): |
| 819 | current[key] = config.get(server, key) |
| 820 | else: |
| 821 | current[key] = default |
| 822 | if (current['server'] == repository or |
| 823 | current['repository'] == repository): |
| 824 | return current |
| 825 | elif 'server-login' in sections: |
| 826 | # old format |
| 827 | server = 'server-login' |
| 828 | if config.has_option(server, 'repository'): |
| 829 | repository = config.get(server, 'repository') |
| 830 | else: |
| 831 | repository = DEFAULT_REPOSITORY |
| 832 | |
| 833 | return {'username': config.get(server, 'username'), |
| 834 | 'password': config.get(server, 'password'), |
| 835 | 'repository': repository, |
| 836 | 'server': server, |
| 837 | 'realm': DEFAULT_REALM} |
| 838 | |
| 839 | return {} |
| 840 | |
| 841 | |
| 842 | # utility functions for 2to3 support |
| 843 | |
| 844 | def run_2to3(files, doctests_only=False, fixer_names=None, |
| 845 | options=None, explicit=None): |
| 846 | """ Wrapper function around the refactor() class which |
| 847 | performs the conversions on a list of python files. |
| 848 | Invoke 2to3 on a list of Python files. The files should all come |
| 849 | from the build area, as the modification is done in-place.""" |
| 850 | |
| 851 | #if not files: |
| 852 | # return |
| 853 | |
| 854 | # Make this class local, to delay import of 2to3 |
| 855 | from lib2to3.refactor import get_fixers_from_package, RefactoringTool |
| 856 | fixers = [] |
| 857 | fixers = get_fixers_from_package('lib2to3.fixes') |
| 858 | |
| 859 | if fixer_names: |
| 860 | for fixername in fixer_names: |
| 861 | fixers.extend(fixer for fixer in |
| 862 | get_fixers_from_package(fixername)) |
| 863 | r = RefactoringTool(fixers, options=options) |
| 864 | r.refactor(files, write=True, doctests_only=doctests_only) |
| 865 | |
| 866 | |
| 867 | class Mixin2to3: |
| 868 | """ Wrapper class for commands that run 2to3. |
| 869 | To configure 2to3, setup scripts may either change |
| 870 | the class variables, or inherit from this class |
| 871 | to override how 2to3 is invoked. |
| 872 | """ |
| 873 | # provide list of fixers to run. |
| 874 | # defaults to all from lib2to3.fixers |
| 875 | fixer_names = None |
| 876 | |
| 877 | # options dictionary |
| 878 | options = None |
| 879 | |
| 880 | # list of fixers to invoke even though they are marked as explicit |
| 881 | explicit = None |
| 882 | |
| 883 | def run_2to3(self, files, doctests_only=False): |
| 884 | """ Issues a call to util.run_2to3. """ |
| 885 | return run_2to3(files, doctests_only, self.fixer_names, |
| 886 | self.options, self.explicit) |
| 887 | |
| 888 | RICH_GLOB = re.compile(r'\{([^}]*)\}') |
Tarek Ziade | ec9b76d | 2011-05-21 11:48:16 +0200 | [diff] [blame] | 889 | _CHECK_RECURSIVE_GLOB = re.compile(r'[^/\\,{]\*\*|\*\*[^/\\,}]') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 890 | _CHECK_MISMATCH_SET = re.compile(r'^[^{]*\}|\{[^}]*$') |
| 891 | |
| 892 | |
| 893 | def iglob(path_glob): |
| 894 | """Extended globbing function that supports ** and {opt1,opt2,opt3}.""" |
| 895 | if _CHECK_RECURSIVE_GLOB.search(path_glob): |
| 896 | msg = """invalid glob %r: recursive glob "**" must be used alone""" |
| 897 | raise ValueError(msg % path_glob) |
| 898 | if _CHECK_MISMATCH_SET.search(path_glob): |
| 899 | msg = """invalid glob %r: mismatching set marker '{' or '}'""" |
| 900 | raise ValueError(msg % path_glob) |
| 901 | return _iglob(path_glob) |
| 902 | |
| 903 | |
| 904 | def _iglob(path_glob): |
| 905 | rich_path_glob = RICH_GLOB.split(path_glob, 1) |
| 906 | if len(rich_path_glob) > 1: |
| 907 | assert len(rich_path_glob) == 3, rich_path_glob |
| 908 | prefix, set, suffix = rich_path_glob |
| 909 | for item in set.split(','): |
| 910 | for path in _iglob(''.join((prefix, item, suffix))): |
| 911 | yield path |
| 912 | else: |
| 913 | if '**' not in path_glob: |
| 914 | for item in std_iglob(path_glob): |
| 915 | yield item |
| 916 | else: |
| 917 | prefix, radical = path_glob.split('**', 1) |
| 918 | if prefix == '': |
| 919 | prefix = '.' |
| 920 | if radical == '': |
| 921 | radical = '*' |
| 922 | else: |
Tarek Ziade | ec9b76d | 2011-05-21 11:48:16 +0200 | [diff] [blame] | 923 | # we support both |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 924 | radical = radical.lstrip('/') |
Tarek Ziade | ec9b76d | 2011-05-21 11:48:16 +0200 | [diff] [blame] | 925 | radical = radical.lstrip('\\') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 926 | for path, dir, files in os.walk(prefix): |
| 927 | path = os.path.normpath(path) |
| 928 | for file in _iglob(os.path.join(path, radical)): |
| 929 | yield file |
| 930 | |
| 931 | |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 932 | # HOWTO change cfg_to_args |
| 933 | # |
| 934 | # This function has two major constraints: It is copied by inspect.getsource |
| 935 | # in generate_setup_py; it is used in generated setup.py which may be run by |
| 936 | # any Python version supported by distutils2 (2.4-3.3). |
| 937 | # |
| 938 | # * Keep objects like D1_D2_SETUP_ARGS static, i.e. in the function body |
| 939 | # instead of global. |
| 940 | # * If you use a function from another module, update the imports in |
| 941 | # SETUP_TEMPLATE. Use only modules, classes and functions compatible with |
| 942 | # all versions: codecs.open instead of open, RawConfigParser.readfp instead |
| 943 | # of read, standard exceptions instead of Packaging*Error, etc. |
| 944 | # * If you use a function from this module, update the template and |
| 945 | # generate_setup_py. |
| 946 | # |
| 947 | # test_util tests this function and the generated setup.py, but does not test |
| 948 | # that it's compatible with all Python versions. |
| 949 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 950 | def cfg_to_args(path='setup.cfg'): |
| 951 | """Compatibility helper to use setup.cfg in setup.py. |
| 952 | |
| 953 | This functions uses an existing setup.cfg to generate a dictionnary of |
| 954 | keywords that can be used by distutils.core.setup(**kwargs). It is used |
| 955 | by generate_setup_py. |
| 956 | |
| 957 | *file* is the path to the setup.cfg file. If it doesn't exist, |
| 958 | PackagingFileError is raised. |
| 959 | """ |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 960 | |
| 961 | # XXX ** == needs testing |
| 962 | D1_D2_SETUP_ARGS = {"name": ("metadata",), |
| 963 | "version": ("metadata",), |
| 964 | "author": ("metadata",), |
| 965 | "author_email": ("metadata",), |
| 966 | "maintainer": ("metadata",), |
| 967 | "maintainer_email": ("metadata",), |
| 968 | "url": ("metadata", "home_page"), |
| 969 | "description": ("metadata", "summary"), |
| 970 | "long_description": ("metadata", "description"), |
| 971 | "download-url": ("metadata",), |
| 972 | "classifiers": ("metadata", "classifier"), |
| 973 | "platforms": ("metadata", "platform"), # ** |
| 974 | "license": ("metadata",), |
| 975 | "requires": ("metadata", "requires_dist"), |
| 976 | "provides": ("metadata", "provides_dist"), # ** |
| 977 | "obsoletes": ("metadata", "obsoletes_dist"), # ** |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 978 | "package_dir": ("files", 'packages_root'), |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 979 | "packages": ("files",), |
| 980 | "scripts": ("files",), |
| 981 | "py_modules": ("files", "modules"), # ** |
| 982 | } |
| 983 | |
| 984 | MULTI_FIELDS = ("classifiers", |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 985 | "platforms", |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 986 | "requires", |
| 987 | "provides", |
| 988 | "obsoletes", |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 989 | "packages", |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 990 | "scripts", |
| 991 | "py_modules") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 992 | |
| 993 | def has_get_option(config, section, option): |
| 994 | if config.has_option(section, option): |
| 995 | return config.get(section, option) |
| 996 | elif config.has_option(section, option.replace('_', '-')): |
| 997 | return config.get(section, option.replace('_', '-')) |
| 998 | else: |
| 999 | return False |
| 1000 | |
| 1001 | # The real code starts here |
| 1002 | config = RawConfigParser() |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1003 | f = codecs.open(path, encoding='utf-8') |
| 1004 | try: |
| 1005 | config.readfp(f) |
| 1006 | finally: |
| 1007 | f.close() |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1008 | |
| 1009 | kwargs = {} |
| 1010 | for arg in D1_D2_SETUP_ARGS: |
| 1011 | if len(D1_D2_SETUP_ARGS[arg]) == 2: |
| 1012 | # The distutils field name is different than packaging's |
| 1013 | section, option = D1_D2_SETUP_ARGS[arg] |
| 1014 | |
| 1015 | else: |
| 1016 | # The distutils field name is the same thant packaging's |
| 1017 | section = D1_D2_SETUP_ARGS[arg][0] |
| 1018 | option = arg |
| 1019 | |
| 1020 | in_cfg_value = has_get_option(config, section, option) |
| 1021 | if not in_cfg_value: |
| 1022 | # There is no such option in the setup.cfg |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 1023 | if arg == 'long_description': |
| 1024 | filenames = has_get_option(config, section, 'description-file') |
| 1025 | if filenames: |
| 1026 | filenames = split_multiline(filenames) |
| 1027 | in_cfg_value = [] |
| 1028 | for filename in filenames: |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1029 | fp = codecs.open(filename, encoding='utf-8') |
| 1030 | try: |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 1031 | in_cfg_value.append(fp.read()) |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1032 | finally: |
| 1033 | fp.close() |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 1034 | in_cfg_value = '\n\n'.join(in_cfg_value) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1035 | else: |
| 1036 | continue |
| 1037 | |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 1038 | if arg == 'package_dir' and in_cfg_value: |
| 1039 | in_cfg_value = {'': in_cfg_value} |
| 1040 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1041 | if arg in MULTI_FIELDS: |
| 1042 | # support multiline options |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 1043 | in_cfg_value = split_multiline(in_cfg_value) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1044 | |
| 1045 | kwargs[arg] = in_cfg_value |
| 1046 | |
| 1047 | return kwargs |
| 1048 | |
| 1049 | |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1050 | SETUP_TEMPLATE = """\ |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1051 | # This script was automatically generated by packaging |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1052 | import codecs |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1053 | from distutils.core import setup |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1054 | try: |
| 1055 | from ConfigParser import RawConfigParser |
| 1056 | except ImportError: |
| 1057 | from configparser import RawConfigParser |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1058 | |
Éric Araujo | ac03a2b | 2012-02-09 21:17:46 +0100 | [diff] [blame^] | 1059 | |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1060 | %(split_multiline)s |
| 1061 | |
| 1062 | %(cfg_to_args)s |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1063 | |
| 1064 | setup(**cfg_to_args()) |
| 1065 | """ |
| 1066 | |
| 1067 | |
| 1068 | def generate_setup_py(): |
| 1069 | """Generate a distutils compatible setup.py using an existing setup.cfg. |
| 1070 | |
| 1071 | Raises a PackagingFileError when a setup.py already exists. |
| 1072 | """ |
| 1073 | if os.path.exists("setup.py"): |
Tarek Ziade | 721ccd0 | 2011-06-02 12:00:44 +0200 | [diff] [blame] | 1074 | raise PackagingFileError("a setup.py file already exists") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1075 | |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1076 | source = SETUP_TEMPLATE % {'split_multiline': getsource(split_multiline), |
| 1077 | 'cfg_to_args': getsource(cfg_to_args)} |
Victor Stinner | 9cf6d13 | 2011-05-19 21:42:47 +0200 | [diff] [blame] | 1078 | with open("setup.py", "w", encoding='utf-8') as fp: |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1079 | fp.write(source) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1080 | |
| 1081 | |
| 1082 | # Taken from the pip project |
| 1083 | # https://github.com/pypa/pip/blob/master/pip/util.py |
| 1084 | def ask(message, options): |
| 1085 | """Prompt the user with *message*; *options* contains allowed responses.""" |
| 1086 | while True: |
| 1087 | response = input(message) |
| 1088 | response = response.strip().lower() |
| 1089 | if response not in options: |
Éric Araujo | 3cab2f1 | 2011-06-08 04:10:57 +0200 | [diff] [blame] | 1090 | print('invalid response:', repr(response)) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1091 | print('choose one of', ', '.join(repr(o) for o in options)) |
| 1092 | else: |
| 1093 | return response |
| 1094 | |
| 1095 | |
| 1096 | def _parse_record_file(record_file): |
| 1097 | distinfo, extra_metadata, installed = ({}, [], []) |
| 1098 | with open(record_file, 'r') as rfile: |
| 1099 | for path in rfile: |
| 1100 | path = path.strip() |
| 1101 | if path.endswith('egg-info') and os.path.isfile(path): |
| 1102 | distinfo_dir = path.replace('egg-info', 'dist-info') |
| 1103 | metadata = path |
| 1104 | egginfo = path |
| 1105 | elif path.endswith('egg-info') and os.path.isdir(path): |
| 1106 | distinfo_dir = path.replace('egg-info', 'dist-info') |
| 1107 | egginfo = path |
| 1108 | for metadata_file in os.listdir(path): |
| 1109 | metadata_fpath = os.path.join(path, metadata_file) |
| 1110 | if metadata_file == 'PKG-INFO': |
| 1111 | metadata = metadata_fpath |
| 1112 | else: |
| 1113 | extra_metadata.append(metadata_fpath) |
| 1114 | elif 'egg-info' in path and os.path.isfile(path): |
| 1115 | # skip extra metadata files |
| 1116 | continue |
| 1117 | else: |
| 1118 | installed.append(path) |
| 1119 | |
| 1120 | distinfo['egginfo'] = egginfo |
| 1121 | distinfo['metadata'] = metadata |
| 1122 | distinfo['distinfo_dir'] = distinfo_dir |
| 1123 | distinfo['installer_path'] = os.path.join(distinfo_dir, 'INSTALLER') |
| 1124 | distinfo['metadata_path'] = os.path.join(distinfo_dir, 'METADATA') |
| 1125 | distinfo['record_path'] = os.path.join(distinfo_dir, 'RECORD') |
| 1126 | distinfo['requested_path'] = os.path.join(distinfo_dir, 'REQUESTED') |
| 1127 | installed.extend([distinfo['installer_path'], distinfo['metadata_path']]) |
| 1128 | distinfo['installed'] = installed |
| 1129 | distinfo['extra_metadata'] = extra_metadata |
| 1130 | return distinfo |
| 1131 | |
| 1132 | |
| 1133 | def _write_record_file(record_path, installed_files): |
| 1134 | with open(record_path, 'w', encoding='utf-8') as f: |
| 1135 | writer = csv.writer(f, delimiter=',', lineterminator=os.linesep, |
| 1136 | quotechar='"') |
| 1137 | |
| 1138 | for fpath in installed_files: |
| 1139 | if fpath.endswith('.pyc') or fpath.endswith('.pyo'): |
| 1140 | # do not put size and md5 hash, as in PEP-376 |
| 1141 | writer.writerow((fpath, '', '')) |
| 1142 | else: |
| 1143 | hash = hashlib.md5() |
| 1144 | with open(fpath, 'rb') as fp: |
| 1145 | hash.update(fp.read()) |
| 1146 | md5sum = hash.hexdigest() |
| 1147 | size = os.path.getsize(fpath) |
| 1148 | writer.writerow((fpath, md5sum, size)) |
| 1149 | |
| 1150 | # add the RECORD file itself |
| 1151 | writer.writerow((record_path, '', '')) |
| 1152 | return record_path |
| 1153 | |
| 1154 | |
| 1155 | def egginfo_to_distinfo(record_file, installer=_DEFAULT_INSTALLER, |
| 1156 | requested=False, remove_egginfo=False): |
| 1157 | """Create files and directories required for PEP 376 |
| 1158 | |
| 1159 | :param record_file: path to RECORD file as produced by setup.py --record |
| 1160 | :param installer: installer name |
| 1161 | :param requested: True if not installed as a dependency |
| 1162 | :param remove_egginfo: delete egginfo dir? |
| 1163 | """ |
| 1164 | distinfo = _parse_record_file(record_file) |
| 1165 | distinfo_dir = distinfo['distinfo_dir'] |
| 1166 | if os.path.isdir(distinfo_dir) and not os.path.islink(distinfo_dir): |
| 1167 | shutil.rmtree(distinfo_dir) |
| 1168 | elif os.path.exists(distinfo_dir): |
| 1169 | os.unlink(distinfo_dir) |
| 1170 | |
| 1171 | os.makedirs(distinfo_dir) |
| 1172 | |
| 1173 | # copy setuptools extra metadata files |
| 1174 | if distinfo['extra_metadata']: |
| 1175 | for path in distinfo['extra_metadata']: |
| 1176 | shutil.copy2(path, distinfo_dir) |
| 1177 | new_path = path.replace('egg-info', 'dist-info') |
| 1178 | distinfo['installed'].append(new_path) |
| 1179 | |
| 1180 | metadata_path = distinfo['metadata_path'] |
| 1181 | logger.info('creating %s', metadata_path) |
| 1182 | shutil.copy2(distinfo['metadata'], metadata_path) |
| 1183 | |
| 1184 | installer_path = distinfo['installer_path'] |
| 1185 | logger.info('creating %s', installer_path) |
| 1186 | with open(installer_path, 'w') as f: |
| 1187 | f.write(installer) |
| 1188 | |
| 1189 | if requested: |
| 1190 | requested_path = distinfo['requested_path'] |
| 1191 | logger.info('creating %s', requested_path) |
Victor Stinner | 4c9706b | 2011-05-19 15:52:59 +0200 | [diff] [blame] | 1192 | open(requested_path, 'wb').close() |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1193 | distinfo['installed'].append(requested_path) |
| 1194 | |
| 1195 | record_path = distinfo['record_path'] |
| 1196 | logger.info('creating %s', record_path) |
| 1197 | _write_record_file(record_path, distinfo['installed']) |
| 1198 | |
| 1199 | if remove_egginfo: |
| 1200 | egginfo = distinfo['egginfo'] |
| 1201 | logger.info('removing %s', egginfo) |
| 1202 | if os.path.isfile(egginfo): |
| 1203 | os.remove(egginfo) |
| 1204 | else: |
| 1205 | shutil.rmtree(egginfo) |
| 1206 | |
| 1207 | |
| 1208 | def _has_egg_info(srcdir): |
| 1209 | if os.path.isdir(srcdir): |
| 1210 | for item in os.listdir(srcdir): |
| 1211 | full_path = os.path.join(srcdir, item) |
| 1212 | if item.endswith('.egg-info') and os.path.isdir(full_path): |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1213 | logger.debug("Found egg-info directory.") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1214 | return True |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1215 | logger.debug("No egg-info directory found.") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1216 | return False |
| 1217 | |
| 1218 | |
| 1219 | def _has_setuptools_text(setup_py): |
| 1220 | return _has_text(setup_py, 'setuptools') |
| 1221 | |
| 1222 | |
| 1223 | def _has_distutils_text(setup_py): |
| 1224 | return _has_text(setup_py, 'distutils') |
| 1225 | |
| 1226 | |
| 1227 | def _has_text(setup_py, installer): |
| 1228 | installer_pattern = re.compile('import {0}|from {0}'.format(installer)) |
| 1229 | with open(setup_py, 'r', encoding='utf-8') as setup: |
| 1230 | for line in setup: |
| 1231 | if re.search(installer_pattern, line): |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1232 | logger.debug("Found %s text in setup.py.", installer) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1233 | return True |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1234 | logger.debug("No %s text found in setup.py.", installer) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1235 | return False |
| 1236 | |
| 1237 | |
| 1238 | def _has_required_metadata(setup_cfg): |
| 1239 | config = RawConfigParser() |
| 1240 | config.read([setup_cfg], encoding='utf8') |
| 1241 | return (config.has_section('metadata') and |
| 1242 | 'name' in config.options('metadata') and |
| 1243 | 'version' in config.options('metadata')) |
| 1244 | |
| 1245 | |
| 1246 | def _has_pkg_info(srcdir): |
| 1247 | pkg_info = os.path.join(srcdir, 'PKG-INFO') |
| 1248 | has_pkg_info = os.path.isfile(pkg_info) |
| 1249 | if has_pkg_info: |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1250 | logger.debug("PKG-INFO file found.") |
| 1251 | else: |
| 1252 | logger.debug("No PKG-INFO file found.") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1253 | return has_pkg_info |
| 1254 | |
| 1255 | |
| 1256 | def _has_setup_py(srcdir): |
| 1257 | setup_py = os.path.join(srcdir, 'setup.py') |
| 1258 | if os.path.isfile(setup_py): |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1259 | logger.debug('setup.py file found.') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1260 | return True |
| 1261 | return False |
| 1262 | |
| 1263 | |
| 1264 | def _has_setup_cfg(srcdir): |
| 1265 | setup_cfg = os.path.join(srcdir, 'setup.cfg') |
| 1266 | if os.path.isfile(setup_cfg): |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1267 | logger.debug('setup.cfg file found.') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1268 | return True |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1269 | logger.debug("No setup.cfg file found.") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1270 | return False |
| 1271 | |
| 1272 | |
| 1273 | def is_setuptools(path): |
| 1274 | """Check if the project is based on setuptools. |
| 1275 | |
| 1276 | :param path: path to source directory containing a setup.py script. |
| 1277 | |
| 1278 | Return True if the project requires setuptools to install, else False. |
| 1279 | """ |
| 1280 | srcdir = os.path.abspath(path) |
| 1281 | setup_py = os.path.join(srcdir, 'setup.py') |
| 1282 | |
| 1283 | return _has_setup_py(srcdir) and (_has_egg_info(srcdir) or |
| 1284 | _has_setuptools_text(setup_py)) |
| 1285 | |
| 1286 | |
| 1287 | def is_distutils(path): |
| 1288 | """Check if the project is based on distutils. |
| 1289 | |
| 1290 | :param path: path to source directory containing a setup.py script. |
| 1291 | |
| 1292 | Return True if the project requires distutils to install, else False. |
| 1293 | """ |
| 1294 | srcdir = os.path.abspath(path) |
| 1295 | setup_py = os.path.join(srcdir, 'setup.py') |
| 1296 | |
| 1297 | return _has_setup_py(srcdir) and (_has_pkg_info(srcdir) or |
| 1298 | _has_distutils_text(setup_py)) |
| 1299 | |
| 1300 | |
| 1301 | def is_packaging(path): |
| 1302 | """Check if the project is based on packaging |
| 1303 | |
| 1304 | :param path: path to source directory containing a setup.cfg file. |
| 1305 | |
| 1306 | Return True if the project has a valid setup.cfg, else False. |
| 1307 | """ |
| 1308 | srcdir = os.path.abspath(path) |
| 1309 | setup_cfg = os.path.join(srcdir, 'setup.cfg') |
| 1310 | |
| 1311 | return _has_setup_cfg(srcdir) and _has_required_metadata(setup_cfg) |
| 1312 | |
| 1313 | |
| 1314 | def get_install_method(path): |
| 1315 | """Check if the project is based on packaging, setuptools, or distutils |
| 1316 | |
| 1317 | :param path: path to source directory containing a setup.cfg file, |
| 1318 | or setup.py. |
| 1319 | |
| 1320 | Returns a string representing the best install method to use. |
| 1321 | """ |
| 1322 | if is_packaging(path): |
| 1323 | return "packaging" |
| 1324 | elif is_setuptools(path): |
| 1325 | return "setuptools" |
| 1326 | elif is_distutils(path): |
| 1327 | return "distutils" |
| 1328 | else: |
| 1329 | raise InstallationException('Cannot detect install method') |
| 1330 | |
| 1331 | |
| 1332 | # XXX to be replaced by shutil.copytree |
| 1333 | def copy_tree(src, dst, preserve_mode=True, preserve_times=True, |
Éric Araujo | 4d15546 | 2011-11-15 11:43:20 +0100 | [diff] [blame] | 1334 | preserve_symlinks=False, update=False, dry_run=False): |
Éric Araujo | f89ebdc | 2011-10-21 06:27:06 +0200 | [diff] [blame] | 1335 | # FIXME use of this function is why we get spurious logging message on |
Éric Araujo | f836162 | 2011-11-14 18:10:19 +0100 | [diff] [blame] | 1336 | # stdout when tests run; kill and replace by shutil! |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1337 | from distutils.file_util import copy_file |
| 1338 | |
| 1339 | if not dry_run and not os.path.isdir(src): |
| 1340 | raise PackagingFileError( |
| 1341 | "cannot copy tree '%s': not a directory" % src) |
| 1342 | try: |
| 1343 | names = os.listdir(src) |
| 1344 | except os.error as e: |
| 1345 | errstr = e[1] |
| 1346 | if dry_run: |
| 1347 | names = [] |
| 1348 | else: |
| 1349 | raise PackagingFileError( |
| 1350 | "error listing files in '%s': %s" % (src, errstr)) |
| 1351 | |
| 1352 | if not dry_run: |
Éric Araujo | 4d15546 | 2011-11-15 11:43:20 +0100 | [diff] [blame] | 1353 | _mkpath(dst) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1354 | |
| 1355 | outputs = [] |
| 1356 | |
| 1357 | for n in names: |
| 1358 | src_name = os.path.join(src, n) |
| 1359 | dst_name = os.path.join(dst, n) |
| 1360 | |
| 1361 | if preserve_symlinks and os.path.islink(src_name): |
| 1362 | link_dest = os.readlink(src_name) |
Éric Araujo | 4d15546 | 2011-11-15 11:43:20 +0100 | [diff] [blame] | 1363 | logger.info("linking %s -> %s", dst_name, link_dest) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1364 | if not dry_run: |
| 1365 | os.symlink(link_dest, dst_name) |
| 1366 | outputs.append(dst_name) |
| 1367 | |
| 1368 | elif os.path.isdir(src_name): |
| 1369 | outputs.extend( |
| 1370 | copy_tree(src_name, dst_name, preserve_mode, |
| 1371 | preserve_times, preserve_symlinks, update, |
Éric Araujo | 4d15546 | 2011-11-15 11:43:20 +0100 | [diff] [blame] | 1372 | dry_run=dry_run)) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1373 | else: |
| 1374 | copy_file(src_name, dst_name, preserve_mode, |
Éric Araujo | 4d15546 | 2011-11-15 11:43:20 +0100 | [diff] [blame] | 1375 | preserve_times, update, dry_run=dry_run) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1376 | outputs.append(dst_name) |
| 1377 | |
| 1378 | return outputs |
| 1379 | |
| 1380 | # cache for by mkpath() -- in addition to cheapening redundant calls, |
| 1381 | # eliminates redundant "creating /foo/bar/baz" messages in dry-run mode |
| 1382 | _path_created = set() |
| 1383 | |
| 1384 | |
| 1385 | # I don't use os.makedirs because a) it's new to Python 1.5.2, and |
| 1386 | # b) it blows up if the directory already exists (I want to silently |
| 1387 | # succeed in that case). |
Éric Araujo | 4d15546 | 2011-11-15 11:43:20 +0100 | [diff] [blame] | 1388 | def _mkpath(name, mode=0o777, dry_run=False): |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1389 | # Detect a common bug -- name is None |
| 1390 | if not isinstance(name, str): |
| 1391 | raise PackagingInternalError( |
| 1392 | "mkpath: 'name' must be a string (got %r)" % (name,)) |
| 1393 | |
| 1394 | # XXX what's the better way to handle verbosity? print as we create |
| 1395 | # each directory in the path (the current behaviour), or only announce |
| 1396 | # the creation of the whole path? (quite easy to do the latter since |
| 1397 | # we're not using a recursive algorithm) |
| 1398 | |
| 1399 | name = os.path.normpath(name) |
| 1400 | created_dirs = [] |
| 1401 | if os.path.isdir(name) or name == '': |
| 1402 | return created_dirs |
| 1403 | if os.path.abspath(name) in _path_created: |
| 1404 | return created_dirs |
| 1405 | |
| 1406 | head, tail = os.path.split(name) |
| 1407 | tails = [tail] # stack of lone dirs to create |
| 1408 | |
| 1409 | while head and tail and not os.path.isdir(head): |
| 1410 | head, tail = os.path.split(head) |
| 1411 | tails.insert(0, tail) # push next higher dir onto stack |
| 1412 | |
| 1413 | # now 'head' contains the deepest directory that already exists |
| 1414 | # (that is, the child of 'head' in 'name' is the highest directory |
| 1415 | # that does *not* exist) |
| 1416 | for d in tails: |
| 1417 | head = os.path.join(head, d) |
| 1418 | abs_head = os.path.abspath(head) |
| 1419 | |
| 1420 | if abs_head in _path_created: |
| 1421 | continue |
| 1422 | |
Éric Araujo | 4d15546 | 2011-11-15 11:43:20 +0100 | [diff] [blame] | 1423 | logger.info("creating %s", head) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1424 | if not dry_run: |
| 1425 | try: |
| 1426 | os.mkdir(head, mode) |
| 1427 | except OSError as exc: |
| 1428 | if not (exc.errno == errno.EEXIST and os.path.isdir(head)): |
| 1429 | raise PackagingFileError( |
| 1430 | "could not create '%s': %s" % (head, exc.args[-1])) |
| 1431 | created_dirs.append(head) |
| 1432 | |
| 1433 | _path_created.add(abs_head) |
| 1434 | return created_dirs |
Éric Araujo | ce5fe83 | 2011-07-08 16:27:12 +0200 | [diff] [blame] | 1435 | |
| 1436 | |
| 1437 | def encode_multipart(fields, files, boundary=None): |
| 1438 | """Prepare a multipart HTTP request. |
| 1439 | |
| 1440 | *fields* is a sequence of (name: str, value: str) elements for regular |
| 1441 | form fields, *files* is a sequence of (name: str, filename: str, value: |
| 1442 | bytes) elements for data to be uploaded as files. |
| 1443 | |
| 1444 | Returns (content_type: bytes, body: bytes) ready for http.client.HTTP. |
| 1445 | """ |
Éric Araujo | f836162 | 2011-11-14 18:10:19 +0100 | [diff] [blame] | 1446 | # Taken from http://code.activestate.com/recipes/146306 |
Éric Araujo | ce5fe83 | 2011-07-08 16:27:12 +0200 | [diff] [blame] | 1447 | |
| 1448 | if boundary is None: |
| 1449 | boundary = b'--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' |
| 1450 | elif not isinstance(boundary, bytes): |
| 1451 | raise TypeError('boundary must be bytes, not %r' % type(boundary)) |
| 1452 | |
| 1453 | l = [] |
| 1454 | for key, values in fields: |
| 1455 | # handle multiple entries for the same name |
| 1456 | if not isinstance(values, (tuple, list)): |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 1457 | values = [values] |
Éric Araujo | ce5fe83 | 2011-07-08 16:27:12 +0200 | [diff] [blame] | 1458 | |
| 1459 | for value in values: |
| 1460 | l.extend(( |
| 1461 | b'--' + boundary, |
| 1462 | ('Content-Disposition: form-data; name="%s"' % |
| 1463 | key).encode('utf-8'), |
| 1464 | b'', |
| 1465 | value.encode('utf-8'))) |
| 1466 | |
| 1467 | for key, filename, value in files: |
| 1468 | l.extend(( |
| 1469 | b'--' + boundary, |
| 1470 | ('Content-Disposition: form-data; name="%s"; filename="%s"' % |
| 1471 | (key, filename)).encode('utf-8'), |
| 1472 | b'', |
| 1473 | value)) |
| 1474 | |
| 1475 | l.append(b'--' + boundary + b'--') |
| 1476 | l.append(b'') |
| 1477 | |
| 1478 | body = b'\r\n'.join(l) |
| 1479 | content_type = b'multipart/form-data; boundary=' + boundary |
| 1480 | return content_type, body |