diff mbox series

[v2] doc: fix the switchers menu

Message ID 20260204-fix-switchers-js-v2-1-ea80107eff85@bootlin.com
State New
Headers show
Series [v2] doc: fix the switchers menu | expand

Commit Message

Antonin Godard Feb. 4, 2026, 10:03 a.m. UTC
Fix the switchers.js script of Bitbake to show what are the supported
versions and be able to switch between them.

The default landing page is the stable branch and shows the BitBake
version along with the corresponding Yocto Project codename. This
hopefully makes it easier to remember the correspondance between the
BitBake version and the Yocto Project version.

This works thanks to a set_versions.py script, which is largely inspired
from yocto-docs. It reads the tags from the repository and tries to
guess the currently checked out version of BitBake on which we are.

The "obsolete" warning is now also shown when browsing outdated manuals,
meaning any version not part of activereleases in set_versions.py.

Signed-off-by: Antonin Godard <antonin.godard@bootlin.com>
---
Note: This will be accompanied by a patch to adapt the Autobuilder to
generate this file for each release.
---
Changes in v2:
- Remove environment information grabbing mechanism, making this fully
  standalone.
- Link to v1: https://lore.kernel.org/r/20250915-fix-switchers-js-v1-0-523ef53fe802@bootlin.com
---
 doc/.gitignore                    |   2 +
 doc/Makefile                      |   3 +-
 doc/bitbake.yaml.in               |   1 +
 doc/conf.py                       |  10 +-
 doc/set_versions.py               | 155 ++++++++++++++++++++++++
 doc/sphinx-static/switchers.js    | 233 -----------------------------------
 doc/sphinx-static/switchers.js.in | 247 ++++++++++++++++++++++++++++++++++++++
 7 files changed, 416 insertions(+), 235 deletions(-)


---
base-commit: cdd79c1768ac396a9c6577e38098da4331507f24
change-id: 20241227-fix-switchers-js-8cf445610b97

Comments

Richard Purdie Feb. 4, 2026, 1:32 p.m. UTC | #1
On Wed, 2026-02-04 at 11:03 +0100, Antonin Godard via lists.openembedded.org wrote:
> Fix the switchers.js script of Bitbake to show what are the supported
> versions and be able to switch between them.
> 
> The default landing page is the stable branch and shows the BitBake
> version along with the corresponding Yocto Project codename. This
> hopefully makes it easier to remember the correspondance between the
> BitBake version and the Yocto Project version.
> 
> This works thanks to a set_versions.py script, which is largely inspired
> from yocto-docs. It reads the tags from the repository and tries to
> guess the currently checked out version of BitBake on which we are.
> 
> The "obsolete" warning is now also shown when browsing outdated manuals,
> meaning any version not part of activereleases in set_versions.py.
> 
> Signed-off-by: Antonin Godard <antonin.godard@bootlin.com>
> ---
> Note: This will be accompanied by a patch to adapt the Autobuilder to
> generate this file for each release.
> ---
> Changes in v2:
> - Remove environment information grabbing mechanism, making this fully
>   standalone.
> - Link to v1: https://lore.kernel.org/r/20250915-fix-switchers-js-v1-0-523ef53fe802@bootlin.com
> ---
>  doc/.gitignore                    |   2 +
>  doc/Makefile                      |   3 +-
>  doc/bitbake.yaml.in               |   1 +

I was about to merge this however is there a requirement to use yaml
here? I ask mainly as we don't currently have yaml config files and I'd
prefer not to start adding them and try to remain consistent with what
we're using...

Cheers,

Richard
Antonin Godard Feb. 4, 2026, 1:41 p.m. UTC | #2
On Wed Feb 4, 2026 at 2:32 PM CET, Richard Purdie wrote:
> On Wed, 2026-02-04 at 11:03 +0100, Antonin Godard via lists.openembedded.org wrote:
>> Fix the switchers.js script of Bitbake to show what are the supported
>> versions and be able to switch between them.
>> 
>> The default landing page is the stable branch and shows the BitBake
>> version along with the corresponding Yocto Project codename. This
>> hopefully makes it easier to remember the correspondance between the
>> BitBake version and the Yocto Project version.
>> 
>> This works thanks to a set_versions.py script, which is largely inspired
>> from yocto-docs. It reads the tags from the repository and tries to
>> guess the currently checked out version of BitBake on which we are.
>> 
>> The "obsolete" warning is now also shown when browsing outdated manuals,
>> meaning any version not part of activereleases in set_versions.py.
>> 
>> Signed-off-by: Antonin Godard <antonin.godard@bootlin.com>
>> ---
>> Note: This will be accompanied by a patch to adapt the Autobuilder to
>> generate this file for each release.
>> ---
>> Changes in v2:
>> - Remove environment information grabbing mechanism, making this fully
>>   standalone.
>> - Link to v1: https://lore.kernel.org/r/20250915-fix-switchers-js-v1-0-523ef53fe802@bootlin.com
>> ---
>>  doc/.gitignore                    |   2 +
>>  doc/Makefile                      |   3 +-
>>  doc/bitbake.yaml.in               |   1 +
>
> I was about to merge this however is there a requirement to use yaml
> here? I ask mainly as we don't currently have yaml config files and I'd
> prefer not to start adding them and try to remain consistent with what
> we're using...

You're right, I don't see any obvious reason to use yaml either, and we could
use JSON instead. I just aligned with what was done on yocto-docs without this
afterthought. I'd be happy to make the switch (here and perhaps we could also do
that on yocto-docs). I'll try it out, thanks.

Antonin
Quentin Schulz Feb. 4, 2026, 3:01 p.m. UTC | #3
Hi Antonin,

On 2/4/26 11:03 AM, Antonin Godard via lists.yoctoproject.org wrote:
> Fix the switchers.js script of Bitbake to show what are the supported
> versions and be able to switch between them.
> 
> The default landing page is the stable branch and shows the BitBake
> version along with the corresponding Yocto Project codename. This
> hopefully makes it easier to remember the correspondance between the
> BitBake version and the Yocto Project version.
> 
> This works thanks to a set_versions.py script, which is largely inspired
> from yocto-docs. It reads the tags from the repository and tries to
> guess the currently checked out version of BitBake on which we are.
> 
> The "obsolete" warning is now also shown when browsing outdated manuals,
> meaning any version not part of activereleases in set_versions.py.
> 
> Signed-off-by: Antonin Godard <antonin.godard@bootlin.com>
> ---
> Note: This will be accompanied by a patch to adapt the Autobuilder to
> generate this file for each release.
> ---
> Changes in v2:
> - Remove environment information grabbing mechanism, making this fully
>    standalone.
> - Link to v1: https://lore.kernel.org/r/20250915-fix-switchers-js-v1-0-523ef53fe802@bootlin.com
> ---
>   doc/.gitignore                    |   2 +
>   doc/Makefile                      |   3 +-
>   doc/bitbake.yaml.in               |   1 +
>   doc/conf.py                       |  10 +-
>   doc/set_versions.py               | 155 ++++++++++++++++++++++++
>   doc/sphinx-static/switchers.js    | 233 -----------------------------------
>   doc/sphinx-static/switchers.js.in | 247 ++++++++++++++++++++++++++++++++++++++
>   7 files changed, 416 insertions(+), 235 deletions(-)
> 
> diff --git a/doc/.gitignore b/doc/.gitignore
> index 69fa449dd96..40ebe76c088 100644
> --- a/doc/.gitignore
> +++ b/doc/.gitignore
> @@ -1 +1,3 @@
>   _build/
> +sphinx-static/switchers.js
> +bitbake.yaml
> diff --git a/doc/Makefile b/doc/Makefile
> index 996f01b7d5c..cdb054d3a52 100644
> --- a/doc/Makefile
> +++ b/doc/Makefile
> @@ -27,9 +27,10 @@ publish: Makefile html singlehtml
>   	sed -i -e 's@index.html#@singleindex.html#@g' $(BUILDDIR)/$(DESTDIR)/singleindex.html
>   
>   clean:
> -	@rm -rf $(BUILDDIR)
> +	@rm -rf $(BUILDDIR) sphinx-static/switchers.js bitbake.yaml
>   
>   # Catch-all target: route all unknown targets to Sphinx using the new
>   # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
>   %: Makefile
> +	$(SOURCEDIR)/set_versions.py
>   	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
> diff --git a/doc/bitbake.yaml.in b/doc/bitbake.yaml.in
> new file mode 100644
> index 00000000000..4085582df60
> --- /dev/null
> +++ b/doc/bitbake.yaml.in
> @@ -0,0 +1 @@
> +DOCCONF_VERSION : "dev"
> diff --git a/doc/conf.py b/doc/conf.py
> index bce386624e2..5b28808c590 100644
> --- a/doc/conf.py
> +++ b/doc/conf.py
> @@ -14,17 +14,25 @@
>   # import sys
>   # sys.path.insert(0, os.path.abspath('.'))
>   
> +import os

I don't see it being used?

>   import sys
>   import datetime
> +import yaml
>   
>   from pathlib import Path
>   
> -current_version = "dev"
> +current_version = 'dev'

Unnecessary noise.

> +
> +with open("bitbake.yaml") as data:
> +    buff = data.read()
> +    subst_vars = yaml.safe_load(buff)
> +    current_version = subst_vars["DOCCONF_VERSION"]
>   
>   # String used in sidebar
>   version = 'Version: ' + current_version
>   if current_version == 'dev':
>       version = 'Version: Current Development'
> +

Ditto.

>   # Version seen in documentation_options.js and hence in js switchers code
>   release = current_version
>   
> diff --git a/doc/set_versions.py b/doc/set_versions.py
> new file mode 100755
> index 00000000000..63fba2fe948
> --- /dev/null
> +++ b/doc/set_versions.py
> @@ -0,0 +1,155 @@
> +#!/usr/bin/env python3
> +#
> +# This is a minimal version of the set_versions.py from yocto-docs,
> +# use to replace VERSIONS_PLACEHOLDER in switchers.js.in by a list defined below
> +# with BITBAKE_ACTIVE_RELEASES in the environment.
> +#

Where?

> +# When the documentation is built with the autobuilder, the versions are
> +# calculated based on the info found in set_versions.py from yocto-docs.
> +#
> +# Copyright Linux Foundation
> +# Author: Antonin Godard <antonin.godard@bootlin.com>
> +#
> +# SPDX-License-Identifier: MIT
> +#
> +
> +import collections
> +import itertools
> +import re
> +import os
> +import subprocess
> +import sys
> +

Sort alphabetically please.

> +from packaging.version import Version
> +from typing import Tuple
> +
> +devbranch = "2.18"
> +ltsseries = ["2.8", "2.0"]
> +activereleases = ["2.16"] + ltsseries
> +
> +yocto_mapping = {
> +    "2.18": "wrynose",
> +    "2.16": "whinlatter",
> +    "2.12": "walnascar",
> +    "2.10": "styhead",
> +    "2.8": "scarthgap",
> +    "2.6": "nanbield",
> +    "2.4": "mickledore",
> +    "2.2": "langdale",
> +    "2.0": "kirkstone",
> +    "1.52": "honister",
> +    "1.50": "hardknott",
> +    "1.48": "gatesgarth",
> +    "1.46": "dunfell",
> +}
> +
> +bbver_re = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$")
> +ourversion = None
> +
> +# Test that we are building from a Git repository
> +try:
> +    subprocess.run(["git", "rev-parse", "--is-inside-work-tree"],
> +                   stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
> +except subprocess.CalledProcessError:
> +    sys.exit("Building bitbake's documentation must be done from its Git repository.\n"
> +             "Clone the repository with the following command:\n"
> +             "git clone https://git.openembedded.org/bitbake ")
> +
> +# Test tags exist and inform the user to fetch if not
> +try:
> +    subprocess.run(["git", "show", f"{ltsseries[0]}.0"],
> +                   stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
> +except subprocess.CalledProcessError:
> +    sys.exit("Please run 'git fetch --tags' before building the documentation")
> +
> +
> +# Try and figure out what we are
> +tags = subprocess.run(["git", "tag", "--points-at", "HEAD"],
> +                      stdout=subprocess.PIPE, stderr=subprocess.PIPE,
> +                      universal_newlines=True).stdout
> +for t in tags.split():
> +    if re.match(bbver_re, t):
> +        ourversion = t

We can break here as we can only match one X.Y.Z tag for a given commit 
hash.

> +
> +if ourversion:
> +    # We're a tagged release
> +    components = ourversion.split(".")
> +    ourseries = f"{components[0]}.{components[1]}"
> +else:
> +    # We're floating on a branch
> +    branch = subprocess.run(["git", "branch", "--show-current"],
> +                            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
> +                            universal_newlines=True).stdout.strip()
> +
> +    if branch == "" or branch not in list(yocto_mapping.keys()) + ["master", "master-next"]:
> +        # We're not on a known release branch so we have to guess. Compare the
> +        # numbers of commits from each release branch and assume the smallest
> +        # number of commits is the one we're based off
> +        possible_branch = None
> +        branch_count = 0
> +        for b in itertools.chain(yocto_mapping.keys(), ["master"]):
> +            result = subprocess.run(["git", "log", "--format=oneline", "HEAD..origin/" + b],
> +                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
> +                                    universal_newlines=True)
> +            if result.returncode == 0:
> +                count = result.stdout.count('\n')
> +                if not possible_branch or count < branch_count:
> +                    print("Branch %s has count %s" % (b, count))
> +                    possible_branch = b
> +                    branch_count = count
> +        if possible_branch:
> +            branch = possible_branch
> +        else:
> +            branch = "master"
> +        print("Nearest release branch estimated to be %s" % branch)
> +
> +    if branch == "master":
> +        ourversion = "dev"
> +        ourseries = devbranch
> +    elif branch == "master-next":
> +        ourversion = "next"
> +        ourseries = devbranch
> +    else:
> +        ourversion = branch
> +        head_commit = subprocess.run(["git", "rev-parse", "--short", "HEAD"],
> +                                     stdout=subprocess.PIPE, stderr=subprocess.PIPE,
> +                                     universal_newlines=True).stdout.strip()
> +        branch_commit = subprocess.run(["git", "rev-parse", "--short", branch],
> +                                        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
> +                                        universal_newlines=True).stdout.strip()
> +        if head_commit != branch_commit:
> +            ourversion += f" ({head_commit})"
> +        ourseries = branch
> +
> +print("Version calculated to be %s" % ourversion)
> +print("Series calculated to be %s" % ourseries)
> +
> +replacements = {
> +    "DOCCONF_VERSION": ourversion,
> +}
> +
> +if os.path.exists("bitbake.yaml.in"):
> +    with open("bitbake.yaml.in", "r") as r, open("bitbake.yaml", "w") as w:
> +        lines = r.readlines()
> +        for line in lines:
> +            data = line.split(":")
> +            k = data[0].strip()
> +            if k in replacements:
> +                w.write("%s : \"%s\"\n" % (k, replacements[k]))
> +            else:
> +                w.write(line)
> +
> +    print("bitbake.yaml generated from bitbake.yaml.in")
> +
> +with open("sphinx-static/switchers.js.in", "r") as r, \
> +     open("sphinx-static/switchers.js", "w") as w:
> +    lines = r.readlines()
> +    for line in lines:
> +        if "VERSIONS_PLACEHOLDER" in line:
> +            if ourversion != "dev":
> +                w.write(f"    'dev': 'Unstable (dev)',\n")
> +            for series in activereleases:
> +                w.write(f"    '{series}': '{series} ({yocto_mapping[series]})',\n")

Why is this different from yp-docs? We don't handle showing the current 
series (ourseries)/version (ourversion) if it's outdated anymore?

To answer Richard on the other thread, we use YAML because JSON doesn't 
allow comments. Also, because we used to want multiline variables (see 
https://yaml-multiline.info/) which aren't supported in JSON, but those 
are gone since 8d993022c2ae ("docs: use literalinclude for system 
requirements"). Not sure we can get rid of it for yp-docs as we only 
override *some* variables in this file by set_versions.py so the 
autobuilder won't replace poky.yaml.in, and the YAML is consumed by 
sphinx/yocto-vars.py.

If we don't plan on having comments in this file *ever*, then the YAML 
dependency isn't necessary.

Another option is INI and use configparser, c.f. 
https://docs.python.org/3/library/configparser.html

It seems that INI supports the ':' as key-value delimiter as well as 
multiline strings (but not the way we used it for YAML!), so it could be 
a drop-in replacement. We could even simply write to the file with 
configparser.write() (though that will drop the in-file comments).

Cheers,
Quentin
Antonin Godard Feb. 5, 2026, 11:34 a.m. UTC | #4
Hi,

On Wed Feb 4, 2026 at 4:01 PM CET, Quentin Schulz via lists.yoctoproject.org wrote:
[...]
>> diff --git a/doc/set_versions.py b/doc/set_versions.py
>> new file mode 100755
>> index 00000000000..63fba2fe948
>> --- /dev/null
>> +++ b/doc/set_versions.py
>> @@ -0,0 +1,155 @@
>> +#!/usr/bin/env python3
>> +#
>> +# This is a minimal version of the set_versions.py from yocto-docs,
>> +# use to replace VERSIONS_PLACEHOLDER in switchers.js.in by a list defined below
>> +# with BITBAKE_ACTIVE_RELEASES in the environment.
>> +#
>
> Where?

A leftover from the previous version, thanks.

[...]
>> +with open("sphinx-static/switchers.js.in", "r") as r, \
>> +     open("sphinx-static/switchers.js", "w") as w:
>> +    lines = r.readlines()
>> +    for line in lines:
>> +        if "VERSIONS_PLACEHOLDER" in line:
>> +            if ourversion != "dev":
>> +                w.write(f"    'dev': 'Unstable (dev)',\n")
>> +            for series in activereleases:
>> +                w.write(f"    '{series}': '{series} ({yocto_mapping[series]})',\n")
>
> Why is this different from yp-docs? We don't handle showing the current 
> series (ourseries)/version (ourversion) if it's outdated anymore?

We do, but from switchers.js, where in build_version_select we push the current
version of the document last:

    buf.push(
      '<option value="' +
        current_version +
        '" selected="selected">' +
        current_title +
        "</option>",
    );

So the version from our branch gets displayed.

(trust me, I've had a hard time locating and understanding how all of this
worked :))

However, you made me realize that due to our branch the abbrev hash, it would
always appear as outdated. I fixed that for v2.

> To answer Richard on the other thread, we use YAML because JSON doesn't 
> allow comments. Also, because we used to want multiline variables (see 
> https://yaml-multiline.info/) which aren't supported in JSON, but those 
> are gone since 8d993022c2ae ("docs: use literalinclude for system 
> requirements"). Not sure we can get rid of it for yp-docs as we only 
> override *some* variables in this file by set_versions.py so the 
> autobuilder won't replace poky.yaml.in, and the YAML is consumed by 
> sphinx/yocto-vars.py.
>
> If we don't plan on having comments in this file *ever*, then the YAML 
> dependency isn't necessary.
>
> Another option is INI and use configparser, c.f. 
> https://docs.python.org/3/library/configparser.html
>
> It seems that INI supports the ':' as key-value delimiter as well as 
> multiline strings (but not the way we used it for YAML!), so it could be 
> a drop-in replacement. We could even simply write to the file with 
> configparser.write() (though that will drop the in-file comments).

If we directly write the ini file with configparser.write(), maybe we can just
maintain the list of variable as Python instead, instead of a separate ".in"
file?

Antonin
Quentin Schulz Feb. 5, 2026, 1:08 p.m. UTC | #5
Hi Antonin,

On 2/5/26 12:34 PM, Antonin Godard wrote:
> Hi,
> 
> On Wed Feb 4, 2026 at 4:01 PM CET, Quentin Schulz via lists.yoctoproject.org wrote:
> [...]
>>> +with open("sphinx-static/switchers.js.in", "r") as r, \
>>> +     open("sphinx-static/switchers.js", "w") as w:
>>> +    lines = r.readlines()
>>> +    for line in lines:
>>> +        if "VERSIONS_PLACEHOLDER" in line:
>>> +            if ourversion != "dev":
>>> +                w.write(f"    'dev': 'Unstable (dev)',\n")
>>> +            for series in activereleases:
>>> +                w.write(f"    '{series}': '{series} ({yocto_mapping[series]})',\n")
>>
>> Why is this different from yp-docs? We don't handle showing the current
>> series (ourseries)/version (ourversion) if it's outdated anymore?
> 
> We do, but from switchers.js, where in build_version_select we push the current
> version of the document last:
> 
>      buf.push(
>        '<option value="' +
>          current_version +
>          '" selected="selected">' +
>          current_title +
>          "</option>",
>      );
> 
> So the version from our branch gets displayed.
> 
> (trust me, I've had a hard time locating and understanding how all of this
> worked :))
> 

Yeah set_versions.py and the switchers aren't the nicest things to dig 
into :/

Is the above mechanism something we could migrate yp-docs switcher to to 
simplify it a bit?

> However, you made me realize that due to our branch the abbrev hash, it would
> always appear as outdated. I fixed that for v2.
> 
>> To answer Richard on the other thread, we use YAML because JSON doesn't
>> allow comments. Also, because we used to want multiline variables (see
>> https://yaml-multiline.info/) which aren't supported in JSON, but those
>> are gone since 8d993022c2ae ("docs: use literalinclude for system
>> requirements"). Not sure we can get rid of it for yp-docs as we only
>> override *some* variables in this file by set_versions.py so the
>> autobuilder won't replace poky.yaml.in, and the YAML is consumed by
>> sphinx/yocto-vars.py.
>>
>> If we don't plan on having comments in this file *ever*, then the YAML
>> dependency isn't necessary.
>>
>> Another option is INI and use configparser, c.f.
>> https://docs.python.org/3/library/configparser.html
>>
>> It seems that INI supports the ':' as key-value delimiter as well as
>> multiline strings (but not the way we used it for YAML!), so it could be
>> a drop-in replacement. We could even simply write to the file with
>> configparser.write() (though that will drop the in-file comments).
> 
> If we directly write the ini file with configparser.write(), maybe we can just
> maintain the list of variable as Python instead, instead of a separate ".in"
> file?
> 

The benefit of having a .in file is that it's explicit what the user can 
use in the docs (if we have yocto-vars.py! which we don't (yet?) in 
BitBake).

What's the usecase for DOCCONF_VERSION BTW? We only use it for setting 
the current version (always "dev") but we never override it? Either in 
release branches, in the autobuilder, only from set_versions.py do we 
read it and override it. But then it's nowhere to be found in the docs 
either so we don't use. Do we actually need this for BitBake?

We could push this even further if we wanted to and have a bitbake.py 
instead? In some internal Sphinx project, we provide a skeleton with 
options to provide (Python) variables through another file we then include:

exec(open('variables.py').read())

is what we use. This would support f-strings (can easily reference a 
variable from another one), multiline strings, etc. No need to parse 
anything, it just needs to be valid Python variable assignment.

Cheers,
Quentin
Antonin Godard Feb. 5, 2026, 1:40 p.m. UTC | #6
Hi,

On Thu Feb 5, 2026 at 2:08 PM CET, Quentin Schulz via lists.yoctoproject.org wrote:
> Hi Antonin,
>
> On 2/5/26 12:34 PM, Antonin Godard wrote:
>> Hi,
>> 
>> On Wed Feb 4, 2026 at 4:01 PM CET, Quentin Schulz via lists.yoctoproject.org wrote:
>> [...]
>>>> +with open("sphinx-static/switchers.js.in", "r") as r, \
>>>> +     open("sphinx-static/switchers.js", "w") as w:
>>>> +    lines = r.readlines()
>>>> +    for line in lines:
>>>> +        if "VERSIONS_PLACEHOLDER" in line:
>>>> +            if ourversion != "dev":
>>>> +                w.write(f"    'dev': 'Unstable (dev)',\n")
>>>> +            for series in activereleases:
>>>> +                w.write(f"    '{series}': '{series} ({yocto_mapping[series]})',\n")
>>>
>>> Why is this different from yp-docs? We don't handle showing the current
>>> series (ourseries)/version (ourversion) if it's outdated anymore?
>> 
>> We do, but from switchers.js, where in build_version_select we push the current
>> version of the document last:
>> 
>>      buf.push(
>>        '<option value="' +
>>          current_version +
>>          '" selected="selected">' +
>>          current_title +
>>          "</option>",
>>      );
>> 
>> So the version from our branch gets displayed.
>> 
>> (trust me, I've had a hard time locating and understanding how all of this
>> worked :))
>> 
>
> Yeah set_versions.py and the switchers aren't the nicest things to dig 
> into :/
>
> Is the above mechanism something we could migrate yp-docs switcher to to 
> simplify it a bit?

Yes, I think I'd like to port these simplifications to yocto-docs soon after,
while memory is still fresh

>> However, you made me realize that due to our branch the abbrev hash, it would
>> always appear as outdated. I fixed that for v2.
>> 
>>> To answer Richard on the other thread, we use YAML because JSON doesn't
>>> allow comments. Also, because we used to want multiline variables (see
>>> https://yaml-multiline.info/) which aren't supported in JSON, but those
>>> are gone since 8d993022c2ae ("docs: use literalinclude for system
>>> requirements"). Not sure we can get rid of it for yp-docs as we only
>>> override *some* variables in this file by set_versions.py so the
>>> autobuilder won't replace poky.yaml.in, and the YAML is consumed by
>>> sphinx/yocto-vars.py.
>>>
>>> If we don't plan on having comments in this file *ever*, then the YAML
>>> dependency isn't necessary.
>>>
>>> Another option is INI and use configparser, c.f.
>>> https://docs.python.org/3/library/configparser.html
>>>
>>> It seems that INI supports the ':' as key-value delimiter as well as
>>> multiline strings (but not the way we used it for YAML!), so it could be
>>> a drop-in replacement. We could even simply write to the file with
>>> configparser.write() (though that will drop the in-file comments).
>> 
>> If we directly write the ini file with configparser.write(), maybe we can just
>> maintain the list of variable as Python instead, instead of a separate ".in"
>> file?
>> 
>
> The benefit of having a .in file is that it's explicit what the user can 
> use in the docs (if we have yocto-vars.py! which we don't (yet?) in 
> BitBake).
>
> What's the usecase for DOCCONF_VERSION BTW? We only use it for setting 
> the current version (always "dev") but we never override it? Either in 
> release branches, in the autobuilder, only from set_versions.py do we 
> read it and override it. But then it's nowhere to be found in the docs 
> either so we don't use. Do we actually need this for BitBake?

We use it in doc/conf.py:

current_version = 'dev'

with open("bitbake.yaml") as data:
    buff = data.read()
    subst_vars = yaml.safe_load(buff)
    current_version = subst_vars["DOCCONF_VERSION"]

So basically we just need a way to pass this information from set_versions.py to
the sphinx-build command.

> We could push this even further if we wanted to and have a bitbake.py 
> instead? In some internal Sphinx project, we provide a skeleton with 
> options to provide (Python) variables through another file we then include:
>
> exec(open('variables.py').read())
>
> is what we use. This would support f-strings (can easily reference a 
> variable from another one), multiline strings, etc. No need to parse 
> anything, it just needs to be valid Python variable assignment.

So basically, you'd write a variables.py file from set_versions.py and read it
from conf.py?

I'm starting to wonder if it just wouldn't be better do move all of this logic
into doc/conf.py, actually.

Thanks,
Antonin
Quentin Schulz Feb. 5, 2026, 2 p.m. UTC | #7
Hi Antonin,

On 2/5/26 2:40 PM, Antonin Godard wrote:
> Hi,
> 
> On Thu Feb 5, 2026 at 2:08 PM CET, Quentin Schulz via lists.yoctoproject.org wrote:
>> Hi Antonin,
>>
>> On 2/5/26 12:34 PM, Antonin Godard wrote:
>>> Hi,
>>>
>>> On Wed Feb 4, 2026 at 4:01 PM CET, Quentin Schulz via lists.yoctoproject.org wrote:
>>> [...]
>>>>> +with open("sphinx-static/switchers.js.in", "r") as r, \
>>>>> +     open("sphinx-static/switchers.js", "w") as w:
>>>>> +    lines = r.readlines()
>>>>> +    for line in lines:
>>>>> +        if "VERSIONS_PLACEHOLDER" in line:
>>>>> +            if ourversion != "dev":
>>>>> +                w.write(f"    'dev': 'Unstable (dev)',\n")
>>>>> +            for series in activereleases:
>>>>> +                w.write(f"    '{series}': '{series} ({yocto_mapping[series]})',\n")
>>>>
>>>> Why is this different from yp-docs? We don't handle showing the current
>>>> series (ourseries)/version (ourversion) if it's outdated anymore?
>>>
>>> We do, but from switchers.js, where in build_version_select we push the current
>>> version of the document last:
>>>
>>>       buf.push(
>>>         '<option value="' +
>>>           current_version +
>>>           '" selected="selected">' +
>>>           current_title +
>>>           "</option>",
>>>       );
>>>
>>> So the version from our branch gets displayed.
>>>
>>> (trust me, I've had a hard time locating and understanding how all of this
>>> worked :))
>>>
>>
>> Yeah set_versions.py and the switchers aren't the nicest things to dig
>> into :/
>>
>> Is the above mechanism something we could migrate yp-docs switcher to to
>> simplify it a bit?
> 
> Yes, I think I'd like to port these simplifications to yocto-docs soon after,
> while memory is still fresh
> 
>>> However, you made me realize that due to our branch the abbrev hash, it would
>>> always appear as outdated. I fixed that for v2.
>>>
>>>> To answer Richard on the other thread, we use YAML because JSON doesn't
>>>> allow comments. Also, because we used to want multiline variables (see
>>>> https://yaml-multiline.info/) which aren't supported in JSON, but those
>>>> are gone since 8d993022c2ae ("docs: use literalinclude for system
>>>> requirements"). Not sure we can get rid of it for yp-docs as we only
>>>> override *some* variables in this file by set_versions.py so the
>>>> autobuilder won't replace poky.yaml.in, and the YAML is consumed by
>>>> sphinx/yocto-vars.py.
>>>>
>>>> If we don't plan on having comments in this file *ever*, then the YAML
>>>> dependency isn't necessary.
>>>>
>>>> Another option is INI and use configparser, c.f.
>>>> https://docs.python.org/3/library/configparser.html
>>>>
>>>> It seems that INI supports the ':' as key-value delimiter as well as
>>>> multiline strings (but not the way we used it for YAML!), so it could be
>>>> a drop-in replacement. We could even simply write to the file with
>>>> configparser.write() (though that will drop the in-file comments).
>>>
>>> If we directly write the ini file with configparser.write(), maybe we can just
>>> maintain the list of variable as Python instead, instead of a separate ".in"
>>> file?
>>>
>>
>> The benefit of having a .in file is that it's explicit what the user can
>> use in the docs (if we have yocto-vars.py! which we don't (yet?) in
>> BitBake).
>>
>> What's the usecase for DOCCONF_VERSION BTW? We only use it for setting
>> the current version (always "dev") but we never override it? Either in
>> release branches, in the autobuilder, only from set_versions.py do we
>> read it and override it. But then it's nowhere to be found in the docs
>> either so we don't use. Do we actually need this for BitBake?
> 
> We use it in doc/conf.py:
> 
> current_version = 'dev'
> 
> with open("bitbake.yaml") as data:
>      buff = data.read()
>      subst_vars = yaml.safe_load(buff)
>      current_version = subst_vars["DOCCONF_VERSION"]
> 
> So basically we just need a way to pass this information from set_versions.py to
> the sphinx-build command.
> 

DOCCONF_VERSION is always "dev". current_version defaults to "dev" too. 
So what is this *actually* used for?

>> We could push this even further if we wanted to and have a bitbake.py
>> instead? In some internal Sphinx project, we provide a skeleton with
>> options to provide (Python) variables through another file we then include:
>>
>> exec(open('variables.py').read())
>>
>> is what we use. This would support f-strings (can easily reference a
>> variable from another one), multiline strings, etc. No need to parse
>> anything, it just needs to be valid Python variable assignment.
> 
> So basically, you'd write a variables.py file from set_versions.py and read it
> from conf.py?
> 
> I'm starting to wonder if it just wouldn't be better do move all of this logic
> into doc/conf.py, actually.
> 

Is DOCCONF_VERSION supposed to be used from the docs? Is DOCCONF_VERSION 
supposed to be set externally such that it makes it to conf.py which 
does something with it? If no is the answer to both, then there's no 
reason for this mechanism to exist in the first place.

Cheers,
Quentin
diff mbox series

Patch

diff --git a/doc/.gitignore b/doc/.gitignore
index 69fa449dd96..40ebe76c088 100644
--- a/doc/.gitignore
+++ b/doc/.gitignore
@@ -1 +1,3 @@ 
 _build/
+sphinx-static/switchers.js
+bitbake.yaml
diff --git a/doc/Makefile b/doc/Makefile
index 996f01b7d5c..cdb054d3a52 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -27,9 +27,10 @@  publish: Makefile html singlehtml
 	sed -i -e 's@index.html#@singleindex.html#@g' $(BUILDDIR)/$(DESTDIR)/singleindex.html
 
 clean:
-	@rm -rf $(BUILDDIR)
+	@rm -rf $(BUILDDIR) sphinx-static/switchers.js bitbake.yaml
 
 # Catch-all target: route all unknown targets to Sphinx using the new
 # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
 %: Makefile
+	$(SOURCEDIR)/set_versions.py
 	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc/bitbake.yaml.in b/doc/bitbake.yaml.in
new file mode 100644
index 00000000000..4085582df60
--- /dev/null
+++ b/doc/bitbake.yaml.in
@@ -0,0 +1 @@ 
+DOCCONF_VERSION : "dev"
diff --git a/doc/conf.py b/doc/conf.py
index bce386624e2..5b28808c590 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -14,17 +14,25 @@ 
 # import sys
 # sys.path.insert(0, os.path.abspath('.'))
 
+import os
 import sys
 import datetime
+import yaml
 
 from pathlib import Path
 
-current_version = "dev"
+current_version = 'dev'
+
+with open("bitbake.yaml") as data:
+    buff = data.read()
+    subst_vars = yaml.safe_load(buff)
+    current_version = subst_vars["DOCCONF_VERSION"]
 
 # String used in sidebar
 version = 'Version: ' + current_version
 if current_version == 'dev':
     version = 'Version: Current Development'
+
 # Version seen in documentation_options.js and hence in js switchers code
 release = current_version
 
diff --git a/doc/set_versions.py b/doc/set_versions.py
new file mode 100755
index 00000000000..63fba2fe948
--- /dev/null
+++ b/doc/set_versions.py
@@ -0,0 +1,155 @@ 
+#!/usr/bin/env python3
+#
+# This is a minimal version of the set_versions.py from yocto-docs,
+# use to replace VERSIONS_PLACEHOLDER in switchers.js.in by a list defined below
+# with BITBAKE_ACTIVE_RELEASES in the environment.
+#
+# When the documentation is built with the autobuilder, the versions are
+# calculated based on the info found in set_versions.py from yocto-docs.
+#
+# Copyright Linux Foundation
+# Author: Antonin Godard <antonin.godard@bootlin.com>
+#
+# SPDX-License-Identifier: MIT
+#
+
+import collections
+import itertools
+import re
+import os
+import subprocess
+import sys
+
+from packaging.version import Version
+from typing import Tuple
+
+devbranch = "2.18"
+ltsseries = ["2.8", "2.0"]
+activereleases = ["2.16"] + ltsseries
+
+yocto_mapping = {
+    "2.18": "wrynose",
+    "2.16": "whinlatter",
+    "2.12": "walnascar",
+    "2.10": "styhead",
+    "2.8": "scarthgap",
+    "2.6": "nanbield",
+    "2.4": "mickledore",
+    "2.2": "langdale",
+    "2.0": "kirkstone",
+    "1.52": "honister",
+    "1.50": "hardknott",
+    "1.48": "gatesgarth",
+    "1.46": "dunfell",
+}
+
+bbver_re = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$")
+ourversion = None
+
+# Test that we are building from a Git repository
+try:
+    subprocess.run(["git", "rev-parse", "--is-inside-work-tree"],
+                   stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
+except subprocess.CalledProcessError:
+    sys.exit("Building bitbake's documentation must be done from its Git repository.\n"
+             "Clone the repository with the following command:\n"
+             "git clone https://git.openembedded.org/bitbake ")
+
+# Test tags exist and inform the user to fetch if not
+try:
+    subprocess.run(["git", "show", f"{ltsseries[0]}.0"],
+                   stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
+except subprocess.CalledProcessError:
+    sys.exit("Please run 'git fetch --tags' before building the documentation")
+
+
+# Try and figure out what we are
+tags = subprocess.run(["git", "tag", "--points-at", "HEAD"],
+                      stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                      universal_newlines=True).stdout
+for t in tags.split():
+    if re.match(bbver_re, t):
+        ourversion = t
+
+if ourversion:
+    # We're a tagged release
+    components = ourversion.split(".")
+    ourseries = f"{components[0]}.{components[1]}"
+else:
+    # We're floating on a branch
+    branch = subprocess.run(["git", "branch", "--show-current"],
+                            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                            universal_newlines=True).stdout.strip()
+
+    if branch == "" or branch not in list(yocto_mapping.keys()) + ["master", "master-next"]:
+        # We're not on a known release branch so we have to guess. Compare the
+        # numbers of commits from each release branch and assume the smallest
+        # number of commits is the one we're based off
+        possible_branch = None
+        branch_count = 0
+        for b in itertools.chain(yocto_mapping.keys(), ["master"]):
+            result = subprocess.run(["git", "log", "--format=oneline", "HEAD..origin/" + b],
+                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                                    universal_newlines=True)
+            if result.returncode == 0:
+                count = result.stdout.count('\n')
+                if not possible_branch or count < branch_count:
+                    print("Branch %s has count %s" % (b, count))
+                    possible_branch = b
+                    branch_count = count
+        if possible_branch:
+            branch = possible_branch
+        else:
+            branch = "master"
+        print("Nearest release branch estimated to be %s" % branch)
+
+    if branch == "master":
+        ourversion = "dev"
+        ourseries = devbranch
+    elif branch == "master-next":
+        ourversion = "next"
+        ourseries = devbranch
+    else:
+        ourversion = branch
+        head_commit = subprocess.run(["git", "rev-parse", "--short", "HEAD"],
+                                     stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                                     universal_newlines=True).stdout.strip()
+        branch_commit = subprocess.run(["git", "rev-parse", "--short", branch],
+                                        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                                        universal_newlines=True).stdout.strip()
+        if head_commit != branch_commit:
+            ourversion += f" ({head_commit})"
+        ourseries = branch
+
+print("Version calculated to be %s" % ourversion)
+print("Series calculated to be %s" % ourseries)
+
+replacements = {
+    "DOCCONF_VERSION": ourversion,
+}
+
+if os.path.exists("bitbake.yaml.in"):
+    with open("bitbake.yaml.in", "r") as r, open("bitbake.yaml", "w") as w:
+        lines = r.readlines()
+        for line in lines:
+            data = line.split(":")
+            k = data[0].strip()
+            if k in replacements:
+                w.write("%s : \"%s\"\n" % (k, replacements[k]))
+            else:
+                w.write(line)
+
+    print("bitbake.yaml generated from bitbake.yaml.in")
+
+with open("sphinx-static/switchers.js.in", "r") as r, \
+     open("sphinx-static/switchers.js", "w") as w:
+    lines = r.readlines()
+    for line in lines:
+        if "VERSIONS_PLACEHOLDER" in line:
+            if ourversion != "dev":
+                w.write(f"    'dev': 'Unstable (dev)',\n")
+            for series in activereleases:
+                w.write(f"    '{series}': '{series} ({yocto_mapping[series]})',\n")
+        else:
+            w.write(line)
+    print("switchers.js generated from switchers.js.in")
diff --git a/doc/sphinx-static/switchers.js b/doc/sphinx-static/switchers.js
deleted file mode 100644
index 32113cfa960..00000000000
--- a/doc/sphinx-static/switchers.js
+++ /dev/null
@@ -1,233 +0,0 @@ 
-(function() {
-  'use strict';
-
-  var all_versions = {
-    'dev': 'dev (3.2)',
-    '3.1.2': '3.1.2',
-    '3.0.3': '3.0.3',
-    '2.7.4': '2.7.4',
-  };
-
-  var all_doctypes = {
-      'single': 'Individual Webpages',
-      'mega': "All-in-one 'Mega' Manual",
-  };
-
-  // Simple version comparision
-  // Return 1 if a > b
-  // Return -1 if a < b
-  // Return 0 if a == b
-  function ver_compare(a, b) {
-    if (a == "dev") {
-       return 1;
-    }
-
-    if (a === b) {
-       return 0;
-    }
-
-    var a_components = a.split(".");
-    var b_components = b.split(".");
-
-    var len = Math.min(a_components.length, b_components.length);
-
-    // loop while the components are equal
-    for (var i = 0; i < len; i++) {
-        // A bigger than B
-        if (parseInt(a_components[i]) > parseInt(b_components[i])) {
-            return 1;
-        }
-
-        // B bigger than A
-        if (parseInt(a_components[i]) < parseInt(b_components[i])) {
-            return -1;
-        }
-    }
-
-    // If one's a prefix of the other, the longer one is greater.
-    if (a_components.length > b_components.length) {
-        return 1;
-    }
-
-    if (a_components.length < b_components.length) {
-        return -1;
-    }
-
-    // Otherwise they are the same.
-    return 0;
-  }
-
-  function build_version_select(current_series, current_version) {
-    var buf = ['<select>'];
-
-    $.each(all_versions, function(version, title) {
-      var series = version.substr(0, 3);
-      if (series == current_series) {
-        if (version == current_version)
-            buf.push('<option value="' + version + '" selected="selected">' + title + '</option>');
-        else
-            buf.push('<option value="' + version + '">' + title + '</option>');
-
-        if (version != current_version)
-            buf.push('<option value="' + current_version + '" selected="selected">' + current_version + '</option>');
-      } else {
-        buf.push('<option value="' + version + '">' + title + '</option>');
-      }
-    });
-
-    buf.push('</select>');
-    return buf.join('');
-  }
-
-  function build_doctype_select(current_doctype) {
-    var buf = ['<select>'];
-
-    $.each(all_doctypes, function(doctype, title) {
-      if (doctype == current_doctype)
-        buf.push('<option value="' + doctype + '" selected="selected">' +
-                 all_doctypes[current_doctype] + '</option>');
-      else
-        buf.push('<option value="' + doctype + '">' + title + '</option>');
-    });
-    if (!(current_doctype in all_doctypes)) {
-        // In case we're browsing a doctype that is not yet in all_doctypes.
-        buf.push('<option value="' + current_doctype + '" selected="selected">' +
-                 current_doctype + '</option>');
-        all_doctypes[current_doctype] = current_doctype;
-    }
-    buf.push('</select>');
-    return buf.join('');
-  }
-
-  function navigate_to_first_existing(urls) {
-    // Navigate to the first existing URL in urls.
-    var url = urls.shift();
-
-    // Web browsers won't redirect file:// urls to file urls using ajax but
-    // its useful for local testing
-    if (url.startsWith("file://")) {
-      window.location.href = url;
-      return;
-    }
-
-    if (urls.length == 0) {
-      window.location.href = url;
-      return;
-    }
-    $.ajax({
-      url: url,
-      success: function() {
-        window.location.href = url;
-      },
-      error: function() {
-        navigate_to_first_existing(urls);
-      }
-    });
-  }
-
-  function get_docroot_url() {
-    var url = window.location.href;
-    var root = DOCUMENTATION_OPTIONS.URL_ROOT;
-
-    var urlarray = url.split('/');
-    // Trim off anything after '/'
-    urlarray.pop();
-    var depth = (root.match(/\.\.\//g) || []).length;
-    for (var i = 0; i < depth; i++) {
-      urlarray.pop();
-    }
-
-    return urlarray.join('/') + '/';
-  }
-
-  function on_version_switch() {
-    var selected_version = $(this).children('option:selected').attr('value');
-    var url = window.location.href;
-    var current_version = DOCUMENTATION_OPTIONS.VERSION;
-    var docroot = get_docroot_url()
-
-    var new_versionpath = selected_version + '/';
-    if (selected_version == "dev")
-        new_versionpath = '';
-
-    // dev versions have no version prefix
-    if (current_version == "dev") {
-        var new_url = docroot + new_versionpath + url.replace(docroot, "");
-        var fallback_url = docroot + new_versionpath;
-    } else {
-        var new_url = url.replace('/' + current_version + '/', '/' + new_versionpath);
-        var fallback_url = new_url.replace(url.replace(docroot, ""), "");
-    }
-
-    console.log(get_docroot_url())
-    console.log(url + " to url " + new_url);
-    console.log(url + " to fallback " + fallback_url);
-
-    if (new_url != url) {
-      navigate_to_first_existing([
-        new_url,
-        fallback_url,
-        'https://www.yoctoproject.org/docs/',
-      ]);
-    }
-  }
-
-  function on_doctype_switch() {
-    var selected_doctype = $(this).children('option:selected').attr('value');
-    var url = window.location.href;
-    if (selected_doctype == 'mega') {
-      var docroot = get_docroot_url()
-      var current_version = DOCUMENTATION_OPTIONS.VERSION;
-      // Assume manuals before 3.2 are using old docbook mega-manual
-      if (ver_compare(current_version, "3.2") < 0) {
-        var new_url = docroot + "mega-manual/mega-manual.html";
-      } else {
-        var new_url = docroot + "singleindex.html";
-      }
-    } else {
-      var new_url = url.replace("singleindex.html", "index.html")
-    }
-
-    if (new_url != url) {
-      navigate_to_first_existing([
-        new_url,
-        'https://www.yoctoproject.org/docs/',
-      ]);
-    }
-  }
-
-  // Returns the current doctype based upon the url
-  function doctype_segment_from_url(url) {
-    if (url.includes("singleindex") || url.includes("mega-manual"))
-      return "mega";
-    return "single";
-  }
-
-  $(document).ready(function() {
-    var release = DOCUMENTATION_OPTIONS.VERSION;
-    var current_doctype = doctype_segment_from_url(window.location.href);
-    var current_series = release.substr(0, 3);
-    var version_select = build_version_select(current_series, release);
-
-    $('.version_switcher_placeholder').html(version_select);
-    $('.version_switcher_placeholder select').bind('change', on_version_switch);
-
-    var doctype_select = build_doctype_select(current_doctype);
-
-    $('.doctype_switcher_placeholder').html(doctype_select);
-    $('.doctype_switcher_placeholder select').bind('change', on_doctype_switch);
-
-    if (ver_compare(release, "3.1") < 0) {
-      $('#outdated-warning').html('Version ' + release + ' of the project is now considered obsolete, please select and use a more recent version');
-      $('#outdated-warning').css('padding', '.5em');
-    } else if (release != "dev") {
-      $.each(all_versions, function(version, title) {
-        var series = version.substr(0, 3);
-        if (series == current_series && version != release) {
-          $('#outdated-warning').html('This document is for outdated version ' + release + ', you should select the latest release version in this series, ' + version + '.');
-          $('#outdated-warning').css('padding', '.5em');
-        }
-      });
-    }
-  });
-})();
diff --git a/doc/sphinx-static/switchers.js.in b/doc/sphinx-static/switchers.js.in
new file mode 100644
index 00000000000..108d1a7d77f
--- /dev/null
+++ b/doc/sphinx-static/switchers.js.in
@@ -0,0 +1,247 @@ 
+// SPDX-License-Identifier: MIT
+(function () {
+  "use strict";
+
+  var all_versions = {
+    VERSIONS_PLACEHOLDER,
+  };
+
+  var all_doctypes = {
+    single: "Individual Webpages",
+    mega: "All-in-one 'Mega' Manual",
+  };
+
+  // Simple version comparision
+  // Return 1 if a > b
+  // Return -1 if a < b
+  // Return 0 if a == b
+  function ver_compare(a, b) {
+    if (a == "dev") {
+      return 1;
+    }
+
+    if (a === b) {
+      return 0;
+    }
+
+    var a_components = a.split(".");
+    var b_components = b.split(".");
+
+    var len = Math.min(a_components.length, b_components.length);
+
+    // loop while the components are equal
+    for (var i = 0; i < len; i++) {
+      // A bigger than B
+      if (parseInt(a_components[i]) > parseInt(b_components[i])) {
+        return 1;
+      }
+
+      // B bigger than A
+      if (parseInt(a_components[i]) < parseInt(b_components[i])) {
+        return -1;
+      }
+    }
+
+    // If one's a prefix of the other, the longer one is greater.
+    if (a_components.length > b_components.length) {
+      return 1;
+    }
+
+    if (a_components.length < b_components.length) {
+      return -1;
+    }
+
+    // Otherwise they are the same.
+    return 0;
+  }
+
+  function build_version_select(current_version) {
+    var buf = ["<select>"];
+
+    $.each(all_versions, function (version, title) {
+      if (current_version != version) {
+        buf.push(
+          '<option value="' +
+          version +
+          '" selected="selected">' +
+          title +
+          "</option>",
+        );
+      }
+    });
+
+    var current_title = current_version;
+    if (current_version in all_versions) {
+      current_title = all_versions[current_version];
+    }
+    buf.push(
+      '<option value="' +
+        current_version +
+        '" selected="selected">' +
+        current_title +
+        "</option>",
+    );
+
+    buf.push("</select>");
+    return buf.join("");
+  }
+
+  function build_doctype_select(current_doctype) {
+    var buf = ["<select>"];
+
+    $.each(all_doctypes, function (doctype, title) {
+      if (doctype == current_doctype)
+        buf.push(
+          '<option value="' +
+            doctype +
+            '" selected="selected">' +
+            all_doctypes[current_doctype] +
+            "</option>",
+        );
+      else buf.push('<option value="' + doctype + '">' + title + "</option>");
+    });
+    if (!(current_doctype in all_doctypes)) {
+      // In case we're browsing a doctype that is not yet in all_doctypes.
+      buf.push(
+        '<option value="' +
+          current_doctype +
+          '" selected="selected">' +
+          current_doctype +
+          "</option>",
+      );
+      all_doctypes[current_doctype] = current_doctype;
+    }
+    buf.push("</select>");
+    return buf.join("");
+  }
+
+  function navigate_to_first_existing(urls) {
+    // Navigate to the first existing URL in urls.
+    var url = urls.shift();
+
+    // Web browsers won't redirect file:// urls to file urls using ajax but
+    // its useful for local testing
+    if (url.startsWith("file://")) {
+      window.location.href = url;
+      return;
+    }
+
+    if (urls.length == 0) {
+      window.location.href = url;
+      return;
+    }
+    $.ajax({
+      url: url,
+      success: function () {
+        window.location.href = url;
+      },
+      error: function () {
+        navigate_to_first_existing(urls);
+      },
+    });
+  }
+
+  function get_docroot_url() {
+    var url = window.location.href;
+    // Try to get the variable from documentation_options.js
+    var root = DOCUMENTATION_OPTIONS.URL_ROOT;
+    if (root == null) {
+      // In recent versions of Sphinx, URL_ROOT was removed from
+      // documentation_options.js, so get it like searchtools.js does.
+      root = document.documentElement.dataset.content_root;
+    }
+
+    var urlarray = url.split("/");
+    // Trim off anything after '/'
+    urlarray.pop();
+    var depth = (root.match(/\.\.\//g) || []).length;
+    for (var i = 0; i < depth; i++) {
+      urlarray.pop();
+    }
+
+    return urlarray.join("/") + "/";
+  }
+
+  function on_version_switch() {
+    var selected_version = $(this).children("option:selected").attr("value");
+    var url = window.location.href;
+    var current_version = DOCUMENTATION_OPTIONS.VERSION;
+    var docroot = get_docroot_url();
+
+    var new_versionpath = selected_version + "/";
+
+    // latest tag is also the default page (without version information)
+    if (docroot.endsWith("dev/")) {
+      var new_url = url.replace("/dev/", "/" + new_versionpath);
+      var fallback_url = new_url.replace(url.replace(docroot, ""), "");
+    } else if (docroot.endsWith(current_version + "/") == false) {
+      var new_url = docroot + new_versionpath + url.replace(docroot, "");
+      var fallback_url = docroot + new_versionpath;
+    } else {
+      var new_url = url.replace(
+        "/" + current_version + "/",
+        "/" + new_versionpath,
+      );
+      var fallback_url = new_url.replace(url.replace(docroot, ""), "");
+    }
+
+    console.log(url + " to url " + new_url);
+    console.log(url + " to fallback " + fallback_url);
+
+    if (new_url != url) {
+      navigate_to_first_existing([
+        new_url,
+        fallback_url,
+        "https://www.yoctoproject.org/bitbake/",
+      ]);
+    }
+  }
+
+  function on_doctype_switch() {
+    var selected_doctype = $(this).children("option:selected").attr("value");
+    var url = window.location.href;
+    if (selected_doctype == "mega") {
+      var docroot = get_docroot_url();
+      var current_version = DOCUMENTATION_OPTIONS.VERSION;
+      var new_url = docroot + "singleindex.html";
+    } else {
+      var new_url = url.replace("singleindex.html", "index.html");
+    }
+
+    if (new_url != url) {
+      navigate_to_first_existing([
+        new_url,
+        "https://www.yoctoproject.org/docs/",
+      ]);
+    }
+  }
+
+  // Returns the current doctype based upon the url
+  function doctype_segment_from_url(url) {
+    if (url.includes("singleindex") || url.includes("mega-manual"))
+      return "mega";
+    return "single";
+  }
+
+  $(document).ready(function () {
+    var release = DOCUMENTATION_OPTIONS.VERSION;
+    var current_doctype = doctype_segment_from_url(window.location.href);
+    var current_series = release.substr(0, 3);
+    var version_select = build_version_select(release);
+
+    $(".version_switcher_placeholder").html(version_select);
+    $(".version_switcher_placeholder select").bind("change", on_version_switch);
+
+    var doctype_select = build_doctype_select(current_doctype);
+
+    $(".doctype_switcher_placeholder").html(doctype_select);
+    $(".doctype_switcher_placeholder select").bind("change", on_doctype_switch);
+
+    if (!(["dev", "next"].includes(release)) && !(release in all_versions)) {
+      $("#outdated-warning").html(
+        "Version " + release + " of the project is now considered obsolete, please select and use a more recent version",
+      );
+      $("#outdated-warning").css("padding", ".5em");
+    }
+  });
+})();