Life of a Sphinx build#
Methodology#
git clone https://github.com/kaycebasques/experiments.git`
cd experiments/20260930
./bazelisk run :sphinx.run
should see:
Starting local Bazel server (9.2.0) and connecting to it...
INFO: Analyzed target //:sphinx.run (140 packages loaded, 5710 targets configured).
INFO: Found 1 target...
Target //:sphinx.run up-to-date:
bazel-bin/sphinx.run
INFO: Elapsed time: 7.654s, Critical Path: 0.30s
INFO: 3 processes: 15 action cache hit, 3 internal.
INFO: Build completed successfully, 3 total actions
INFO: Running command line: bazel-bin/sphinx.run
+ exec env -- bin/build --show-traceback --builder=html --fail-on-warning _sphinx/_sources /tmp/sphinx-out
> /home/kayce/.cache/bazel/_bazel_kayce/a88c0074b8ac7102a27d3a3445d1ab50/execroot/_main/bazel-out/k8-fastbuild/bin/sphinx.run.runfiles/_main/build.py(70)<module>()
-> main()
(Pdb)
press s then Enter
--Call--
> /home/kayce/.cache/bazel/_bazel_kayce/a88c0074b8ac7102a27d3a3445d1ab50/execroot/_main/bazel-out/k8-fastbuild/bin/sphinx.run.runfiles/rules_python++pip+pypi_312_sphinx/site-packages/sphinx/cmd/build.py(475)main()
> https://github.com/sphinx-doc/sphinx/blob/v9.1.0/sphinx/cmd/build.py#L475
-> def main(argv: Sequence[str] = (), /) -> int:
(Pdb)
note the url to the source code (https://github.com/sphinx-doc/sphinx/blob/v9.1.0/sphinx/cmd/build.py#L475)
Walkthrough#
as expected, we start in the main function of sphinx-build
the very first thing that happens is setting a locale
1def main(argv: Sequence[str] = (), /) -> int:
2 locale.setlocale(locale.LC_ALL, '')
3 sphinx.locale.init_console()
4
5 if not argv:
6 argv = sys.argv[1:]
7
8 # Allow calling as 'python -m sphinx build …'
9 if argv[:1] == ['build']:
10 argv = argv[1:]
11
12 if argv[:1] == ['--bug-report']:
13 return _bug_report_info()
14 if argv[:1] == ['-M']:
15 from sphinx.cmd import make_mode
16
17 return make_mode.run_make_mode(argv[1:])
18 else:
19 return build_main(argv)
this setlocale function comes from cpython. the if block gets skipped,
so we go straight to the return. they do something weird with
_setlocale. it seems like it’s defined as setlocale which seems like an
infinite loop
1def setlocale(category, locale=None):
2
3 """ Set the locale for the given category. The locale can be
4 a string, an iterable of two strings (language code and encoding),
5 or None.
6
7 Iterables are converted to strings using the locale aliasing
8 engine. Locale strings are passed directly to the C lib.
9
10 category may be given as one of the LC_* values.
11
12 """
13 if locale and not isinstance(locale, _builtin_str):
14 # convert to string
15 locale = normalize(_build_localename(locale))
16 return _setlocale(category, locale)
we return to the main function of sphinx-build. now it’s time to enter
init_console
1def main(argv: Sequence[str] = (), /) -> int:
2 locale.setlocale(locale.LC_ALL, '')
3 sphinx.locale.init_console()
4
5 if not argv:
6 argv = sys.argv[1:]
7
8 # Allow calling as 'python -m sphinx build …'
9 if argv[:1] == ['build']:
10 argv = argv[1:]
11
12 if argv[:1] == ['--bug-report']:
13 return _bug_report_info()
14 if argv[:1] == ['-M']:
15 from sphinx.cmd import make_mode
16
17 return make_mode.run_make_mode(argv[1:])
18 else:
19 return build_main(argv)
our minimal conf.py doesn’t define a locale dir so sphinx sets one for us:
1def init_console(
2 locale_dir: str | os.PathLike[str] | None = None,
3 catalog: str = 'sphinx',
4) -> tuple[NullTranslations, bool]:
5 """Initialize locale for console.
6
7 .. versionadded:: 1.8
8 """
9 if locale_dir is None:
10 locale_dir = _LOCALE_DIR
11 if sys.platform == 'win32':
12 language = None
13 else:
14 try:
15 # encoding is ignored
16 language, _ = locale.getlocale(locale.LC_MESSAGES)
17 except AttributeError:
18 # Fallback to the default language in case LC_MESSAGES is not defined.
19 language = None
20 return init([locale_dir], language, catalog, 'console')
in my case local_dir gets set to:
PosixPath('/home/kayce/.cache/bazel/_bazel_kayce/a88c0074b8ac7102a27d3a3445d1ab50/external/rules_python++pip+pypi_312_sphinx/site-packages/sphinx/locale')
my platform is linux x86 (or amd64 or whatever we call it) so the next code
to execute is the try block:
1def init_console(
2 locale_dir: str | os.PathLike[str] | None = None,
3 catalog: str = 'sphinx',
4) -> tuple[NullTranslations, bool]:
5 """Initialize locale for console.
6
7 .. versionadded:: 1.8
8 """
9 if locale_dir is None:
10 locale_dir = _LOCALE_DIR
11 if sys.platform == 'win32':
12 language = None
13 else:
14 try:
15 # encoding is ignored
16 language, _ = locale.getlocale(locale.LC_MESSAGES)
17 except AttributeError:
18 # Fallback to the default language in case LC_MESSAGES is not defined.
19 language = None
20 return init([locale_dir], language, catalog, 'console')
so we enter getlocale
(I still don’t understand why we don’t step into _setlocale)
category is set to 5 in my case which seems to equal LC_MESSAGES
so the if category == LC_ALL check is False and we proceed to
_parse_localname
1def getlocale(category=LC_CTYPE):
2
3 """ Returns the current setting for the given locale category as
4 tuple (language code, encoding).
5
6 category may be one of the LC_* value except LC_ALL. It
7 defaults to LC_CTYPE.
8
9 Except for the code 'C', the language code corresponds to RFC
10 1766. code and encoding can be None in case the values cannot
11 be determined.
12
13 """
14 localename = _setlocale(category)
15 if category == LC_ALL and ';' in localename:
16 raise TypeError('category LC_ALL is not supported')
17 return _parse_localename(localename)