builder.braak.pro/buildbot/config/worker/doc_manual.py
2024-11-20 16:13:44 +01:00

310 lines
10 KiB
Python
Executable file

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: 2011-2024 Blender Authors
# <pep8 compliant>
import argparse
import json
import os
import pathlib
import re
import sys
import time
from collections import OrderedDict
from datetime import timedelta
from typing import Optional, Sequence
sys.path.append(str(pathlib.Path(__file__).resolve().parent.parent))
import worker.configure
import worker.utils
class ManualBuilder(worker.utils.Builder):
def __init__(self, args: argparse.Namespace):
super().__init__(args, "blender", "blender-manual")
self.needs_all_locales = args.needs_all_locales
self.needs_package_delivery = args.needs_package_delivery
self.doc_format = args.doc_format
self.build_path = self.track_path / "build"
self.setup_track_path()
def get_locales(self) -> Sequence[str]:
locales = ["en"]
if self.needs_all_locales:
locale_path = self.code_path / "locale"
locales += [
item.name
for item in locale_path.iterdir()
if not item.name.startswith(".")
]
return locales
def update(builder: ManualBuilder) -> None:
builder.update_source()
if builder.needs_all_locales:
worker.utils.update_source(
"blender", "blender-manual-translations", builder.code_path / "locale"
)
def check(builder: ManualBuilder) -> None:
os.chdir(builder.track_path)
worker.utils.call_pipenv(
["install", "--pre", "--requirements", builder.code_path / "requirements.txt"]
)
os.chdir(builder.code_path)
make_cmd = "make.bat" if builder.platform == "windows" else "make"
worker.utils.call_pipenv(["run", make_cmd, "check_structure"])
# worker.utils.call_pipenv(["run", make_cmd, "check_syntax"])
# worker.utils.call_pipenv(["run", make_cmd, "check_spelling"])
def compile_doc(builder: ManualBuilder) -> None:
# Install requirements.
os.chdir(builder.track_path)
worker.utils.call_pipenv(
["install", "--pre", "--requirements", builder.code_path / "requirements.txt"]
)
# Determine format and locales
locales = builder.get_locales()
doc_format = builder.doc_format
# Clean build folder
worker.utils.remove_dir(builder.build_path)
os.makedirs(builder.build_path, exist_ok=True)
os.chdir(builder.code_path)
branches_config = builder.get_branches_config()
# Check manual version matches track.
conf_file_path = builder.code_path / "manual" / "conf.py"
conf_text = conf_file_path.read_text()
match = re.search(r"blender_version\s*=\s*['\"](.*)['\"]", conf_text)
expected_version = branches_config.track_major_minor_versions[builder.track_id]
found_version = match.groups(0)[0] if match else "nothing"
if found_version != expected_version:
raise Exception(
f"Expected blender_version {expected_version}, but found {found_version} in manual/conf.py"
)
def filter_output(line: str) -> Optional[str]:
if line.find("WARNING: unknown mimetype for .doctrees") != -1:
return None
elif line.find("copying images...") != -1:
return None
return line
# Generate manual
for locale in locales:
start_timestamp = time.time()
worker.utils.info(f"Generating {locale} in {doc_format}")
num_threads = worker.configure.get_thread_count(thread_memory_in_GB=1.25)
os.chdir(builder.code_path)
build_output_path = builder.build_path / doc_format / locale
worker.utils.call_pipenv(
[
"run",
"sphinx-build",
"-b",
doc_format,
"-j",
str(num_threads),
"-D",
f"language={locale}",
"./manual",
build_output_path,
],
filter_output=filter_output,
)
if doc_format == "epub":
if not build_output_path.rglob("*.epub"):
raise Exception(f"Expected epub files missing in {build_output_path}")
# Hack appropriate versions.json URL into version_switch.js
worker.utils.info("Replacing URL in version_switch.js")
version_switch_file_path = (
build_output_path / "_static" / "js" / "version_switch.js"
)
versions_file_url = (
f"https://docs.blender.org/{builder.service_env_id}/versions.json"
)
version_switch_text = version_switch_file_path.read_text()
version_switch_text = version_switch_text.replace(
"https://docs.blender.org/versions.json", versions_file_url
)
version_switch_text = version_switch_text.replace(
"https://docs.blender.org/PROD/versions.json", versions_file_url
)
version_switch_text = version_switch_text.replace(
"https://docs.blender.org/UATEST/versions.json", versions_file_url
)
version_switch_file_path.write_text(version_switch_text)
time_total = time.time() - start_timestamp
time_delta = str(timedelta(seconds=time_total))
worker.utils.info(f"Generated {locale} in {doc_format} in {time_delta}")
def package(builder: ManualBuilder) -> None:
if not builder.needs_package_delivery:
worker.utils.info("No package delivery needed, skipping packaging")
return
locales = builder.get_locales()
doc_format = builder.doc_format
os.chdir(builder.build_path)
compression_option = "" # "-mx=9"
package_file_name = f"blender_manual_{doc_format}.zip"
build_package_path = builder.build_path / "package"
for locale in locales:
package_file_path = build_package_path / locale / package_file_name
worker.utils.remove_file(package_file_path)
source_path = f"{doc_format}/{locale}"
cmd = [
"7z",
"a",
"-tzip",
package_file_path,
source_path,
"-r",
"-xr!.doctrees",
compression_option,
]
worker.utils.call(cmd)
cmd = [
"7z",
"rn",
package_file_path,
source_path,
f"blender_manual_{builder.track_id}_{locale}.{doc_format}",
]
worker.utils.call(cmd)
def deliver(builder: ManualBuilder) -> None:
locales = builder.get_locales()
doc_format = builder.doc_format
# Get versions
branches_config = builder.get_branches_config()
version = branches_config.track_major_minor_versions[builder.track_id]
dev_version = branches_config.track_major_minor_versions["vdev"]
latest_version = branches_config.doc_stable_major_minor_version
# Get remote paths
worker_config = builder.get_worker_config()
connect_id = f"{worker_config.docs_user}@{worker_config.docs_machine}"
docs_remote_path = (
pathlib.Path(worker_config.docs_folder)
/ "docs.blender.org"
/ "htdocs"
/ builder.service_env_id
)
# Sync each locale
for locale in locales:
worker.utils.info(f"Syncing {locale}")
# Create directory
remote_path = docs_remote_path / "manual" / locale
version_remote_path = remote_path / version
worker.utils.call_ssh(connect_id, ["mkdir", "-p", version_remote_path])
if doc_format == "html":
# Sync html files
source_path = f"{builder.build_path}/{doc_format}/{locale}/"
dest_path = f"{connect_id}:{version_remote_path}/"
# Exclude packaged download files; these get synced with `needs_package_delivery`.
worker.utils.rsync(
source_path,
dest_path,
exclude_paths=[".doctrees", "blender_manual_*.zip"],
delete=True,
delete_path_check=str(version_remote_path),
)
# Create links
if builder.track_id == "vdev":
worker.utils.info(f"Creating links for {locale}")
worker.utils.call_ssh(
connect_id,
["ln", "-svF", remote_path / dev_version, remote_path / "dev"],
)
worker.utils.call_ssh(
connect_id,
[
"ln",
"-svF",
remote_path / latest_version,
remote_path / "latest",
],
)
if builder.needs_package_delivery:
# Sync zip package
worker.utils.info(f"Syncing package for {locale}")
build_package_path = builder.build_path / "package"
package_file_name = f"blender_manual_{doc_format}.zip"
source_path = build_package_path / locale / package_file_name
dest_path = f"{connect_id}:{version_remote_path}/{package_file_name}"
worker.utils.rsync(source_path, dest_path, exclude_paths=[".doctrees"])
# Create and sync versions.json
worker.utils.info("Creating and syncing versions.json")
doc_version_labels = branches_config.doc_manual_version_labels
versions_path = builder.build_path / "versions.json"
versions_path.write_text(json.dumps(doc_version_labels, indent=2))
worker.utils.info(versions_path.read_text())
dest_path = f"{connect_id}:{docs_remote_path}/versions.json"
worker.utils.rsync(versions_path, dest_path)
def clean(builder: ManualBuilder) -> None:
worker.utils.remove_dir(builder.build_path)
if __name__ == "__main__":
steps: worker.utils.BuilderSteps = OrderedDict()
steps["configure-machine"] = worker.configure.configure_machine
steps["update"] = update
steps["check"] = check
steps["compile"] = compile_doc
steps["package"] = package
steps["deliver"] = deliver
steps["clean"] = clean
parser = worker.utils.create_argument_parser(steps=steps)
parser.add_argument("--needs-all-locales", action="store_true", required=False)
parser.add_argument("--needs-package-delivery", action="store_true", required=False)
parser.add_argument(
"--doc-format",
default="html",
type=str,
required=False,
choices=["html", "epub"],
)
args = parser.parse_args()
builder = ManualBuilder(args)
builder.run(args.step, steps)