From a70558611dd6f6e1089410f8adfd378cbaaed6de Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 22 Jan 2026 15:09:45 -0600 Subject: [PATCH 1/6] Add DocFX updater script --- scripts/BUILD.bazel | 8 +++ scripts/update_docfx.py | 141 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 scripts/update_docfx.py diff --git a/scripts/BUILD.bazel b/scripts/BUILD.bazel index bdce92f78c3b0..2af97e0d6730c 100644 --- a/scripts/BUILD.bazel +++ b/scripts/BUILD.bazel @@ -38,6 +38,14 @@ py_binary( srcs = ["update_multitool_binaries.py"], ) +py_binary( + name = "update_docfx", + srcs = ["update_docfx.py"], + deps = [ + requirement("packaging"), + ], +) + java_binary( name = "google-java-format", jvm_flags = [ diff --git a/scripts/update_docfx.py b/scripts/update_docfx.py new file mode 100644 index 0000000000000..5b9dc743fa40f --- /dev/null +++ b/scripts/update_docfx.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +"""Update docfx_repo.bzl to the latest DocFX package on NuGet. + +This script fetches the latest stable DocFX version (or a user-specified one), +computes the nupkg sha256, and rewrites dotnet/private/docfx_repo.bzl. +""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import urllib.request + +from packaging.version import InvalidVersion, Version + +NUGET_INDEX_URL = "https://api.nuget.org/v3-flatcontainer/docfx/index.json" +NUGET_NUPKG_URL = "https://api.nuget.org/v3-flatcontainer/docfx/{version}/docfx.{version}.nupkg" + + +def fetch_json(url): + with urllib.request.urlopen(url) as response: + return json.loads(response.read()) + + +def choose_version(versions, allow_prerelease, explicit_version=None): + if explicit_version: + return explicit_version + + parsed = [] + for v in versions: + try: + pv = Version(v) + except InvalidVersion: + continue + if not allow_prerelease and pv.is_prerelease: + continue + parsed.append((pv, v)) + + if not parsed: + # Fall back to any parseable version. + for v in versions: + try: + parsed.append((Version(v), v)) + except InvalidVersion: + continue + + if not parsed: + raise ValueError("No parseable DocFX versions found in NuGet index") + + return max(parsed, key=lambda item: item[0])[1] + + +def sha256_of_url(url): + digest = hashlib.sha256() + with urllib.request.urlopen(url) as response: + while True: + chunk = response.read(1024 * 1024) + if not chunk: + break + digest.update(chunk) + return digest.hexdigest() + + +def render_docfx_repo(version, sha256): + return f'''\ +"""Repository rule to download the docfx NuGet package.""" + +_BUILD = """ +package(default_visibility = ["//visibility:public"]) +exports_files(glob(["**/*"])) +filegroup(name = "docfx_dll", srcs = ["tools/net8.0/any/docfx.dll"]) +""" + +def _docfx_repo_impl(ctx): + ctx.download_and_extract( + url = "https://api.nuget.org/v3-flatcontainer/docfx/{{0}}/docfx.{{0}}.nupkg".format(ctx.attr.version), + sha256 = ctx.attr.sha256, + type = "zip", + ) + ctx.file("BUILD.bazel", _BUILD) + +docfx_repo = repository_rule( + implementation = _docfx_repo_impl, + attrs = {{ + "version": attr.string(mandatory = True), + "sha256": attr.string(mandatory = True), + }}, +) + +def _docfx_extension_impl(module_ctx): + docfx_repo( + name = "docfx", + version = "{version}", + sha256 = "{sha256}", + ) + return module_ctx.extension_metadata(reproducible = True) + +docfx_extension = module_extension(implementation = _docfx_extension_impl) +''' + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--version", + help="Use this DocFX version instead of the latest stable.", + ) + parser.add_argument( + "--allow-prerelease", + action="store_true", + help="Allow prerelease versions when selecting latest.", + ) + parser.add_argument( + "--output", + default="dotnet/private/docfx_repo.bzl", + help="Output file path (default: dotnet/private/docfx_repo.bzl)", + ) + args = parser.parse_args() + + index = fetch_json(NUGET_INDEX_URL) + versions = index.get("versions", []) + if not versions: + raise ValueError("NuGet index returned no versions for DocFX") + + version = choose_version(versions, args.allow_prerelease, args.version) + nupkg_url = NUGET_NUPKG_URL.format(version=version) + sha256 = sha256_of_url(nupkg_url) + + output_path = Path(args.output) + if not output_path.is_absolute(): + workspace_dir = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + if workspace_dir: + output_path = Path(workspace_dir) / output_path + output_path.write_text(render_docfx_repo(version, sha256)) + + print(f"Updated {output_path} to DocFX {version}") + + +if __name__ == "__main__": + main() From 9616d25002ca231e61a6aa67b89925697f7072f3 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 22 Jan 2026 15:12:48 -0600 Subject: [PATCH 2/6] Bump DocFX artifact pin --- dotnet/private/docfx_repo.bzl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/private/docfx_repo.bzl b/dotnet/private/docfx_repo.bzl index 0e4e5e8e8362e..48db4742478a7 100644 --- a/dotnet/private/docfx_repo.bzl +++ b/dotnet/private/docfx_repo.bzl @@ -25,8 +25,8 @@ docfx_repo = repository_rule( def _docfx_extension_impl(module_ctx): docfx_repo( name = "docfx", - version = "2.78.2", - sha256 = "68b70b5e3e3f0df0dd858b228131fec40ca45493bb5b93f77b9ab3a38b21f7fb", + version = "2.78.4", + sha256 = "56faca4233a743b446a7584ff8195d8bb09a33aaa97e9e089c1e6b112212a848", ) return module_ctx.extension_metadata(reproducible = True) From 8fe05988014c4f435484b9a2e70d0fa1764ef099 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 22 Jan 2026 15:13:11 -0600 Subject: [PATCH 3/6] Run DocFX updater in dotnet/update-deps.sh --- dotnet/update-deps.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/update-deps.sh b/dotnet/update-deps.sh index b7fa1f7fd74f2..b6e6b4dcf9571 100755 --- a/dotnet/update-deps.sh +++ b/dotnet/update-deps.sh @@ -21,3 +21,5 @@ DOTNET="${DOTNET:-dotnet}" ("$DOTNET" tool restore && "$DOTNET" tool run paket install) bazel run @rules_dotnet//tools/paket2bazel:paket2bazel -- --dependencies-file "$(pwd)/paket.dependencies" --output-folder "$(pwd)" ) + +bazel run //scripts:update_docfx From 894dde4995169c6263b2fbde80fadc6cf058918a Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 22 Jan 2026 15:59:31 -0600 Subject: [PATCH 4/6] Fix import order in update_docfx.py Co-Authored-By: Claude Opus 4.5 --- scripts/update_docfx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update_docfx.py b/scripts/update_docfx.py index 5b9dc743fa40f..95757afc95582 100644 --- a/scripts/update_docfx.py +++ b/scripts/update_docfx.py @@ -9,8 +9,8 @@ import hashlib import json import os -from pathlib import Path import urllib.request +from pathlib import Path from packaging.version import InvalidVersion, Version From 3ce148d9e6bcb59551b86d0896351d62bd817beb Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 22 Jan 2026 18:20:14 -0600 Subject: [PATCH 5/6] Fix version selection in update_docfx.py - Validate explicit version exists in NuGet index before use - Remove fallback that ignored --allow-prerelease flag Co-Authored-By: Claude Opus 4.5 --- scripts/update_docfx.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/update_docfx.py b/scripts/update_docfx.py index 95757afc95582..b75dee368842c 100644 --- a/scripts/update_docfx.py +++ b/scripts/update_docfx.py @@ -25,6 +25,10 @@ def fetch_json(url): def choose_version(versions, allow_prerelease, explicit_version=None): if explicit_version: + if explicit_version not in versions: + raise ValueError( + f"Requested DocFX version {explicit_version!r} not found in NuGet index" + ) return explicit_version parsed = [] @@ -38,15 +42,12 @@ def choose_version(versions, allow_prerelease, explicit_version=None): parsed.append((pv, v)) if not parsed: - # Fall back to any parseable version. - for v in versions: - try: - parsed.append((Version(v), v)) - except InvalidVersion: - continue - - if not parsed: - raise ValueError("No parseable DocFX versions found in NuGet index") + if allow_prerelease: + raise ValueError("No parseable DocFX versions found in NuGet index") + else: + raise ValueError( + "No stable DocFX versions found. Use --allow-prerelease to include prereleases." + ) return max(parsed, key=lambda item: item[0])[1] From 406d8a834ee8bf9d77e78f5144ddbcf14fad0de7 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 22 Jan 2026 18:30:32 -0600 Subject: [PATCH 6/6] Fix version selection and use urllib3 in update_docfx.py - Validate explicit version exists in NuGet index before use - Remove fallback that ignored --allow-prerelease flag - Switch from urllib.request to urllib3 for consistency with other scripts Co-Authored-By: Claude Opus 4.5 --- scripts/BUILD.bazel | 1 + scripts/update_docfx.py | 26 +++++++++++--------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/scripts/BUILD.bazel b/scripts/BUILD.bazel index 2af97e0d6730c..4da240564d112 100644 --- a/scripts/BUILD.bazel +++ b/scripts/BUILD.bazel @@ -43,6 +43,7 @@ py_binary( srcs = ["update_docfx.py"], deps = [ requirement("packaging"), + requirement("urllib3"), ], ) diff --git a/scripts/update_docfx.py b/scripts/update_docfx.py index b75dee368842c..16dfc21b966cc 100644 --- a/scripts/update_docfx.py +++ b/scripts/update_docfx.py @@ -9,26 +9,26 @@ import hashlib import json import os -import urllib.request from pathlib import Path +import urllib3 from packaging.version import InvalidVersion, Version NUGET_INDEX_URL = "https://api.nuget.org/v3-flatcontainer/docfx/index.json" NUGET_NUPKG_URL = "https://api.nuget.org/v3-flatcontainer/docfx/{version}/docfx.{version}.nupkg" +http = urllib3.PoolManager() + def fetch_json(url): - with urllib.request.urlopen(url) as response: - return json.loads(response.read()) + r = http.request("GET", url) + return json.loads(r.data) def choose_version(versions, allow_prerelease, explicit_version=None): if explicit_version: if explicit_version not in versions: - raise ValueError( - f"Requested DocFX version {explicit_version!r} not found in NuGet index" - ) + raise ValueError(f"Requested DocFX version {explicit_version!r} not found in NuGet index") return explicit_version parsed = [] @@ -45,21 +45,17 @@ def choose_version(versions, allow_prerelease, explicit_version=None): if allow_prerelease: raise ValueError("No parseable DocFX versions found in NuGet index") else: - raise ValueError( - "No stable DocFX versions found. Use --allow-prerelease to include prereleases." - ) + raise ValueError("No stable DocFX versions found. Use --allow-prerelease to include prereleases.") return max(parsed, key=lambda item: item[0])[1] def sha256_of_url(url): digest = hashlib.sha256() - with urllib.request.urlopen(url) as response: - while True: - chunk = response.read(1024 * 1024) - if not chunk: - break - digest.update(chunk) + r = http.request("GET", url, preload_content=False) + for chunk in r.stream(1024 * 1024): + digest.update(chunk) + r.release_conn() return digest.hexdigest()