Skip to content

Commit f27dbeb

Browse files
authored
Do not rebuild when it's not needed (like there's no updates). (#171)
1 parent 03e8f07 commit f27dbeb

File tree

1 file changed

+120
-54
lines changed

1 file changed

+120
-54
lines changed

build_docs.py

+120-54
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@
3737
import sys
3838
from bisect import bisect_left as bisect
3939
from collections import OrderedDict
40+
from datetime import datetime as dt, timezone
4041
from pathlib import Path
4142
from string import Template
4243
from textwrap import indent
44+
from time import perf_counter, sleep
4345
from typing import Iterable
4446
from urllib.parse import urljoin
4547

@@ -246,8 +248,6 @@ def run(cmd, cwd=None) -> subprocess.CompletedProcess:
246248
cmdstring,
247249
indent("\n".join(result.stdout.split("\n")[-20:]), " "),
248250
)
249-
else:
250-
logging.debug("Run: %r OK", cmdstring)
251251
result.check_returncode()
252252
return result
253253

@@ -292,7 +292,13 @@ def get_ref(self, pattern):
292292
return self.run("show-ref", "-s", "tags/" + pattern).stdout.strip()
293293

294294
def fetch(self):
295-
self.run("fetch")
295+
"""Try (and retry) to run git fetch."""
296+
try:
297+
return self.run("fetch")
298+
except subprocess.CalledProcessError as err:
299+
logging.error("'git fetch' failed (%s), retrying...", err.stderr)
300+
sleep(5)
301+
return self.run("fetch")
296302

297303
def switch(self, branch_or_tag):
298304
"""Reset and cleans the repository to the given branch or tag."""
@@ -354,20 +360,6 @@ def locate_nearest_version(available_versions, target_version):
354360
return tuple_to_version(found)
355361

356362

357-
def translation_branch(repo: Repository, needed_version: str):
358-
"""Some cpython versions may be untranslated, being either too old or
359-
too new.
360-
361-
This function looks for remote branches on the given repo, and
362-
returns the name of the nearest existing branch.
363-
364-
It could be enhanced to also search for tags.
365-
"""
366-
remote_branches = repo.run("branch", "-r").stdout
367-
branches = re.findall(r"/([0-9]+\.[0-9]+)$", remote_branches, re.M)
368-
return locate_nearest_version(branches, needed_version)
369-
370-
371363
@contextmanager
372364
def edit(file: Path):
373365
"""Context manager to edit a file "in place", use it as:
@@ -612,11 +604,15 @@ def parse_args():
612604
def setup_logging(log_directory: Path):
613605
"""Setup logging to stderr if ran by a human, or to a file if ran from a cron."""
614606
if sys.stderr.isatty():
615-
logging.basicConfig(format="%(levelname)s:%(message)s", stream=sys.stderr)
607+
logging.basicConfig(
608+
format="%(asctime)s %(levelname)s: %(message)s", stream=sys.stderr
609+
)
616610
else:
617611
log_directory.mkdir(parents=True, exist_ok=True)
618612
handler = logging.handlers.WatchedFileHandler(log_directory / "docsbuild.log")
619-
handler.setFormatter(logging.Formatter("%(levelname)s:%(asctime)s:%(message)s"))
613+
handler.setFormatter(
614+
logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
615+
)
620616
logging.getLogger().addHandler(handler)
621617
logging.getLogger().setLevel(logging.DEBUG)
622618

@@ -652,19 +648,19 @@ def full_build(self):
652648

653649
def run(self) -> bool:
654650
"""Build and publish a Python doc, for a language, and a version."""
651+
start_time = perf_counter()
652+
logging.info("Running.")
655653
try:
656654
self.cpython_repo.switch(self.version.branch_or_tag)
657655
if self.language.tag != "en":
658656
self.clone_translation()
659-
self.build_venv()
660-
self.build()
661-
self.copy_build_to_webroot()
657+
if self.should_rebuild():
658+
self.build_venv()
659+
self.build()
660+
self.copy_build_to_webroot()
661+
self.save_state(build_duration=perf_counter() - start_time)
662662
except Exception as err:
663-
logging.exception(
664-
"Exception while building %s version %s",
665-
self.language.tag,
666-
self.version.name,
667-
)
663+
logging.exception("Badly handled exception, human, please help.")
668664
if sentry_sdk:
669665
sentry_sdk.capture_exception(err)
670666
return False
@@ -676,10 +672,13 @@ def checkout(self) -> Path:
676672
return self.build_root / "cpython"
677673

678674
def clone_translation(self):
679-
"""Clone the translation repository from github.
675+
self.translation_repo.update()
676+
self.translation_repo.switch(self.translation_branch)
677+
678+
@property
679+
def translation_repo(self):
680+
"""See PEP 545 for translations repository naming convention."""
680681

681-
See PEP 545 for repository naming convention.
682-
"""
683682
locale_repo = f"https://github.com/python/python-docs-{self.language.tag}.git"
684683
locale_clone_dir = (
685684
self.build_root
@@ -688,17 +687,25 @@ def clone_translation(self):
688687
/ self.language.iso639_tag
689688
/ "LC_MESSAGES"
690689
)
691-
repo = Repository(locale_repo, locale_clone_dir)
692-
repo.update()
693-
repo.switch(translation_branch(repo, self.version.name))
690+
return Repository(locale_repo, locale_clone_dir)
691+
692+
@property
693+
def translation_branch(self):
694+
"""Some cpython versions may be untranslated, being either too old or
695+
too new.
696+
697+
This function looks for remote branches on the given repo, and
698+
returns the name of the nearest existing branch.
699+
700+
It could be enhanced to also search for tags.
701+
"""
702+
remote_branches = self.translation_repo.run("branch", "-r").stdout
703+
branches = re.findall(r"/([0-9]+\.[0-9]+)$", remote_branches, re.M)
704+
return locate_nearest_version(branches, self.version.name)
694705

695706
def build(self):
696707
"""Build this version/language doc."""
697-
logging.info(
698-
"Build start for version: %s, language: %s",
699-
self.version.name,
700-
self.language.tag,
701-
)
708+
logging.info("Build start.")
702709
sphinxopts = list(self.language.sphinxopts)
703710
sphinxopts.extend(["-q"])
704711
if self.language.tag != "en":
@@ -774,11 +781,7 @@ def build(self):
774781
setup_switchers(
775782
self.versions, self.languages, self.checkout / "Doc" / "build" / "html"
776783
)
777-
logging.info(
778-
"Build done for version: %s, language: %s",
779-
self.version.name,
780-
self.language.tag,
781-
)
784+
logging.info("Build done.")
782785

783786
def build_venv(self):
784787
"""Build a venv for the specific Python version.
@@ -799,11 +802,7 @@ def build_venv(self):
799802

800803
def copy_build_to_webroot(self):
801804
"""Copy a given build to the appropriate webroot with appropriate rights."""
802-
logging.info(
803-
"Publishing start for version: %s, language: %s",
804-
self.version.name,
805-
self.language.tag,
806-
)
805+
logging.info("Publishing start.")
807806
self.www_root.mkdir(parents=True, exist_ok=True)
808807
if self.language.tag == "en":
809808
target = self.www_root / self.version.name
@@ -873,7 +872,7 @@ def copy_build_to_webroot(self):
873872
]
874873
)
875874
if self.full_build:
876-
logging.debug("Copying dist files")
875+
logging.debug("Copying dist files.")
877876
run(
878877
[
879878
"chown",
@@ -916,11 +915,69 @@ def copy_build_to_webroot(self):
916915
purge(*prefixes)
917916
for prefix in prefixes:
918917
purge(*[prefix + p for p in changed])
919-
logging.info(
920-
"Publishing done for version: %s, language: %s",
921-
self.version.name,
922-
self.language.tag,
923-
)
918+
logging.info("Publishing done")
919+
920+
def should_rebuild(self):
921+
state = self.load_state()
922+
if not state:
923+
logging.info("Should rebuild: no previous state found.")
924+
return True
925+
cpython_sha = self.cpython_repo.run("rev-parse", "HEAD").stdout.strip()
926+
if self.language.tag != "en":
927+
translation_sha = self.translation_repo.run(
928+
"rev-parse", "HEAD"
929+
).stdout.strip()
930+
if translation_sha != state["translation_sha"]:
931+
logging.info(
932+
"Should rebuild: new translations (from %s to %s)",
933+
state["translation_sha"],
934+
translation_sha,
935+
)
936+
return True
937+
if cpython_sha != state["cpython_sha"]:
938+
diff = self.cpython_repo.run(
939+
"diff", "--name-only", state["cpython_sha"], cpython_sha
940+
).stdout
941+
if "Doc/" in diff:
942+
logging.info(
943+
"Should rebuild: Doc/ has changed (from %s to %s)",
944+
state["cpython_sha"],
945+
cpython_sha,
946+
)
947+
return True
948+
logging.info("Nothing changed, no rebuild needed.")
949+
return False
950+
951+
def load_state(self) -> dict:
952+
state_file = self.build_root / "state.toml"
953+
try:
954+
return tomlkit.loads(state_file.read_text(encoding="UTF-8"))[
955+
f"/{self.language.tag}/{self.version.name}/"
956+
]
957+
except KeyError:
958+
return {}
959+
960+
def save_state(self, build_duration: float):
961+
"""Save current cpython sha1 and current translation sha1.
962+
963+
Using this we can deduce if a rebuild is needed or not.
964+
"""
965+
state_file = self.build_root / "state.toml"
966+
try:
967+
states = tomlkit.parse(state_file.read_text(encoding="UTF-8"))
968+
except FileNotFoundError:
969+
states = tomlkit.document()
970+
971+
state = {}
972+
state["cpython_sha"] = self.cpython_repo.run("rev-parse", "HEAD").stdout.strip()
973+
if self.language.tag != "en":
974+
state["translation_sha"] = self.translation_repo.run(
975+
"rev-parse", "HEAD"
976+
).stdout.strip()
977+
state["last_build"] = dt.now(timezone.utc)
978+
state["last_build_duration"] = build_duration
979+
states[f"/{self.language.tag}/{self.version.name}/"] = state
980+
state_file.write_text(tomlkit.dumps(states), encoding="UTF-8")
924981

925982

926983
def symlink(www_root: Path, language: Language, directory: str, name: str, group: str):
@@ -1063,6 +1120,11 @@ def build_docs(args) -> bool:
10631120
cpython_repo.update()
10641121
while todo:
10651122
version, language = todo.pop()
1123+
logging.root.handlers[0].setFormatter(
1124+
logging.Formatter(
1125+
f"%(asctime)s %(levelname)s {language.tag}/{version.name}: %(message)s"
1126+
)
1127+
)
10661128
if sentry_sdk:
10671129
with sentry_sdk.configure_scope() as scope:
10681130
scope.set_tag("version", version.name)
@@ -1071,6 +1133,10 @@ def build_docs(args) -> bool:
10711133
version, versions, language, languages, cpython_repo, **vars(args)
10721134
)
10731135
all_built_successfully &= builder.run()
1136+
logging.root.handlers[0].setFormatter(
1137+
logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
1138+
)
1139+
10741140
build_sitemap(versions, languages, args.www_root, args.group)
10751141
build_404(args.www_root, args.group)
10761142
build_robots_txt(

0 commit comments

Comments
 (0)