1
# -*- coding: utf-8 -*-
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.
14
from .compat import sysconfig, detect_encoding, ZipFile
15
from .resources import finder
16
from .util import (FileOperator, get_export_entry, convert_path,
17
get_executable, in_venv)
19
logger = logging.getLogger(__name__)
21
_DEFAULT_MANIFEST = '''
22
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
23
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
24
<assemblyIdentity version="1.0.0.0"
25
processorArchitecture="X86"
29
<!-- Identify the application security requirements. -->
30
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
33
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
34
</requestedPrivileges>
37
</assembly>'''.strip()
39
# check if Python is called on the first line with this expression
40
FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$')
41
SCRIPT_TEMPLATE = '''# -*- coding: utf-8 -*-
42
if __name__ == '__main__':
45
def _resolve(module, func):
47
mod = sys.modules[module]
48
parts = func.split('.')
49
result = getattr(mod, parts.pop(0))
51
result = getattr(result, p)
55
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
57
func = _resolve('%(module)s', '%(func)s')
58
rc = func() # None interpreted as 0
59
except Exception as e: # only supporting Python >= 2.6
60
sys.stderr.write('%%s\\n' %% e)
66
def _enquote_executable(executable):
68
# make sure we quote only the executable in case of env
69
# for example /usr/bin/env "/dir with spaces/bin/jython"
70
# instead of "/usr/bin/env /dir with spaces/bin/jython"
72
if executable.startswith('/usr/bin/env '):
73
env, _executable = executable.split(' ', 1)
74
if ' ' in _executable and not _executable.startswith('"'):
75
executable = '%s "%s"' % (env, _executable)
77
if not executable.startswith('"'):
78
executable = '"%s"' % executable
82
class ScriptMaker(object):
84
A class to copy or create scripts from source scripts or callable
87
script_template = SCRIPT_TEMPLATE
89
executable = None # for shebangs
91
def __init__(self, source_dir, target_dir, add_launchers=True,
92
dry_run=False, fileop=None):
93
self.source_dir = source_dir
94
self.target_dir = target_dir
95
self.add_launchers = add_launchers
98
# It only makes sense to set mode bits on POSIX.
99
self.set_mode = (os.name == 'posix') or (os.name == 'java' and
101
self.variants = set(('', 'X.Y'))
102
self._fileop = fileop or FileOperator(dry_run)
104
self._is_nt = os.name == 'nt' or (
105
os.name == 'java' and os._name == 'nt')
107
def _get_alternate_executable(self, executable, options):
108
if options.get('gui', False) and self._is_nt: # pragma: no cover
109
dn, fn = os.path.split(executable)
110
fn = fn.replace('python', 'pythonw')
111
executable = os.path.join(dn, fn)
114
if sys.platform.startswith('java'): # pragma: no cover
115
def _is_shell(self, executable):
117
Determine if the specified executable is a script
121
with open(executable) as fp:
122
return fp.read(2) == '#!'
123
except (OSError, IOError):
124
logger.warning('Failed to open %s', executable)
127
def _fix_jython_executable(self, executable):
128
if self._is_shell(executable):
129
# Workaround for Jython is not needed on Linux systems.
132
if java.lang.System.getProperty('os.name') == 'Linux':
134
elif executable.lower().endswith('jython.exe'):
135
# Use wrapper exe for Jython on Windows
137
return '/usr/bin/env %s' % executable
139
def _get_shebang(self, encoding, post_interp=b'', options=None):
142
executable = self.executable
143
enquote = False # assume this will be taken care of
144
elif not sysconfig.is_python_build():
145
executable = get_executable()
146
elif in_venv(): # pragma: no cover
147
executable = os.path.join(sysconfig.get_path('scripts'),
148
'python%s' % sysconfig.get_config_var('EXE'))
149
else: # pragma: no cover
150
executable = os.path.join(
151
sysconfig.get_config_var('BINDIR'),
152
'python%s%s' % (sysconfig.get_config_var('VERSION'),
153
sysconfig.get_config_var('EXE')))
155
executable = self._get_alternate_executable(executable, options)
157
if sys.platform.startswith('java'): # pragma: no cover
158
executable = self._fix_jython_executable(executable)
159
# Normalise case for Windows
160
executable = os.path.normcase(executable)
161
# If the user didn't specify an executable, it may be necessary to
162
# cater for executable paths with spaces (not uncommon on Windows)
164
executable = _enquote_executable(executable)
165
# Issue #51: don't use fsencode, since we later try to
166
# check that the shebang is decodable using utf-8.
167
executable = executable.encode('utf-8')
168
# in case of IronPython, play safe and enable frames support
169
if (sys.platform == 'cli' and '-X:Frames' not in post_interp
170
and '-X:FullFrames' not in post_interp): # pragma: no cover
171
post_interp += b' -X:Frames'
172
shebang = b'#!' + executable + post_interp + b'\n'
173
# Python parser starts to read a script using UTF-8 until
174
# it gets a #coding:xxx cookie. The shebang has to be the
175
# first line of a file, the #coding:xxx cookie cannot be
176
# written before. So the shebang has to be decodable from
179
shebang.decode('utf-8')
180
except UnicodeDecodeError: # pragma: no cover
182
'The shebang (%r) is not decodable from utf-8' % shebang)
183
# If the script is encoded to a custom encoding (use a
184
# #coding:xxx cookie), the shebang has to be decodable from
185
# the script encoding too.
186
if encoding != 'utf-8':
188
shebang.decode(encoding)
189
except UnicodeDecodeError: # pragma: no cover
191
'The shebang (%r) is not decodable '
192
'from the script encoding (%r)' % (shebang, encoding))
195
def _get_script_text(self, entry):
196
return self.script_template % dict(module=entry.prefix,
199
manifest = _DEFAULT_MANIFEST
201
def get_manifest(self, exename):
202
base = os.path.basename(exename)
203
return self.manifest % base
205
def _write_script(self, names, shebang, script_bytes, filenames, ext):
206
use_launcher = self.add_launchers and self._is_nt
207
linesep = os.linesep.encode('utf-8')
209
script_bytes = shebang + linesep + script_bytes
210
else: # pragma: no cover
212
launcher = self._get_launcher('t')
214
launcher = self._get_launcher('w')
216
with ZipFile(stream, 'w') as zf:
217
zf.writestr('__main__.py', script_bytes)
218
zip_data = stream.getvalue()
219
script_bytes = launcher + shebang + linesep + zip_data
221
outname = os.path.join(self.target_dir, name)
222
if use_launcher: # pragma: no cover
223
n, e = os.path.splitext(outname)
224
if e.startswith('.py'):
226
outname = '%s.exe' % outname
228
self._fileop.write_binary_file(outname, script_bytes)
230
# Failed writing an executable - it might be in use.
231
logger.warning('Failed to write executable - trying to '
232
'use .deleteme logic')
233
dfname = '%s.deleteme' % outname
234
if os.path.exists(dfname):
235
os.remove(dfname) # Not allowed to fail here
236
os.rename(outname, dfname) # nor here
237
self._fileop.write_binary_file(outname, script_bytes)
238
logger.debug('Able to replace executable using '
243
pass # still in use - ignore error
245
if self._is_nt and not outname.endswith('.' + ext): # pragma: no cover
246
outname = '%s.%s' % (outname, ext)
247
if os.path.exists(outname) and not self.clobber:
248
logger.warning('Skipping existing file %s', outname)
250
self._fileop.write_binary_file(outname, script_bytes)
252
self._fileop.set_executable_mode([outname])
253
filenames.append(outname)
255
def _make_script(self, entry, filenames, options=None):
258
args = options.get('interpreter_args', [])
260
args = ' %s' % ' '.join(args)
261
post_interp = args.encode('utf-8')
262
shebang = self._get_shebang('utf-8', post_interp, options=options)
263
script = self._get_script_text(entry).encode('utf-8')
266
if '' in self.variants:
267
scriptnames.add(name)
268
if 'X' in self.variants:
269
scriptnames.add('%s%s' % (name, sys.version[0]))
270
if 'X.Y' in self.variants:
271
scriptnames.add('%s-%s' % (name, sys.version[:3]))
272
if options and options.get('gui', False):
276
self._write_script(scriptnames, shebang, script, filenames, ext)
278
def _copy_script(self, script, filenames):
280
script = os.path.join(self.source_dir, convert_path(script))
281
outname = os.path.join(self.target_dir, os.path.basename(script))
282
if not self.force and not self._fileop.newer(script, outname):
283
logger.debug('not copying %s (up-to-date)', script)
286
# Always open the file, but ignore failures in dry-run mode --
287
# that way, we'll get accurate feedback if we can read the
290
f = open(script, 'rb')
291
except IOError: # pragma: no cover
296
first_line = f.readline()
297
if not first_line: # pragma: no cover
298
logger.warning('%s: %s is an empty file (skipping)',
299
self.get_command_name(), script)
302
match = FIRST_LINE_RE.match(first_line.replace(b'\r\n', b'\n'))
305
post_interp = match.group(1) or b''
310
self._fileop.copy_file(script, outname)
312
self._fileop.set_executable_mode([outname])
313
filenames.append(outname)
315
logger.info('copying and adjusting %s -> %s', script,
317
if not self._fileop.dry_run:
318
encoding, lines = detect_encoding(f.readline)
320
shebang = self._get_shebang(encoding, post_interp)
321
if b'pythonw' in first_line: # pragma: no cover
325
n = os.path.basename(outname)
326
self._write_script([n], shebang, f.read(), filenames, ext)
332
return self._fileop.dry_run
335
def dry_run(self, value):
336
self._fileop.dry_run = value
338
if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'): # pragma: no cover
339
# Executable launcher support.
340
# Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/
342
def _get_launcher(self, kind):
343
if struct.calcsize('P') == 8: # 64-bit
347
name = '%s%s.exe' % (kind, bits)
348
# Issue 31: don't hardcode an absolute package name, but
349
# determine it relative to the current package
350
distlib_package = __name__.rsplit('.', 1)[0]
351
result = finder(distlib_package).find(name).bytes
356
def make(self, specification, options=None):
360
:param specification: The specification, which is either a valid export
361
entry specification (to make a script from a
362
callable) or a filename (to make a script by
363
copying from a source location).
364
:param options: A dictionary of options controlling script generation.
365
:return: A list of all absolute pathnames written to.
368
entry = get_export_entry(specification)
370
self._copy_script(specification, filenames)
372
self._make_script(entry, filenames, options=options)
375
def make_multiple(self, specifications, options=None):
377
Take a list of specifications and make scripts from them,
378
:param specifications: A list of specifications.
379
:return: A list of all absolute pathnames written to,
382
for specification in specifications:
383
filenames.extend(self.make(specification, options))