SerenityOS 端口系统完全指南:package.sh、构建流水线与移植实战
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
本篇技术指南以 SerenityOS 仓库 Ports/README.md 为骨架,结合 .port_include.sh 等核心实现源码,系统讲解 SerenityOS 的软件移植(Porting)体系:如何安装与批量构建第三方软件、package.sh脚本的每个子命令与执行流水线,以及如何为某个软件编写自己的端口脚本并贡献回上游。读完本文,你将掌握在 SerenityOS 构建环境中安装、卸载、调试第三方软件,以及将任意开源软件移植到 SerenityOS 的完整方法论。
一、什么是 SerenityOS 的 Ports 系统
SerenityOS 是一个从零构建的类 Unix 操作系统,其用户态没有继承任何现成的二进制生态。为了让 Bash、Vim、DOOM、SDL2 等经典开源软件能够在这套全新系统上运行,Serenity 提供了ports(端口)机制:把第三方软件源码下载下来,打上针对 SerenityOS 的适配补丁,再用 Serenity 自己的交叉编译工具链编译安装,最终落盘到系统镜像的 root 文件系统里。
这套机制的全部入口都位于仓库的 Ports 目录:
- 每个子目录对应一个可移植软件(如 Ports/bash、Ports/ncurses);
- 每个子目录内必须有一个
package.shBash 脚本,声明软件名称、版本、依赖、下载地址与编译选项; - 可选的
patches/目录存放针对 SerenityOS 的补丁文件; - 目录根的 .port_include.sh 是整套脚本框架的"引擎",所有端口的魔法都发生在这里;
- AvailablePorts.md 以表格形式列出当前仓库中全部可用端口及其版本。
使用前提:执行端口脚本前,你必须已经成功构建过 SerenityOS 本体,并处于 Serenity 的构建环境中(交叉编译工具链、Build/<arch>/Root根文件系统均已就绪)。端口脚本安装的目标位置是Build/<架构>/Root(即根文件系统暂存目录),而不是宿主机的/usr。
二、可用端口清单与外部端口
2.1 官方端口清单
完整的端口目录维护在 AvailablePorts.md,每个条目包含端口目录名、软件全名、版本号与上游官网。以其中几项为例:
| 端口目录 | 软件名 | 版本 |
|---|---|---|
bash | GNU Bash | 5.3 |
ncurses | ncurses | 6.6 |
doom | DOOM | 613f870 |
gn | GN Meta Build System | 2023.07.12 |
git | Git | 2.55.0 |
该清单涵盖了从核心工具链(gcc、binutils、cmake、ninja)、编程语言运行时(python3、ruby、php、zig)、图形库(SDL2全家桶、cairo、pango)到经典游戏(doom、quake、nethack、openttd)的广泛生态。
2.2 外部端口:SERENITY_PORT_DIRS
官方端口以外的第三方移植(例如你自己维护的本地端口仓库)可能依赖来自其他位置的额外软件包。此时可以用环境变量SERENITY_PORT_DIRS指向一个本地端口目录(支持多个路径,用冒号分隔):
export SERENITY_PORT_DIRS="/path/to/port/dir/:/other/path/"从源码实现看,该变量在 .port_include.sh 的installdepends函数中被消费:当某个依赖在官方 Ports 目录中找不到时,脚本会按冒号拆分SERENITY_PORT_DIRS,逐个目录查找名为<依赖名>的子目录并执行其./package.sh --auto;全部找不到则报错Dependency $depend could not be found.并退出。这是"外部端口"机制的核心工作方式。
三、使用端口脚本:安装与批量构建
每个端口目录中的package.sh是唯一入口。它通过 shebang#!/usr/bin/env -S bash ../.port_include.sh被框架接管——脚本会先 source 端口自身的变量声明,再转入框架的分发逻辑。
3.1 基本用法
- 安装单个端口:进入该端口目录并直接运行
./package.sh; - 安装全部端口:在 Ports 根目录运行
./build_all.sh,若想先清理旧构建产物,把clean作为第一个参数传入; - 重装所有已安装端口:运行 build_installed.sh。当 SerenityOS 的 LibC 等基础库发生变更时,通常需要这样做来让所有已装软件跟随重编译;同样可传
clean先清理旧文件。
不带任何参数运行./package.sh等价于依次执行installdepends、fetch、patch、configure、build、install,这也是常规安装推荐的方式。对应源码中parse_arguments为空参数时调用do_all的逻辑(见 .port_include.sh)。
3.2 安装数据库 installed.db
已安装端口被记录在Build/<架构>/Root/usr/Ports/installed.db——一个纯文本数据库。以ncurses为例,其内容形如:
manual ncurses 6.6 auto readline 8.3 dependency ncurses readline- 手动安装的端口标记为
manual,作为依赖自动安装的标记为auto(对应--auto选项); - 带依赖的端口会额外写入一行
dependency <port> <dep...>。
这个文件可以随时删除,而且在清空构建目录(如执行./package.sh clean或清理Build/)时必须编辑或删除它,否则端口依赖可能不会按预期重新安装。从源码看,addtodb在写入前会先通过package_install_state检查该端口是否已记录,已存在则跳过;installdepends也会据此跳过已安装的依赖。
3.3 各子命令详解
package.sh支持以下子命令,均可直接作为第一个参数传入:
fetch
默认行为:下载、校验并解压端口在files变量中声明的所有文件。框架会区分简单下载(URL#SHA256)与 Git 仓库(git+URL#REVISION)两种格式分别处理;简单下载用curl --fail -L拉取后,用sha256sum比对校验和,不匹配会删除重试一次,仍失败则报错退出(见 .port_include.sh 的fetch_simple)。
patch
应用端口patches/*.patch目录下的全部补丁。补丁应用成功后,会在workdir中创建.xxx.patch_applied标记文件,确保同一补丁只应用一次;若workdir本身是 Git 仓库(如 Git 格式的 files),则改用git am应用,并在全部应用后打上patchedtag(patch_internal函数)。
configure
默认运行端口的configscript(通常即configure脚本)并传入configopts。注意:--host=${SERENITY_ARCH}-serenity总是会被附加(在有SERENITY_SOURCE_DIR的托管构建环境下),如果这不符合需求,需要覆盖configure函数。若useconfigure为false,则跳过该步骤并打印提示。
build
默认在workdir中执行make并附加makeopts(默认-j$(nproc),即并行编译)。执行前会先通过ensure_build做健全性检查——验证${DESTDIR}/usr/lib/libc.so是否存在,不存在则提示"Serenity 尚未构建/安装或安装位置异常"并退出。
install
默认执行make install并附加installopts,同时总是传递DESTDIR="${SERENITY_INSTALL_ROOT}"(即Build/${SERENITY_ARCH}/Root)。安装完成后框架还会依次执行install_main_launcher与install_main_icon:若端口声明了launcher_name、launcher_category、launcher_command,会在${DESTDIR}/res/apps/下生成.af应用配置(启动器配置 中 DOOM 端口即声明了launcher_category='&Games');若同时提供了icon_file,则用 ImageMagick 提取 16x16 与 32x32 图标并通过objcopy --add-section serenity_icon_s/serenity_icon_m写入可执行文件。
shell
先完成依赖安装、fetch、patch,然后带着构建环境进入workdir打开一个交互式 Bash shell,方便手动调试编译问题。
installdepends
按depends列表安装所有依赖端口(含通过SERENITY_PORT_DIRS查找外部端口),未安装的依赖以--auto方式安装。
clean
默认删除workdir中所有.out等构建产物(实际实现是清空PORT_BUILD_DIR)。
clean_dist
删除所有根据files列表下载下来的归档或 Git 裸仓库副本。
clean_all
clean与clean_dist的组合。
uninstall
若端口带plist文件(记录安装文件清单),则据此从 Serenity 构建目录中删除对应文件,并从installed.db移除manual记录。注意:只有标记为manual的端口才能卸载,且必须存在plist文件,否则报错(见 .port_include.sh 的uninstall函数)。
dev
开发模式,用于引导式地修改端口补丁:
- 进入一个以本地(另一个)Git 仓库为"干净基线"的 Git 工作副本中,该基线即打好补丁、可直接构建的版本;
- 这个"远程"仓库可以被 push、pull,与普通 Git 远程无异;
- 退出 dev shell 后,所有补丁会被自动更新(
git format-patch重新生成),并提示是否重新生成补丁 ReadMe(do_generate_patch_readme); - 该模式还支持通过半自动化的引导流程,帮助把旧补丁迁移到新版本;
- 额外支持
--no-depends选项,跳过依赖的拉取与构建; - 注意:dev 模式下对
workdir的一切改动都是临时的,退出时会被git clean还原(脚本注册了 EXIT trap)。
--auto
与不带参数等价(执行完整流水线),但会把端口标记为"自动安装",用于依赖安装场景。此外框架还支持showproperty(打印变量值)、generate_patch_readme、interactive等隐藏子命令。
批量脚本的额外参数:build_all.sh 除clean外还接受verbose(输出完整日志而非重定向到 /dev/null)与failfast(任一端口失败立即退出);该脚本会智能跳过已处理过的端口及其依赖,避免重复构建。
四、编写端口脚本:package.sh 的变量体系
package.sh是一个简单的 Bash 脚本,通过 shebang 被 .port_include.sh 框架 source 执行。补丁及其他文件是可选的。一个最小端口脚本长这样:
#!/usr/bin/env -S bash ../.port_include.sh port='foo' version='1.2.3' useconfigure='true' files=( "https://example.com/foo-${version}.tar.gz#9acd50f9a2af37e471f761c3fe7b8dea5617e51dac802fe6c177b74abf0abb5a" ) depends=( 'bar' 'baz' )框架会先加载该脚本(. "$@"),因此这些变量在框架函数中全部可见。以下变量具有特殊功能:
port
端口的"包名",通常与脚本所在目录同名,也是构建目录Build/<arch>/Ports/<port>与数据库记录的关键字。
version
端口版本号,会写入installed.db,并通常通过变量插值参与files中的文件名(如bash-${version}.tar.gz)。
useconfigure
布尔开关(默认false)。为true时,configure步骤会依次运行pre_configure、configure、post_configure;为false时直接跳过并打印"该端口不使用 configure 脚本"。参考 Ports/doom/package.sh——DOOM 端口未设置该变量,仅通过makeopts/installopts指定--directory=doomgeneric/ --file=Makefile.sdl直接 make。
configscript
configure步骤默认执行的脚本名,默认值为configure。框架在调用前会chmod +x确保可执行。
configopts
传给configscript的选项数组。如前所述,--host=${SERENITY_ARCH}-serenity总会附加。以 Ports/bash/package.sh 为例:
configopts=("--disable-nls" "--without-bash-malloc" "CFLAGS=-std=c17" "CFLAGS_FOR_BUILD=-std=c17")makeopts
传给make的选项,默认-j$(nproc)。DOOM 端口用它在默认build函数中切换 Makefile 与子目录:
makeopts=( '--directory=doomgeneric/' '--file=Makefile.sdl' )installopts
传给make install的选项,DESTDIR="${SERENITY_INSTALL_ROOT}"总会附加。
depends
其他 SerenityOS 端口的依赖数组,在installdepends步骤安装:
depends=( 'gettext' 'ncurses' )例如 Bash 端口声明依赖readline(Ports/bash/package.sh),DOOM 端口依赖SDL2与SDL2_mixer(Ports/doom/package.sh)。
files
端口所需外部文件数组,支持两种格式:
简单下载:URL#HASH,其中URL是使用curl下载的地址,HASH是用于校验的 SHA256 哈希:
files=( "https://example.com/foo-${version}.tar.xz#9acd50f9a2af37e471f761c3fe7b8dea5617e51dac802fe6c177b74abf0abb5a" )若文件是压缩 tar 归档(.tar.gz/.tar.bz2/.tar.xz/.tar.lz/.tar.zst/.tgz等)、gzip 文件或 zip 文件,会被自动解压;.exe/.htm及其他未知类型则直接复制进工作目录。
Git 仓库:git+URL#REVISION,REVISION可以是任何git fetch接受的修订标识(commit hash、tag、分支):
files=( 'git+https://gn.googlesource.com/gn#fae280eabe5d31accc53100137459ece19a7a295' )对应 Ports/gn/package.sh 中 GN 端口的真实写法。实现上,框架会先建立裸仓库作为备份(git init --bare+git fetch --tags),再用git worktree add检出到构建目录,并处理子模块(submodule update --init --recursive);若目标修订变化,会执行clean -ffdx、reset --hard并重新 checkout。
workdir
用于执行命令与清理的工作目录,通常是解压后源码目录名,默认值为$port-$version。DOOM 端口因上游 tar 包内目录名特殊,显式设置了workdir="doomgeneric-${version}"。
prefix
端口目录的位置,目前仅用于package.db文件,默认是$SERENITY_SOURCE_DIR/Ports。官方贡献的端口不应覆盖它。
patchlevel
传给patch的-p/--strip参数值(参见man patch),默认1。
icon_file
用于端口启动器图标的文件,假定同时包含 16x16 与 32x32 两个图层。Bash 端口中以#icon_file=FIXME注释形式预留。
use_fresh_config_sub
布尔开关(默认false)。为true时,会在打补丁阶段用上游最新的config.sub替换config_sub_paths指向的文件。SerenityOS 是全新的平台三元组,老版本config.sub不认识${arch}-pc-serenity,因此许多 autoconf 项目都需要这个开关。参考 Ports/bash/package.sh:Bash 同时开启了use_fresh_config_sub='true'与use_fresh_config_guess='true',并把路径指向support/config.sub与support/config.guess(Bash 源码内的非标准位置)。
config_sub_paths
autoconf 使用的config.sub文件路径(相对$workdir),默认(config.sub)。对应的还有config_guess_paths与use_fresh_config_guess。框架的get_new_config_sub会先检查当前config.sub是否已包含serenity,没有则从上游 config.git 下载新版替换(.port_include.sh)。
其他可选变量
launcher_name/launcher_category/launcher_command/launcher_workdir/launcher_run_in_terminal:用于生成桌面启动器(.af文件)。DOOM 端口示例:launcher_name='Doom'、launcher_category='&Games'、launcher_command='/usr/local/bin/doom'。若launcher_command含空格,框架会自动在/usr/local/libexec/下生成一个包装脚本。
五、覆盖函数与工具函数
安装过程的各个步骤被拆分为独立的 Bash 函数,端口脚本可以像这样覆盖默认实现:
build() { run mybuildtool --foo --bar }5.1 可覆盖的函数
函数名与前面介绍的子命令一一对应,含义不言自明:
pre_fetch/post_fetchpre_patchpre_configure/configure/post_configurebuildpre_install/install/post_installclean/clean_dist/clean_all
框架通过func_defined检查端口脚本是否定义了这些函数,未定义则回退到默认实现(.port_include.sh 中大量func_defined xxx || xxx() { ... }即是此机制)。这意味着端口作者可以只写自己需要定制的步骤。
真实案例:Ports/gn/package.sh 同时覆盖了configure(run python3 build/gen.py --platform='serenity' --allow-warnings)、build(run ninja -C out)与install(复制out/gn与out/gn_unittests到/usr/local/bin);Ports/ncurses/package.sh 覆盖了pre_configure(导出CPPFLAGS='-P')、install(检测宿主机tic >= 6.1以便生成 terminfo 数据,并把TIC_PATH传给 make)和post_install(为宽字符库与非宽字符库建立大量兼容符号链接)。
5.2 内置工具函数
以下工具函数不可覆盖,供端口脚本在自定义函数中调用:
run
记录命令日志后在端口的workdir中执行:echo "+ $@"后(cd "$workdir" && "$@")。
run_nocd
记录命令日志后在当前工作目录(即Ports/$port)执行。
run_replace_in_file
用 Perl 正则表达式替换文件内容:
run_replace_in_file "s/define FOO 1/undef FOO/" config.h真实案例见 Ports/bash/package.sh 的build():先用run_replace_in_file关闭 Bash 的GETCWD_BROKEN与CAN_REDEFINE_GETENV宏,再执行make。
5.3 默认流水线的源码级视角
do_all对应的完整流水线在框架中由多个do_*函数串联(.port_include.sh):
do_installdepends:打印Installing dependencies of $port...并安装依赖;do_fetch:buildstep fetch——每个步骤的输出都会被sed加上[port/步骤名]前缀并着色,失败时以红色高亮Error in step port/步骤名 (status=N),同时保留退出码让批量脚本判断成败;do_patch:pre_patch+patch_internal;do_configure:先ensure_build检查 libc.so,再按需刷新config.sub/config.guess,最后pre_configure+configure+post_configure;do_build:ensure_build+build;do_install:pre_install+install+install_main_launcher+install_main_icon+post_install,最后addtodb写入installed.db。
这个分层设计保证了"单端口安装"与"批量构建"(build_all.sh 逐个目录执行./package.sh)共享同一套逻辑。
六、补丁机制与补丁 ReadMe
适配 SerenityOS 的核心手段是补丁。以 Ports/ncurses/patches 为例:
0001-Teach-configure-about-serenity.patch:教会 autoconf 的 configure 脚本识别 serenity 平台;0002-Disable-mixed-case-directory-names-when-building-on-.patch:在 macOS 上构建时禁用大小写混合的 terminfo 目录名(因为 macOS 文件系统大小写不敏感,tic只会生成十六进制目录名,而 configure 默认仍开启混合大小写特性,导致查找x/xterm这类条目失败)。
每个端口目录下的 patches/ReadMe.md 汇总了补丁的主题与背景说明——它由generate_patch_readme子命令自动生成(从补丁的 git 提交信息中提取 Subject 与正文,跳过Co-Authored-By行),也可以手动更新。
补丁应用规则(来自patch_internal):
- 只在
workdir尚未打过某补丁时应用(通过.补丁名_applied标记文件判断); - 普通 tar 源码用
patch -p$patchlevel应用;Git 检出源则用git am --keep-cr --keep-non-patch; - 应用完成后为 Git 工作树打上
patchedtag,作为 dev 模式比较补丁变化的基线。
七、如何贡献新端口
为 SerenityOS 贡献端口有两种途径:
- 新增端口:把软件成功编译通过,补齐必要的补丁与
package.sh脚本(参考上文变量与函数体系,尽量复用框架默认实现); - 更新既有端口:升级版本、让之前不可用的功能可用等,同时务必同步更新对应的补丁。
一个实用的工作流是使用dev模式:进入被 Git 管理的端口工作副本,直接修改源码、跑./package.sh build验证,退出后框架会用git format-patch自动把改动导出为patches/*.patch并提示生成 ReadMe。对于大版本升级这种补丁大面积失效的场景,dev模式的半自动引导流程也能显著降低迁移成本。若依赖来自第三方,记得把本地端口目录通过SERENITY_PORT_DIRS暴露给框架。
八、小结:从使用到移植的完整路径
SerenityOS 的端口系统是一套"约定优于配置"的 Bash 框架:package.sh声明元数据,.port_include.sh 提供流水线与默认行为,installed.db跟踪安装状态,patches/承载平台适配。日常使用只需三步——构建好 SerenityOS、cd进端口目录、运行./package.sh;想要贡献新软件,则按 AvailablePorts.md 的清单约定补充脚本与补丁即可。深入阅读 .port_include.sh 源码,还能发现诸如 ccache 加速、SERENITY_STRIPPED_ENV环境剥离、showproperty调试等更多细节,是理解整套构建体系的最佳入口。
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考