blob: 1a7d88d85ab706dc2ead23119388159bc32c2884 [file] [log] [blame]
#!/usr/bin/env python
from __future__ import print_function
import os
import sys
import json
import shutil
import tempfile
import optparse
import zipfile
# Try to detect if we're running from source via the buck repo by
# looking for the .arcconfig file. If found, add the appropriate
# deps to our python path, so we can find the twitter libs and
# setuptools at runtime. Also, locate the `pkg_resources` modules
# via our local setuptools import.
if not zipfile.is_zipfile(sys.argv[0]):
buck_root = os.sep.join(__file__.split(os.sep)[:-6])
sys.path.insert(0, os.path.join(
buck_root,
'third-party/py/twitter-commons/src/python'))
sys.path.insert(0, os.path.join(
buck_root, 'third-party/py/setuptools'))
pkg_resources_py = os.path.join(
buck_root,
'third-party/py/setuptools/pkg_resources.py')
# Otherwise, we're running from a PEX, so import the `pkg_resources`
# module via a resource.
else:
import pkg_resources
pkg_resources_py_tmp = tempfile.NamedTemporaryFile(
prefix='pkg_resources.py')
pkg_resources_py_tmp.write(
pkg_resources.resource_string(__name__, 'pkg_resources.py'))
pkg_resources_py_tmp.flush()
pkg_resources_py = pkg_resources_py_tmp.name
from twitter.common.python.pex_builder import PEXBuilder
from twitter.common.python.interpreter import PythonInterpreter
def dereference_symlinks(src):
"""
Resolve all symbolic references that `src` points to. Note that this
is different than `os.path.realpath` as path components leading up to
the final location may still be symbolic links.
"""
while os.path.islink(src):
src = os.path.join(os.path.dirname(src), os.readlink(src))
return src
def main():
parser = optparse.OptionParser(usage="usage: %prog [options] output")
parser.add_option('--entry-point', default='__main__')
parser.add_option('--python', default=sys.executable)
options, args = parser.parse_args()
if len(args) == 1:
output = args[0]
else:
parser.error("'output' positional argument is required")
return 1
# The manifest is passed via stdin, as it can sometimes get too large
# to be passed as a CLA.
manifest = json.load(sys.stdin)
# Setup a temp dir that the PEX builder will use as its scratch dir.
tmp_dir = tempfile.mkdtemp()
try:
# The version of pkg_resources.py (from setuptools) on some distros is
# too old for PEX. So we keep a recent version in the buck repo and
# force it into the process by constructing a custom PythonInterpreter
# instance using it.
interpreter = PythonInterpreter(
options.python,
PythonInterpreter.from_binary(options.python).identity,
extras={})
pex_builder = PEXBuilder(
path=tmp_dir,
interpreter=interpreter,
)
# Mark this PEX as zip-safe, meaning everything will stayed zipped up
# and we'll rely on python's zip-import mechanism to load modules from
# the PEX. This may not work in some situations (e.g. native
# libraries, libraries that want to find resources via the FS), so
# we'll want to revisit this.
pex_builder.info.zip_safe = True
# Set the starting point for this PEX.
pex_builder.info.entry_point = options.entry_point
# Copy in our version of `pkg_resources`.
pex_builder.add_source(
dereference_symlinks(pkg_resources_py),
os.path.join(pex_builder.BOOTSTRAP_DIR, 'pkg_resources.py'))
# Add the sources listed in the manifest.
for dst, src in manifest['modules'].iteritems():
# NOTE(agallagher): calls the `add_source` and `add_resource` below
# hard-link the given source into the PEX temp dir. Since OS X and
# Linux behave different when hard-linking a source that is a
# symbolic link (Linux does *not* follow symlinks), resolve any
# layers of symlinks here to get consistent behavior.
try:
pex_builder.add_source(dereference_symlinks(src), dst)
except OSError as e:
raise Exception("Failed to add {}: {}".format(src, e))
# Add resources listed in the manifest.
for dst, src in manifest['resources'].iteritems():
# NOTE(agallagher): see rationale above.
pex_builder.add_resource(dereference_symlinks(src), dst)
# Generate the PEX file.
pex_builder.build(output)
# Always try cleaning up the scratch dir, ignoring failures.
finally:
shutil.rmtree(tmp_dir, True)
sys.exit(main())