Skip to content

Instantly share code, notes, and snippets.

@ssummer3
Last active June 28, 2018 02:36
Show Gist options
  • Save ssummer3/c366c88de389aad1faaae318a3ba31bd to your computer and use it in GitHub Desktop.
Save ssummer3/c366c88de389aad1faaae318a3ba31bd to your computer and use it in GitHub Desktop.
Local CodeBuild
#! /usr/bin/env python
""" local codebuild implementation
Parses buildspec.yml files, and executes them locally, as per:
http://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec-ref-example
"""
from __future__ import print_function
import functools
import glob
import os
import subprocess
import sys
import tempfile
import yaml
# optimizations and setup
log = print
popen = functools.partial(
subprocess.Popen,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
universal_newlines=True)
yaml_load = functools.partial(
yaml.load, Loader=getattr(yaml, 'CLoader', yaml.Loader))
def parse_buildspec(buildspec_filepath):
""" parse buildspec.yml v0.2
- buildspec_filepath: <buildspec.yml> with path (default "./buildspec.yml")
- returns: (env, phases, artifacts)
"""
with open(buildspec_filepath) as f:
buildspec = yaml_load(f)
assert float(
buildspec.pop('version')) == 0.2, "Unsupported buildspec version"
env = buildspec.pop('env', {}).pop('variables', {})
phases = buildspec.pop('phases', {})
artifacts = buildspec.pop('artifacts', {})
if buildspec:
log("Unsupported buildspec components: {}".format(buildspec))
return env, phases, artifacts
def do_phases(phases_commands, env=None, phase_list=None):
""" execute phases_commands for each phase in phase_list (in order)
The environment and current working directory carry over between phases.
- phases_commands: {phase: [commands]}
- env: {key:value} for environment (default os.environ)
- phase_list: [phases] in run order (default ['install', 'pre_build', 'build', 'post_build'])
- returns: None
"""
env = env or os.environ.copy()
phase_list = phase_list or ('install', 'pre_build', 'build', 'post_build')
cwd = os.getcwd()
os.chdir(os.getenv('CODEBUILD_SRC_DIR', os.curdir))
for current_phase in phase_list:
log("phase: {}".format(current_phase))
if current_phase in phases_commands:
commands = phases_commands.pop(current_phase).pop('commands')
assert isinstance(commands, list), "Phase commands must be a list"
for command in commands:
log("{}: {}".format(current_phase, command))
p = popen(command, env=env)
stdout, _ = p.communicate()
log('\n'.join((stdout, '')))
if p.return_code:
sys.exit(return_code)
os.chdir(cwd)
def do_artifacts(artifacts, target_directory=None):
""" parse artifacts and move them to a designated target_directory
target_directory: "<path>" (default "$CODEBUILD_SRC_DIR}/build"
TODO: the glob parsing is non-trivial and currently incorrect
"""
discard_paths = artifacts.pop('discard-paths', False)
base_directory = artifacts.pop('base-directory', '.')
files = artifacts.pop('files')
assert isinstance(files, list), "Artifact files must be a list"
if artifacts:
log("Unsupported artifact components: {}".format(artifacts))
cwd = os.getcwd()
os.chdir(os.getenv('CODEBUILD_SRC_DIR', os.curdir))
target_directory = target_directory or tempfile.mkdtemp(
prefix='build', dir=os.getcwd())
for file_glob in files:
for file_name in glob.glob(file_glob):
log("artifact: {}".format(file_name))
shutil.copy2(file_name, target_directory)
log("Artifact output: {}".format(target_directory))
os.chdir(cwd)
def main(buildspec_file='buildspec.yml'):
codebuild_src_dir = os.getenv('CODEBUILD_SRC_DIR', os.getcwd())
if os.path.sep not in os.path.normpath(buildspec_file):
buildspec_file = os.path.sep.join((codebuild_src_dir, buildspec_file))
assert os.path.isfile(buildspec_file), "{} not found".format(
buildspec_file)
env, phases, artifacts = parse_buildspec(buildspec_file)
# override default environment
codebuild_env = os.environ.copy()
codebuild_env.update(env)
do_phases(phases, codebuild_env)
do_artifacts(artifacts)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment