Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_vendor/distlib/scripts.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

175 statements  

1# -*- coding: utf-8 -*- 

2# 

3# Copyright (C) 2013-2026 Vinay Sajip. 

4# Licensed to the Python Software Foundation under a contributor agreement. 

5# See LICENSE.txt and CONTRIBUTORS.txt. 

6# 

7from io import BytesIO 

8import logging 

9import os 

10import re 

11import struct 

12import sys 

13import time 

14from zipfile import ZipInfo 

15 

16from . import DistlibException 

17from .compat import sysconfig, detect_encoding, ZipFile 

18from .resources import finder 

19from .util import (FileOperator, get_export_entry, convert_path, get_executable, get_platform, in_venv, 

20 is_in_directory) 

21 

22logger = logging.getLogger(__name__) 

23 

24_DEFAULT_MANIFEST = ''' 

25<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 

26<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> 

27 <assemblyIdentity version="1.0.0.0" 

28 processorArchitecture="X86" 

29 name="%s" 

30 type="win32"/> 

31 

32 <!-- Identify the application security requirements. --> 

33 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> 

34 <security> 

35 <requestedPrivileges> 

36 <requestedExecutionLevel level="asInvoker" uiAccess="false"/> 

37 </requestedPrivileges> 

38 </security> 

39 </trustInfo> 

40</assembly>'''.strip() 

41 

42# check if Python is called on the first line with this expression 

43FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$') 

44SCRIPT_TEMPLATE = r'''# -*- coding: utf-8 -*- 

45import re 

46import sys 

47if __name__ == '__main__': 

48 from %(module)s import %(import_name)s 

49 sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 

50 sys.exit(%(func)s()) 

51''' 

52 

53# Pre-fetch the contents of all executable wrapper stubs. 

54# This is to address https://github.com/pypa/pip/issues/12666. 

55# When updating pip, we rename the old pip in place before installing the 

56# new version. If we try to fetch a wrapper *after* that rename, the finder 

57# machinery will be confused as the package is no longer available at the 

58# location where it was imported from. So we load everything into memory in 

59# advance. 

60 

61if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'): 

62 # Issue 31: don't hardcode an absolute package name, but 

63 # determine it relative to the current package 

64 DISTLIB_PACKAGE = __name__.rsplit('.', 1)[0] 

65 

66 WRAPPERS = { 

67 r.name: r.bytes 

68 for r in finder(DISTLIB_PACKAGE).iterator("") 

69 if r.name.endswith(".exe") 

70 } 

71 

72 

73def enquote_executable(executable): 

74 if ' ' in executable: 

75 # make sure we quote only the executable in case of env 

76 # for example /usr/bin/env "/dir with spaces/bin/jython" 

77 # instead of "/usr/bin/env /dir with spaces/bin/jython" 

78 # otherwise whole 

79 if executable.startswith('/usr/bin/env '): 

80 env, _executable = executable.split(' ', 1) 

81 if ' ' in _executable and not _executable.startswith('"'): 

82 executable = '%s "%s"' % (env, _executable) 

83 else: 

84 if not executable.startswith('"'): 

85 executable = '"%s"' % executable 

86 return executable 

87 

88 

89# Keep the old name around (for now), as there is at least one project using it! 

90_enquote_executable = enquote_executable 

91 

92 

93class ScriptMaker(object): 

94 """ 

95 A class to copy or create scripts from source scripts or callable 

96 specifications. 

97 """ 

98 script_template = SCRIPT_TEMPLATE 

99 

100 executable = None # for shebangs 

101 

102 def __init__(self, source_dir, target_dir, add_launchers=True, dry_run=False, fileop=None): 

103 self.source_dir = source_dir 

104 self.target_dir = target_dir 

105 self.add_launchers = add_launchers 

106 self.force = False 

107 self.clobber = False 

108 # It only makes sense to set mode bits on POSIX. 

109 self.set_mode = (os.name == 'posix') or (os.name == 'java' and os._name == 'posix') 

110 self.variants = set(('', 'X.Y')) 

111 self._fileop = fileop or FileOperator(dry_run) 

112 

113 self._is_nt = os.name == 'nt' or (os.name == 'java' and os._name == 'nt') 

114 self.version_info = sys.version_info 

115 

116 def _get_alternate_executable(self, executable, options): 

117 if options.get('gui', False) and self._is_nt: # pragma: no cover 

118 dn, fn = os.path.split(executable) 

119 fn = fn.replace('python', 'pythonw') 

120 executable = os.path.join(dn, fn) 

121 return executable 

122 

123 if sys.platform.startswith('java'): # pragma: no cover 

124 

125 def _is_shell(self, executable): 

126 """ 

127 Determine if the specified executable is a script 

128 (contains a #! line) 

129 """ 

130 try: 

131 with open(executable) as fp: 

132 return fp.read(2) == '#!' 

133 except (OSError, IOError): 

134 logger.warning('Failed to open %s', executable) 

135 return False 

136 

137 def _fix_jython_executable(self, executable): 

138 if self._is_shell(executable): 

139 # Workaround for Jython is not needed on Linux systems. 

140 import java 

141 

142 if java.lang.System.getProperty('os.name') == 'Linux': 

143 return executable 

144 elif executable.lower().endswith('jython.exe'): 

145 # Use wrapper exe for Jython on Windows 

146 return executable 

147 return '/usr/bin/env %s' % executable 

148 

149 def _build_shebang(self, executable, post_interp): 

150 """ 

151 Build a shebang line. In the simple case (on Windows, or a shebang line 

152 which is not too long or contains spaces) use a simple formulation for 

153 the shebang. Otherwise, use /bin/sh as the executable, with a contrived 

154 shebang which allows the script to run either under Python or sh, using 

155 suitable quoting. Thanks to Harald Nordgren for his input. 

156 

157 See also: http://www.in-ulm.de/~mascheck/various/shebang/#length 

158 https://hg.mozilla.org/mozilla-central/file/tip/mach 

159 """ 

160 if os.name != 'posix': 

161 simple_shebang = True 

162 elif getattr(sys, "cross_compiling", False): 

163 # In a cross-compiling environment, the shebang will likely be a 

164 # script; this *must* be invoked with the "safe" version of the 

165 # shebang, or else using os.exec() to run the entry script will 

166 # fail, raising "OSError 8 [Errno 8] Exec format error". 

167 simple_shebang = False 

168 else: 

169 # Add 3 for '#!' prefix and newline suffix. 

170 shebang_length = len(executable) + len(post_interp) + 3 

171 if sys.platform == 'darwin': 

172 max_shebang_length = 512 

173 else: 

174 max_shebang_length = 127 

175 simple_shebang = ((b' ' not in executable) and (shebang_length <= max_shebang_length)) 

176 

177 if simple_shebang: 

178 result = b'#!' + executable + post_interp + b'\n' 

179 else: 

180 result = b'#!/bin/sh\n' 

181 result += b"'''exec' " + executable + post_interp + b' "$0" "$@"\n' 

182 result += b"' '''\n" 

183 return result 

184 

185 def _get_shebang(self, encoding, post_interp=b'', options=None): 

186 enquote = True 

187 if self.executable: 

188 executable = self.executable 

189 enquote = False # assume this will be taken care of 

190 elif not sysconfig.is_python_build(): 

191 executable = get_executable() 

192 elif in_venv(): # pragma: no cover 

193 executable = os.path.join(sysconfig.get_path('scripts'), 'python%s' % sysconfig.get_config_var('EXE')) 

194 else: # pragma: no cover 

195 if os.name == 'nt': 

196 # for Python builds from source on Windows, no Python executables with 

197 # a version suffix are created, so we use python.exe 

198 executable = os.path.join(sysconfig.get_config_var('BINDIR'), 

199 'python%s' % (sysconfig.get_config_var('EXE'))) 

200 else: 

201 executable = os.path.join( 

202 sysconfig.get_config_var('BINDIR'), 

203 'python%s%s' % (sysconfig.get_config_var('VERSION'), sysconfig.get_config_var('EXE'))) 

204 if options: 

205 executable = self._get_alternate_executable(executable, options) 

206 

207 if sys.platform.startswith('java'): # pragma: no cover 

208 executable = self._fix_jython_executable(executable) 

209 

210 # Normalise case for Windows - COMMENTED OUT 

211 # executable = os.path.normcase(executable) 

212 # N.B. The normalising operation above has been commented out: See 

213 # issue #124. Although paths in Windows are generally case-insensitive, 

214 # they aren't always. For example, a path containing a ẞ (which is a 

215 # LATIN CAPITAL LETTER SHARP S - U+1E9E) is normcased to ß (which is a 

216 # LATIN SMALL LETTER SHARP S' - U+00DF). The two are not considered by 

217 # Windows as equivalent in path names. 

218 

219 # If the user didn't specify an executable, it may be necessary to 

220 # cater for executable paths with spaces (not uncommon on Windows) 

221 if enquote: 

222 executable = enquote_executable(executable) 

223 # Issue #51: don't use fsencode, since we later try to 

224 # check that the shebang is decodable using utf-8. 

225 executable = executable.encode('utf-8') 

226 # in case of IronPython, play safe and enable frames support 

227 if (sys.platform == 'cli' and '-X:Frames' not in post_interp and 

228 '-X:FullFrames' not in post_interp): # pragma: no cover 

229 post_interp += b' -X:Frames' 

230 shebang = self._build_shebang(executable, post_interp) 

231 # Python parser starts to read a script using UTF-8 until 

232 # it gets a #coding:xxx cookie. The shebang has to be the 

233 # first line of a file, the #coding:xxx cookie cannot be 

234 # written before. So the shebang has to be decodable from 

235 # UTF-8. 

236 try: 

237 shebang.decode('utf-8') 

238 except UnicodeDecodeError: # pragma: no cover 

239 raise ValueError('The shebang (%r) is not decodable from utf-8' % shebang) 

240 # If the script is encoded to a custom encoding (use a 

241 # #coding:xxx cookie), the shebang has to be decodable from 

242 # the script encoding too. 

243 if encoding != 'utf-8': 

244 try: 

245 shebang.decode(encoding) 

246 except UnicodeDecodeError: # pragma: no cover 

247 raise ValueError('The shebang (%r) is not decodable ' 

248 'from the script encoding (%r)' % (shebang, encoding)) 

249 return shebang 

250 

251 def _get_script_text(self, entry): 

252 return self.script_template % dict( 

253 module=entry.prefix, import_name=entry.suffix.split('.')[0], func=entry.suffix) 

254 

255 manifest = _DEFAULT_MANIFEST 

256 

257 def get_manifest(self, exename): 

258 base = os.path.basename(exename) 

259 return self.manifest % base 

260 

261 def _write_script(self, names, shebang, script_bytes, filenames, ext): 

262 use_launcher = self.add_launchers and self._is_nt 

263 if not use_launcher: 

264 script_bytes = shebang + script_bytes 

265 else: # pragma: no cover 

266 if ext == 'py': 

267 launcher = self._get_launcher('t') 

268 else: 

269 launcher = self._get_launcher('w') 

270 stream = BytesIO() 

271 with ZipFile(stream, 'w') as zf: 

272 source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH') 

273 if source_date_epoch: 

274 date_time = time.gmtime(int(source_date_epoch))[:6] 

275 zinfo = ZipInfo(filename='__main__.py', date_time=date_time) 

276 zf.writestr(zinfo, script_bytes) 

277 else: 

278 zf.writestr('__main__.py', script_bytes) 

279 zip_data = stream.getvalue() 

280 script_bytes = launcher + shebang + zip_data 

281 for name in names: 

282 outname = os.path.abspath(os.path.join(self.target_dir, name)) 

283 if not is_in_directory(outname, self.target_dir): 

284 raise DistlibException('Attempt to escape script directory') 

285 if use_launcher: # pragma: no cover 

286 n, e = os.path.splitext(outname) 

287 if e.startswith('.py'): 

288 outname = n 

289 outname = '%s.exe' % outname 

290 try: 

291 self._fileop.write_binary_file(outname, script_bytes) 

292 except Exception: 

293 # Failed writing an executable - it might be in use. 

294 logger.warning('Failed to write executable - trying to ' 

295 'use .deleteme logic') 

296 dfname = '%s.deleteme' % outname 

297 if os.path.exists(dfname): 

298 os.remove(dfname) # Not allowed to fail here 

299 os.rename(outname, dfname) # nor here 

300 self._fileop.write_binary_file(outname, script_bytes) 

301 logger.debug('Able to replace executable using ' 

302 '.deleteme logic') 

303 try: 

304 os.remove(dfname) 

305 except Exception: 

306 pass # still in use - ignore error 

307 else: 

308 if self._is_nt and not outname.endswith('.' + ext): # pragma: no cover 

309 outname = '%s.%s' % (outname, ext) 

310 if os.path.exists(outname) and not self.clobber: 

311 logger.warning('Skipping existing file %s', outname) 

312 continue 

313 self._fileop.write_binary_file(outname, script_bytes) 

314 if self.set_mode: 

315 self._fileop.set_executable_mode([outname]) 

316 filenames.append(outname) 

317 

318 variant_separator = '-' 

319 

320 def get_script_filenames(self, name): 

321 result = set() 

322 if '' in self.variants: 

323 result.add(name) 

324 if 'X' in self.variants: 

325 result.add('%s%s' % (name, self.version_info[0])) 

326 if 'X.Y' in self.variants: 

327 result.add('%s%s%s.%s' % (name, self.variant_separator, self.version_info[0], self.version_info[1])) 

328 return result 

329 

330 def _make_script(self, entry, filenames, options=None): 

331 post_interp = b'' 

332 if options: 

333 args = options.get('interpreter_args', []) 

334 if args: 

335 args = ' %s' % ' '.join(args) 

336 post_interp = args.encode('utf-8') 

337 shebang = self._get_shebang('utf-8', post_interp, options=options) 

338 script = self._get_script_text(entry).encode('utf-8') 

339 scriptnames = self.get_script_filenames(entry.name) 

340 if options and options.get('gui', False): 

341 ext = 'pyw' 

342 else: 

343 ext = 'py' 

344 self._write_script(scriptnames, shebang, script, filenames, ext) 

345 

346 def _copy_script(self, script, filenames): 

347 adjust = False 

348 script = os.path.join(self.source_dir, convert_path(script)) 

349 outname = os.path.join(self.target_dir, os.path.basename(script)) 

350 if not self.force and not self._fileop.newer(script, outname): 

351 logger.debug('not copying %s (up-to-date)', script) 

352 return 

353 

354 # Always open the file, but ignore failures in dry-run mode -- 

355 # that way, we'll get accurate feedback if we can read the 

356 # script. 

357 try: 

358 f = open(script, 'rb') 

359 except IOError: # pragma: no cover 

360 if not self.dry_run: 

361 raise 

362 f = None 

363 else: 

364 first_line = f.readline() 

365 if not first_line: # pragma: no cover 

366 logger.warning('%s is an empty file (skipping)', script) 

367 return 

368 

369 match = FIRST_LINE_RE.match(first_line.replace(b'\r\n', b'\n')) 

370 if match: 

371 adjust = True 

372 post_interp = match.group(1) or b'' 

373 

374 if not adjust: 

375 if f: 

376 f.close() 

377 self._fileop.copy_file(script, outname) 

378 if self.set_mode: 

379 self._fileop.set_executable_mode([outname]) 

380 filenames.append(outname) 

381 else: 

382 logger.info('copying and adjusting %s -> %s', script, self.target_dir) 

383 if not self._fileop.dry_run: 

384 encoding, lines = detect_encoding(f.readline) 

385 f.seek(0) 

386 shebang = self._get_shebang(encoding, post_interp) 

387 if b'pythonw' in first_line: # pragma: no cover 

388 ext = 'pyw' 

389 else: 

390 ext = 'py' 

391 n = os.path.basename(outname) 

392 self._write_script([n], shebang, f.read(), filenames, ext) 

393 if f: 

394 f.close() 

395 

396 @property 

397 def dry_run(self): 

398 return self._fileop.dry_run 

399 

400 @dry_run.setter 

401 def dry_run(self, value): 

402 self._fileop.dry_run = value 

403 

404 if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'): # pragma: no cover 

405 # Executable launcher support. 

406 # Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/ 

407 

408 def _get_launcher(self, kind): 

409 if struct.calcsize('P') == 8: # 64-bit 

410 bits = '64' 

411 else: 

412 bits = '32' 

413 platform_suffix = '-arm' if get_platform() == 'win-arm64' else '' 

414 name = '%s%s%s.exe' % (kind, bits, platform_suffix) 

415 if name not in WRAPPERS: 

416 msg = ('Unable to find resource %s in package %s' % 

417 (name, DISTLIB_PACKAGE)) 

418 raise ValueError(msg) 

419 return WRAPPERS[name] 

420 

421 # Public API follows 

422 

423 def make(self, specification, options=None): 

424 """ 

425 Make a script. 

426 

427 :param specification: The specification, which is either a valid export 

428 entry specification (to make a script from a 

429 callable) or a filename (to make a script by 

430 copying from a source location). 

431 :param options: A dictionary of options controlling script generation. 

432 :return: A list of all absolute pathnames written to. 

433 """ 

434 filenames = [] 

435 entry = get_export_entry(specification) 

436 if entry is None: 

437 self._copy_script(specification, filenames) 

438 else: 

439 self._make_script(entry, filenames, options=options) 

440 return filenames 

441 

442 def make_multiple(self, specifications, options=None): 

443 """ 

444 Take a list of specifications and make scripts from them, 

445 :param specifications: A list of specifications. 

446 :return: A list of all absolute pathnames written to, 

447 """ 

448 filenames = [] 

449 for specification in specifications: 

450 filenames.extend(self.make(specification, options)) 

451 return filenames