BLD Add DISTDIR variable. ### Description

Adds `DISTDIR` env variable that points to the directory where libraries should be copied to when building shared libraries with recipe.

### Checklists

- [x] Add new / update outdated documentation

# Configuration file for the Sphinx documentation builder.

# -- Path setup --------------------------------------------------------------

import atexit
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
from unittest import mock

import micropip

panels_add_bootstrap_css = False

# -- Project information -----------------------------------------------------

project = "Pyodide"
copyright = "2019-2022, Pyodide contributors and Mozilla"

# -- General configuration ---------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.autosummary",
    "sphinx.ext.intersphinx",
    "sphinx.ext.napoleon",
    "myst_parser",
    "sphinx_js",
    "sphinx_click",
    "autodocsumm",
    "sphinx_pyodide",
    "sphinx_argparse_cli",
    "sphinx_issues",
    "sphinx_autodoc_typehints",
    "sphinx_design",  # Used for tabs in building-from-sources.md
]


myst_enable_extensions = ["substitution"]

js_language = "typescript"
jsdoc_config_path = "../src/js/tsconfig.json"
root_for_relative_js_paths = "../src/"
issues_github_path = "pyodide/pyodide"

versionwarning_message = (
    "This is the development version of the documentation. "
    'See <a href="https://pyodide.org/">here</a> for latest stable '
    "documentation. Please do not use Pyodide with non "
    "versioned (`dev`) URLs from the CDN for deployed applications!"
)

autosummary_generate = True
autodoc_default_flags = ["members", "inherited-members"]

intersphinx_mapping = {
    "python": ("https://docs.python.org/3.10", None),
    "micropip": (f"https://micropip.pyodide.org/en/v{micropip.__version__}/", None),
    "numpy": ("https://numpy.org/doc/stable/", None),
}

# Add modules to be mocked.
mock_modules = ["tomli"]

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

# The suffix(es) of source filenames.
source_suffix = [".rst", ".md"]

# The master toctree document.
master_doc = "index"

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = [
    "_build",
    "Thumbs.db",
    ".DS_Store",
    "README.md",
    "sphinx_pyodide",
    ".*",
]

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None

# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_book_theme"
html_logo = "_static/img/pyodide-logo.png"

# theme-specific options
html_theme_options: dict[str, Any] = {
    "announcement": "",
}

# paths that contain custom static files (such as style sheets)
html_static_path = ["_static"]


html_css_files = [
    "css/pyodide.css",
]

# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
# html_sidebars = {}

# -- Options for HTMLHelp output ---------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = "Pyodidedoc"

# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]

# Try not to cause side effects if we are imported incidentally.

IN_SPHINX = "sphinx" in sys.modules and hasattr(sys.modules["sphinx"], "application")
IN_READTHEDOCS = "READTHEDOCS" in os.environ
IN_READTHEDOCS_LATEST = (
    IN_READTHEDOCS and os.environ.get("READTHEDOCS_VERSION") == "latest"
)


base_dir = Path(__file__).resolve().parent.parent
extra_sys_path_dirs = [
    str(base_dir),
    str(base_dir / "pyodide-build"),
    str(base_dir / "src/py"),
    str(base_dir / "packages/micropip/src"),
]


if IN_SPHINX:
    # sphinx_pyodide is imported before setup() is called because it's a sphinx
    # extension, so we need it to be on the path early. Everything else can be
    # added to the path in setup().
    #
    # TODO: pip install -e sphinx-pyodide instead.
    sys.path = [str(base_dir / "docs/sphinx_pyodide")] + sys.path


def patch_docs_argspec():
    import builtins

    from sphinx_pyodide.util import docs_argspec

    # override docs_argspec, _pyodide.docs_argspec will read this value back.
    # Must do this before importing pyodide!
    setattr(builtins, "--docs_argspec--", docs_argspec)


def patch_inspect():
    # Monkey patch for python3.11 incompatible code
    import inspect

    if not hasattr(inspect, "getargspec"):
        inspect.getargspec = inspect.getfullargspec  # type: ignore[assignment]


def prevent_parens_after_js_class_xrefs():
    from sphinx.domains.javascript import JavaScriptDomain, JSXRefRole

    JavaScriptDomain.roles["class"] = JSXRefRole()


def apply_patches():
    patch_docs_argspec()
    patch_inspect()
    prevent_parens_after_js_class_xrefs()


def calculate_pyodide_version(app):
    import pyodide

    config = app.config

    # The full version, including alpha/beta/rc tags.
    config.release = config.version = version = pyodide.__version__

    if ".dev" in version or os.environ.get("READTHEDOCS_VERSION") == "latest":
        CDN_URL = "https://cdn.jsdelivr.net/pyodide/dev/full/"
    else:
        CDN_URL = f"https://cdn.jsdelivr.net/pyodide/v{version}/full/"

    app.config.CDN_URL = CDN_URL
    app.config.html_title = f"Version {version}"

    app.config.global_replacements = {
        "{{PYODIDE_CDN_URL}}": CDN_URL,
        "{{VERSION}}": version,
    }


def set_announcement_message():
    html_theme_options["announcement"] = (
        versionwarning_message if IN_READTHEDOCS_LATEST else ""
    )


def write_console_html(app):
    # Make console.html file
    env = {"PYODIDE_BASE_URL": app.config.CDN_URL}
    os.makedirs(app.outdir, exist_ok=True)
    os.makedirs("../dist", exist_ok=True)
    res = subprocess.check_output(
        ["make", "-C", "..", "dist/console.html"],
        env=env,
        stderr=subprocess.STDOUT,
        encoding="utf-8",
    )
    print(res)

    # insert the Plausible analytics script to console.html
    console_html_lines = (
        Path("../dist/console.html").read_text().splitlines(keepends=True)
    )
    for idx, line in enumerate(list(console_html_lines)):
        if 'pyodide.js">' in line:
            # insert the analytics script after the `pyodide.js` script
            console_html_lines.insert(
                idx,
                '<script defer data-domain="pyodide.org" src="https://plausible.io/js/plausible.js"></script>\n',
            )
            break
    else:
        raise ValueError("Could not find pyodide.js in the <head> section")
    output_path = Path(app.outdir) / "console.html"
    output_path.write_text("".join(console_html_lines))


def ensure_typedoc_on_path():
    if shutil.which("typedoc"):
        return
    os.environ["PATH"] += f':{str(Path("../src/js/node_modules/.bin").resolve())}'
    print(os.environ["PATH"])
    if shutil.which("typedoc"):
        return
    if IN_READTHEDOCS:
        subprocess.run(["npm", "ci"], cwd="../src/js")
    if shutil.which("typedoc"):
        return
    raise Exception(
        "Before building the Pyodide docs you must run 'npm install' in 'src/js'."
    )


def create_generated_typescript_files(app):
    shutil.copy("../src/core/pyproxy.ts", "../src/js/pyproxy.gen.ts")
    shutil.copy("../src/core/error_handling.ts", "../src/js/error_handling.gen.ts")
    app.config.js_source_path = [str(x) for x in Path("../src/js").glob("*.ts")]

    def remove_pyproxy_gen_ts():
        Path("../src/js/pyproxy.gen.ts").unlink(missing_ok=True)

    atexit.register(remove_pyproxy_gen_ts)


def prune_webloop_docs():
    # Prevent API docs for webloop methods: they are the same as for base event loop
    # and it clutters api docs too much
    from sphinx_pyodide.util import delete_attrs

    import pyodide.console
    import pyodide.webloop

    delete_attrs(pyodide.webloop.WebLoop)
    delete_attrs(pyodide.webloop.WebLoopPolicy)
    delete_attrs(pyodide.console.PyodideConsole)

    for module in mock_modules:
        sys.modules[module] = mock.Mock()


def prune_jsproxy_constructor_docs():
    from pyodide.ffi import JsProxy

    del JsProxy.__new__


def prune_docs():
    prune_webloop_docs()
    prune_jsproxy_constructor_docs()


# https://github.com/sphinx-doc/sphinx/issues/4054
def global_replace(app, docname, source):
    result = source[0]
    for key in app.config.global_replacements:
        result = result.replace(key, app.config.global_replacements[key])
    source[0] = result


always_document_param_types = True


def typehints_formatter(annotation, config):
    """Adjust the rendering of various types that sphinx_autodoc_typehints mishandles"""
    from sphinx_autodoc_typehints import (
        get_annotation_class_name,
        get_annotation_module,
    )

    try:
        module = get_annotation_module(annotation)
        class_name = get_annotation_class_name(annotation, module)
    except ValueError:
        return None
    full_name = f"{module}.{class_name}"
    if full_name == "typing.TypeVar":
        # The way sphinx-autodoc-typehints renders TypeVar is too noisy for my
        # taste
        return f"``{annotation.__name__}``"
    if full_name == "ast.Module":
        return "`Module <https://docs.python.org/3/library/ast.html#module-ast>`_"
    return None


def setup(app):
    sys.path = extra_sys_path_dirs + sys.path
    app.add_config_value("global_replacements", {}, True)
    app.add_config_value("CDN_URL", "", True)
    app.connect("source-read", global_replace)

    set_announcement_message()
    apply_patches()
    calculate_pyodide_version(app)
    ensure_typedoc_on_path()
    create_generated_typescript_files(app)
    write_console_html(app)
    prune_docs()

# Configuration file for the Sphinx documentation builder.

# -- Path setup --------------------------------------------------------------

import atexit
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
from unittest import mock

import micropip

panels_add_bootstrap_css = False

# -- Project information -----------------------------------------------------

project = "Pyodide"
copyright = "2019-2022, Pyodide contributors and Mozilla"

# -- General configuration ---------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.autosummary",
    "sphinx.ext.intersphinx",
    "sphinx.ext.napoleon",
    "myst_parser",
    "sphinx_js",
    "sphinx_click",
    "autodocsumm",
    "sphinx_pyodide",
    "sphinx_argparse_cli",
    "sphinx_issues",
    "sphinx_autodoc_typehints",
    "sphinx_design",  # Used for tabs in building-from-sources.md
]


myst_enable_extensions = ["substitution"]

js_language = "typescript"
jsdoc_config_path = "../src/js/tsconfig.json"
root_for_relative_js_paths = "../src/"
issues_github_path = "pyodide/pyodide"

versionwarning_message = (
    "This is the development version of the documentation. "
    'See <a href="https://pyodide.org/">here</a> for latest stable '
    "documentation. Please do not use Pyodide with non "
    "versioned (`dev`) URLs from the CDN for deployed applications!"
)

autosummary_generate = True
autodoc_default_flags = ["members", "inherited-members"]

intersphinx_mapping = {
    "python": ("https://docs.python.org/3.10", None),
    "micropip": (f"https://micropip.pyodide.org/en/v{micropip.__version__}/", None),
    "numpy": ("https://numpy.org/doc/stable/", None),
}

# Add modules to be mocked.
mock_modules = ["tomli"]

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

# The suffix(es) of source filenames.
source_suffix = [".rst", ".md"]

# The master toctree document.
master_doc = "index"

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = [
    "_build",
    "Thumbs.db",
    ".DS_Store",
    "README.md",
    "sphinx_pyodide",
    ".*",
]

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None

# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_book_theme"
html_logo = "_static/img/pyodide-logo.png"

# theme-specific options
html_theme_options: dict[str, Any] = {
    "announcement": "",
}

# paths that contain custom static files (such as style sheets)
html_static_path = ["_static"]


html_css_files = [
    "css/pyodide.css",
]

# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
# html_sidebars = {}

# -- Options for HTMLHelp output ---------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = "Pyodidedoc"

# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]

# Try not to cause side effects if we are imported incidentally.

IN_SPHINX = "sphinx" in sys.modules and hasattr(sys.modules["sphinx"], "application")
IN_READTHEDOCS = "READTHEDOCS" in os.environ
IN_READTHEDOCS_LATEST = (
    IN_READTHEDOCS and os.environ.get("READTHEDOCS_VERSION") == "latest"
)


base_dir = Path(__file__).resolve().parent.parent
extra_sys_path_dirs = [
    str(base_dir),
    str(base_dir / "pyodide-build"),
    str(base_dir / "src/py"),
    str(base_dir / "packages/micropip/src"),
]


if IN_SPHINX:
    # sphinx_pyodide is imported before setup() is called because it's a sphinx
    # extension, so we need it to be on the path early. Everything else can be
    # added to the path in setup().
    #
    # TODO: pip install -e sphinx-pyodide instead.
    sys.path = [str(base_dir / "docs/sphinx_pyodide")] + sys.path


def patch_docs_argspec():
    import builtins

    from sphinx_pyodide.util import docs_argspec

    # override docs_argspec, _pyodide.docs_argspec will read this value back.
    # Must do this before importing pyodide!
    setattr(builtins, "--docs_argspec--", docs_argspec)


def patch_inspect():
    # Monkey patch for python3.11 incompatible code
    import inspect

    if not hasattr(inspect, "getargspec"):
        inspect.getargspec = inspect.getfullargspec  # type: ignore[assignment]


def prevent_parens_after_js_class_xrefs():
    from sphinx.domains.javascript import JavaScriptDomain, JSXRefRole

    JavaScriptDomain.roles["class"] = JSXRefRole()


def apply_patches():
    patch_docs_argspec()
    patch_inspect()
    prevent_parens_after_js_class_xrefs()


def calculate_pyodide_version(app):
    import pyodide

    config = app.config

    # The full version, including alpha/beta/rc tags.
    config.release = config.version = version = pyodide.__version__

    if ".dev" in version or os.environ.get("READTHEDOCS_VERSION") == "latest":
        CDN_URL = "https://cdn.jsdelivr.net/pyodide/dev/full/"
    else:
        CDN_URL = f"https://cdn.jsdelivr.net/pyodide/v{version}/full/"

    app.config.CDN_URL = CDN_URL
    app.config.html_title = f"Version {version}"

    app.config.global_replacements = {
        "{{PYODIDE_CDN_URL}}": CDN_URL,
        "{{VERSION}}": version,
    }


def set_announcement_message():
    html_theme_options["announcement"] = (
        versionwarning_message if IN_READTHEDOCS_LATEST else ""
    )


def write_console_html(app):
    # Make console.html file
    env = {"PYODIDE_BASE_URL": app.config.CDN_URL}
    os.makedirs(app.outdir, exist_ok=True)
    os.makedirs("../dist", exist_ok=True)
    res = subprocess.check_output(
        ["make", "-C", "..", "dist/console.html"],
        env=env,
        stderr=subprocess.STDOUT,
        encoding="utf-8",
    )
    print(res)

    # insert the Plausible analytics script to console.html
    console_html_lines = (
        Path("../dist/console.html").read_text().splitlines(keepends=True)
    )
    for idx, line in enumerate(list(console_html_lines)):
        if 'pyodide.js">' in line:
            # insert the analytics script after the `pyodide.js` script
            console_html_lines.insert(
                idx,
                '<script defer data-domain="pyodide.org" src="https://plausible.io/js/plausible.js"></script>\n',
            )
            break
    else:
        raise ValueError("Could not find pyodide.js in the <head> section")
    output_path = Path(app.outdir) / "console.html"
    output_path.write_text("".join(console_html_lines))


def ensure_typedoc_on_path():
    if shutil.which("typedoc"):
        return
    os.environ["PATH"] += f':{str(Path("../src/js/node_modules/.bin").resolve())}'
    print(os.environ["PATH"])
    if shutil.which("typedoc"):
        return
    if IN_READTHEDOCS:
        subprocess.run(["npm", "ci"], cwd="../src/js")
    if shutil.which("typedoc"):
        return
    raise Exception(
        "Before building the Pyodide docs you must run 'npm install' in 'src/js'."
    )


def create_generated_typescript_files(app):
    shutil.copy("../src/core/pyproxy.ts", "../src/js/pyproxy.gen.ts")
    shutil.copy("../src/core/error_handling.ts", "../src/js/error_handling.gen.ts")
    app.config.js_source_path = [str(x) for x in Path("../src/js").glob("*.ts")]

    def remove_pyproxy_gen_ts():
        Path("../src/js/pyproxy.gen.ts").unlink(missing_ok=True)

    atexit.register(remove_pyproxy_gen_ts)


def prune_webloop_docs():
    # Prevent API docs for webloop methods: they are the same as for base event loop
    # and it clutters api docs too much
    from sphinx_pyodide.util import delete_attrs

    import pyodide.console
    import pyodide.webloop

    delete_attrs(pyodide.webloop.WebLoop)
    delete_attrs(pyodide.webloop.WebLoopPolicy)
    delete_attrs(pyodide.console.PyodideConsole)

    for module in mock_modules:
        sys.modules[module] = mock.Mock()


def prune_jsproxy_constructor_docs():
    from pyodide.ffi import JsProxy

    del JsProxy.__new__


def prune_docs():
    prune_webloop_docs()
    prune_jsproxy_constructor_docs()


# https://github.com/sphinx-doc/sphinx/issues/4054
def global_replace(app, docname, source):
    result = source[0]
    for key in app.config.global_replacements:
        result = result.replace(key, app.config.global_replacements[key])
    source[0] = result


always_document_param_types = True


def typehints_formatter(annotation, config):
    """Adjust the rendering of various types that sphinx_autodoc_typehints mishandles"""
    from sphinx_autodoc_typehints import (
        get_annotation_class_name,
        get_annotation_module,
    )

    try:
        module = get_annotation_module(annotation)
        class_name = get_annotation_class_name(annotation, module)
    except ValueError:
        return None
    full_name = f"{module}.{class_name}"
    if full_name == "typing.TypeVar":
        # The way sphinx-autodoc-typehints renders TypeVar is too noisy for my
        # taste
        return f"``{annotation.__name__}``"
    if full_name == "ast.Module":
        return "`Module <https://docs.python.org/3/library/ast.html#module-ast>`_"
    return None


def setup(app):
    sys.path = extra_sys_path_dirs + sys.path
    app.add_config_value("global_replacements", {}, True)
    app.add_config_value("CDN_URL", "", True)
    app.connect("source-read", global_replace)

    set_announcement_message()
    apply_patches()
    calculate_pyodide_version(app)
    ensure_typedoc_on_path()
    create_generated_typescript_files(app)
    write_console_html(app)
    prune_docs()
