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

https://github.com/sphinx-doc/sphinx/blob/v9.1.0/sphinx/cmd/build.py#L475#
 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

https://github.com/sphinx-doc/sphinx/blob/v9.1.0/sphinx/cmd/build.py#L475#
 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

https://github.com/sphinx-doc/sphinx/blob/v9.1.0/sphinx/cmd/build.py#L475#
 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:

https://github.com/sphinx-doc/sphinx/blob/v9.1.0/sphinx/cmd/build.py#L475#
 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:

https://github.com/sphinx-doc/sphinx/blob/v9.1.0/sphinx/cmd/build.py#L475#
 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

https://github.com/python/cpython/blob/v3.12.12/Lib/locale.py#L582#
 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)