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

169 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:48 +0000

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

2# 

3# Copyright (C) 2013-2015 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 .compat import sysconfig, detect_encoding, ZipFile 

17from .resources import finder 

18from .util import (FileOperator, get_export_entry, convert_path, 

19 get_executable, get_platform, in_venv) 

20 

21logger = logging.getLogger(__name__) 

22 

23_DEFAULT_MANIFEST = ''' 

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

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

26 <assemblyIdentity version="1.0.0.0" 

27 processorArchitecture="X86" 

28 name="%s" 

29 type="win32"/> 

30 

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

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

33 <security> 

34 <requestedPrivileges> 

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

36 </requestedPrivileges> 

37 </security> 

38 </trustInfo> 

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

40 

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

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

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

44import re 

45import sys 

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

47if __name__ == '__main__': 

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

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

50''' 

51 

52 

53def enquote_executable(executable): 

54 if ' ' in executable: 

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

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

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

58 # otherwise whole 

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

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

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

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

63 else: 

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

65 executable = '"%s"' % executable 

66 return executable 

67 

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

69_enquote_executable = enquote_executable 

70 

71class ScriptMaker(object): 

72 """ 

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

74 specifications. 

75 """ 

76 script_template = SCRIPT_TEMPLATE 

77 

78 executable = None # for shebangs 

79 

80 def __init__(self, source_dir, target_dir, add_launchers=True, 

81 dry_run=False, fileop=None): 

82 self.source_dir = source_dir 

83 self.target_dir = target_dir 

84 self.add_launchers = add_launchers 

85 self.force = False 

86 self.clobber = False 

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

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

89 os._name == 'posix') 

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

91 self._fileop = fileop or FileOperator(dry_run) 

92 

93 self._is_nt = os.name == 'nt' or ( 

94 os.name == 'java' and os._name == 'nt') 

95 self.version_info = sys.version_info 

96 

97 def _get_alternate_executable(self, executable, options): 

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

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

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

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

102 return executable 

103 

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

105 def _is_shell(self, executable): 

106 """ 

107 Determine if the specified executable is a script 

108 (contains a #! line) 

109 """ 

110 try: 

111 with open(executable) as fp: 

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

113 except (OSError, IOError): 

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

115 return False 

116 

117 def _fix_jython_executable(self, executable): 

118 if self._is_shell(executable): 

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

120 import java 

121 

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

123 return executable 

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

125 # Use wrapper exe for Jython on Windows 

126 return executable 

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

128 

129 def _build_shebang(self, executable, post_interp): 

130 """ 

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

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

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

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

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

136 

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

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

139 """ 

140 if os.name != 'posix': 

141 simple_shebang = True 

142 else: 

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

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

145 if sys.platform == 'darwin': 

146 max_shebang_length = 512 

147 else: 

148 max_shebang_length = 127 

149 simple_shebang = ((b' ' not in executable) and 

150 (shebang_length <= max_shebang_length)) 

151 

152 if simple_shebang: 

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

154 else: 

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

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

157 result += b"' '''" 

158 return result 

159 

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

161 enquote = True 

162 if self.executable: 

163 executable = self.executable 

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

165 elif not sysconfig.is_python_build(): 

166 executable = get_executable() 

167 elif in_venv(): # pragma: no cover 

168 executable = os.path.join(sysconfig.get_path('scripts'), 

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

170 else: # pragma: no cover 

171 executable = os.path.join( 

172 sysconfig.get_config_var('BINDIR'), 

173 'python%s%s' % (sysconfig.get_config_var('VERSION'), 

174 sysconfig.get_config_var('EXE'))) 

175 if not os.path.isfile(executable): 

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

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

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

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

180 if options: 

181 executable = self._get_alternate_executable(executable, options) 

182 

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

184 executable = self._fix_jython_executable(executable) 

185 

186 # Normalise case for Windows - COMMENTED OUT 

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

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

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

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

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

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

193 # Windows as equivalent in path names. 

194 

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

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

197 if enquote: 

198 executable = enquote_executable(executable) 

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

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

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

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

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

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

205 post_interp += b' -X:Frames' 

206 shebang = self._build_shebang(executable, post_interp) 

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

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

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

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

211 # UTF-8. 

212 try: 

213 shebang.decode('utf-8') 

214 except UnicodeDecodeError: # pragma: no cover 

215 raise ValueError( 

216 'The shebang (%r) is not decodable from utf-8' % shebang) 

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

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

219 # the script encoding too. 

220 if encoding != 'utf-8': 

221 try: 

222 shebang.decode(encoding) 

223 except UnicodeDecodeError: # pragma: no cover 

224 raise ValueError( 

225 'The shebang (%r) is not decodable ' 

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

227 return shebang 

228 

229 def _get_script_text(self, entry): 

230 return self.script_template % dict(module=entry.prefix, 

231 import_name=entry.suffix.split('.')[0], 

232 func=entry.suffix) 

233 

234 manifest = _DEFAULT_MANIFEST 

235 

236 def get_manifest(self, exename): 

237 base = os.path.basename(exename) 

238 return self.manifest % base 

239 

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

241 use_launcher = self.add_launchers and self._is_nt 

242 linesep = os.linesep.encode('utf-8') 

243 if not shebang.endswith(linesep): 

244 shebang += linesep 

245 if not use_launcher: 

246 script_bytes = shebang + script_bytes 

247 else: # pragma: no cover 

248 if ext == 'py': 

249 launcher = self._get_launcher('t') 

250 else: 

251 launcher = self._get_launcher('w') 

252 stream = BytesIO() 

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

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

255 if source_date_epoch: 

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

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

258 zf.writestr(zinfo, script_bytes) 

259 else: 

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

261 zip_data = stream.getvalue() 

262 script_bytes = launcher + shebang + zip_data 

263 for name in names: 

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

265 if use_launcher: # pragma: no cover 

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

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

268 outname = n 

269 outname = '%s.exe' % outname 

270 try: 

271 self._fileop.write_binary_file(outname, script_bytes) 

272 except Exception: 

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

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

275 'use .deleteme logic') 

276 dfname = '%s.deleteme' % outname 

277 if os.path.exists(dfname): 

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

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

280 self._fileop.write_binary_file(outname, script_bytes) 

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

282 '.deleteme logic') 

283 try: 

284 os.remove(dfname) 

285 except Exception: 

286 pass # still in use - ignore error 

287 else: 

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

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

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

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

292 continue 

293 self._fileop.write_binary_file(outname, script_bytes) 

294 if self.set_mode: 

295 self._fileop.set_executable_mode([outname]) 

296 filenames.append(outname) 

297 

298 variant_separator = '-' 

299 

300 def get_script_filenames(self, name): 

301 result = set() 

302 if '' in self.variants: 

303 result.add(name) 

304 if 'X' in self.variants: 

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

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

307 result.add('%s%s%s.%s' % (name, self.variant_separator, 

308 self.version_info[0], self.version_info[1])) 

309 return result 

310 

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

312 post_interp = b'' 

313 if options: 

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

315 if args: 

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

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

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

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

320 scriptnames = self.get_script_filenames(entry.name) 

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

322 ext = 'pyw' 

323 else: 

324 ext = 'py' 

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

326 

327 def _copy_script(self, script, filenames): 

328 adjust = False 

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

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

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

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

333 return 

334 

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

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

337 # script. 

338 try: 

339 f = open(script, 'rb') 

340 except IOError: # pragma: no cover 

341 if not self.dry_run: 

342 raise 

343 f = None 

344 else: 

345 first_line = f.readline() 

346 if not first_line: # pragma: no cover 

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

348 return 

349 

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

351 if match: 

352 adjust = True 

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

354 

355 if not adjust: 

356 if f: 

357 f.close() 

358 self._fileop.copy_file(script, outname) 

359 if self.set_mode: 

360 self._fileop.set_executable_mode([outname]) 

361 filenames.append(outname) 

362 else: 

363 logger.info('copying and adjusting %s -> %s', script, 

364 self.target_dir) 

365 if not self._fileop.dry_run: 

366 encoding, lines = detect_encoding(f.readline) 

367 f.seek(0) 

368 shebang = self._get_shebang(encoding, post_interp) 

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

370 ext = 'pyw' 

371 else: 

372 ext = 'py' 

373 n = os.path.basename(outname) 

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

375 if f: 

376 f.close() 

377 

378 @property 

379 def dry_run(self): 

380 return self._fileop.dry_run 

381 

382 @dry_run.setter 

383 def dry_run(self, value): 

384 self._fileop.dry_run = value 

385 

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

387 # Executable launcher support. 

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

389 

390 def _get_launcher(self, kind): 

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

392 bits = '64' 

393 else: 

394 bits = '32' 

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

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

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

398 # determine it relative to the current package 

399 distlib_package = __name__.rsplit('.', 1)[0] 

400 resource = finder(distlib_package).find(name) 

401 if not resource: 

402 msg = ('Unable to find resource %s in package %s' % (name, 

403 distlib_package)) 

404 raise ValueError(msg) 

405 return resource.bytes 

406 

407 # Public API follows 

408 

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

410 """ 

411 Make a script. 

412 

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

414 entry specification (to make a script from a 

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

416 copying from a source location). 

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

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

419 """ 

420 filenames = [] 

421 entry = get_export_entry(specification) 

422 if entry is None: 

423 self._copy_script(specification, filenames) 

424 else: 

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

426 return filenames 

427 

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

429 """ 

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

431 :param specifications: A list of specifications. 

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

433 """ 

434 filenames = [] 

435 for specification in specifications: 

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

437 return filenames