37
37
import sys
38
38
from bisect import bisect_left as bisect
39
39
from collections import OrderedDict
40
+ from datetime import datetime as dt , timezone
40
41
from pathlib import Path
41
42
from string import Template
42
43
from textwrap import indent
44
+ from time import perf_counter , sleep
43
45
from typing import Iterable
44
46
from urllib .parse import urljoin
45
47
@@ -246,8 +248,6 @@ def run(cmd, cwd=None) -> subprocess.CompletedProcess:
246
248
cmdstring ,
247
249
indent ("\n " .join (result .stdout .split ("\n " )[- 20 :]), " " ),
248
250
)
249
- else :
250
- logging .debug ("Run: %r OK" , cmdstring )
251
251
result .check_returncode ()
252
252
return result
253
253
@@ -292,7 +292,13 @@ def get_ref(self, pattern):
292
292
return self .run ("show-ref" , "-s" , "tags/" + pattern ).stdout .strip ()
293
293
294
294
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" )
296
302
297
303
def switch (self , branch_or_tag ):
298
304
"""Reset and cleans the repository to the given branch or tag."""
@@ -354,20 +360,6 @@ def locate_nearest_version(available_versions, target_version):
354
360
return tuple_to_version (found )
355
361
356
362
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
-
371
363
@contextmanager
372
364
def edit (file : Path ):
373
365
"""Context manager to edit a file "in place", use it as:
@@ -612,11 +604,15 @@ def parse_args():
612
604
def setup_logging (log_directory : Path ):
613
605
"""Setup logging to stderr if ran by a human, or to a file if ran from a cron."""
614
606
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
+ )
616
610
else :
617
611
log_directory .mkdir (parents = True , exist_ok = True )
618
612
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
+ )
620
616
logging .getLogger ().addHandler (handler )
621
617
logging .getLogger ().setLevel (logging .DEBUG )
622
618
@@ -652,19 +648,19 @@ def full_build(self):
652
648
653
649
def run (self ) -> bool :
654
650
"""Build and publish a Python doc, for a language, and a version."""
651
+ start_time = perf_counter ()
652
+ logging .info ("Running." )
655
653
try :
656
654
self .cpython_repo .switch (self .version .branch_or_tag )
657
655
if self .language .tag != "en" :
658
656
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 )
662
662
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." )
668
664
if sentry_sdk :
669
665
sentry_sdk .capture_exception (err )
670
666
return False
@@ -676,10 +672,13 @@ def checkout(self) -> Path:
676
672
return self .build_root / "cpython"
677
673
678
674
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."""
680
681
681
- See PEP 545 for repository naming convention.
682
- """
683
682
locale_repo = f"https://github.com/python/python-docs-{ self .language .tag } .git"
684
683
locale_clone_dir = (
685
684
self .build_root
@@ -688,17 +687,25 @@ def clone_translation(self):
688
687
/ self .language .iso639_tag
689
688
/ "LC_MESSAGES"
690
689
)
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 )
694
705
695
706
def build (self ):
696
707
"""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." )
702
709
sphinxopts = list (self .language .sphinxopts )
703
710
sphinxopts .extend (["-q" ])
704
711
if self .language .tag != "en" :
@@ -774,11 +781,7 @@ def build(self):
774
781
setup_switchers (
775
782
self .versions , self .languages , self .checkout / "Doc" / "build" / "html"
776
783
)
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." )
782
785
783
786
def build_venv (self ):
784
787
"""Build a venv for the specific Python version.
@@ -799,11 +802,7 @@ def build_venv(self):
799
802
800
803
def copy_build_to_webroot (self ):
801
804
"""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." )
807
806
self .www_root .mkdir (parents = True , exist_ok = True )
808
807
if self .language .tag == "en" :
809
808
target = self .www_root / self .version .name
@@ -873,7 +872,7 @@ def copy_build_to_webroot(self):
873
872
]
874
873
)
875
874
if self .full_build :
876
- logging .debug ("Copying dist files" )
875
+ logging .debug ("Copying dist files. " )
877
876
run (
878
877
[
879
878
"chown" ,
@@ -916,11 +915,69 @@ def copy_build_to_webroot(self):
916
915
purge (* prefixes )
917
916
for prefix in prefixes :
918
917
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" )
924
981
925
982
926
983
def symlink (www_root : Path , language : Language , directory : str , name : str , group : str ):
@@ -1063,6 +1120,11 @@ def build_docs(args) -> bool:
1063
1120
cpython_repo .update ()
1064
1121
while todo :
1065
1122
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
+ )
1066
1128
if sentry_sdk :
1067
1129
with sentry_sdk .configure_scope () as scope :
1068
1130
scope .set_tag ("version" , version .name )
@@ -1071,6 +1133,10 @@ def build_docs(args) -> bool:
1071
1133
version , versions , language , languages , cpython_repo , ** vars (args )
1072
1134
)
1073
1135
all_built_successfully &= builder .run ()
1136
+ logging .root .handlers [0 ].setFormatter (
1137
+ logging .Formatter ("%(asctime)s %(levelname)s: %(message)s" )
1138
+ )
1139
+
1074
1140
build_sitemap (versions , languages , args .www_root , args .group )
1075
1141
build_404 (args .www_root , args .group )
1076
1142
build_robots_txt (
0 commit comments