""" 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-。" 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/"), 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') # Go 官方 tarball 解压后固定得到 <父目录>/go parent = d.rsplit('/', 1)[0] if '/' in d else '/opt' extracted = parent.rstrip('/') + '/go' if extracted == d.rstrip('/'): # install_dir 恰好等于解压目标(/opt/go), 无需再 mv out.append('rm -rf ' + d) out.append('tar -C ' + parent + ' -xzf go' + ver + '.linux-amd64.tar.gz') else: # install_dir 与解压目录不同(如 /usr/local/go), 先解压到临时父目录再移动 out.append('rm -rf ' + d) out.append('rm -rf ' + parent + '/go') out.append('tar -C ' + parent + ' -xzf go' + ver + '.linux-amd64.tar.gz') out.append('mv ' + extracted + ' ' + 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//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 <