blob: d1fcb598429e718f0fc45d949e3bac0cca700076 [file] [log] [blame]
# 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.
"""Command line interface to subpar compiler"""
import argparse
import io
import os
import re
from subpar.compiler import error
from subpar.compiler import python_archive
def bool_from_string(raw_value):
"""Parse a boolean command line argument value"""
if raw_value == 'True':
return True
elif raw_value == 'False':
return False
else:
raise argparse.ArgumentTypeError(
'Value must be True or False, got %r instead.' % raw_value)
def make_command_line_parser():
"""Return an object that can parse this program's command line"""
parser = argparse.ArgumentParser(
description='Subpar Python Executable Builder')
parser.add_argument(
'main_filename',
help='Python source file to use as main entry point')
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.',
required=True)
parser.add_argument(
'--manifest_root',
help='Root directory of all relative paths in manifest file.',
default=os.getcwd())
parser.add_argument(
'--output_par',
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)
parser.add_argument(
'--interpreter',
help='Interpreter to use instead of determining it from the stub file')
# The default timestamp is "Jan 1 1980 00:00:00 utc", which is the
# earliest time that can be stored in a zip file.
#
# "Seconds since Unix epoch" was chosen to be compatible with
# the SOURCE_DATE_EPOCH standard
#
# Numeric value is from running this:
# "date --date='Jan 1 1980 00:00:00 utc' --utc +%s"
parser.add_argument(
'--timestamp',
help='Timestamp (in seconds since Unix epoch) for all stored files',
type=int,
default=315532800,
)
# See
# http://setuptools.readthedocs.io/en/latest/setuptools.html#setting-the-zip-safe-flag
# for background and explanation.
parser.add_argument(
'--zip_safe',
help='Safe to import modules and access datafiles straight from zip ' +
'archive? If False, all files will be extracted to a temporary ' +
'directory at the start of execution.',
type=bool_from_string,
required=True)
parser.add_argument(
'--import_root',
help='Path to add to sys.path, may be repeated to provide multiple roots.',
action='append',
default=[],
dest='import_roots')
return parser
def parse_stub(stub_filename):
"""Parse interpreter path from a py_binary() stub.
We assume the stub is utf-8 encoded.
TODO(bazelbuild/bazel#7805): Remove this once we can access the py_runtime from Starlark.
Returns path to Python interpreter
"""
# Find the interpreter
interpreter_regex = re.compile(r'''^PYTHON_BINARY = '([^']*)'$''')
interpreter = None
with io.open(stub_filename, 'rt', encoding='utf8') as stub_file:
for line in stub_file:
interpreter_match = interpreter_regex.match(line)
if interpreter_match:
interpreter = interpreter_match.group(1)
if not interpreter:
raise error.Error('Failed to parse stub file [%s]' % stub_filename)
# Determine the Python interpreter, checking for default toolchain.
#
# This somewhat mirrors the logic in python_stub_template.txt, but we don't support
# relative paths (i.e., in-workspace interpreters). This is because the interpreter
# will be used in the .par file's shebang, and putting a relative path in a shebang
# is extremely brittle and non-relocatable. (The reason the standard py_binary rule
# can use an in-workspace interpreter is that its stub script runs in a separate
# process and has a shebang referencing the system interpreter). As a special case,
# if the Python target is using the autodetecting Python toolchain, which is
# technically an in-workspace runtime, we rewrite it to "/usr/bin/env python[2|3]"
# rather than fail.
if interpreter.startswith('//'):
raise error.Error('Python interpreter must not be a label [%s]' %
stub_filename)
elif interpreter.startswith('/'):
pass
elif interpreter == 'bazel_tools/tools/python/py3wrapper.sh': # Default toolchain
# Replace default toolchain python3 wrapper with default python3 on path
interpreter = '/usr/bin/env python3'
elif interpreter == 'bazel_tools/tools/python/py2wrapper.sh': # Default toolchain
# Replace default toolchain python2 wrapper with default python2 on path
interpreter = '/usr/bin/env python2'
elif '/' in interpreter:
raise error.Error(
'par files require a Python runtime that is ' +
'installed on the system, not defined inside the workspace. Use ' +
'a `py_runtime` with an absolute path, not a label.')
else:
interpreter = '/usr/bin/env %s' % interpreter
return interpreter
def main(argv):
"""Command line interface to Subpar"""
parser = make_command_line_parser()
args = parser.parse_args(argv[1:])
# Parse interpreter from stub file that's not available in Starlark
interpreter = parse_stub(args.stub_file)
if args.interpreter:
interpreter = args.interpreter
par = python_archive.PythonArchive(
main_filename=args.main_filename,
import_roots=args.import_roots,
interpreter=interpreter,
output_filename=args.output_par,
manifest_filename=args.manifest_file,
manifest_root=args.manifest_root,
timestamp=args.timestamp,
zip_safe=args.zip_safe,
)
par.create()