# baby lnmp **Repository Path**: obaby/baby-lnmp ## Basic Information - **Project Name**: baby lnmp - **Description**: lnmp 一键安装包 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-23 - **Last Updated**: 2026-10-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # LNMP 一键安装包 — 功能实现逻辑说明 本文面向**想读懂或改造这套脚本的人**,从代码层面说明 `lnmp2.3/` 里每个功能是怎么实现的:入口如何分派、变量如何流转、编译与安装分几步、状态怎么判定、升级/卸载/扩展为什么这么写。 > 面向使用者的安装教程请看 `lnmp2.3/README` 与 。本文只讲**实现**,不重复操作指南。 --- ## 1. 总体设计 一句话概括:**用 Bash 把「下载源码 → configure/make → 铺配置文件 → 注册服务 → 校验结果」这一套手工流程固化下来**,全部组件装到 `/usr/local/*`,不依赖发行版的软件包版本。 几个贯穿全局的设计选择: | 设计点 | 实现方式 | 原因 / 影响 | | --- | --- | --- | | 语言 | 纯 Bash + `include/*.sh` 函数库 | 无运行时依赖,任何 Linux 都能跑 | | 安装方式 | 源码编译(数据库可选官方预编译包) | 版本自由、可挂自定义编译参数 | | 安装前缀 | `/usr/local/nginx`、`/usr/local/php`、`/usr/local/mysql`、`/usr/local/mariadb`、`/usr/local/apache` | 与系统包完全隔离 | | 配置驱动 | `lnmp.conf`(用户可改) + `include/version.sh`(脚本内定) | 升级时复用同一份配置 | | 状态判定 | **不看数据库/清单,只 `test -s` 关键文件** | 幂等、可手工干预 | | 选择项 | 全局变量 `Stack` / `DBSelect` / `PHPSelect` / `Bin` / `SelectMalloc` | 编号即菜单项,贯穿所有分支 | | 错误处理 | **没有 `set -e`**,每步失败继续,最后靠 `Check_*` 判定 | 能一次跑完收集日志,但报错点靠日志定位 | `cur_dir=$(pwd)` 是每个入口脚本的第一句:**所有操作都假设你在安装包目录内执行**,`include/`、`conf/`、`src/` 都用相对路径引用。 --- ## 2. 目录与职责 ``` lnmp2.3/ ├── install.sh # 安装总入口:参数分派 + 主流程编排 ├── uninstall.sh # 卸载入口:识别当前栈 → 备份数据 → 删目录 ├── upgrade.sh # 升级入口:8 个子命令分派 ├── addons.sh # 扩展插件入口:{install|uninstall} + 插件名 ├── pureftpd.sh # Pure-FTPd 安装入口 ├── upgrade1.x-2.1.sh # 历史版本迁移专用(1.x → 2.1) ├── lnmp.conf # 用户级配置(镜像、目录、编译参数、开关) ├── include/ │ ├── main.sh # 选单、系统识别、下载、镜像、启动项、DB 连接等公共设施 │ ├── init.sh # 系统准备:时区/NTP/换源/依赖/swap/第三方库编译 │ ├── version.sh # 版本号表 + 编号→版本号的解析(延迟加载) │ ├── nginx.sh # Nginx 编译安装 │ ├── php.sh # 16 个 PHP 版本的编译安装 + 选项推导 │ ├── mysql.sh # MySQL 各版本安装 + 安全初始化 │ ├── mariadb.sh # MariaDB 各版本安装 + 安全初始化 │ ├── apache.sh # Apache 2.2/2.4(LNMPA/LAMP) │ ├── only.sh # 单项安装:仅 Nginx / 仅数据库 │ ├── multiplephp.sh # 多 PHP 版本安装 │ ├── upgrade_*.sh # 各组件升级 / MySQL→MariaDB 迁移 │ ├── end.sh # 收尾:防火墙、开机启动、结果校验、成功/失败输出 │ └── <插件>.sh # 各扩展的编译脚本(redis/opcache/xcache/…) ├── conf/ │ ├── nginx.conf / nginx_a.conf # LNMP / LNMPA 的 Nginx 主配置模板 │ ├── enable-php*.conf # 各 PHP 版本的 fastcgi 片段 │ ├── httpd2x-*.conf # Apache 主配置模板 │ ├── lnmp / lnmpa / lamp # 常驻管理脚本,安装后落到 /bin/lnmp │ ├── rewrite/ # 各程序伪静态规则 │ ├── *.repo # 旧发行版换源模板 │ └── config.inc.php / ocp.php / index.html / pure-ftpd.conf … ├── init.d/ # SysV 启动脚本 + systemd unit 模板 ├── src/patch/ # 打给 Nginx / MySQL 的兼容补丁 └── tools/ # 独立小工具(备份、日志切割、502 检测、重置密码…) ``` **分层关系**:入口脚本只做「参数解析 + 分支 + 日志重定向」,真正的动作全在 `include/`;`include/` 里的函数只依赖 `lnmp.conf`、`version.sh` 提供的变量和 `cur_dir`。`conf/` 是纯静态模板,按需 `\cp` 到目标目录再 `sed` 打补丁。 --- ## 3. 执行模型 ### 3.1 变量即状态 一次安装的「配置」由一组全局变量表达,后续所有分支都围绕它们判断: | 变量 | 取值来源 | 含义 | | --- | --- | --- | | `Stack` | `install.sh` 的 `$1` | `lnmp` / `lnmpa` / `lamp` / `nginx` / `db` / `mphp` | | `DISTRO` / `PM` | `Get_Dist_Name()` 解析 `/etc/issue`、`/etc/*-release` | 发行版名与包管理器(`yum`/`apt`) | | `DBSelect` | 菜单输入 | 数据库编号,`0` 表示不装 | | `PHPSelect` | 菜单输入 | PHP 编号 | | `Bin` | 菜单输入 | `y` 用数据库官方预编译包,`n` 源码编译 | | `SelectMalloc` | 菜单输入 | 内存分配器:不装 / jemalloc / tcmalloc | | `DB_Root_Password` | 交互输入 | 数据库 root 密码 | | `LNMP_Auto` | 环境变量 | 非空时跳过所有「按任意键继续」 | 编号到版本的映射是**唯一事实来源**: - 数据库 `1–5`、`11` → MySQL(`1=5.1.73` … `5=8.0.46`、`11=8.4.8`,**默认 11**) - 数据库 `6–10`、`12`、`13` → MariaDB(`6=5.5.68` … `13=11.8.6`) - PHP `1–16` → `php-5.2.17` … `php-8.5.5`(**默认 14 = PHP 8.3.30**) 菜单文案用数组下标取版本名(`${PHP_Info[n-1]}`),所以编号与数组必须严格错位一一对应;PHP 5.2 还有一条额外约束:`DBSelect=0` 时不允许选。 后续代码大量用正则来区分家族,例如 `[[ "${DBSelect}" =~ ^(1|2|3|4|5|11)$ ]]` 表示 MySQL 家族。**新增版本时最容易踩的坑就是漏掉某一处正则**。 ### 3.2 延迟加载 version.sh `version.sh` 在多数入口里**不是**开头 source,而是在 `Press_Install()` 里 source: ```bash Press_Install() { if [ -z ${LNMP_Auto} ]; then echo "Press any key to install...or Press Ctrl+c to cancel" ... 等待按键 ... fi . include/version.sh Kill_PM } ``` 因为 `Php_Ver`、`Mysql_Ver` 等变量是 `if [ "${DBSelect}" = ... ]` 动态赋值的,必须等用户选完才能确定。这是一个**隐式时序约束**:任何在 `Press_Install` 之前使用 `*_Ver` 的代码都会拿到空值。 `Press_Install` 还顺手做两件事:清掉可能卡住的包管理器进程(`Kill_PM`,含 `yum.pid`、`dpkg` 锁),避免安装过程被前一个 apt/yum 事务阻塞。 ### 3.3 日志与时间 - 主安装:`LNMP_Stack | tee /root/lnmp-install.log` - 单项安装:`/root/nginx-install.log`、`/root/install_database.log` - 升级:`/root/upgrade_<组件><时间戳>.log` - 计时:`start_time` 在 `CentOS_InstallNTP` / `Deb_InstallNTP` 里设置,成功页用 `date +%s` 差值算总耗时 ### 3.4 无 `set -e` 脚本没有开启「出错即退出」,所以**单步失败不会中断**,而是继续往下跑,最后靠 `Check_*` 系列通过检查产物文件来判断整体成败。好处是一次跑完能看到完整的失败面;代价是失败原因必须翻日志。真正会 `exit 1` 的只有显式的前置拦截(非 root、配置非法、已安装、发行版不支持等)。 --- ## 4. install.sh 主流程 ### 4.1 分派前 ```mermaid flowchart TD A["install.sh"] --> B{"uid 是否等于 0"} B -- 否 --> B1["exit 1"] B -- 是 --> C["Stack = 参数 或 lnmp"] C --> D["source lnmp.conf 与 include 下的脚本"] D --> E["Get_Dist_Name 得到 DISTRO / PM / ARCH"] E --> F{"DISTRO 未知"} F -- 是 --> F1["exit 1"] F -- 否 --> G{"Stack 为 lnmp / lnmpa / lamp 且 bin/lnmp 已存在"} G -- 是 --> G1["提示已安装并退出"] G -- 否 --> H["Check_LNMPConf 校验配置"] H --> I["打印 banner"] I --> J{"case Stack 分派"} J --> K["lnmp / lnmpa / lamp / nginx / db / mphp"] ``` 三个前置拦截值得注意: 1. **root 检查**:`id -u != 0` 直接退出。 2. **已安装拦截**:只对 `lnmp|lnmpa|lamp` 生效,判据是 `/bin/lnmp` 是否存在——这是「已装过一套」的事实标志。所以才会有 `nginx`/`db` 单项在已装机器上能跑的情况。 3. **配置校验**(`Check_LNMPConf`):`lnmp.conf` 必须存在且能读出 `Download_Mirror`、`MySQL_Data_Dir`、`MariaDB_Data_Dir`、`Default_Website_Dir`;并且这三个目录都不能是 `/`(防止后续 `rm -rf` 误伤根目录)。 ### 4.2 三类完整安装 ``` lnmp → Dispaly_Selection → LNMP_Stack lnmpa → Dispaly_Selection → LNMPA_Stack(先 Apache_Selection) lamp → Dispaly_Selection → LAMP_Stack (先 Apache_Selection) ``` `Dispaly_Selection`(源码里就是这个拼写)依次问:数据库版本、PHP 版本、内存分配器,并顺带收集 root 密码。 `LNMPA_Stack` / `LAMP_Stack` 额外在 `Init_Install` **之前**调用 `Apache_Selection`,因为它需要 `ApacheSelect` 才能把 `Apache_Ver` 和 Apache 模块类型定下来。其中还有一条硬约束:PHP 5.2 与 Apache 2.4 不兼容,会自动强制降到 2.2。 ### 4.3 LNMP_Stack 八步 | 步骤 | 函数 | 作用 | | --- | --- | --- | | 1 | `Init_Install` | 系统准备 + 数据库安装(见 4.4) | | 2 | `Install_PHP` | 按 `PHPSelect` 分派到 `Install_PHP_XX` | | 3 | `LNMP_PHP_Opt` | 按内存调 php-fpm `pm.*` 参数 | | 4 | `Install_Nginx` | 编译 Nginx | | 5 | `Creat_PHP_Tools` | 铺 phpinfo / 探针 / opcache 面板 / phpMyAdmin | | 6 | `Add_Iptables_Rules` | 写入防火墙规则并持久化 | | 7 | `Add_LNMP_Startup` | 落 `/bin/lnmp`、注册并启动三个服务 | | 8 | `Check_LNMP_Install` | 校验 Nginx/DB/PHP 产物,输出成功或失败 | 顺序上 **PHP 先于 Nginx**:因为 Nginx 的配置和 php-fpm 的 socket 路径都要先确定 PHP 是否就绪;而 `LNMP_PHP_Opt` 必须紧跟 PHP 安装,趁 `php-fpm.conf` 刚生成时改参数。 `lnmpa` 的顺序是 Apache → PHP → Nginx(Nginx 作为 80 端口反向代理,PHP 以 `mod_php` 挂在 Apache 上);`lamp` 是 Apache → PHP,不装 Nginx。 ### 4.4 Init_Install 内部顺序 这个函数是「让机器具备编译条件」的总装线,顺序有依赖关系: | 阶段 | 动作 | 说明 | | --- | --- | --- | | 1 | `Press_Install` | 等确认、加载版本表、清包管理器锁 | | 2 | `Print_APP_Ver` + `Get_Dist_Version` + `Print_Sys_Info` | 展示将装版本、系统信息、内存/磁盘 | | 3 | `Check_Hosts` | 修 `/etc/hosts`(缺 localhost 记录时补齐) | | 4 | `Check_CMPT` | 版本兼容性硬校验 | | 5 | `Modify_Source` + `Check_Mirror` | 换国内源、探测下载镜像 | | 6 | `Add_Swap` | 低内存机器按内存量创建 swap(并检查磁盘余量) | | 7 | `Set_Timezone` | 设时区为 `Asia/Shanghai` | | 8 | NTP + `RemoveAMP` + `Dependent` | 装时间同步、卸载系统自带 AMP、装编译依赖 | | 9 | `Disable_Selinux` | 关闭 SELinux | | 10 | `Check_Download` | **预下载**所有源码包(含依赖库、Nginx、DB、PHP、phpMyAdmin、探针) | | 11 | 第三方库编译 | libiconv → libmcrypt → mhash → mcrypt → freetype → pcre → icu4c | | 12 | 内存分配器 | 按 `SelectMalloc` 装 jemalloc / tcmalloc | | 13 | 系统库适配 | `CentOS_Lib_Opt` / `Deb_Lib_Opt` | | 14 | 数据库 | 按 `DBSelect` 分派 `Install_MySQL_XX` / `Install_MariaDB_XX` | | 15 | 收尾 | `TempMycnf_Clean`、`Clean_DB_Src_Dir`、`Check_PHP_Option` | 两个设计细节: - **`Check_Download` 先整包预下载**:把所有 tarball 拉到 `src/` 再开始编译,避免编译到一半因网络中断前功尽弃;同时因为 `Download_Files` 会跳过已存在文件,重跑安装时能复用。 - **`Check_PHP_Option` 放在最后**:它要把 `Enable_PHP_*` 开关翻译成 `PHP_Buildin_Option` 字符串(可能顺带装 `libldap`、`libsodium`、`uw-imap` 等系统依赖),必须在 PHP 编译前完成,但又依赖前面装好的基础库。 --- ## 5. 配置与版本两条线 ### 5.1 lnmp.conf —— 用户可改的部分 ```bash Download_Mirror='https://soft.lnmp.com' # 下载源(静态文件服务器,详见 6.4) Nginx_Modules_Options='' # 追加给 nginx ./configure PHP_Modules_Options='' # 追加给 php ./configure MySQL_Data_Dir='/usr/local/mysql/var' # 数据库数据目录 MariaDB_Data_Dir='/usr/local/mariadb/var' Default_Website_Dir='/home/wwwroot/default' # 默认站点目录 Enable_Nginx_Openssl='y' # 是否把 OpenSSL 源码编进 Nginx Enable_Nginx_Lua='n' # LuaJIT + lua-nginx-module Enable_Ngx_FancyIndex='n' Enable_Swap='y' # 是否自动加 swap Enable_PHP_Exif/Fileinfo/Ldap/Bz2/Sodium/Imap='n' Download_Insecure='n' # y 时 wget 加 --no-check-certificate Enable_Download_Checksum='y' # 是否做 SHA256 校验(需 checksums.sha256,见 6.8) ``` 这些值在安装、升级、单项安装、卸载里**都会被读取**,所以改过之后要备份;`upgrade.sh` 也 source 同一份 `lnmp.conf`,保证升级参数与安装参数一致。 注意 `Download_Mirror` 的覆盖面有限:它只影响把它当 URL 前缀使用的调用点,MySQL 预编译包、MariaDB、redis、ionCube 等仍直连官方域名,详见 **6.5**。 ### 5.2 version.sh —— 脚本内定的版本表 分两类:**常量**(`Nginx_Ver='nginx-1.30.0'`、`Openssl_New_Ver='openssl-3.5.5'`、`Pcre_Ver` 等)和**按选择推导**(`Mysql_Ver`/`Mariadb_Ver`/`Php_Ver`/`PhpMyAdmin_Ver`/`Apache_Ver`)。 phpMyAdmin 版本是**跟着 PHP 走**的:PHP 5.2–5.4 用 4.0.10.20,5.5–5.6 用 4.9.11,7.x/8.x 用 5.2.3。这就是为什么换 PHP 版本会连带换 phpMyAdmin。 --- ## 6. 下载与校验 ### 6.1 Download_Files ```bash Download_Files() { URL=$1; FileName=$2 [ "${Download_Insecure}" = "y" ] && Wget_TLS_Option="--no-check-certificate" if [ -s "${FileName}" ]; then echo "${FileName} [found]" Verify_Download_File "${FileName}" else wget -c --progress=dot --prefer-family=IPv4 ${Wget_TLS_Option} "${URL}" -O "${FileName}" Verify_Download_File "${FileName}" fi } ``` 三个行为决定了它的可用性: 1. **已存在即跳过**(`[ -s ]`),所以源码包可以手工预放到 `src/`,也可以断点续传(`wget -c`)。 2. **强制 IPv4**,规避部分 VPS 的 IPv6 出口问题。 3. **默认校验 TLS 证书**(`Download_Insecure='n'`),旧系统若证书链不全才需要改配置。 ### 6.2 Verify_Download_File - `Enable_Download_Checksum != y` → 直接跳过 - `src/checksums.sha256` 不存在 → 跳过(本发行包里通常没有这个文件,属于可选增强) - 文件不在清单里 → 跳过 - 在清单里但哈希不符 → **删除文件并返回失败**,逼出「重新下载」 ### 6.3 Tar_Cd 统一解压 + 进目录,按扩展名分派 `tar zxf/jxf/Jxf`,并且**解压前先 `rm -rf` 同名目录**,避免上次残留导致 `mv`/`cp` 出错。 ### 6.4 下载源:`Download_Mirror` 覆盖了什么 `Download_Mirror`(默认 `https://soft.lnmp.com`)**不是包仓库**,没有元数据索引、不做依赖解析,只是一个**静态 HTTP 文件服务器**:把上游官方 tarball 按类别存好供国内加速。它只被 `Download_Files` 调用点以 URL 前缀形式拼接,因此**能覆盖哪些组件,完全取决于每个调用点怎么拼 URL**。 镜像的真实目录结构(实测可达): | 前缀 | 用途 | 示例 | | --- | --- | --- | | `/web/` | Web 组件、PHP 源码、PHP 扩展 | `/web/nginx/nginx-1.30.0.tar.gz` | | `/lib/` | 底层依赖库(OpenSSL、PCRE、freetype、icu4c、boost…) | `/lib/openssl/openssl-3.5.5.tar.gz` | | `/datebase/` | 数据库源码、phpMyAdmin | `/datebase/phpmyadmin/phpMyAdmin-5.2.3-all-languages.tar.xz` | | `/ftp/` | Pure-FTPd | `/ftp/pure-ftpd/pure-ftpd-1.0.49.tar.bz2` | | `/prober/` | 探针 | `/prober/p.tar.gz` | > **`datebase` 是脚本里的真实拼写**(`database` 的笔误),因为镜像目录也按这个拼出来的,所以一直能工作。自建镜像**必须沿用这个错拼**,否则 `include/init.sh:518` 与 `include/only.sh:163` 会 404。 ### 6.5 三类下载去向(关键不对称) 下载路径分成三条互不统属的线: **A 类:走 `Download_Mirror`** —— 绝大多数组件 | 类别 | 镜像路径 | 代码位置 | | --- | --- | --- | | Nginx 源码 | `/web/nginx/` | `init.sh:497`、`only.sh:48` | | PHP 源码 | `/web/php/` | `init.sh:528` | | PCRE(Nginx 依赖) | `/web/pcre/` | `init.sh:676` | | OpenSSL 1.0.2u / 3.5.5 / 1.1.1w | `/lib/openssl/` | `init.sh:801/820/846` | | Apache + APR + APR-util | `/web/apache/` | `init.sh:535-537` | | phpMyAdmin | `/datebase/phpmyadmin/` | `init.sh:532` | | **MySQL 源码**(`Bin=n`) | `/datebase/mysql/` | `init.sh:518` | | 基础库 | `/lib/libiconv`、`/web/libmcrypt`、`/web/mhash`、`/lib/freetype`、`/lib/curl`、`/lib/icu4c`、`/lib/boost`、`/lib/libzip`、`/lib/nghttp2`、`/lib/autoconf` | `init.sh` 各 `Install_*` | | 内存分配器 | `/lib/jemalloc`、`/lib/tcmalloc`、`/lib/libunwind` | `init.sh:491-494` | | Lua 相关 | `/lib/lua/`(LuaJIT、lua-nginx-module、ngx_devel_kit、resty) | `nginx.sh:36-40` | | PHP 扩展 | `/web/redis`、`/web/apcu`、`/web/memcached`、`/web/swoole`、`/web/opcache`、`/web/xcache`、`/web/sodium`、`/web/imagemagick` … | 各插件脚本 | | Zend 系列 | `/web/zend/` | `php.sh:316` 等 | | Pure-FTPd | `/ftp/pure-ftpd/` | `pureftpd.sh:44` | | 探针 | `/prober/p.tar.gz` | `init.sh:533` | **B 类:硬编码官方域名,`Download_Mirror` 管不到** | 组件 | 实际 URL | 位置 | | --- | --- | --- | | **MySQL 预编译包**(`Bin=y`,8.0/8.4) | `https://cdn.mysql.com/Downloads/MySQL-8.4/...`,失败回退 `/archives/...` | `init.sh:508-515` | | **MariaDB**(预编译与源码都一样) | `https://downloads.mariadb.org/rest-api/mariadb//...` | `init.sh:523/525` | | redis 服务端 | `http://download.redis.io/releases/` | `redis.sh:23` | | phpredis 扩展 | `http://pecl.php.net/get/` | `redis.sh:69-75` | | ionCube | `https://downloads.ioncube.com/` | `ionCube.sh:32` | | OpenSSL 3.x 兜底 | `https://www.openssl.org/source/` | `init.sh:822` | 最容易踩的坑就在 MySQL 的分叉上——**同样选 MySQL,源码编译走国内镜像,预编译包却要连美国 CDN**: ```bash elif [[ "${Bin}" = "y" && "${DBSelect}" = "11" ]]; then Download_Files https://cdn.mysql.com/Downloads/MySQL-8.4/${Mysql_Ver}-linux-glibc2.17-${DB_ARCH}.tar.xz ... [[ $? -ne 0 ]] && Download_Files https://cdn.mysql.com/archives/mysql-8.4/... else Download_Files ${Download_Mirror}/datebase/mysql/${Mysql_Ver}.tar.gz ${Mysql_Ver}.tar.gz fi ``` 而 `Bin` 的默认值恰好是 `y`(镜像检测可用时),所以默认配置下「卡在数据库那一步」通常就是 `cdn.mysql.com` 在慢或不通。 **C 类:升级走第三套地址** 升级脚本**不复用** `Check_Download`,各自现取: | 脚本 | 来源策略 | | --- | --- | | `upgrade_nginx.sh` | 直接 `wget http://nginx.org/download/...`,**不用 `Download_Files`**、不查镜像 | | `upgrade_php.sh` / `upgrade_mphp.sh` | `php.vpszt.com` → `php.net/distributions` → `museum.php.net/php5` 三级回退 | | `upgrade_mysql.sh` | `cdn.mysql.com` | | `upgrade_mariadb.sh` | `downloads.mariadb.org` | | `upgrade_phpmyadmin.sh` | 走 `Download_Mirror` | ### 6.6 文件落点与离线部署 所有下载都先 `cd ${cur_dir}/src`,tarball 统一堆在 `lnmp2.3/src/` 下,文件名取 basename。安装完成后 `Clean_*_Src_Dir` 只删**解压出来的源码目录**,压缩包保留,供重装/升级复用。 这带来一个实用的离线部署能力:**把 tarball 按同名预放进 `src/`,脚本就不会联网**——`Download_Files` 第一句 `[ -s "${FileName}" ]` 直接跳过。A 类和 B 类都适用,B 类只要文件名对得上(如 `mysql-8.4.8-linux-glibc2.17-x86_64.tar.xz`)同样不会触发下载。 ### 6.7 镜像探测的真实行为 `Check_Mirror` 的触发条件是**写死比对旧域名**: ```bash if [ "${Download_Mirror}" = "https://soft.vpser.net" ]; then ``` 由此产生两个反直觉的结论: 1. **填别的值(哪怕写错)不会触发任何探测**。脚本会直接使用它,不做连通性预检,最终表现为一串 `wget` 失败。只有填成 `https://soft.vpser.net` 才会按「服务器所在国家」在 `soft1`/`soft2` 之间探测,全失败则提示手工改 `lnmp.conf` 并退出。国家判断走 `Get_Country`(请求 `ip.vpszt.com/country`)。 2. **默认值下 `Check_Mirror` 是空操作**,所以默认安装的下载可用性完全取决于 `soft.lnmp.com` 本身。 ### 6.8 校验为何实际不生效 `Enable_Download_Checksum` 默认是 `y`,但 `Verify_Download_File` 还要求 `src/checksums.sha256` 存在,而**本发行包里没有这个文件**,于是每次都静默跳过。想让校验真正起作用,需要自己生成清单(格式 `sha256 文件名`,第二列必须与 `Download_Files` 的 `$2` 完全一致)。 同理,`Download_Insecure='n'` 意味着**默认校验证书**,这是 `wget` 报错的高频来源;只有老旧系统或中间人代理环境才需要改 `y`(会加 `--no-check-certificate`)。 --- ## 7. 各组件实现 ### 7.1 第三方库(init.sh) 统一模式:`Download_Files → Tar_Cd → ./configure → Make_Install → rm -rf 源码目录`。 `Make_Install` 是全局的编译封装: ```bash Make_Install() { make -j `grep 'processor' /proc/cpuinfo | wc -l` [ $? -ne 0 ] && make # 并行失败退化为单线程 make install } ``` `PHP_Make_Install` 在此基础上加 `ZEND_EXTRA_LIBS='-liconv'`,解决 PHP 静态链接 libiconv 的问题。 值得单独一提的: - `Install_Libmcrypt` 会额外编 `libltdl/` 并手工补 `libmcrypt.so*` 软链,因为它的 `./configure` 不带 ltdl 安装。 - `Install_Pcre` 装的 pcre 8.45 是给 Nginx 用的(`--with-pcre=` 指向源码目录)。 - `Install_Icu4c` / `Install_Icu60`:给 PHP `intl` 用;新发行版系统 icu 太新/太旧时会退回自编译 `icu60`。 - `Add_Swap` 按内存分档(<1G 建 1G、1–2G 建 2G、2–4G 建 4G、4–16G 建 8G),且磁盘余量不足时禁用。 ### 7.2 Nginx(nginx.sh) `Install_Nginx` 的流程: ``` Install_Nginx_Openssl # 决定 --with-openssl 指向哪个 openssl 源码 Install_Nginx_Lua # 可选:LuaJIT + lua-nginx-module + resty 库 Install_Ngx_FancyIndex # 可选:目录索引美化 Tar_Cd nginx-1.30.0 patch (Fedora/gcc8 兼容) ./configure ... # 按版本分支两套参数 Make_Install 铺 conf/ + 建 www 用户/目录 + 装 init.d/systemd ``` 关键实现点: 1. **OpenSSL 是源码编进去的**,不是链接系统库。`Enable_Nginx_Openssl='y'` 时按 Nginx 版本选 `openssl-1.0.2u` 或 `openssl-3.5.5`,传 `--with-openssl=${cur_dir}/src/openssl-x`。这样即使系统 OpenSSL 很旧也能开 TLSv1.2/1.3。 2. **两套 configure 参数以 1.9.4 为分界**:老版本用 `--with-http_spdy_module --with-ipv6`,新版本换成 `--with-http_v2_module --with-http_v3_module --with-stream*`。 3. **用户可注入**:`${Nginx_Modules_Options}` 永远拼在最后,便于加第三方模块。 4. `LNMPA` 栈会拷 `conf/nginx_a.conf` 而不是 `conf/nginx.conf`,并额外铺 `proxy.conf`、`proxy-pass-php.conf`。 5. 安装末尾按内核版本给 `listen 80 default_server` 加 `reuseport`,并在 WSL 下注入 `fastcgi_buffering off`。 6. `.user.ini` 写 `open_basedir` 并用 `chattr +i` 锁定,同时往 `fastcgi.conf` 追加一条 `PHP_ADMIN_VALUE`——这是 LNMP 防跨站的核心手段,卸载时对应要 `chattr -i`。 ### 7.3 PHP(php.sh) **一个版本一个函数**:`Install_PHP_52` … `Install_PHP_85`。它们的骨架几乎相同: ``` Install 依赖(libzip / icu 视版本) Tar_Cd ./configure ← 按 Stack 分两套:--enable-fpm 或 --with-apxs2 PHP_Make_Install Ln_PHP_Bin # 软链 php/phpize/pear/pecl 到 /usr/bin,清空 conf.d cp php.ini-production → /usr/local/php/etc/php.ini sed 调优 php.ini Pear_Pecl_Set + Install_Composer 生成 php-fpm.conf(仅 lnmp 栈) 装 init.d/php-fpm + systemd unit ``` **configure 参数是「内置基础项 + 动态项 + 用户项」三段拼接**: ```bash ./configure ...固定项... ${with_curl} ... ${with_openssl} ... \ ${PHP_Buildin_Option} ${PHP_Modules_Options} ``` 动态项由 `Check_PHP_Option` 统一生成,每个 `PHP_with_XX()` 负责「开关判断 + 依赖安装 + 返回参数」: | 函数 | 逻辑 | | --- | --- | | `PHP_with_openssl` | 系统 OpenSSL ≥1.1 且 PHP 5.x/7.0 时,改用自编译 `openssl-1.0.2u`(`UseOldOpenssl=y`) | | `PHP_with_curl` | ARM CentOS 或用了旧 OpenSSL 时,改链接自编译 curl 到 `/usr/local/curl` | | `PHP_with_fileinfo` | 关闭且内存 <1G 时显式 `--disable-fileinfo`(防编译时 OOM) | | `PHP_with_Ldap/Bz2/Sodium/Imap` | 开启时先装系统 dev 包(含 EPEL、`uw-imap` 的 rpm),再给参数 | | `PHP_with_Intl` | 新版发行版 + 旧 PHP 时编译 `icu60` 并 `--with-icu-dir` | **php.ini 调优是硬编码 sed**:上传 50M、`date.timezone=PRC`、`cgi.fix_pathinfo=0`、`max_execution_time=300`,以及一长串 `disable_functions`(`exec`/`system`/`shell_exec`/`proc_open`/`symlink` 等)。这套默认值偏安全保守,需要放开就用 `tools/remove_disable_function.sh`。 **php-fpm.conf 是 heredoc 现写的**:`listen = /tmp/php-cgi.sock`(unix socket 而非 127.0.0.1:9000),`pm=dynamic` 初始值很小,随后 `LNMP_PHP_Opt` 按内存重写: | 内存 | max_children | start_servers | min/max_spare | | --- | --- | --- | --- | | 1–2G | 20 | 10 | 10 / 20 | | 2–4G | 40 | 20 | 20 / 40 | | 4–8G | 60 | 30 | 30 / 60 | | >8G | 80 | 40 | 40 / 80 | ### 7.4 MySQL / MariaDB(mysql.sh / mariadb.sh) 每个版本一个 `Install_MySQL_XX`,内部**先分叉 Bin/Source**,然后殊途同归: ``` [Bin=y] Tar_Cd 官方预编译包 → mv 到 /usr/local/mysql [Bin=n] Tar_Cd 源码 → (5.1/5.5) ./configure / (5.6+) cmake → Make_Install ────────── 以下两条路径共用 ────────── 建 mysql 用户组/用户 生成 /etc/my.cnf(heredoc,含 MySQL_Opt 追加的内存分配器配置) mysqld --initialize-insecure 初始化数据目录 装 init.d/mysql + systemd unit 写 /etc/ld.so.conf.d/mysql.conf,软链 lib/include MySQL_Sec_Setting ``` 要点: 1. **预编译包只支持 x86_64/i686/aarch64**,非这些架构菜单里会强制 `Bin=n`;MySQL 8.x / MariaDB 10.3+ 在源码编译模式下还要求内存 >1G,否则 `exit 1`。 2. `my.cnf` 里 `datadir` 直接取 `lnmp.conf` 的 `MySQL_Data_Dir`,所以改配置能换数据目录。 3. **`--initialize-insecure` 建的是空密码实例**,紧接着 `MySQL_Sec_Setting` 才设密码并加固。 4. `MySQL_Sec_Setting` 是幂等设计的核心:先尝试 `mysqladmin -u root password`,失败则用 `~/.emptymy.cnf` 空密码登录,再按版本用不同语法改密码(5.7 用 `PASSWORD()`、8.x 用 `SET PASSWORD`、5.1–5.6 用 `UPDATE mysql.user`);随后**删匿名用户、禁 root 远程登录、删 test 库、FLUSH**,最后停库——把启动留给统一的 `Add_*_Startup` 阶段。 5. `Make_TempMycnf` / `TempMycnf_Clean` 是配套的临时凭据机制:需要免交互执行 SQL 时写一个带密码的临时 cnf,用完删除。 MariaDB 的 `Mariadb_Sec_Setting` 结构完全对应,只是版本号和 socket/软链路径不同。 ### 7.5 Apache(apache.sh) 仅在 `lnmpa`/`lamp` 使用,`Install_Apache_22/24` 负责编译与铺配置。PHP 以 Apache 模块方式加载,所以 `Check_Apache_Files` 的判据是 `modules/libphp*.so` 是否存在,且**按 PHP 大版本区分模块名**: - PHP 5.x → `libphp5.so` - PHP 7.x → `libphp7.so` - PHP 8.x → `libphp.so` 这也是为什么 PHP 版本与 Apache 版本要先一起定下来。 ### 7.6 站点附属物(Creat_PHP_Tools) 安装完成后往默认站点目录铺: | 文件 | 作用 | | --- | --- | | `phpinfo.php` | `phpinfo()` | | `p.php` | 探针(来自 `src/p.tar.gz`) | | `index.html` + `lnmp.gif` | 默认首页 | | `ocp.php` | opcache 管理面板(PHP ≥ 5.4) | | `phpmyadmin/` | 解压 phpMyAdmin + 换 `config.inc.php` + 随机化 blowfish_secret + 建 `upload/`、`save/` | --- ## 8. 服务注册与启动 ### 8.1 双轨启动管理 `StartUp` / `Remove_StartUp` 同时兼容 systemd 和 SysV: ``` systemctl 存在 且 非 WSL/Docker 且 有对应 .service 文件 → systemctl daemon-reload && systemctl enable/disable 否则 → yum: chkconfig --add/--del + on/off → apt: update-rc.d -f defaults / remove ``` WSL 与 Docker 被显式排除在 systemd 之外(容器里 systemd 通常不可用,改用 chkconfig/rc.d)。`init.d/` 目录里同时提供 `init.d.nginx`(SysV)和 `nginx.service`(systemd),安装时两份都拷过去,由 `StartUp` 决定启用哪条路径。 ### 8.2 落 /bin/lnmp 并按栈改写 `Add_LNMP_Startup` / `Add_LNMPA_Startup` / `Add_LAMP_Startup` 会把 `conf/lnmp`、`conf/lnmpa` 或 `conf/lamp` 拷成 `/bin/lnmp`,然后**用 sed 就地改写模板里的分支**: ```bash # 装的是 MariaDB → 把管理脚本里的 mysql 路径换成 mariadb sed -i 's#/etc/init.d/mysql#/etc/init.d/mariadb#' /bin/lnmp # DBSelect=0(不装数据库)→ 删掉数据库相关行 sed -i 's#/etc/init.d/mysql.*##' /bin/lnmp # PHP 5.2 的 pid 路径不同 sed -i 's#/usr/local/php/var/run/php-fpm.pid#/usr/local/php/logs/php-fpm.pid#' /bin/lnmp ``` 这是「一份模板适配多种组合」的典型手法:**不为每种组合维护独立脚本,而是安装后原地裁剪**。 ### 8.3 防火墙 `Add_Iptables_Rules` 只在系统有 `iptables` 时执行,规则顺序固定:放行 lo → 放行已建立连接 → 放行 22/80/443 → **DROP 3306** → 放行 ICMP。随后按发行版持久化(`iptables-services` 或 `iptables-persistent`),并在 CentOS 上停掉 `firewalld` 避免规则互斥。 --- ## 9. 结果校验 安装是否成功**不看退出码,只看产物**: | 校验函数 | 判据 | 置位标志 | | --- | --- | --- | | `Check_Nginx_Files` | `nginx.conf` + `sbin/nginx` | `isNginx` | | `Check_DB_Files` | `bin/mysql` + `bin/mysqld_safe` + `/etc/my.cnf`(MariaDB 查对应路径) | `isDB` | | `Check_PHP_Files` | lnmp 栈查 `sbin/php-fpm`+`etc/php.ini`+`bin/php`;其他栈查 `bin/php`+`etc/php.ini` | `isPHP` | | `Check_Apache_Files` | `bin/httpd` + 对应 `libphp*.so` + `httpd.conf` | `isApache` | `Check_LNMP_Install` 要求三者同时 ok 才调 `Print_Sucess_Info`;否则 `Print_Failed_Info` 会**删除 `/bin/lnmp`**(避免留下一个「看起来装好了」的假象)并提示上传 `/root/lnmp-install.log`。 成功页会顺手 `lnmp status` + 列监听端口 + 打印耗时,等于做了一次冒烟测试。 `DBSelect=0` 时 `Check_DB_Files` 直接把 `isDB` 置 ok,所以「不装数据库」是合法路径而不是失败。 --- ## 10. 单项安装(only.sh) `install.sh` 的 case 里有三个非完整安装分支: ### 10.1 `./install.sh nginx` `Install_Only_Nginx` 是一条**精简版流水线**: ``` Press_Install → Get_Dist_Version → Modify_Source → Nginx_Dependent → 编译 pcre → Download nginx → Install_Nginx → StartUp/StartOrStop nginx → Add_Iptables_Rules → 铺首页 + /bin/lnmp → Check_Nginx_Files ``` 与完整安装的差异很重要: - **不执行** `Add_Swap`、`Set_Timezone`、`Check_CMPT`、`Check_Download`(不全量预下载,只按需下 pcre + nginx) - **会执行** `Nginx_Dependent`,而它包含 `rpm -e httpd` / `apt-get purge apache2*` ——**在已有 Apache 的机器上会强拆** - 不装 PHP,所以 PHP 站点会 502;但会覆盖 `/usr/local/nginx` 并重装 `/bin/lnmp` - 不受「已安装拦截」限制(那段判断只拦 lnmp/lnmpa/lamp) ### 10.2 `./install.sh db` `Install_Only_Database` 先 `Check_DB`,若已存在数据库直接退出;然后走 `Database_Selection` 选版本/预编译包,再 `Press_Install` + `Install_Database`。 `Install_Database` 复用了完整安装里的 `DB_Dependent → Check_Openssl → Install_MySQL/MariaDB_XX → TempMycnf_Clean → StartUp/Start → Check_DB_Files`,所以它产出的数据库与完整安装完全一致,可以事后直接 `lnmp database add` 使用。 **破坏性**:`DB_Dependent` 会卸载系统包安装的 MySQL/MariaDB 并清库(脚本会红字警告 + 等待确认)。 ### 10.3 `./install.sh mphp` `Install_Multiplephp` 只允许 LNMP 栈(先 `Check_Stack`,非 lnmp 直接退出),详见第 11 节。 --- ## 11. 多 PHP 版本机制 核心思路:**不改主 PHP,另起一套前缀 + 独立 socket + 独立 init 脚本**。 | 维度 | 主 PHP | 附加版本 | | --- | --- | --- | | 安装目录 | `/usr/local/php` | `/usr/local/php8.4`(版本号后缀) | | socket | `/tmp/php-cgi.sock` | `/tmp/php-cgi8.4.sock` | | 启动脚本 | `/etc/init.d/php-fpm` | `/etc/init.d/php-fpm8.4` | | Nginx 片段 | `enable-php.conf` | `enable-php8.4.conf` | | 切换方式 | — | 改 vhost 里的 `include` 行 | `conf/enable-phpX.Y.conf` 与 `init.d/init.d.php-fpmX.Y` 都是**预生成模板**,装哪个版本就拷哪个。 `lnmp vhost add` 的 `Multiple_PHP_Select()` 通过「同时存在 `phpX.Y/sbin/php-fpm`、`enable-phpX.Y.conf`、`init.d/php-fpmX.Y`」来识别已装版本,并生成对应的 `include` 行;pathinfo 模式还会现场从 `enable-php-pathinfo.conf` 复制一份并 `sed` 改 socket 名。 `multiplephp.sh` 则是把 `install/upgrade_php.sh` 的函数拿来做「装到带后缀的前缀」,这也是为什么 1400 多行的文件里大部分内容和 `php.sh` 高度相似。 --- ## 12. 升级机制 `upgrade.sh` 只做分派和日志: | 参数 | 函数 | 特点 | | --- | --- | --- | | `nginx` | `Upgrade_Nginx` | **热升级**:`mv` 旧二进制为 `nginx.<时间戳>` → 拷新 `objs/nginx` → `nginx -t` → `make upgrade` | | `mysql` | `Upgrade_MySQL` | 先备份数据,再装新版并原地升级 | | `mariadb` | `Upgrade_MariaDB` | 同上 | | `php` | `Upgrade_PHP`(`Stack=lnmp`) | 重编译到 `/usr/local/php`,保留配置文件 | | `phpa` | `Upgrade_PHP` | 针对 LNMPA/LAMP(`mod_php` 场景) | | `m2m` | `Upgrade_MySQL2MariaDB` | 跨引擎迁移,单独流程 | | `phpmyadmin` | `Upgrade_phpMyAdmin` | 换目录 + 保留 config | | `mphp` | `Upgrade_Multiplephp` | 仅小版本升级 | Nginx 的升级方式最典型地区分了「编译产物替换」与「服务重建」:不重装服务、不改配置,只换二进制并让 master 拉起新 worker,因此**不断连接**。其它组件做不到热替换,都是「装新的 → 迁移配置/数据 → 重启」。 升级会复用 `lnmp.conf` 与 `version.sh` 里的模块参数,但**不读版本编号**(升级时用户直接输入目标版本号)。 下载源上还有一个易被忽略的差异:**升级不走安装期的镜像体系**。`upgrade_nginx.sh` 直接 `wget http://nginx.org/download/...`(完全绕过 `Download_Files` 与 `Download_Mirror`),PHP 走 `php.vpszt.com` → `php.net` → `museum.php.net` 三级回退,MySQL/MariaDB 直连官方 CDN。所以「安装很快」不代表「升级也快」,详见 **6.5 的 C 类**。 --- ## 13. 扩展插件机制(addons.sh) 入口是两级参数:`./addons.sh {install|uninstall} <插件名>`,无参数则给菜单。`action` 是安装/卸载,`action2` 是插件名。 实现上分两类: 1. **编译成 PHP 扩展**(redis、memcached、apcu、imagick、exif…):`phpize → ./configure --with-php-config=/usr/local/php/bin/php-config → make → make install`,然后往 `/usr/local/php/conf.d/` 写 ini,最后重启 PHP。 2. **独立服务**(redis-server、memcached):单独编译到 `/usr/local/redis`、`/usr/local/memcached`,装 `init.d` + systemd,可选装 PHP 客户端扩展。 **多版本感知**是这里的难点:`Select_PHP()` 会检测是否存在 `phpX.Y` 附加版本,让用户选定目标 PHP,然后设置 `PHP_Path` 和 `PHPFPM_Initd` 供后续编译/重启使用。`Get_PHP_Ext_Dir()`(基于 `php-config --extension-dir`)用来定位扩展落盘目录。 `lnmp.conf` 里的 `Enable_PHP_*` 开关**只影响首次安装**(编进 PHP 或作为内置选项),部分第三方模块(如 swoole)明确不支持走配置开关,只能用 `addons.sh`。 --- ## 14. 卸载机制(uninstall.sh) ``` root 检查 → source main.sh → Check_DB / Get_Dist_Name → Check_Stack 识别当前栈 → 菜单选 1/2/3 → 打印将删除的路径清单 → 倒计时 3 秒 → 按任意键确认 → 对应 Uninstall_XXX ``` 关键实现点: 1. **数据目录是 `mv` 不是 `rm`**:`mv ${MySQL_Data_Dir} /root/databases_backup_$(date +%Y%m%d%H%M%S)`,卸载不会丢库。 2. 先 `lnmp kill` + `lnmp stop`,再 `Remove_StartUp` 取消开机启动,避免删文件时服务还在跑。 3. 删 `.user.ini` 前必须先 `chattr -i`(安装时加过 immutable)。 4. 多 PHP 用 `for mphp in /usr/local/php[5,7].[0-9]` 遍历,逐个停服务 + 取消启动 + 删目录。 5. acme.sh 会走 `--uninstall` 并清理 crontab 里的续期任务。 6. Apache 相关文件只在 LNMPA/LAMP 分支删除。 --- ## 15. 常驻管理脚本 `/bin/lnmp` `conf/lnmp`(≈59KB)是安装后留在系统里的「运维控制台」,核心是一个大 `case "${arg1}"`: | 子命令 | 实现入口 | 逻辑要点 | | --- | --- | --- | | `start/stop/restart/reload/kill/status` | `lnmp_start` 等 | 串行操作 nginx / 数据库 / php-fpm(或 httpd) | | `nginx|mysql|mariadb|php-fpm|pureftpd ` | 直接转发 `/etc/init.d/` | 单服务控制 | | `vhost {add|list|del}` | `Function_Vhost` → `Add_VHost` 等 | 见下 | | `database {add|list|edit|del}` | `Function_Database` | 需先 `Verify_DB_Password` 拿 root 凭据,结束 `TempMycnf_Clean` | | `ftp {add|list|edit|del|show}` | `Function_Ftp` | 先 `Check_Pureftpd` | | `ssl add` | `Add_SSL_Menu` + `Add_SSL` | Let's Encrypt/ZeroSSL 证书申请与配置改写 | | `dnsssl|dns {ali|cf|dp|he|gd|aws…}` | `Add_Dns_SSL` | 泛域名,走各厂商 DNS API | | `onlyssl {…}` | `Add_Dns_SSL_Only` | 只签证书,不动 Nginx 配置 | `lnmp vhost add` 是其中最复杂的流程,按顺序收集:域名(查重)→ 附加域名 → 站点目录(默认 `/home/wwwroot/`)→ 伪静态(`conf/rewrite/` 里挑)→ pathinfo 开关 → 访问日志 → IPv6 → **多 PHP 版本选择** → 是否建库 → 是否建 FTP → 是否申请 SSL。 收集完之后才真正落盘:`mkdir` + `chmod 755` + `chown www:www` → `Add_VHost_Config` 用 heredoc 生成 `vhost/.conf` → 写 `.user.ini` 并 `chattr +i` → **`php-fpm reload`** → 最后才依次执行 `Add_Database` / `Add_Ftp` / `Add_SSL`(把建库建 FTP 这类耗时操作放在站点已可用之后),并在末尾回显账号密码等信息。 注意它只 `reload` php-fpm 而不 reload Nginx——因为 `Add_VHost_Config` 内部已完成配置检查与 Nginx 重载。 --- ## 16. tools 辅助脚本 这些是**独立脚本**,不参与主流程,可拷到任意位置运行: | 脚本 | 实现要点 | | --- | --- | | `backup.sh` | 打包站点 + `mysqldump`,按日期建目录 | | `cut_nginx_logs.sh` | `mv` 日志 + `kill -USR1` 让 Nginx 重开文件,配合 crontab | | `check502.sh` | 探 `php-fpm` 进程,挂掉就重启(配合 crontab) | | `reset_mysql_root_password.sh` | `--skip-grant-tables` 起库 → 改密码 → 恢复并重启 | | `remove_disable_function.sh` | 直接改写 `php.ini` 的 `disable_functions` + 重启 | | `remove_open_basedir_restriction.sh` | 解 `chattr` + 清 `open_basedir` | | `denyhosts.sh` / `fail2ban.sh` | SSH 防爆破工具安装 | --- ## 17. 安全与幂等护栏汇总 | 护栏 | 位置 | 行为 | | --- | --- | --- | | 非 root 退出 | 所有入口脚本 | `id -u != 0` → exit | | 发行版未知退出 | `install.sh` | `DISTRO=unknow` → exit | | 已安装拦截 | `install.sh` | `/bin/lnmp` 存在且为完整安装 → 提示先 `uninstall.sh` | | 配置完整性 | `Check_LNMPConf` | 缺 `lnmp.conf`/关键字段为空 → exit | | 目录防呆 | `Check_LNMPConf` | `MySQL_Data_Dir`/`MariaDB_Data_Dir`/`Default_Website_Dir` 不得为 `/` | | 版本兼容硬校验 | `Check_CMPT` | 新数据库/PHP 配老发行版 → exit;PHP 5.2 配新发行版 → exit | | 内存下限 | `Database_Selection` / `Install_Only_Database` | 源码编译 MySQL 8.x / MariaDB 10.3+ 且内存 ≤1G → exit | | 下载校验 | `Verify_Download_File` | SHA256 不符则删文件重下 | | 失败即撤牌 | `Print_Failed_Info` | 校验失败删除 `/bin/lnmp` | | 数据保护 | `uninstall.sh` | 数据库目录 `mv` 到 `/root/databases_backup_*` | | 无人值守 | `Press_Install` | `LNMP_Auto` 非空跳过确认 | **幂等性**主要靠「文件存在即跳过」:`Download_Files` 复用已有源码包、`Install_Nginx_Openssl` 解压前清目录、`Select_PHP`/`Multiple_PHP_Select` 用产物文件探测已装版本。但**安装本身不是幂等的**:完整安装有 `/bin/lnmp` 拦截,单项安装则是覆盖式重装。 --- ## 18. 改造指引:新增一个 PHP 版本要动哪些地方 这是最能体现体系耦合度的地方,按依赖顺序列: 1. `include/version.sh`:给 `PHPSelect` 加一个编号分支,赋 `Php_Ver='php-8.6.x'`。 2. `include/main.sh` 的 `PHP_Selection()`:菜单文案数组 `PHP_Info[n]` 与新编号对齐(编号是数组下标 + 1,容易错位)。 3. `install.sh` 的 `Install_PHP()`:加一条 `elif [ "${PHPSelect}" = "17" ]; then Install_PHP_86`。 4. `include/php.sh`:复制 `Install_PHP_85` 改版本号,并检查 `PHP_with_*` 里的版本正则是否要包含新版本。 5. `conf/enable-php8.6.conf` + `init.d/init.d.php-fpm8.6`:新增模板。 6. `include/multiplephp.sh`:多版本安装分支。 7. `include/upgrade_mphp.sh` / `include/upgrade_php.sh`:升级分支。 8. `conf/lnmp` 的 `Multiple_PHP_Select()`、`addons.sh` 的 `Select_PHP()`:两处硬编码的已装版本探测列表。 9. `include/end.sh`:`Check_PHP_Files`、`Check_Apache_Files` 的版本正则(Apache 模块名按 PHP 大版本区分)。 10. `include/init.sh` 的 `Check_Download()`:新增 tarball 下载项。 同理,新增数据库版本要改 `version.sh`、`Database_Selection`、`install.sh` 的 `Init_Install`/`Install_Database`、`mysql.sh`/`mariadb.sh`、`end.sh` 的 `Check_DB_Files`/`Clean_DB_Src_Dir`,以及 `only.sh`、`uninstall.sh`、`upgrade_*.sh` 里的每一条 `DBSelect` 正则。 **改造口诀:编号 → 版本变量 → 下载 → 安装函数 → 校验 → 清理**,六处必须同时改。 --- ## 19. 已知边界与注意事项 1. **没有「只装 PHP」的入口**。`install.sh` 只有 `nginx`/`db`/`mphp` 三个单项;PHP 只能随完整安装或作为附加版本(`mphp`)安装。 2. **单项安装不是「无损追加」**:`nginx` 分支会卸载系统 Apache 并覆盖 `/usr/local/nginx`;`db` 分支会卸载系统 MySQL/MariaDB 并删库。 3. **没有 `set -e`**:中间步骤失败不会立刻停,排查必须看 `/root/*.log`。 4. **`version.sh` 的加载时机**是隐式契约:`Press_Install` 之前 `*_Ver` 全是空值。 5. **版本号硬编码**:`install.sh:18`、`uninstall.sh:13` 里 `LNMP_Ver='2.2'`,与目录名 `lnmp2.3` 不一致(跟随上游发版节奏,仅用于显示)。 6. **`cur_dir=$(pwd)`**:必须在安装包目录内执行,否则相对路径全错。 7. **`Download_Mirror` 只覆盖一部分组件**:MySQL 预编译包、MariaDB、redis、ionCube 等直连官方域名,改镜像无效,详见 6.5。 8. **`Download_Mirror` 的默认值与 `Check_Mirror` 逻辑绑定**:只有设成旧的 `soft.vpser.net` 才会触发镜像探测;填其它值(含写错)不做任何连通性预检,直接使用。 9. **镜像目录名是 `datebase`(拼写错误但不可改)**:自建镜像必须沿用,否则 MySQL 源码与 phpMyAdmin 会 404。 10. **SHA256 校验默认静默跳过**:`Enable_Download_Checksum='y'` 需配合 `src/checksums.sha256`,本发行包不带该文件。 11. **`Bin` 的默认值取决于 `CheckMirror`**:`CheckMirror != n`(无镜像检测需求时)默认用预编译包,否则默认源码编译。这也决定了数据库下载走 CDN 还是镜像。 12. **升级与安装用两套下载源**:`upgrade_nginx.sh` 绕过 `Download_Files` 直连 `nginx.org`。 13. **php-fpm 用 unix socket**(`/tmp/php-cgi.sock`),不是 TCP 9000;排查 502 时先看这个路径与 `.user.ini` 的 `open_basedir`。 14. **`.user.ini` 被 `chattr +i` 锁死**,手工改 `open_basedir` 前必须先解锁。 --- ## 20. 一图总览 ```mermaid flowchart LR subgraph S1["入口脚本"] I["install.sh"] U["uninstall.sh"] G["upgrade.sh"] A["addons.sh"] P["pureftpd.sh"] end subgraph S2["公共层"] M["main.sh
选单 / 系统 / 下载 / 启动"] V["version.sh
编号 → 版本"] C["lnmp.conf
用户配置"] N["init.sh
换源 / 依赖 / 基础库 / swap"] E["end.sh
防火墙 / 启动 / 校验"] end subgraph S3["组件层"] NG["nginx.sh"] PH["php.sh"] MY["mysql.sh"] MA["mariadb.sh"] AP["apache.sh"] MP["multiplephp.sh"] ON["only.sh"] end subgraph S4["产物"] R1["usr/local/nginx"] R2["usr/local/php 及多版本目录"] R3["usr/local/mysql 或 mariadb"] R4["bin/lnmp"] R5["etc/init.d 与 systemd unit"] end I --> M --> V I --> N I --> NG & PH & MY & MA & AP & MP & ON G --> NG & PH & MY & MA A --> PH P --> R5 U --> R4 I --> E --> R4 & R5 NG --> R1 PH --> R2 MY --> R3 MA --> R3 ``` 一句话串联:**入口脚本定 Stack → 选单定编号 → `version.sh` 换算出下载目标 → `init.sh` 把机器准备好 → 组件脚本编译安装到 `/usr/local` → `end.sh` 注册服务并核对产物 → `/bin/lnmp` 承接后续运维**。