From c6cd3ce48cfcf0066798a1cdd8ef2585a5a21e99 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 4 Jun 2021 11:48:50 -0700 Subject: [PATCH] Initial commit --- .gitignore | 4 ++ README.rst | 70 ++++++++++++++++++++++++++++++++ bou.py | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 ++ setup.cfg | 36 +++++++++++++++++ setup.py | 6 +++ 6 files changed, 225 insertions(+) create mode 100644 .gitignore create mode 100644 README.rst create mode 100755 bou.py create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52d2587 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.egg-info +build +dist +index.html diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..ca4a7ae --- /dev/null +++ b/README.rst @@ -0,0 +1,70 @@ +bou +=== + +Bou (pronounced "bow") is a simple builder or task runner which uses a YAML file for task configuration. + +"Bou" is `Afrikaans`_ for "build". + +Installation +------------ + +Install bou with pip: + +.. code-block:: + + $ pip install bou + + +Task Configuration +------------------ + +When run without any parameters, bou will search for a file named ``bou.yaml``, ``bou.yml``, ``build.yaml`` or ``build.yml`` + +Here's a basic example: + +.. code-block:: yaml + + stages: + - build + - test + steps: + build: + stage: build + script: + - make + test: + stage: test + script: + - make test + + +Environment Variables +--------------------- + +Bou also supports setting environment variables, both at a global level, as well as at a step level. As a convenience, +bou sets up an initial environment variable named ``BASE_DIR`` which is the directory the build file is in. + +Environment variables defined at a global level are set first when a step is run, and then the step-level environment +variables are set. + +.. code-block:: yaml + + stages: + - build + environment: + - PYTHON=python3 + steps: + build: + stage: build + environment: + - SOURCE=$BASE_DIR/src + script: + - $PYTHON $SOURCE/setup.py build + + +Source Code +----------- + +The source code to bou is available on my personal Git server: https://git.snyman.info/superfly/bou + +.. _Afrikaans: https://en.wikipedia.org/wiki/Afrikaans diff --git a/bou.py b/bou.py new file mode 100755 index 0000000..f7519be --- /dev/null +++ b/bou.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +import os +import re +import sys +from argparse import ArgumentParser +from pathlib import Path +from subprocess import Popen, PIPE, STDOUT + +import yaml + +BUILD_FILES = ['build.yaml', 'build.yml', 'bou.yaml', 'bou.yml'] +ENV_VAR = re.compile(r'(\$([A-Za-z0-9_]+))') + + +def parse_args(): + parser = ArgumentParser() + parser.add_argument('-f', '--file', dest='build_file', help='Path to the build file') + parser.add_argument('stage_or_step', nargs='?', default=None, help='Run a particular stage or step') + return parser.parse_args() + + +def setup_env(env, base_path): + """Set up the environment dictionary, resolving shell variables""" + if isinstance(env, list): + env = {pair.split('=')[0]: pair.split('=')[1] for pair in env} + env = dict(BASE_DIR=str(base_path), **env) + for key, value in env.items(): + match = ENV_VAR.search(value) + if match: + value = value.replace(match.group(1), env[match.group(2)]) + env[key] = value + return dict(**os.environ, **env) + + +def setup_step(config, step_name): + """Prepare a step for usage""" + step = config['steps'][step_name] + step['environment'] = config.get('environment', []) + step.get('environment', []) + return step + + +def get_steps_for_stage(config, stage_name): + steps = [] + for step_name in config['steps'].keys(): + if config['steps'][step_name]['stage'] == stage_name: + steps.append(setup_step(config, step_name)) + return steps + + +def run_step(step, base_path): + script = step['script'] + if isinstance(script, list): + script = os.linesep.join(script) + env = setup_env(step['environment'], base_path) + proc = Popen([script], shell=True, stdout=PIPE, stderr=STDOUT, env=env) + for output in iter(lambda: proc.stdout.read(1), b''): + sys.stdout.buffer.write(output) + sys.stdout.buffer.flush() + return proc.returncode == 0 + + +def run_stage(config, stage_name, base_path): + for step in get_steps_for_stage(config, stage_name): + result = run_step(step, base_path) + if not result: + break + + +def get_build_file(): + """Determine the local build file""" + base_path = Path.cwd() + for child in base_path.iterdir(): + if child.name in BUILD_FILES: + return child.resolve() + return None + + +def main(): + """Run the build system""" + args = parse_args() + if args.build_file: + build_file = Path(args.build_file).resolve() + else: + build_file = get_build_file() + if not build_file: + print('Could not find a valid build file') + return 1 + base_path = build_file.parent + config = yaml.full_load(build_file.open()) + if args.stage_or_step: + if args.stage_or_step in config['stages']: + run_stage(config, args.stage_or_step, base_path) + elif args.stage_or_step in config['steps'].keys(): + step = setup_step(config, args.stage_or_step) + run_step(config, step, base_path) + else: + print('"{stage}" is not a valid stage or step name'.format(stage=args.stage_or_step)) + return 2 + else: + for stage_name in config['stages']: + run_stage(config, stage_name, base_path) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0803597 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,36 @@ +[metadata] +name = bou +version = 0.0.1 +author = Raoul Snyman +author_email = raoul@snyman.info +description = Simple YAML-driven build or task runner +long_description = file:README.rst +long_description_content_type = text/restructuredtext +url = https://bou-project.org +license = MIT +classifiers = + Development Status :: 3 - Alpha, + Intended Audience :: Developers, + License :: OSI Approved :: MIT License, + Operating System :: POSIX, + Programming Language :: Python :: 3, + Programming Language :: Python :: 3.7, + Programming Language :: Python :: 3.8, + Programming Language :: Python :: 3.9, + Programming Language :: Python :: 3.10, + Topic :: Utilities +keywords = build, task + +[options] +py_modules = bou +python_requires = >=3.7 + +[options.entry_points] +console_scripts = + bou = bou:main + +[wheel] +universal = 1 + +[flake8] +max-line-length = 120 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..50ed9f4 --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +""" +The bou package +""" +from setuptools import setup + +setup()