diff options
Diffstat (limited to 'doc/bst2html.py')
-rwxr-xr-x | doc/bst2html.py | 303 |
1 files changed, 188 insertions, 115 deletions
diff --git a/doc/bst2html.py b/doc/bst2html.py index 33e86e05c..9c5aaaf9a 100755 --- a/doc/bst2html.py +++ b/doc/bst2html.py @@ -25,13 +25,18 @@ # https://github.com/Kronuz/ansi2html.git # import os +import sys import re +import shlex import subprocess +from collections import Mapping +from contextlib import contextmanager from tempfile import TemporaryDirectory import click from buildstream import _yaml +from buildstream._exceptions import BstError _ANSI2HTML_STYLES = {} @@ -169,20 +174,163 @@ def ansi2html(text, palette='solarized'): return sub -# FIXME: Workaround a setuptools bug which fails to include symbolic -# links in the source distribution. +# workdir() # -# Remove this hack once setuptools is fixed -def workaround_setuptools_bug(project): - os.makedirs(os.path.join(project, "files", "links"), exist_ok=True) - try: - os.symlink(os.path.join("usr", "lib"), os.path.join(project, "files", "links", "lib")) - os.symlink(os.path.join("usr", "bin"), os.path.join(project, "files", "links", "bin")) - os.symlink(os.path.join("usr", "etc"), os.path.join(project, "files", "links", "etc")) - except FileExistsError: - # If the files exist, we're running from a git checkout and - # not a source distribution, no need to complain - pass +# Sets up a new temp directory with a config file +# +# Args: +# work_directory (str): The directory where to create a tempdir first +# source_cache (str): The directory of a source cache to share with, or None +# +# Yields: +# The buildstream.conf full path +# +@contextmanager +def workdir(source_cache=None): + with TemporaryDirectory(prefix='run-bst-', dir=os.getcwd()) as tempdir: + + bst_config_file = os.path.join(tempdir, 'buildstream.conf') + config = { + 'sourcedir': source_cache, + 'artifactdir': os.path.join(tempdir, 'artifacts'), + 'logdir': os.path.join(tempdir, 'logs'), + 'builddir': os.path.join(tempdir, 'build'), + } + _yaml.dump(config, bst_config_file) + + yield (tempdir, bst_config_file) + + +# run_command() +# +# Runs a command +# +# Args: +# config_file (str): The path to the config file to use +# directory (str): The project directory +# command (str): A command string +# +# Returns: +# (str): The colorized combined stdout/stderr of BuildStream +# +def run_command(config_file, directory, command): + click.echo("Running command in directory '{}': bst {}".format(directory, command), err=True) + + argv = ['bst', '--colors', '--config', config_file] + shlex.split(command) + p = subprocess.Popen(argv, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, _ = p.communicate() + return out.decode('utf-8').strip() + + +# generate_html +# +# Generate html based on the output +# +# Args: +# output (str): The output of the BuildStream command +# directory (str): The project directory +# config_file (str): The config file +# source_cache (str): The source cache +# tempdir (str): The base work directory +# palette (str): The rendering color style +# command (str): The command +# +# Returns: +# (str): The html formatted output +# +def generate_html(output, directory, config_file, source_cache, tempdir, palette, command): + + test_base_name = os.path.basename(directory) + show_command = 'bst ' + command + + # Substitute some things we want normalized for the docs + output = re.sub(os.environ.get('HOME'), '/home/user', output) + output = re.sub(config_file, '/home/user/.config/buildstream.conf', output) + output = re.sub(source_cache, '/home/user/.cache/buildstream/sources', output) + output = re.sub(tempdir, '/home/user/.cache/buildstream', output) + output = re.sub(directory, '/home/user/{}'.format(test_base_name), output) + + # Now convert to HTML and add some surrounding sugar + output = ansi2html(output, palette=palette) + + # Finally format it nicely into a <div> + output = '<!--\n' + \ + ' WARNING: This file was generated with bst2html.py\n' + \ + '-->\n' + \ + '<div class="highlight" style="font-size:x-small">' + \ + '<pre>\n' + \ + '<span style="color:#C4A000;font-weight:bold">user@host</span>:' + \ + '<span style="color:#3456A4;font-weight:bold">~/{}</span>$ '.format(test_base_name) + \ + show_command + '\n\n' + \ + output + '\n' + \ + '</pre></div>\n' + + return output + + +def run_session(description, tempdir, source_cache, palette, config_file): + + desc = _yaml.load(description, shortname=os.path.basename(description)) + desc_dir = os.path.dirname(description) + + # FIXME: Workaround a setuptools bug where the symlinks + # we store in git dont get carried into a release + # tarball. This workaround lets us build docs from + # a source distribution tarball. + # + symlinks = _yaml.node_get(desc, Mapping, 'workaround-symlinks', default_value={}) + for symlink, target in _yaml.node_items(symlinks): + + # Resolve real path to where symlink should be + symlink = os.path.join(desc_dir, symlink) + + # Ensure dir exists + symlink_dir = os.path.dirname(symlink) + os.makedirs(symlink_dir, exist_ok=True) + + click.echo("Generating symlink at: {} (target: {})".format(symlink, target), err=True) + + # Generate a symlink + try: + os.symlink(target, symlink) + except FileExistsError: + # If the files exist, we're running from a git checkout and + # not a source distribution, no need to complain + pass + + # Run commands + # + commands = _yaml.node_get(desc, list, 'commands') + for c in commands: + command = _yaml.node_get(desc, Mapping, 'commands', indices=[commands.index(c)]) + + # Get the directory where this command should be run + directory = _yaml.node_get(command, str, 'directory') + directory = os.path.join(desc_dir, directory) + directory = os.path.realpath(directory) + + # Run the command + command_str = _yaml.node_get(command, str, 'command') + command_out = run_command(config_file, directory, command_str) + + # Encode and save the output if that was asked for + output = _yaml.node_get(command, str, 'output', default_value=None) + if output is not None: + + # Convert / Generate a nice <div> + converted = generate_html(command_out, directory, config_file, + source_cache, tempdir, palette, + command_str) + + # Save it + filename = os.path.join(desc_dir, output) + filename = os.path.realpath(filename) + output_dir = os.path.dirname(filename) + os.makedirs(output_dir, exist_ok=True) + with open(filename, 'wb') as f: + f.write(converted.encode('utf-8')) + + click.echo("Saved session at '{}'".format(filename), err=True) @click.command(short_help="Run a bst command and capture stdout/stderr in html") @@ -205,121 +353,42 @@ def workaround_setuptools_bug(project): def run_bst(directory, source_cache, description, palette, output, command): """Run a bst command and capture stdout/stderr in html - This command normally takes a description yaml file, the format - of that file is as follows: - - \b - # A relative path to the project, from the description file itself - directory: path/to/project - - \b - # A list of commands to run in preparation - prepare-commands: - - fetch hello.bst - - \b - # The command to generate html output for - command: build hello.bst + This command normally takes a description yaml file, see the HACKING + file for information on it's format. """ - prepare_commands = [] - - if description: - desc = _yaml.load(description, shortname=os.path.basename(description)) - desc_dir = os.path.dirname(description) + if not source_cache and os.environ.get('BST_SOURCE_CACHE'): + source_cache = os.environ['BST_SOURCE_CACHE'] - command_str = _yaml.node_get(desc, str, 'command') - command = command_str.split() + with workdir(source_cache=source_cache) as (tempdir, config_file): - # The directory should be relative to where the description file was - # stored - directory_str = _yaml.node_get(desc, str, 'directory') - directory = os.path.join(desc_dir, directory_str) - directory = os.path.realpath(directory) - - prepare = _yaml.node_get(desc, list, 'prepare-commands', default_value=[]) - for prepare_command in prepare: - prepare_commands.append(prepare_command) + if not source_cache: + source_cache = os.path.join(tempdir, 'sources') - else: - if not command: - command = [] + if description: + run_session(description, tempdir, source_cache, palette, config_file) + return + # Run a command specified on the CLI + # if not directory: directory = os.getcwd() else: directory = os.path.abspath(directory) directory = os.path.realpath(directory) - # FIXME: Here we just setup a files/links subdir with - # the symlinks we want for usrmerge, ideally - # we dont need this anymore once setuptools gets - # fixed. - # - workaround_setuptools_bug(directory) - - test_base_name = os.path.basename(directory) - - if not source_cache and os.environ.get('BST_SOURCE_CACHE'): - source_cache = os.environ['BST_SOURCE_CACHE'] - - with TemporaryDirectory(prefix='run-bst-', dir=directory) as tempdir: - bst_config_file = os.path.join(tempdir, 'buildstream.conf') - final_command = ['bst', '--colors', '--config', bst_config_file] - final_command += command - - show_command = ['bst'] - show_command += command - show_command_string = ' '.join(show_command) - - if not source_cache: - source_cache = os.path.join(tempdir, 'sources') - - config = { - 'sourcedir': source_cache, - 'artifactdir': os.path.join(tempdir, 'artifacts'), - 'logdir': os.path.join(tempdir, 'logs'), - 'builddir': os.path.join(tempdir, 'build'), - } - _yaml.dump(config, bst_config_file) + if not command: + command = [] + command_str = " ".join(command) - # Run some prepare commands if they were specified + # Run the command # - for prepare_command_str in prepare_commands: - prepare_command = ['bst', '--config', bst_config_file] + prepare_command_str.split() - p = subprocess.Popen(prepare_command, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - _, _ = p.communicate() + command_out = run_command(config_file, directory, command_str) - # Run BuildStream and collect the output in a single string, - # with the ANSI escape sequences forced enabled. + # Generate a nice html div for this output # - p = subprocess.Popen(final_command, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - out, _ = p.communicate() - decoded = out.decode('utf-8').strip() - - # Substitute some things we want normalized for the docs - decoded = re.sub(os.environ.get('HOME'), '/home/user', decoded) - decoded = re.sub(bst_config_file, '/home/user/.config/buildstream.conf', decoded) - decoded = re.sub(source_cache, '/home/user/.cache/buildstream/sources', decoded) - decoded = re.sub(tempdir, '/home/user/.cache/buildstream', decoded) - decoded = re.sub(directory, '/home/user/{}'.format(test_base_name), decoded) - - # Now convert to HTML and add some surrounding sugar - div_style = 'font-size:x-small' - converted = ansi2html(decoded, palette=palette) - converted = '<div class="highlight" style="{}">'.format(div_style) + \ - '<pre>\n' + \ - '<span style="color:#C4A000;font-weight:bold">user@host</span>:' + \ - '<span style="color:#3456A4;font-weight:bold">~/{}</span>$ '.format(test_base_name) + \ - show_command_string + '\n\n' + \ - converted + '\n' + \ - '</pre></div>\n' - - # Prepend a warning - # - converted = '<!--\n' + \ - ' WARNING: This file was generated with bst2html.py\n' + \ - '-->\n' + \ - converted + converted = generate_html(command_out, directory, config_file, + source_cache, tempdir, palette, + command_str) if output is None: click.echo(converted) @@ -329,7 +398,11 @@ def run_bst(directory, source_cache, description, palette, output, command): with open(output, 'wb') as f: f.write(converted.encode('utf-8')) - return p.returncode + return 0 if __name__ == '__main__': - run_bst() + try: + run_bst() + except BstError as e: + click.echo("Error: {}".format(e), err=True) + sys.exit(-1) |