/
jython.py
557 lines (489 loc) · 19.6 KB
/
jython.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
#!/usr/bin/env python2.7 -E
# -*- coding: utf-8 -*-
# Launch script for Jython. It may be run directly (note the shebang line), but
# importantly it supplies python.exe, the launcher we use on Windows.
#
# Each time this file changes, we must regenerate an executable with
# PyInstaller, using the command:
#
# pyinstaller --onefile jython.py
#
# This is best done in a virtual environment (more about this in the Jython
# Developers' Guide).
import glob
import inspect
import os
import os.path
import pipes
import shlex
import subprocess
import sys
from collections import OrderedDict
is_windows = os.name == "nt" or (os.name == "java" and os._name == "nt")
# A note about encoding:
#
# A major motivation for this program is to launch Jython on Windows, where
# console and file encoding may be different. Command-line arguments and
# environment variables are presented in Python 2.7 as byte-data, encoded
# "somehow". It becomes important to know which decoding to use as soon as
# paths may contain non-ascii characters. It is not the console encoding.
# Experiment shows that sys.getfilesystemencoding() is generally applicable
# to arguments, environment variables and spawning a subprocess.
#
# On a Windows 10 box, this comes up with pseudo-codec 'mbcs'. This supports
# European accented characters pretty well.
#
# When localised to Chinese(simplified) the FS encoding mbcs includes many
# more points than cp936 (the console encoding), although it still struggles
# with European accented characters.
ENCODING = sys.getfilesystemencoding() or "utf-8"
def get_env(envvar, default=None):
""" Return the named environment variable, decoded to Unicode."""
v = os.environ.get(envvar, default)
# Result may be bytes but we want unicode for the command
if isinstance(v, bytes):
v = v.decode(ENCODING)
# Remove quotes sometimes necessary around the value
if v is not None and v.startswith('"') and v.endswith('"'):
v = v[1:-1]
return v
def get_env_mem(envvar, default):
""" Return the named memory environment variable, decoded to Unicode.
The default should begin with -Xmx or -Xss as in the java command,
but this part will be added to the environmental value if missing.
"""
# Tolerate default given as bytes, as we're bound to forget sometimes
if isinstance(default, bytes):
default = default.decode(ENCODING)
v = os.environ.get(envvar, default)
# Result may be bytes but we want unicode for the command
if isinstance(v, bytes):
v = v.decode(ENCODING)
# Accept either a form like 16m or one like -Xmx16m
if not v.startswith(u"-X"):
v = default[:4] + v
return v
def encode_list(args, encoding=ENCODING):
""" Convert list of Unicode strings to list of encoded byte strings."""
r = []
for a in args:
if not isinstance(a, bytes): a = a.encode(encoding)
r.append(a)
return r
def decode_list(args, encoding=ENCODING):
""" Convert list of byte strings to list of Unicode strings."""
r = []
for a in args:
if not isinstance(a, unicode): a = a.decode(encoding)
r.append(a)
return r
def parse_launcher_args(args):
""" Process the given argument list into two objects, the first part being
a namespace of checked arguments to the interpreter itself, and the rest
being the Python program it will run and its arguments.
"""
class Namespace(object):
pass
parsed = Namespace()
parsed.boot = False # --boot flag given
parsed.jdb = False # --jdb flag given
parsed.help = False # --help or -h flag given
parsed.print_requested = False # --print flag given
parsed.profile = False # --profile flag given
parsed.properties = OrderedDict() # properties to give the JVM
parsed.java = [] # any other arguments to give the JVM
it = iter(args)
next(it) # ignore sys.argv[0]
i = 1
while True:
try:
arg = next(it)
except StopIteration:
break
if arg.startswith(u"-D"):
k, v = arg[2:].split(u"=")
parsed.properties[k] = v
i += 1
elif arg in (u"-J-classpath", u"-J-cp"):
try:
next_arg = next(it)
except StopIteration:
bad_option("Argument expected for -J-classpath option")
if next_arg.startswith("-"):
bad_option("Bad option for -J-classpath")
parsed.classpath = next_arg
i += 2
elif arg.startswith(u"-J-Xmx"):
parsed.mem = arg[2:]
i += 1
elif arg.startswith(u"-J-Xss"):
parsed.stack = arg[2:]
i += 1
elif arg.startswith(u"-J"):
parsed.java.append(arg[2:])
i += 1
elif arg == u"--print":
parsed.print_requested = True
i += 1
elif arg in (u"-h", u"--help"):
parsed.help = True
elif arg in (u"--boot", u"--jdb", u"--profile"):
setattr(parsed, arg[2:], True)
i += 1
elif arg == u"--":
i += 1
break
else:
break
return parsed, args[i:]
class JythonCommand(object):
def __init__(self, args, jython_args):
self.args = args
self.jython_args = jython_args
@property
def uname(self):
if hasattr(self, "_uname"):
return self._uname
if is_windows:
self._uname = u"windows"
else:
uname = subprocess.check_output(["uname"]).strip().lower()
if uname.startswith("cygwin"):
self._uname = u"cygwin"
else:
self._uname = uname.decode(ENCODING)
return self._uname
@property
def java_home(self):
if not hasattr(self, "_java_home"):
self.setup_java_command()
return self._java_home
@property
def java_command(self):
if not hasattr(self, "_java_command"):
self.setup_java_command()
return self._java_command
def setup_java_command(self):
""" Sets java_home and java_command according to environment and parsed
launcher arguments --jdb and --help.
"""
if self.args.help:
self._java_home = None
self._java_command = u"java"
return
command = u"jdb" if self.args.jdb else u"java"
self._java_home = get_env("JAVA_HOME")
if self._java_home is None or self.uname == u"cygwin":
# Assume java or jdb on the path
self._java_command = command
else:
# Assume java or jdb in JAVA_HOME/bin
self._java_command = os.path.join(self._java_home, u"bin", command)
@property
def executable(self):
"""Path to executable"""
if hasattr(self, "_executable"):
return self._executable
# Modified from
# http://stackoverflow.com/questions/3718657/how-to-properly-determine-current-script-directory-in-python/22881871#22881871
if getattr(sys, "frozen", False): # py2exe, PyInstaller, cx_Freeze
# Frozen. Let it go with the executable path.
bytes_path = sys.executable
else:
# Not frozen. Any object defined in this file will do.
bytes_path = inspect.getfile(JythonCommand)
# Python 2 thinks in bytes. Carefully normalise in Unicode.
path = os.path.realpath(bytes_path.decode(ENCODING))
try:
# If shorter, make this relative to the CWD.
relpath = os.path.relpath(path, os.getcwdu())
if len(relpath) < len(path): path = relpath
except ValueError:
# Many reasons why this might be impossible: use an absolute path.
path = os.path.abspath(path)
self._executable = path
return self._executable
@property
def jython_home(self):
if hasattr(self, "_jython_home"):
return self._jython_home
home = get_env("JYTHON_HOME")
if home is None:
# Not just dirname twice in case dirname(executable) == ''
home = os.path.join(os.path.dirname(self.executable), u'..')
# This could be a relative path like .\..
home = os.path.normpath(home)
if self.uname == u"cygwin":
# Even on Cygwin, we need a Windows-style path for this
home = unicode_subprocess(["cygpath", "--windows", home])
self._jython_home = home
return self._jython_home
@property
def jython_opts():
return get_env("JYTHON_OPTS", "")
@property
def classpath_delimiter(self):
return ";" if (is_windows or self.uname == "cygwin") else ":"
@property
def jython_jars(self):
if hasattr(self, "_jython_jars"):
return self._jython_jars
if os.path.exists(os.path.join(self.jython_home, "jython-dev.jar")):
jars = [os.path.join(self.jython_home, "jython-dev.jar")]
if self.args.boot:
# Wildcard expansion does not work for bootclasspath
for jar in glob.glob(os.path.join(self.jython_home, "javalib", "*.jar")):
jars.append(jar)
else:
jars.append(os.path.join(self.jython_home, "javalib", "*"))
elif not os.path.exists(os.path.join(self.jython_home, "jython.jar")):
bad_option(u"""{} contains neither jython-dev.jar nor jython.jar.
Try running this script from the 'bin' directory of an installed Jython or
setting JYTHON_HOME.""".format(self.jython_home))
else:
jars = [os.path.join(self.jython_home, "jython.jar")]
self._jython_jars = jars
return self._jython_jars
@property
def java_classpath(self):
if hasattr(self.args, "classpath"):
return self.args.classpath
else:
return get_env("CLASSPATH", ".")
@property
def java_mem(self):
if hasattr(self.args, "mem"):
return self.args.mem
else:
return get_env_mem("JAVA_MEM", "-Xmx512m")
@property
def java_stack(self):
if hasattr(self.args, "stack"):
return self.args.stack
else:
return get_env_mem("JAVA_STACK", "-Xss2560k")
@property
def java_opts(self):
return [self.java_mem, self.java_stack]
@property
def java_profile_agent(self):
return os.path.join(self.jython_home, "javalib", "profile.jar")
def set_encoding(self):
if "JAVA_ENCODING" not in os.environ and self.uname == "darwin" and "file.encoding" not in self.args.properties:
self.args.properties["file.encoding"] = "UTF-8"
def make_classpath(self, jars):
return self.classpath_delimiter.join(jars)
def convert_path(self, arg):
if self.uname == u"cygwin":
if not arg.startswith(u"/cygdrive/"):
return arg.replace(u"/", u"\\")
else:
arg = arg.replace('*', r'\*') # prevent globbing
return unicode_subprocess(["cygpath", "-pw", arg])
else:
return arg
def unicode_subprocess(self, unicode_command):
""" Launch a command with subprocess.check_output() and read the
output, except everything is expected to be in Unicode.
"""
cmd = []
for c in unicode_command:
if isinstance(c, bytes):
cmd.append(c)
else:
cmd.append(c.encode(ENCODING))
return subprocess.check_output(cmd).strip().decode(ENCODING)
@property
def command(self):
# Set default file encoding for just for Darwin (?)
self.set_encoding()
# Begin to build the Java part of the ultimate command
args = [self.java_command]
args.extend(self.java_opts)
args.extend(self.args.java)
# Get the class path right (depends on --boot)
classpath = self.java_classpath
jython_jars = self.jython_jars
if self.args.boot:
args.append(u"-Xbootclasspath/a:%s" % self.convert_path(self.make_classpath(jython_jars)))
else:
classpath = self.make_classpath(jython_jars) + self.classpath_delimiter + classpath
args.extend([u"-classpath", self.convert_path(classpath)])
if "python.home" not in self.args.properties:
args.append(u"-Dpython.home=%s" % self.convert_path(self.jython_home))
if "python.executable" not in self.args.properties:
args.append(u"-Dpython.executable=%s" % self.convert_path(self.executable))
if "python.launcher.uname" not in self.args.properties:
args.append(u"-Dpython.launcher.uname=%s" % self.uname)
# Determine whether running on a tty for the benefit of
# running on Cygwin. This step is needed because the Mintty
# terminal emulator doesn't behave like a standard Microsoft
# Windows tty, and so JNR Posix doesn't detect it properly.
if "python.launcher.tty" not in self.args.properties:
args.append(u"-Dpython.launcher.tty=%s" % str(os.isatty(sys.stdin.fileno())).lower())
if self.uname == u"cygwin" and "python.console" not in self.args.properties:
args.append(u"-Dpython.console=org.python.core.PlainConsole")
if self.args.profile:
args.append(u"-XX:-UseSplitVerifier")
args.append(u"-javaagent:%s" % self.convert_path(self.java_profile_agent))
for k, v in self.args.properties.iteritems():
args.append(u"-D%s=%s" % (k, v))
args.append(u"org.python.util.jython")
if self.args.help:
args.append(u"--help")
args.extend(self.jython_args)
return args
def bad_option(msg):
print >> sys.stderr, u"""
{msg}
usage: jython [option] ... [-c cmd | -m mod | file | -] [arg] ...
Try `jython -h' for more information.
""".format(msg=msg)
sys.exit(2)
def print_help():
print >> sys.stderr, """
Jython launcher-specific options:
-Dname=value : pass name=value property to Java VM (e.g. -Dpython.path=/a/b/c)
-Jarg : pass argument through to Java VM (e.g. -J-Xmx512m)
--boot : speeds up launch performance by putting Jython jars on the boot classpath
--help : this help message
--jdb : run under JDB java debugger
--print : print the Java command with args for launching Jython instead of executing it
--profile: run with the Java Interactive Profiler (http://jiprof.sf.net)
-- : pass remaining arguments through to Jython
Jython launcher environment variables:
JAVA_MEM : Java memory size as a java option e.g. -Xmx600m or just 600m
JAVA_STACK : Java stack size as a java option e.g. -Xss5120k or just 5120k
JAVA_OPTS : options to pass directly to Java
JAVA_HOME : Java installation directory
JYTHON_HOME: Jython installation directory
JYTHON_OPTS: default command line arguments
"""
def support_java_opts(args):
""" Generator from options intended for the JVM. Options beginning -D go
through unchanged, others are prefixed with -J.
"""
# Input is expected to be Unicode, but just in case ...
if isinstance(args, bytes): args = args.decode(ENCODING)
it = iter(args)
while it:
arg = next(it)
if arg.startswith(u"-D"):
yield arg
elif arg in (u"-classpath", u"-cp"):
yield u"-J" + arg
try:
yield next(it)
except StopIteration:
bad_option("Argument expected for -classpath option in JAVA_OPTS")
else:
yield u"-J" + arg
# copied from subprocess module in Jython; see
# http://bugs.python.org/issue1724822 where it is discussed to include
# in Python 3.x for shlex:
def cmdline2list(cmdline):
"""Build an argv list from a Microsoft shell style cmdline str
The reverse of list2cmdline that follows the same MS C runtime
rules.
"""
whitespace = ' \t'
# count of preceding '\'
bs_count = 0
in_quotes = False
arg = []
argv = []
for ch in cmdline:
if ch in whitespace and not in_quotes:
if arg:
# finalize arg and reset
argv.append(''.join(arg))
arg = []
bs_count = 0
elif ch == '\\':
arg.append(ch)
bs_count += 1
elif ch == '"':
if not bs_count % 2:
# Even number of '\' followed by a '"'. Place one
# '\' for every pair and treat '"' as a delimiter
if bs_count:
del arg[-(bs_count / 2):]
in_quotes = not in_quotes
else:
# Odd number of '\' followed by a '"'. Place one '\'
# for every pair and treat '"' as an escape sequence
# by the remaining '\'
del arg[-(bs_count / 2 + 1):]
arg.append(ch)
bs_count = 0
else:
# regular char
arg.append(ch)
bs_count = 0
# A single trailing '"' delimiter yields an empty arg
if arg or in_quotes:
argv.append(''.join(arg))
return argv
def get_env_opts(envvar):
""" Return a list of the values in the named environment variable,
split according to shell conventions, and decoded to Unicode.
"""
opts = os.environ.get(envvar, "") # bytes at this point
if is_windows:
opts = cmdline2list(opts)
else:
opts = shlex.split(opts)
return decode_list(opts)
def main(sys_args):
# The entire program must work in Unicode
sys_args = decode_list(sys_args)
# sys_args[0] is this script (which we'll replace with 'java' eventually).
# Insert options for the java command from the environment.
sys_args[1:1] = support_java_opts(get_env_opts("JAVA_OPTS"))
# Parse the composite arguments (yes, even the ones from JAVA_OPTS),
# and return the "unparsed" tail considered arguments for Jython itself.
args, jython_args = parse_launcher_args(sys_args)
# Build the data from which we can generate the command ultimately.
# Jython options supplied from the environment stand in front of the
# unparsed tail from the command line.
jython_opts = get_env_opts("JYTHON_OPTS")
jython_command = JythonCommand(args, jython_opts + jython_args)
# This is the "fully adjusted" command to launch, but still as Unicode.
command = jython_command.command
if args.profile and not args.help:
try:
os.unlink("profile.txt")
except OSError:
pass
if args.print_requested and not args.help:
if jython_command.uname == u"windows":
# Add escapes and quotes necessary to Windows.
# Normally used for a byte strings but Python is tolerant :)
command_line = subprocess.list2cmdline(command)
else:
# Just concatenate with spaces
command_line = u" ".join(command)
# It is possible the Unicode cannot be encoded for the console
enc = sys.stdout.encoding or 'ascii'
sys.stdout.write(command_line.encode(enc, 'replace'))
else:
if not (is_windows or not hasattr(os, "execvp") or args.help or
jython_command.uname == u"cygwin"):
# Replace this process with the java process.
#
# NB such replacements actually do not work under Windows,
# but if tried, they also fail very badly by hanging.
# So don't even try!
command = encode_list(command)
os.execvp(command[0], command[1:])
else:
result = 1
try:
result = subprocess.call(encode_list(command))
if args.help:
print_help()
except KeyboardInterrupt:
pass
sys.exit(result)
if __name__ == "__main__":
main(sys.argv)