mirror of
https://github.com/kennethreitz/bob-builder-1.git
synced 2026-06-05 23:10:17 +00:00
00a5fc7f0b
when hitting Ctrl+C in a 'docker run bob …', the main script itself actually can't be terminated by Ctrl+C, because it's PID 1 in that case, rather than just shutting down silently (by terminating itself using a SIGINT), we will see the subprocess exit (because Ctrl+C is sent to the whole process group) When this happens, the return code of the process will be negative, to indicate that it didn't exit with that code, but instead got terminated by a signal of that (absolute) number
198 lines
6.7 KiB
Python
198 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
import re
|
|
import shutil
|
|
import signal
|
|
import sys
|
|
from tempfile import mkstemp, mkdtemp
|
|
from subprocess import Popen
|
|
|
|
from .utils import (
|
|
archive_tree, extract_tree, get_with_wildcard, iter_marker_lines, mkdir_p,
|
|
print_stderr, S3ConnectionHandler)
|
|
|
|
|
|
WORKSPACE = os.environ.get('WORKSPACE_DIR', 'workspace')
|
|
DEFAULT_BUILD_PATH = os.environ.get('DEFAULT_BUILD_PATH', '/app/.heroku/')
|
|
S3_BUCKET = os.environ.get('S3_BUCKET')
|
|
S3_PREFIX = os.environ.get('S3_PREFIX', '')
|
|
UPSTREAM_S3_BUCKET = os.environ.get('UPSTREAM_S3_BUCKET')
|
|
UPSTREAM_S3_PREFIX = os.environ.get('UPSTREAM_S3_PREFIX', '')
|
|
|
|
# Append a slash for backwards compatibility.
|
|
if S3_PREFIX and not S3_PREFIX.endswith('/'):
|
|
S3_PREFIX = '{0}/'.format(S3_PREFIX)
|
|
if UPSTREAM_S3_PREFIX and not UPSTREAM_S3_PREFIX.endswith('/'):
|
|
UPSTREAM_S3_PREFIX = '{0}/'.format(UPSTREAM_S3_PREFIX)
|
|
|
|
DEPS_MARKER = '# Build Deps: '
|
|
BUILD_PATH_MARKER = '# Build Path: '
|
|
|
|
class Formula(object):
|
|
|
|
def __init__(self, path, override_path=None):
|
|
self.path = path
|
|
self.archived_path = None
|
|
self.override_path = override_path
|
|
|
|
if not S3_BUCKET:
|
|
print_stderr('The environment variable S3_BUCKET must be set to the bucket name.', title='ERROR')
|
|
sys.exit(1)
|
|
|
|
s3 = S3ConnectionHandler()
|
|
self.bucket = s3.get_bucket(S3_BUCKET)
|
|
self.upstream = s3.get_bucket(UPSTREAM_S3_BUCKET) if UPSTREAM_S3_BUCKET else None
|
|
|
|
def __repr__(self):
|
|
return '<Formula {}>'.format(self.path)
|
|
|
|
@property
|
|
def workspace_path(self):
|
|
return os.path.join(WORKSPACE, self.path)
|
|
|
|
@property
|
|
def full_path(self):
|
|
return os.path.abspath(self.workspace_path)
|
|
|
|
@property
|
|
def exists(self):
|
|
"""Returns True if the forumla appears to exist."""
|
|
return os.path.exists(self.workspace_path)
|
|
|
|
@property
|
|
def depends_on(self):
|
|
"""Extracts a list of declared dependencies from a given formula."""
|
|
# Depends: libraries/libsqlite, libraries/libsqlite
|
|
|
|
depends = []
|
|
|
|
for result in iter_marker_lines(DEPS_MARKER, self.full_path):
|
|
# Split on both space and comma.
|
|
result = re.split(r'[ ,]+', result)
|
|
depends.extend(result)
|
|
|
|
return depends
|
|
|
|
@property
|
|
def build_path(self):
|
|
"""Extracts a declared build path from a given formula."""
|
|
|
|
for result in iter_marker_lines(BUILD_PATH_MARKER, self.full_path):
|
|
return result
|
|
|
|
# If none was provided, fallback to default.
|
|
return DEFAULT_BUILD_PATH
|
|
|
|
def resolve_deps(self):
|
|
|
|
# Dependency metadata, extracted from bash comments.
|
|
deps = self.depends_on
|
|
|
|
if deps:
|
|
print_stderr('Fetching dependencies... found {}:'.format(len(deps)))
|
|
|
|
for dep in deps:
|
|
print_stderr(' - {}'.format(dep))
|
|
|
|
key_name = '{}{}.tar.gz'.format(S3_PREFIX, dep)
|
|
key = get_with_wildcard(self.bucket, key_name)
|
|
|
|
if not key and self.upstream:
|
|
print_stderr(' Not found in S3_BUCKET, trying UPSTREAM_S3_BUCKET...')
|
|
key_name = '{}{}.tar.gz'.format(UPSTREAM_S3_PREFIX, dep)
|
|
key = get_with_wildcard(self.upstream, key_name)
|
|
|
|
if not key:
|
|
print_stderr('Archive {} does not exist.\n'
|
|
'Please deploy it to continue.'.format(key_name), title='ERROR')
|
|
sys.exit(1)
|
|
|
|
# Grab the Dep from S3, download it to a temp file.
|
|
archive = mkstemp(prefix='bob-dep-', suffix='.tar.gz')[1]
|
|
key.get_contents_to_filename(archive)
|
|
|
|
# Extract the Dep to the appropriate location.
|
|
extract_tree(archive, self.build_path)
|
|
|
|
print_stderr()
|
|
|
|
def build(self):
|
|
# Prepare build directory.
|
|
if os.path.exists(self.build_path):
|
|
shutil.rmtree(self.build_path)
|
|
mkdir_p(self.build_path)
|
|
|
|
self.resolve_deps()
|
|
|
|
# Temporary directory where work will be carried out, because of David.
|
|
cwd_path = mkdtemp(prefix='bob-')
|
|
|
|
print_stderr('Building formula {} in {}:\n'.format(self.path, cwd_path))
|
|
|
|
# Execute the formula script.
|
|
args = ["/usr/bin/env", "bash", "--", self.full_path, self.build_path]
|
|
if self.override_path != None:
|
|
args.append(self.override_path)
|
|
|
|
p = Popen(args, cwd=cwd_path, shell=False, stderr=sys.stdout.fileno()) # we have to pass sys.stdout.fileno(), because subprocess.STDOUT will not do what we want on older versions: https://bugs.python.org/issue22274
|
|
|
|
p.wait()
|
|
|
|
if p.returncode > 0:
|
|
print_stderr('Formula exited with return code {}.'.format(p.returncode), title='ERROR')
|
|
sys.exit(1)
|
|
elif p.returncode < 0: # script was terminated by signal number abs(returncode)
|
|
signum = abs(p.returncode)
|
|
try:
|
|
# Python 3.5+
|
|
signame = signal.Signals(signum).name
|
|
except AttributeError:
|
|
signame = signum
|
|
print_stderr('Formula terminated by signal {}.'.format(signame), title='ERROR')
|
|
sys.exit(128+signum) # best we can do, given how we weren't terminated ourselves with the same signal (maybe we're PID 1, maybe another reason)
|
|
|
|
print_stderr('\nBuild complete: {}'.format(self.build_path))
|
|
|
|
def archive(self):
|
|
"""Archives the build directory as a tar.gz."""
|
|
archive = mkstemp(prefix='bob-build-', suffix='.tar.gz')[1]
|
|
archive_tree(self.build_path, archive)
|
|
|
|
print_stderr('Created: {}'.format(archive))
|
|
self.archived_path = archive
|
|
|
|
def deploy(self, allow_overwrite=False):
|
|
"""Deploys the formula's archive to S3."""
|
|
assert self.archived_path
|
|
|
|
if self.bucket.connection.anon:
|
|
print_stderr('Deploy requires valid AWS credentials.', title='ERROR')
|
|
sys.exit(1)
|
|
|
|
if self.override_path != None:
|
|
name = self.override_path
|
|
else:
|
|
name = self.path
|
|
|
|
key_name = '{}{}.tar.gz'.format(S3_PREFIX, name)
|
|
|
|
key = self.bucket.get_key(key_name)
|
|
|
|
if key:
|
|
if not allow_overwrite:
|
|
print_stderr('Archive {} already exists.\n'
|
|
'Use the --overwrite flag to continue.'.format(key_name), title='ERROR')
|
|
sys.exit(1)
|
|
else:
|
|
key = self.bucket.new_key(key_name)
|
|
|
|
url = key.generate_url(0, query_auth=False)
|
|
print_stderr('Uploading to: {}'.format(url))
|
|
|
|
# Upload the archive, set permissions.
|
|
key.set_contents_from_filename(self.archived_path)
|
|
key.set_acl('public-read')
|
|
|
|
print_stderr('Upload complete!')
|