chromium/third_party/blink/web_tests/external/wpt/docs/frontend.py

import argparse
import logging
import os
import subprocess
import sys

here = os.path.dirname(__file__)
wpt_root = os.path.abspath(os.path.join(here, ".."))

# Directories relative to the wpt root that we want to include in the docs
# Sphinx doesn't support including files outside of docs/ so we temporarily symlink
# these directories under docs/ whilst running the build.
link_dirs = [
    "tools/wptserve",
    "tools/certs",
    "tools/wptrunner",
    "tools/webtransport",
    "tools/third_party/pywebsocket3",
]

logger = logging.getLogger()


def link_source_dirs():
    created = set()
    failed = []
    for rel_path in link_dirs:
        rel_path = rel_path.replace("/", os.path.sep)
        src = os.path.join(wpt_root, rel_path)
        dest = os.path.join(here, rel_path)
        try:
            dest_dir = os.path.dirname(dest)
            if not os.path.exists(dest_dir):
                os.makedirs(dest_dir)
                created.add(dest_dir)
            if not os.path.exists(dest):
                os.symlink(src, dest, target_is_directory=True)
            else:
                if (not os.path.islink(dest) or
                    os.path.join(os.path.dirname(dest), os.readlink(dest)) != src):
                    # The file exists but it isn't a link or points at the wrong target
                    raise OSError("File exists")
        except Exception as e:
            failed.append((dest, e))
        else:
            created.add(dest)
    return created, failed


def unlink_source_dirs(created):
    # Sort backwards in length to remove all files before getting to directory
    for path in sorted(created, key=lambda x: -len(x)):
        # This will also remove empty parent directories
        if not os.path.islink(path) and os.path.isdir(path):
            os.removedirs(path)
        else:
            os.unlink(path)


def get_parser():
    p = argparse.ArgumentParser()
    p.add_argument("--type", default="html", help="Output type (default: html)")
    p.add_argument("--docker", action="store_true", help="Run inside the docs docker image")
    p.add_argument("--serve", default=None, nargs="?", const=8000,
                   type=int, help="Run a server on the specified port (default: 8000)")
    return p


def docker_build(tag="wpt:docs"):
    subprocess.check_call(["docker",
                           "build",
                           "--pull",
                           "--tag", tag,
                           here])

def docker_run(**kwargs):
    cmd = ["docker", "run"]
    cmd.extend(["--mount",
                 "type=bind,source=%s,target=/app/web-platform-tests" % wpt_root])
    if kwargs["serve"] is not None:
        serve = str(kwargs["serve"])
        cmd.extend(["--expose", serve, "--publish", f"{serve}:{serve}"])
    cmd.extend(["-w", "/app/web-platform-tests"])
    if os.isatty(os.isatty(sys.stdout.fileno())):
        cmd.append("-it")
    cmd.extend(["wpt:docs", "./wpt"])
    # /app/venv is created during docker build and is always active inside the
    # container.
    cmd.extend(["--venv", "/app/venv", "--skip-venv-setup"])
    cmd.extend(["build-docs", "--type", kwargs["type"]])
    if kwargs["serve"] is not None:
        cmd.extend(["--serve", str(kwargs["serve"])])
    logger.debug(" ".join(cmd))
    return subprocess.call(cmd)


def build(_venv, **kwargs):
    if kwargs["docker"]:
        docker_build()
        return docker_run(**kwargs)

    out_dir = os.path.join(here, "_build")
    try:
        created, failed = link_source_dirs()
        if failed:
            failure_msg = "\n".join(f"{dest}: {err}" for (dest, err) in failed)
            logger.error(f"Failed to create source symlinks:\n{failure_msg}")
            sys.exit(1)
        if kwargs["serve"] is not None:
            executable = "sphinx-autobuild"
            extras = ["--port", str(kwargs["serve"]),
                      "--host", "0.0.0.0",
                      "--watch", os.path.abspath(os.path.join(here, os.pardir, "resources")),
                      # Ignore changes to files specified with glob pattern
                      "--ignore", "**/flycheck_*",
                      "--ignore", "**/.*",
                      "--ignore", "**/#*",
                      "--ignore", "docs/frontend.py",
                      "--ignore", "docs/Dockerfile"]
        else:
            executable = "sphinx-build"
            extras = []
        cmd = [executable, "-n", "-v", "-b", kwargs["type"], "-j", "auto"] + extras + [here, out_dir]
        logger.debug(" ".join(cmd))
        subprocess.check_call(cmd)
    finally:
        unlink_source_dirs(created)