frontend.py (4632B)
1 import argparse 2 import logging 3 import os 4 import subprocess 5 import sys 6 7 here = os.path.dirname(__file__) 8 wpt_root = os.path.abspath(os.path.join(here, "..")) 9 10 # Directories relative to the wpt root that we want to include in the docs 11 # Sphinx doesn't support including files outside of docs/ so we temporarily symlink 12 # these directories under docs/ whilst running the build. 13 link_dirs = [ 14 "tools/wptserve", 15 "tools/certs", 16 "tools/wptrunner", 17 "tools/webtransport", 18 "tools/third_party/pywebsocket3", 19 ] 20 21 logger = logging.getLogger() 22 23 24 def link_source_dirs(): 25 created = set() 26 failed = [] 27 for rel_path in link_dirs: 28 rel_path = rel_path.replace("/", os.path.sep) 29 src = os.path.join(wpt_root, rel_path) 30 dest = os.path.join(here, rel_path) 31 try: 32 dest_dir = os.path.dirname(dest) 33 if not os.path.exists(dest_dir): 34 os.makedirs(dest_dir) 35 created.add(dest_dir) 36 if not os.path.exists(dest): 37 os.symlink(src, dest, target_is_directory=True) 38 else: 39 if (not os.path.islink(dest) or 40 os.path.join(os.path.dirname(dest), os.readlink(dest)) != src): 41 # The file exists but it isn't a link or points at the wrong target 42 raise OSError("File exists") 43 except Exception as e: 44 failed.append((dest, e)) 45 else: 46 created.add(dest) 47 return created, failed 48 49 50 def unlink_source_dirs(created): 51 # Sort backwards in length to remove all files before getting to directory 52 for path in sorted(created, key=lambda x: -len(x)): 53 # This will also remove empty parent directories 54 if not os.path.islink(path) and os.path.isdir(path): 55 os.removedirs(path) 56 else: 57 os.unlink(path) 58 59 60 def get_parser(): 61 p = argparse.ArgumentParser() 62 p.add_argument("--type", default="html", help="Output type (default: html)") 63 p.add_argument("--docker", action="store_true", 64 help="Run inside the docs docker image") 65 p.add_argument("--serve", nargs="?", const=8000, 66 type=int, help="Run a server on the specified port (default: 8000)") 67 return p 68 69 70 def docker_build(tag="wpt:docs"): 71 subprocess.check_call(["docker", 72 "build", 73 "--pull", 74 "--tag", tag, 75 here]) 76 77 def docker_run(**kwargs): 78 cmd = ["docker", "run"] 79 cmd.extend(["--mount", 80 "type=bind,source=%s,target=/app/web-platform-tests" % wpt_root]) 81 if kwargs["serve"] is not None: 82 serve = str(kwargs["serve"]) 83 cmd.extend(["--expose", serve, "--publish", f"{serve}:{serve}"]) 84 cmd.extend(["-w", "/app/web-platform-tests"]) 85 if os.isatty(os.isatty(sys.stdout.fileno())): 86 cmd.append("-it") 87 cmd.extend(["wpt:docs", "./wpt"]) 88 # /app/venv is created during docker build and is always active inside the 89 # container. 90 cmd.extend(["--venv", "/app/venv", "--skip-venv-setup"]) 91 cmd.extend(["build-docs", "--type", kwargs["type"]]) 92 if kwargs["serve"] is not None: 93 cmd.extend(["--serve", str(kwargs["serve"])]) 94 logger.debug(" ".join(cmd)) 95 return subprocess.call(cmd) 96 97 98 def build(_venv, **kwargs): 99 if kwargs["docker"]: 100 docker_build() 101 return docker_run(**kwargs) 102 103 out_dir = os.path.join(here, "_build") 104 try: 105 created, failed = link_source_dirs() 106 if failed: 107 failure_msg = "\n".join(f"{dest}: {err}" for (dest, err) in failed) 108 logger.error(f"Failed to create source symlinks:\n{failure_msg}") 109 sys.exit(1) 110 if kwargs["serve"] is not None: 111 executable = "sphinx-autobuild" 112 extras = ["--port", str(kwargs["serve"]), 113 "--host", "0.0.0.0", 114 "--watch", os.path.abspath(os.path.join(here, os.pardir, "resources")), 115 # Ignore changes to files specified with glob pattern 116 "--ignore", "**/flycheck_*", 117 "--ignore", "**/.*", 118 "--ignore", "**/#*", 119 "--ignore", "docs/frontend.py", 120 "--ignore", "docs/Dockerfile"] 121 else: 122 executable = "sphinx-build" 123 extras = [] 124 cmd = [executable, "-n", "-v", "-b", kwargs["type"], "-j", "auto"] + extras + [here, out_dir] 125 logger.debug(" ".join(cmd)) 126 subprocess.check_call(cmd) 127 finally: 128 unlink_source_dirs(created)