Files
shell-gen/generators/runtimes.py
T
cnbug 1ed978a5a1 runtimes: fix Node.js + Java download URL 404s
Both Node.js and Java generators were emitting URLs that 404'd when
selected against the official dist servers.

  Node.js: emitted v<major>.x placeholders (e.g. v24.x). nodejs.org's
    dist server requires exact version paths (e.g. v24.19.0); the v<x>.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/<major>/jdk/x64/linux/OpenJDK<major>U-jdk_x64_linux_hotspot_<exact>.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.
2026-08-04 16:38:44 +08:00

712 lines
36 KiB
Python

"""
Runtime / language / compiler version installers.
Each generator supports multiple versions and uses source build for exotic versions
when no distro package is available.
"""
from . import register, Generator, Field
from . import bash_header, quote, bool_str, yes
# ========================== Python (pyenv-style from source) ==========================
class Python(Generator):
id = "python"
title = "Python 任意版本 (源码编译)"
category = "runtimes"
icon = "🐍"
tags = ["python", "pyenv", "source"]
description = "从 python.org 下载源码编译,支持 2.7 / 3.6 - 3.13 任意版本,启用 SSL/zlib/sqlite3。"
warnings = [
"源码编译会下载 ~25MB,编译 5-15 分钟,需要 build-essential。",
"建议先运行 gcc 编译器安装脚本(同分类)以保证编译依赖。",
]
fields = [
Field("version", "Python 版本", "select", default="3.12.6",
options=["2.7.18", "3.6.15", "3.7.17", "3.8.20", "3.9.20",
"3.10.14", "3.11.11", "3.12.6", "3.13.0", "3.13.1"]),
Field("install_dir", "安装目录", "text", default="/opt/python",
help="最终路径 = $install_dir/$version (如 /opt/python/3.12.6)"),
Field("enable_optimizations", "启用 PGO/LTO (慢但快 ~10%)", "checkbox", default="yes"),
Field("shared", "编译为 .so 共享库", "checkbox", default="no",
help="如果其他工具(如 mod_wsgi)需要动态链接则开启。"),
Field("install_pip", "安装 pip", "checkbox", default="yes"),
Field("symlink_bin", "软链 bin 目录到 PATH", "checkbox", default="no",
help="yes = 软链 /opt/python/$version/bin/* 到 /usr/local/bin/"),
Field("ssl_backend", "SSL 后端", "select", default="openssl",
options=["openssl", "libressl"]),
]
def render(self, p):
ver = p.get("version", "3.12.6")
d = p.get("install_dir", "/opt/python")
opt = " --enable-optimizations" if bool_str(p.get("enable_optimizations", True)) else ""
shared = " --enable-shared" if bool_str(p.get("shared")) else ""
ssl = p.get("ssl_backend", "openssl")
sym = bool_str(p.get("symlink_bin"))
pip = bool_str(p.get("install_pip", True))
out = [bash_header(self.title)]
out.append('log "Installing Python ' + ver + ' from source..."')
out.append('command -v gcc >/dev/null || $PKG_INSTALL gcc make build-essential \\\n'
' libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \\\n'
' libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev \\\n'
' || $PKG_INSTALL gcc make openssl-devel bzip2-devel libffi-devel zlib-devel \\\n'
' readline-devel sqlite-devel ncurses-devel xz-devel')
out.append('cd /tmp')
out.append('curl -fsSL -o Python.tgz https://www.python.org/ftp/python/' + ver + '/Python-' + ver + '.tgz')
out.append('tar -xzf Python.tgz')
out.append('cd Python-' + ver)
cfg = ('./configure --prefix=' + d + '/' + ver +
' --with-ensurepip=install' if pip else './configure --prefix=' + d + '/' + ver + ' --without-ensurepip')
cfg += opt + shared
if ssl == "libressl":
cfg += ' --with-openssl=/usr/local'
out.append(cfg)
out.append('make -j"$(nproc)"')
out.append('make altinstall')
out.append('cd /tmp && rm -rf Python-' + ver + ' Python.tgz')
out.append('')
out.append('PYBIN=' + d + '/' + ver + '/bin/python' + '.'.join(ver.split('.')[:2]))
if sym:
out.append('log "Symlinking binaries to /usr/local/bin/..."')
out.append('for b in ' + d + '/' + ver + '/bin/*; do\n'
' ln -sf "$b" /usr/local/bin/"$(basename "$b")" || true\n'
'done')
out.append('log "Verifying..."')
out.append('$PYBIN --version')
if pip:
out.append('$PYBIN -m pip --version || $PYBIN -m ensurepip --upgrade')
return "\n".join(out) + "\n"
# ========================== Node.js (nvm-style from binary) ==========================
class NodeJS(Generator):
id = "nodejs"
title = "Node.js 任意版本 (官方二进制)"
category = "runtimes"
icon = "🟢"
tags = ["nodejs", "node", "npm"]
description = "从 nodejs.org 下载预编译二进制,可选装 yarn / pnpm。"
fields = [
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"),
Field("install_pnpm", "安装 pnpm", "checkbox", default="yes"),
Field("npm_registry", "NPM Registry", "text",
default="https://registry.npmmirror.com",
help="默认走国内镜像,留空 = 官方。"),
]
def render(self, p):
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))
pnpm = bool_str(p.get("install_pnpm", True))
reg = p.get("npm_registry", "https://registry.npmmirror.com")
out = [bash_header(self.title)]
out.append('log "Installing Node.js from: ' + url + '"')
out.append('cd /tmp')
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 + ((' --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')
if reg:
out.append('npm config set registry ' + quote(reg))
if yarn:
out.append('npm install -g yarn')
if pnpm:
out.append('npm install -g pnpm')
out.append('node --version')
out.append('npm --version')
return "\n".join(out) + "\n"
# ========================== GCC (from source) ==========================
class GCC(Generator):
id = "gcc"
title = "GCC 任意版本 (源码编译)"
category = "runtimes"
icon = "🛠️"
tags = ["gcc", "compiler", "source"]
description = "从 gcc.gnu.org 下载源码,编译并安装到 /opt/gcc-<ver>。"
warnings = [
"GCC 12+ 编译需 30-60 分钟,需要约 5GB 磁盘。",
"建议另装 'system gcc' (apt/yum) 留作系统默认,本工具不替换系统 gcc。",
]
fields = [
Field("version", "GCC 版本", "select", default="13.2.0",
options=["8.5.0", "9.5.0", "10.5.0", "11.4.0", "12.3.0",
"13.2.0", "14.1.0"]),
Field("languages", "支持语言", "text", default="c,c++,fortran",
help="逗号分隔,如 c,c++,objc,fortran,go"),
Field("install_dir", "安装目录", "text", default="/opt/gcc",
help="最终 = $install_dir/<ver>"),
Field("enable_lto", "启用 LTO (慢但快 ~10%)", "checkbox", default="no"),
Field("enable_libsan", "启用 Sanitizers (ASan/TSan)", "checkbox", default="no"),
Field("make_jobs", "make -j 任务数", "text", default="$(nproc)",
help="默认 = CPU 核数,改小可降低内存峰值。"),
]
def render(self, p):
ver = p.get("version", "13.2.0")
langs = p.get("languages", "c,c++,fortran")
d = p.get("install_dir", "/opt/gcc")
lto = " --enable-lto" if bool_str(p.get("enable_lto")) else ""
libsan = " --enable-libsanitizer" if bool_str(p.get("enable_libsan")) else ""
jobs = p.get("make_jobs", "$(nproc)")
out = [bash_header(self.title)]
out.append('log "Installing GCC ' + ver + ' from source..."')
out.append('$PKG_INSTALL wget gcc gcc-c++ make texinfo bison flex')
out.append('cd /tmp')
out.append('curl -fsSL -O https://ftp.gnu.org/gnu/gcc/gcc-' + ver + '/gcc-' + ver + '.tar.xz')
out.append('tar -xJf gcc-' + ver + '.tar.xz')
out.append('cd gcc-' + ver)
out.append('./contrib/download_prerequisites')
out.append('mkdir -p build && cd build')
out.append('../configure --prefix=' + d + '/' + ver +
' --enable-languages=' + langs +
' --disable-multilib --with-system-zlib' + lto + libsan)
out.append('make -j' + jobs)
out.append('make install')
out.append('cd /tmp && rm -rf gcc-' + ver + ' gcc-' + ver + '.tar.xz build')
out.append('log "Verifying..."')
out.append(d + '/' + ver + '/bin/gcc --version')
out.append('echo "Add to PATH: export PATH=' + d + '/' + ver + '/bin:$PATH"')
return "\n".join(out) + "\n"
# ========================== Make ==========================
class Make(Generator):
id = "make"
title = "GNU Make 任意版本 (源码编译)"
category = "runtimes"
icon = "🔨"
tags = ["make", "build", "gnu"]
description = "从 gnu.org 下载 make 源码并安装到 /opt。"
fields = [
Field("version", "Make 版本", "select", default="4.4.1",
options=["4.2.1", "4.3", "4.4", "4.4.1"]),
Field("install_dir", "安装目录", "text", default="/opt/make"),
]
def render(self, p):
ver = p.get("version", "4.4.1")
d = p.get("install_dir", "/opt/make")
out = [bash_header(self.title)]
out.append('log "Installing make ' + ver + '..."')
out.append('$PKG_INSTALL gcc make')
out.append('cd /tmp')
out.append('curl -fsSL -O https://ftp.gnu.org/gnu/make/make-' + ver + '.tar.gz')
out.append('tar -xzf make-' + ver + '.tar.gz')
out.append('cd make-' + ver)
out.append('./configure --prefix=' + d + '/' + ver)
out.append('make -j"$(nproc)"')
out.append('make install')
out.append('cd /tmp && rm -rf make-' + ver + ' make-' + ver + '.tar.gz')
out.append(d + '/' + ver + '/bin/make --version')
return "\n".join(out) + "\n"
# ========================== CMake ==========================
class CMake(Generator):
id = "cmake"
title = "CMake 任意版本 (官方脚本安装)"
category = "runtimes"
icon = "🧱"
tags = ["cmake", "build", "makefile"]
description = "从 cmake.org 下载官方二进制,安装到 /opt。"
fields = [
Field("version", "CMake 版本", "select", default="3.30.0",
options=["3.20.0", "3.22.0", "3.24.0", "3.25.0", "3.26.0",
"3.27.0", "3.28.0", "3.29.0", "3.30.0"]),
Field("install_dir", "安装目录", "text", default="/opt/cmake"),
Field("symlink_bin", "软链到 /usr/local/bin", "checkbox", default="yes"),
]
def render(self, p):
ver = p.get("version", "3.30.0")
d = p.get("install_dir", "/opt/cmake")
sym = bool_str(p.get("symlink_bin", True))
out = [bash_header(self.title)]
out.append('log "Installing CMake ' + ver + '..."')
out.append('cd /tmp')
out.append('curl -fsSL -O https://github.com/Kitware/CMake/releases/download/v' + ver + '/cmake-' + ver + '-linux-x86_64.tar.gz')
out.append('rm -rf ' + d)
out.append('mkdir -p ' + d)
out.append('tar -xzf cmake-' + ver + '-linux-x86_64.tar.gz --strip-components=1 -C ' + d)
out.append('rm -f cmake-' + ver + '-linux-x86_64.tar.gz')
if sym:
out.append('for b in ' + d + '/bin/*; do ln -sf "$b" /usr/local/bin/"$(basename "$b")"; done')
out.append('cmake --version')
return "\n".join(out) + "\n"
# ========================== Go ==========================
class GoLang(Generator):
id = "golang"
title = "Go 任意版本 (官方二进制)"
category = "runtimes"
icon = "🐹"
tags = ["go", "golang"]
description = "从 go.dev 下载 Go 官方二进制,设置 GOPROXY 国内镜像。"
fields = [
Field("version", "Go 版本", "select", default="1.22.5",
options=["1.18.10", "1.19.13", "1.20.14", "1.21.12", "1.22.5", "1.23.0"]),
Field("install_dir", "安装目录", "text", default="/opt/go"),
Field("goproxy", "GOPROXY", "text", default="https://goproxy.cn,direct",
help="默认 goproxy.cn,留空 = GOPROXY=off"),
Field("symlink_bin", "软链到 /usr/local/bin", "checkbox", default="yes"),
]
def render(self, p):
ver = p.get("version", "1.22.5")
d = p.get("install_dir", "/opt/go")
proxy = p.get("goproxy", "https://goproxy.cn,direct")
sym = bool_str(p.get("symlink_bin", True))
out = [bash_header(self.title)]
out.append('log "Installing Go ' + ver + '..."')
out.append('cd /tmp')
out.append('curl -fsSL -O https://go.dev/dl/go' + ver + '.linux-amd64.tar.gz')
out.append('rm -rf ' + d)
out.append('tar -C ' + d.rsplit('/', 1)[0] + ' -xzf go' + ver + '.linux-amd64.tar.gz')
out.append('mv ' + d.rsplit('/', 1)[0] + '/go ' + d)
out.append('rm -f go' + ver + '.linux-amd64.tar.gz')
if sym:
out.append('for b in ' + d + '/bin/*; do ln -sf "$b" /usr/local/bin/"$(basename "$b")"; done')
if proxy:
out.append('go env -w GOPROXY=' + quote(proxy))
out.append('go env -w GOSUMDB=sum.golang.google.cn')
out.append('go version')
out.append('echo "Add to PATH: export PATH=' + d + '/bin:$PATH"')
return "\n".join(out) + "\n"
# ========================== OpenJDK ==========================
class Java(Generator):
id = "java"
title = "OpenJDK 任意版本 (清华镜像)"
category = "runtimes"
icon = ""
tags = ["java", "jdk", "openjdk"]
description = "从清华 Tuna Adoptium 镜像下载 OpenJDK 预编译版本,速度比 java.net 官方快。"
fields = [
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/<ver>/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 from: ' + url + '"')
out.append('cd /tmp')
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)
out.append('rm -f jdk.tar.gz')
if sym:
out.append('update-alternatives --install /usr/bin/java java ' + d + '/bin/java 9999 || true')
out.append('update-alternatives --install /usr/bin/javac javac ' + d + '/bin/javac 9999 || true')
out.append('for b in ' + d + '/bin/*; do ln -sf "$b" /usr/local/bin/"$(basename "$b")"; done')
out.append('export JAVA_HOME=' + d)
out.append(d + '/bin/java --version')
out.append('echo "Add to PATH: export PATH=' + d + '/bin:$PATH"')
return "\n".join(out) + "\n"
# ========================== PHP ==========================
class PHP(Generator):
id = "php"
title = "PHP 任意版本 (源码编译)"
category = "runtimes"
icon = "🐘"
tags = ["php", "apache", "nginx", "fpm"]
description = "从 php.net 编译 PHP,启用 fpm、常用扩展。"
fields = [
Field("version", "PHP 版本", "select", default="8.3.10",
options=["7.4.33", "8.0.30", "8.1.29", "8.2.22", "8.3.10"]),
Field("install_dir", "安装目录", "text", default="/opt/php"),
Field("enable_fpm", "启用 PHP-FPM", "checkbox", default="yes"),
Field("extensions", "编译扩展", "text",
default="mysqli,pdo,pdo_mysql,gd,mbstring,curl,xml,zip,intl,opcache,bcmath"),
]
def render(self, p):
ver = p.get("version", "8.3.10")
d = p.get("install_dir", "/opt/php")
fpm = bool_str(p.get("enable_fpm", True))
exts = p.get("extensions", "mysqli,pdo,pdo_mysql,gd,mbstring,curl,xml,zip,intl,opcache,bcmath")
out = [bash_header(self.title)]
out.append('log "Installing PHP ' + ver + ' from source..."')
out.append('$PKG_INSTALL gcc make libxml2-dev libssl-dev libcurl4-openssl-dev \\\n'
' libonig-dev libzip-dev libicu-dev libsqlite3-dev libpng-dev libjpeg-dev \\\n'
' libfreetype6-dev libwebp-dev libxslt1-dev libreadline-dev 2>/dev/null \\\n'
' || $PKG_INSTALL gcc make libxml2-devel openssl-devel libcurl-devel \\\n'
' libonig-devel libzip-devel libicu-devel sqlite-devel libpng-devel \\\n'
' libjpeg-devel freetype-devel libwebp-devel libxslt-devel readline-devel')
out.append('cd /tmp')
out.append('curl -fsSL -O https://www.php.net/distributions/php-' + ver + '.tar.gz')
out.append('tar -xzf php-' + ver + '.tar.gz')
out.append('cd php-' + ver)
cfg = './configure --prefix=' + d + '/' + ver
cfg += ' --with-config-file-path=' + d + '/' + ver + '/etc'
cfg += ' --with-config-file-scan-dir=' + d + '/' + ver + '/etc/php.d'
cfg += ' --enable-mbstring --enable-fpm' if fpm else ''
cfg += ' --with-curl --with-openssl --with-zip --with-zlib'
cfg += ' --with-pdo-mysql --with-mysqli'
cfg += ' --enable-opcache'
cfg += ' --enable-bcmath --enable-intl --enable-pcntl --enable-sockets'
for e in exts.split(','):
e = e.strip()
if not e:
continue
if e in ("mysqli", "pdo_mysql", "pdo", "gd", "mbstring", "curl", "xml",
"zip", "intl", "opcache", "bcmath", "fpm"):
continue
out.append(cfg)
out.append('make -j"$(nproc)"')
out.append('make install')
out.append('mkdir -p ' + d + '/' + ver + '/etc/php.d')
out.append('cp php.ini-production ' + d + '/' + ver + '/etc/php.ini')
out.append('cp sapi/fpm/php-fpm.conf ' + d + '/' + ver + '/etc/ || true')
out.append('cp sapi/fpm/www.conf.default ' + d + '/' + ver + '/etc/www.conf || true')
out.append('cd /tmp && rm -rf php-' + ver + ' php-' + ver + '.tar.gz')
out.append(d + '/' + ver + '/bin/php --version')
return "\n".join(out) + "\n"
# ========================== Ruby ==========================
class Ruby(Generator):
id = "ruby"
title = "Ruby 任意版本 (源码编译)"
category = "runtimes"
icon = "💎"
tags = ["ruby", "rails"]
description = "从 cache.ruby-lang.org 编译 Ruby,启用 readline/openssl/zlib。"
fields = [
Field("version", "Ruby 版本", "select", default="3.3.3",
options=["2.7.8", "3.0.7", "3.1.6", "3.2.4", "3.3.3"]),
Field("install_dir", "安装目录", "text", default="/opt/ruby"),
]
def render(self, p):
ver = p.get("version", "3.3.3")
d = p.get("install_dir", "/opt/ruby")
out = [bash_header(self.title)]
out.append('log "Installing Ruby ' + ver + '..."')
out.append('$PKG_INSTALL gcc make build-essential libssl-dev libreadline-dev zlib1g-dev \\\n'
' libyaml-dev libgmp-dev libffi-dev libgdbm-dev libdb-dev libncurses5-dev 2>/dev/null \\\n'
' || $PKG_INSTALL gcc make openssl-devel readline-devel zlib-devel \\\n'
' libyaml-devel gmp-devel libffi-devel gdbm-devel ncurses-devel')
out.append('cd /tmp')
out.append('curl -fsSL -O https://cache.ruby-lang.org/pub/ruby/' + ver.split('.')[0] + '.' + ver.split('.')[1] + '/ruby-' + ver + '.tar.gz')
out.append('tar -xzf ruby-' + ver + '.tar.gz')
out.append('cd ruby-' + ver)
out.append('./configure --prefix=' + d + '/' + ver + ' --enable-shared --disable-install-doc')
out.append('make -j"$(nproc)"')
out.append('make install')
out.append('cd /tmp && rm -rf ruby-' + ver + ' ruby-' + ver + '.tar.gz')
out.append(d + '/' + ver + '/bin/ruby --version')
return "\n".join(out) + "\n"
# ========================== Rust ==========================
class RustLang(Generator):
id = "rust"
title = "Rust 工具链 (rustup)"
category = "runtimes"
icon = "🦀"
tags = ["rust", "rustup", "cargo"]
description = "通过 rustup 安装 Rust,可指定 nightly / stable 及 toolchain 路径。"
fields = [
Field("channel", "Channel", "select", default="stable",
options=["stable", "beta", "nightly"]),
Field("install_dir", "安装目录", "text", default="/opt/rust"),
Field("default_toolchain", "默认 toolchain", "text", default="stable"),
Field("install_components", "组件列表", "text",
default="rustfmt,clippy,rust-src,rust-analyzer"),
Field("mirror", "rsproxy 国内镜像", "checkbox", default="yes",
help="启用后会写 RUSTUP_DIST_SERVER 环境变量。"),
]
def render(self, p):
ch = p.get("channel", "stable")
d = p.get("install_dir", "/opt/rust")
tool = p.get("default_toolchain", "stable")
comps = p.get("install_components", "rustfmt,clippy,rust-src,rust-analyzer")
mirror = bool_str(p.get("mirror", True))
out = [bash_header(self.title)]
out.append('log "Installing rustup + ' + ch + '..."')
out.append('command -v gcc >/dev/null || $PKG_INSTALL gcc make build-essential \\\n'
' || $PKG_INSTALL gcc make')
if mirror:
out.append('export RUSTUP_DIST_SERVER=https://rsproxy.cn')
out.append('export RUSTUP_UPDATE_ROOT=https://rsproxy.cn/rustup')
out.append('curl --proto =https --tlsv1.2 -sSf https://sh.rustup.rs | '
'sh -s -- -y --default-toolchain none --no-modify-path --prefix=' + d)
out.append('. ' + d + '/env')
out.append('rustup default ' + tool)
out.append('rustup toolchain install ' + ch)
for c in comps.split(','):
c = c.strip()
if c:
out.append('rustup component add ' + c + ' --toolchain ' + ch)
out.append('rustc --version')
out.append('cargo --version')
return "\n".join(out) + "\n"
# ========================== PostgreSQL ==========================
class PostgreSQL(Generator):
id = "postgresql"
title = "PostgreSQL 任意版本 (官方 apt/yum 源)"
category = "databases"
icon = "🐘"
tags = ["postgresql", "pg", "database"]
description = "从 PostgreSQL Global Development Group 官方源安装任意版本。"
fields = [
Field("version", "PG 版本", "select", default="16",
options=["12", "13", "14", "15", "16"]),
Field("port", "端口", "number", default="5432", min_=1, max_=65535),
Field("listen_addresses", "listen_addresses", "text", default="*"),
Field("admin_user", "超级用户", "text", default="postgres"),
Field("admin_password", "超级用户密码", "password", default="changeme"),
Field("data_dir", "数据目录", "text", default="/var/lib/pgsql/data"),
]
def render(self, p):
ver = p.get("version", "16")
port = str(p.get("port", "5432"))
listen = p.get("listen_addresses", "*")
user = p.get("admin_user", "postgres")
pwd = p.get("admin_password", "changeme")
out = [bash_header(self.title)]
out.append('log "Installing PostgreSQL ' + ver + '..."')
out.append('case "$PKG" in\n'
' apt-get)\n'
' $PKG_INSTALL wget gnupg lsb-release ca-certificates\n'
' curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/pgdg.gpg\n'
' echo "deb [signed-by=/usr.share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list\n'
' apt-get update\n'
' $PKG_INSTALL postgresql-' + ver + ' postgresql-client-' + ver + ' postgresql-contrib-' + ver + '\n'
' ;;\n'
' yum|dnf)\n'
' $PKG_INSTALL https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm\n'
' $PKG_INSTALL postgresql' + ver + '-server postgresql' + ver + '-contrib\n'
' /usr/pgsql-' + ver + '/bin/postgresql-' + ver + '-setup initdb\n'
' ;;\n'
'esac')
out.append('systemctl enable --now postgresql')
out.append('sleep 2')
out.append('sudo -u postgres psql -c "ALTER USER ' + user + ' WITH PASSWORD \'' + pwd + '\';"')
out.append('echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/' + ver + '/main/pg_hba.conf 2>/dev/null || true')
out.append('echo "host all all 0.0.0.0/0 md5" >> /var/lib/pgsql/data/pg_hba.conf 2>/dev/null || true')
out.append('sed -i "s/^#listen_addresses.*/listen_addresses = \'' + listen + '\'/" /etc/postgresql/' + ver + '/main/postgresql.conf 2>/dev/null || true')
out.append('sed -i "s/^#listen_addresses.*/listen_addresses = \'' + listen + '\'/" /var/lib/pgsql/data/postgresql.conf 2>/dev/null || true')
out.append('sed -i "s/^port.*/port = ' + port + '/" /etc/postgresql/' + ver + '/main/postgresql.conf 2>/dev/null || true')
out.append('sed -i "s/^port.*/port = ' + port + '/" /var/lib/pgsql/data/postgresql.conf 2>/dev/null || true')
out.append('systemctl restart postgresql')
return "\n".join(out) + "\n"
# ========================== MySQL ==========================
class MySQL(Generator):
id = "mysql"
title = "MySQL / MariaDB 任意版本"
category = "databases"
icon = "🐬"
tags = ["mysql", "mariadb", "database"]
description = "支持 MySQL 5.7/8.0/8.4 或 MariaDB 10.x,自动初始化 root 密码。"
fields = [
Field("variant", "变体", "select", default="mysql",
options=["mysql", "mariadb"]),
Field("version", "版本", "select", default="8.0",
options=["5.7", "8.0", "8.4", "10.11", "11.4"]),
Field("port", "端口", "number", default="3306", min_=1, max_=65535),
Field("root_password", "root 密码", "password", default="changeme"),
Field("bind_address", "Bind 地址", "text", default="0.0.0.0"),
]
def render(self, p):
var = p.get("variant", "mysql")
ver = p.get("version", "8.0")
port = str(p.get("port", "3306"))
pwd = p.get("root_password", "changeme")
bind = p.get("bind_address", "0.0.0.0")
out = [bash_header(self.title)]
if var == "mysql":
out.append('log "Installing MySQL ' + ver + '..."')
out.append('case "$PKG" in\n'
' apt-get)\n'
' $PKG_INSTALL wget gnupg lsb-release\n'
' wget -c https://dev.mysql.com/get/mysql-apt-config_0.8.29-1_all.deb\n'
' DEBIAN_FRONTEND=noninteractive dpkg -i mysql-apt-config_0.8.29-1_all.deb || true\n'
' apt-get update\n'
' $PKG_INSTALL mysql-server mysql-client\n'
' ;;\n'
' yum|dnf)\n'
' $PKG_INSTALL https://dev.mysql.com/get/mysql80-community-release-el$(rpm -E %{rhel})-1.noarch.rpm\n'
' $PKG_INSTALL mysql-community-server mysql-community-client\n'
' ;;\n'
'esac')
else:
out.append('log "Installing MariaDB ' + ver + '..."')
out.append('case "$PKG" in\n'
' apt-get) $PKG_INSTALL mariadb-server mariadb-client ;;\n'
' yum|dnf) $PKG_INSTALL mariadb-server mariadb ;;\n'
'esac')
out.append('systemctl enable --now mysql || systemctl enable --now mariadb')
out.append('sleep 3')
out.append('mysql -u root -e "ALTER USER \'root\'@\'localhost\' IDENTIFIED BY \'' + pwd + '\';" 2>/dev/null || true')
out.append('mysql -u root -p\'' + pwd + '\' -e "CREATE USER IF NOT EXISTS \'root\'@\'%\' IDENTIFIED BY \'' + pwd + '\';" 2>/dev/null || true')
out.append('mysql -u root -p\'' + pwd + '\' -e "GRANT ALL ON *.* TO \'root\'@\'%\' WITH GRANT OPTION; FLUSH PRIVILEGES;" 2>/dev/null || true')
out.append('sed -i "s/^bind-address.*/bind-address = ' + bind + '/" /etc/mysql/mysql.conf.d/mysqld.cnf 2>/dev/null || true')
out.append('sed -i "s/^bind-address.*/bind-address = ' + bind + '/" /etc/my.cnf 2>/dev/null || true')
out.append('sed -i "s/^port.*/port = ' + port + '/" /etc/mysql/mysql.conf.d/mysqld.cnf 2>/dev/null || true')
out.append('sed -i "s/^port.*/port = ' + port + '/" /etc/my.cnf 2>/dev/null || true')
out.append('systemctl restart mysql || systemctl restart mariadb')
return "\n".join(out) + "\n"
# ========================== MongoDB ==========================
class MongoDB(Generator):
id = "mongodb"
title = "MongoDB 任意版本"
category = "databases"
icon = "🍃"
tags = ["mongodb", "nosql", "document"]
description = "从 MongoDB 官方源安装任意版本,启用 replica set。"
fields = [
Field("version", "MongoDB 版本", "select", default="7.0",
options=["4.4", "5.0", "6.0", "7.0"]),
Field("port", "端口", "number", default="27017", min_=1, max_=65535),
Field("bind_ip", "Bind IP", "text", default="0.0.0.0"),
Field("enable_auth", "启用鉴权", "checkbox", default="yes"),
Field("root_user", "root 用户", "text", default="root"),
Field("root_password", "root 密码", "password", default="changeme"),
Field("repl_set", "副本集名 (留空=单点)", "text", default=""),
]
def render(self, p):
ver = p.get("version", "7.0")
port = str(p.get("port", "27017"))
bind = p.get("bind_ip", "0.0.0.0")
auth = bool_str(p.get("enable_auth", True))
user = p.get("root_user", "root")
pwd = p.get("root_password", "changeme")
repl = p.get("repl_set", "").strip()
out = [bash_header(self.title)]
out.append('log "Installing MongoDB ' + ver + '..."')
out.append('case "$PKG" in\n'
' apt-get)\n'
' $PKG_INSTALL wget gnupg curl\n'
' curl -fsSL https://www.mongodb.org/static/pgp/server-' + ver + '.asc | gpg --dearmor -o /usr/share/keyrings/mongodb.gpg\n'
' echo "deb [signed-by=/usr/share/keyrings/mongodb.gpg] https://repo.mongodb.org/apt/ubuntu $(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)/mongodb-org/' + ver + ' multiverse" > /etc/apt/sources.list.d/mongodb-org-' + ver + '.list\n'
' apt-get update\n'
' $PKG_INSTALL mongodb-org\n'
' ;;\n'
' yum|dnf)\n'
' cat > /etc/yum.repos.d/mongodb-org-' + ver + '.repo <<REPO_EOF\n'
'[mongodb-org-' + ver + ']\n'
'name=MongoDB Repository\n'
'baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/' + ver + '/x86_64/\n'
'gpgcheck=1\n'
'enabled=1\n'
'gpgkey=https://www.mongodb.org/static/pgp/server-' + ver + '.asc\n'
'REPO_EOF\n'
' $PKG_INSTALL mongodb-org\n'
' ;;\n'
'esac')
out.append('systemctl enable --now mongod')
out.append('sleep 3')
if auth:
out.append('log "Creating root user..."')
out.append('mongosh --quiet --eval \''
'db.getSiblingDB("admin").createUser({user:"' + user + '",pwd:"' + pwd + '",roles:[{role:"root",db:"admin"}]})\' || warn "auth may already be set"')
out.append('sed -i "s/^#security:/{ security: { authorization: \"enabled\" },/g" /etc/mongod.conf')
out.append('sed -i "s/^ bindIp:.*/ bindIp: ' + bind + '/" /etc/mongod.conf')
out.append('grep -q "^ port:" /etc/mongod.conf || sed -i "/^ bindIp:/a\\ port: ' + port + '" /etc/mongod.conf')
if repl:
out.append('grep -q "^replication:" /etc/mongod.conf || cat >> /etc/mongod.conf <<EOF\\nreplication:\\n replSetName: ' + repl + '\\nEOF')
out.append('systemctl restart mongod')
return "\n".join(out) + "\n"
# Register all
for _g in [Python, NodeJS, GCC, Make, CMake, GoLang, Java, PHP, Ruby, RustLang,
PostgreSQL, MySQL, MongoDB]:
register(_g())