[Jython-checkins] jython: Many compatibility fixes for launcher (bin/jython, bin/jython.exe)
jim.baker
jython-checkins at python.org
Mon Apr 20 21:22:57 CEST 2015
https://hg.python.org/jython/rev/585cc365d4bd
changeset: 7687:585cc365d4bd
user: Jim Baker <jim.baker at rackspace.com>
date: Mon Apr 20 13:22:51 2015 -0600
summary:
Many compatibility fixes for launcher (bin/jython, bin/jython.exe)
Now supports CLASSPATH, and correctly parses JAVA_OPTS and JYTHON_OPTS
for inclusion into the command line for org.python.util.jython, which
is the actual main program for command line Jython.
Updated installer so it generates a shebang line for the launcher that
is specific to the CPython 2.7 installation that's available. This
change allows for -E to be passed as an option, which does not work if
shebang uses #!/usr/bin/env to select, as it previously did.
More robust testing, including unit tests that was ported from the now
removed test_bat_jy, but now applied to all platforms.
RC2 introduced a bug where jython -classpath CLASSPATH could work;
this has been removed in favor of jython -J-classpath CLASSPATH (old
behavior); this interferes with the processing of the -c option.
Fixes http://bugs.jython.org/issue2311 and http://bugs.jython.org/issue2319
files:
Lib/test/test_bat_jy.py | 411 ----------
Lib/test/test_jython_launcher.py | 260 ++++++
installer/src/java/org/python/util/install/StartScriptGenerator.java | 55 +-
src/shell/jython.exe | Bin
src/shell/jython.py | 322 +++++--
5 files changed, 510 insertions(+), 538 deletions(-)
diff --git a/Lib/test/test_bat_jy.py b/Lib/test/test_bat_jy.py
deleted file mode 100644
--- a/Lib/test/test_bat_jy.py
+++ /dev/null
@@ -1,411 +0,0 @@
-'''Tests jython.bat using the --print option'''
-
-import os
-import sys
-import unittest
-import tempfile
-
-from test import test_support
-
-from java.lang import IllegalThreadStateException
-from java.lang import Runtime
-from java.lang import System
-from java.lang import Thread
-from java.io import File
-from java.io import BufferedReader;
-from java.io import InputStreamReader;
-
-class Monitor(Thread):
- def __init__(self, process):
- self.process = process
- self.output = ''
-
- def run(self):
- reader = BufferedReader(InputStreamReader(self.getStream()))
- try:
- line = reader.readLine()
- while line:
- self.output += line
- line = reader.readLine()
- finally:
- reader.close()
-
- def getOutput(self):
- return self.output
-
-class StdoutMonitor(Monitor):
- def __init_(self, process):
- Monitor.__init__(self, process)
-
- def getStream(self):
- return self.process.getInputStream()
-
-class StderrMonitor(Monitor):
- def __init_(self, process):
- Monitor.__init__(self, process)
-
- def getStream(self):
- return self.process.getErrorStream()
-
-class StarterProcess:
- def writeStarter(self, args, javaHome, jythonHome, jythonOpts, internals=False):
- (starter, starterPath) = tempfile.mkstemp(suffix='.bat', prefix='starter', text=True)
- starter.close()
- outfilePath = starterPath[:-4] + '.out'
- starter = open(starterPath, 'w') # open starter as simple file
- try:
- if javaHome:
- starter.write('set JAVA_HOME=%s\n' % javaHome)
- if jythonHome:
- starter.write('set JYTHON_HOME=%s\n' % jythonHome)
- if jythonOpts:
- starter.write('set JYTHON_OPTS=%s\n' % jythonOpts)
- if internals:
- starter.write('set _JYTHON_OPTS=leaking_internals\n')
- starter.write('set _JYTHON_HOME=c:/leaking/internals\n')
- starter.write(self.buildCommand(args, outfilePath))
- return (starterPath, outfilePath)
- finally:
- starter.close()
-
- def buildCommand(self, args, outfilePath):
- line = ''
- for arg in args:
- line += arg
- line += ' '
- line += '> '
- line += outfilePath
- line += ' 2>&1'
- return line
-
- def getOutput(self, outfilePath):
- lines = ''
- outfile = open(outfilePath, 'r')
- try:
- for line in outfile.readlines():
- lines += line
- finally:
- outfile.close()
- return lines
-
- def isAlive(self, process):
- try:
- process.exitValue()
- return False
- except IllegalThreadStateException:
- return True
-
- def run(self, args, javaHome, jythonHome, jythonOpts, internals=False):
- ''' creates a start script, executes it and captures the output '''
- (starterPath, outfilePath) = self.writeStarter(args, javaHome, jythonHome, jythonOpts, internals)
- try:
- process = Runtime.getRuntime().exec(starterPath)
- stdoutMonitor = StdoutMonitor(process)
- stderrMonitor = StderrMonitor(process)
- stdoutMonitor.start()
- stderrMonitor.start()
- while self.isAlive(process):
- Thread.sleep(300)
- return self.getOutput(outfilePath)
- finally:
- os.remove(starterPath)
- os.remove(outfilePath)
-
-class BaseTest(unittest.TestCase):
- def quote(self, s):
- return '"' + s + '"'
-
- def unquote(self, s):
- if len(s) > 0:
- if s[:1] == '"':
- s = s[1:]
- if len(s) > 0:
- if s[-1:] == '"':
- s = s[:-1]
- return s
-
- def getHomeDir(self):
- ex = sys.executable
- tail = ex[-15:]
- if tail == '\\bin\\jython.bat':
- home = ex[:-15]
- else:
- home = ex[:-11] # \jython.bat
- return home
-
- def assertOutput(self, flags=None, javaHome=None, jythonHome=None, jythonOpts=None, internals=False):
- args = [self.quote(sys.executable), '--print']
- memory = None
- stack = None
- prop = None
- jythonArgs = None
- boot = False
- jdb = False
- if flags:
- for flag in flags:
- if flag[:2] == '-J':
- if flag[2:6] == '-Xmx':
- memory = flag[6:]
- elif flag[2:6] == '-Xss':
- stack = flag[6:]
- elif flag[2:4] == '-D':
- prop = flag[2:]
- elif flag[:2] == '--':
- if flag[2:6] == 'boot':
- boot = True
- elif flag[2:5] == 'jdb':
- jdb = True
- else:
- if jythonArgs:
- jythonArgs += ' '
- jythonArgs += flag
- else:
- jythonArgs = flag
- jythonArgs = jythonArgs.replace('%%', '%') # workaround two .bat files
- args.append(flag)
- process = StarterProcess()
- out = process.run(args, javaHome, jythonHome, jythonOpts, internals)
- self.assertNotEquals('', out)
- homeIdx = out.find('-Dpython.home=')
- java = 'java'
- if javaHome:
- java = self.quote(self.unquote(javaHome) + '\\bin\\java')
- elif jdb:
- java = 'jdb'
- if not memory:
- memory = '512m'
- if not stack:
- stack = '1152k'
- beginning = java + ' '
- if prop:
- beginning += ' ' + prop
- beginning += ' -Xmx' + memory + ' -Xss' + stack + ' '
- self.assertEquals(beginning, out[:homeIdx])
- executableIdx = out.find('-Dpython.executable=')
- homeDir = self.getHomeDir()
- if jythonHome:
- homeDir = self.unquote(jythonHome)
- home = '-Dpython.home=' + self.quote(homeDir) + ' '
- self.assertEquals(home, out[homeIdx:executableIdx])
- if boot:
- classpathFlag = '-Xbootclasspath/a:'
- else:
- classpathFlag = '-classpath'
- classpathIdx = out.find(classpathFlag)
- executable = '-Dpython.executable=' + self.quote(sys.executable) + ' '
- if not boot:
- executable += ' '
- self.assertEquals(executable, out[executableIdx:classpathIdx])
- # ignore full contents of classpath at the moment
- classIdx = out.find('org.python.util.jython')
- self.assertTrue(classIdx > classpathIdx)
- restIdx = classIdx + len('org.python.util.jython')
- rest = out[restIdx:].strip()
- if jythonOpts:
- self.assertEquals(self.quote(jythonOpts), rest)
- else:
- if jythonArgs:
- self.assertEquals(jythonArgs, rest)
- else:
- self.assertEquals('', rest)
-
-class VanillaTest(BaseTest):
- def test_plain(self):
- self.assertOutput()
-
-class JavaHomeTest(BaseTest):
- def test_unquoted(self):
- # for the build bot, try to specify a real java home
- javaHome = System.getProperty('java.home', 'C:\\Program Files\\Java\\someJava')
- self.assertOutput(javaHome=javaHome)
-
- def test_quoted(self):
- self.assertOutput(javaHome=self.quote('C:\\Program Files\\Java\\someJava'))
-
- # this currently fails, meaning we accept only quoted (x86) homes ...
- def __test_x86_unquoted(self):
- self.assertOutput(javaHome='C:\\Program Files (x86)\\Java\\someJava')
-
- def test_x86_quoted(self):
- self.assertOutput(javaHome=self.quote('C:\\Program Files (x86)\\Java\\someJava'))
-
-class JythonHomeTest(BaseTest):
- def createJythonJar(self, parentDir):
- jar = File(parentDir, 'jython.jar')
- if not jar.exists():
- self.assertTrue(jar.createNewFile())
- return jar
-
- def cleanup(self, tmpdir, jar=None):
- if jar and jar.exists():
- self.assertTrue(jar.delete())
- os.rmdir(tmpdir)
-
- def test_unquoted(self):
- jythonHomeDir = tempfile.mkdtemp()
- jar = self.createJythonJar(jythonHomeDir)
- self.assertOutput(jythonHome=jythonHomeDir)
- self.cleanup(jythonHomeDir, jar)
-
- def test_quoted(self):
- jythonHomeDir = tempfile.mkdtemp()
- jar = self.createJythonJar(jythonHomeDir)
- self.assertOutput(jythonHome=self.quote(jythonHomeDir))
- self.cleanup(jythonHomeDir, jar)
-
-class JythonOptsTest(BaseTest):
- def test_single(self):
- self.assertOutput(jythonOpts='myOpt')
-
- def test_multiple(self):
- self.assertOutput(jythonOpts='some arbitrary options')
-
-class InternalsTest(BaseTest):
- def test_no_leaks(self):
- self.assertOutput(internals=True)
-
-class JavaOptsTest(BaseTest):
- def test_memory(self):
- self.assertOutput(['-J-Xmx321m'])
-
- def test_stack(self):
- self.assertOutput(['-J-Xss321k'])
-
- def test_property(self):
- self.assertOutput(['-J-DmyProperty=myValue'])
-
- def test_property_singlequote(self):
- self.assertOutput(["-J-DmyProperty='myValue'"])
-
- # a space inside value does not work in jython.bat
- def __test_property_singlequote_space(self):
- self.assertOutput(["-J-DmyProperty='my Value'"])
-
- def test_property_doublequote(self):
- self.assertOutput(['-J-DmyProperty="myValue"'])
-
- # a space inside value does not work in jython.bat
- def __test_property_doublequote_space(self):
- self.assertOutput(['-J-DmyProperty="my Value"'])
-
- def test_property_underscore(self):
- self.assertOutput(['-J-Dmy_Property=my_Value'])
-
-class ArgsTest(BaseTest):
- def test_file(self):
- self.assertOutput(['test.py'])
-
- def test_dash(self):
- self.assertOutput(['-i'])
-
- def test_combined(self):
- self.assertOutput(['-W', 'action', 'line'])
-
- def test_singlequoted(self):
- self.assertOutput(['-c', "'import sys;'"])
-
- def test_doublequoted(self):
- self.assertOutput(['-c', '"print \'something\'"'])
-
- def test_nestedquotes(self):
- self.assertOutput(['-c', '"print \'something \"really\" cool\'"'])
-
- def test_nestedquotes2(self):
- self.assertOutput(['-c', "'print \"something \'really\' cool\"'"])
-
- def test_underscored(self):
- self.assertOutput(['-jar', 'my_stuff.jar'])
-
- def test_property(self):
- self.assertOutput(['-DmyProperty=myValue'])
-
- def test_property_underscored(self):
- self.assertOutput(['-DmyProperty=my_Value'])
-
- def test_property_singlequoted(self):
- self.assertOutput(["-DmyProperty='my_Value'"])
-
- def test_property_doublequoted(self):
- self.assertOutput(['-DmyProperty="my_Value"'])
-
-class DoubleDashTest(BaseTest):
- def test_boot(self):
- self.assertOutput(['--boot'])
-
- def test_jdb(self):
- self.assertOutput(['--jdb'])
-
-class GlobPatternTest(BaseTest):
- def test_star_nonexisting(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '*.nonexisting', '*.nonexisting'])
-
- def test_star_nonexisting_doublequoted(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '"*.nonexisting"', '"*.nonexisting"'])
-
- def test_star_nonexistingfile_singlequoted(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', "'*.nonexisting'", "'*.nonexisting'"])
-
- def test_star_existing(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '*.bat', '*.bat'])
-
- def test_star_existing_doublequoted(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '"*.bat"', '"*.bat"'])
-
- def test_star_existing_singlequoted(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', "'*.bat'", "'*.bat'"])
-
-class ArgsSpacesTest(BaseTest):
- def test_doublequoted(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '"part1 part2"', '2nd'])
-
- def test_singlequoted(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', "'part1 part2'", '2nd'])
-
- # this test currently fails
- def __test_unbalanced_doublequote(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'Scarlet O"Hara', '2nd'])
-
- def test_unbalanced_singlequote(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', "Scarlet O'Hara", '2nd'])
-
-class ArgsSpecialCharsTest(BaseTest):
- # exclamation marks are still very special ...
- def __test_exclamationmark(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'foo!', 'ba!r', '!baz', '!'])
-
- # because we go through a starter.bat file, we have to simulate % with %%
- def test_percentsign(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'foo%%1', '%%1bar', '%%1', '%%'])
-
- def test_colon(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'foo:', ':bar'])
-
- # a semicolon at the beginning of an arg currently fails (e.g. ;bar)
- def test_semicolon(self):
- self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'foo;'])
-
-class DummyTest(unittest.TestCase):
- def test_nothing(self):
- pass
-
-def test_main():
- if os._name == 'nt':
- test_support.run_unittest(VanillaTest,
- JavaHomeTest,
- JythonHomeTest,
- JythonOptsTest,
- InternalsTest,
- JavaOptsTest,
- ArgsTest,
- DoubleDashTest,
- GlobPatternTest,
- ArgsSpacesTest,
- ArgsSpecialCharsTest)
- else:
- # provide at least one test for the other platforms - happier build bots
- test_support.run_unittest(DummyTest)
-
-
-if __name__ == '__main__':
- test_main()
-
diff --git a/Lib/test/test_jython_launcher.py b/Lib/test/test_jython_launcher.py
new file mode 100644
--- /dev/null
+++ b/Lib/test/test_jython_launcher.py
@@ -0,0 +1,260 @@
+# Cross-platform testing of the Jython launcher (bin/jython or bin/jython.exe) using --print
+# Replaces test_bat_jy, with some test cases directly used with minor adaptation from that test
+
+import os
+import pprint
+import shlex
+import subprocess
+import sys
+import unittest
+from collections import OrderedDict
+from test import test_support
+
+launcher = None
+uname = None
+is_windows = False
+some_jar = os.path.join(os.sep, "a", "b", "c", "some.jar")
+
+
+def get_launcher(executable):
+ # accounts for continued presence of jython bash script
+ # when not installed with the installer or if CPython 2.7
+ # is not available
+ if os._name == "nt":
+ return executable
+ exec_path = os.path.dirname(sys.executable)
+ jython_py = os.path.join(exec_path, "jython.py")
+ if os.path.exists(jython_py):
+ return jython_py
+ else:
+ # presumably jython.py has been renamed to jython, generally
+ # by the installer
+ return executable
+
+
+def get_uname():
+ _uname = None
+ try:
+ _uname = subprocess.check_output(["uname"]).strip().lower()
+ if _uname.startswith("cygwin"):
+ _uname = "cygwin"
+ except OSError:
+ if os._name == "nt":
+ _uname = "windows"
+ return _uname
+
+
+def classpath_delimiter():
+ return ";" if (os._name == "nt" or uname == "cygwin") else ":"
+
+
+class TestLauncher(unittest.TestCase):
+
+ def get_cmdline(self, cmd, env):
+
+ output = subprocess.check_output(cmd, env=env).rstrip()
+ if is_windows:
+ return subprocess._cmdline2list(output)
+ else:
+ return shlex.split(output)
+
+ def get_newenv(self, env=os.environ):
+ newenv = env.copy()
+ for var in ("CLASSPATH",
+ "JAVA_MEM", "JAVA_HOME", "JAVA_OPTS", "JAVA_STACK",
+ "JYTHON_HOME", "JYTHON_OPTS"):
+ try:
+ del newenv[var]
+ except KeyError:
+ pass
+ return newenv
+
+ def get_properties(self, args):
+ props = OrderedDict()
+ for arg in args:
+ if arg.startswith("-D"):
+ k, v = arg[2:].split("=")
+ props[k] = v
+ return props
+
+ def test_classpath_env(self):
+ env = self.get_newenv()
+ env["CLASSPATH"] = some_jar
+ args = self.get_cmdline([launcher, "--print"], env=env)
+ it = iter(args)
+ while it:
+ arg = next(it)
+ if arg == "-classpath":
+ self.assertEqual(next(it).split(classpath_delimiter())[-1], some_jar)
+ break
+
+ def test_classpath(self):
+ env = self.get_newenv()
+ args = self.get_cmdline([launcher, "--print", "-J-cp", some_jar], env=env)
+ it = iter(args)
+ while it:
+ arg = next(it)
+ if arg == "-classpath":
+ self.assertEqual(next(it).split(classpath_delimiter())[-1], some_jar)
+ break
+
+ def test_java_home(self):
+ env = self.get_newenv()
+ my_java = os.path.join(os.sep, "foo", "bar", "my very own (x86) java")
+ env["JAVA_HOME"] = my_java
+ args = self.get_cmdline([launcher, "--print"], env)
+ self.assertEqual(args[0], os.path.join(my_java, "bin", "java"))
+ self.assertEqual(args[1], "-Xmx512m")
+ self.assertEqual(args[2], "-Xss1024k")
+ self.assertEqual(args[-1], "org.python.util.jython")
+
+ def test_java_opts(self):
+ env = self.get_newenv()
+ env["JAVA_OPTS"] = '-Dfoo=bar -Dbaz="some property" -Xmx2g -classpath %s' % some_jar
+ args = self.get_cmdline([launcher, "--print"], env)
+ props = self.get_properties(args)
+ self.assertEqual(args[0], "java")
+ self.assertEqual(args[1], "-Xmx2g")
+ self.assertEqual(args[2], "-Xss1024k")
+ self.assertEqual(args[3], "-classpath", args)
+ self.assertEqual(args[4].split(classpath_delimiter())[-1], some_jar)
+ self.assertEqual(args[-1], "org.python.util.jython")
+ self.assertEqual(props["foo"], "bar")
+ self.assertEqual(props["baz"], "some property")
+
+ def test_default_options(self):
+ env = self.get_newenv()
+ args = self.get_cmdline([launcher, "--print"], env)
+ props = self.get_properties(args)
+ self.assertEqual(args[0], "java")
+ self.assertEqual(args[1], "-Xmx512m")
+ self.assertEqual(args[2], "-Xss1024k")
+ self.assertEqual(args[-1], "org.python.util.jython")
+ self.assertIn("python.home", props)
+ self.assertIn("python.executable", props)
+ self.assertIn("python.launcher.uname", props)
+ self.assertIn("python.launcher.tty", props)
+
+ def test_mem_env(self):
+ env = self.get_newenv()
+ env["JAVA_MEM"] = "-Xmx4g"
+ env["JAVA_STACK"] = "-Xss2m"
+ args = self.get_cmdline([launcher, "--print"], env)
+ self.assertEqual(args[0], "java")
+ self.assertEqual(args[1], "-Xmx4g")
+ self.assertEqual(args[2], "-Xss2m")
+ self.assertEqual(args[-1], "org.python.util.jython")
+
+ def test_mem_options(self):
+ env = self.get_newenv()
+ args = self.get_cmdline([launcher, "-J-Xss2m", "-J-Xmx4g", "--print"], env)
+ self.assertEqual(args[0], "java")
+ self.assertEqual(args[1], "-Xmx4g", args)
+ self.assertEqual(args[2], "-Xss2m", args)
+ self.assertEqual(args[-1], "org.python.util.jython")
+
+ def test_jython_opts_env(self):
+ env = self.get_newenv()
+ env["JYTHON_OPTS"] = '-c "print 47"'
+ args = self.get_cmdline([launcher, "--print"], env)
+ self.assertEqual(args[0], "java")
+ self.assertEqual(args[1], "-Xmx512m")
+ self.assertEqual(args[2], "-Xss1024k")
+ self.assertEqual(args[-3], "org.python.util.jython")
+ self.assertEqual(args[-2], "-c")
+ self.assertEqual(args[-1], "print 47")
+
+ def test_options(self):
+ env = self.get_newenv()
+ args = self.get_cmdline(
+ [launcher,
+ "-Dquoted=a \"quoted\" option",
+ "-Dunder_score=with_underscores",
+ "-Dstarred=*/*/more/*/*",
+ "--print"], env)
+ props = self.get_properties(args)
+ self.assertEqual(props["quoted"], 'a "quoted" option')
+ self.assertEqual(props["under_score"], "with_underscores")
+ self.assertEqual(props["starred"], "*/*/more/*/*")
+
+ def assertHelp(self, output):
+ self.assertIn(
+ "usage: jython [option] ... [-c cmd | -m mod | file | -] [arg] ...",
+ output)
+
+ def test_help(self):
+ self.assertHelp(subprocess.check_output([launcher, "--help"], stderr=subprocess.STDOUT))
+ self.assertHelp(subprocess.check_output([launcher, "--print", "--help"], stderr=subprocess.STDOUT))
+ self.assertHelp(subprocess.check_output([launcher, "--help", "--jdb"], stderr=subprocess.STDOUT))
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ subprocess.check_output([launcher, "--bad-arg"], stderr=subprocess.STDOUT)
+ self.assertHelp(cm.exception.output)
+
+ def test_remaining_args(self):
+ env = self.get_newenv()
+ args = self.get_cmdline([launcher, "--print", "--", "--help"], env)
+ self.assertEqual(args[-2], "org.python.util.jython")
+ self.assertEqual(args[-1], "--help")
+
+ args = self.get_cmdline([launcher, "--print", "yolk", "--help"], env)
+ self.assertEqual(args[-3], "org.python.util.jython")
+ self.assertEqual(args[-2], "yolk")
+ self.assertEqual(args[-1], "--help")
+
+ def assertCommand(self, command):
+ args = self.get_cmdline([launcher, "--print"] + command, self.get_newenv())
+ self.assertEqual(args[(len(args) - len(command)):], command)
+
+ def test_file(self):
+ self.assertCommand(['test.py'])
+
+ def test_dash(self):
+ self.assertCommand(['-i'])
+
+ def test_combined(self):
+ self.assertCommand(['-W', 'action', 'line'])
+
+ def test_singlequoted(self):
+ self.assertCommand(['-c', "'import sys;'"])
+
+ def test_doublequoted(self):
+ self.assertCommand(['-c', '"print \'something\'"'])
+
+ def test_nestedquotes(self):
+ self.assertCommand(['-c', '"print \'something \"really\" cool\'"'])
+
+ def test_nestedquotes2(self):
+ self.assertCommand(['-c', "'print \"something \'really\' cool\"'"])
+
+ def test_starred_args(self):
+ self.assertCommand(["my python command.py", "*/*/my ARGS/*.txt"])
+
+ def test_exclamationmark(self):
+ self.assertCommand(['-c', 'import sys; print sys.argv[1:]', 'foo!', 'ba!r', '!baz', '!', '!!'])
+
+ def test_percentsign(self):
+ self.assertCommand(['-c', 'import sys; print sys.argv[1:]', 'foo%1', 'foo%%1', '%%1bar', '%%1', '%1', '%', '%%'])
+
+ def test_colon(self):
+ self.assertCommand(['-c', 'import sys; print sys.argv[1:]', 'foo:', ':bar'])
+
+ def test_semicolon(self):
+ self.assertCommand(['-c', ';import sys; print sys.argv[1:]', 'foo;'])
+
+
+def test_main():
+ global is_windows
+ global launcher
+ global uname
+
+ if sys.executable is None:
+ return
+ launcher = get_launcher(sys.executable)
+ uname = get_uname()
+ is_windows = uname in ("cygwin", "windows")
+ test_support.run_unittest(
+ TestLauncher)
+
+
+if __name__ == "__main__":
+ test_main()
diff --git a/installer/src/java/org/python/util/install/StartScriptGenerator.java b/installer/src/java/org/python/util/install/StartScriptGenerator.java
--- a/installer/src/java/org/python/util/install/StartScriptGenerator.java
+++ b/installer/src/java/org/python/util/install/StartScriptGenerator.java
@@ -1,10 +1,13 @@
package org.python.util.install;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermissions;
@@ -16,24 +19,51 @@
_targetDirectory = targetDirectory;
}
- protected boolean hasCPython27() {
- int errorCode = 0;
+ protected String getShebang() {
+ String shebang = null;
try {
String command[] = new String[]{
"/usr/bin/env", "python2.7", "-E",
"-c",
"import sys; " +
"assert sys.version_info.major == 2 and sys.version_info.minor == 7, " +
- "'Need Python 2.7, got %r' % (sys.version_info,)"};
+ "'Need Python 2.7, got %r' % (sys.version_info,);" +
+ "print sys.executable"};
long timeout = 3000;
ChildProcess childProcess = new ChildProcess(command, timeout);
childProcess.setDebug(false);
childProcess.setSilent(true);
- errorCode = childProcess.run();
+ int errorCode = childProcess.run();
+ if (errorCode == 0) {
+ // The whole point of this exercise is that we do not
+ // want the launcher to interpret or otherwise intercept
+ // any PYTHON environment variables that are being passed through.
+ // However, a shebang like /usr/bin/env python2.7 -E
+ // with an extra argument (-E) in general does not work,
+ // such as on Linux, so we have to replace with a hard-coded
+ // path
+ shebang = "#!" + childProcess.getStdout().get(0) + " -E";
+ }
} catch (Throwable t) {
- errorCode = 1;
}
- return errorCode == 0;
+ return shebang;
+ }
+
+ private final void generateLauncher(String shebang, File infile, File outfile)
+ throws IOException {
+ try (
+ BufferedReader br = new BufferedReader(new FileReader(infile));
+ BufferedWriter bw = new BufferedWriter(new FileWriter(outfile))) {
+ int i = 0;
+ for (String line; (line = br.readLine()) != null; i += 1) {
+ if (i == 0) {
+ bw.write(shebang);
+ } else {
+ bw.write(line);
+ }
+ bw.newLine();
+ }
+ }
}
protected final void generateStartScripts() throws IOException {
@@ -43,12 +73,13 @@
Files.delete(bindir.resolve("jython.py"));
}
else {
- if (hasCPython27()) {
- Files.move(bindir.resolve("jython.py"), bindir.resolve("jython"),
- StandardCopyOption.REPLACE_EXISTING);
- } else {
- Files.delete(bindir.resolve("jython.py"));
+ String shebang = getShebang();
+ if (shebang != null) {
+ generateLauncher(shebang,
+ bindir.resolve("jython.py").toFile(),
+ bindir.resolve("jython").toFile());
}
+ Files.delete(bindir.resolve("jython.py"));
Files.delete(bindir.resolve("jython.exe"));
Files.delete(bindir.resolve("python27.dll"));
Files.setPosixFilePermissions(bindir.resolve("jython"),
diff --git a/src/shell/jython.exe b/src/shell/jython.exe
index 61c68b6657c774bb7d40bcbb3d263411490695eb..2a03827dd99c8d2d908074a2e3de15abf56e30d2
GIT binary patch
[stripped]
diff --git a/src/shell/jython.py b/src/shell/jython.py
--- a/src/shell/jython.py
+++ b/src/shell/jython.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2.7
+#!/usr/bin/env python2.7 -E
# -*- coding: utf-8 -*-
# Launch script for Jython. It may be wrapped as an executable with
@@ -7,12 +7,12 @@
# bin/jython if CPython 2.7 is available with the above shebang
# invocation.
-import argparse
import glob
import inspect
import os
import os.path
import pipes
+import shlex
import subprocess
import sys
from collections import OrderedDict
@@ -21,64 +21,69 @@
is_windows = os.name == "nt" or (os.name == "java" and os._name == "nt")
-def make_parser(provided_args):
- parser = argparse.ArgumentParser(description="Jython", add_help=False)
- parser.add_argument("-D", dest="properties", action="append")
- parser.add_argument("-J", dest="java", action="append")
- parser.add_argument("--boot", action="store_true")
- parser.add_argument("--jdb", action="store_true")
- parser.add_argument("--help", "-h", action="store_true")
- parser.add_argument("--print", dest="print_requested", action="store_true")
- parser.add_argument("--profile", action="store_true")
- args, remainder = parser.parse_known_args(provided_args)
+def parse_launcher_args(args):
+ class Namespace(object):
+ pass
+ parsed = Namespace()
+ parsed.java = []
+ parsed.properties = OrderedDict()
+ parsed.boot = False
+ parsed.jdb = False
+ parsed.help = False
+ parsed.print_requested = False
+ parsed.profile = False
+ parsed.jdb = None
- items = args.java or []
- args.java = []
- for item in items:
- if item.startswith("-Xmx"):
- args.mem = item
- elif item.startswith("-Xss"):
- args.stack = item
- else:
- args.java.append(item)
-
- # need to account for the fact that -c and -cp/-classpath are ambiguous options as far
- # as argparse is concerned, so parse separately
- args.classpath = []
- r = iter(remainder)
- r2 = []
+ it = iter(args)
+ next(it) # ignore sys.argv[0]
+ i = 1
while True:
try:
- arg = next(r)
+ arg = next(it)
except StopIteration:
break
- if arg == "-cp" or arg == "-classpath":
+ if arg.startswith("-D"):
+ k, v = arg[2:].split("=")
+ parsed.properties[k] = v
+ i += 1
+ elif arg in ("-J-classpath", "-J-cp"):
try:
- args.classpath = next(r)
- if args.classpath.startswith("-"):
- parser.error("Invalid classpath for -classpath: %s" % repr(args.classpath)[1:])
+ next_arg = next(it)
except StopIteration:
- parser.error("-classpath requires an argument")
+ 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("-J-Xmx"):
+ parsed.mem = arg[2:]
+ i += 1
+ elif arg.startswith("-J-Xss"):
+ parsed.stack = arg[2:]
+ i += 1
+ elif arg.startswith("-J"):
+ parsed.java.append(arg[2:])
+ i += 1
+ elif arg == "--print":
+ parsed.print_requested = True
+ i += 1
+ elif arg in ("-h", "--help"):
+ parsed.help = True
+ elif arg in ("--boot", "--jdb", "--profile"):
+ setattr(parsed, arg[2:], True)
+ i += 1
+ elif arg == "--":
+ i += 1
+ break
else:
- r2.append(arg)
- remainder = r2
+ break
- if args.properties is None:
- args.properties = []
- props = OrderedDict()
- for kv in args.properties:
- k, v = kv.split("=")
- props[k] = v
- args.properties = props
- args.encoding = args.properties.get("file.encoding", None)
-
- return parser, args
+ return parsed, args[i:]
class JythonCommand(object):
- def __init__(self, parser, args, jython_args):
- self.parser = parser
+ def __init__(self, args, jython_args):
self.args = args
self.jython_args = jython_args
@@ -109,13 +114,16 @@
return self._java_command
def setup_java_command(self):
+ if self.args.help:
+ self._java_home = None
+ self._java_command = "java"
+ return
+
if "JAVA_HOME" not in os.environ:
self._java_home = None
self._java_command = "jdb" if self.args.jdb else "java"
else:
self._java_home = os.environ["JAVA_HOME"]
- #if self.uname == "cygwin":
- # self._java_home = subprocess.check_output(["cygpath", "--windows", self._java_home]).strip()
if self.uname == "cygwin":
self._java_command = "jdb" if self.args.jdb else "java"
else:
@@ -159,51 +167,52 @@
return ";" if (is_windows or self.uname == "cygwin") else ":"
@property
- def classpath(self):
- if hasattr(self, "_classpath"):
- return self._classpath
+ 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")]
- jars.append(os.path.join(self.jython_home, "javalib", "*"))
+ 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")):
- self.parser.error(
-"""{executable}:
-{jython_home} contains neither jython-dev.jar nor jython.jar.
+ bad_option("""{jython_home} contains neither jython-dev.jar nor jython.jar.
Try running this script from the 'bin' directory of an installed Jython or
-setting {envvar_specifier}JYTHON_HOME.""".\
- format(
- executable=self.executable,
+setting {envvar_specifier}JYTHON_HOME.""".format(
jython_home=self.jython_home,
envvar_specifier="%" if self.uname == "windows" else "$"))
else:
jars = [os.path.join(self.jython_home, "jython.jar")]
- self._classpath = self.classpath_delimiter.join(jars)
- if self.args.classpath and not self.args.boot:
- self._classpath += self.classpath_delimiter + self.args.classpath
- return self._classpath
+ self._jython_jars = jars
+ return self._jython_jars
+
+ @property
+ def java_classpath(self):
+ if hasattr(self.args, "classpath"):
+ return self.args.classpath
+ else:
+ return os.environ.get("CLASSPATH", ".")
@property
def java_mem(self):
- if hasattr(self.args.java, "mem"):
- return self.args.java.mem
+ if hasattr(self.args, "mem"):
+ return self.args.mem
else:
return os.environ.get("JAVA_MEM", "-Xmx512m")
@property
def java_stack(self):
- if hasattr(self.args.java, "stack"):
- return self.args.java.mem
+ if hasattr(self.args, "stack"):
+ return self.args.stack
else:
return os.environ.get("JAVA_STACK", "-Xss1024k")
@property
def java_opts(self):
- if "JAVA_OPTS" in os.environ:
- options = os.environ["JAVA_OPTS"].split()
- else:
- options = []
- options.extend([self.java_mem, self.java_stack])
- return options
+ return [self.java_mem, self.java_stack]
@property
def java_profile_agent(self):
@@ -219,6 +228,9 @@
else:
return arg
+ def make_classpath(self, jars):
+ return self.classpath_delimiter.join(jars)
+
def convert_path(self, arg):
if self.uname == "cygwin":
if not arg.startswith("/cygdrive/"):
@@ -235,20 +247,25 @@
args = [self.java_command]
args.extend(self.java_opts)
args.extend(self.args.java)
+
+ classpath = self.java_classpath
+ jython_jars = self.jython_jars
if self.args.boot:
- args.append("-Xbootclasspath/a:%s" % self.convert_path(self.classpath))
- if self.args.classpath:
- args.extend(["-classpath", self.convert_path(self.args.classpath)])
+ args.append("-Xbootclasspath/a:%s" % self.convert_path(self.make_classpath(jython_jars)))
else:
- args.extend(["-classpath", self.convert_path(self.classpath)])
+ classpath = self.make_classpath(jython_jars) + self.classpath_delimiter + classpath
+ args.extend(["-classpath", self.convert_path(classpath)])
+
if "python.home" not in self.args.properties:
args.append("-Dpython.home=%s" % self.convert_path(self.jython_home))
if "python.executable" not in self.args.properties:
args.append("-Dpython.executable=%s" % self.convert_path(self.executable))
if "python.launcher.uname" not in self.args.properties:
args.append("-Dpython.launcher.uname=%s" % self.uname)
- # determine if is-a-tty for the benefit of running on cygwin - mintty doesn't behave like
- # a standard windows tty and so JNR posix doesn't detect it properly
+ # Determines 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("-Dpython.launcher.tty=%s" % str(os.isatty(sys.stdin.fileno())).lower())
if self.uname == "cygwin" and "python.console" not in self.args.properties:
@@ -265,66 +282,141 @@
return args
+def bad_option(msg):
+ print >> sys.stderr, """
+{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 options:
+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)
---jdb : run under JDB
---print : print the Java command instead of executing it
+--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)
---boot : put jython on the boot classpath (disables the bytecode verifier)
-- : pass remaining arguments through to Jython
Jython launcher environment variables:
+JAVA_MEM : Java memory (sets via -Xmx)
+JAVA_OPTS : options to pass directly to Java
+JAVA_STACK : Java stack size (sets via -Xss)
JAVA_HOME : Java installation directory
JYTHON_HOME: Jython installation directory
JYTHON_OPTS: default command line arguments
"""
+def support_java_opts(args):
+ it = iter(args)
+ while it:
+ arg = next(it)
+ if arg.startswith("-D"):
+ yield arg
+ elif arg in ("-classpath", "-cp"):
+ yield "-J" + arg
+ try:
+ yield next(it)
+ except StopIteration:
+ bad_option("Argument expected for -classpath option in JAVA_OPTS")
+ else:
+ yield "-J" + arg
-def split_launcher_args(args):
- it = iter(args)
- i = 1
- next(it)
- while True:
- try:
- arg = next(it)
- except StopIteration:
- break
- if arg.startswith("-D") or arg.startswith("-J") or \
- arg in ("--boot", "--jdb", "--help", "--print", "--profile"):
- i += 1
- elif arg in ("-cp", "-classpath"):
- i += 1
- try:
- next(it)
- i += 1
- except StopIteration:
- break # will be picked up in argparse, where an error will be raised
- elif arg == "--":
- i += 1
- break
+
+# 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:
- break
- return args[:i], args[i:]
+ # 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))
-def main():
+ return argv
+
+
+def decode_args(sys_args):
+ args = [sys_args[0]]
+
+ def get_env_opts(envvar):
+ opts = os.environ.get(envvar, "")
+ if is_windows:
+ return cmdline2list(opts)
+ else:
+ return shlex.split(opts)
+
+ java_opts = get_env_opts("JAVA_OPTS")
+ jython_opts = get_env_opts("JYTHON_OPTS")
+
+ args.extend(support_java_opts(java_opts))
+ args.extend(sys_args[1:])
+
if sys.stdout.encoding:
if sys.stdout.encoding.lower() == "cp65001":
sys.exit("""Jython does not support code page 65001 (CP_UTF8).
Please try another code page by setting it with the chcp command.""")
- sys.argv = [arg.decode(sys.stdout.encoding) for arg in sys.argv]
- launcher_args, jython_args = split_launcher_args(sys.argv)
- parser, args = make_parser(launcher_args)
- jython_command = JythonCommand(parser, args, jython_args)
+ args = [arg.decode(sys.stdout.encoding) for arg in args]
+ jython_opts = [arg.decode(sys.stdout.encoding) for arg in jython_opts]
+
+ return args, jython_opts
+
+
+def main(sys_args):
+ sys_args, jython_opts = decode_args(sys_args)
+ args, jython_args = parse_launcher_args(sys_args)
+ jython_command = JythonCommand(args, jython_opts + jython_args)
command = jython_command.command
- if args.profile:
+ if args.profile and not args.help:
try:
os.unlink("profile.txt")
except OSError:
pass
- if args.print_requested:
+ if args.print_requested and not args.help:
if jython_command.uname == "windows":
print subprocess.list2cmdline(jython_command.command)
else:
@@ -349,4 +441,4 @@
if __name__ == "__main__":
- main()
+ main(sys.argv)
--
Repository URL: https://hg.python.org/jython
More information about the Jython-checkins
mailing list