diff --git a/docs/Dockerfile.docs b/docs/Dockerfile.docs index cd9e712dcd..b3f078baaa 100644 --- a/docs/Dockerfile.docs +++ b/docs/Dockerfile.docs @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -FROM ubuntu:22.04 +FROM ubuntu:24.04 # various documentation dependencies RUN apt-get update -q=2 \ @@ -53,6 +53,7 @@ RUN wget https://github.com/pseudomuto/protoc-gen-doc/releases/download/v1.3.2/p && rm /tmp/protoc-gen-doc.tar.gz # install sphinx et al +ENV PIP_BREAK_SYSTEM_PACKAGES=1 RUN pip3 install \ ablog \ attrs \ @@ -75,8 +76,13 @@ RUN pip3 install \ sphinxcontrib-bibtex RUN pip3 install \ - --index-url https://urm.nvidia.com/artifactory/api/pypi/ct-omniverse-pypi/simple/ \ - nvidia-sphinx-theme + --extra-index-url https://pypi.nvidia.com \ + nvidia-sphinx-theme \ + sphinx==7.4.7 + +RUN curl -fL https://install-cli.jfrog.io | sh + +RUN git config --global --add safe.directory "*" # Set visitor script to be included on every HTML page ENV VISITS_COUNTING_SCRIPT="//assets.adobedtm.com/b92787824f2e0e9b68dc2e993f9bd995339fe417/satelliteLib-7ba51e58dc61bcb0e9311aadd02a0108ab24cc6c.js" diff --git a/docs/conf.py b/docs/conf.py index 0b44f7c8b2..387e0f3c37 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -35,9 +35,12 @@ # -- Path setup -------------------------------------------------------------- import json +import logging import os import re +import subprocess from datetime import date +from logging.handlers import RotatingFileHandler # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -61,6 +64,45 @@ # at the end of the file. # current_dir = os.getcwd() # os.chdir("docs") +# -- Setup logger ------------------------------------------------------------ + + +def setup_logger(name, log_file, level=logging.INFO, max_bytes=1048576, backup_count=5): + logger = logging.getLogger(name) + logger.setLevel(level) + + # Prevent adding multiple handlers if the function is called multiple times + if not logger.handlers: + # Create handlers + file_handler = RotatingFileHandler( + log_file, maxBytes=max_bytes, backupCount=backup_count + ) + console_handler = logging.StreamHandler() + + # Set the logging level for handlers + file_handler.setLevel(level) + console_handler.setLevel(level) + + # Create a logging format + BLUE = "\033[94m" + RESET = "\033[0m" + formatter = logging.Formatter( + f"{BLUE}%(asctime)s - %(name)s - %(levelname)s - {RESET}%(message)s" + ) + file_handler.setFormatter(formatter) + console_handler.setFormatter(formatter) + + # Add handlers to the logger + logger.addHandler(file_handler) + logger.addHandler(console_handler) + return logger + + +logger = setup_logger( + os.path.basename(__file__), + os.environ.get("TRITON_SERVER_DOCS_LOG_FILE", "/tmp/docs.log"), +) +logger.info(f"Defined logger for {os.path.basename(__file__)}") # -- Project information ----------------------------------------------------- @@ -70,14 +112,19 @@ # Get the version of Triton this is building. version_long = "0.0.0" +logger.info(f"Getting version from ../TRITON_VERSION") with open("../TRITON_VERSION") as f: version_long = f.readline() version_long = version_long.strip() + logger.info(f"Version: {version_long}") + version_short = re.match(r"^[\d]+\.[\d]+\.[\d]+", version_long).group(0) +logger.info(f"Version short: {version_short}") version_short_split = version_short.split(".") +logger.info(f"Version short split: {version_short_split}") one_before = f"{version_short_split[0]}.{int(version_short_split[1]) - 1}.{version_short_split[2]}" - +logger.info(f"One before: {one_before}") # maintain left-side bar toctrees in `contents` file # so it doesn't show up needlessly in the index page @@ -180,7 +227,7 @@ "json_url": "https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/_static/switcher.json", "version_match": one_before if "dev" in version_long else version_short, }, - "navbar_start": ["navbar-logo", "version-switcher"], + # "navbar_start": ["navbar-logo", "version-switcher"], "primary_sidebar_end": [], } @@ -194,6 +241,8 @@ } ) +logger.info(f"html_theme_options: {html_theme_options}") + deploy_ngc_org = "nvidia" deploy_ngc_team = "triton" myst_substitutions = { @@ -203,6 +252,8 @@ else deploy_ngc_org, } +logger.info(f"myst_substitutions: {myst_substitutions}") + def ultimateReplace(app, docname, source): result = source[0] @@ -219,6 +270,7 @@ def ultimateReplace(app, docname, source): if deploy_ngc_team else deploy_ngc_org, } +logger.info(f"ultimate_replacements: {ultimate_replacements}") # bibtex_bibfiles = ["references.bib"] # To test that style looks good with common bibtex config @@ -233,25 +285,24 @@ def ultimateReplace(app, docname, source): # SETUP SWITCHER ############################### switcher_path = os.path.join(html_static_path[0], "switcher.json") +logger.info(f"switcher_path: {switcher_path}") versions = [] -# Triton 2 releases -correction = -1 if "dev" in version_long else 0 -upper_bound = version_short.split(".")[1] -for i in range(2, int(version_short.split(".")[1]) + correction): - versions.append((f"2.{i}.0", f"triton-inference-server-2{i}0")) -# Triton 1 releases -for i in range(0, 15): - versions.append((f"1.{i}.0", f"tensorrt_inference_server_1{i}0")) - -# Triton Beta Releases -for i in range(1, 11): - versions.append((f"0.{i}.0_beta", f"inference_server_0{i}0_beta")) +# Obtain Triton Server Release Tags. +tags = subprocess.run(["git", "tag", "--list", "v*"], capture_output=True, text=True) +tags_list = sorted(tags.stdout.strip().splitlines(), key=Version, reverse=True) +logger.info(f"Found source tags: {tags_list}") + +for v in tags_list: + versions.append( + ( + v.replace("v", ""), + f"triton-inference-server-{v.replace('v', '').replace('.', '')}", + ) + ) -# Patch releases -# Add here. +logger.info(f"Defined dictionary of versions: {versions}") -versions = sorted(versions, key=lambda v: Version(v[0]), reverse=True) # Build switcher data json_data = [] @@ -263,6 +314,7 @@ def ultimateReplace(app, docname, source): "url": f"https://docs.nvidia.com/deeplearning/triton-inference-server/archives/{v[1]}/user-guide/docs", } ) + if "dev" in version_long: json_data.insert( 0, @@ -284,6 +336,7 @@ def ultimateReplace(app, docname, source): # Trim to last N releases. json_data = json_data[0:12] +logger.info(f"Trimmed to last 12 release...") json_data.append( { @@ -293,19 +346,22 @@ def ultimateReplace(app, docname, source): } ) -# validate the links for i, d in enumerate(json_data): + logger.info(f"Validating link: {d['url']}") h = httplib2.Http() resp = h.request(d["url"], "HEAD") if int(resp[0]["status"]) >= 400: print(d["url"], "NOK", resp[0]["status"]) - exit(1) + # exit(1) -# Write switcher data to file +logger.info(f"Writing switcher data to file: {switcher_path}") with open(switcher_path, "w") as f: json.dump(json_data, f, ensure_ascii=False, indent=4) +logger.info("Configuration completed...") + + def setup(app): app.add_config_value("ultimate_replacements", {}, True) app.connect("source-read", ultimateReplace) diff --git a/docs/generate_docs.py b/docs/generate_docs.py index b7abe51ed0..a38b06166b 100755 --- a/docs/generate_docs.py +++ b/docs/generate_docs.py @@ -31,8 +31,8 @@ import os import re import subprocess -from collections import defaultdict from functools import partial +from logging.handlers import RotatingFileHandler # Global constants server_abspath = os.environ.get("SERVER_ABSPATH", os.getcwd()) @@ -59,208 +59,200 @@ triton_github_url_reg = re.compile( rf"{triton_repo_patn}/([^/#]+)(?:{tag_patn})?/*([^#]*)\s*(?=#|$)" ) -# relpath_patn = r"]\s*\(\s*([^)]+)\)" # Hyperlink in a .md file, excluding embedded images. hyperlink_reg = re.compile(r"((?". + Replace Triton Inference Server GitHub URLs with relative paths for: + 1. URL is a doc file (e.g., ".md" file). + 2. URL is a directory with README.md and ends with "#
". Examples: https://github.com/triton-inference-server/server/blob/main/docs/protocol#restricted-protocols https://github.com/triton-inference-server/server/blob/main/docs/protocol/extension_shared_memory.md https://github.com/triton-inference-server/server/blob/main/docs/user_guide/model_configuration.md#dynamic-batcher - - Keep URL in the following cases: - https://github.com/triton-inference-server/server/tree/r24.02 - https://github.com/triton-inference-server/server/blob/main/build.py - https://github.com/triton-inference-server/server/blob/main/qa - https://github.com/triton-inference-server/server/blob/main/CONTRIBUTING.md """ m = triton_github_url_reg.match(url) - # Do not replace URL if it is not a Triton GitHub file. if not m: return url target_repo_name = m.group(1) + logger.info(f"Found target repository: {target_repo_name}") target_relpath_from_target_repo = os.path.normpath(m.groups("")[1]) + logger.info( + f"Found target relative path from target repository: {target_relpath_from_target_repo}" + ) section = url[len(m.group(0)) :] + logger.info(f"Found section: {section}") valid_hashtag = section not in ["", "#"] and section.startswith("#") - if target_repo_name == "server": - target_path = os.path.join(server_abspath, target_relpath_from_target_repo) - else: - target_path = os.path.join( + target_path = ( + os.path.join(server_abspath, target_relpath_from_target_repo) + if target_repo_name == "server" + else os.path.join( server_docs_abspath, target_repo_name, target_relpath_from_target_repo ) - - # Return URL if it points to a path outside server/docs. + ) + logger.info(f"Found target path: {target_path}") + # Return URL if it points to a path outside server/docs if os.path.commonpath([server_docs_abspath, target_path]) != server_docs_abspath: return url - - if ( + logger.info( + f"Target path is under server/docs directory: {os.path.commonpath([server_docs_abspath, target_path]) == server_docs_abspath}" + ) + # Check if target is valid for conversion + is_md_file = ( os.path.isfile(target_path) and os.path.splitext(target_path)[1] == ".md" and not is_excluded(target_path) - ): - pass - elif ( + ) + logger.info(f"Target path is a valid .md file: {is_md_file}") + is_dir_with_readme = ( os.path.isdir(target_path) and os.path.isfile(os.path.join(target_path, "README.md")) and valid_hashtag and not is_excluded(os.path.join(target_path, "README.md")) - ): + ) + logger.info(f"Target path is a directory with README.md: {is_dir_with_readme}") + if is_md_file: + pass + elif is_dir_with_readme: target_path = os.path.join(target_path, "README.md") else: return url + logger.info( + f"Target path is a valid .md file or a directory with README.md: {is_md_file or is_dir_with_readme}" + ) - # The "target_path" must be a file at this line. relpath = os.path.relpath(target_path, start=os.path.dirname(src_doc_path)) + logger.info(f"Found relative path: {relpath}") return re.sub(triton_github_url_reg, relpath, url, 1) @@ -274,32 +266,26 @@ def replace_relpath_with_url(relpath, src_doc_path): Examples: ../examples/model_repository ../examples/model_repository/inception_graphdef/config.pbtxt - - Keep relpath in the following cases: - build.md - build.md#building-with-docker - #building-with-docker - ../getting_started/quickstart.md - ../protocol#restricted-protocols """ - target_path = relpath.rsplit("#")[0] + target_path = relpath.rsplit("#", 1)[0] section = relpath[len(target_path) :] valid_hashtag = section not in ["", "#"] + if relpath.startswith("#"): target_path = os.path.basename(src_doc_path) - target_path = os.path.join(os.path.dirname(src_doc_path), target_path) - target_path = os.path.normpath(target_path) + + target_path = os.path.normpath( + os.path.join(os.path.dirname(src_doc_path), target_path) + ) src_git_repo_name = get_git_repo_name(src_doc_path) - url = f"https://github.com/triton-inference-server/{src_git_repo_name}/blob/main/" - if src_git_repo_name == "server": - src_repo_abspath = server_abspath - # TODO: Assert the relative path not pointing to cloned repo, e.g. client. - # This requires more information which may be stored in a global variable. - else: - src_repo_abspath = os.path.join(server_docs_abspath, src_git_repo_name) + src_repo_abspath = ( + server_abspath + if src_git_repo_name == "server" + else os.path.join(server_docs_abspath, src_git_repo_name) + ) - # Assert target path is under the current repo directory. + # Assert target path is under the current repo directory assert os.path.commonpath([src_repo_abspath, target_path]) == src_repo_abspath target_path_from_src_repo = os.path.relpath(target_path, start=src_repo_abspath) @@ -310,9 +296,10 @@ def replace_relpath_with_url(relpath, src_doc_path): and valid_hashtag and os.path.isfile(os.path.join(target_path, "README.md")) ): - relpath = os.path.join(relpath.rsplit("#")[0], "README.md") + section + relpath = os.path.join(relpath.rsplit("#", 1)[0], "README.md") + section target_path = os.path.join(target_path, "README.md") + # Keep relpath if it's a valid .md file in docs if ( os.path.isfile(target_path) and os.path.splitext(target_path)[1] == ".md" @@ -321,53 +308,37 @@ def replace_relpath_with_url(relpath, src_doc_path): and not is_excluded(target_path) ): return relpath - else: - return url + target_path_from_src_repo + section + + return f"https://github.com/triton-inference-server/{src_git_repo_name}/blob/main/{target_path_from_src_repo}{section}" def replace_hyperlink(m, src_doc_path): """ - TODO: Support of HTML tags for future docs. - Markdown allows , e.g. ]+>. Whether we want to - find and replace the link depends on if they link to internal .md files - or allows relative paths. I haven't seen one such case in our doc so - should be safe for now. + Replace hyperlinks in markdown files. + TODO: Support HTML tags for future docs (e.g., ). """ - hyperlink_str = m.group(2) - match = http_reg.match(hyperlink_str) - - if match: - # Hyperlink is a URL. - res = replace_url_with_relpath(hyperlink_str, src_doc_path) - else: - # Hyperlink is a relative path. - res = replace_relpath_with_url(hyperlink_str, src_doc_path) - + res = ( + replace_url_with_relpath(hyperlink_str, src_doc_path) + if http_reg.match(hyperlink_str) + else replace_relpath_with_url(hyperlink_str, src_doc_path) + ) return m.group(1) + res + m.group(3) -def preprocess_docs(exclude_paths=[]): - # Find all ".md" files inside the current repo. - if exclude_paths: - cmd = ( - ["find", server_docs_abspath, "-type", "d", "\\("] - + " -o ".join([f"-path './{dir}'" for dir in exclude_paths]).split(" ") - + ["\\)", "-prune", "-o", "-type", "f", "-name", "'*.md'", "-print"] - ) - else: - cmd = ["find", server_docs_abspath, "-name", "'*.md'"] - cmd = " ".join(cmd) +def preprocess_docs(exclude_paths=None): + """Find all .md files and preprocess their hyperlinks.""" + # Find all ".md" files + cmd = f"find {server_docs_abspath} -name '*.md'" result = subprocess.run(cmd, check=True, capture_output=True, text=True, shell=True) - docs_list = list(filter(None, result.stdout.split("\n"))) + docs_list = [path for path in result.stdout.split("\n") if path] - # Read, preprocess and write back to each document file. + # Read, preprocess and write back to each document file for doc_abspath in docs_list: if is_excluded(doc_abspath): continue - content = None - with open(doc_abspath, "r") as f: + with open(doc_abspath) as f: content = f.read() content = hyperlink_reg.sub( @@ -380,35 +351,21 @@ def preprocess_docs(exclude_paths=[]): def main(): - args = parser.parse_args() - repo_tag = args.repo_tag - repository_filename = args.repo_file - github_org = args.github_organization - - # Change working directory to server/docs. + """Main function to clone repositories, preprocess docs, and build HTML.""" + logger.info("Starting setup Triton Server documentation for Sphinx build...") + logger.info(f"Collecting repositories from {args.repo_file}...") os.chdir(server_docs_abspath) - run_command("make clean") - repositories = "" - with open(repository_filename, "r") as f: - repositories = f.read() - f.close() + with open(args.repo_file) as f: + repository_list = f.read().strip().split("\n") - repository_list = repositories.strip().split("\n") + # Clone repositories for repository in repository_list: run_command(f"rm -rf {repository}") - clone_from_github(repository, repo_tag, github_org) + clone_from_github(repository, args.repo_tag, args.github_organization) - # Preprocess documents in server_docs_abspath after all repos are cloned. + # Preprocess documents after all repos are cloned preprocess_docs() - run_command("make html") - - # Clean up working directory. - for repository in repository_list: - run_command(f"rm -rf {repository}") - - # Return to previous working directory server/. - os.chdir(server_abspath) if __name__ == "__main__":