Python 3 support (#3)

* Make compiler and runtime able to run under python3.

* Read python interpreter path from Bazel

* Add integration tests for Python 3 support

* Remove unused shebang lines

* Formatting fix: Use single quotes consistently

* Update README with Python 3 support
diff --git a/README.md b/README.md
index 73d3f74..e5448e0 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,6 @@
 
 * C extension modules in 'deps' is not yet supported
 * Automatic re-extraction of '.runfiles' is not yet supported
-* Python 3 is not yet supported
 * Does not include a copy of the Python interpreter ('hermetic .par')
 
 ## Example
diff --git a/compiler/BUILD b/compiler/BUILD
index 704d583..bbc0f8d 100644
--- a/compiler/BUILD
+++ b/compiler/BUILD
@@ -15,23 +15,40 @@
     data = [
         "//runtime:support.py",
     ],
+    srcs_version = "PY2AND3",
 )
 
 py_library(
     name = "test_utils",
     srcs = ["test_utils.py"],
+    srcs_version = "PY2AND3",
 )
 
 py_binary(
     name = "compiler",
     srcs = ["compiler.py"],
+    default_python_version = "PY2",
+    main = "compiler.py",
+    srcs_version = "PY2AND3",
+    deps = [":compiler_lib"],
+)
+
+py_binary(
+    name = "compiler3",
+    srcs = ["compiler.py"],
+    default_python_version = "PY3",
+    main = "compiler.py",
+    srcs_version = "PY2AND3",
     deps = [":compiler_lib"],
 )
 
 [py_test(
-    name = src_name + "_test",
+    name = "%s_%s_test" % (src_name, version),
     size = "small",
     srcs = [src_name + "_test.py"],
+    default_python_version = version,
+    main = src_name + "_test.py",
+    srcs_version = "PY2AND3",
     deps = [
         ":compiler_lib",
         ":test_utils",
@@ -41,4 +58,7 @@
     "manifest_parser",
     "python_archive",
     "stored_resource",
+] for version in [
+    "PY2",
+    "PY3",
 ]]
diff --git a/compiler/cli.py b/compiler/cli.py
index f29f9a2..be6a347 100644
--- a/compiler/cli.py
+++ b/compiler/cli.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,6 +15,7 @@
 """Command line interface to subpar compiler"""
 
 import argparse
+import io
 import os
 import re
 
@@ -34,14 +33,6 @@
         help='Python source file to use as main entry point')
 
     parser.add_argument(
-        '--imports_from_stub',
-        help='Read imports attribute from the specified stub file',
-        required=True)
-    parser.add_argument(
-        '--interpreter',
-        help='Filename of Python executable to invoke archive with.',
-        default='/usr/bin/python2')
-    parser.add_argument(
         '--manifest_file',
         help='File listing all files to be included in this parfile. This is ' +
         'typically generated by bazel in a target\'s .runfiles_manifest file.',
@@ -54,26 +45,51 @@
         '--outputpar',
         help='Filename of generated par file.',
         required=True)
+    parser.add_argument(
+        '--stub_file',
+        help='Read imports and interpreter path from the specified stub file',
+        required=True)
     return parser
 
 
-def parse_imports_from_stub(stub_filename):
-    """Parse the imports attribute from a py_binary() stub.
+def parse_stub(stub_filename):
+    """Parse the imports and interpreter path from a py_binary() stub.
+
+    We assume the stub is utf-8 encoded.
 
     TODO(b/29227737): Remove this once we can access imports from skylark.
 
-    Returns a list of relative paths
+    Returns (list of relative paths, path to Python interpreter)
     """
-    regex = re.compile(r'''^  python_imports = '([^']*)'$''')
-    with open(stub_filename, 'rb') as stub_file:
+    imports_regex = re.compile(r'''^  python_imports = '([^']*)'$''')
+    interpreter_regex = re.compile(r'''^PYTHON_BINARY = '([^']*)'$''')
+    import_roots = None
+    interpreter = None
+    with io.open(stub_filename, 'rt', encoding='utf8') as stub_file:
         for line in stub_file:
-            match = regex.match(line)
-            if match:
-                import_roots = match.group(1).split(':')
+            importers_match = imports_regex.match(line)
+            if importers_match:
+                import_roots = importers_match.group(1).split(':')
                 # Filter out empty paths
                 import_roots = [x for x in import_roots if x]
-                return import_roots
-    raise error.Error('Failed to parse stub file [%s]' % stub_filename)
+            interpreter_match = interpreter_regex.match(line)
+            if interpreter_match:
+                interpreter = interpreter_match.group(1)
+    if import_roots is None or not interpreter:
+        raise error.Error('Failed to parse stub file [%s]' % stub_filename)
+
+    # Match the search logic in stub_template.txt
+    if interpreter.startswith('//'):
+        raise error.Error('Python interpreter must not be a label [%s]' %
+                          stub_filename)
+    elif interpreter.startswith('/'):
+        pass
+    elif '/' in interpreter:
+        pass
+    else:
+        interpreter = '/usr/bin/env %s' % interpreter
+
+    return (import_roots, interpreter)
 
 
 def main(argv):
@@ -81,13 +97,13 @@
     parser = make_command_line_parser()
     args = parser.parse_args(argv[1:])
 
-    # Read import roots
-    import_roots = parse_imports_from_stub(args.imports_from_stub)
+    # Parse information from stub file that's too hard to compute in Skylark
+    import_roots, interpreter = parse_stub(args.stub_file)
 
     par = python_archive.PythonArchive(
         main_filename=args.main_filename,
         import_roots=import_roots,
-        interpreter=args.interpreter,
+        interpreter=interpreter,
         output_filename=args.outputpar,
         manifest_filename=args.manifest_file,
         manifest_root=args.manifest_root,
diff --git a/compiler/cli_test.py b/compiler/cli_test.py
index d11e107..95a937b 100644
--- a/compiler/cli_test.py
+++ b/compiler/cli_test.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,37 +24,69 @@
     def test_make_command_line_parser(self):
         parser = cli.make_command_line_parser()
         args = parser.parse_args([
-            '--imports_from_stub=quux',
             '--manifest_file=bar',
             '--manifest_root=bazz',
             '--outputpar=baz',
+            '--stub_file=quux',
             'foo',
         ])
         self.assertEqual(args.manifest_file, 'bar')
 
-    def test_parse_imports_from_stub(self):
+    def test_stub(self):
         valid_cases = [
-            ["  python_imports = ''",
-             []],
-            ["  python_imports = 'myworkspace/spam/eggs'",
-             ['myworkspace/spam/eggs']],
-            ["  python_imports = 'myworkspace/spam/eggs:otherworkspace'",
-             ['myworkspace/spam/eggs', 'otherworkspace']],
+            # Empty list
+            [b"""
+  python_imports = ''
+PYTHON_BINARY = '/usr/bin/python'
+""",
+             ([], '/usr/bin/python')],
+            # Single import
+            [b"""
+  python_imports = 'myworkspace/spam/eggs'
+PYTHON_BINARY = '/usr/bin/python'
+""",
+             (['myworkspace/spam/eggs'], '/usr/bin/python')],
+            # Multiple imports
+            [b"""
+  python_imports = 'myworkspace/spam/eggs:otherworkspace'
+PYTHON_BINARY = '/usr/bin/python'
+""",
+             (['myworkspace/spam/eggs', 'otherworkspace'], '/usr/bin/python')],
+            # Relative path to interpreter
+            [b"""
+  python_imports = ''
+PYTHON_BINARY = 'mydir/python'
+""",
+             ([], 'mydir/python')],
+            # Search for interpreter on $PATH
+            [b"""
+  python_imports = ''
+PYTHON_BINARY = 'python'
+""",
+             ([], '/usr/bin/env python')],
         ]
         for content, expected in valid_cases:
             with test_utils.temp_file(content) as stub_file:
-                actual = cli.parse_imports_from_stub(stub_file.name)
+                actual = cli.parse_stub(stub_file.name)
                 self.assertEqual(actual, expected)
 
         invalid_cases = [
-            '',
-            '\n\n',
-            '  python_imports=',
+            b'',
+            b'\n\n',
+            # No interpreter
+            b"  python_imports = 'myworkspace/spam/eggs'",
+            # No imports
+            b"PYTHON_BINARY = 'python'\n",
+            # Interpreter is label
+            b"""
+  python_imports = ''
+PYTHON_BINARY = '//mypackage:python'
+""",
             ]
         for content in invalid_cases:
             with test_utils.temp_file(content) as stub_file:
                 with self.assertRaises(error.Error):
-                    cli.parse_imports_from_stub(stub_file.name)
+                    cli.parse_stub(stub_file.name)
 
 
 if __name__ == '__main__':
diff --git a/compiler/compiler.py b/compiler/compiler.py
index 4882a4e..5eb30d9 100644
--- a/compiler/compiler.py
+++ b/compiler/compiler.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/compiler/manifest_parser.py b/compiler/manifest_parser.py
index 79a87e9..3bd2e70 100644
--- a/compiler/manifest_parser.py
+++ b/compiler/manifest_parser.py
@@ -17,7 +17,10 @@
 The format is described in
 https://github.com/bazelbuild/bazel/blob/master/src/main/tools/build-runfiles.cc
 
+We assume manifest files are utf-8 encoded.
+
 """
+import io
 
 from subpar.compiler import error
 
@@ -41,7 +44,7 @@
 
     """
     manifest = {}
-    with open(manifest_filename, 'rb') as f:
+    with io.open(manifest_filename, 'rt', encoding='utf8') as f:
         lineno = 0
         for line in f:
             # Split line into fields
diff --git a/compiler/manifest_parser_test.py b/compiler/manifest_parser_test.py
index e4cee1c..4de3f3f 100644
--- a/compiler/manifest_parser_test.py
+++ b/compiler/manifest_parser_test.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,11 +24,11 @@
     def test_parse_manifest_valid(self):
         valid = (
             # 1 field, no trailing space
-            'ccccc/__init__.py\n' +
+            b'ccccc/__init__.py\n' +
             # 1 field, trailing space
-            'ccccc/ddddd/__init__.py \n' +
+            b'ccccc/ddddd/__init__.py \n' +
             # 2 fields
-            'ccccc/ddddd/eeeee /code/rrrrr/ccccc/ddddd/eeeee\n'
+            b'ccccc/ddddd/eeeee /code/rrrrr/ccccc/ddddd/eeeee\n'
         )
         expected = {
             'ccccc/__init__.py': None,
@@ -44,13 +42,13 @@
     def test_parse_manifest_invalid(self):
         invalids = [
             # Repeated name
-            ('ccccc/__init__.py \n' +
-             'ccccc/ddddd/__init__.py \n' +
-             'ccccc/__init__.py \n'),
+            (b'ccccc/__init__.py \n' +
+             b'ccccc/ddddd/__init__.py \n' +
+             b'ccccc/__init__.py \n'),
             # Too many spaces
-            'ccccc/__init__.py foo bar\n',
+            b'ccccc/__init__.py foo bar\n',
             # Not enough spaces
-            '\n\n',
+            b'\n\n',
         ]
         for invalid in invalids:
             with test_utils.temp_file(invalid) as t:
diff --git a/compiler/python_archive.py b/compiler/python_archive.py
index 7895718..db66a0b 100755
--- a/compiler/python_archive.py
+++ b/compiler/python_archive.py
@@ -28,6 +28,7 @@
 
 """
 
+import io
 import logging
 import os
 import pkgutil
@@ -49,9 +50,9 @@
 """
 
 # Fully qualified names of subpar packages
-_compiler_package = 'subpar.compiler'
-_runtime_package = 'subpar.runtime'
-_runtime_path = 'subpar/runtime'
+_subpar_package = 'subpar'
+_compiler_package = _subpar_package + '.compiler'
+_runtime_package = _subpar_package + '.runtime'
 
 # List of files from the runtime package to include in every .par file
 _runtime_support_files = ['support.py',]
@@ -74,7 +75,7 @@
                  manifest_filename,
                  manifest_root,
                  output_filename,
-    ):
+                ):
         self.main_filename = main_filename
 
         self.import_roots = import_roots
@@ -135,11 +136,16 @@
         Returns:
             A StoredResource
         """
-        contents = _main_template % {
+        template_contents = _main_template % {
             'runtime_package': _runtime_package,
             'import_roots': str(self.import_roots),
         }
-        contents = contents + open(self.main_filename, 'r').read()
+        with open(self.main_filename, 'rb') as main_file:
+            main_contents = main_file.read()
+        # We don't know the encoding of the main source file, so
+        # require that the template be pure ascii, which we can safely
+        # prepend.
+        contents = template_contents.encode('ascii') + main_contents
         return stored_resource.StoredContent('__main__.py', contents)
 
     def scan_manifest(self, manifest):
@@ -190,7 +196,8 @@
         This tells the operating system (well, UNIX) how to execute the file.
         """
         logging.debug('Writing boilerplate...')
-        temp_parfile.write('#!%s\n' % self.interpreter)
+        boilerplate = '#!%s\n' % self.interpreter
+        temp_parfile.write(boilerplate.encode('ascii'))
 
     def write_zip_data(self, temp_parfile, stored_resources):
         """Write the second part of a parfile, consisting of ZIP data
@@ -211,7 +218,7 @@
         """Move newly created parfile to its final filename."""
         # Python 2 doesn't have os.replace, so use os.rename which is
         # not atomic in all cases.
-        os.chmod(temp_parfile_name, 0755)
+        os.chmod(temp_parfile_name, 0o0755)
         os.rename(temp_parfile_name, self.output_filename)
 
 
@@ -230,8 +237,8 @@
     Returns:
         A StoredResource representing the content of that file
     """
-    stored_filename = os.path.join(_runtime_path, name)
-    content = pkgutil.get_data(_runtime_package, name)
+    stored_filename = os.path.join(_subpar_package, 'runtime', name)
+    content = pkgutil.get_data(_subpar_package, 'runtime/' + name)
     # content is None means the file wasn't found.  content == '' is
     # valid, it means the file was found and was empty.
     if content is None:
diff --git a/compiler/python_archive_test.py b/compiler/python_archive_test.py
index f58a961..aa7922e 100644
--- a/compiler/python_archive_test.py
+++ b/compiler/python_archive_test.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,11 +35,12 @@
         if not os.path.exists(self.input_dir):
             os.makedirs(self.input_dir)
         self.manifest_filename = os.path.join(self.input_dir, 'manifest')
-        self.main_file = test_utils.temp_file('print("Hello World!")',
+        self.main_file = test_utils.temp_file(b'print("Hello World!")',
                                               suffix='.py')
+        manifest_content = '%s %s\n' % (
+            os.path.basename(self.main_file.name), self.main_file.name)
         self.manifest_file = test_utils.temp_file(
-            '%s %s\n' %
-            (os.path.basename(self.main_file.name), self.main_file.name))
+            manifest_content.encode('utf8'))
         self.output_dir = os.path.join(self.tmpdir, 'output')
         if not os.path.exists(self.output_dir):
             os.makedirs(self.output_dir)
@@ -66,19 +65,19 @@
             par.create()
 
     def test_create_manifest_parse_error(self):
-        with test_utils.temp_file('blah blah blah\n') as manifest_file:
+        with test_utils.temp_file(b'blah blah blah\n') as manifest_file:
             par = self._construct(manifest_filename=manifest_file.name)
             with self.assertRaises(error.Error):
                 par.create()
 
     def test_create_manifest_contains___main___py(self):
-        with test_utils.temp_file('__main__.py\n') as manifest_file:
+        with test_utils.temp_file(b'__main__.py\n') as manifest_file:
             par = self._construct(manifest_filename=manifest_file.name)
             with self.assertRaises(error.Error):
                 par.create()
 
     def test_create_source_file_not_found(self):
-        with test_utils.temp_file('foo.py doesnotexist.py\n') as manifest_file:
+        with test_utils.temp_file(b'foo.py doesnotexist.py\n') as manifest_file:
             par = self._construct(manifest_filename=manifest_file.name)
             with self.assertRaises(OSError):
                 par.create()
@@ -114,7 +113,7 @@
         par.create()
         self.assertTrue(os.path.exists(self.output_filename))
         self.assertEqual(
-            subprocess.check_output([self.output_filename]), 'Hello World!\n')
+            subprocess.check_output([self.output_filename]), b'Hello World!\n')
 
     def test_create_temp_parfile(self):
         par = self._construct()
@@ -146,7 +145,7 @@
     def test_scan_manifest_has_collision(self):
         par = self._construct()
         # Support file already present in manifest, use manifest version
-        with test_utils.temp_file('blah blah\n') as shadowing_support_file:
+        with test_utils.temp_file(b'blah blah\n') as shadowing_support_file:
             manifest = {
                 'foo.py': '/something/foo.py',
                 'subpar/runtime/support.py': shadowing_support_file.name,
@@ -189,7 +188,7 @@
         tmpdir = test_utils.mkdtemp()
         filename = os.path.join(tmpdir, 'afile')
         with open(filename, 'wb') as f:
-            f.write('dontcare')
+            f.write(b'dontcare')
         # File exists
         self.assertTrue(os.path.exists(filename))
         python_archive.remove_if_present(filename)
@@ -198,6 +197,8 @@
         python_archive.remove_if_present(filename)
         self.assertFalse(os.path.exists(filename))
 
+    def test_fetch_support_file(self):
+        resource = python_archive.fetch_support_file('__init__.py')
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/compiler/stored_resource_test.py b/compiler/stored_resource_test.py
index c3664b4..2856b0a 100644
--- a/compiler/stored_resource_test.py
+++ b/compiler/stored_resource_test.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -42,14 +40,14 @@
         z.close()
 
     def test_StoredFile(self):
-        expected_content = 'Contents of foo/bar'
+        expected_content = b'Contents of foo/bar'
         name = 'foo/bar'
         f = test_utils.temp_file(expected_content)
         resource = stored_resource.StoredFile(name, f.name)
         self._write_and_check(resource, name, expected_content)
 
     def test_StoredContent(self):
-        expected_content = 'Contents of foo/bar'
+        expected_content = b'Contents of foo/bar'
         name = 'foo/bar'
         resource = stored_resource.StoredContent(name, expected_content)
         self._write_and_check(resource, name, expected_content)
diff --git a/compiler/test_utils.py b/compiler/test_utils.py
index b1056c1..62a16c4 100644
--- a/compiler/test_utils.py
+++ b/compiler/test_utils.py
@@ -21,12 +21,7 @@
 
 def get_test_tmpdir():
     """Get default test temp dir."""
-    tmpdir = os.environ.get('TEST_TMPDIR', '')
-    if not tmpdir:
-        tempfile.gettempdir()
-        testname = os.path.splitext(os.path.basename(sys.argv[0]))[0]
-        if testname:
-            tmpdir = os.path.join(tmpdir, testname)
+    tmpdir = os.environ.get('TEST_TMPDIR', tempfile.gettempdir())
     return tmpdir
 
 
@@ -35,8 +30,9 @@
     return tempfile.mkdtemp(dir=get_test_tmpdir())
 
 
-def temp_file(contents, suffix='', tmpdir=None):
+def temp_file(contents, suffix=''):
     """Create a self-deleting temp file with the given content"""
+    tmpdir = get_test_tmpdir()
     t = tempfile.NamedTemporaryFile(suffix=suffix, dir=tmpdir)
     t.write(contents)
     t.flush()
diff --git a/runtime/BUILD b/runtime/BUILD
index be4a37f..5357705 100644
--- a/runtime/BUILD
+++ b/runtime/BUILD
@@ -6,12 +6,28 @@
         "support.py",
         "//:__init__.py",
     ],
+    srcs_version = "PY2AND3",
 )
 
 py_test(
-    name = "support_test",
+    name = "support_PY2_test",
     size = "small",
     srcs = ["support_test.py"],
+    default_python_version = "PY2",
+    main = "support_test.py",
+    srcs_version = "PY2AND3",
+    deps = [
+        ":support",
+    ],
+)
+
+py_test(
+    name = "support_PY3_test",
+    size = "small",
+    srcs = ["support_test.py"],
+    default_python_version = "PY3",
+    main = "support_test.py",
+    srcs_version = "PY2AND3",
     deps = [
         ":support",
     ],
diff --git a/runtime/support.py b/runtime/support.py
index 5e6c259..53ad9b6 100644
--- a/runtime/support.py
+++ b/runtime/support.py
@@ -41,7 +41,7 @@
     """Print a debugging message in the same format as python -vv output"""
     if sys.flags.verbose:
         sys.stderr.write(msg)
-        sys.stderr.write("\n")
+        sys.stderr.write('\n')
 
 
 def _find_archive():
@@ -66,7 +66,7 @@
     # Add third-party library entries to sys.path
     archive_path = _find_archive()
     if not archive_path:
-        warnings.warn("Failed to initialize .par file runtime support",
+        warnings.warn('Failed to initialize .par file runtime support',
                       ImportWarning)
         return
 
diff --git a/runtime/support_test.py b/runtime/support_test.py
index 950d769..9b9d8b0 100644
--- a/runtime/support_test.py
+++ b/runtime/support_test.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import StringIO
+import io
 import sys
 import unittest
 
@@ -26,12 +24,12 @@
     def test_log(self):
         old_stderr = sys.stderr
         try:
-            mock_stderr = StringIO.StringIO()
+            mock_stderr = io.StringIO()
             sys.stderr = mock_stderr
             # pylint: disable=protected-access,no-self-use
-            support._log("Test Log Message")
+            support._log('Test Log Message')
             if sys.flags.verbose:
-                expected = "Test Log Message\n"
+                expected = 'Test Log Message\n'
             else:
                 expected = ""
             self.assertEqual(mock_stderr.getvalue(), expected)
diff --git a/subpar.bzl b/subpar.bzl
index ad68d30..e9f27ba 100644
--- a/subpar.bzl
+++ b/subpar.bzl
@@ -71,9 +71,9 @@
 
     # Assemble command line for .par compiler
     args = [
-        '--imports_from_stub', stub_file,
         '--manifest_file', sources_file.path,
         '--outputpar', ctx.outputs.executable.path,
+        '--stub_file', stub_file,
         main_py_file.path,
     ]
     ctx.action(
@@ -82,7 +82,7 @@
         progress_message='Building par file %s' % ctx.label,
         executable=ctx.executable._compiler,
         arguments=args,
-        mnemonic="PythonCompile",
+        mnemonic='PythonCompile',
     )
 
     # .par file itself has no runfiles and no providers
@@ -109,6 +109,7 @@
             single_file = True,
         ),
         "imports": attr.string_list(default = []),
+        "default_python_version": attr.string(mandatory = True),
         "_compiler": attr.label(
             default = Label("//compiler"),
             executable = True,
@@ -152,4 +153,6 @@
     native.py_binary(name=name, **kwargs)
     main = kwargs.get('main', name + '.py')
     imports = kwargs.get('imports')
-    parfile(name=name + '.par', src=name, main=main, imports=imports)
+    default_python_version = kwargs.get('default_python_version', 'PY2')
+    parfile(name=name + '.par', src=name, main=main, imports=imports,
+            default_python_version=default_python_version)
diff --git a/tests/BUILD b/tests/BUILD
index 7dd7011..43d3c3e 100644
--- a/tests/BUILD
+++ b/tests/BUILD
@@ -15,100 +15,84 @@
 )
 
 # Targets used by tests below
-par_binary(
-    name = "package_c/c",
+[par_binary(
+    name = "package_c/c_%s" % version,
     srcs = ["package_c/c.py"],
-    deps = ["//tests/package_b:b"],
-)
+    default_python_version = version,
+    main = "package_c/c.py",
+    srcs_version = "PY2AND3",
+    deps = ["//tests/package_b:b_%s" % version],
+) for version in [
+    "PY2",
+    "PY3",
+]]
 
-par_binary(
-    name = "package_d/d",
+[par_binary(
+    name = "package_d/d_%s" % version,
     srcs = ["package_d/d.py"],
+    default_python_version = version,
     imports = [
         "package_b",
         "package_c",
     ],
+    main = "package_d/d.py",
+    srcs_version = "PY2AND3",
     deps = [
-        "//tests:package_c/c",
-        "//tests/package_b:b",
+        "//tests:package_c/c_%s" % version,
+        "//tests/package_b:b_%s" % version,
     ],
-)
+) for version in [
+    "PY2",
+    "PY3",
+]]
 
-par_binary(
-    name = "package_e/e",
+[par_binary(
+    name = "package_e/e_%s" % version,
     srcs = ["package_e/e.py"],
     data = [
         "@test_workspace//:data_file.txt",
     ],
+    default_python_version = version,
+    main = "package_e/e.py",
+    srcs_version = "PY2AND3",
+) for version in [
+    "PY2",
+    "PY3",
+]]
+
+par_binary(
+    name = "package_f/f_PY2",
+    srcs = ["package_f/f_PY2.py"],
+)
+
+par_binary(
+    name = "package_f/f_PY3",
+    srcs = ["package_f/f_PY3.py"],
+    default_python_version = "PY3",
+    srcs_version = "PY2AND3",
 )
 
 # Test targets
-sh_test(
-    name = "basic_test",
+[sh_test(
+    name = "%s_%s" % (name, version),
     size = "small",
     srcs = ["test_harness.sh"],
     args = [
-        "tests/package_a/a.par",
-        "tests/package_a/a_filelist.txt",
+        "tests%s_%s.par" % (path, version),
+        "tests%s_%s_filelist.txt" % (path, version),
     ],
     data = [
-        "//tests/package_a:a.par",
-        "//tests/package_a:a_filelist.txt",
+        "//tests%s_%s.par" % (label, version),
+        "//tests%s_%s_filelist.txt" % (label, version),
     ],
-)
-
-sh_test(
-    name = "direct_dependency_test",
-    size = "small",
-    srcs = ["test_harness.sh"],
-    args = [
-        "tests/package_b/b.par",
-        "tests/package_b/b_filelist.txt",
-    ],
-    data = [
-        "//tests/package_b:b.par",
-        "//tests/package_b:b_filelist.txt",
-    ],
-)
-
-sh_test(
-    name = "indirect_dependency_test",
-    size = "small",
-    srcs = ["test_harness.sh"],
-    args = [
-        "tests/package_c/c.par",
-        "tests/package_c/c_filelist.txt",
-    ],
-    data = [
-        "//tests:package_c/c.par",
-        "//tests:package_c/c_filelist.txt",
-    ],
-)
-
-sh_test(
-    name = "import_root_test",
-    size = "small",
-    srcs = ["test_harness.sh"],
-    args = [
-        "tests/package_d/d.par",
-        "tests/package_d/d_filelist.txt",
-    ],
-    data = [
-        "//tests:package_d/d.par",
-        "//tests:package_d/d_filelist.txt",
-    ],
-)
-
-sh_test(
-    name = "external_workspace_test",
-    size = "small",
-    srcs = ["test_harness.sh"],
-    args = [
-        "tests/package_e/e.par",
-        "tests/package_e/e_filelist.txt",
-    ],
-    data = [
-        "//tests:package_e/e.par",
-        "//tests:package_e/e_filelist.txt",
-    ],
-)
+) for name, label, path in [
+    ("basic_test", "/package_a:a", "/package_a/a"),
+    ("direct_dependency_test", "/package_b:b", "/package_b/b"),
+    ("indirect_dependency_test", ":package_c/c", "/package_c/c"),
+    ("import_root_test", ":package_d/d", "/package_d/d"),
+    ("external_workspace_test", ":package_e/e", "/package_e/e"),
+    ("version_test", ":package_f/f", "/package_f/f"),
+] for version in [
+    "PY2",
+    "PY3",
+]]
diff --git a/tests/package_a/BUILD b/tests/package_a/BUILD
index b25a03e..589defd 100644
--- a/tests/package_a/BUILD
+++ b/tests/package_a/BUILD
@@ -4,19 +4,28 @@
 
 load("//:subpar.bzl", "par_binary")
 
-exports_files(["a_filelist.txt"])
+exports_files([
+    "a_PY2_filelist.txt",
+    "a_PY3_filelist.txt",
+])
 
-par_binary(
-    name = "a",
+[par_binary(
+    name = "a_%s" % version,
     srcs = [
         "a.py",
-        "//:__init__.py",
     ],
     data = ["a_dat.txt"],
-)
+    default_python_version = version,
+    main = "a.py",
+    srcs_version = "PY2AND3",
+) for version in [
+    "PY2",
+    "PY3",
+]]
 
 py_library(
     name = "a_lib",
     srcs = ["a_lib.py"],
     data = ["a_lib_dat.txt"],
+    srcs_version = "PY2AND3",
 )
diff --git a/tests/package_a/a.py b/tests/package_a/a.py
index 9702fc4..2f0eea6 100644
--- a/tests/package_a/a.py
+++ b/tests/package_a/a.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -61,22 +59,22 @@
     try:
         # pylint: disable=import-self
         from . import a as a1
-        raise AssertionError("This shouldn't have worked: %r" % a1)
-    except (AttributeError, ImportError) as e:
-        assert e.message == 'cannot import name a', e
+        raise AssertionError('This shouldn\'t have worked: %r' % a1)
+    except ImportError as e:
+        assert 'cannot import name' in str(e), e
     try:
         # pylint: disable=import-self
         import subpar.tests.package_a.a as a2
-        raise AssertionError("This shouldn't have worked: %r" % a2)
-    except (AttributeError, ImportError) as e:
-        assert e.message == "'module' object has no attribute 'a'", e
+        raise AssertionError('This shouldn\'t have worked: %r' % a2)
+    except AttributeError as e:
+        assert "'module' object has no attribute 'a'" in str(e), e
 
 
 def main():
     print('In a.py main()')
     # Test resource extraction
     a_dat = pkgutil.get_data('subpar.tests.package_a', 'a_dat.txt')
-    assert (a_dat == "Dummy data file for a.py\n"), a_dat
+    assert (a_dat == b'Dummy data file for a.py\n'), a_dat
 
 
 if __name__ == '__main__':
diff --git a/tests/package_a/a_filelist.txt b/tests/package_a/a_PY2_filelist.txt
similarity index 87%
copy from tests/package_a/a_filelist.txt
copy to tests/package_a/a_PY2_filelist.txt
index bdbf7a5..1976cd0 100644
--- a/tests/package_a/a_filelist.txt
+++ b/tests/package_a/a_PY2_filelist.txt
@@ -4,6 +4,6 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_a/__init__.py
-subpar/tests/package_a/a
 subpar/tests/package_a/a.py
+subpar/tests/package_a/a_PY2
 subpar/tests/package_a/a_dat.txt
diff --git a/tests/package_a/a_filelist.txt b/tests/package_a/a_PY3_filelist.txt
similarity index 87%
rename from tests/package_a/a_filelist.txt
rename to tests/package_a/a_PY3_filelist.txt
index bdbf7a5..606238e 100644
--- a/tests/package_a/a_filelist.txt
+++ b/tests/package_a/a_PY3_filelist.txt
@@ -4,6 +4,6 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_a/__init__.py
-subpar/tests/package_a/a
 subpar/tests/package_a/a.py
+subpar/tests/package_a/a_PY3
 subpar/tests/package_a/a_dat.txt
diff --git a/tests/package_a/a_lib.py b/tests/package_a/a_lib.py
index 895f253..4bf4df6 100644
--- a/tests/package_a/a_lib.py
+++ b/tests/package_a/a_lib.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,4 +21,4 @@
     print('In a_lib.py lib()')
     # Test resource extraction
     a_lib_dat = pkgutil.get_data('subpar.tests.package_a', 'a_lib_dat.txt')
-    assert (a_lib_dat == "Dummy data file for a_lib.py\n"), a_lib_dat
+    assert (a_lib_dat == b'Dummy data file for a_lib.py\n'), a_lib_dat
diff --git a/tests/package_b/BUILD b/tests/package_b/BUILD
index 9f11f8a..e222c8d 100644
--- a/tests/package_b/BUILD
+++ b/tests/package_b/BUILD
@@ -4,14 +4,23 @@
 
 load("//:subpar.bzl", "par_binary")
 
-exports_files(["b_filelist.txt"])
+exports_files([
+    "b_PY2_filelist.txt",
+    "b_PY3_filelist.txt",
+])
 
-par_binary(
-    name = "b",
+[par_binary(
+    name = "b_%s" % version,
     srcs = ["b.py"],
     data = ["b_dat.txt"],
+    default_python_version = version,
+    main = "b.py",
+    srcs_version = "PY2AND3",
     deps = [
-        "//tests/package_a:a",
+        "//tests/package_a:a_%s" % version,
         "//tests/package_a:a_lib",
     ],
-)
+) for version in [
+    "PY2",
+    "PY3",
+]]
diff --git a/tests/package_b/b.py b/tests/package_b/b.py
index a96d0a5..07ed083 100644
--- a/tests/package_b/b.py
+++ b/tests/package_b/b.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,7 +30,7 @@
     print('In b.py main()')
     # Test resource extraction
     b_dat = pkgutil.get_data('subpar.tests.package_b', 'b_dat.txt')
-    assert (b_dat == "Dummy data file for b.py\n"), b_dat
+    assert (b_dat == b'Dummy data file for b.py\n'), b_dat
 
 
 if __name__ == '__main__':
diff --git a/tests/package_b/b_filelist.txt b/tests/package_b/b_PY2_filelist.txt
similarity index 86%
copy from tests/package_b/b_filelist.txt
copy to tests/package_b/b_PY2_filelist.txt
index 9177503..f6b0955 100644
--- a/tests/package_b/b_filelist.txt
+++ b/tests/package_b/b_PY2_filelist.txt
@@ -4,12 +4,12 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_a/__init__.py
-subpar/tests/package_a/a
 subpar/tests/package_a/a.py
+subpar/tests/package_a/a_PY2
 subpar/tests/package_a/a_dat.txt
 subpar/tests/package_a/a_lib.py
 subpar/tests/package_a/a_lib_dat.txt
 subpar/tests/package_b/__init__.py
-subpar/tests/package_b/b
 subpar/tests/package_b/b.py
+subpar/tests/package_b/b_PY2
 subpar/tests/package_b/b_dat.txt
diff --git a/tests/package_b/b_filelist.txt b/tests/package_b/b_PY3_filelist.txt
similarity index 86%
rename from tests/package_b/b_filelist.txt
rename to tests/package_b/b_PY3_filelist.txt
index 9177503..7ace434 100644
--- a/tests/package_b/b_filelist.txt
+++ b/tests/package_b/b_PY3_filelist.txt
@@ -4,12 +4,12 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_a/__init__.py
-subpar/tests/package_a/a
 subpar/tests/package_a/a.py
+subpar/tests/package_a/a_PY3
 subpar/tests/package_a/a_dat.txt
 subpar/tests/package_a/a_lib.py
 subpar/tests/package_a/a_lib_dat.txt
 subpar/tests/package_b/__init__.py
-subpar/tests/package_b/b
 subpar/tests/package_b/b.py
+subpar/tests/package_b/b_PY3
 subpar/tests/package_b/b_dat.txt
diff --git a/tests/package_c/c.py b/tests/package_c/c.py
index 6f941ca..3b8bf3d 100644
--- a/tests/package_c/c.py
+++ b/tests/package_c/c.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tests/package_c/c_filelist.txt b/tests/package_c/c_PY2_filelist.txt
similarity index 83%
copy from tests/package_c/c_filelist.txt
copy to tests/package_c/c_PY2_filelist.txt
index 361772f..5506e91 100644
--- a/tests/package_c/c_filelist.txt
+++ b/tests/package_c/c_PY2_filelist.txt
@@ -4,15 +4,15 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_a/__init__.py
-subpar/tests/package_a/a
 subpar/tests/package_a/a.py
+subpar/tests/package_a/a_PY2
 subpar/tests/package_a/a_dat.txt
 subpar/tests/package_a/a_lib.py
 subpar/tests/package_a/a_lib_dat.txt
 subpar/tests/package_b/__init__.py
-subpar/tests/package_b/b
 subpar/tests/package_b/b.py
+subpar/tests/package_b/b_PY2
 subpar/tests/package_b/b_dat.txt
 subpar/tests/package_c/__init__.py
-subpar/tests/package_c/c
 subpar/tests/package_c/c.py
+subpar/tests/package_c/c_PY2
diff --git a/tests/package_c/c_filelist.txt b/tests/package_c/c_PY3_filelist.txt
similarity index 83%
rename from tests/package_c/c_filelist.txt
rename to tests/package_c/c_PY3_filelist.txt
index 361772f..507636e 100644
--- a/tests/package_c/c_filelist.txt
+++ b/tests/package_c/c_PY3_filelist.txt
@@ -4,15 +4,15 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_a/__init__.py
-subpar/tests/package_a/a
 subpar/tests/package_a/a.py
+subpar/tests/package_a/a_PY3
 subpar/tests/package_a/a_dat.txt
 subpar/tests/package_a/a_lib.py
 subpar/tests/package_a/a_lib_dat.txt
 subpar/tests/package_b/__init__.py
-subpar/tests/package_b/b
 subpar/tests/package_b/b.py
+subpar/tests/package_b/b_PY3
 subpar/tests/package_b/b_dat.txt
 subpar/tests/package_c/__init__.py
-subpar/tests/package_c/c
 subpar/tests/package_c/c.py
+subpar/tests/package_c/c_PY3
diff --git a/tests/package_d/d.py b/tests/package_d/d.py
index 12e9373..e05906f 100644
--- a/tests/package_d/d.py
+++ b/tests/package_d/d.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tests/package_d/d_filelist.txt b/tests/package_d/d_PY2_filelist.txt
similarity index 81%
rename from tests/package_d/d_filelist.txt
rename to tests/package_d/d_PY2_filelist.txt
index 5e460e0..f38f57c 100644
--- a/tests/package_d/d_filelist.txt
+++ b/tests/package_d/d_PY2_filelist.txt
@@ -4,18 +4,18 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_a/__init__.py
-subpar/tests/package_a/a
 subpar/tests/package_a/a.py
+subpar/tests/package_a/a_PY2
 subpar/tests/package_a/a_dat.txt
 subpar/tests/package_a/a_lib.py
 subpar/tests/package_a/a_lib_dat.txt
 subpar/tests/package_b/__init__.py
-subpar/tests/package_b/b
 subpar/tests/package_b/b.py
+subpar/tests/package_b/b_PY2
 subpar/tests/package_b/b_dat.txt
 subpar/tests/package_c/__init__.py
-subpar/tests/package_c/c
 subpar/tests/package_c/c.py
+subpar/tests/package_c/c_PY2
 subpar/tests/package_d/__init__.py
-subpar/tests/package_d/d
 subpar/tests/package_d/d.py
+subpar/tests/package_d/d_PY2
diff --git a/tests/package_d/d_filelist.txt b/tests/package_d/d_PY3_filelist.txt
similarity index 81%
copy from tests/package_d/d_filelist.txt
copy to tests/package_d/d_PY3_filelist.txt
index 5e460e0..b29dfff 100644
--- a/tests/package_d/d_filelist.txt
+++ b/tests/package_d/d_PY3_filelist.txt
@@ -4,18 +4,18 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_a/__init__.py
-subpar/tests/package_a/a
 subpar/tests/package_a/a.py
+subpar/tests/package_a/a_PY3
 subpar/tests/package_a/a_dat.txt
 subpar/tests/package_a/a_lib.py
 subpar/tests/package_a/a_lib_dat.txt
 subpar/tests/package_b/__init__.py
-subpar/tests/package_b/b
 subpar/tests/package_b/b.py
+subpar/tests/package_b/b_PY3
 subpar/tests/package_b/b_dat.txt
 subpar/tests/package_c/__init__.py
-subpar/tests/package_c/c
 subpar/tests/package_c/c.py
+subpar/tests/package_c/c_PY3
 subpar/tests/package_d/__init__.py
-subpar/tests/package_d/d
 subpar/tests/package_d/d.py
+subpar/tests/package_d/d_PY3
diff --git a/tests/package_e/e.py b/tests/package_e/e.py
index 83f09e8..1345b93 100644
--- a/tests/package_e/e.py
+++ b/tests/package_e/e.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python2
-
 # Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tests/package_e/e_filelist.txt b/tests/package_e/e_PY2_filelist.txt
similarity index 87%
rename from tests/package_e/e_filelist.txt
rename to tests/package_e/e_PY2_filelist.txt
index d0688e7..4a23e16 100644
--- a/tests/package_e/e_filelist.txt
+++ b/tests/package_e/e_PY2_filelist.txt
@@ -4,6 +4,6 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_e/__init__.py
-subpar/tests/package_e/e
 subpar/tests/package_e/e.py
+subpar/tests/package_e/e_PY2
 test_workspace/data_file.txt
diff --git a/tests/package_e/e_filelist.txt b/tests/package_e/e_PY3_filelist.txt
similarity index 87%
copy from tests/package_e/e_filelist.txt
copy to tests/package_e/e_PY3_filelist.txt
index d0688e7..48736c6 100644
--- a/tests/package_e/e_filelist.txt
+++ b/tests/package_e/e_PY3_filelist.txt
@@ -4,6 +4,6 @@
 subpar/runtime/support.py
 subpar/tests/__init__.py
 subpar/tests/package_e/__init__.py
-subpar/tests/package_e/e
 subpar/tests/package_e/e.py
+subpar/tests/package_e/e_PY3
 test_workspace/data_file.txt
diff --git a/tests/package_f/f_PY2.py b/tests/package_f/f_PY2.py
new file mode 100644
index 0000000..74d2b27
--- /dev/null
+++ b/tests/package_f/f_PY2.py
@@ -0,0 +1,28 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Integration test program F for Subpar.
+
+Test Python2 specific functionality
+"""
+
+import sys
+
+def main():
+    assert sys.version_info.major == 2, sys.version
+    print('In f_PY2.py main()')
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/package_f/f_PY2_filelist.txt b/tests/package_f/f_PY2_filelist.txt
new file mode 100644
index 0000000..8b382cb
--- /dev/null
+++ b/tests/package_f/f_PY2_filelist.txt
@@ -0,0 +1,8 @@
+__main__.py
+subpar/__init__.py
+subpar/runtime/__init__.py
+subpar/runtime/support.py
+subpar/tests/__init__.py
+subpar/tests/package_f/__init__.py
+subpar/tests/package_f/f_PY2
+subpar/tests/package_f/f_PY2.py
diff --git a/tests/package_f/f_PY3.py b/tests/package_f/f_PY3.py
new file mode 100644
index 0000000..e421ead
--- /dev/null
+++ b/tests/package_f/f_PY3.py
@@ -0,0 +1,28 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Integration test program F for Subpar.
+
+Test Python3 specific functionality
+"""
+
+import sys
+
+def main():
+    assert sys.version_info.major == 3, sys.version
+    print('In f_PY2.py main()')
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/package_f/f_PY3_filelist.txt b/tests/package_f/f_PY3_filelist.txt
new file mode 100644
index 0000000..b9ba660
--- /dev/null
+++ b/tests/package_f/f_PY3_filelist.txt
@@ -0,0 +1,8 @@
+__main__.py
+subpar/__init__.py
+subpar/runtime/__init__.py
+subpar/runtime/support.py
+subpar/tests/__init__.py
+subpar/tests/package_f/__init__.py
+subpar/tests/package_f/f_PY3
+subpar/tests/package_f/f_PY3.py