blob: b497a6b03078719a80aab2f27178ef72843c70c5 [file] [log] [blame]
#!/usr/bin/python
#
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Creates a self-extracting Python executable."""
DESCRIPTION = """Creates a self-extracting Python executable.
The generated executable contains a copy of the entire factory code
and can be used to executed any "well-behaved" executable, in
particular the shopfloor server and tools like mount_partition. Simply
generate an output file whose name is the same as any symlink in the bin
directory, or create a symlink to the generated archive with such a name.
For instance:
make_par -o shopfloor_server
# ./shopfloor_server is now a fully-functional, standalone shopfloor server
# including all dependencies.
or:
make_par # Generates factory.par
ln -s factory.par shopfloor_server
ln -s factory.par mount_partition
# You can now run either ./shopfloor_server or ./mount_partition.
"""
import argparse
import glob
import logging
import os
import re
import shutil
import subprocess
import sys
import tempfile
from distutils.sysconfig import get_python_lib
import factory_common # pylint: disable=W0611
from cros.factory.test import factory
from cros.factory.utils.process_utils import Spawn, SpawnOutput
# Template for the header that will be placed before the ZIP file to
# execute a script based on the name which is used to execute it. The
# literal string "MODULES" is replaced with a map from executable name
# to fully-qualified module name (e.g., {'mount_partition':
# 'cros.factory.tools.mount_partition'}).
HEADER_TEMPLATE = """#!/bin/sh
# This par file.
par="$(readlink -f $0)"
# The directory this par file is in.
dir="$(dirname "$par")"
if [ -e $dir/factory_common.py ]; then
# The .par file has been expanded. Print a warning and use the expanded
# file.
echo WARNING: factory.par has been unzipped. Using the unzipped files. >& 2
export PYTHONPATH="$dir":"$PYTHONPATH"
else
export PYTHONPATH="$par":"$PYTHONPATH"
fi
export PYTHONIOENCODING=utf-8
exec python -c \
"import os, runpy, sys
# Remove '-c' from argument list.
sys.argv = sys.argv[1:]
# List of modules, based on symlinks in 'bin' when make_par was run.
# The actual list is substituted in by the make_par script.
modules = MODULES
# Use the name we were executed as to determine which module to run.
if sys.argv[0].endswith('.par') and len(sys.argv) > 1:
# Not run as a symlink; remove argv[0]. This allows you to run, e.g.,
# factory.par gooftool probe.
sys.argv = sys.argv[1:]
name = os.path.basename(sys.argv[0])
module = modules.get(name)
# Set the process title, if available
try:
from setproctitle import setproctitle
setproctitle(' '.join(sys.argv))
except:
pass # Oh well
# Set process title so killall/pkill will work.
try:
import ctypes
buff = ctypes.create_string_buffer(len(name) + 1)
buff.value = name
ctypes.cdll.LoadLibrary('libc.so.6').prctl(15, ctypes.byref(buff), 0, 0, 0)
except:
pass # Oh well
if not module:
# Display an error message describing the valid commands.
print >>sys.stderr, (
'Unable to run %s. To run a file within this archive,' % name)
print >>sys.stderr, (
'rename it to (or create a symbolic link from) one of the following:')
print >>sys.stderr
for m in sorted(modules.keys()):
print >>sys.stderr, ' %s' % m
sys.exit(1)
runpy.run_module(module, run_name='__main__')" "$0" "$@"
echo Unable to exec "$*" >& 2
exit 1
# ZIP file follows...
"""
def main(argv=None):
parser = argparse.ArgumentParser(
description=DESCRIPTION,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--verbose', '-v', action='count')
parser.add_argument(
'--output', '-o', metavar='FILE', default='factory.par',
help='output file (default: %(default)s')
parser.add_argument(
'--add-zip', action='append', default=[],
help='zip files containing extra files to include')
parser.add_argument(
'--mini', action='store_true',
help='Build smaller version suitable for installation into test image')
args = parser.parse_args(argv)
logging.basicConfig(level=logging.WARNING - 10 * (args.verbose or 0))
tmp = os.path.realpath(tempfile.mkdtemp(prefix='make_par.'))
try:
par_build = os.path.join(tmp, 'par_build')
os.mkdir(par_build)
# Copy our py sources and bins, and any overlays, into the src
# directory.
src = os.path.join(tmp, 'src')
os.mkdir(src)
Spawn(['rsync', '-a',
'--exclude', 'testdata',
os.path.join(factory.FACTORY_PATH, 'py'),
os.path.join(factory.FACTORY_PATH, 'bin'),
src],
log=True, check_call=True)
# Add files from overlay.
for f in args.add_zip:
Spawn(['unzip', '-oq', f, '-d', src],
log=True, check_call=True)
cros = os.path.join(par_build, 'cros')
os.mkdir(cros)
rsync_args = ['rsync', '-a',
'--exclude', '*_unittest.py',
'--exclude', 'factory_common.py*',
'--include', '*.py']
if args.mini:
# Exclude some piggy directories we'll never need for the mini
# par, since we will not run these things in a test image.
rsync_args.extend(['--exclude', 'static',
'--exclude', 'testdata',
'--exclude', 'goofy',
'--exclude', 'goofy_monolithic',
'--exclude', 'goofy_split',
'--exclude', 'minijack',
'--exclude', 'pytests'])
else:
rsync_args.extend(['--include', '*.css',
'--include', '*.csv',
'--include', '*.html',
'--include', '*.js',
'--include', '*.png',
# Config file templates needed by the factory_flow
# tool.
'--include', 'factory_flow/templates/*',
# We must include goofy explicitly, as it is
# a symlink that would otherwise be excluded
# by the * wildcard.
'--include', 'goofy'])
rsync_args.extend([
'--include', '*/',
'--exclude', '*',
os.path.join(src, 'py/'),
os.path.join(cros, 'factory')])
Spawn(rsync_args, log=True, check_call=True)
# Copy necessary third-party packages.
python_lib = get_python_lib()
standard_lib = get_python_lib(plat_specific=False, standard_lib=True)
rsync_args = ['rsync', '-a',
os.path.join(standard_lib, 'argparse.py'),
'third_party/jsonrpclib/jsonrpclib']
# No need for third-party deps in the mini version; they are already
# present in test images.
if not args.mini:
rsync_args.extend([os.path.join(python_lib, 'google'),
os.path.join(python_lib, 'yaml')])
rsync_args.append(par_build)
Spawn(rsync_args, log=True, check_call=True, cwd=factory.FACTORY_PATH)
# Add empty __init__.py files so Python realizes these directories
# are modules.
open(os.path.join(cros, '__init__.py'), 'w')
open(os.path.join(cros, 'factory', '__init__.py'), 'w')
# Add an empty factory_common file (since many scripts import
# factory_common).
open(os.path.join(par_build, 'factory_common.py'), 'w')
# Zip 'em up!
factory_par = os.path.join(tmp, 'factory.par')
Spawn(['zip', '-qr', factory_par, '.'],
check_call=True, log=True, cwd=par_build)
# Build a map of runnable modules based on symlinks in bin.
modules = {}
bin_dir = os.path.join(src, 'bin')
for f in glob.glob(os.path.join(bin_dir, '*')):
if not os.path.islink(f):
continue
dest = os.readlink(f)
match = re.match('\.\./py/(.+)\.py$', dest)
if not match:
continue
module = 'cros.factory.%s' % match.group(1).replace('/', '.')
name = module.rpartition('.')[2]
logging.info('Mapping binary %s to %s', name, module)
modules[name] = module
# Also map the name of symlink to binary.
link_name = os.path.basename(f)
if link_name != name:
logging.info('Mapping binary %s to %s', link_name, module)
modules[link_name] = module
# Concatenate the header and the par file.
with open(args.output, 'wb') as out:
out.write(HEADER_TEMPLATE.replace('MODULES', repr(modules)))
shutil.copyfileobj(open(factory_par), out)
os.fchmod(out.fileno(), 0755)
# Done!
print 'Created %s (%d bytes)' % (args.output, os.path.getsize(args.output))
# Sanity check: make sure we can run 'make_par --help' within the
# archive, in a totally clean environment, and see the help
# message.
try:
link = os.path.join(tmp, 'make_par')
os.symlink(os.path.realpath(args.output), link)
output = SpawnOutput(
[link, '--help'], env={}, cwd='/',
check_call=True, read_stdout=True,log_stderr_on_error=True)
if 'show this help message and exit' not in output:
logging.error('Unable to run "make_par --help": output is %r', output)
except subprocess.CalledProcessError:
logging.error('Unable to run "make_par --help" within the .par file')
return False
return True
finally:
shutil.rmtree(tmp)
if __name__ == '__main__':
sys.exit(0 if main() else 1)