python (3.11.7)
1 """distutils.util
2
3 Miscellaneous utility functions -- anything that doesn't fit into
4 one of the other *util.py modules.
5 """
6
7 import os
8 import re
9 import importlib.util
10 import string
11 import sys
12 import distutils
13 from distutils.errors import DistutilsPlatformError
14 from distutils.dep_util import newer
15 from distutils.spawn import spawn
16 from distutils import log
17 from distutils.errors import DistutilsByteCompileError
18
19 def get_host_platform():
20 """Return a string that identifies the current platform. This is used mainly to
21 distinguish platform-specific build directories and platform-specific built
22 distributions. Typically includes the OS name and version and the
23 architecture (as supplied by 'os.uname()'), although the exact information
24 included depends on the OS; eg. on Linux, the kernel version isn't
25 particularly important.
26
27 Examples of returned values:
28 linux-i586
29 linux-alpha (?)
30 solaris-2.6-sun4u
31
32 Windows will return one of:
33 win-amd64 (64bit Windows on AMD64 (aka x86_64, Intel64, EM64T, etc)
34 win32 (all others - specifically, sys.platform is returned)
35
36 For other non-POSIX platforms, currently just returns 'sys.platform'.
37
38 """
39 if os.name == 'nt':
40 if 'amd64' in sys.version.lower():
41 return 'win-amd64'
42 if '(arm)' in sys.version.lower():
43 return 'win-arm32'
44 if '(arm64)' in sys.version.lower():
45 return 'win-arm64'
46 return sys.platform
47
48 # Set for cross builds explicitly
49 if "_PYTHON_HOST_PLATFORM" in os.environ:
50 return os.environ["_PYTHON_HOST_PLATFORM"]
51
52 if os.name != "posix" or not hasattr(os, 'uname'):
53 # XXX what about the architecture? NT is Intel or Alpha,
54 # Mac OS is M68k or PPC, etc.
55 return sys.platform
56
57 # Try to distinguish various flavours of Unix
58
59 (osname, host, release, version, machine) = os.uname()
60
61 # Convert the OS name to lowercase, remove '/' characters, and translate
62 # spaces (for "Power Macintosh")
63 osname = osname.lower().replace('/', '')
64 machine = machine.replace(' ', '_')
65 machine = machine.replace('/', '-')
66
67 if osname[:5] == "linux":
68 # At least on Linux/Intel, 'machine' is the processor --
69 # i386, etc.
70 # XXX what about Alpha, SPARC, etc?
71 return "%s-%s" % (osname, machine)
72 elif osname[:5] == "sunos":
73 if release[0] >= "5": # SunOS 5 == Solaris 2
74 osname = "solaris"
75 release = "%d.%s" % (int(release[0]) - 3, release[2:])
76 # We can't use "platform.architecture()[0]" because a
77 # bootstrap problem. We use a dict to get an error
78 # if some suspicious happens.
79 bitness = {2147483647:"32bit", 9223372036854775807:"64bit"}
80 machine += ".%s" % bitness[sys.maxsize]
81 # fall through to standard osname-release-machine representation
82 elif osname[:3] == "aix":
83 from _aix_support import aix_platform
84 return aix_platform()
85 elif osname[:6] == "cygwin":
86 osname = "cygwin"
87 rel_re = re.compile (r'[\d.]+', re.ASCII)
88 m = rel_re.match(release)
89 if m:
90 release = m.group()
91 elif osname[:6] == "darwin":
92 import _osx_support, distutils.sysconfig
93 osname, release, machine = _osx_support.get_platform_osx(
94 distutils.sysconfig.get_config_vars(),
95 osname, release, machine)
96
97 return "%s-%s-%s" % (osname, release, machine)
98
99 def get_platform():
100 if os.name == 'nt':
101 TARGET_TO_PLAT = {
102 'x86' : 'win32',
103 'x64' : 'win-amd64',
104 'arm' : 'win-arm32',
105 }
106 return TARGET_TO_PLAT.get(os.environ.get('VSCMD_ARG_TGT_ARCH')) or get_host_platform()
107 else:
108 return get_host_platform()
109
110 def convert_path (pathname):
111 """Return 'pathname' as a name that will work on the native filesystem,
112 i.e. split it on '/' and put it back together again using the current
113 directory separator. Needed because filenames in the setup script are
114 always supplied in Unix style, and have to be converted to the local
115 convention before we can actually use them in the filesystem. Raises
116 ValueError on non-Unix-ish systems if 'pathname' either starts or
117 ends with a slash.
118 """
119 if os.sep == '/':
120 return pathname
121 if not pathname:
122 return pathname
123 if pathname[0] == '/':
124 raise ValueError("path '%s' cannot be absolute" % pathname)
125 if pathname[-1] == '/':
126 raise ValueError("path '%s' cannot end with '/'" % pathname)
127
128 paths = pathname.split('/')
129 while '.' in paths:
130 paths.remove('.')
131 if not paths:
132 return os.curdir
133 return os.path.join(*paths)
134
135 # convert_path ()
136
137
138 def change_root (new_root, pathname):
139 """Return 'pathname' with 'new_root' prepended. If 'pathname' is
140 relative, this is equivalent to "os.path.join(new_root,pathname)".
141 Otherwise, it requires making 'pathname' relative and then joining the
142 two, which is tricky on DOS/Windows and Mac OS.
143 """
144 if os.name == 'posix':
145 if not os.path.isabs(pathname):
146 return os.path.join(new_root, pathname)
147 else:
148 return os.path.join(new_root, pathname[1:])
149
150 elif os.name == 'nt':
151 (drive, path) = os.path.splitdrive(pathname)
152 if path[0] == '\\':
153 path = path[1:]
154 return os.path.join(new_root, path)
155
156 else:
157 raise DistutilsPlatformError("nothing known about platform '%s'" % os.name)
158
159
160 _environ_checked = 0
161 def check_environ ():
162 """Ensure that 'os.environ' has all the environment variables we
163 guarantee that users can use in config files, command-line options,
164 etc. Currently this includes:
165 HOME - user's home directory (Unix only)
166 PLAT - description of the current platform, including hardware
167 and OS (see 'get_platform()')
168 """
169 global _environ_checked
170 if _environ_checked:
171 return
172
173 if os.name == 'posix' and 'HOME' not in os.environ:
174 try:
175 import pwd
176 os.environ['HOME'] = pwd.getpwuid(os.getuid())[5]
177 except (ImportError, KeyError):
178 # bpo-10496: if the current user identifier doesn't exist in the
179 # password database, do nothing
180 pass
181
182 if 'PLAT' not in os.environ:
183 os.environ['PLAT'] = get_platform()
184
185 _environ_checked = 1
186
187
188 def subst_vars (s, local_vars):
189 """Perform shell/Perl-style variable substitution on 'string'. Every
190 occurrence of '$' followed by a name is considered a variable, and
191 variable is substituted by the value found in the 'local_vars'
192 dictionary, or in 'os.environ' if it's not in 'local_vars'.
193 'os.environ' is first checked/augmented to guarantee that it contains
194 certain values: see 'check_environ()'. Raise ValueError for any
195 variables not found in either 'local_vars' or 'os.environ'.
196 """
197 check_environ()
198 def _subst (match, local_vars=local_vars):
199 var_name = match.group(1)
200 if var_name in local_vars:
201 return str(local_vars[var_name])
202 else:
203 return os.environ[var_name]
204
205 try:
206 return re.sub(r'\$([a-zA-Z_][a-zA-Z_0-9]*)', _subst, s)
207 except KeyError as var:
208 raise ValueError("invalid variable '$%s'" % var)
209
210 # subst_vars ()
211
212
213 def grok_environment_error (exc, prefix="error: "):
214 # Function kept for backward compatibility.
215 # Used to try clever things with EnvironmentErrors,
216 # but nowadays str(exception) produces good messages.
217 return prefix + str(exc)
218
219
220 # Needed by 'split_quoted()'
221 _wordchars_re = _squote_re = _dquote_re = None
222 def _init_regex():
223 global _wordchars_re, _squote_re, _dquote_re
224 _wordchars_re = re.compile(r'[^\\\'\"%s ]*' % string.whitespace)
225 _squote_re = re.compile(r"'(?:[^'\\]|\\.)*'")
226 _dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"')
227
228 def split_quoted (s):
229 """Split a string up according to Unix shell-like rules for quotes and
230 backslashes. In short: words are delimited by spaces, as long as those
231 spaces are not escaped by a backslash, or inside a quoted string.
232 Single and double quotes are equivalent, and the quote characters can
233 be backslash-escaped. The backslash is stripped from any two-character
234 escape sequence, leaving only the escaped character. The quote
235 characters are stripped from any quoted string. Returns a list of
236 words.
237 """
238
239 # This is a nice algorithm for splitting up a single string, since it
240 # doesn't require character-by-character examination. It was a little
241 # bit of a brain-bender to get it working right, though...
242 if _wordchars_re is None: _init_regex()
243
244 s = s.strip()
245 words = []
246 pos = 0
247
248 while s:
249 m = _wordchars_re.match(s, pos)
250 end = m.end()
251 if end == len(s):
252 words.append(s[:end])
253 break
254
255 if s[end] in string.whitespace: # unescaped, unquoted whitespace: now
256 words.append(s[:end]) # we definitely have a word delimiter
257 s = s[end:].lstrip()
258 pos = 0
259
260 elif s[end] == '\\': # preserve whatever is being escaped;
261 # will become part of the current word
262 s = s[:end] + s[end+1:]
263 pos = end+1
264
265 else:
266 if s[end] == "'": # slurp singly-quoted string
267 m = _squote_re.match(s, end)
268 elif s[end] == '"': # slurp doubly-quoted string
269 m = _dquote_re.match(s, end)
270 else:
271 raise RuntimeError("this can't happen (bad char '%c')" % s[end])
272
273 if m is None:
274 raise ValueError("bad string (mismatched %s quotes?)" % s[end])
275
276 (beg, end) = m.span()
277 s = s[:beg] + s[beg+1:end-1] + s[end:]
278 pos = m.end() - 2
279
280 if pos >= len(s):
281 words.append(s)
282 break
283
284 return words
285
286 # split_quoted ()
287
288
289 def execute (func, args, msg=None, verbose=0, dry_run=0):
290 """Perform some action that affects the outside world (eg. by
291 writing to the filesystem). Such actions are special because they
292 are disabled by the 'dry_run' flag. This method takes care of all
293 that bureaucracy for you; all you have to do is supply the
294 function to call and an argument tuple for it (to embody the
295 "external action" being performed), and an optional message to
296 print.
297 """
298 if msg is None:
299 msg = "%s%r" % (func.__name__, args)
300 if msg[-2:] == ',)': # correct for singleton tuple
301 msg = msg[0:-2] + ')'
302
303 log.info(msg)
304 if not dry_run:
305 func(*args)
306
307
308 def strtobool (val):
309 """Convert a string representation of truth to true (1) or false (0).
310
311 True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
312 are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
313 'val' is anything else.
314 """
315 val = val.lower()
316 if val in ('y', 'yes', 't', 'true', 'on', '1'):
317 return 1
318 elif val in ('n', 'no', 'f', 'false', 'off', '0'):
319 return 0
320 else:
321 raise ValueError("invalid truth value %r" % (val,))
322
323
324 def byte_compile (py_files,
325 optimize=0, force=0,
326 prefix=None, base_dir=None,
327 verbose=1, dry_run=0,
328 direct=None):
329 """Byte-compile a collection of Python source files to .pyc
330 files in a __pycache__ subdirectory. 'py_files' is a list
331 of files to compile; any files that don't end in ".py" are silently
332 skipped. 'optimize' must be one of the following:
333 0 - don't optimize
334 1 - normal optimization (like "python -O")
335 2 - extra optimization (like "python -OO")
336 If 'force' is true, all files are recompiled regardless of
337 timestamps.
338
339 The source filename encoded in each bytecode file defaults to the
340 filenames listed in 'py_files'; you can modify these with 'prefix' and
341 'basedir'. 'prefix' is a string that will be stripped off of each
342 source filename, and 'base_dir' is a directory name that will be
343 prepended (after 'prefix' is stripped). You can supply either or both
344 (or neither) of 'prefix' and 'base_dir', as you wish.
345
346 If 'dry_run' is true, doesn't actually do anything that would
347 affect the filesystem.
348
349 Byte-compilation is either done directly in this interpreter process
350 with the standard py_compile module, or indirectly by writing a
351 temporary script and executing it. Normally, you should let
352 'byte_compile()' figure out to use direct compilation or not (see
353 the source for details). The 'direct' flag is used by the script
354 generated in indirect mode; unless you know what you're doing, leave
355 it set to None.
356 """
357
358 # Late import to fix a bootstrap issue: _posixsubprocess is built by
359 # setup.py, but setup.py uses distutils.
360 import subprocess
361
362 # nothing is done if sys.dont_write_bytecode is True
363 if sys.dont_write_bytecode:
364 raise DistutilsByteCompileError('byte-compiling is disabled.')
365
366 # First, if the caller didn't force us into direct or indirect mode,
367 # figure out which mode we should be in. We take a conservative
368 # approach: choose direct mode *only* if the current interpreter is
369 # in debug mode and optimize is 0. If we're not in debug mode (-O
370 # or -OO), we don't know which level of optimization this
371 # interpreter is running with, so we can't do direct
372 # byte-compilation and be certain that it's the right thing. Thus,
373 # always compile indirectly if the current interpreter is in either
374 # optimize mode, or if either optimization level was requested by
375 # the caller.
376 if direct is None:
377 direct = (__debug__ and optimize == 0)
378
379 # "Indirect" byte-compilation: write a temporary script and then
380 # run it with the appropriate flags.
381 if not direct:
382 try:
383 from tempfile import mkstemp
384 (script_fd, script_name) = mkstemp(".py")
385 except ImportError:
386 from tempfile import mktemp
387 (script_fd, script_name) = None, mktemp(".py")
388 log.info("writing byte-compilation script '%s'", script_name)
389 if not dry_run:
390 if script_fd is not None:
391 script = os.fdopen(script_fd, "w")
392 else:
393 script = open(script_name, "w")
394
395 with script:
396 script.write("""\
397 from distutils.util import byte_compile
398 files = [
399 """)
400
401 # XXX would be nice to write absolute filenames, just for
402 # safety's sake (script should be more robust in the face of
403 # chdir'ing before running it). But this requires abspath'ing
404 # 'prefix' as well, and that breaks the hack in build_lib's
405 # 'byte_compile()' method that carefully tacks on a trailing
406 # slash (os.sep really) to make sure the prefix here is "just
407 # right". This whole prefix business is rather delicate -- the
408 # problem is that it's really a directory, but I'm treating it
409 # as a dumb string, so trailing slashes and so forth matter.
410
411 #py_files = map(os.path.abspath, py_files)
412 #if prefix:
413 # prefix = os.path.abspath(prefix)
414
415 script.write(",\n".join(map(repr, py_files)) + "]\n")
416 script.write("""
417 byte_compile(files, optimize=%r, force=%r,
418 prefix=%r, base_dir=%r,
419 verbose=%r, dry_run=0,
420 direct=1)
421 """ % (optimize, force, prefix, base_dir, verbose))
422
423 msg = distutils._DEPRECATION_MESSAGE
424 cmd = [sys.executable]
425 cmd.extend(subprocess._optim_args_from_interpreter_flags())
426 cmd.append(f'-Wignore:{msg}:DeprecationWarning')
427 cmd.append(script_name)
428 spawn(cmd, dry_run=dry_run)
429 execute(os.remove, (script_name,), "removing %s" % script_name,
430 dry_run=dry_run)
431
432 # "Direct" byte-compilation: use the py_compile module to compile
433 # right here, right now. Note that the script generated in indirect
434 # mode simply calls 'byte_compile()' in direct mode, a weird sort of
435 # cross-process recursion. Hey, it works!
436 else:
437 from py_compile import compile
438
439 for file in py_files:
440 if file[-3:] != ".py":
441 # This lets us be lazy and not filter filenames in
442 # the "install_lib" command.
443 continue
444
445 # Terminology from the py_compile module:
446 # cfile - byte-compiled file
447 # dfile - purported source filename (same as 'file' by default)
448 if optimize >= 0:
449 opt = '' if optimize == 0 else optimize
450 cfile = importlib.util.cache_from_source(
451 file, optimization=opt)
452 else:
453 cfile = importlib.util.cache_from_source(file)
454 dfile = file
455 if prefix:
456 if file[:len(prefix)] != prefix:
457 raise ValueError("invalid prefix: filename %r doesn't start with %r"
458 % (file, prefix))
459 dfile = dfile[len(prefix):]
460 if base_dir:
461 dfile = os.path.join(base_dir, dfile)
462
463 cfile_base = os.path.basename(cfile)
464 if direct:
465 if force or newer(file, cfile):
466 log.info("byte-compiling %s to %s", file, cfile_base)
467 if not dry_run:
468 compile(file, cfile, dfile)
469 else:
470 log.debug("skipping byte-compilation of %s to %s",
471 file, cfile_base)
472
473 # byte_compile ()
474
475 def rfc822_escape (header):
476 """Return a version of the string escaped for inclusion in an
477 RFC-822 header, by ensuring there are 8 spaces space after each newline.
478 """
479 lines = header.split('\n')
480 sep = '\n' + 8 * ' '
481 return sep.join(lines)
482
483 # 2to3 support
484
485 def run_2to3(files, fixer_names=None, options=None, explicit=None):
486 """Invoke 2to3 on a list of Python files.
487 The files should all come from the build area, as the
488 modification is done in-place. To reduce the build time,
489 only files modified since the last invocation of this
490 function should be passed in the files argument."""
491
492 if not files:
493 return
494
495 # Make this class local, to delay import of 2to3
496 from lib2to3.refactor import RefactoringTool, get_fixers_from_package
497 class ESC[4;38;5;81mDistutilsRefactoringTool(ESC[4;38;5;149mRefactoringTool):
498 def log_error(self, msg, *args, **kw):
499 log.error(msg, *args)
500
501 def log_message(self, msg, *args):
502 log.info(msg, *args)
503
504 def log_debug(self, msg, *args):
505 log.debug(msg, *args)
506
507 if fixer_names is None:
508 fixer_names = get_fixers_from_package('lib2to3.fixes')
509 r = DistutilsRefactoringTool(fixer_names, options=options)
510 r.refactor(files, write=True)
511
512 def copydir_run_2to3(src, dest, template=None, fixer_names=None,
513 options=None, explicit=None):
514 """Recursively copy a directory, only copying new and changed files,
515 running run_2to3 over all newly copied Python modules afterward.
516
517 If you give a template string, it's parsed like a MANIFEST.in.
518 """
519 from distutils.dir_util import mkpath
520 from distutils.file_util import copy_file
521 from distutils.filelist import FileList
522 filelist = FileList()
523 curdir = os.getcwd()
524 os.chdir(src)
525 try:
526 filelist.findall()
527 finally:
528 os.chdir(curdir)
529 filelist.files[:] = filelist.allfiles
530 if template:
531 for line in template.splitlines():
532 line = line.strip()
533 if not line: continue
534 filelist.process_template_line(line)
535 copied = []
536 for filename in filelist.files:
537 outname = os.path.join(dest, filename)
538 mkpath(os.path.dirname(outname))
539 res = copy_file(os.path.join(src, filename), outname, update=1)
540 if res[1]: copied.append(outname)
541 run_2to3([fn for fn in copied if fn.lower().endswith('.py')],
542 fixer_names=fixer_names, options=options, explicit=explicit)
543 return copied
544
545 class ESC[4;38;5;81mMixin2to3:
546 '''Mixin class for commands that run 2to3.
547 To configure 2to3, setup scripts may either change
548 the class variables, or inherit from individual commands
549 to override how 2to3 is invoked.'''
550
551 # provide list of fixers to run;
552 # defaults to all from lib2to3.fixers
553 fixer_names = None
554
555 # options dictionary
556 options = None
557
558 # list of fixers to invoke even though they are marked as explicit
559 explicit = None
560
561 def run_2to3(self, files):
562 return run_2to3(files, self.fixer_names, self.options, self.explicit)