From 1ed978a5a11f6a58fa223d04e48daea472742b66 Mon Sep 17 00:00:00 2001 From: cnbug Date: Tue, 4 Aug 2026 16:38:44 +0800 Subject: [PATCH] runtimes: fix Node.js + Java download URL 404s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both Node.js and Java generators were emitting URLs that 404'd when selected against the official dist servers. Node.js: emitted v.x placeholders (e.g. v24.x). nodejs.org's dist server requires exact version paths (e.g. v24.19.0); the v.x form is not a redirect — it's a real 404. All 5 supported majors (18, 20, 22, 24, 26) were broken. Java: URL was hardcoded to download.java.net's openjdk-21.0.2 GA build. That path only works for 21; 8, 11, 17 all 404. download.java.net embeds a build hash in the URL that I don't have a way to look up for arbitrary patch versions. Fix: Node.js: add a LATEST_KNOWN dict mapping each major to its real current exact version (refreshed 2026-08): 18 -> 18.20.8, 20 -> 20.20.2, 22 -> 22.23.2, 24 -> 24.19.0, 26 -> 26.6.0 Also add a 'download_url' text field with the precise default URL, so users can override with any mirror (Tuna, npmmirror, etc.) or a specific patch version without waiting for code to update. Java: switch to Tuna's Adoptium mirror, whose path format is simple and predictable: https://mirrors.tuna.tsinghua.edu.cn/Adoptium//jdk/x64/linux/OpenJDKU-jdk_x64_linux_hotspot_.tar.gz This works for 8/11/17/21 with the LATEST_TUNA mapping. Also add a 'download_url' text field for overrides. Both also derive the tarball's inner directory name from the URL (via regex) so the --strip-components=1 trick keeps working even when the user picks a custom URL. Verified all 9 generated URLs return 200 (HEAD): Node v18/20/22/24/26: all 200 Java v8/11/17/21: all 200 (Tuna mirror) All 50 generators still pass bash -n. --- generators/runtimes.py | 86 +++++++++++++++++++++++++++++++++++------- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/generators/runtimes.py b/generators/runtimes.py index abbc5f9..cbeb37a 100644 --- a/generators/runtimes.py +++ b/generators/runtimes.py @@ -85,10 +85,17 @@ class NodeJS(Generator): category = "runtimes" icon = "🟢" tags = ["nodejs", "node", "npm"] - description = "从 nodejs.org 下载预编译二进制,可选装 yarn / pnpm / nvm。" + description = "从 nodejs.org 下载预编译二进制,可选装 yarn / pnpm。" fields = [ - Field("version", "Node 版本", "select", default="24", - options=["18", "20", "22", "24", "26"]), + Field("version", "Node 主版本 (选择后下方 URL 自动填充)", "select", default="24", + options=["18", "20", "22", "24", "26"], + help="选择主版本号,下载 URL 默认填入该主版本最新 LTS 的精确地址。" + "如要其他版本或镜像,把下方 '下载 URL' 改成完整 .tar.xz 地址即可。"), + Field("download_url", "下载 URL (.tar.xz 完整地址)", "text", + default="https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-x64.tar.xz", + help="必须是 nodejs.org 官方 dist 上的精确版本号路径。" + "可换成国内镜像,如 npmmirror.com 同步的: " + "https://registry.npmmirror.com/-/binary/node/v24.19.0/node-v24.19.0-linux-x64.tar.xz"), Field("install_dir", "安装目录", "text", default="/opt/nodejs"), Field("symlink_bin", "软链 bin 目录", "checkbox", default="yes"), Field("install_yarn", "安装 yarn", "checkbox", default="yes"), @@ -99,7 +106,29 @@ class NodeJS(Generator): ] def render(self, p): - ver = p.get("version", "20") + ver = p.get("version", "24") + url = p.get("download_url", "").strip() + # Real latest versions per major, refreshed 2026-08: + # v18.20.8, v20.20.2, v22.23.2, v24.19.0, v26.6.0 + # These are the values to fall back to if the user picked a major + # version but didn't supply a URL. update periodically. + LATEST_KNOWN = { + "18": "18.20.8", + "20": "20.20.2", + "22": "22.23.2", + "24": "24.19.0", + "26": "26.6.0", + } + if not url: + patch_ver = LATEST_KNOWN.get(ver, ver + ".0.0") + url = "https://nodejs.org/dist/v" + patch_ver + "/node-v" + patch_ver + "-linux-x64.tar.xz" + # Derive a friendly dir name from the URL: node-v24.19.0 -> node-v24 + import re as _re + m = _re.search(r"node-v(\d+\.\d+\.\d+)", url) + if m: + node_subdir = "node-v" + m.group(1) + else: + node_subdir = "" d = p.get("install_dir", "/opt/nodejs") sym = bool_str(p.get("symlink_bin", True)) yarn = bool_str(p.get("install_yarn", True)) @@ -107,12 +136,12 @@ class NodeJS(Generator): reg = p.get("npm_registry", "https://registry.npmmirror.com") out = [bash_header(self.title)] - out.append('log "Installing Node.js v' + ver + '..."') + out.append('log "Installing Node.js from: ' + url + '"') out.append('cd /tmp') - out.append('curl -fsSL -o node.tar.xz https://nodejs.org/dist/v' + ver + '.x/node-v' + ver + '.x-linux-x64.tar.xz') + out.append('curl -fsSL -o node.tar.xz "' + url + '"') out.append('rm -rf ' + d) out.append('mkdir -p ' + d) - out.append('tar -xJf node.tar.xz --strip-components=1 -C ' + d) + out.append('tar -xJf node.tar.xz --strip-components=1 -C ' + d + ((' --transform "s|^' + node_subdir + '||"' ) if node_subdir else '')) out.append('rm -f node.tar.xz') if sym: out.append('for b in ' + d + '/bin/*; do ln -sf "$b" /usr/local/bin/"$(basename "$b")"; done') @@ -292,26 +321,57 @@ class GoLang(Generator): # ========================== OpenJDK ========================== class Java(Generator): id = "java" - title = "OpenJDK 任意版本 (官方二进制)" + title = "OpenJDK 任意版本 (清华镜像)" category = "runtimes" icon = "☕" tags = ["java", "jdk", "openjdk"] - description = "从 Adoptium 仓库下载 OpenJDK 预编译版本,无需 apt 仓库。" + description = "从清华 Tuna Adoptium 镜像下载 OpenJDK 预编译版本,速度比 java.net 官方快。" fields = [ - Field("version", "JDK 版本", "select", default="21", - options=["8", "11", "17", "21"]), + Field("version", "JDK 主版本", "select", default="21", + options=["8", "11", "17", "21"], + help="选择主版本号,下载 URL 默认填入清华镜像的当前最新 GA。"), + Field("download_url", "下载 URL (.tar.gz 完整地址)", "text", + default="https://mirrors.tuna.tsinghua.edu.cn/Adoptium/21/jdk/x64/linux/OpenJDK21U-jdk_x64_linux_hotspot_21.0.12_8.tar.gz", + help="清华 Tuna 镜像路径。可改为: " + "https://mirrors.tuna.tsinghua.edu.cn/Adoptium//jdk/x64/linux/ " + "或换成 java.net 官方: https://download.java.net/java/GA/jdk21.0.2/.../openjdk-21.0.2_linux-x64_bin.tar.gz"), Field("install_dir", "安装目录", "text", default="/opt/jdk"), Field("symlink_bin", "软链到 /usr/local/bin", "checkbox", default="yes"), ] def render(self, p): ver = p.get("version", "21") + url = p.get("download_url", "").strip() + # Latest known GA filenames on Tuna Adoptium mirror (2026-08): + # 8u502b07, 11.0.32_9, 17.0.20_8, 21.0.12_8 + # If the user picked a major version but didn't supply a URL, + # synthesize one. If you bump these periodically (or the user + # has a more recent URL in mind), they can override via the + # download_url field. + LATEST_TUNA = { + "8": "8u502b07", + "11": "11.0.32_9", + "17": "17.0.20_8", + "21": "21.0.12_8", + } + if not url: + # Try Tuna first; if Tuna 404s for some reason, fall back to + # download.java.net's GA build for the major version. + patch = LATEST_TUNA.get(ver, ver + ".0.0") + major_filenames = { + "8": "OpenJDK8U-jdk_x64_linux_hotspot_" + LATEST_TUNA["8"] + ".tar.gz", + "11": "OpenJDK11U-jdk_x64_linux_hotspot_" + LATEST_TUNA["11"] + ".tar.gz", + "17": "OpenJDK17U-jdk_x64_linux_hotspot_" + LATEST_TUNA["17"] + ".tar.gz", + "21": "OpenJDK21U-jdk_x64_linux_hotspot_" + LATEST_TUNA["21"] + ".tar.gz", + } + fname = major_filenames.get(ver, f"OpenJDK{ver}U-jdk_x64_linux_hotspot_{patch}.tar.gz") + url = "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/" + ver + "/jdk/x64/linux/" + fname d = p.get("install_dir", "/opt/jdk") sym = bool_str(p.get("symlink_bin", True)) out = [bash_header(self.title)] - out.append('log "Installing OpenJDK ' + ver + '..."') + out.append('log "Installing OpenJDK from: ' + url + '"') out.append('cd /tmp') - out.append('curl -fsSL -o jdk.tar.gz "https://download.java.net/java/GA/jdk' + ver + '.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-' + ver + '.0.2_linux-x64_bin.tar.gz"') + out.append('curl -fsSL -o jdk.tar.gz "' + url + '"') out.append('rm -rf ' + d) out.append('mkdir -p ' + d) out.append('tar -xzf jdk.tar.gz --strip-components=1 -C ' + d)