Initial commit

This commit is contained in:
Raoul Snyman 2021-06-04 11:48:50 -07:00
parent 54ac6f1e2e
commit c6cd3ce48c
Signed by: raoul
GPG Key ID: F55BCED79626AE9C
6 changed files with 225 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.egg-info
build
dist
index.html

70
README.rst Normal file
View File

@ -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

106
bou.py Executable file
View File

@ -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())

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

36
setup.cfg Normal file
View File

@ -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

6
setup.py Normal file
View File

@ -0,0 +1,6 @@
"""
The bou package
"""
from setuptools import setup
setup()