diff --git a/.github/workflows/build-pages.yml b/.github/workflows/build-pages.yml index c07f5f14..4021b875 100644 --- a/.github/workflows/build-pages.yml +++ b/.github/workflows/build-pages.yml @@ -69,6 +69,9 @@ jobs: git -C third_party/musl rev-parse --verify HEAD git submodule status --recursive + - name: Check both documentation manuals + run: python3 tools/test_documentation.py + - name: Import APK signing key id: signing_key env: diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml new file mode 100644 index 00000000..2a55f004 --- /dev/null +++ b/.github/workflows/check-docs.yml @@ -0,0 +1,53 @@ +name: Check documentation + +on: + push: + paths: &documentation_paths + - 'Documentation/**' + - 'kernel/reliefnt' + - 'Makefile' + - 'configs/build-version' + - 'scripts/build-docs.sh' + - 'scripts/reliefos/Makefile.site' + - 'tools/build/docs-page.sh' + - 'tools/test_documentation.py' + - '.github/workflows/check-docs.yml' + - '.github/workflows/build-pages.yml' + pull_request: + paths: *documentation_paths + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: reliefos-docs-${{ github.ref }} + cancel-in-progress: true + +jobs: + documentation: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + sphinx: ['5.3.*', '7.2.6', '9.*'] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Checkout kernel documentation + run: git submodule update --init kernel/reliefnt + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install documentation dependencies + run: python -m pip install -r Documentation/requirements.txt 'Sphinx==${{ matrix.sphinx }}' + + - name: Check both manuals and the documentation portal + run: python tools/test_documentation.py diff --git a/.gitmodules b/.gitmodules index b4c58537..b26a2f35 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,30 +1,15 @@ [submodule "third_party/litehtml"] path = third_party/litehtml url = https://github.com/litehtml/litehtml.git -[submodule "third_party/mbedtls"] - path = third_party/mbedtls - url = https://github.com/Mbed-TLS/mbedtls.git [submodule "third_party/busybox"] path = third_party/busybox url = https://github.com/mirror/busybox.git [submodule "third_party/pl_editor"] path = third_party/pl_editor url = https://github.com/Leonmmcoset/pl_editor.git -[submodule "third_party/zlib"] - path = third_party/zlib - url = https://github.com/madler/zlib.git -[submodule "third_party/libpng"] - path = third_party/libpng - url = https://github.com/pnggroup/libpng.git [submodule "third_party/stardustui"] path = third_party/stardustui url = https://github.com/xingji-studio/StardustUI.git -[submodule "third_party/sqlite"] - path = third_party/sqlite - url = https://github.com/sqlite/sqlite.git -[submodule "third_party/sl"] - path = third_party/sl - url = https://github.com/mtoyoda/sl.git [submodule "third_party/portablegl"] path = third_party/portablegl url = https://github.com/rswinkle/PortableGL.git @@ -34,9 +19,6 @@ [submodule "third_party/mimalloc"] path = third_party/mimalloc url = https://github.com/microsoft/mimalloc.git -[submodule "third_party/ncurses"] - path = third_party/ncurses - url = https://github.com/ThomasDickey/ncurses-snapshots.git [submodule "kernel/ntclks"] path = kernel/reliefnt url = https://github.com/ReliefOSProject/ReliefNT.git diff --git a/Documentation/admin-guide/installation.rst b/Documentation/admin-guide/installation.rst index b5196ca7..c30a2fe5 100644 --- a/Documentation/admin-guide/installation.rst +++ b/Documentation/admin-guide/installation.rst @@ -22,6 +22,9 @@ 引导方式、分区和账户,最后确认安装。TTY 同样在分区之前选择引导方式。 Xorg 桌面默认安装,也可选择纯控制台系统。 +图形向导在语言选择后显示 Apache License 2.0 英文原文,可滚动阅读全文。 +该文本直接取自仓库的 ``LICENSE``,中文界面也保留许可证原文。 + 新安装默认同时安装 BIOS、UEFI 引导器,可分别取消。两者都取消时不写入 可直接启动的 loader,必须另行配置引导。自动 GPT 布局根据所选引导方式生成: @@ -61,19 +64,101 @@ AHCI SATA 设备上的专用文件或未挂载分区,再使用 ``mkswap`` 初 才能执行。``swapoff`` 需要先恢复仍被引用的页面,内存不足时可能失败并保留 活动区域;不能把失败当成已卸载,也不保证所有后续分配立即成功。 +共享 ESP 与多系统 +================= + +与已有系统共存时选择手动分区,为 ReliefOS 指定单独的根分区。 +所选根分区会格式化;已有 FAT32 ESP 默认保留,不要求它达到自动方案的 +128 MiB,但必须有足够剩余空间。只有显式勾选格式化 ESP 才会清除其中的 +所有 EFI 文件。TTY 对应 ``format-esp yes`` / ``format-esp no``。 +自动分区仍是明确的整盘擦除方案,不是保留其他系统的并列安装。 + +新安装将物理 ESP 挂到 ``/efi``,并把 +``/efi/reliefos/instances/<根分区 PARTUUID>`` bind 到 ``/boot``。 +每个实例有独立的 GRUB 配置、根分区绑定、A/B 内核状态和版本文件; +内核包更新检查实例所有权,拒绝把当前根的内核发布到另一个实例。 +initramfs 在切换真实根之前按 fstab 完成两层挂载。 + +UEFI 文件位于 ``/EFI/ReliefOS/<根分区 PARTUUID>/grubx64.efi``; +标准厂商入口 ``/EFI/ReliefOS/grubx64.efi`` 指向一个已有安装,其菜单可选择其他实例。 +已有的其他系统 EFI 文件和未知的 ``/EFI/BOOT/BOOTX64.EFI`` 保持不变。 +只有空的 fallback 路径,或内容摘要和所有权记录均匹配的 ReliefOS fallback, +才会被安装或更新。固件启动变量和 BootOrder 不由此文件复制过程修改; +若固件没有显示新入口,使用完成页显示的 EFI 文件路径添加启动项。 + +GRUB 在启动时枚举其他 ReliefOS 实例;UEFI 菜单识别常见的 Windows、 +shim/GRUB 和 systemd-boot 文件,BIOS 菜单支持 Windows 分区引导及其他 +系统的 GRUB 配置。其他系统的配置只在用户选择相应条目后执行。 +BIOS 安装会替换所选整盘的主引导代码;只需 UEFI 共存时可取消 BIOS。 +旧预制磁盘若没有 ESP 上的 ``core.img`` 副本,更新器会校验 MBR 中的 +GRUB 核心位置及 BIOS Boot 分区内的块列表,识别实际嵌入的引导器。 +新预制镜像同时保存该副本,保持与安装器生成的布局一致。 + +Live 只有一个普通启动项。安装介质保留图形和控制台两个安装入口; +磁盘保留普通启动及高级选项中的日志暂停、关闭内核日志颜色和有效旧内核回退。 +不再展示失效的 debug/autospawn 参数、重复日志条目或同内核副本伪装的回退。 +ISO 优先从固件交接的实际介质加载菜单,Live 与 Installer 的后备标记相互独立。 + +独立磁盘镜像身份 +================ + +``IMAGE_INSTANCE_ID``(也可在 menuconfig 中设置 ``DISK_INSTANCE_ID``) +决定预制磁盘的 GPT 磁盘和分区身份,并同步生成 fstab、GRUB 根绑定和查找标记。 +同一输入、同一实例标识保持可重现;需要同时连接的镜像应使用不同标识,例如:: + + make O=out/desktop-a defconfig + make O=out/desktop-a IMAGE_INSTANCE_ID=desktop-a image-vmdk + make O=out/desktop-b defconfig + make O=out/desktop-b IMAGE_INSTANCE_ID=desktop-b image-vmdk + +逐字节复制一张已有磁盘仍会复制其 UUID,不能把这样的副本当作独立安装同时连接。 +正式安装到不同根分区时使用各自 GPT 身份,不依赖预制镜像的实例标识。 + + 更新已有系统 ============ 更新不自动重分区。旧两分区 UEFI 磁盘保持原有布局,缺少 BIOS Boot 空间时增加 BIOS 引导的请求应在写入前失败。先备份用户文件和引导分区, -检查已安装 APK 数据库、信任密钥及所选更新介质。 -磁盘探测完成后进入引导方式页,默认选中已检测到的引导器,再进入更新确认页; -返回引导方式页不会覆盖用户刚修改的选择。未选中的现有引导器会保留。 +检查已安装 APK 数据库、信任密钥及所选更新介质。正常关闭已安装系统后, +再从安装介质启动。 +在安装器中选择更新和目标磁盘后,独立的后台进程只读检查已安装系统、桌面 +类型、软件包数据库、安装介质和空间。确认页列出当前版本、介质版本、根分区、 +ESP 和空间需求,并默认选中已检测到的 BIOS/UEFI 引导器。核对后点击更新。 +同盘有多个安装时,可在目标磁盘页选择明确的根分区和 ESP;留空 ESP 时读取 +所选根的 fstab,不能简单取磁盘上第一个 ESP。显式选择与该根的 fstab 不一致时 +拒绝更新,避免把引导文件写到其他系统的分区。旧的 ``/boot`` 直接挂载 FAT32 +布局继续兼容,更新不强制改变现有挂载布局。 +旧布局中未能通过所有权验证的现有 EFI 可执行文件及相邻配置会保留, +不会因系统更新而覆盖。兼容命令 ``reliefos-grub-installer`` 现直接转交原生 +CLI,需要明确的 ``--disk``、``--boot-directory`` 和根分区身份; +不再接受无法绑定安装实例的旧式单个 ESP 路径复制操作。 +TTY 入口使用相同的后端,并在显示摘要和引导选项后通过 ``y/N`` 确认。 + +检查过程使用私有 APK 数据库和已安装系统的信任密钥,验证签名索引、完整 +软件包归档以及离线依赖。确认后的执行会再次核对磁盘、分区 UUID、索引和 +已安装包数据库及 world 选择;目标已经变化时需要重新检查。已被其他路径挂载的分区、 +无法唯一识别的根分区或 ESP,以及早于已安装系统的介质会在写入前被拒绝。 根文件系统使用 APK 更新,保留外部包、world 选择和受保护配置;新默认配置 可能以 ``.apk-new`` 出现。根事务成功后才发布引导文件。 +离线更新只从安装介质的签名仓库取得新软件包。``world`` 中已有的 +``@testing``、``@alpine`` 等标签在本次事务的私有配置中注册到该仓库, +使已安装的外部软件包可以继续满足依赖,不要求原仓库在线或保留索引缓存。 +原有仓库配置和标签选择保持不变;介质和已安装软件包无法满足的版本约束 +仍会在预检查时拒绝,不会强制忽略依赖。 更新保留现有 fstab 和 swap 设置,不会自动添加或更改交换文件。 +更新详情显示软件包和引导操作的实际输出,完整日志位于安装器运行环境的 +``/var/log/installer-update.log``。失败页面提供重新检查入口。 + +目标系统的 ``/var/lib/reliefos/installer-update/state`` 持久记录软件包、根同步、 +引导提交和完成阶段。同一介质的引导提交失败后,重新检查会核对已完成的根 +事务,再继续引导提交;软件包阶段失败则重新预检并执行包事务。旧内核模块 +保留与 A/B 内核发布继续使用共同的原生引导接口。更新期间由独立进程持有 +安装器更新锁,释放目标挂载后才报告完成。APK 已记录为损坏的系统包会先通过 +已验证的归档执行定向修复,再执行升级;已有的外部包错误保留并在日志中说明。 + 单文件原子替换不等于全部包、ESP 和 BIOS 写入的整体断电原子事务。 -旧回滚内核可能不认识 PARTUUID,不能假定所有旧菜单都能自动恢复。 +回退条目只使用通过完整性检查的匹配内核与 initramfs,不把当前内核的别名当作回退。 运行验证和未覆盖范围见 :doc:`limitations`。 diff --git a/Documentation/admin-guide/packages.rst b/Documentation/admin-guide/packages.rst index e861df36..61e6e5f6 100644 --- a/Documentation/admin-guide/packages.rst +++ b/Documentation/admin-guide/packages.rst @@ -23,6 +23,23 @@ 同名外部命令使用 GNU 入口,旧路径由 APK 管理并转发;详情与验收方法见 :doc:`coreutils`。 +基础依赖 +======== + +sl、ncurses、zlib、libpng、SQLite、mbedTLS 3 由签名 Alpine APK 提供。 +系统直接请求官方命令、终端工具/数据和 SQLite 命令/运行库,不再生成本地包装包。 +文件所有权可通过 +``apk info --who-owns /usr/bin/sl``、``/usr/bin/tput``、``/usr/lib/libsqlite3.so.0`` 查询。 +离线更新器在预检时将旧包装包的 world 请求转换为官方包,保留其他包和配置; +正式事务自动卸载旧包,完成后清除离线缓存中的旧包装包归档。 +新的自有包使用第 4 代版本前缀,保证从第 2、3 代包按正常版本顺序升级。 +旧的 ``sqlite.so.3`` ABI 已移除。 +依赖旧自定义 SQLite VFS 或厂商内部符号的外部程序需要重新编译。 + +RPR 本地发布树继续包含完整的签名索引、原始上游 APK 和自有包,构建 +``rpr-pages`` 不等于部署到线上。运行库因新增 ELF NEEDED 而得到真实 APK +依赖,不能只复制新的 ``libreliefos.so.2`` 而遗漏配套库。 + 内核与模块更新 ============== diff --git a/Documentation/applications/reliefplayer.rst b/Documentation/applications/reliefplayer.rst index 4ae89c23..1f270a95 100644 --- a/Documentation/applications/reliefplayer.rst +++ b/Documentation/applications/reliefplayer.rst @@ -2,7 +2,7 @@ ReliefPlayer ============ -ReliefPlayer 是独立的 FLTK 本地音频播放器,位于“网络和多媒体”应用分组, +ReliefPlayer 是独立的 FLTK 本地音视频播放器,位于“网络和多媒体”应用分组, 不属于 ReliefOffice。使用系统中文字体和 FLTK 默认 gtk+ 控件。 .. code-block:: sh @@ -10,12 +10,14 @@ ReliefPlayer 是独立的 FLTK 本地音频播放器,位于“网络和多媒 reliefplayer reliefplayer /home/test/music.mp3 reliefplayer /home/test/recording.wav + reliefplayer /home/test/movie.mp4 -点击“打开音频”选择文件,打开后自动播放。支持播放/暂停、停止、拖动进度、 +点击“打开媒体”选择文件,打开后自动播放。支持播放/暂停、停止、拖动进度、 前后跳转 10 秒、软件音量和静音。窗口显示文件名、格式、采样率、声道、位深、 -已播放时间和总时长。文件管理器中的 MP3、WAV 默认使用本程序打开。 +已播放时间和总时长。MP4 视频在窗口中保持比例显示,支持常见的直角旋转元数据。 +文件管理器中的 MP3、WAV、MP4 默认使用本程序打开。 -- Ctrl+O:打开音频。 +- Ctrl+O:打开媒体。 - 空格:播放或暂停。 - 左右方向键:前后跳转 10 秒。 - Home:回到开头。 @@ -31,15 +33,31 @@ MP3 使用仓库已有 minimp3,支持单/双声道 MPEG Layer III,包括可 首次打开扫描帧索引以确定总时长,定位时预解码恢复状态。时间按音频帧统计, 不裁切编码器延迟和尾部 padding。会跳过 ID3 和 APEv2 标签边界, 不读取封面或展示标签信息。 -APEv2 边界布局参考 `FFmpeg 的官方实现 `_, -不引入 FFmpeg 作为运行时依赖。 +APEv2 边界布局参考 `FFmpeg 的官方实现 `_。 WAV 支持 RIFF 中的单/双声道 PCM 8、16、24、32 位和 IEEE float32; 支持容器位深与有效位深相同的对应 WAVE_FORMAT_EXTENSIBLE 子格式。 采样率范围 8–192 kHz,不支持其他 WAV 压缩编码、RF64 或多声道源文件。 最多打开 2 GiB 的文件;MP3 索引最多 100 万帧。 +MP4 使用锁定的官方 FFmpeg 8.1.3 LGPL 精简共享库,支持 H.264、MPEG-4 Part 2、 +HEVC 视频及 AAC、MP3 音轨;音频最多 8 声道,转换为 48 kHz 立体声。 +无音轨的视频使用单调时钟播放,不需要打开音频设备。含音轨的视频按音频设备 +已经播放的帧同步画面,定位会刷新解码器并丢弃关键帧到目标位置之间的预解码数据。 +不支持的编码、损坏文件和解码失败会报告错误。 + +视频使用软件解码,源画面最多 4096 × 2160,显示快照最多 1280 × 720 像素 +(竖屏时可交换边长)。不含硬件加速、HDR 色调映射、字幕或多音轨选择; +高分辨率视频的流畅程度取决于虚拟机性能。 + +``make fetch`` 下载锁定源码,``upstream-ffmpeg`` 离线构建五个解码共享库。 +播放器启用时,运行库及 LGPL 许可证随 ``reliefos-media-codecs`` APK 一起进入镜像; +开发头文件留在独立构建目录。库名及 SONAME 带有 ``-reliefplayer`` 后缀, +可以与 Alpine 的完整 FFmpeg/VLC 依赖共存。FFmpeg 不构建命令行程序、网络协议或外部编码器。 + 解码、文件读取和音频写入由独立子进程执行,GUI 通过非阻塞管道控制。 +视频通过有界共享内存发布 RGB 快照,GUI 保存自己的画面副本;双方只尝试获取 +复制锁,不等待对方,也不在锁内读文件或解码。 采样率和设备不同时使用线性插值;设备输出为 S16_LE,使用前置立体声, 额外硬件声道置零。非阻塞短写、设备回压和排空均保留未完成数据; 播放失败不会把进度伪装为完成。设备连续 5 秒既无写入也无播放进展时报告错误。 diff --git a/Documentation/kbuild/building.rst b/Documentation/kbuild/building.rst index b9b6b546..db1f83a6 100644 --- a/Documentation/kbuild/building.rst +++ b/Documentation/kbuild/building.rst @@ -17,6 +17,12 @@ Clang 必须包含 x86_64 compiler-rt builtins。典型 Debian/Ubuntu 依赖为: HTML 文档另需 Sphinx,见 :doc:`../process/documentation`。Sphinx 仅服务文档; 内核和用户态生产编译不因此改用 Python、Meson 或 Ninja。 +``make test-build`` 中的播放器宿主检查另需 ``ffmpeg``、``libavformat-dev``、 +``libavcodec-dev``、``libavutil-dev``、``libswresample-dev`` 和 ``libswscale-dev``。 +这些是宿主检查依赖;生产播放器使用锁定源码构建的 musl 库。 +可通过 ``PLAYER_TARGET_BUILD=/绝对路径/out/x86_64/release sh tests/build/test-reliefplayer-mp4.sh`` +在 Linux 宿主上同时运行实际精简库和 musl 版本的解码/控制检查;这不等于来宾播放验证。 + 构建顺序 ======== diff --git a/Documentation/kbuild/sdk.rst b/Documentation/kbuild/sdk.rst index 69e176a8..83d78b47 100644 --- a/Documentation/kbuild/sdk.rst +++ b/Documentation/kbuild/sdk.rst @@ -7,7 +7,7 @@ SDK 与用户态移植 SDK 为可重定位 musl sysroot,C 驱动 ``reliefos-musl-cc`` 按自身位置找头与库。 验证时必须真正编译、链接和运行适当探针,覆盖带空格的重定位路径、动态与 -``STATIC=1``;成功列出归档成员不能替代消费验证。 +``-static``;成功列出归档成员不能替代消费验证。 公开头通过白名单导出,不复制内核私有头。 库与 ABI @@ -17,3 +17,9 @@ SDK 为可重定位 musl sysroot,C 驱动 ``reliefos-musl-cc`` 按自身位置 装载依赖,不用宿主 glibc 库冒充目标产物。旧库与旧转发头的兼容范围见 :doc:`../userspace-api/abi`。新接口需同步公开头、运行库、内核实现、消费者、 头导出及包归属,不能只改声明。 + +SDK 同时携带锁定版本的 zlib、libpng、SQLite、mbedTLS 3 头文件、动态库和静态库。 +驱动为扩展库补全 PNG、zlib、mbedTLS 的传递链接依赖;直接使用 SQLite 的程序 +仍需传入 ``-lsqlite3``。开发包的来源元数据位于 ``share/reliefos/sdk/apk``, +许可位于 ``share/licenses``。所有 mbedTLS 消费者必须使用随包配置头,不能使用 +旧私有 ``MBEDTLS_CONFIG_FILE`` 配置改变结构布局。 diff --git a/Documentation/kbuild/third-party.rst b/Documentation/kbuild/third-party.rst index aaa3aa4b..698dcefc 100644 --- a/Documentation/kbuild/third-party.rst +++ b/Documentation/kbuild/third-party.rst @@ -18,10 +18,22 @@ Linux 6.12 Kbuild/Kconfig 的来源与逐文件摘要见 ``scripts/kbuild-source 当前 GRUB 2.16 的 EFI 路径使用 GOP;上游已移除仅用于旧固件的 UGA 支持, 镜像模块清单不再请求 ``efi_uga``。Legacy BIOS 的引导路径保持独立。 -musl、BusyBox、SQLite、FLTK、Xorg/Mesa、GRUB、认证库等保留各自原生构建与 +musl、BusyBox、GRUB、认证库等保留各自原生构建与 移植边界。目标程序与开发 SDK 的 staging 不可混用。上游 README、许可证、 COPYING、归属和字体许可是来源资料,保留原文,不属于待清理的过程报告。 +musl 的互斥锁属性探测补丁将内核缺少 PI futex 的 ``ENOSYS`` 转换为 +POSIX 的 ``ENOTSUP``,以便 PulseAudio 等调用者回退到普通互斥锁。 +该补丁不伪装成已实现优先级继承,也不改变其他 futex 错误。补丁及摘要由 +依赖锁管理;已有镜像需要重新构建 musl 和系统包才会取得此修正。 + +ReliefPlayer 的 MP4 解码使用官方 FFmpeg 8.1.3 源码,以 LGPL 配置构建 +``avformat``、``avcodec``、``avutil``、``swresample`` 和 ``swscale``。 +源码 tag、commit、归档摘要和许可证由依赖锁固定;构建使用独立的 ``-reliefplayer`` +库名和 SONAME,避免与用户安装的完整 FFmpeg 运行库发生文件及提供者冲突。 +只将选中的播放器所需运行库与 LGPL 原文装入 ``reliefos-media-codecs`` 包, +不向镜像发布 FFmpeg 命令行程序或开发头文件。 + 移植与打包 ========== @@ -29,3 +41,34 @@ COPYING、归属和字体许可是来源资料,保留原文,不属于待清 动态库依赖、桌面入口及许可证。不要仅为安装成功关闭上游验证。 标准控制台工具没有必要伪装成自研图形应用或分配桌面图标。 上游源码修订先在其仓库提交并推送,再更新父仓引用。 + +预编译基础依赖 +============== + +sl、ncurses、zlib、libpng、SQLite 和 mbedTLS 3 使用 Alpine v3.24 x86_64 的 +原始签名 APK;不再初始化对应的六个源码子模块。准确版本与 SHA256 均在依赖锁中。 +运行时 APK 经过 ``upstream-apk`` 校验;``runtime-development`` 仅从锁定且通过 +官方签名校验的运行、dev 和 static 包暂存匹配的头文件与库,不将开发树覆盖到来宾。 +生产构建离线消费缓存,缺包时先执行 ``make fetch``。 + +``libreliefos.so.2`` 和 ``libleonos.so.2`` 通过 ELF NEEDED 引用独立的 PNG、zlib、 +mbedTLS 库。所有使用方采用 ``mbedtls3-dev`` 的配置头;不能继续定义旧的 ALT 配置, +也不能混用 main 仓库的 mbedTLS 4.x。时钟、熵和线程接口由官方库经 musl/内核获取, +证书链、域名、有效期验证继续生效。 + +ReliefMail 链接标准 ``libsqlite3.so.0``,不再单独编译 amalgamation 或旧的 +``sqlite.so.3`` 自定义 VFS。数据库继续使用 DELETE journal、FULL synchronous、 +foreign_keys,并显式设置 ``mmap_size=0``。扩展库的厂商私有符号不是稳定 ABI, +需要使用它们的外部应用应针对独立库及同版本头文件重新构建。 + +``component:sl``、``component:ncurses``、``component:sqlite`` 锁特性由镜像的 +``etc/reliefos/apk-components`` 选择,保留 BUILD/IMAGE 的组件约束;运行库的 +基础依赖在控制台和桌面配置中均可用。命令和库由官方包直接提供,不生成本地 +包装包、注册表包装入口或迁移说明载荷。离线更新器迁移旧 world 请求并卸载旧包, +保留独立的许可证文本。许可证在 ``resources/licenses``, +SDK 另带原始 ``.PKGINFO`` 记录;源码和构建配方可从对应 Alpine aports 获取。 + +目标 ABI 使用实际 SDK 在 Linux/QEMU 验证。带 ASan 的 glibc 宿主测试不能链接 musl +静态归档,因此 ``host-runtime-development`` 从锁定的同版本 SQLite/mbedTLS 发布归档 +构建原生测试库(使用官方配置);该目标仅属于测试,不参与系统、SDK 或发行包构建。 +可通过 ``RELIEFOS_TEST_DEP_ROOT`` 指向其他 O 的 ``host/runtime-development``。 diff --git a/Documentation/process/documentation.rst b/Documentation/process/documentation.rst index e482ee0e..02553e50 100644 --- a/Documentation/process/documentation.rst +++ b/Documentation/process/documentation.rst @@ -9,6 +9,7 @@ 按读者任务组织 admin-guide、applications、kbuild、userspace-api、process, 每个目录提供 index.rst 和显式 toctree。保持段落可读,一页解决一个主题。 标题使用等号上下线,章用等号、节用横线、子节用波浪线;代码用字面块或 code-block。 +标题装饰线按显示宽度计算,汉字通常占两列;混排标题的上下线均须覆盖整行标题。 交叉引用使用 doc/ref 角色,避免绝对宿主路径、陈旧构建编号和失效截图链接。 先写目的、前提与可操作步骤,再写接口约束、错误和限制。不把计划、聊天交接、 @@ -34,3 +35,17 @@ ReliefNT 的内核手册只在 ReliefNT 仓库维护;主仓手册记录用户 构建启用 nitpicky 和 warnings-as-errors;缺失交叉引用、未收入目录、错误语法 均须修复,不能通过关闭警告或排除现行页面让检查变绿。 文档生成所需 Python/Sphinx 仅属于文档工具,不进入目标系统编译链。 + +完整文档检查无需编译内核或应用,但须初始化所选 ReliefNT 子模块:: + + git submodule update --init kernel/reliefnt + python3 tools/test_documentation.py + +此检查通过两仓的 ``make htmldocs`` 入口和实际 Pages 文档入口构建全部页面, +核对每个 RST 页面、站内链接、HTML 锚点和资源,拒绝依赖网站根路径的链接、 +JavaScript 及源文档泄漏。只检查现有文档入口可传 ``--site 路径``。 +外部网站链接不在此离线检查范围内。 + +``Check documentation`` 工作流在推送和 PR 时独立检查文档,覆盖 Sphinx 5.3、 +Pages 当前使用的 7.2.6 和 9.x。Pages 发布工作流也会先运行文档检查,再构建 +系统镜像;两处均保留严格警告检查,不以关闭警告绕过失败。 diff --git a/Documentation/userspace-api/abi.rst b/Documentation/userspace-api/abi.rst index c293b578..6a72660b 100644 --- a/Documentation/userspace-api/abi.rst +++ b/Documentation/userspace-api/abi.rst @@ -62,3 +62,24 @@ X11 应用通过 Xorg 通信。未知或旧兼容路径不能当作新的公开 历史 ABI 快照保持不变;集成验证要求已移除的图形接口不存在,并继续检查 其他接口的布局、符号类型和 SONAME。进程/时钟与 fbdev 辅助声明分别位于 ```` 和 ````,应用必须重新构建。 + +独立安装实例的引导接口 +====================== + +```` 保持原有 ``reliefos_boot_install_options`` 布局及旧接口, +新增 ``reliefos_boot_prepare_instance`` 创建带所有权标记的根 PARTUUID 命名目录, +``reliefos_boot_validate_instance`` 只读核对该目录和物理 ESP 的实际 inode, +``reliefos_boot_install_instance_at_root`` 发布实例内核、菜单及独立 EFI 文件。 +新接口显式传入物理 ESP;``boot_dir`` 可为实例目录的 bind 挂载点。 +目录遍历拒绝 symlink,未知目录内容不能被认作本安装器所有。 +低层内核包发布还会核对实例标记中的 PARTUUID 与实际根设备,防止跨实例更新。 +原有直接挂载 ESP 到 ``/boot`` 的系统继续使用旧接口,不改变其现有文件布局。 + +第三方库拆分 +============ + +zlib、libpng、mbedTLS 3 已从扩展库的对象集合移到各自官方共享库;公开的 ReliefOS +接口与旧 leonos 别名继续保留,链接器应显式链接使用的第三方库。旧扩展库偶然 +导出的厂商内部符号、旧私有 mbedTLS 配置及 ``sqlite.so.3`` VFS 不属于稳定兼容 +接口。使用这些实现细节的程序须用 SDK 里的匹配头文件重编译;SQLite 使用标准 +``libsqlite3.so.0`` POSIX VFS。历史 ABI 快照保持原样,兼容检查继续覆盖第一方接口。 diff --git a/Kbuild b/Kbuild index 044010bf..a978f71e 100644 --- a/Kbuild +++ b/Kbuild @@ -10,6 +10,7 @@ include $(srctree)/scripts/reliefos/Makefile.headers include $(srctree)/scripts/reliefos/Makefile.boot include $(srctree)/scripts/reliefos/Makefile.third-party include $(srctree)/scripts/reliefos/Makefile.pam +include $(srctree)/scripts/reliefos/Makefile.apk-dependencies include $(srctree)/scripts/reliefos/Makefile.runtime include $(srctree)/scripts/reliefos/Makefile.upstream include $(srctree)/scripts/reliefos/Makefile.modules diff --git a/Kconfig b/Kconfig index 0bdb4910..ae149c07 100644 --- a/Kconfig +++ b/Kconfig @@ -20,6 +20,14 @@ config IMAGE_SIZE_MIB Minimum size requested by the BIOS/UEFI disk-image builder. The builder may increase it to fit the root filesystem and partition overhead. +config DISK_INSTANCE_ID + string "Standalone disk image instance identity" + default "reliefos" + help + Use a distinct identity for disk images that will be connected together. + It deterministically derives GPT disk and partition UUIDs. An exact + byte-for-byte clone retains its identities, just as on other systems. + config INSTALLER_ROOT_SIZE_MIB int "Minimum installer runtime root size in MiB" range 32 4096 diff --git a/NOTICE b/NOTICE index 2dd2bdbb..639f8a3b 100644 --- a/NOTICE +++ b/NOTICE @@ -22,14 +22,14 @@ Runtime, libraries, and applications * GNU GRUB 2: GPL-3.0-or-later. See the GRUB source and boot artifacts. * Mbed TLS 3.6.7: Apache-2.0 OR GPL-2.0-or-later. See - third_party/mbedtls/LICENSE. + resources/licenses/mbedtls3-LICENSE. * musl 1.2.6: MIT and upstream notices; see third_party/musl/COPYRIGHT. * mimalloc 3.5.1: MIT; see third_party/mimalloc/LICENSE. -* zlib 1.3.2: zlib License. See third_party/zlib/LICENSE. +* zlib 1.4: zlib License. See resources/licenses/zlib-LICENSE. Copyright (C) 1995-2026 Jean-loup Gailly and Mark Adler. -* libpng 1.6.58: libpng License. See third_party/libpng/LICENSE. -* SQLite 3.46.1: public-domain dedication and blessing. See - third_party/sqlite/LICENSE.md. +* libpng 1.6.59: libpng License. See resources/licenses/libpng-LICENSE. +* SQLite 3.53.4: public-domain dedication and blessing. See + resources/licenses/sqlite-LICENSE. * BusyBox 1.36.1: GPL-2.0-only. See third_party/busybox/LICENSE. * GNU findutils 4.10.0: GPL-3.0-or-later. The unmodified official Alpine APK and its pinned checksum are recorded in configs/openrc-packages.json. @@ -54,7 +54,7 @@ Runtime, libraries, and applications isolated staging root's /usr/share/licenses// directory. * Vim (vim, vim-common, xxd): Vim License. Installed from signed Alpine APKs; versions and archive hashes are recorded in configs/dependencies.lock.json. -* ncurses 6.6: MIT-style license. See third_party/ncurses/COPYING. +* ncurses 6.6: MIT-style license. See resources/licenses/ncurses-LICENSE. * Dyne musl 2.2.0 native toolchain: GCC 15.1.0 and GNU binutils 2.44 (GPL-3.0-or-later), musl (MIT), and the upstream bundled static libraries. GCC runtime libraries use the GCC Runtime Library Exception where applicable. @@ -68,7 +68,7 @@ Runtime, libraries, and applications license metadata is in .PKGINFO. * StardustUI: MIT. See third_party/stardustui/LICENSE. * PL Editor (LeonOS fork): MIT. See third_party/pl_editor/LICENSE. -* sl: permissive upstream license. See third_party/sl/LICENSE. +* sl: permissive upstream license. See resources/licenses/sl-LICENSE. * litehtml: BSD-3-Clause. See third_party/litehtml/LICENSE. * Gumbo HTML Parser: Apache-2.0. See third_party/litehtml/src/gumbo/LICENSE. * ab_glyph_rasterizer 0.1.10: Apache-2.0. See the nested StardustUI @@ -114,3 +114,9 @@ covered by their own GPL-2.0 terms, with texts in LICENSES/preferred/GPL-2.0 and LICENSES/kbuild/COPYING. Project compiler-policy adapters are separate files. These build-tool terms do not relabel the ReliefOS-owned application or operating-system source. + +Binary dependencies sl, ncurses, zlib, libpng, SQLite and mbedtls3 are distributed +as unmodified signed Alpine v3.24 APKs. Exact versions, archive URLs, SHA256 and +license metadata are recorded in configs/dependencies.lock.json. Source recipes +are available at https://gitlab.alpinelinux.org/alpine/aports (v3.24). SDK archives +include matching development/static inputs and their original .PKGINFO records. diff --git a/boot/grub/bios-modules.list b/boot/grub/bios-modules.list index d449f24b..97b9956a 100644 --- a/boot/grub/bios-modules.list +++ b/boot/grub/bios-modules.list @@ -1 +1 @@ -biosdisk part_gpt fat iso9660 multiboot2 normal cmp search search_fs_file configfile echo serial terminal video video_bochs video_cirrus vbe all_video font gfxterm gfxmenu regexp hashsum gcry_sha256 crypto loadenv test +biosdisk part_gpt part_msdos ext2 chain ntfs linux gzio reboot search_fs_uuid search_label fat iso9660 multiboot2 normal cmp search search_fs_file configfile echo serial terminal video video_bochs video_cirrus vbe all_video font gfxterm gfxmenu regexp hashsum gcry_sha256 crypto loadenv test diff --git a/boot/grub/embedded.cfg b/boot/grub/embedded.cfg index 48f78e7f..0ae1b7d0 100644 --- a/boot/grub/embedded.cfg +++ b/boot/grub/embedded.cfg @@ -2,4 +2,5 @@ insmod regexp regexp --set=1:reliefos_boot_device '^\(([^)]+)\)' "$cmdpath" set root="$reliefos_boot_device" +configfile $cmdpath/grub.cfg configfile ($root)/grub/grub.cfg diff --git a/boot/grub/grub.cfg b/boot/grub/grub.cfg index da7b8ed3..6b74c940 100644 --- a/boot/grub/grub.cfg +++ b/boot/grub/grub.cfg @@ -5,17 +5,25 @@ insmod terminal insmod part_gpt insmod fat insmod multiboot2 +insmod regexp insmod all_video insmod font insmod gfxterm insmod gfxmenu -set reliefos_root=PARTUUID=5c13543b-732c-4f81-8652-621124484420 -if [ -f ($root)/reliefos/config/root.cfg ]; then - source ($root)/reliefos/config/root.cfg +# A per-instance EFI wrapper or BIOS prefix identifies its own boot tree. +set reliefos_directory= +set reliefos_root= +set reliefos_prefix_device= +regexp --set=1:reliefos_prefix_device '^\(([^)]+)\)' "$prefix" +if [ "$reliefos_prefix_device" = "$root" ]; then + regexp --set=1:reliefos_directory '^\([^)]*\)(.*)/grub$' "$prefix" fi -export reliefos_root -if [ -f ($root)/grub/fonts/leonos-unicode.pf2 ]; then - loadfont ($root)/grub/fonts/leonos-unicode.pf2 +if [ -f ($root)$reliefos_directory/reliefos/config/root.cfg ]; then + source ($root)$reliefos_directory/reliefos/config/root.cfg +fi +export reliefos_root reliefos_directory +if [ -f ($root)$reliefos_directory/grub/fonts/leonos-unicode.pf2 ]; then + loadfont ($root)$reliefos_directory/grub/fonts/leonos-unicode.pf2 fi set color_normal=light-gray/black set color_highlight=black/light-gray @@ -24,79 +32,44 @@ set gfxpayload=keep serial --unit=0 --speed=115200 terminal_input console serial terminal_output gfxterm -set theme=($root)/grub/theme/theme.txt +set theme=($root)$reliefos_directory/grub/theme/theme.txt export theme -source ($root)/grub/kernel-slots.cfg - -if [ "$reliefos_boot_ready" = 1 ]; then -menuentry "ReliefOS" { - multiboot2 $reliefos_kernel root=$reliefos_root log=screen,serial - module2 $reliefos_initramfs initramfs - # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. - set gfxpayload=keep - boot -} - -menuentry "ReliefOS (console log)" { - multiboot2 $reliefos_kernel root=$reliefos_root log=screen,serial bootlog=1 - module2 $reliefos_initramfs initramfs - # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. - set gfxpayload=keep - boot -} - -menuentry "ReliefOS (with boot log)" { - multiboot2 $reliefos_kernel root=$reliefos_root log=screen,serial bootlog=1 bootlog-pause=1 - module2 $reliefos_initramfs initramfs - # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. - set gfxpayload=keep - boot -} - -menuentry "ReliefOS (debug mode)" { - multiboot2 $reliefos_kernel root=$reliefos_root log=screen,serial bootlog=1 debug=1 autospawn=hello autospawn=uidemo autospawn=memtest - module2 $reliefos_initramfs initramfs - # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. - set gfxpayload=keep - boot -} - -menuentry "ReliefOS (SVGA II 3D test)" { - multiboot2 $reliefos_kernel root=$reliefos_root log=screen,serial svga3d-triangle=1 bootlog=1 bootlog-pause=1 - module2 $reliefos_initramfs initramfs - # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. - set gfxpayload=keep - boot -} +source ($root)$reliefos_directory/grub/kernel-slots.cfg +if ! regexp '^PARTUUID=[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' "$reliefos_root"; then + set reliefos_boot_ready=0 + set timeout=-1 + echo "ReliefOS: missing or invalid root identity; use installation media to repair boot files." fi -if [ -n "$reliefos_previous_release" ]; then - menuentry "ReliefOS (previous complete kernel)" { - multiboot2 $reliefos_previous_directory/kernel.sys root=$reliefos_root log=screen,serial - module2 $reliefos_previous_directory/initramfs.cpio initramfs - set gfxpayload=keep - boot - } -fi - -# Imported aliases are exposed only when BOTH files match a checked selection. -# A kernel-only legacy copy cannot establish a safe modular boot entry. if [ "$reliefos_boot_ready" = 1 ]; then -if [ -f ($root)/leonos/kernel.sys -a -f ($root)/leonos/initramfs.cpio ]; then -if cmp ($root)/leonos/kernel.sys ($root)$reliefos_kernel; then -if cmp ($root)/leonos/initramfs.cpio ($root)$reliefos_initramfs; then - menuentry "ReliefOS (legacy rollback)" { - multiboot2 /leonos/kernel.sys root=$reliefos_root log=screen,serial - module2 /leonos/initramfs.cpio initramfs - # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. + menuentry "ReliefOS" { + multiboot2 $reliefos_kernel root=$reliefos_root log=screen,serial + module2 $reliefos_initramfs initramfs set gfxpayload=keep boot } + submenu "Advanced options" { + menuentry "Pause after startup diagnostics" { + multiboot2 $reliefos_kernel root=$reliefos_root log=screen,serial bootlog-pause=1 + module2 $reliefos_initramfs initramfs + set gfxpayload=keep + boot + } + menuentry "Start without kernel log colors" { + multiboot2 $reliefos_kernel root=$reliefos_root log=screen,serial logcolor=0 + module2 $reliefos_initramfs initramfs + set gfxpayload=keep + boot + } + if [ -n "$reliefos_previous_release" ]; then + menuentry "Previous complete kernel" { + multiboot2 $reliefos_previous_directory/kernel.sys root=$reliefos_root log=screen,serial + module2 $reliefos_previous_directory/initramfs.cpio initramfs + set gfxpayload=keep + boot + } + fi + } fi -fi -fi -fi - -menuentry "Reboot" { - reboot -} +source ($root)$reliefos_directory/grub/other-systems.cfg +menuentry "Reboot" { reboot } diff --git a/boot/grub/installer.cfg b/boot/grub/installer.cfg index 99c222a9..4eb301c3 100644 --- a/boot/grub/installer.cfg +++ b/boot/grub/installer.cfg @@ -10,9 +10,6 @@ insmod all_video insmod font insmod gfxterm insmod gfxmenu -if ! search --no-floppy --file /reliefos-installer-iso.marker --set=root; then - search --no-floppy --file /leonos-installer-iso.marker --set=root -fi if [ -f ($root)/grub/fonts/leonos-unicode.pf2 ]; then loadfont ($root)/grub/fonts/leonos-unicode.pf2 fi @@ -27,7 +24,7 @@ set theme=($root)/grub/theme/theme.txt export theme menuentry "Install ReliefOS" { - multiboot2 /reliefos/kernel.sys root=/ log=screen,serial bootlog=1 mode=installer + multiboot2 /reliefos/kernel.sys root=/ log=screen,serial mode=installer module2 /reliefos/initramfs.cpio initramfs # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. set gfxpayload=keep @@ -44,24 +41,6 @@ menuentry "Install ReliefOS (console session)" { boot } -menuentry "Install ReliefOS (Enable boot log screen)" { - multiboot2 /reliefos/kernel.sys root=/ log=screen,serial bootlog=1 bootlog-pause=1 mode=installer - module2 /reliefos/initramfs.cpio initramfs - # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. - set gfxpayload=keep - module2 /install/root.fat reliefos-installer-root - boot -} - -menuentry "Install ReliefOS (debug log)" { - multiboot2 /reliefos/kernel.sys root=/ log=screen,serial bootlog=1 debug=1 mode=installer - module2 /reliefos/initramfs.cpio initramfs - # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. - set gfxpayload=keep - module2 /install/root.fat reliefos-installer-root - boot -} - menuentry "Reboot" { reboot } diff --git a/boot/grub/installer_embedded.cfg b/boot/grub/installer_embedded.cfg index a10238b0..a03bee30 100644 --- a/boot/grub/installer_embedded.cfg +++ b/boot/grub/installer_embedded.cfg @@ -3,9 +3,10 @@ insmod iso9660 insmod search insmod search_fs_file insmod configfile -set root= -search --no-floppy --file /reliefos-installer-iso.marker --set=root +insmod regexp +regexp --set=1:reliefos_boot_device '^\(([^)]+)\)' "$cmdpath" +set root="$reliefos_boot_device" configfile ($root)/grub/grub.cfg -# Compatibility for older media; reached only if the first menu returns/fails. -search --no-floppy --file /leonos-installer-iso.marker --set=root +set root= +search --no-floppy --file /@RELIEFOS_ISO_MARKER@ --set=root configfile ($root)/grub/grub.cfg diff --git a/boot/grub/kernel-slots.cfg b/boot/grub/kernel-slots.cfg index 01a8bbb3..33719a01 100644 --- a/boot/grub/kernel-slots.cfg +++ b/boot/grub/kernel-slots.cfg @@ -14,9 +14,9 @@ function reliefos_read_slot { set reliefos_module_abi= set reliefos_kernel_sha256= set reliefos_initramfs_sha256= - if [ -s ($root)/reliefos/state/kernel-$1.env -a -s ($root)/reliefos/state/kernel-$1.sha256 ]; then - if hashsum --hash sha256 --check ($root)/reliefos/state/kernel-$1.sha256 --prefix ($root)/reliefos/state; then - load_env --file=($root)/reliefos/state/kernel-$1.env reliefos_generation reliefos_release reliefos_module_abi reliefos_kernel_sha256 reliefos_initramfs_sha256 + if [ -s ($root)$reliefos_directory/reliefos/state/kernel-$1.env -a -s ($root)$reliefos_directory/reliefos/state/kernel-$1.sha256 ]; then + if hashsum --hash sha256 --check ($root)$reliefos_directory/reliefos/state/kernel-$1.sha256 --prefix ($root)$reliefos_directory/reliefos/state; then + load_env --file=($root)$reliefos_directory/reliefos/state/kernel-$1.env reliefos_generation reliefos_release reliefos_module_abi reliefos_kernel_sha256 reliefos_initramfs_sha256 else return 1 fi @@ -41,7 +41,7 @@ function reliefos_read_slot { set reliefos_s_abi=$reliefos_module_abi set reliefos_s_kernel=$reliefos_kernel_sha256 set reliefos_s_initramfs=$reliefos_initramfs_sha256 - set reliefos_version_dir=($root)/reliefos/versions/$reliefos_release/$reliefos_initramfs_sha256 + set reliefos_version_dir=($root)$reliefos_directory/reliefos/versions/$reliefos_release/$reliefos_initramfs_sha256 if [ -s $reliefos_version_dir/SHA256SUMS -a -s $reliefos_version_dir/boot.env ]; then if hashsum --hash sha256 --check $reliefos_version_dir/SHA256SUMS --prefix $reliefos_version_dir; then # A list can lose a whole record while its remaining hashes pass. @@ -111,18 +111,18 @@ if [ "$reliefos_b_valid" = 1 ]; then if [ "$reliefos_a_valid" = 1 ]; then if [ "$reliefos_a_release" != "$reliefos_b_release" -o "$reliefos_a_initramfs" != "$reliefos_b_initramfs" ]; then set reliefos_previous_release=$reliefos_a_release - set reliefos_previous_directory=/reliefos/versions/$reliefos_a_release/$reliefos_a_initramfs + set reliefos_previous_directory=$reliefos_directory/reliefos/versions/$reliefos_a_release/$reliefos_a_initramfs fi fi elif [ "$reliefos_a_release" != "$reliefos_b_release" -o "$reliefos_a_initramfs" != "$reliefos_b_initramfs" ]; then set reliefos_previous_release=$reliefos_b_release - set reliefos_previous_directory=/reliefos/versions/$reliefos_b_release/$reliefos_b_initramfs + set reliefos_previous_directory=$reliefos_directory/reliefos/versions/$reliefos_b_release/$reliefos_b_initramfs fi set reliefos_boot_ready=1 fi if [ "$reliefos_boot_ready" = 1 ]; then - set reliefos_kernel=/reliefos/versions/$reliefos_selected_release/$reliefos_selected_initramfs/kernel.sys - set reliefos_initramfs=/reliefos/versions/$reliefos_selected_release/$reliefos_selected_initramfs/initramfs.cpio + set reliefos_kernel=$reliefos_directory/reliefos/versions/$reliefos_selected_release/$reliefos_selected_initramfs/kernel.sys + set reliefos_initramfs=$reliefos_directory/reliefos/versions/$reliefos_selected_release/$reliefos_selected_initramfs/initramfs.cpio echo "ReliefOS: checked boot slot $reliefos_selected, release $reliefos_selected_release" else set timeout=-1 diff --git a/boot/grub/live.cfg b/boot/grub/live.cfg index 43d859b3..043f2c27 100644 --- a/boot/grub/live.cfg +++ b/boot/grub/live.cfg @@ -8,9 +8,6 @@ insmod multiboot2 insmod all_video insmod font insmod gfxterm -if ! search --no-floppy --file /reliefos-installer-iso.marker --set=root; then - search --no-floppy --file /leonos-installer-iso.marker --set=root -fi loadfont /grub/fonts/leonos-unicode.pf2 set gfxmode=1920x1080,1280x800,1024x768,auto set gfxpayload=keep @@ -18,17 +15,8 @@ serial --unit=0 --speed=115200 terminal_input console serial terminal_output gfxterm serial -menuentry "ReliefOS Live Desktop (musl + Vim + GCC)" { - multiboot2 /reliefos/kernel.sys root=/ log=screen,serial bootlog=1 mode=live - module2 /reliefos/initramfs.cpio initramfs - # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. - set gfxpayload=keep - module2 /install/root.fat reliefos-installer-root - boot -} - -menuentry "ReliefOS Live Desktop (boot log)" { - multiboot2 /reliefos/kernel.sys root=/ log=screen,serial mode=live bootlog=1 +menuentry "ReliefOS Live Desktop" { + multiboot2 /reliefos/kernel.sys root=/ log=screen,serial mode=live module2 /reliefos/initramfs.cpio initramfs # Multiboot2 applies its framebuffer hint while loading; retain the menu mode. set gfxpayload=keep diff --git a/boot/grub/other-systems.cfg b/boot/grub/other-systems.cfg new file mode 100644 index 00000000..8e6c3043 --- /dev/null +++ b/boot/grub/other-systems.cfg @@ -0,0 +1,92 @@ +# Enumerate installed boot trees and EFI applications at boot, including disks +# added after installation. Never execute another system's config automatically. +insmod regexp +insmod part_gpt +insmod part_msdos +insmod fat +insmod ext2 +insmod ntfs +for config in (*)/reliefos/instances/*/grub/grub.cfg; do + if [ -f "$config" -a "$config" != "($root)$reliefos_directory/grub/grub.cfg" ]; then + if regexp --set=1:device --set=2:instance_uuid '^(.*)/reliefos/instances/([0-9a-f-]{36})/grub/grub.cfg$' "$config"; then + menuentry "ReliefOS ($instance_uuid on $device)" "$config" { + set prefix=$2 + regexp --set=1:prefix '^(.*)/grub.cfg$' "$prefix" + regexp --set=1:root '^\(([^)]+)\)' "$prefix" + configfile $prefix/grub.cfg + } + fi + fi +done +for config in (*)/grub/grub.cfg; do + if regexp --set=1:device '^(.*)/grub/grub.cfg$' "$config"; then + if [ -f "$config" -a -f "$device/reliefos/config/root.cfg" -a "$config" != "($root)$reliefos_directory/grub/grub.cfg" ]; then + menuentry "ReliefOS ($device)" "$config" { + regexp --set=1:root '^\(([^)]+)\)' "$2" + regexp --set=1:prefix '^(.*)/grub.cfg$' "$2" + configfile "$2" + } + fi + fi +done +if [ "$grub_platform" = efi ]; then + insmod chain + for vendor in (*)/EFI/*; do + if regexp '/EFI/(ReliefOS|BOOT|boot)$' "$vendor"; then continue; fi + set loader= + if [ -f "$vendor/Boot/bootmgfw.efi" ]; then + set loader=$vendor/Boot/bootmgfw.efi + elif [ -f "$vendor/shimx64.efi" ]; then + set loader=$vendor/shimx64.efi + elif [ -f "$vendor/grubx64.efi" ]; then + set loader=$vendor/grubx64.efi + elif [ -f "$vendor/systemd-bootx64.efi" ]; then + set loader=$vendor/systemd-bootx64.efi + elif [ -f "$vendor/bootx64.efi" ]; then + set loader=$vendor/bootx64.efi + fi + if [ -n "$loader" ]; then + regexp --set=1:system_name '/EFI/([^/]+)$' "$vendor" + menuentry "$system_name" "$loader" { + chainloader "$2" + boot + } + fi + done + for loader in (*)/EFI/BOOT/BOOTX64.EFI; do + regexp --set=1:device '^(.*)/EFI/BOOT/BOOTX64.EFI$' "$loader" + if [ -f "$loader" -a ! -f "$device/EFI/BOOT/reliefos-owner" -a ! -f "$device/reliefos/config/root.cfg" ]; then + menuentry "Other EFI bootloader ($device)" "$loader" { + chainloader "$2" + boot + } + fi + done +else + for manager in (*)/bootmgr; do + if [ -f "$manager" ]; then + menuentry "Windows (BIOS)" "$manager" { + regexp --set=1:root '^\(([^)]+)\)' "$2" + chainloader +1 + boot + } + fi + done + for device in (*); do + set config= + if [ -f "$device/boot/grub/grub.cfg" ]; then + set config=$device/boot/grub/grub.cfg + elif [ -f "$device/boot/grub2/grub.cfg" ]; then + set config=$device/boot/grub2/grub.cfg + elif [ -f "$device/grub/grub.cfg" -a ! -f "$device/reliefos/config/root.cfg" ]; then + set config=$device/grub/grub.cfg + fi + if [ -n "$config" -a "$config" != "($root)$reliefos_directory/grub/grub.cfg" ]; then + menuentry "Other system ($device)" "$config" { + regexp --set=1:root '^\(([^)]+)\)' "$2" + regexp --set=1:prefix '^(.*)/grub.cfg$' "$2" + configfile "$2" + } + fi + done +fi diff --git a/configs/apk-ownership.json b/configs/apk-ownership.json index 370fe065..3dff8184 100644 --- a/configs/apk-ownership.json +++ b/configs/apk-ownership.json @@ -2,6 +2,11 @@ "version": 1, "current_distributor": "reliefos-build", "apk_registration": "not-installed", + "upstream_components": { + "sl": ["sl"], + "ncurses": ["ncurses", "ncurses-terminfo"], + "sqlite": ["sqlite", "sqlite-libs"] + }, "groups": { "reliefos-kernel": { "destination": "reliefos", @@ -37,7 +42,13 @@ "destination": "reliefos", "components": ["reliefplayer"], "paths": ["usr/bin/reliefplayer", "usr/share/applications/reliefos-reliefplayer.desktop"], - "reason": "ReliefPlayer audio executable, launcher, desktop entry and component artwork are installed and removed together. minimp3 remains embedded in its consumers." + "reason": "ReliefPlayer audio/video executable, launcher, desktop entry and component artwork are installed and removed together. minimp3 remains embedded in its consumers." + }, + "reliefos-media-codecs": { + "destination": "reliefos", + "components": [], + "paths": ["usr/lib/libavformat-reliefplayer.so", "usr/lib/libavcodec-reliefplayer.so", "usr/lib/libavutil-reliefplayer.so", "usr/lib/libswresample-reliefplayer.so", "usr/lib/libswscale-reliefplayer.so", "usr/share/licenses/ffmpeg"], + "reason": "Pinned LGPL FFmpeg libraries for local MP4 decoding in ReliefPlayer; headers and development files stay outside the runtime image." }, "reliefos-network-controller": { "destination": "reliefos", @@ -209,12 +220,6 @@ "alpine_candidates": ["e2fsprogs", "dosfstools", "exfatprogs"], "reason": "Transfer formatter/checker aliases with commands; preserve ext2 installer behavior." }, - "ncurses": { - "destination": "alpine-after-validation", - "components": ["ncurses"], - "alpine_candidates": ["ncurses", "ncurses-terminfo"], - "reason": "Resolve terminal data/tools split and libraries from repository metadata; preserve leonos TERM support." - }, "ca-certificates-bundle": { "destination": "alpine-after-validation", "components": [], @@ -222,19 +227,6 @@ "alpine_candidates": ["ca-certificates-bundle"], "reason": "The actual CA bundle is packaged separately so curl dependencies and authenticated upstream updates can own it." }, - "sqlite": { - "destination": "alpine-after-validation", - "components": ["sqlite"], - "paths": ["usr/lib/sqlite.so.3"], - "alpine_candidates": ["sqlite", "sqlite-libs"], - "reason": "Current custom SONAME and native consumers need migration before upstream library replacement." - }, - "sl": { - "destination": "alpine-after-validation", - "components": ["sl"], - "alpine_candidates": ["sl"], - "reason": "Generic optional terminal application." - }, "trust": { "destination": "alpine-after-validation", "components": [], diff --git a/configs/components.toml b/configs/components.toml index 2c645331..36366e41 100644 --- a/configs/components.toml +++ b/configs/components.toml @@ -524,7 +524,7 @@ entry = true sdk = false api = false open_with = true -extensions = [".mp3", ".wav"] +extensions = [".mp3", ".wav", ".mp4"] [[components]] id = "wavplay" diff --git a/configs/dependencies.lock.json b/configs/dependencies.lock.json index a0dad10b..0a55e88e 100644 --- a/configs/dependencies.lock.json +++ b/configs/dependencies.lock.json @@ -1,6 +1,18 @@ { "schema_version": 1, "dependencies": [ + { + "id": "ffmpeg", + "kind": "tarball", + "version": "8.1.3", + "commit": "1041abdc962f4cc4f394aa8de9dc5236c0c3b9e7", + "url": "https://codeload.github.com/FFmpeg/FFmpeg/tar.gz/refs/tags/n8.1.3", + "sha256": "09f990289327d3ebfedf4fc15cae3f884a65dab7a5aac7bbb5b01ff83900efd9", + "directory": "FFmpeg-n8.1.3", + "script": "tools/build/ffmpeg.sh", + "target": "upstream-ffmpeg", + "license_in_source": "COPYING.LGPLv2.1" + }, { "id": "alpine-bash", "kind": "apk", @@ -1199,7 +1211,7 @@ "directory": "libpng-1.6.59-r0", "script": "tools/build/upstream-apk.sh", "target": "apk-root", - "feature": "xorg", + "feature": "base", "note": "Alpine v3.24 main x86_64 APKINDEX license = Libpng; embedded APK signature is verified by apk-tools", "license_in_source": ".PKGINFO" }, @@ -2375,7 +2387,7 @@ "directory": "zlib-1.4-r0", "script": "tools/build/upstream-apk.sh", "target": "apk-root", - "feature": "xorg", + "feature": "base", "note": "Alpine v3.24 main x86_64 APKINDEX license = Zlib; embedded APK signature is verified by apk-tools", "license_in_source": ".PKGINFO" }, @@ -2509,15 +2521,6 @@ "script": "tools/build/upstream.sh", "target": "auth-upstream" }, - { - "id": "libpng", - "kind": "submodule", - "commit": "3061454d980de7d53608f594194cfac722721d2a", - "directory": "third_party/libpng", - "script": "build.py:archive:libpng", - "target": "musl-runtime", - "license": "third_party/libpng/LICENSE" - }, { "id": "libxcrypt", "kind": "tarball", @@ -2585,15 +2588,6 @@ "script": "tools/build_musl_ltp.py", "target": "musl-ltp" }, - { - "id": "mbedtls", - "kind": "submodule", - "commit": "068ff080b369adfac81509f9b57b2afabaf82dc5", - "directory": "third_party/mbedtls", - "script": "build.py:kernel", - "target": "kernel", - "license": "third_party/mbedtls/LICENSE" - }, { "id": "mimalloc", "kind": "submodule", @@ -2616,6 +2610,10 @@ { "path": "patches/musl/0001-enforce-password-file-lock.patch", "sha256": "e336ed20b2d338dbbf88588dff043e8ce349d0d0044277d31be229d600bb93ad" + }, + { + "path": "patches/musl/0002-report-unsupported-pi-mutex-protocol.patch", + "sha256": "6454c5786fe36c513e5dbc69eb221cfdde840f90e49e00139da4c3e55cece039" } ] }, @@ -2630,15 +2628,6 @@ "target": "musl-locales", "license_in_source": "LICENSE" }, - { - "id": "ncurses", - "kind": "submodule", - "commit": "0096bd402c4a9c8f39bd7ed266e1b8920327e4d8", - "directory": "third_party/ncurses", - "script": "tools/build/upstream-terminal.sh", - "target": "terminal-packages", - "license": "third_party/ncurses/COPYING" - }, { "id": "pl_editor", "kind": "submodule", @@ -2669,24 +2658,6 @@ "script": "tools/build/upstream.sh", "target": "auth-upstream" }, - { - "id": "sl", - "kind": "submodule", - "commit": "923e7d7ebc5c1f009755bdeb789ac25658ccce03", - "directory": "third_party/sl", - "script": "tools/build/upstream-app.sh", - "target": "sl", - "license": "third_party/sl/LICENSE" - }, - { - "id": "sqlite", - "kind": "submodule", - "commit": "f3d536d37825302e31ed0eddd811c689f38f85a3", - "directory": "third_party/sqlite", - "script": "tools/build/upstream-app.sh", - "target": "sqlite", - "license": "third_party/sqlite/LICENSE.md" - }, { "id": "stardustui", "kind": "submodule", @@ -2731,15 +2702,6 @@ "script": "tools/build/upstream.sh", "target": "auth-upstream" }, - { - "id": "zlib", - "kind": "submodule", - "commit": "da607da739fa6047df13e66a2af6b8bec7c2a498", - "directory": "third_party/zlib", - "script": "build.py:archive:zlib", - "target": "musl-runtime", - "license": "third_party/zlib/LICENSE" - }, { "id": "alsa-lib", "kind": "tarball", @@ -2814,6 +2776,212 @@ "license_in_source": "COPYING", "checksum_source": "https://raw.githubusercontent.com/buildroot/buildroot/master/boot/grub2/grub2.hash", "note": "Official GRUB 2.16 release; signature fingerprint DFB62CC1A987E6C76EBF8143916EC0708CDFDDFD; BIOS and EFI modules and tools are built from the same locked source." + }, + { + "id": "alpine-mbedtls3", + "kind": "apk", + "version": "3.6.7-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/community/x86_64/mbedtls3-3.6.7-r0.apk", + "sha256": "d4ac9dec9efd61bb7cc72929eaf944279439068b0b07c7d2167bfab98dce9273", + "directory": "mbedtls3-3.6.7-r0", + "script": "tools/build/upstream-apk.sh", + "target": "apk-root", + "feature": "base", + "note": "Alpine v3.24 community x86_64; official APK signature verified before use; license = Apache-2.0 OR GPL-2.0-or-later.", + "license_in_source": ".PKGINFO" + }, + { + "id": "alpine-ncurses", + "kind": "apk", + "version": "6.6_p20260516-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/ncurses-6.6_p20260516-r0.apk", + "sha256": "a36d43d2bacbfaab10fe76221f62704066d8bbb5800d7d3a9c2a562e723fad8a", + "directory": "ncurses-6.6_p20260516-r0", + "script": "tools/build/upstream-apk.sh", + "target": "apk-root", + "feature": "component:ncurses", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = X11.", + "license_in_source": ".PKGINFO" + }, + { + "id": "alpine-ncurses-terminfo", + "kind": "apk", + "version": "6.6_p20260516-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/ncurses-terminfo-6.6_p20260516-r0.apk", + "sha256": "cd84fa2666e59ea2e224cb936db96006a40d9bd850d938c0d9f2c034eaae6425", + "directory": "ncurses-terminfo-6.6_p20260516-r0", + "script": "tools/build/upstream-apk.sh", + "target": "apk-root", + "feature": "component:ncurses", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = X11.", + "license_in_source": ".PKGINFO" + }, + { + "id": "alpine-sl", + "kind": "apk", + "version": "5.02-r1", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/community/x86_64/sl-5.02-r1.apk", + "sha256": "aa3ceb845953904ab61459f0f97416903c56a80b428f8bc320d879b9e58c1a57", + "directory": "sl-5.02-r1", + "script": "tools/build/upstream-apk.sh", + "target": "apk-root", + "feature": "component:sl", + "note": "Alpine v3.24 community x86_64; official APK signature verified before use; license = MIT.", + "license_in_source": ".PKGINFO" + }, + { + "id": "alpine-sqlite", + "kind": "apk", + "version": "3.53.4-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/sqlite-3.53.4-r0.apk", + "sha256": "c51f4aae4fdbbec678639cdaa7af6758eef331dbf7299e2777fa7020b41ff194", + "directory": "sqlite-3.53.4-r0", + "script": "tools/build/upstream-apk.sh", + "target": "apk-root", + "feature": "component:sqlite", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = blessing.", + "license_in_source": ".PKGINFO" + }, + { + "id": "alpine-sqlite-libs", + "kind": "apk", + "version": "3.53.4-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/sqlite-libs-3.53.4-r0.apk", + "sha256": "a8a216e53d22faa3f04d2e3650c1991af66f8d0b42fadff5afcdb9afb60b19e5", + "directory": "sqlite-libs-3.53.4-r0", + "script": "tools/build/upstream-apk.sh", + "target": "apk-root", + "feature": "component:sqlite", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = blessing.", + "license_in_source": ".PKGINFO" + }, + { + "id": "runtime-dev-libpng-dev", + "kind": "apk", + "version": "1.6.59-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/libpng-dev-1.6.59-r0.apk", + "sha256": "71731d72ac2bd884401d6322639d4da73de606759c0af19fe1f783f55d9f3688", + "directory": "libpng-dev-1.6.59-r0", + "script": "tools/build/runtime-development.sh", + "target": "runtime-development", + "feature": "base", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = Libpng.", + "license_in_source": ".PKGINFO" + }, + { + "id": "runtime-dev-libpng-static", + "kind": "apk", + "version": "1.6.59-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/libpng-static-1.6.59-r0.apk", + "sha256": "2ebc64ebe210aa006839f0af0e93ea6f01200ecb57da53eb06e46865fedcc2ac", + "directory": "libpng-static-1.6.59-r0", + "script": "tools/build/runtime-development.sh", + "target": "runtime-development", + "feature": "base", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = Libpng.", + "license_in_source": ".PKGINFO" + }, + { + "id": "runtime-dev-mbedtls3-dev", + "kind": "apk", + "version": "3.6.7-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/community/x86_64/mbedtls3-dev-3.6.7-r0.apk", + "sha256": "510a08d0bf913391469ac8dc1a7f0fa8a27c046f38593cad045c663a7c1b5e0d", + "directory": "mbedtls3-dev-3.6.7-r0", + "script": "tools/build/runtime-development.sh", + "target": "runtime-development", + "feature": "base", + "note": "Alpine v3.24 community x86_64; official APK signature verified before use; license = Apache-2.0 OR GPL-2.0-or-later.", + "license_in_source": ".PKGINFO" + }, + { + "id": "runtime-dev-mbedtls3-static", + "kind": "apk", + "version": "3.6.7-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/community/x86_64/mbedtls3-static-3.6.7-r0.apk", + "sha256": "83a6707af57599f8085b45f845defad73a5de3dfea46d06874b0f65b90234dc4", + "directory": "mbedtls3-static-3.6.7-r0", + "script": "tools/build/runtime-development.sh", + "target": "runtime-development", + "feature": "base", + "note": "Alpine v3.24 community x86_64; official APK signature verified before use; license = Apache-2.0 OR GPL-2.0-or-later.", + "license_in_source": ".PKGINFO" + }, + { + "id": "runtime-dev-sqlite-dev", + "kind": "apk", + "version": "3.53.4-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/sqlite-dev-3.53.4-r0.apk", + "sha256": "c30a6155ebd456f37524242c2a60690e516666fa487fba6630280e7ecdf9442f", + "directory": "sqlite-dev-3.53.4-r0", + "script": "tools/build/runtime-development.sh", + "target": "runtime-development", + "feature": "base", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = blessing.", + "license_in_source": ".PKGINFO" + }, + { + "id": "runtime-dev-sqlite-static", + "kind": "apk", + "version": "3.53.4-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/sqlite-static-3.53.4-r0.apk", + "sha256": "83d2dabbda8e65d76d6c8d7486996891a1fde7e8053289d58aeb5f7d917b63ae", + "directory": "sqlite-static-3.53.4-r0", + "script": "tools/build/runtime-development.sh", + "target": "runtime-development", + "feature": "base", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = blessing.", + "license_in_source": ".PKGINFO" + }, + { + "id": "runtime-dev-zlib-dev", + "kind": "apk", + "version": "1.4-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/zlib-dev-1.4-r0.apk", + "sha256": "889ed6014fc9acb815a7755732d4099bd84b1176b4845853bb050361d8b876b2", + "directory": "zlib-dev-1.4-r0", + "script": "tools/build/runtime-development.sh", + "target": "runtime-development", + "feature": "base", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = Zlib.", + "license_in_source": ".PKGINFO" + }, + { + "id": "runtime-dev-zlib-static", + "kind": "apk", + "version": "1.4-r0", + "url": "https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/zlib-static-1.4-r0.apk", + "sha256": "3e02130f63e764b969f14ae62fe1f50b58436f709895c9d9fa2b7496c98a03b5", + "directory": "zlib-static-1.4-r0", + "script": "tools/build/runtime-development.sh", + "target": "runtime-development", + "feature": "base", + "note": "Alpine v3.24 main x86_64; official APK signature verified before use; license = Zlib.", + "license_in_source": ".PKGINFO" + }, + { + "id": "host-test-mbedtls3", + "kind": "tarball", + "version": "3.6.7", + "url": "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-3.6.7/mbedtls-3.6.7.tar.bz2", + "sha256": "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6", + "directory": "mbedtls-3.6.7", + "script": "tools/build/host-runtime-development.sh", + "target": "host-runtime-development", + "license_in_source": "LICENSE", + "note": "Host-only native libraries for ASan/UBSan source tests; never used in target runtime, SDK, APKs or release images." + }, + { + "id": "host-test-sqlite3", + "kind": "tarball", + "version": "3.53.4", + "url": "https://sqlite.org/2026/sqlite-autoconf-3530400.tar.gz", + "sha256": "0e9483900e92cd5de8fd48d16bf9200145a61f7fd5be542a5ac81d8a9516eb9c", + "directory": "sqlite-autoconf-3530400", + "script": "tools/build/host-runtime-development.sh", + "target": "host-runtime-development", + "license_in_source": "README.txt", + "note": "Host-only native libraries for ASan/UBSan source tests; never used in target runtime, SDK, APKs or release images." } ] } diff --git a/configs/nls/messages.c b/configs/nls/messages.c index 22d1622d..afacfb52 100644 --- a/configs/nls/messages.c +++ b/configs/nls/messages.c @@ -685,15 +685,15 @@ static const char *const relieftodo_static_labels[] = { /* ReliefPlayer labels returned by the decoder error table. */ static const char *const reliefplayer_static_labels[] = { N_("ReliefPlayer"), - N_("Could not read the audio file"), - N_("Invalid or truncated audio file"), - N_("Unsupported audio format"), - N_("Audio file exceeds the supported limit"), + N_("Could not read the media file"), + N_("Invalid or truncated media file"), + N_("Unsupported media format or codec"), + N_("Media exceeds the supported size or resolution"), N_("Not enough memory"), N_("Audio device is unavailable"), N_("Audio device is busy"), - N_("Audio playback failed"), - N_("Audio process stopped unexpectedly"), + N_("Media playback failed"), + N_("Playback process stopped unexpectedly"), }; /* ReliefCapture worker errors and application name. */ diff --git a/configs/nls/po/leonos.pot b/configs/nls/po/leonos.pot index 60648060..9310b16b 100644 --- a/configs/nls/po/leonos.pot +++ b/configs/nls/po/leonos.pot @@ -550,10 +550,6 @@ msgstr "" msgid "Accounts" msgstr "" -#: userland/apps/installer/main.cpp:477 -msgid "Acknowledgements for public resources and open-source projects." -msgstr "" - #: userland/apps/calc/units.cpp:22 msgid "Acre" msgstr "" @@ -778,10 +774,6 @@ msgstr "" msgid "Alpine packages and local configuration are preserved." msgstr "" -#: userland/apps/installer/main.cpp:1027 -msgid "Alpine packages and local configuration are retained. Boot files are updated after the package transaction succeeds." -msgstr "" - #: userland/apps/reliefexcel/window.cpp:525 msgid "Amount" msgstr "" @@ -2350,10 +2342,6 @@ msgstr "" msgid "Confirm Trash clear" msgstr "" -#: userland/apps/installer/main.cpp:1015 -msgid "Confirm Update" -msgstr "" - #: userland/apps/reliefmail/window_reader.cpp:170 msgid "Confirm action" msgstr "" @@ -5998,10 +5986,6 @@ msgstr "" msgid "Installed" msgstr "" -#: userland/apps/installer/main.cpp:1017 -msgid "Installed ReliefOS packages will be upgraded." -msgstr "" - #: userland/apps/reliefapk/window.cpp:688 msgid "Installed size" msgstr "" @@ -6591,6 +6575,7 @@ msgid "Library" msgstr "" #: userland/apps/reliefapk/window.cpp:684 +#: userland/apps/installer/main.cpp msgid "License" msgstr "" @@ -11814,14 +11799,6 @@ msgstr "" msgid "Text..." msgstr "" -#: userland/apps/installer/main.cpp:476 -msgid "Thank You" -msgstr "" - -#: userland/apps/installer/main.cpp:214 -msgid "Thanks" -msgstr "" - #: userland/apps/oshlp/main.c:1208 msgid "The .hlp file could not be read." msgstr "" @@ -12574,10 +12551,6 @@ msgstr "" msgid "Type INSTALL to enable the Install button." msgstr "" -#: userland/apps/installer/main.cpp:1033 -msgid "Type UPDATE to enable the Update button." -msgstr "" - #: userland/apps/reliefmail/window_actions.cpp:491 msgid "Type the account-bound Trash phrase." msgstr "" @@ -15030,10 +15003,6 @@ msgstr "" msgid "Choose bootloaders" msgstr "" -#: userland/apps/installer/main.cpp:969 -msgid "Existing partitions are preserved. BIOS requires an unformatted BIOS Boot partition. Skipped existing bootloaders are preserved." -msgstr "" - #: userland/apps/installer/main.cpp:608 msgid "Manual layout: assign a FAT32 ESP and an ext4 root. BIOS Boot is not required for the selected bootloaders." msgstr "" @@ -15058,10 +15027,6 @@ msgstr "" msgid "The automatic layout does not fit this disk. Select another disk or configure partitions manually." msgstr "" -#: userland/apps/installer/main.cpp:1006 -msgid "The initial selection matches the bootloaders found on this disk." -msgstr "" - #: userland/apps/installer/main.cpp:707 msgid "Type AUTO to erase the disk and create ESP and root partitions:" msgstr "" @@ -15461,3 +15426,291 @@ msgstr "" #: userland/apps/network-controller/window.cpp msgid "Adapter counters are not exported by this kernel. Address, gateway, DNS and connection diagnostics remain available." msgstr "" + +#: userland/apps/installer +msgid "Checking update target" +msgstr "" + +#: userland/apps/installer +msgid "Administrator privileges are required" +msgstr "" + +#: userland/apps/installer +msgid "Identifying the installed system" +msgstr "" + +#: userland/apps/installer +msgid "Checking installation media and free space" +msgstr "" + +#: userland/apps/installer +msgid "Installation media is older than the installed system" +msgstr "" + +#: userland/apps/installer +msgid "Target or media changed. Check the update again." +msgstr "" + +#: userland/apps/installer +msgid "Checking bootloader compatibility" +msgstr "" + +#: userland/apps/installer +msgid "Verifying signed packages and dependencies" +msgstr "" + +#: userland/apps/installer +msgid "Update checks completed" +msgstr "" + +#: userland/apps/installer +msgid "Preparing the installed system for update" +msgstr "" + +#: userland/apps/installer +msgid "Recovering the previous update attempt" +msgstr "" + +#: userland/apps/installer +msgid "Applying signed system packages" +msgstr "" + +#: userland/apps/installer +msgid "Synchronizing the updated root filesystem" +msgstr "" + +#: userland/apps/installer +msgid "Resuming bootloader publication" +msgstr "" + +#: userland/apps/installer +msgid "Publishing kernel and bootloader files" +msgstr "" + +#: userland/apps/installer +msgid "Finishing the system update" +msgstr "" + +#: userland/apps/installer +msgid "System update completed successfully" +msgstr "" + +#: userland/apps/installer +msgid "Writing the update log" +msgstr "" + +#: userland/apps/installer +msgid "Update worker failed" +msgstr "" + +#: userland/apps/installer +msgid "Check Again" +msgstr "" + +#: userland/apps/installer +msgid "Check Update" +msgstr "" + +#: userland/apps/installer +msgid "Ready to update" +msgstr "" + +#: userland/apps/installer +msgid "Review the detected system and start the update." +msgstr "" + +#: userland/apps/installer +msgid "Cannot start the update worker" +msgstr "" + +#: userland/apps/installer +msgid "Preparing the system update" +msgstr "" + +#: userland/apps/installer +msgid "Checking the selected system" +msgstr "" + +#: userland/apps/installer +msgid "Review system update" +msgstr "" + +#: userland/apps/installer +msgid "Media version" +msgstr "" + +#: userland/apps/installer +msgid "Target disk" +msgstr "" + +#: userland/apps/installer +msgid "Boot partition" +msgstr "" + +#: userland/apps/installer +msgid "Desktop environment" +msgstr "" + +#: userland/apps/installer +msgid "Console" +msgstr "" + +#: userland/apps/installer +msgid "Free / required space" +msgstr "" + +#: userland/apps/installer +msgid "Update bootloader software" +msgstr "" + +#: userland/apps/installer +msgid "Accounts, personal files, installed software and local configuration are preserved." +msgstr "" + +#: userland/apps/installer +msgid "A previous update is incomplete. This attempt will verify the system and recover the pending update stages." +msgstr "" + +#: userland/apps/installer +msgid "Signed packages are checked before changes. Boot files are published after the package transaction succeeds." +msgstr "" + +#: userland/apps/installer +msgid "Update details" +msgstr "" + +#: userland/apps/installer +msgid "Checking system update" +msgstr "" + +#: userland/apps/installer +msgid "Review the details, then check the update again to retry." +msgstr "" + +#: userland/apps/installer +msgid "System version" +msgstr "" + +#: userland/apps/installer +msgid "Proceed with system update? [y/N]: " +msgstr "" + +#: configs/nls/messages.c:688 +msgid "Could not read the media file" +msgstr "" + +#: configs/nls/messages.c:689 +msgid "Invalid or truncated media file" +msgstr "" + +#: configs/nls/messages.c:691 +msgid "Media exceeds the supported size or resolution" +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:181 +msgid "Media files (*.{mp3,MP3,wav,WAV,mp4,MP4})" +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:55 +msgid "Media playback failed" +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:272 +msgid "No audio track" +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:85 +msgid "No media file selected" +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:43 +msgid "Open an MP3, WAV or MP4 file to begin" +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:181 +msgid "Open media" +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:81 +msgid "Open media..." +msgstr "" + +#: configs/nls/messages.c:696 +msgid "Playback process stopped unexpectedly" +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:45 +msgid "Reading media information..." +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:330 +msgid "ReliefPlayer\nMP3 / WAV audio and MP4 video player\n\nCtrl+O: open media\nSpace: play or pause\nLeft / Right: seek 10 seconds\nHome: return to the beginning\n\nVolume changes affect this player only.\nSupports PCM WAV 8/16/24/32-bit and float32, mono or stereo.\nMP4: H.264, MPEG-4 or HEVC video; AAC or MP3 audio.\nLocal files only; no network streams or media library." +msgstr "" + +#: userland/apps/reliefplayer/window.cpp:221 +msgid "Stopping playback..." +msgstr "" + +#: configs/nls/messages.c:690 +msgid "Unsupported media format or codec" +msgstr "" + +#: userland/apps/installer/installer_tty.c:435 +msgid "Automatic installation erases the selected disk." +msgstr "" + +#: userland/apps/installer/main.cpp:691 +msgid "Automatically identify a single ReliefOS installation" +msgstr "" + +#: userland/apps/installer/main.cpp:449 +msgid "Cannot identify the selected partitions" +msgstr "" + +#: userland/apps/installer/main.cpp:765 +msgid "EFI system partition" +msgstr "" + +#: userland/apps/installer/installer_tty.c:381 +msgid "ESP partition number (blank: use fstab): " +msgstr "" + +#: userland/apps/installer/main.cpp:1067 +msgid "Format selected ESP (erase all EFI boot files on it)" +msgstr "" + +#: userland/apps/installer/installer_tty.c:437 +msgid "Only the selected root is formatted; existing ESP files are preserved." +msgstr "" + +#: userland/apps/installer/main.cpp:1285 +msgid "Only the selected root will be formatted. Existing ESP files and other partitions are preserved." +msgstr "" + +#: userland/apps/installer/installer_tty.c:382 +msgid "ReliefOS root partition number (blank: auto): " +msgstr "" + +#: userland/apps/installer/main.cpp:763 +msgid "ReliefOS system partition" +msgstr "" + +#: userland/apps/installer/installer_tty.c:436 +msgid "The selected root and ESP will be formatted." +msgstr "" + +#: userland/apps/installer/main.cpp:1284 +msgid "The selected root and ESP will be formatted. All EFI boot files on that ESP will be erased." +msgstr "" + +#: userland/apps/installer/main.cpp:1063 +msgid "The selected root is formatted. An existing FAT32 ESP is preserved by default." +msgstr "" + +#: userland/apps/installer/main.cpp:1378 +msgid "UEFI boot file (add it in firmware if no automatic entry appears):" +msgstr "" + +#: userland/apps/installer/main.cpp:692 +msgid "Use the selected installation's fstab" +msgstr "" diff --git a/configs/nls/po/zh_CN.po b/configs/nls/po/zh_CN.po index c003b024..23de6246 100644 --- a/configs/nls/po/zh_CN.po +++ b/configs/nls/po/zh_CN.po @@ -579,10 +579,6 @@ msgstr "账户创建失败" msgid "Accounts" msgstr "账户" -#: userland/apps/installer/main.cpp:477 -msgid "Acknowledgements for public resources and open-source projects." -msgstr "公共资源与开源项目致谢。" - #: userland/apps/calc/units.cpp:22 msgid "Acre" msgstr "英亩" @@ -807,10 +803,6 @@ msgstr "阿尔法" msgid "Alpine packages and local configuration are preserved." msgstr "保留 Alpine 软件包及本地配置。" -#: userland/apps/installer/main.cpp:1027 -msgid "Alpine packages and local configuration are retained. Boot files are updated after the package transaction succeeds." -msgstr "保留 Alpine 软件包和本地配置,软件包事务成功后再更新启动文件。" - #: userland/apps/reliefexcel/window.cpp:525 msgid "Amount" msgstr "金额" @@ -2388,10 +2380,6 @@ msgstr "确认 SMTP 发送" msgid "Confirm Trash clear" msgstr "确认清空废纸篓" -#: userland/apps/installer/main.cpp:1015 -msgid "Confirm Update" -msgstr "确认更新" - #: userland/apps/reliefmail/window_reader.cpp:170 msgid "Confirm action" msgstr "确认操作" @@ -6079,10 +6067,6 @@ msgstr "安装介质不完整" msgid "Installed" msgstr "已安装" -#: userland/apps/installer/main.cpp:1017 -msgid "Installed ReliefOS packages will be upgraded." -msgstr "将升级已安装的 ReliefOS 软件包。" - #: userland/apps/reliefapk/window.cpp:688 msgid "Installed size" msgstr "已安装大小" @@ -6677,6 +6661,7 @@ msgid "Library" msgstr "公式库" #: userland/apps/reliefapk/window.cpp:684 +#: userland/apps/installer/main.cpp msgid "License" msgstr "许可证" @@ -11966,14 +11951,6 @@ msgstr "文本宽度溢出" msgid "Text..." msgstr "文本…" -#: userland/apps/installer/main.cpp:476 -msgid "Thank You" -msgstr "感谢" - -#: userland/apps/installer/main.cpp:214 -msgid "Thanks" -msgstr "感谢" - #: userland/apps/oshlp/main.c:1208 msgid "The .hlp file could not be read." msgstr "无法读取 .hlp 文件。" @@ -12734,10 +12711,6 @@ msgstr "输入 INITIALIZE 以替换分区表:" msgid "Type INSTALL to enable the Install button." msgstr "输入 INSTALL 以启用安装按钮。" -#: userland/apps/installer/main.cpp:1033 -msgid "Type UPDATE to enable the Update button." -msgstr "输入 UPDATE 以启用更新按钮。" - #: userland/apps/reliefmail/window_actions.cpp:491 msgid "Type the account-bound Trash phrase." msgstr "输入与此账户绑定的废纸篓确认短语。" @@ -15554,9 +15527,6 @@ msgstr "工作进程错误" #~ msgid "System information (logo unavailable)" #~ msgstr "系统版本信息(Logo 不可用)" -#~ msgid "System version" -#~ msgstr "系统版本" - #~ msgid "Tar extract failed" #~ msgstr "Tar解压失败" @@ -16822,10 +16792,6 @@ msgstr "引导器:" msgid "Choose bootloaders" msgstr "选择引导方式" -#: userland/apps/installer/main.cpp:969 -msgid "Existing partitions are preserved. BIOS requires an unformatted BIOS Boot partition. Skipped existing bootloaders are preserved." -msgstr "保留现有分区。BIOS 引导需要未格式化的 BIOS Boot 分区。未选中的现有引导器会保留。" - #: userland/apps/installer/main.cpp:608 msgid "Manual layout: assign a FAT32 ESP and an ext4 root. BIOS Boot is not required for the selected bootloaders." msgstr "手动布局:指定 FAT32 ESP 和 ext4 根分区。当前引导选择不需要 BIOS Boot 分区。" @@ -16850,10 +16816,6 @@ msgstr "跳过" msgid "The automatic layout does not fit this disk. Select another disk or configure partitions manually." msgstr "此磁盘无法容纳自动方案。请选择其他磁盘或手动配置分区。" -#: userland/apps/installer/main.cpp:1006 -msgid "The initial selection matches the bootloaders found on this disk." -msgstr "默认选择与此磁盘上检测到的引导器一致。" - #: userland/apps/installer/main.cpp:707 msgid "Type AUTO to erase the disk and create ESP and root partitions:" msgstr "输入 AUTO 擦除此磁盘,并创建 ESP 和根分区:" @@ -17290,3 +17252,291 @@ msgstr "所选系统不支持交换空间" #: userland/apps/installer msgid "Swap requires an AHCI disk" msgstr "交换文件需要 AHCI 磁盘" + +#: userland/apps/installer +msgid "Checking update target" +msgstr "正在检查更新目标" + +#: userland/apps/installer +msgid "Administrator privileges are required" +msgstr "需要管理员权限" + +#: userland/apps/installer +msgid "Identifying the installed system" +msgstr "正在识别已安装的系统" + +#: userland/apps/installer +msgid "Checking installation media and free space" +msgstr "正在检查安装介质与可用空间" + +#: userland/apps/installer +msgid "Installation media is older than the installed system" +msgstr "安装介质的版本早于已安装系统" + +#: userland/apps/installer +msgid "Target or media changed. Check the update again." +msgstr "目标系统或安装介质已变化,请重新检查更新。" + +#: userland/apps/installer +msgid "Checking bootloader compatibility" +msgstr "正在检查引导器兼容性" + +#: userland/apps/installer +msgid "Verifying signed packages and dependencies" +msgstr "正在校验软件包签名与依赖" + +#: userland/apps/installer +msgid "Update checks completed" +msgstr "更新检查完成" + +#: userland/apps/installer +msgid "Preparing the installed system for update" +msgstr "正在准备更新已安装的系统" + +#: userland/apps/installer +msgid "Recovering the previous update attempt" +msgstr "正在恢复上一次未完成的更新" + +#: userland/apps/installer +msgid "Applying signed system packages" +msgstr "正在更新已签名的系统软件包" + +#: userland/apps/installer +msgid "Synchronizing the updated root filesystem" +msgstr "正在同步更新后的根文件系统" + +#: userland/apps/installer +msgid "Resuming bootloader publication" +msgstr "正在继续提交引导文件" + +#: userland/apps/installer +msgid "Publishing kernel and bootloader files" +msgstr "正在提交内核与引导文件" + +#: userland/apps/installer +msgid "Finishing the system update" +msgstr "正在完成系统更新" + +#: userland/apps/installer +msgid "System update completed successfully" +msgstr "系统更新成功" + +#: userland/apps/installer +msgid "Writing the update log" +msgstr "正在写入更新日志" + +#: userland/apps/installer +msgid "Update worker failed" +msgstr "后台更新进程执行失败" + +#: userland/apps/installer +msgid "Check Again" +msgstr "重新检查" + +#: userland/apps/installer +msgid "Check Update" +msgstr "检查更新" + +#: userland/apps/installer +msgid "Ready to update" +msgstr "可以开始更新" + +#: userland/apps/installer +msgid "Review the detected system and start the update." +msgstr "请核对识别到的系统信息,然后开始更新。" + +#: userland/apps/installer +msgid "Cannot start the update worker" +msgstr "无法启动后台更新进程" + +#: userland/apps/installer +msgid "Preparing the system update" +msgstr "正在准备系统更新" + +#: userland/apps/installer +msgid "Checking the selected system" +msgstr "正在检查所选系统" + +#: userland/apps/installer +msgid "Review system update" +msgstr "确认系统更新" + +#: userland/apps/installer +msgid "Media version" +msgstr "安装介质版本" + +#: userland/apps/installer +msgid "Target disk" +msgstr "目标磁盘" + +#: userland/apps/installer +msgid "Boot partition" +msgstr "引导分区" + +#: userland/apps/installer +msgid "Desktop environment" +msgstr "桌面环境" + +#: userland/apps/installer +msgid "Console" +msgstr "控制台" + +#: userland/apps/installer +msgid "Free / required space" +msgstr "可用/所需空间" + +#: userland/apps/installer +msgid "Update bootloader software" +msgstr "更新引导器程序" + +#: userland/apps/installer +msgid "Accounts, personal files, installed software and local configuration are preserved." +msgstr "保留账户、个人文件、已安装软件和本地配置。" + +#: userland/apps/installer +msgid "A previous update is incomplete. This attempt will verify the system and recover the pending update stages." +msgstr "上一次更新尚未完成。本次将重新校验系统,并恢复待完成的更新步骤。" + +#: userland/apps/installer +msgid "Signed packages are checked before changes. Boot files are published after the package transaction succeeds." +msgstr "写入前校验软件包签名,软件包事务成功后再提交引导文件。" + +#: userland/apps/installer +msgid "Update details" +msgstr "更新详情" + +#: userland/apps/installer +msgid "Checking system update" +msgstr "正在检查系统更新" + +#: userland/apps/installer +msgid "Review the details, then check the update again to retry." +msgstr "请查看详细日志,然后重新检查更新以重试。" + +#: userland/apps/installer +msgid "System version" +msgstr "系统版本" + +#: userland/apps/installer +msgid "Proceed with system update? [y/N]: " +msgstr "确认更新系统?[y/N] " + +#: configs/nls/messages.c:688 +msgid "Could not read the media file" +msgstr "无法读取媒体文件" + +#: configs/nls/messages.c:689 +msgid "Invalid or truncated media file" +msgstr "媒体文件无效或不完整" + +#: configs/nls/messages.c:691 +msgid "Media exceeds the supported size or resolution" +msgstr "媒体大小或分辨率超出支持范围" + +#: userland/apps/reliefplayer/window.cpp:181 +msgid "Media files (*.{mp3,MP3,wav,WAV,mp4,MP4})" +msgstr "媒体文件 (*.{mp3,MP3,wav,WAV,mp4,MP4})" + +#: userland/apps/reliefplayer/window.cpp:55 +msgid "Media playback failed" +msgstr "媒体播放失败" + +#: userland/apps/reliefplayer/window.cpp:272 +msgid "No audio track" +msgstr "无音轨" + +#: userland/apps/reliefplayer/window.cpp:85 +msgid "No media file selected" +msgstr "尚未选择媒体文件" + +#: userland/apps/reliefplayer/window.cpp:43 +msgid "Open an MP3, WAV or MP4 file to begin" +msgstr "打开 MP3、WAV 或 MP4 文件开始播放" + +#: userland/apps/reliefplayer/window.cpp:181 +msgid "Open media" +msgstr "打开媒体" + +#: userland/apps/reliefplayer/window.cpp:81 +msgid "Open media..." +msgstr "打开媒体…" + +#: configs/nls/messages.c:696 +msgid "Playback process stopped unexpectedly" +msgstr "播放进程意外退出" + +#: userland/apps/reliefplayer/window.cpp:45 +msgid "Reading media information..." +msgstr "正在读取媒体信息……" + +#: userland/apps/reliefplayer/window.cpp:330 +msgid "ReliefPlayer\nMP3 / WAV audio and MP4 video player\n\nCtrl+O: open media\nSpace: play or pause\nLeft / Right: seek 10 seconds\nHome: return to the beginning\n\nVolume changes affect this player only.\nSupports PCM WAV 8/16/24/32-bit and float32, mono or stereo.\nMP4: H.264, MPEG-4 or HEVC video; AAC or MP3 audio.\nLocal files only; no network streams or media library." +msgstr "ReliefPlayer\nMP3 / WAV 音频与 MP4 视频播放器\n\nCtrl+O:打开媒体\n空格:播放或暂停\n左右方向键:前后跳转 10 秒\nHome:回到开头\n\n音量只影响本程序。\n支持单/双声道 PCM WAV 8/16/24/32 位和 float32。\nMP4:H.264、MPEG-4 或 HEVC 视频;AAC 或 MP3 音轨。\n仅播放本地文件,不含网络串流或媒体库。" + +#: userland/apps/reliefplayer/window.cpp:221 +msgid "Stopping playback..." +msgstr "正在停止播放……" + +#: configs/nls/messages.c:690 +msgid "Unsupported media format or codec" +msgstr "不支持的媒体格式或编码" + +#: userland/apps/installer/installer_tty.c:435 +msgid "Automatic installation erases the selected disk." +msgstr "自动安装会擦除所选磁盘。" + +#: userland/apps/installer/main.cpp:691 +msgid "Automatically identify a single ReliefOS installation" +msgstr "自动识别唯一的 ReliefOS 安装" + +#: userland/apps/installer/main.cpp:449 +msgid "Cannot identify the selected partitions" +msgstr "无法识别所选分区" + +#: userland/apps/installer/main.cpp:765 +msgid "EFI system partition" +msgstr "EFI 系统分区" + +#: userland/apps/installer/installer_tty.c:381 +msgid "ESP partition number (blank: use fstab): " +msgstr "ESP 分区编号(留空:使用 fstab):" + +#: userland/apps/installer/main.cpp:1067 +msgid "Format selected ESP (erase all EFI boot files on it)" +msgstr "格式化所选 ESP(擦除其中所有 EFI 引导文件)" + +#: userland/apps/installer/installer_tty.c:437 +msgid "Only the selected root is formatted; existing ESP files are preserved." +msgstr "仅格式化所选根分区;保留现有 ESP 文件。" + +#: userland/apps/installer/main.cpp:1285 +msgid "Only the selected root will be formatted. Existing ESP files and other partitions are preserved." +msgstr "仅格式化所选根分区。保留现有 ESP 文件和其他分区。" + +#: userland/apps/installer/installer_tty.c:382 +msgid "ReliefOS root partition number (blank: auto): " +msgstr "ReliefOS 根分区编号(留空:自动识别):" + +#: userland/apps/installer/main.cpp:763 +msgid "ReliefOS system partition" +msgstr "ReliefOS 系统分区" + +#: userland/apps/installer/installer_tty.c:436 +msgid "The selected root and ESP will be formatted." +msgstr "将格式化所选根分区和 ESP。" + +#: userland/apps/installer/main.cpp:1284 +msgid "The selected root and ESP will be formatted. All EFI boot files on that ESP will be erased." +msgstr "将格式化所选根分区和 ESP,擦除该 ESP 上的所有 EFI 引导文件。" + +#: userland/apps/installer/main.cpp:1063 +msgid "The selected root is formatted. An existing FAT32 ESP is preserved by default." +msgstr "将格式化所选根分区。默认保留现有 FAT32 ESP。" + +#: userland/apps/installer/main.cpp:1378 +msgid "UEFI boot file (add it in firmware if no automatic entry appears):" +msgstr "UEFI 引导文件(若未自动出现入口,请在固件中添加):" + +#: userland/apps/installer/main.cpp:692 +msgid "Use the selected installation's fstab" +msgstr "使用所选安装的 fstab" diff --git a/kernel/reliefnt b/kernel/reliefnt index f1129c75..80edb7e4 160000 --- a/kernel/reliefnt +++ b/kernel/reliefnt @@ -1 +1 @@ -Subproject commit f1129c7582c8207114ef99e5ee17157ead980132 +Subproject commit 80edb7e421b20a45f37fc1eb5168175277245654 diff --git a/patches/musl/0002-report-unsupported-pi-mutex-protocol.patch b/patches/musl/0002-report-unsupported-pi-mutex-protocol.patch new file mode 100644 index 00000000..e32729a9 --- /dev/null +++ b/patches/musl/0002-report-unsupported-pi-mutex-protocol.patch @@ -0,0 +1,14 @@ +diff --git a/src/thread/pthread_mutexattr_setprotocol.c b/src/thread/pthread_mutexattr_setprotocol.c +--- a/src/thread/pthread_mutexattr_setprotocol.c ++++ b/src/thread/pthread_mutexattr_setprotocol.c +@@ -15,6 +15,10 @@ int pthread_mutexattr_setprotocol(pthread_mutexattr_t *a, int protocol) + if (r < 0) { + volatile int lk = 0; + r = -__syscall(SYS_futex, &lk, FUTEX_LOCK_PI, 0, 0); ++ /* A kernel without PI futexes cannot implement this POSIX ++ * protocol. Report the attribute capability, not a raw ++ * missing-syscall error, so callers can use PRIO_NONE. */ ++ if (r == ENOSYS) r = ENOTSUP; + a_store(&check_pi_result, r); + } + if (r) return r; diff --git a/resources/licenses/libpng-LICENSE b/resources/licenses/libpng-LICENSE new file mode 100644 index 00000000..1b765ae9 --- /dev/null +++ b/resources/licenses/libpng-LICENSE @@ -0,0 +1,134 @@ +COPYRIGHT NOTICE, DISCLAIMER, and LICENSE +========================================= + +PNG Reference Library License version 2 +--------------------------------------- + + * Copyright (c) 1995-2026 The PNG Reference Library Authors. + * Copyright (c) 2018-2026 Cosmin Truta. + * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. + * Copyright (c) 1996-1997 Andreas Dilger. + * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +The software is supplied "as is", without warranty of any kind, +express or implied, including, without limitation, the warranties +of merchantability, fitness for a particular purpose, title, and +non-infringement. In no event shall the Copyright owners, or +anyone distributing the software, be liable for any damages or +other liability, whether in contract, tort or otherwise, arising +from, out of, or in connection with the software, or the use or +other dealings in the software, even if advised of the possibility +of such damage. + +Permission is hereby granted to use, copy, modify, and distribute +this software, or portions hereof, for any purpose, without fee, +subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you + use this software in a product, an acknowledgment in the product + documentation would be appreciated, but is not required. + + 2. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + + +PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) +----------------------------------------------------------------------- + +libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are +Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are +derived from libpng-1.0.6, and are distributed according to the same +disclaimer and license as libpng-1.0.6 with the following individuals +added to the list of Contributing Authors: + + Simon-Pierre Cadieux + Eric S. Raymond + Mans Rullgard + Cosmin Truta + Gilles Vollant + James Yu + Mandar Sahastrabuddhe + Google Inc. + Vadim Barkov + +and with the following additions to the disclaimer: + + There is no warranty against interference with your enjoyment of + the library or against infringement. There is no warranty that our + efforts or the library will fulfill any of your particular purposes + or needs. This library is provided with all faults, and the entire + risk of satisfactory quality, performance, accuracy, and effort is + with the user. + +Some files in the "contrib" directory and some configure-generated +files that are distributed with libpng have other copyright owners, and +are released under other open source licenses. + +libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are +Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from +libpng-0.96, and are distributed according to the same disclaimer and +license as libpng-0.96, with the following individuals added to the +list of Contributing Authors: + + Tom Lane + Glenn Randers-Pehrson + Willem van Schaik + +libpng versions 0.89, June 1996, through 0.96, May 1997, are +Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, +and are distributed according to the same disclaimer and license as +libpng-0.88, with the following individuals added to the list of +Contributing Authors: + + John Bowler + Kevin Bracey + Sam Bushell + Magnus Holmgren + Greg Roelofs + Tom Tanner + +Some files in the "scripts" directory have other copyright owners, +but are released under this license. + +libpng versions 0.5, May 1995, through 0.88, January 1996, are +Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +For the purposes of this copyright and license, "Contributing Authors" +is defined as the following set of individuals: + + Andreas Dilger + Dave Martindale + Guy Eric Schalnat + Paul Schmidt + Tim Wegner + +The PNG Reference Library is supplied "AS IS". The Contributing +Authors and Group 42, Inc. disclaim all warranties, expressed or +implied, including, without limitation, the warranties of +merchantability and of fitness for any purpose. The Contributing +Authors and Group 42, Inc. assume no liability for direct, indirect, +incidental, special, exemplary, or consequential damages, which may +result from the use of the PNG Reference Library, even if advised of +the possibility of such damage. + +Permission is hereby granted to use, copy, modify, and distribute this +source code, or portions hereof, for any purpose, without fee, subject +to the following restrictions: + + 1. The origin of this source code must not be misrepresented. + + 2. Altered versions must be plainly marked as such and must not + be misrepresented as being the original source. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + +The Contributing Authors and Group 42, Inc. specifically permit, +without fee, and encourage the use of this source code as a component +to supporting the PNG file format in commercial products. If you use +this source code in a product, acknowledgment is not required but would +be appreciated. diff --git a/resources/licenses/mbedtls3-LICENSE b/resources/licenses/mbedtls3-LICENSE new file mode 100644 index 00000000..776ac77e --- /dev/null +++ b/resources/licenses/mbedtls3-LICENSE @@ -0,0 +1,553 @@ +Mbed TLS files are provided under a dual [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) +OR [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) license. +This means that users may choose which of these licenses they take the code +under. + +The full text of each of these licenses is given below. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +=============================================================================== + + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/resources/licenses/ncurses-LICENSE b/resources/licenses/ncurses-LICENSE new file mode 100644 index 00000000..6182a3bc --- /dev/null +++ b/resources/licenses/ncurses-LICENSE @@ -0,0 +1,29 @@ +Copyright 2018-2024,2025 Thomas E. Dickey +Copyright 1998-2017,2018 Free Software Foundation, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, distribute with modifications, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE ABOVE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR +THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name(s) of the above copyright +holders shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without prior written +authorization. + +-- vile:txtmode fc=72 +-- $Id: COPYING,v 1.14 2025/01/04 10:53:46 tom Exp $ diff --git a/resources/licenses/sl-LICENSE b/resources/licenses/sl-LICENSE new file mode 100644 index 00000000..19b1c72c --- /dev/null +++ b/resources/licenses/sl-LICENSE @@ -0,0 +1,6 @@ +Copyright 1993,1998,2014 Toyoda Masashi (mtoyoda@acm.org) + +Everyone is permitted to do anything on this program including copying, +modifying, and improving, unless you try to pretend that you wrote it. +i.e., the above copyright notice has to appear in all copies. +THE AUTHOR DISCLAIMS ANY RESPONSIBILITY WITH REGARD TO THIS SOFTWARE. diff --git a/resources/licenses/sqlite-LICENSE b/resources/licenses/sqlite-LICENSE new file mode 100644 index 00000000..f68a6c17 --- /dev/null +++ b/resources/licenses/sqlite-LICENSE @@ -0,0 +1,6 @@ +The author disclaims copyright to this source code. In place of +a legal notice, here is a blessing: + + * May you do good and not evil. + * May you find forgiveness for yourself and forgive others. + * May you share freely, never taking more than you give. diff --git a/resources/licenses/zlib-LICENSE b/resources/licenses/zlib-LICENSE new file mode 100644 index 00000000..b7a69d05 --- /dev/null +++ b/resources/licenses/zlib-LICENSE @@ -0,0 +1,22 @@ +Copyright notice: + + (C) 1995-2026 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 4261b73b..576ca612 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -145,11 +145,6 @@ if [ -f scripts/kconfig/conf.c ] && [ -f scripts/Makefile.build ]; then else missing 'scripts/Makefile.build or scripts/kconfig/conf.c' fi -if [ -f third_party/zlib/contrib/puff/puff.c ]; then - reliefos_log OK 'zlib reference inflate (contrib/puff) present' -else - missing 'third_party/zlib/contrib/puff/puff.c' -fi # Which upstream sources the build needs is the lock file's job, so doctor asks # it rather than keeping a second, easily stale list here. lock=${LOCK:-} diff --git a/scripts/reliefos/Makefile.apk b/scripts/reliefos/Makefile.apk index 3c2c2a5d..c4bb7b00 100644 --- a/scripts/reliefos/Makefile.apk +++ b/scripts/reliefos/Makefile.apk @@ -8,10 +8,10 @@ APK_REPOSITORY := $(APK_WORK)/repository APK_MANIFEST := $(APK_WORK)/manifest.json APK_OWNERSHIP := $(APK_WORK)/ownership.tsv APK_SIGNING_KEY ?= $(HOME)/.local/share/leonos/apk-signing/key.pem -# Generation 2 supersedes the generation-1 BusyBox/binutils ownership regression, -# including targets partially updated by those media. Published releases must use -# an increasing SOURCE_DATE_EPOCH (or an explicit increasing version prefix). -APK_BUILD_VERSION ?= 2.$(SOURCE_DATE_EPOCH)-r0 +# Generation 4 removes the terminal/SQLite wrapper packages entirely. +# This sorts after generation 3 even for development builds at the same commit. +# Published releases must still use an increasing SOURCE_DATE_EPOCH. +APK_BUILD_VERSION ?= 4.$(SOURCE_DATE_EPOCH)-r0 APK_STAGE_SCRIPT := $(RELIEFOS_SRC)/tools/build/apk-stage.sh APK_STAGE_INPUTS := $(RELIEFOS_SRC)/tools/build/apk-layout.sh $(RELIEFOS_LAYOUT_TOOL) $(RELIEFOS_LOCK) $(RELIEFOS_DEPS_TOOL) BUILD_LOG_INPUTS := $(RELIEFOS_SHELL_LOG) $(RELIEFOS_SRC)/tools/build/run-logged.sh $(RELIEFOS_SRC)/tools/build/format-log.awk @@ -26,7 +26,7 @@ $(APK_MANAGED_ROOT)/.apk-complete $(APK_REPOSITORY)/packages.adb $(APK_MANIFEST) $(APK_RAW_STAMP) $(APK_STAGE_SCRIPT) $(APK_STAGE_INPUTS) $(RELIEFOS_APK_OWN) $(UPSTREAM_APK) \ $(UPSTREAM_APK_ROOT)/.complete $(RELIEFOS_SRC)/configs/apk-ownership.json \ $(RELIEFOS_SRC)/resources/licenses/apk-tools-LICENSE $(MUSL_STAMP) \ - $(RELIEFOS_SRC)/userland/storage/leonos-apk-update \ + $(RELIEFOS_SRC)/userland/storage/reliefos-apk-update \ $(RELIEFOS_SRC)/userland/storage/busybox-binutils-links \ $(RELIEFOS_SRC)/system/rootfs/etc/apk/protected_paths.d/leonos.list \ $(RELIEFOS_SRC)/system/rootfs/etc/apk/protected_paths.d/reliefos.list \ diff --git a/scripts/reliefos/Makefile.apk-dependencies b/scripts/reliefos/Makefile.apk-dependencies new file mode 100644 index 00000000..863bdbe2 --- /dev/null +++ b/scripts/reliefos/Makefile.apk-dependencies @@ -0,0 +1,44 @@ +# Binary runtime dependencies are staged before compiling their consumers. +UPSTREAM_ROOT := $(O)/upstream +UPSTREAM_APK_ROOT := $(UPSTREAM_ROOT)/apk +UPSTREAM_APK := $(UPSTREAM_APK_ROOT)/apk.static +$(UPSTREAM_APK) $(UPSTREAM_APK_ROOT)/.complete &: $(RELIEFOS_SRC)/tools/build/upstream-apk.sh $(RELIEFOS_LOCK) $(RELIEFOS_DEPS_TOOL) $(wildcard $(RELIEFOS_SRC)/system/rootfs/etc/apk/keys/*) + $(Q)sh $(RELIEFOS_SRC)/tools/build/upstream-apk.sh $(RELIEFOS_SRC) $(abspath $(RELIEFOS_DEPS_TOOL)) $(RELIEFOS_LOCK) $(RELIEFOS_CACHE) $(abspath $(UPSTREAM_APK_ROOT)) +.PHONY: upstream-apk +upstream-apk: $(UPSTREAM_APK) $(UPSTREAM_APK_ROOT)/.complete + + +RUNTIME_DEVELOPMENT_ROOT := $(UPSTREAM_ROOT)/runtime-development +RUNTIME_DEP_LIBDIR := $(RUNTIME_DEVELOPMENT_ROOT)/usr/lib +RUNTIME_DEP_SHARED := $(addprefix $(RUNTIME_DEP_LIBDIR)/,libpng.so libz.so libmbedtls.so libmbedx509.so libmbedcrypto.so) +RUNTIME_DEP_STATIC := $(addprefix $(RUNTIME_DEP_LIBDIR)/,libpng.a libz.a libmbedtls.a libmbedx509.a libmbedcrypto.a) +RUNTIME_DEVELOPMENT_PRODUCTS := $(addprefix $(RUNTIME_DEVELOPMENT_ROOT)/usr/include/,zlib.h zconf.h png.h pngconf.h pnglibconf.h sqlite3.h mbedtls/ssl.h mbedtls/mbedtls_config.h) $(RUNTIME_DEP_SHARED) $(RUNTIME_DEP_STATIC) $(RUNTIME_DEP_LIBDIR)/libsqlite3.so $(RUNTIME_DEP_LIBDIR)/libsqlite3.a +$(RUNTIME_DEVELOPMENT_ROOT)/.complete $(RUNTIME_DEVELOPMENT_PRODUCTS) &: $(RELIEFOS_SRC)/tools/build/runtime-development.sh $(RELIEFOS_LOCK) $(RELIEFOS_DEPS_TOOL) $(UPSTREAM_APK) $(addprefix $(RELIEFOS_SRC)/resources/licenses/,zlib-LICENSE libpng-LICENSE sqlite-LICENSE mbedtls3-LICENSE) + $(Q)sh $(RELIEFOS_SRC)/tools/build/runtime-development.sh $(RELIEFOS_SRC) $(abspath $(RELIEFOS_DEPS_TOOL)) $(RELIEFOS_LOCK) $(RELIEFOS_CACHE) $(abspath $(UPSTREAM_APK)) $(abspath $(RUNTIME_DEVELOPMENT_ROOT)) + $(Q)touch $(RUNTIME_DEVELOPMENT_PRODUCTS) +$(O_META)/runtime-development-present.parameters: FORCE $(RELIEFOS_SRC)/tools/build/upstream-verify.sh + $(Q)sh $(RELIEFOS_SRC)/tools/build/upstream-verify.sh $(RUNTIME_DEVELOPMENT_ROOT) $(RUNTIME_DEVELOPMENT_ROOT).files $@ +$(RUNTIME_DEVELOPMENT_ROOT)/.complete $(RUNTIME_DEVELOPMENT_PRODUCTS): $(O_META)/runtime-development-present.parameters +.PHONY: runtime-development +runtime-development: $(RUNTIME_DEVELOPMENT_ROOT)/.complete $(RUNTIME_DEVELOPMENT_PRODUCTS) +reliefos-upstream: upstream-apk +export RUNTIME_DEVELOPMENT_ROOT RUNTIME_DEP_SHARED RUNTIME_DEP_STATIC + +# Retain public build goal names while their payload comes from verified APKs. +.PHONY: upstream-ncurses upstream-app-sl upstream-app-sqlite +upstream-ncurses upstream-app-sl: upstream-apk +upstream-app-sqlite: upstream-apk runtime-development + +# Source-based sanitizer tests need host-libc libraries. Musl APK archives must +# never be linked into glibc tests (SQLite's libc internals differ). +HOST_RUNTIME_DEVELOPMENT_ROOT := $(O_HOST)/runtime-development +HOST_RUNTIME_DEVELOPMENT_PRODUCTS := $(addprefix $(HOST_RUNTIME_DEVELOPMENT_ROOT)/usr/lib/,libmbedtls.a libmbedx509.a libmbedcrypto.a libsqlite3.a) +RELIEFOS_PARAMETERS_host-runtime-development := cc=$(HOSTCC) +$(if $(RELIEFOS_PASSIVE),,$(eval $(call KBUILD_PARAMETERS,host-runtime-development))) +$(HOST_RUNTIME_DEVELOPMENT_ROOT)/.complete $(HOST_RUNTIME_DEVELOPMENT_PRODUCTS) &: $(O_META)/host-runtime-development.parameters $(RELIEFOS_SRC)/tools/build/host-runtime-development.sh $(RELIEFOS_UPSTREAM_MAKE_ENV) $(RELIEFOS_LOCK) $(RELIEFOS_DEPS_TOOL) $(RUNTIME_DEVELOPMENT_PRODUCTS) + +$(Q)sh $(RELIEFOS_SRC)/tools/build/host-runtime-development.sh $(abspath $(RELIEFOS_DEPS_TOOL)) $(RELIEFOS_LOCK) $(RELIEFOS_CACHE) $(abspath $(RUNTIME_DEVELOPMENT_ROOT)) $(abspath $(O_HOST))/runtime-development-work $(abspath $(HOST_RUNTIME_DEVELOPMENT_ROOT)) '$(HOSTCC)' +$(O_META)/host-runtime-development-present.parameters: FORCE $(RELIEFOS_SRC)/tools/build/upstream-verify.sh + $(Q)sh $(RELIEFOS_SRC)/tools/build/upstream-verify.sh $(HOST_RUNTIME_DEVELOPMENT_ROOT) $(HOST_RUNTIME_DEVELOPMENT_ROOT).files $@ +$(HOST_RUNTIME_DEVELOPMENT_ROOT)/.complete $(HOST_RUNTIME_DEVELOPMENT_PRODUCTS): $(O_META)/host-runtime-development-present.parameters +.PHONY: host-runtime-development +host-runtime-development: $(HOST_RUNTIME_DEVELOPMENT_ROOT)/.complete $(HOST_RUNTIME_DEVELOPMENT_PRODUCTS) diff --git a/scripts/reliefos/Makefile.images b/scripts/reliefos/Makefile.images index d5eac56a..ef025f87 100644 --- a/scripts/reliefos/Makefile.images +++ b/scripts/reliefos/Makefile.images @@ -1,5 +1,5 @@ # Host tools own the disk formats; Make owns each image dependency. -# Both firmware platforms use the locked GRUB 2.12 release. An explicit +# Both firmware platforms use the locked GRUB release. An explicit # GRUB_EFI_DIR may select another complete compatible platform input tree. GRUB_EFI_DIR ?= $(GRUB_EFI_ROOT)/lib/grub/x86_64-efi GRUB_EFI_INPUTS := $(wildcard $(GRUB_EFI_DIR)/*) @@ -7,7 +7,7 @@ GRUB_EFI_INPUTS := $(wildcard $(GRUB_EFI_DIR)/*) # wildcard drops absent files, so list these paths explicitly to make a missing # required prebuilt module fail instead of falling through to Kbuild's generic # $(obj)/%.mod rule. -GRUB_EFI_REQUIRED_MODULES := part_gpt fat iso9660 multiboot2 normal cmp search search_fs_file regexp configfile echo serial terminal video video_bochs video_cirrus efi_gop all_video font gfxterm gfxmenu hashsum gcry_sha256 crypto loadenv test +GRUB_EFI_REQUIRED_MODULES := part_gpt part_msdos ext2 chain ntfs linux gzio reboot search_fs_uuid search_label fat iso9660 multiboot2 normal cmp search search_fs_file regexp configfile echo serial terminal video video_bochs video_cirrus efi_gop all_video font gfxterm gfxmenu hashsum gcry_sha256 crypto loadenv test GRUB_EFI_REQUIRED_INPUTS := $(addprefix $(GRUB_EFI_DIR)/,$(addsuffix .mod,$(GRUB_EFI_REQUIRED_MODULES))) GRUB_EFI_PROTECTED_INPUTS := $(sort $(GRUB_EFI_INPUTS) $(GRUB_EFI_REQUIRED_INPUTS)) # These are prebuilt image inputs, not Kbuild outputs. Explicit file rules with @@ -30,15 +30,20 @@ RELIEFOS_EXT4_TIME := $(RELIEFOS_EXT2_TIME) ESP_STAGE := $(O_STAGE)/esp ESP_STAMP := $(O_STAGE)/esp.complete IMAGE_DISPLAY := $(O_GENERATED)/config/display.conf +IMAGE_INSTANCE_ID ?= $(or $(subst ",,$(CONFIG_DISK_INSTANCE_ID)),reliefos) +RELIEFOS_IMAGE_IDENTITY := $(RELIEFOS_SRC)/tools/build/image-identity.sh +RELIEFOS_IMAGE_INSTANCE_QUOTED = '$(subst ','"'"',$(IMAGE_INSTANCE_ID))' +LIVE_ROOT_UUID := $(shell RELIEFOS_IMAGE_INSTANCE=live sh $(RELIEFOS_IMAGE_IDENTITY) root) +DISK_ROOT_UUID := $(shell RELIEFOS_IMAGE_INSTANCE=$(RELIEFOS_IMAGE_INSTANCE_QUOTED) sh $(RELIEFOS_IMAGE_IDENTITY) root) $(IMAGE_DISPLAY): $(RELIEFOS_CONFIG_FILE) $(RELIEFOS_EMIT) $(Q)mkdir -p $(@D) $(Q)awk 'BEGIN{theme="metro"} /^CONFIG_VMDK_DEFAULT_THEME_WIN95=y$$/{theme="win95"} END{print "theme="theme}' $< | $(RELIEFOS_EMIT) --input - --output $@ RELIEFOS_EXT4_FEATURES := none,filetype,extents,dir_index,metadata_csum,64bit,flex_bg,has_journal,large_file,huge_file,extra_isize -RELIEFOS_PARAMETERS_images := epoch=$(SOURCE_DATE_EPOCH)|boot=gpt-bios-uefi|root-fs=ext4|ext4-features=$(RELIEFOS_EXT4_FEATURES)|grub=$(GRUB_EFI_DIR)|bios=$(GRUB_BIOS_DIR)|grub-tool=$(GRUB_MKSTANDALONE)|grub-mkimage=$(GRUB_MKIMAGE)|grub-bios-setup=$(GRUB_BIOS_SETUP)|mke2fs=$(shell mke2fs -V 2>&1 | head -n1)|mtools=$(shell mcopy -V 2>&1 | head -n1)|disk-size=$(CONFIG_IMAGE_SIZE_MIB)|installer-size=$(CONFIG_INSTALLER_ROOT_SIZE_MIB)|qemu-img=$(shell qemu-img --version 2>/dev/null | head -n1)|xorriso=$(shell xorriso -version 2>/dev/null | head -n1)|sfdisk=$(shell sfdisk --version 2>/dev/null) +RELIEFOS_PARAMETERS_images := instance=$(IMAGE_INSTANCE_ID)|epoch=$(SOURCE_DATE_EPOCH)|boot=gpt-bios-uefi|root-fs=ext4|ext4-features=$(RELIEFOS_EXT4_FEATURES)|grub=$(GRUB_EFI_DIR)|bios=$(GRUB_BIOS_DIR)|grub-tool=$(GRUB_MKSTANDALONE)|grub-mkimage=$(GRUB_MKIMAGE)|grub-bios-setup=$(GRUB_BIOS_SETUP)|mke2fs=$(shell mke2fs -V 2>&1 | head -n1)|mtools=$(shell mcopy -V 2>&1 | head -n1)|disk-size=$(CONFIG_IMAGE_SIZE_MIB)|installer-size=$(CONFIG_INSTALLER_ROOT_SIZE_MIB)|qemu-img=$(shell qemu-img --version 2>/dev/null | head -n1)|xorriso=$(shell xorriso -version 2>/dev/null | head -n1)|sfdisk=$(shell sfdisk --version 2>/dev/null) $(if $(RELIEFOS_PASSIVE),,$(eval $(call KBUILD_PARAMETERS,images))) $(ESP_STAMP): $(RELIEFOS_INITRAMFS) $(RELIEFOS_INITRAMFS_MANIFEST) $(RELIEFOS_KERNEL_TOOLS) $(ESP_STAMP): $(RELIEFOS_SRC)/boot/grub/kernel-slots.cfg -$(ESP_STAMP): $(O_GENERATED)/system/kernel.sys $(GRUB_FONT) $(IMAGE_DISPLAY) $(RELIEFOS_SRC)/tools/build/boot-stage.sh $(RELIEFOS_SRC)/boot/grub/bios-modules.list $(RELIEFOS_SRC)/boot/grub/embedded.cfg $(RELIEFOS_SRC)/boot/grub/grub.cfg $(RELIEFOS_SRC)/boot/grub/theme/theme.txt $(GRUB_EFI_INPUTS) $(GRUB_EFI_REQUIRED_INPUTS) $(GRUB_BIOS_INPUTS) $(GRUB_BIOS_REQUIRED_INPUTS) $(GRUB_HOST_TOOLS) $(GRUB_HOST_PRODUCTS) $(GRUB_EFI_PRODUCTS) $(O_META)/images.parameters +$(ESP_STAMP): $(O_GENERATED)/system/kernel.sys $(GRUB_FONT) $(IMAGE_DISPLAY) $(RELIEFOS_SRC)/tools/build/boot-stage.sh $(RELIEFOS_SRC)/boot/grub/bios-modules.list $(RELIEFOS_SRC)/boot/grub/embedded.cfg $(RELIEFOS_SRC)/boot/grub/grub.cfg $(RELIEFOS_SRC)/boot/grub/kernel-slots.cfg $(RELIEFOS_SRC)/boot/grub/other-systems.cfg $(RELIEFOS_SRC)/boot/grub/theme/theme.txt $(GRUB_EFI_INPUTS) $(GRUB_EFI_REQUIRED_INPUTS) $(GRUB_BIOS_INPUTS) $(GRUB_BIOS_REQUIRED_INPUTS) $(GRUB_HOST_TOOLS) $(GRUB_HOST_PRODUCTS) $(GRUB_EFI_PRODUCTS) $(O_META)/images.parameters $(Q)GRUB_MKSTANDALONE='$(GRUB_MKSTANDALONE)' RELIEFOS_KERNEL_TOOLS='$(abspath $(RELIEFOS_KERNEL_TOOLS))' sh $(RELIEFOS_SRC)/tools/build/boot-stage.sh $(RELIEFOS_SRC) $(GRUB_EFI_DIR) $(GRUB_BIOS_DIR) $(O_GENERATED)/system/kernel.sys $(RELIEFOS_INITRAMFS) $(GRUB_FONT) $(IMAGE_DISPLAY) $(ESP_STAGE) $(SOURCE_DATE_EPOCH) $(Q)touch $@ $(Q)sh $(RELIEFOS_SRC)/tools/build/stage-inventory.sh write $(ESP_STAGE) $(O_META)/esp.files @@ -57,8 +62,8 @@ DISK_ROOT_EXT4 := $(O_IMAGES)/disk-root.ext4 LIVE_ROOT_STAGE := $(O_STAGE)/live-root DISK_ROOT_STAGE := $(O_STAGE)/disk-root define RELIEFOS_STANDALONE_STAGE -$(O_STAGE)/$(1)-root.complete: $(APK_MANAGED_ROOT)/.apk-complete $(RELIEFOS_SRC)/tools/build/standalone-root.sh $(wildcard $(RELIEFOS_SRC)/system/test-accounts/*) $(RELIEFOS_CONFIG_FILE) - $$(Q)sh $$(RELIEFOS_SRC)/tools/build/standalone-root.sh $$(RELIEFOS_SRC) $$(APK_MANAGED_ROOT) $$(O_STAGE)/$(1)-root $(1) '$$(subst ",,$(CONFIG_VMDK_DEFAULT_LANG))' +$(O_STAGE)/$(1)-root.complete: $(APK_MANAGED_ROOT)/.apk-complete $(RELIEFOS_SRC)/tools/build/standalone-root.sh $(wildcard $(RELIEFOS_SRC)/system/test-accounts/*) $(RELIEFOS_CONFIG_FILE) $$(RELIEFOS_IMAGE_IDENTITY) $$(O_META)/images.parameters + $$(Q)RELIEFOS_IMAGE_INSTANCE=$$(RELIEFOS_IMAGE_INSTANCE_QUOTED) sh $$(RELIEFOS_SRC)/tools/build/standalone-root.sh $$(RELIEFOS_SRC) $$(APK_MANAGED_ROOT) $$(O_STAGE)/$(1)-root $(1) '$$(subst ",,$(CONFIG_VMDK_DEFAULT_LANG))' $$(Q)touch $$@ $$(Q)sh $$(RELIEFOS_SRC)/tools/build/stage-inventory.sh write $$(O_STAGE)/$(1)-root $$(O_META)/$(1)-root.files endef @@ -75,13 +80,13 @@ INSTALLER_ISO := $(O_IMAGES)/reliefos-installer.iso $(LIVE_ISO) $(INSTALLER_ISO): $(BUILD_LOG_INPUTS) $(INSTALLER_STAGE_STAMP): $(USERLAND_DIR)/installer.elf $(ROOT_EXT2): $(O_STAGE)/live-root.complete $(RELIEFOS_EXT2_TIME) $(RELIEFOS_SRC)/tools/build/images.sh $(O_META)/images.parameters - $(Q)RELIEFOS_EXT2_TIME=$(RELIEFOS_EXT2_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext2 $(LIVE_ROOT_STAGE) $@ $(SOURCE_DATE_EPOCH) 5c13543b-732c-4f81-8652-621124484420 + $(Q)RELIEFOS_EXT2_TIME=$(RELIEFOS_EXT2_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext2 $(LIVE_ROOT_STAGE) $@ $(SOURCE_DATE_EPOCH) $(LIVE_ROOT_UUID) $(DISK_ROOT_EXT2): $(O_STAGE)/disk-root.complete $(RELIEFOS_EXT2_TIME) $(RELIEFOS_SRC)/tools/build/images.sh $(O_META)/images.parameters - $(Q)RELIEFOS_EXT2_TIME=$(RELIEFOS_EXT2_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext2 $(DISK_ROOT_STAGE) $@ $(SOURCE_DATE_EPOCH) 5c13543b-732c-4f81-8652-621124484420 + $(Q)RELIEFOS_EXT2_TIME=$(RELIEFOS_EXT2_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext2 $(DISK_ROOT_STAGE) $@ $(SOURCE_DATE_EPOCH) $(DISK_ROOT_UUID) $(ROOT_EXT4): $(O_STAGE)/live-root.complete $(RELIEFOS_EXT4_TIME) $(RELIEFOS_SRC)/tools/build/images.sh $(O_META)/images.parameters - $(Q)RELIEFOS_EXT4_TIME=$(RELIEFOS_EXT4_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext4 $(LIVE_ROOT_STAGE) $@ $(SOURCE_DATE_EPOCH) 5c13543b-732c-4f81-8652-621124484420 + $(Q)RELIEFOS_EXT4_TIME=$(RELIEFOS_EXT4_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext4 $(LIVE_ROOT_STAGE) $@ $(SOURCE_DATE_EPOCH) $(LIVE_ROOT_UUID) $(DISK_ROOT_EXT4): $(O_STAGE)/disk-root.complete $(RELIEFOS_EXT4_TIME) $(RELIEFOS_SRC)/tools/build/images.sh $(O_META)/images.parameters - $(Q)RELIEFOS_EXT4_TIME=$(RELIEFOS_EXT4_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext4 $(DISK_ROOT_STAGE) $@ $(SOURCE_DATE_EPOCH) 5c13543b-732c-4f81-8652-621124484420 + $(Q)RELIEFOS_EXT4_TIME=$(RELIEFOS_EXT4_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext4 $(DISK_ROOT_STAGE) $@ $(SOURCE_DATE_EPOCH) $(DISK_ROOT_UUID) RELIEFOS_DEDUP_TOOL := $(RELIEFOS_HOST_BIN)/reliefos-dedup tools: $(RELIEFOS_DEDUP_TOOL) $(INSTALLER_STAGE_STAMP): $(RELIEFOS_DEDUP_TOOL) $(APK_STAGE_INPUTS) @@ -94,8 +99,8 @@ $(INSTALLER_EXT2): $(INSTALLER_STAGE_STAMP) $(RELIEFOS_EXT2_TIME) $(RELIEFOS_SRC $(Q)IMAGE_MIN_MIB=$(or $(CONFIG_INSTALLER_ROOT_SIZE_MIB),400) RELIEFOS_EXT2_TIME=$(RELIEFOS_EXT2_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext2 $(INSTALLER_ROOT) $@ $(SOURCE_DATE_EPOCH) 9e1f6d46-3f41-42d2-9917-9a21a2134401 $(INSTALLER_EXT4): $(INSTALLER_STAGE_STAMP) $(RELIEFOS_EXT4_TIME) $(RELIEFOS_SRC)/tools/build/images.sh $(O_META)/images.parameters $(Q)IMAGE_MIN_MIB=$(or $(CONFIG_INSTALLER_ROOT_SIZE_MIB),400) RELIEFOS_EXT4_TIME=$(RELIEFOS_EXT4_TIME) sh $(RELIEFOS_SRC)/tools/build/images.sh ext4 $(INSTALLER_ROOT) $@ $(SOURCE_DATE_EPOCH) 9e1f6d46-3f41-42d2-9917-9a21a2134401 -$(DISK_RAW) $(DISK_VMDK) &: $(DISK_ROOT_EXT4) $(ESP_STAMP) $(RELIEFOS_SRC)/tools/build/disk.sh $(RELIEFOS_SRC)/boot/grub/bios-modules.list $(GRUB_BIOS_INPUTS) $(GRUB_BIOS_REQUIRED_INPUTS) $(GRUB_HOST_TOOLS) $(O_META)/images.parameters - $(Q)IMAGE_MIN_MIB=$(or $(CONFIG_IMAGE_SIZE_MIB),1024) sh $(RELIEFOS_SRC)/tools/build/disk.sh $(ESP_STAGE) $(DISK_ROOT_EXT4) $(DISK_RAW) $(DISK_VMDK) $(SOURCE_DATE_EPOCH) $(GRUB_BIOS_DIR) $(GRUB_MKIMAGE) $(GRUB_BIOS_SETUP) +$(DISK_RAW) $(DISK_VMDK) &: $(DISK_ROOT_EXT4) $(ESP_STAMP) $(RELIEFOS_SRC)/tools/build/disk.sh $(RELIEFOS_IMAGE_IDENTITY) $(RELIEFOS_SRC)/boot/grub/bios-modules.list $(GRUB_BIOS_INPUTS) $(GRUB_BIOS_REQUIRED_INPUTS) $(GRUB_HOST_TOOLS) $(O_META)/images.parameters + $(Q)RELIEFOS_IMAGE_INSTANCE=$(RELIEFOS_IMAGE_INSTANCE_QUOTED) IMAGE_MIN_MIB=$(or $(CONFIG_IMAGE_SIZE_MIB),1024) sh $(RELIEFOS_SRC)/tools/build/disk.sh $(ESP_STAGE) $(DISK_ROOT_EXT4) $(DISK_RAW) $(DISK_VMDK) $(SOURCE_DATE_EPOCH) $(GRUB_BIOS_DIR) $(GRUB_MKIMAGE) $(GRUB_BIOS_SETUP) $(LIVE_ISO): $(ROOT_EXT4) $(ESP_STAMP) $(RELIEFOS_SRC)/tools/build/iso.sh $(RELIEFOS_SRC)/boot/grub/live.cfg $(RELIEFOS_SRC)/boot/grub/installer_embedded.cfg $(RELIEFOS_SRC)/boot/grub/bios-modules.list $(GRUB_BIOS_INPUTS) $(GRUB_BIOS_REQUIRED_INPUTS) $(GRUB_HOST_TOOLS) $(O_META)/images.parameters $(Q)GRUB_MKSTANDALONE='$(GRUB_MKSTANDALONE)' sh $(RELIEFOS_SRC)/tools/build/iso.sh $(RELIEFOS_SRC) $(GRUB_EFI_DIR) $(ESP_STAGE) $(ROOT_EXT4) $(RELIEFOS_SRC)/boot/grub/live.cfg $@ $(SOURCE_DATE_EPOCH) RELIEFOSLIVE $(GRUB_BIOS_DIR) $(GRUB_MKIMAGE) $(INSTALLER_ISO): $(INSTALLER_EXT4) $(ESP_STAMP) $(RELIEFOS_SRC)/tools/build/iso.sh $(RELIEFOS_SRC)/boot/grub/installer.cfg $(RELIEFOS_SRC)/boot/grub/installer_embedded.cfg $(RELIEFOS_SRC)/boot/grub/bios-modules.list $(GRUB_BIOS_INPUTS) $(GRUB_BIOS_REQUIRED_INPUTS) $(GRUB_HOST_TOOLS) $(O_META)/images.parameters diff --git a/scripts/reliefos/Makefile.rootfs b/scripts/reliefos/Makefile.rootfs index 548f31e6..ee6aa532 100644 --- a/scripts/reliefos/Makefile.rootfs +++ b/scripts/reliefos/Makefile.rootfs @@ -9,13 +9,13 @@ APK_RAW_STAMP := $(ROOTFS_RAW_STAMP) ROOTFS_SOURCE_DIRS := $(RELIEFOS_SRC)/system $(RELIEFOS_SRC)/resources/build-art $(RELIEFOS_SRC)/userland/storage $(RELIEFOS_SRC)/userland/apps ROOTFS_SOURCE_DIRS += $(RELIEFOS_SRC)/resources/reliefmath $(RELIEFOS_SRC)/resources/licenses/reliefmath ROOTFS_SOURCE_DIRS += $(RELIEFOS_SRC)/userland/fastfetch $(RELIEFOS_SRC)/logo.png $(RELIEFOS_SRC)/test/test.mp3 $(RELIEFOS_SRC)/Documentation/admin-guide/packages.rst $(RELIEFOS_SRC)/configs/apk-ownership.json -ROOTFS_SOURCE_DIRS += $(wildcard $(addprefix $(RELIEFOS_SRC)/third_party/,busybox/LICENSE sl/LICENSE pl_editor/LICENSE)) -ROOTFS_SOURCE_DIRS += $(RELIEFOS_SRC)/tools/build/rpr-config.sh +ROOTFS_SOURCE_DIRS += $(wildcard $(addprefix $(RELIEFOS_SRC)/third_party/,busybox/LICENSE pl_editor/LICENSE)) +ROOTFS_SOURCE_DIRS += $(RELIEFOS_SRC)/tools/build/rpr-config.sh $(addprefix $(RELIEFOS_SRC)/resources/licenses/,sl-LICENSE ncurses-LICENSE sqlite-LICENSE zlib-LICENSE libpng-LICENSE mbedtls3-LICENSE) $(O_META)/rootfs-sources.parameters: FORCE $(RELIEFOS_SRC)/tools/build/tree-signature.sh $(Q)sh $(RELIEFOS_SRC)/tools/build/tree-signature.sh $@ $(ROOTFS_SOURCE_DIRS) -ROOTFS_UPSTREAM_PRODUCTS = $(foreach package,$(UPSTREAM_PACKAGES) ncurses,$(UPSTREAM_ROOT)/$(package)/root/.complete $(upstream_$(package)_products)) +ROOTFS_UPSTREAM_PRODUCTS = $(foreach package,$(UPSTREAM_PACKAGES),$(UPSTREAM_ROOT)/$(package)/root/.complete $(upstream_$(package)_products)) ROOTFS_UPSTREAM_PRODUCTS += $(GRUB_GUEST_ROOT)/.complete $(GRUB_GUEST_PRODUCTS) -ROOTFS_APP_PRODUCTS = $(USERLAND_DIR)/motd.elf $(addprefix $(USERLAND_DIR)/,$(addsuffix .elf,$(RELIEFOS_COMPONENT_APPS))) $(BUSYBOX_ELF) $(BUSYBOX_LINKS) $(SQLITE_SO) $(USERLAND_DIR)/sl.elf +ROOTFS_APP_PRODUCTS = $(USERLAND_DIR)/motd.elf $(addprefix $(USERLAND_DIR)/,$(addsuffix .elf,$(RELIEFOS_COMPONENT_APPS))) $(BUSYBOX_ELF) $(BUSYBOX_LINKS) ROOTFS_APP_PRODUCTS += $(if $(filter soundctl,$(RELIEFOS_COMPONENT_APPS)),$(AUDIO_CONTROL_SO)) ROOTFS_APP_PRODUCTS += $(if $(filter reliefmath,$(RELIEFOS_COMPONENT_APPS)),$(USERLAND_DIR)/reliefmath-render.elf) ROOTFS_APP_PRODUCTS += $(USERLAND_DIR)/bootinstall.elf @@ -24,7 +24,7 @@ $(ROOTFS_RAW_STAMP) $(ROOTFS_MANIFEST): $(NLS_MO) $(NLS_MUSL_MO) $(RELIEFOS_SRC) RELIEFOS_PARAMETERS_rootfs := epoch=$(SOURCE_DATE_EPOCH)|sources=$(O_META)/rootfs-sources.parameters|components=$(RELIEFOS_COMPONENTS_ENABLED) $(if $(RELIEFOS_PASSIVE),,$(eval $(call KBUILD_PARAMETERS,rootfs))) $(ROOTFS_RAW_STAMP) $(ROOTFS_MANIFEST) &: $(RELIEFOS_SRC)/tools/build/rootfs-stage.sh $(RELIEFOS_SRC)/tools/build/reliefmath-resources-stage.sh $(RELIEFOS_SRC)/tools/build/busybox-links.sh $(RELIEFOS_SRC)/configs/app-icons.list $(RELIEFOS_STAGE_TOOL) $(RELIEFOS_LAYOUT_TOOL) $(O_META)/rootfs-sources.parameters $(ROOTFS_UPSTREAM_PRODUCTS) $(ROOTFS_APP_PRODUCTS) $(RUNTIME_SO) $(RUNTIME_COMPAT_SO) $(KERNELDEBUG_SYS) $(COMPONENT_METADATA) $(RELIEFOS_CONFIG_FILE) $(O_META)/rootfs.parameters $(wildcard $(RELIEFOS_SRC)/system/xorg/*) - $(Q)ROOTFS_UPSTREAM_PACKAGES='$(UPSTREAM_PACKAGES) ncurses' sh $(RELIEFOS_SRC)/tools/build/rootfs-stage.sh $(RELIEFOS_SRC) $(abspath $(O)) $(abspath $(RELIEFOS_CONFIG_FILE)) $(abspath $(COMPONENT_METADATA)) $(abspath $(RELIEFOS_STAGE_TOOL)) $(abspath $(RELIEFOS_LAYOUT_TOOL)) $(abspath $(ROOTFS_RAW)) $(abspath $(ROOTFS_MANIFEST)) $(SOURCE_DATE_EPOCH) + $(Q)ROOTFS_UPSTREAM_PACKAGES='$(UPSTREAM_PACKAGES)' sh $(RELIEFOS_SRC)/tools/build/rootfs-stage.sh $(RELIEFOS_SRC) $(abspath $(O)) $(abspath $(RELIEFOS_CONFIG_FILE)) $(abspath $(COMPONENT_METADATA)) $(abspath $(RELIEFOS_STAGE_TOOL)) $(abspath $(RELIEFOS_LAYOUT_TOOL)) $(abspath $(ROOTFS_RAW)) $(abspath $(ROOTFS_MANIFEST)) $(SOURCE_DATE_EPOCH) $(Q)touch $(ROOTFS_RAW_STAMP) .PHONY: rootfs-raw rootfs-raw: $(ROOTFS_RAW_STAMP) $(ROOTFS_MANIFEST) diff --git a/scripts/reliefos/Makefile.rpr b/scripts/reliefos/Makefile.rpr index e3f9b181..0cca4f71 100644 --- a/scripts/reliefos/Makefile.rpr +++ b/scripts/reliefos/Makefile.rpr @@ -19,7 +19,7 @@ reliefnt-release-guard: $(Q)$(RELIEFNT_RELEASE_GUARD_CMD) # Protocol 4 binds the complete runtime-validated kernel package to the exact # signed APK/index cohort; a kernel.sys-only publication cannot enable modules. -$(RPR_PAGES)/.complete $(RPR_PAGES)/manifest.json &: $(RPR_APP_PACKAGES) $(APK_MANIFEST) $(APK_REPOSITORY)/packages.adb $(RELIEFOS_KERNEL_SYS) $(RELIEFOS_KERNEL_PAYLOAD_MANIFEST) $(RELIEFOS_KERNEL_TOOLS) $(RELIEFOS_KERNEL_RECOVERY) $(RELIEFOS_SRC)/tools/build/rpr-pages.sh +$(RPR_PAGES)/.complete $(RPR_PAGES)/manifest.json &: $(RPR_APP_PACKAGES) $(APK_MANIFEST) $(APK_REPOSITORY)/packages.adb $(RELIEFOS_KERNEL_SYS) $(RELIEFOS_KERNEL_PAYLOAD_MANIFEST) $(RELIEFOS_KERNEL_TOOLS) $(RELIEFOS_KERNEL_RECOVERY) $(RELIEFOS_SRC)/tools/build/rpr-pages.sh $(wildcard $(RELIEFOS_SRC)/system/rootfs/etc/apk/keys/*.pub) $(Q)$(RELIEFNT_RELEASE_GUARD_CMD) $(Q)SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH) RELIEFOS_KERNEL_TOOLS='$(abspath $(RELIEFOS_KERNEL_TOOLS))' RELIEFOS_KERNEL_RECOVERY='$(abspath $(RELIEFOS_KERNEL_RECOVERY))' sh $(RELIEFOS_SRC)/tools/build/rpr-pages.sh $(APK_REPOSITORY) $(RPR_APPS) $(RELIEFOS_KERNEL_SYS) $(RELIEFOS_KERNEL_PAYLOAD) $(RELIEFNT_DIR)/configs/build-version $(UPSTREAM_APK) '$(APK_SIGNING_KEY)' $(RPR_PAGES) $(Q)sh $(RELIEFOS_SRC)/tools/build/stage-inventory.sh write $(RPR_PAGES) $(O_META)/rpr-pages.files diff --git a/scripts/reliefos/Makefile.runtime b/scripts/reliefos/Makefile.runtime index a48708ce..42c98d49 100644 --- a/scripts/reliefos/Makefile.runtime +++ b/scripts/reliefos/Makefile.runtime @@ -7,7 +7,7 @@ RUNTIME_COMPAT_SO := $(RUNTIME_DIR)/libleonos.so.2 RUNTIME_ARCHIVE := $(O)/musl/lib/libreliefos.a RUNTIME_COMPAT_ARCHIVE := $(O)/musl/lib/libleonos.a GBK_TABLE := $(O_INCLUDE)/generated/reliefos_gbk_table.h -PNG_CONFIG := $(O_INCLUDE)/libpng/pnglibconf.h +PNG_CONFIG := $(RUNTIME_DEVELOPMENT_ROOT)/usr/include/pnglibconf.h # Make's built-in actions and the C++ extension need these values in each # recursive directory build. These are the same tools selected by the outer @@ -24,9 +24,6 @@ export RELIEFOS_CC_ID RELIEFOS_CXX_ID RELIEFOS_LD_ID RELIEFOS_AR_ID RELIEFOS_CAPTURE_RUNTIME_KBUILD := 1 include $(RELIEFOS_SRC)/userland/runtime/src/Kbuild include $(RELIEFOS_SRC)/userland/auth/Kbuild -include $(RELIEFOS_SRC)/userland/runtime/vendor/libpng/Kbuild -include $(RELIEFOS_SRC)/userland/runtime/vendor/mbedtls/Kbuild -include $(RELIEFOS_SRC)/userland/runtime/vendor/zlib/Kbuild RELIEFOS_CAPTURE_RUNTIME_KBUILD := RUNTIME_FLAGS := --target=$(TRIPLE_USER) $(RELIEFOS_OPTIMIZATION_FLAGS) -std=c11 \ @@ -34,23 +31,16 @@ RUNTIME_FLAGS := --target=$(TRIPLE_USER) $(RELIEFOS_OPTIMIZATION_FLAGS) -std=c11 -Wall -Wextra -DRELIEFOS_USE_MUSL -D_GNU_SOURCE -mno-avx -mno-avx2 \ -I$(HEADER_EXPORT_INCLUDE) -I$(PAM_ROOT)/usr/include -I$(AUTH_ROOT)/usr/include -I$(MUSL_SYSROOT)/include \ -I$(RELIEFOS_SRC)/userland/runtime/include \ - -I$(O_INCLUDE) -I$(RELIEFOS_SRC)/include -I$(RELIEFOS_SRC)/third_party/mbedtls/include \ - -I$(RELIEFOS_SRC)/third_party/zlib -I$(RELIEFOS_SRC)/third_party/libpng \ - -I$(O_INCLUDE)/libpng -DMBEDTLS_CONFIG_FILE='"reliefos_mbedtls_config.h"' \ + -I$(O_INCLUDE) -I$(RELIEFOS_SRC)/include -isystem $(RUNTIME_DEVELOPMENT_ROOT)/usr/include \ -include $(O_INCLUDE)/generated/autoconf.h \ -ffile-prefix-map=$(RELIEFOS_SRC)=. -ffile-prefix-map=$(RELIEFOS_OUTPUT)=out RUNTIME_CFLAGS ?= RUNTIME_AUTH_LIBS := $(PAM_LIB) $(PAM_MISC_LIB) $(AUTH_ROOT)/lib/libcrypt.so.2 -RUNTIME_HEADERS := $(PAM_HEADER) $(AUTH_ROOT)/usr/include/crypt.h +RUNTIME_HEADERS := $(PAM_HEADER) $(AUTH_ROOT)/usr/include/crypt.h $(RUNTIME_DEVELOPMENT_PRODUCTS) RUNTIME_BUILTINS := $(if $(RELIEFOS_PASSIVE),,$(shell $(TARGET_CC) --target=$(TRIPLE_USER) --rtlib=compiler-rt -print-libgcc-file-name 2>/dev/null)) -# Link member order matches the previous sorted source list: third-party -# runtime inputs first, then auth and the main runtime (whose syscall.S stays -# last in that directory list). Each source still gets its own Kbuild context. +# Only first-party code is embedded; official library ABIs remain separate. RUNTIME_OBJECTS := \ - $(addprefix $(O)/userland/runtime/vendor/libpng/,$(RELIEFOS_LIBPNG_OBJECTS)) \ - $(addprefix $(O)/userland/runtime/vendor/mbedtls/,$(RELIEFOS_MBEDTLS_OBJECTS)) \ - $(addprefix $(O)/userland/runtime/vendor/zlib/,$(RELIEFOS_ZLIB_OBJECTS)) \ $(addprefix $(O)/userland/auth/,$(RELIEFOS_AUTH_OBJECTS)) \ $(addprefix $(O)/userland/runtime/src/,$(RELIEFOS_RUNTIME_SRC_OBJECTS)) @@ -60,12 +50,9 @@ export GBK_TABLE PNG_CONFIG MUSL_STAMP RUNTIME_BUILTINS # Directory recursion must be enabled at the root for product targets to use # each child built-in.a as the freshness edge for their direct object links. need-builtin := 1 -obj-y += userland/runtime/src/ userland/auth/ \ - userland/runtime/vendor/libpng/ userland/runtime/vendor/mbedtls/ \ - userland/runtime/vendor/zlib/ userland/musl-dev/ +obj-y += userland/runtime/src/ userland/auth/ userland/musl-dev/ -userland/runtime/src userland/auth userland/runtime/vendor/libpng \ -userland/runtime/vendor/mbedtls userland/runtime/vendor/zlib: \ +userland/runtime/src userland/auth: \ $(AUTOCONF_H) $(GBK_TABLE) $(PNG_CONFIG) $(MUSL_STAMP) $(RUNTIME_HEADERS) \ $(HEADER_EXPORT_MANIFEST) $(RELIEFOS_CC_ID) $(RELIEFOS_AR_ID) @@ -73,19 +60,15 @@ $(GBK_TABLE): $(RELIEFOS_GBK_TOOL) $(RELIEFOS_SRC)/third_party/litehtml/src/enco $(Q)mkdir -p $(dir $@) $(Q)$(RELIEFOS_GBK_TOOL) $(RELIEFOS_SRC)/third_party/litehtml/src/encodings.cpp $@ -$(PNG_CONFIG): $(RELIEFOS_SRC)/third_party/libpng/scripts/pnglibconf.h.prebuilt $(RELIEFOS_SRC)/tools/build/png-config.sh $(RELIEFOS_EMIT) - $(Q)mkdir -p $(dir $@) - $(Q)sh $(RELIEFOS_SRC)/tools/build/png-config.sh $< $@ $(RELIEFOS_EMIT) - quiet_cmd_reliefos_runtime_shared = LD $@ - cmd_reliefos_runtime_shared = test -f '$(RUNTIME_BUILTINS)' || { echo 'missing compiler-rt builtins; select a complete Clang toolchain' >&2; exit 1; }; $(TARGET_LD) -shared --no-undefined --hash-style=both -soname $(notdir $@) -o $@.tmp $(RUNTIME_OBJECTS) -L$(MUSL_SYSROOT)/lib -l:libmimalloc.so.3 $(RUNTIME_AUTH_LIBS) -lc $(RUNTIME_BUILTINS); mv $@.tmp $@ -$(RUNTIME_SO) $(RUNTIME_COMPAT_SO): $(O)/userland/runtime/src/built-in.a $(O)/userland/auth/built-in.a $(O)/userland/runtime/vendor/libpng/built-in.a $(O)/userland/runtime/vendor/mbedtls/built-in.a $(O)/userland/runtime/vendor/zlib/built-in.a $(RUNTIME_AUTH_LIBS) $(MUSL_SYSROOT)/lib/libc.so $(MUSL_SYSROOT)/lib/libmimalloc.so.3 $(RUNTIME_BUILTINS) $(RELIEFOS_LD_ID) FORCE + cmd_reliefos_runtime_shared = test -f '$(RUNTIME_BUILTINS)' || { echo 'missing compiler-rt builtins; select a complete Clang toolchain' >&2; exit 1; }; $(TARGET_LD) -shared --no-undefined --hash-style=both -soname $(notdir $@) -o $@.tmp $(RUNTIME_OBJECTS) -L$(MUSL_SYSROOT)/lib -l:libmimalloc.so.3 $(RUNTIME_AUTH_LIBS) $(RUNTIME_DEP_SHARED) -lc $(RUNTIME_BUILTINS); mv $@.tmp $@ +$(RUNTIME_SO) $(RUNTIME_COMPAT_SO): $(O)/userland/runtime/src/built-in.a $(O)/userland/auth/built-in.a $(RUNTIME_AUTH_LIBS) $(RUNTIME_DEP_SHARED) $(MUSL_SYSROOT)/lib/libc.so $(MUSL_SYSROOT)/lib/libmimalloc.so.3 $(RUNTIME_BUILTINS) $(RELIEFOS_LD_ID) FORCE $(call if_changed,reliefos_runtime_shared) targets += $(RUNTIME_SO) $(RUNTIME_COMPAT_SO) quiet_cmd_reliefos_runtime_archive = AR $@ cmd_reliefos_runtime_archive = rm -f $@.tmp; $(TARGET_AR) rcsD $@.tmp $(RUNTIME_OBJECTS); mv $@.tmp $@ -$(RUNTIME_ARCHIVE) $(RUNTIME_COMPAT_ARCHIVE): $(O)/userland/runtime/src/built-in.a $(O)/userland/auth/built-in.a $(O)/userland/runtime/vendor/libpng/built-in.a $(O)/userland/runtime/vendor/mbedtls/built-in.a $(O)/userland/runtime/vendor/zlib/built-in.a $(RELIEFOS_AR_ID) FORCE +$(RUNTIME_ARCHIVE) $(RUNTIME_COMPAT_ARCHIVE): $(O)/userland/runtime/src/built-in.a $(O)/userland/auth/built-in.a $(RELIEFOS_AR_ID) FORCE $(call if_changed,reliefos_runtime_archive) targets += $(RUNTIME_ARCHIVE) $(RUNTIME_COMPAT_ARCHIVE) diff --git a/scripts/reliefos/Makefile.sdk b/scripts/reliefos/Makefile.sdk index 8b1f89b7..a8b797fb 100644 --- a/scripts/reliefos/Makefile.sdk +++ b/scripts/reliefos/Makefile.sdk @@ -12,8 +12,8 @@ $(COMPONENT_SELECTION): $(O_CONFIG)/components.mk $(RELIEFOS_CONFIG_FILE) $(RELI --config $(RELIEFOS_CONFIG_FILE) --output $(O_CONFIG)/components.mk --selection $@ MUSL_SDK_REQUIRED := $(MUSL_SDK)/bin/reliefos-musl-cc $(MUSL_SDK)/include/stdio.h \ - $(MUSL_SDK)/include/pnglibconf.h $(MUSL_SDK)/lib/crt1.o $(MUSL_SDK)/lib/crti.o \ - $(MUSL_SDK)/lib/crtn.o $(MUSL_SDK)/lib/libc.a $(MUSL_SDK)/lib/libc.so \ + $(MUSL_SDK)/include/pnglibconf.h $(MUSL_SDK)/include/mbedtls/ssl.h $(MUSL_SDK)/include/sqlite3.h $(addprefix $(MUSL_SDK)/lib/,libz.a libz.so.1 libpng.a libpng16.so.16 libsqlite3.a libsqlite3.so.0 libmbedcrypto.a libmbedcrypto.so.16 libmbedx509.a libmbedx509.so.7 libmbedtls.a libmbedtls.so.21) $(MUSL_SDK)/lib/crt1.o $(MUSL_SDK)/lib/crti.o \ + $(MUSL_SDK)/lib/crtn.o $(MUSL_SDK)/lib/libc.a $(MUSL_SDK)/lib/libc.so $(MUSL_SDK)/lib/libcrypt.a \ $(MUSL_SDK)/lib/libreliefos.so.2 $(MUSL_SDK)/lib/libleonos.so.2 \ $(MUSL_SDK)/lib/libreliefos.a $(MUSL_SDK)/lib/libleonos.a \ $(MUSL_SDK)/include/alsa/asoundlib.h $(MUSL_SDK)/include/opl3.h \ @@ -23,8 +23,8 @@ MUSL_SDK_REQUIRED := $(MUSL_SDK)/bin/reliefos-musl-cc $(MUSL_SDK)/include/stdio. $(MUSL_SDK_REQUIRED) &: $(RUNTIME_SO) $(RUNTIME_COMPAT_SO) $(RUNTIME_ARCHIVE) $(RUNTIME_COMPAT_ARCHIVE) $(RUNTIME_BUILTINS) \ $(MUSL_STAMP) $(RELIEFOS_MUSL_ARTIFACTS) $(PAM_STAMP) $(AUTH_STAMP) $(SDK_INPUT_HEADERS) \ $(HEADER_EXPORT_MANIFEST) $(AUDIO_SDK_PRODUCTS) \ - $(PNG_CONFIG) $(RELIEFOS_SDK_DRIVER) $(O_META)/sdk.parameters $(RELIEFOS_SRC)/tools/build/musl-sdk.sh - $(Q)set -eu; AUDIO_CONTROL_SO=$(abspath $(AUDIO_CONTROL_SO)) AUDIO_ALSA_ROOT=$(abspath $(AUDIO_ALSA_ROOT)) AUDIO_OPL_ROOT=$(abspath $(AUDIO_OPL_ROOT)) RUNTIME_BUILTINS=$(RUNTIME_BUILTINS) PNG_CONFIG=$(PNG_CONFIG) sh $(RELIEFOS_SRC)/tools/build/musl-sdk.sh $(RELIEFOS_SRC) $(MUSL_SYSROOT) $(RUNTIME_SO) $(RUNTIME_ARCHIVE) $(RUNTIME_COMPAT_SO) $(RUNTIME_COMPAT_ARCHIVE) $(PAM_ROOT) $(AUTH_ROOT) $(RELIEFOS_SDK_DRIVER) $(MUSL_SDK) $(SDK_EPOCH) $(HEADER_EXPORT_INCLUDE) + $(RUNTIME_DEVELOPMENT_PRODUCTS) $(RELIEFOS_SDK_DRIVER) $(O_META)/sdk.parameters $(RELIEFOS_SRC)/tools/build/musl-sdk.sh + $(Q)set -eu; AUDIO_CONTROL_SO=$(abspath $(AUDIO_CONTROL_SO)) AUDIO_ALSA_ROOT=$(abspath $(AUDIO_ALSA_ROOT)) AUDIO_OPL_ROOT=$(abspath $(AUDIO_OPL_ROOT)) RUNTIME_BUILTINS=$(RUNTIME_BUILTINS) RUNTIME_DEVELOPMENT_ROOT=$(abspath $(RUNTIME_DEVELOPMENT_ROOT)) sh $(RELIEFOS_SRC)/tools/build/musl-sdk.sh $(RELIEFOS_SRC) $(MUSL_SYSROOT) $(RUNTIME_SO) $(RUNTIME_ARCHIVE) $(RUNTIME_COMPAT_SO) $(RUNTIME_COMPAT_ARCHIVE) $(PAM_ROOT) $(AUTH_ROOT) $(RELIEFOS_SDK_DRIVER) $(MUSL_SDK) $(SDK_EPOCH) $(HEADER_EXPORT_INCLUDE) $(Q)set -eu; for product in $(MUSL_SDK_REQUIRED); do test -f "$$product" || exit 1; touch "$$product"; done $(Q)set -eu; find $(MUSL_SDK) -mindepth 1 ! -type d -printf '%P\n' | LC_ALL=C sort >$(MUSL_SDK).files diff --git a/scripts/reliefos/Makefile.tests b/scripts/reliefos/Makefile.tests index 42b611e6..f4590919 100644 --- a/scripts/reliefos/Makefile.tests +++ b/scripts/reliefos/Makefile.tests @@ -3,6 +3,8 @@ # The core harness is POSIX shell and C: the acceptance rule for this migration # may not depend on the interpreter it is auditing (plan section 2). +export RELIEFOS_TEST_DEP_ROOT := $(abspath $(HOST_RUNTIME_DEVELOPMENT_ROOT)) + RELIEFOS_TEST_TMP := $(if $(TMPDIR),$(TMPDIR),/tmp)/reliefos-tests-$(shell id -u) # C unit tests for the shared host primitives and the JSON reader. @@ -80,7 +82,7 @@ endef leonos-test-tools-plain: reliefos-test-tools-plain leonos-test-tools-sanitised: reliefos-test-tools-sanitised -test-build: test-tools $(RELIEFOS_HOST_TOOLS) $(INITRAMFS_BUSYBOX_ELF) $(INITRAMFS_BUSYBOX_LINKS) $(RELIEFOS_KERNEL_RECOVERY) $(HEADER_EXPORT_MANIFEST) +test-build: $(HOST_RUNTIME_DEVELOPMENT_PRODUCTS) test-tools $(RELIEFOS_HOST_TOOLS) $(INITRAMFS_BUSYBOX_ELF) $(INITRAMFS_BUSYBOX_LINKS) $(RELIEFOS_KERNEL_RECOVERY) $(HEADER_EXPORT_MANIFEST) @$(call RELIEFOS_RUN_CONTRACT_TESTS,$(RELIEFOS_BUILD_TESTS)) @$(RELIEFOS_TEST_PYTHON) $(RELIEFOS_SRC)/tools/test_xorg_qemu.py --source-only diff --git a/scripts/reliefos/Makefile.third-party b/scripts/reliefos/Makefile.third-party index db46ed49..e4128427 100644 --- a/scripts/reliefos/Makefile.third-party +++ b/scripts/reliefos/Makefile.third-party @@ -171,7 +171,7 @@ RELIEFOS_AUTH_CFLAGS := -O2 -mno-avx -mno-avx2 RELIEFOS_AUTH_ARTIFACTS := \ $(AUTH_ROOT)/usr/include/crypt.h \ $(AUTH_ROOT)/usr/include/linux/openat2.h \ - $(AUTH_ROOT)/lib/libcrypt.so.2 \ + $(AUTH_ROOT)/lib/libcrypt.so.2 $(AUTH_ROOT)/lib/libcrypt.a \ $(AUTH_ROOT)/share/licenses/libxcrypt/LICENSE \ $(AUTH_ROOT)/share/licenses/linux-headers/LICENSE diff --git a/scripts/reliefos/Makefile.upstream b/scripts/reliefos/Makefile.upstream index b69eb75e..8a2a932d 100644 --- a/scripts/reliefos/Makefile.upstream +++ b/scripts/reliefos/Makefile.upstream @@ -1,7 +1,7 @@ # Include after third-party.mk, pam.mk and runtime.mk. Each package owns an isolated root. UPSTREAM_ROOT := $(O)/upstream UPSTREAM_SCRIPT := $(RELIEFOS_SRC)/tools/build/upstream.sh -UPSTREAM_PACKAGES := libmd libbsd util-linux sudo shadow e2fsprogs dosfstools exfatprogs coreutils alsa-lib alsa-utils nuked-opl3 +UPSTREAM_PACKAGES := libmd libbsd util-linux sudo shadow e2fsprogs dosfstools exfatprogs coreutils alsa-lib alsa-utils nuked-opl3 ffmpeg upstream_libmd_outputs := lib/libmd.so.0 usr/include/md5.h upstream_libbsd_outputs := lib/libbsd.so.0 usr/include/bsd/stdlib.h upstream_util-linux_outputs := usr/lib/libuuid.a usr/lib/libblkid.a bin/su usr/sbin/fdisk bin/mount bin/lsblk @@ -13,12 +13,13 @@ upstream_exfatprogs_outputs := usr/sbin/mkfs.exfat usr/sbin/fsck.exfat COREUTILS_PROGRAMS := $(shell cat $(RELIEFOS_SRC)/userland/coreutils/programs.list) upstream_coreutils_outputs := $(addprefix usr/bin/,$(COREUTILS_PROGRAMS)) usr/libexec/coreutils/libstdbuf.so upstream_coreutils_inputs := $(RELIEFOS_SRC)/userland/coreutils/programs.list $(RELIEFOS_SRC)/tools/build/coreutils-check.sh -upstream_ncurses_outputs := usr/lib/libncursesw.a usr/lib/libtinfow.a usr/include/curses.h upstream_alsa-lib_outputs := usr/lib/libasound.so.2 usr/lib/libasound.a usr/include/alsa/asoundlib.h usr/share/alsa/alsa.conf upstream_alsa-utils_outputs := usr/bin/aplay usr/bin/arecord usr/bin/amixer usr/sbin/alsactl usr/bin/speaker-test upstream_nuked-opl3_outputs := usr/lib/libopl3.so.1 usr/include/opl3.h upstream_nuked-opl3_script := $(RELIEFOS_SRC)/tools/build/opl3.sh -$(foreach package,$(UPSTREAM_PACKAGES) ncurses,$(eval upstream_$(package)_primary := $(upstream_$(package)_outputs))) +upstream_ffmpeg_outputs := usr/lib/libavformat-reliefplayer.so.62 usr/lib/libavcodec-reliefplayer.so.62 usr/lib/libavutil-reliefplayer.so.60 usr/lib/libswresample-reliefplayer.so.6 usr/lib/libswscale-reliefplayer.so.9 usr/include/libavformat/avformat.h usr/include/libavcodec/avcodec.h usr/include/libavutil/avutil.h usr/include/libswresample/swresample.h usr/include/libswscale/swscale.h +upstream_ffmpeg_script := $(RELIEFOS_SRC)/tools/build/ffmpeg.sh +$(foreach package,$(UPSTREAM_PACKAGES),$(eval upstream_$(package)_primary := $(upstream_$(package)_outputs))) upstream_libbsd_deps := libmd upstream_shadow_deps := libmd libbsd upstream_e2fsprogs_deps := util-linux @@ -52,52 +53,7 @@ reliefos-storage: upstream-e2fsprogs upstream-dosfstools upstream-exfatprogs leonos-upstream: reliefos-upstream leonos-storage: reliefos-storage -# Terminal packages use the pinned submodules and upstream Makefiles. -UPSTREAM_TERMINAL_SCRIPT := $(RELIEFOS_SRC)/tools/build/upstream-terminal.sh -define RELIEFOS_TERMINAL_RULE -RELIEFOS_PARAMETERS_upstream-$(1) := cc=$(TARGET_CC)|triple=$(TRIPLE_USER)|lock=$(RELIEFOS_LOCK_DIGEST)|identity=$(shell $(TARGET_CC) --version 2>/dev/null | head -n1) -$$(if $$(RELIEFOS_PASSIVE),,$$(eval $$(call KBUILD_PARAMETERS,upstream-$(1)))) -upstream_$(1)_products := $$(addprefix $$(UPSTREAM_ROOT)/$(1)/root/,$$(sort $$(upstream_$(1)_outputs))) -$$(UPSTREAM_ROOT)/$(1)/root/.complete $$(upstream_$(1)_products) &: $$(UPSTREAM_TERMINAL_SCRIPT) $$(RELIEFOS_UPSTREAM_MAKE_ENV) $$(RELIEFOS_LOCK) $$(RELIEFOS_DEPS_TOOL) $$(O_META)/upstream-$(1).parameters $$(MUSL_STAMP) $$(RELIEFOS_MUSL_ARTIFACTS) - $$(Q)mkdir -p $$(O_LOGS) - +$$(Q)case "$$$${MAKEFLAGS%% *}" in *n*) exit 0;; esac; sh $$(UPSTREAM_TERMINAL_SCRIPT) $(1) $$(RELIEFOS_SRC) $$(abspath $$(RELIEFOS_DEPS_TOOL)) $$(RELIEFOS_LOCK) $$(abspath $$(UPSTREAM_ROOT))/$(1)/work $$(abspath $$(UPSTREAM_ROOT))/$(1)/root $$(abspath $$(MUSL_SYSROOT)) $$(TARGET_CC) $$(TRIPLE_USER) >$$(O_LOGS)/upstream-$(1).log 2>&1 || { tail -n 50 $$(O_LOGS)/upstream-$(1).log >&2; exit 1; } - $$(Q)for product in $$(addprefix $$(UPSTREAM_ROOT)/$(1)/root/,$$(upstream_$(1)_primary)); do test -e "$$$$product" || exit 1; touch "$$$$product"; done - $$(Q)touch $$(UPSTREAM_ROOT)/$(1)/root/.complete -.PHONY: upstream-$(1) -upstream-$(1): $$(UPSTREAM_ROOT)/$(1)/root/.complete $$(upstream_$(1)_products) -endef -$(foreach package,ncurses,$(eval $(call RELIEFOS_TERMINAL_RULE,$(package)))) -reliefos-upstream: upstream-ncurses - -UPSTREAM_APK_ROOT := $(UPSTREAM_ROOT)/apk -UPSTREAM_APK := $(UPSTREAM_APK_ROOT)/apk.static -$(UPSTREAM_APK) $(UPSTREAM_APK_ROOT)/.complete &: $(RELIEFOS_SRC)/tools/build/upstream-apk.sh $(RELIEFOS_LOCK) $(RELIEFOS_DEPS_TOOL) $(wildcard $(RELIEFOS_SRC)/system/rootfs/etc/apk/keys/*) - $(Q)sh $(RELIEFOS_SRC)/tools/build/upstream-apk.sh $(RELIEFOS_SRC) $(abspath $(RELIEFOS_DEPS_TOOL)) $(RELIEFOS_LOCK) $(RELIEFOS_CACHE) $(abspath $(UPSTREAM_APK_ROOT)) -.PHONY: upstream-apk -upstream-apk: $(UPSTREAM_APK) $(UPSTREAM_APK_ROOT)/.complete -reliefos-upstream: upstream-apk - UPSTREAM_APP_DIR := $(O)/userland -SQLITE_SO := $(UPSTREAM_APP_DIR)/sqlite.so.3 -SQLITE_ARCHIVE := $(UPSTREAM_APP_DIR)/libsqlite3.a -SQLITE_HEADER := $(UPSTREAM_APP_DIR)/sqlite3.h -SQLITE_AMALGAMATION := $(UPSTREAM_APP_DIR)/sqlite3.c -SQLITE_VENDOR_OBJECT := $(UPSTREAM_APP_DIR)/sqlite3.o -SQLITE_VFS_FLAGS := --target=$(TRIPLE_USER) $(RELIEFOS_OPTIMIZATION_FLAGS) -std=gnu11 \ - -ffreestanding -fno-stack-protector -fPIC -ffunction-sections -fdata-sections \ - -nostdinc -isystem $(shell $(TARGET_CC) -print-resource-dir 2>/dev/null)/include \ - -I$(MUSL_SYSROOT)/include -I$(RELIEFOS_SRC)/userland/runtime/include \ - -I$(RELIEFOS_SRC)/include -I$(HEADER_EXPORT_INCLUDE) -I$(O_INCLUDE) \ - -I$(UPSTREAM_APP_DIR) -I$(RELIEFOS_SRC)/third_party/sqlite \ - -DRELIEFOS_USE_MUSL -D_GNU_SOURCE \ - -D_POSIX_C_SOURCE=200809L -DSQLITE_OS_OTHER=1 -DSQLITE_THREADSAFE=0 \ - -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_OMIT_WAL=1 \ - -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_MAX_MMAP_SIZE=0 -UPSTREAM_SQLITE_VFS_FLAGS := $(SQLITE_VFS_FLAGS) -export UPSTREAM_SQLITE_VFS_FLAGS -upstream_app_sl_outputs := sl.elf -upstream_app_sqlite_outputs := sqlite3.o sqlite3.h sqlite3.c - define RELIEFOS_UPSTREAM_APP upstream_app_$(1)_inputs := $$(shell find $$(RELIEFOS_SRC)/third_party/$(if $(filter pleditor,$(1)),pl_editor,$(1)) $$(RELIEFOS_SRC)/userland/$(if $(filter pleditor,$(1)),apps/pleditor,$(1)) -type f ! -name .git 2>/dev/null | LC_ALL=C sort) RELIEFOS_PARAMETERS_upstream-app-$(1) := cc=$(TARGET_CC)|identity=$(shell $(TARGET_CC) --version 2>/dev/null | head -n1)|ld=$(TARGET_LD)|ar=$(TARGET_AR)|cflags=$(RELIEFOS_OPTIMIZATION_FLAGS)|ldflags=$(RELIEFOS_LINK_POLICY_FLAGS)|input-list-sha=$$(shell find $$(RELIEFOS_SRC)/third_party/$(if $(filter pleditor,$(1)),pl_editor,$(1)) $$(RELIEFOS_SRC)/userland/$(if $(filter pleditor,$(1)),apps/pleditor,$(1)) -type f ! -name .git 2>/dev/null | LC_ALL=C sort | sha256sum | cut -d' ' -f1) @@ -109,30 +65,6 @@ $$(addprefix $$(UPSTREAM_APP_DIR)/,$$(sort $$(upstream_app_$(1)_outputs))) &: $$ .PHONY: upstream-app-$(1) upstream-app-$(1): $$(addprefix $$(UPSTREAM_APP_DIR)/,$$(sort $$(upstream_app_$(1)_outputs))) endef -$(foreach package,sl sqlite,$(eval $(call RELIEFOS_UPSTREAM_APP,$(package)))) -obj-y += userland/sqlite/ -RELIEFOS_CAPTURE_SQLITE_VFS_KBUILD := 1 -include $(RELIEFOS_SRC)/userland/sqlite/Kbuild -RELIEFOS_CAPTURE_SQLITE_VFS_KBUILD := -SQLITE_VFS_OBJECT := $(O)/userland/sqlite/$(firstword $(RELIEFOS_SQLITE_VFS_OBJECTS)) -userland/sqlite: $(SQLITE_VENDOR_OBJECT) $(SQLITE_HEADER) $(RELIEFOS_CC_ID) $(RELIEFOS_AR_ID) $(AUTOCONF_H) $(MUSL_STAMP) $(HEADER_EXPORT_MANIFEST) - -quiet_cmd_reliefos_sqlite_shared = LD $@ - cmd_reliefos_sqlite_shared = $(TARGET_LD) $(RELIEFOS_LINK_POLICY_FLAGS) -shared --no-undefined --hash-style=both -z max-page-size=0x1000 -soname sqlite.so.3 -o $@.tmp $(SQLITE_VENDOR_OBJECT) $(SQLITE_VFS_OBJECT) -L$(MUSL_SYSROOT)/lib -l:libmimalloc.so.3 $(RUNTIME_SO) -lc; mv $@.tmp $@ -$(SQLITE_SO): $(O)/userland/sqlite/built-in.a $(SQLITE_VENDOR_OBJECT) $(MUSL_SYSROOT)/lib/libc.so $(MUSL_SYSROOT)/lib/libmimalloc.so.3 $(RUNTIME_SO) $(RELIEFOS_LD_ID) FORCE - $(call if_changed,reliefos_sqlite_shared) -targets += $(SQLITE_SO) - -quiet_cmd_reliefos_sqlite_archive = AR $@ - cmd_reliefos_sqlite_archive = rm -f $@.tmp; $(TARGET_AR) rcs $@.tmp $(SQLITE_VENDOR_OBJECT) $(SQLITE_VFS_OBJECT); mv $@.tmp $@ -$(SQLITE_ARCHIVE): $(O)/userland/sqlite/built-in.a $(SQLITE_VENDOR_OBJECT) $(RELIEFOS_AR_ID) FORCE - $(call if_changed,reliefos_sqlite_archive) -targets += $(SQLITE_ARCHIVE) - -.PHONY: upstream-app-sqlite -upstream-app-sqlite: $(addprefix $(UPSTREAM_APP_DIR)/,$(sort $(upstream_app_sqlite_outputs))) $(SQLITE_SO) $(SQLITE_ARCHIVE) -reliefos-upstream: upstream-app-sl upstream-app-sqlite - BUSYBOX_ELF := $(UPSTREAM_APP_DIR)/busybox.elf BUSYBOX_LINKS := $(UPSTREAM_APP_DIR)/busybox.links UPSTREAM_EPOCH := $(if $(SOURCE_DATE_EPOCH),$(SOURCE_DATE_EPOCH),$(shell git -C $(RELIEFOS_SRC) log -1 --format=%ct)) @@ -169,7 +101,7 @@ $(O_META)/upstream-$(1)-present.parameters: FORCE $(RELIEFOS_SRC)/tools/build/up $(Q)sh $(RELIEFOS_SRC)/tools/build/upstream-verify.sh $(UPSTREAM_ROOT)/$(1)/root $(UPSTREAM_ROOT)/$(1)/work/installed-files $$@ $(UPSTREAM_ROOT)/$(1)/root/.complete $$(upstream_$(1)_products): $(O_META)/upstream-$(1)-present.parameters endef -$(foreach package,$(UPSTREAM_PACKAGES) ncurses,$(eval $(call RELIEFOS_UPSTREAM_PRESENCE,$(package)))) +$(foreach package,$(UPSTREAM_PACKAGES),$(eval $(call RELIEFOS_UPSTREAM_PRESENCE,$(package)))) $(O_META)/upstream-apk-present.parameters: FORCE $(RELIEFOS_SRC)/tools/build/upstream-verify.sh $(Q)sh $(RELIEFOS_SRC)/tools/build/upstream-verify.sh $(UPSTREAM_APK_ROOT) $(UPSTREAM_APK_ROOT)/installed-files $@ $(UPSTREAM_APK) $(UPSTREAM_APK_ROOT)/.complete: $(O_META)/upstream-apk-present.parameters diff --git a/scripts/reliefos/Makefile.userland b/scripts/reliefos/Makefile.userland index 3a2bf911..b024a107 100644 --- a/scripts/reliefos/Makefile.userland +++ b/scripts/reliefos/Makefile.userland @@ -41,13 +41,16 @@ $(foreach app,$(USERLAND_FLTK_APPS),$(eval USERLAND_DEPS_$(app) += $(X11_DEVELOP USERLAND_LIBS_reliefcapture += $(X11_DEVELOPMENT_ROOT)/usr/lib/libX11.so USERLAND_DEPS_reliefcapture += $(X11_DEVELOPMENT_ROOT)/usr/lib/libX11.so -USERLAND_EXTRA_reliefmail := -I$(UPSTREAM_APP_DIR) -USERLAND_DEPS_reliefmail += $(SQLITE_HEADER) $(SQLITE_AMALGAMATION) +USERLAND_LIBS_reliefmail += $(RUNTIME_DEP_LIBDIR)/libsqlite3.so +USERLAND_DEPS_reliefmail += $(RUNTIME_DEVELOPMENT_PRODUCTS) USERLAND_DEPS_osver += $(BUILD_INFO_HEADER) USERLAND_EXTRA_doom := -DRELIEFOS_DOOM -DLEONOS_DOOM -DFEATURE_SOUND -I$(RELIEFOS_SRC)/third_party/doomgeneric/doomgeneric -I$(AUDIO_OPL_ROOT)/usr/include USERLAND_LIBS_doom += $(AUDIO_OPL_ROOT)/usr/lib/libopl3.so.1 $(RUNTIME_BUILTINS) USERLAND_DEPS_doom += $(upstream_nuked-opl3_products) -USERLAND_EXTRA_reliefplayer := -I$(RELIEFOS_SRC)/third_party/minimp3 +MEDIA_CODEC_ROOT := $(UPSTREAM_ROOT)/ffmpeg/root +USERLAND_EXTRA_reliefplayer := -I$(RELIEFOS_SRC)/third_party/minimp3 -isystem $(MEDIA_CODEC_ROOT)/usr/include +USERLAND_LIBS_reliefplayer += $(addprefix $(MEDIA_CODEC_ROOT)/usr/lib/,libavformat-reliefplayer.so.62 libavcodec-reliefplayer.so.62 libswresample-reliefplayer.so.6 libswscale-reliefplayer.so.9 libavutil-reliefplayer.so.60) +USERLAND_DEPS_reliefplayer += $(upstream_ffmpeg_products) # The historical port-script compiler flags for pleditor are intentionally # retained. Only the transform/copy stage stays in the adapter; Kbuild compiles @@ -81,8 +84,8 @@ USERLAND_CRT_$(1) := $(USERLAND_CRT) RELIEFOS_APP_BUILTIN_$(1) := $(O)/userland/apps/$(1)/built-in.a userland/apps/$(1): $(AUTOCONF_H) $(MUSL_STAMP) $(PNG_CONFIG) $(HEADER_EXPORT_MANIFEST) $$(USERLAND_DEPS_$(1)) $$(if $$(USERLAND_C_OBJECTS_$(1)),$$(RELIEFOS_CC_ID)) $$(if $$(USERLAND_CXX_OBJECTS_$(1)),$$(RELIEFOS_CXX_ID)) $$(RELIEFOS_AR_ID) quiet_cmd_reliefos_app_link_$(1) = LD $$@ - cmd_reliefos_app_link_$(1) = $$(TARGET_LD) $$(USERLAND_LINK_FLAGS) $$(USERLAND_LDFLAGS) -o $$@.tmp $$(MUSL_SYSROOT)/lib/Scrt1.o $$(MUSL_SYSROOT)/lib/crti.o $$(USERLAND_OBJECTS_$(1)) -L$$(MUSL_SYSROOT)/lib -l:libmimalloc.so.3 --start-group $(5) $$(USERLAND_LIBS_$(1)) -lc --end-group $$(MUSL_SYSROOT)/lib/crtn.o; mv $$@.tmp $$@ -$(3): $$(RELIEFOS_APP_BUILTIN_$(1)) $(5) $$(USERLAND_LIBS_$(1)) $$(USERLAND_DEPS_$(1)) $$(USERLAND_CRT) $$(MUSL_SYSROOT)/lib/libc.so $$(MUSL_SYSROOT)/lib/libmimalloc.so.3 $$(RELIEFOS_LD_ID) FORCE + cmd_reliefos_app_link_$(1) = $$(TARGET_LD) $$(USERLAND_LINK_FLAGS) $$(USERLAND_LDFLAGS) -o $$@.tmp $$(MUSL_SYSROOT)/lib/Scrt1.o $$(MUSL_SYSROOT)/lib/crti.o $$(USERLAND_OBJECTS_$(1)) -L$$(MUSL_SYSROOT)/lib -l:libmimalloc.so.3 --start-group $(5) $$(USERLAND_LIBS_$(1)) $$(RUNTIME_DEP_SHARED) -lc --end-group $$(MUSL_SYSROOT)/lib/crtn.o; mv $$@.tmp $$@ +$(3): $$(RELIEFOS_APP_BUILTIN_$(1)) $$(RUNTIME_DEP_SHARED) $(5) $$(USERLAND_LIBS_$(1)) $$(USERLAND_DEPS_$(1)) $$(USERLAND_CRT) $$(MUSL_SYSROOT)/lib/libc.so $$(MUSL_SYSROOT)/lib/libmimalloc.so.3 $$(RELIEFOS_LD_ID) FORCE $$(call if_changed,reliefos_app_link_$(1)) targets += $(3) .PHONY: app-$(1) @@ -105,8 +108,8 @@ $(eval $(call RELIEFOS_APP,gptinit,gptinit,$(O)/userland-installer/gptinit.elf,$ define RELIEFOS_STATIC_RUNTIME_APP userland/apps/$(1): $(AUTOCONF_H) $(MUSL_STAMP) $(HEADER_EXPORT_MANIFEST) $(RELIEFOS_CC_ID) $(RELIEFOS_AR_ID) quiet_cmd_reliefos_static_app_$(1) = LD $$@ - cmd_reliefos_static_app_$(1) = $$(TARGET_LD) $$(RELIEFOS_LINK_POLICY_FLAGS) -static --gc-sections -z max-page-size=0x1000 -o $$@.tmp $$(MUSL_SYSROOT)/lib/crt1.o $$(MUSL_SYSROOT)/lib/crti.o $$(USERLAND_OBJECTS_$(1)) --start-group $$(RUNTIME_ARCHIVE) $$(MUSL_SYSROOT)/lib/libc.a $$(RUNTIME_BUILTINS) --end-group $$(MUSL_SYSROOT)/lib/crtn.o && mv $$@.tmp $$@ -$(2): $(O)/userland/apps/$(1)/built-in.a $(RUNTIME_ARCHIVE) $(MUSL_SYSROOT)/lib/crt1.o $(MUSL_SYSROOT)/lib/crti.o $(MUSL_SYSROOT)/lib/crtn.o $(MUSL_SYSROOT)/lib/libc.a $(RELIEFOS_LD_ID) FORCE + cmd_reliefos_static_app_$(1) = $$(TARGET_LD) $$(RELIEFOS_LINK_POLICY_FLAGS) -static --gc-sections -z max-page-size=0x1000 -o $$@.tmp $$(MUSL_SYSROOT)/lib/crt1.o $$(MUSL_SYSROOT)/lib/crti.o $$(USERLAND_OBJECTS_$(1)) --start-group $$(RUNTIME_ARCHIVE) $$(RUNTIME_DEP_STATIC) $$(MUSL_SYSROOT)/lib/libc.a $$(RUNTIME_BUILTINS) --end-group $$(MUSL_SYSROOT)/lib/crtn.o && mv $$@.tmp $$@ +$(2): $(O)/userland/apps/$(1)/built-in.a $(RUNTIME_ARCHIVE) $(RUNTIME_DEP_STATIC) $(MUSL_SYSROOT)/lib/crt1.o $(MUSL_SYSROOT)/lib/crti.o $(MUSL_SYSROOT)/lib/crtn.o $(MUSL_SYSROOT)/lib/libc.a $(RELIEFOS_LD_ID) FORCE $$(call if_changed,reliefos_static_app_$(1)) targets += $(2) .PHONY: app-$(1) diff --git a/system/initramfs/functions b/system/initramfs/functions index 64f2eb3f..e320ea6e 100644 --- a/system/initramfs/functions +++ b/system/initramfs/functions @@ -30,3 +30,33 @@ parse_cmdline() { case $root in *'"'*|*' '*|*'\'*|*'..'*) return 1 ;; esac return 0 } + +# Parse data only. The selected PARTUUID, when supplied by GRUB, must own the +# per-installation /boot view; a copied fstab cannot retarget another system. +parse_boot_fstab() { + esp_source= esp_type= esp_options= esp_point=/boot boot_bind= + [ -r "$1" ] || return 0 + while read -r source point type flags rest || [ -n "$source" ]; do + case $source in ''|'#'*) continue ;; esac + case $point in /boot|/efi) ;; *) continue ;; esac + if [ "$point" = /boot ] && [ "$type" = none ]; then + [ "$flags" = bind ] || return 1 + [ -z "$boot_bind" ] || return 1 + case $source in /efi/reliefos/instances/????????-????-????-????-????????????) ;; *) return 1 ;; esac + instance_uuid=${source##*/} + case $instance_uuid in *[!0-9a-f-]*) return 1 ;; esac + [ -z "${2:-}" ] || [ "$instance_uuid" = "$2" ] || return 1 + boot_bind=$source + continue + fi + case $type in vfat|fat32) ;; *) return 1 ;; esac + [ -z "$esp_source" ] || return 1 + esp_source=$source; esp_type=$type; esp_options=$flags; esp_point=$point + done < "$1" + if [ "$esp_point" = /efi ]; then + [ -n "$boot_bind" ] && [ -n "$esp_source" ] || return 1 + else + [ -z "$boot_bind" ] || return 1 + fi + return 0 +} diff --git a/system/initramfs/init b/system/initramfs/init index e777a3b2..d7eba50e 100644 --- a/system/initramfs/init +++ b/system/initramfs/init @@ -84,19 +84,19 @@ esac # must never attach an unrelated physical disk's ESP as their boot state. case $mode in installer|live) ;; *) - esp_source= esp_type= esp_options= - if [ -r /newroot/etc/fstab ]; then - while read -r source point type flags rest; do - case $source in ''|'#'*) continue ;; esac - [ "$point" = /boot ] || continue - [ -z "$esp_source" ] || fail 'multiple /boot entries in fstab' - esp_source=$source; esp_type=$type; esp_options=$flags - done diff --git a/tests/build/brand-allowlist.tsv b/tests/build/brand-allowlist.tsv index 219735f1..21eea2e2 100644 --- a/tests/build/brand-allowlist.tsv +++ b/tests/build/brand-allowlist.tsv @@ -53,6 +53,7 @@ main compatibility include/leonos/tar.h 逐文件确认含 canonical reliefos/re main compatibility include/leonos/text.h 逐文件确认含 canonical reliefos/reliefnt include;旧 guard/宏/类型名只转发别名,无第二份 struct/enum/typedef/extern 布局定义。 main compatibility include/leonos/text_input.h 逐文件确认含 canonical reliefos/reliefnt include;旧 guard/宏/类型名只转发别名,无第二份 struct/enum/typedef/extern 布局定义。 main compatibility include/reliefos/layout.h frozen task 6 guest layout values +main compatibility Documentation/kbuild/third-party.rst 说明 libleonos.so.2 旧 SONAME 的独立依赖与兼容边界 main compatibility scripts/reliefos/Makefile.apk 沿用用户已存在签名私钥路径、旧工具脚本源文件及兼容 APK protected_paths 文件名 main compatibility scripts/reliefos/Makefile.host LEONOS_* 旧输入变量回退,RELIEFOS_* 优先 main compatibility scripts/reliefos/Makefile.kernel NTCLKS_DIR/O 输入兼容及 ntclks-fetch 目标别名;旧外部 URL 说明 @@ -122,6 +123,7 @@ main compatibility tests/fixtures/brand-abi/old_api.c compiles a legacy SDK cons main compatibility tests/integration/brand-abi.sh compiles and checks both SDK ABIs main compatibility tests/integration/pam-product.sh PAM 模块历史符号导出检查 main compatibility tests/integration/test-apk-upgrade.sh 真实旧包数据库、world、签名和配置升级的回归输入 +main compatibility tests/integration/test-runtime-apk-upgrade.sh 旧命令包装包及旧前缀、版本和仓库 tag 的清除回归输入;新系统不保留这些包 main compatibility tests/integration/test-kernel-update.sh 真实旧系统引导菜单、旧 ESP /leonos 及旧更新命令入口的升级回滚测试 main compatibility tests/integration/test-motd-qemu.py 仅隔离诊断 ISO 已有 LEONOSMOTD volume ID;未改磁盘协议标签。 main compatibility tests/integration/test-official-tools.sh 断言旧非官方包/目录不得存在于新产品 @@ -146,7 +148,7 @@ main compatibility tools/build/rootfs-stage.sh 旧数据库拒绝策略、双 SO main compatibility tools/build/rpr-apps.sh APK metadata 显式 replaces:leonos-* 升级旧包 main compatibility tools/build/rpr-pages.sh 旧 RSA key 下载名、health marker 与旧世界包替换说明供已安装客户端 main compatibility tools/build/standalone-root.sh PROFILE 字节值 leonos-standalone-test-v1 明确冻结,目录已 canonical -main compatibility tools/build/upstream-app.sh 构建仍保留的 leonos_sqlite_vfs.c 源文件路径 +main compatibility tools/build/upstream-app.sh 构建脚本保留旧 /usr/lib/leonos 动态库搜索路径 main compatibility tools/build/upstream-busybox.sh 读取保留的 leonos.config 并过滤旧 CONFIG_LEONOS_* 端口选项 main compatibility tools/build/verify-pages.sh 验证已发布旧 RPR 公钥文件名同时存在 main compatibility tools/build_api.py 冻结 leonos-api 格式字段;显式双接受旧应用安装路径 @@ -273,7 +275,7 @@ main compatibility userland/runtime/include/leonos/syscall.h 逐文件确认含 main compatibility userland/runtime/include/leonos/text_input.h 逐文件确认含 canonical reliefos/reliefnt include;旧 guard/宏/类型名只转发别名,无第二份 struct/enum/typedef/extern 布局定义。 main compatibility userland/runtime/include/leonos/tls.h 逐文件确认含 canonical reliefos/reliefnt include;旧 guard/宏/类型名只转发别名,无第二份 struct/enum/typedef/extern 布局定义。 main compatibility userland/runtime/include/leonos/unix_ipc.h 逐文件确认含 canonical reliefos/reliefnt include;旧 guard/宏/类型名只转发别名,无第二份 struct/enum/typedef/extern 布局定义。 -main compatibility userland/runtime/include/leonos_mbedtls_config.h 旧 mbedTLS 配置头转发 canonical 并给旧 time 宏别名 +main compatibility userland/runtime/include/leonos_mbedtls_config.h 旧 mbedTLS 配置 include 路径转发官方匹配配置;不再使用私有 ALT 结构 main compatibility userland/runtime/include/ncurses.h canonical guard/库说明已为 RELIEFOS/libreliefos;仅 leonos_curses_window 旧公开类型宏别名。 main compatibility userland/runtime/include/reliefos/unix_ipc.h frozen task 6 guest socket paths main compatibility userland/runtime/src/admin.c 所有当前命中均为已发布旧 ELF 导出 alias 或说明,别名指向同一 canonical 实现。 @@ -304,8 +306,8 @@ main compatibility userland/runtime/src/tar.c 所有当前命中均为已发布 main compatibility userland/runtime/src/text_encoding.c 所有当前命中均为已发布旧 ELF 导出 alias 或说明,别名指向同一 canonical 实现。 main compatibility userland/runtime/src/tls.c 旧 leonos_* 已发布 ELF alias 符号,转发到当前 reliefos_* 实现 main compatibility userland/runtime/src/unix_ipc.c 所有当前命中均为已发布旧 ELF 导出 alias 或说明,别名指向同一 canonical 实现。 -main compatibility userland/sqlite/leonos_sqlite_vfs.c 仅文件名与 .zName="leonos" 为已注册 SQLite VFS 标识;私有 C 类型/函数已迁 reliefos。 -main compatibility userland/storage/leonos-apk-update 旧脚本名入口与旧 world/package prefix 显式迁到新包,不能清零升级识别 +main compatibility userland/storage/reliefos-apk-update 旧 world/package prefix 显式迁到新包,不能清零升级识别 +main compatibility userland/apps/installer/installer_update_model.c 只读识别旧系统 ID、配置命名空间与包名前缀,保留已安装系统的升级入口 main compatibility third_party/doomgeneric/README.leonos.md 保留既有端口说明文件名;当前标题正文已 ReliefOS main compatibility third_party/doomgeneric/doomgeneric/r_data.c LEONOS_DOOM 是现有第三方端口编译开关;当前说明已 ReliefOS main compatibility third_party/doomgeneric/doomgeneric/w_file_stdc.c LEONOS_DOOM 与旧公开 leonos/fs.h 保留第三方端口源兼容;当前说明已 ReliefOS diff --git a/tests/build/test-apk-ownership.sh b/tests/build/test-apk-ownership.sh index 70905664..d01da9f1 100644 --- a/tests/build/test-apk-ownership.sh +++ b/tests/build/test-apk-ownership.sh @@ -17,6 +17,11 @@ missing = sorted(required - groups.keys()) legacy = sorted(name for name in groups if name.startswith("leonos-")) if missing or legacy or policy.get("current_distributor") != "reliefos-build": raise SystemExit(f"APK ownership identity is not migrated: missing={missing}, legacy={legacy}, distributor={policy.get('current_distributor')}") +if {"sl", "ncurses", "sqlite"} & groups.keys(): + raise SystemExit("official terminal/SQLite packages must not create local APK ownership groups") +if policy.get("upstream_components") != {"sl": ["sl"], "ncurses": ["ncurses", "ncurses-terminfo"], + "sqlite": ["sqlite", "sqlite-libs"]}: + raise SystemExit("terminal/SQLite components must identify their real upstream packages") for app in ("reliefexcel", "reliefword", "reliefppt"): if app not in groups["reliefos-apps"]["components"]: raise SystemExit(f"{app} is missing from reliefos-apps ownership") diff --git a/tests/build/test-apk-stage-selection.sh b/tests/build/test-apk-stage-selection.sh index 287b8c29..01d94dde 100755 --- a/tests/build/test-apk-stage-selection.sh +++ b/tests/build/test-apk-stage-selection.sh @@ -32,6 +32,9 @@ printf '%s\t%s\t%s\n' "$lock" "$id" "$field" >> "$FAKE_DEPS_LOG" case $field:$id in feature:alpine-openrc|feature:alpine-xorg-fonts) printf 'base\n' ;; feature:alpine-xorg-server|feature:alpine-xterm|feature:alpine-icewm|feature:alpine-mesa-dri-gallium) printf 'xorg\n' ;; + feature:alpine-sl) printf 'component:sl\n' ;; + feature:alpine-ncurses) printf 'component:ncurses\n' ;; + feature:alpine-sqlite-libs) printf 'component:sqlite\n' ;; feature:alpine-broken-feature) printf 'weird\n' ;; url:alpine-icewm) printf 'https://fixture.invalid/apk/icewm-0.1.apk\n' ;; feature:*) echo "no dependency named \"$id\"" >&2; exit 1 ;; @@ -273,6 +276,21 @@ expect_no_grep "$s/own.log" 'etc/fixture-openrc.conf' expect_no_grep "$s/own.log" 'usr/bin/xorg-server' expect_grep "$s/apk.log" 'add reliefos-base icewm=0.1 mesa-dri-gallium=0.1 openrc=0.1 xorg-fonts=0.1 xorg-server=0.1 xterm=0.1' +# Optional binary packages follow IMAGE selection in both backends. +build_archive "$w/upstream/packages" sl 0.1 usr/bin/sl +build_archive "$w/upstream/packages" ncurses 0.1 usr/bin/tput +build_archive "$w/upstream/packages" sqlite-libs 0.1 usr/lib/libsqlite3.so.0 +printf 'sl\nsqlite\n' > "$w/raw/etc/reliefos/apk-components" +stage components +expect_present "$w/components/repository/sl-0.1.apk" +expect_present "$w/components/repository/sqlite-libs-0.1.apk" +expect_absent "$w/components/repository/ncurses-0.1.apk" +expect_no_grep "$w/components/own.log" 'usr/bin/sl' +: > "$w/raw/etc/reliefos/apk-components" +stage no-components +for package in sl ncurses sqlite-libs; do expect_absent "$w/no-components/repository/$package-0.1.apk"; done +rm "$w/upstream/packages/sl-0.1.apk" "$w/upstream/packages/ncurses-0.1.apk" "$w/upstream/packages/sqlite-libs-0.1.apk" + # --- marker failures stop staging before any scan or transaction --- for marker in '' 'bogus' 'reliefos extra' "reliefos${newline}xorg${newline}" 'xorg '; do set_marker "$marker" diff --git a/tests/build/test-boot-mount-policy.sh b/tests/build/test-boot-mount-policy.sh new file mode 100644 index 00000000..63e34d16 --- /dev/null +++ b/tests/build/test-boot-mount-policy.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -eu +src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +. "$src/system/initramfs/functions" +work=$(mktemp -d "${TMPDIR:-/tmp}/reliefos-boot-policy.XXXXXX") +trap 'rm -rf "$work"' EXIT HUP INT TERM +uuid=12345678-1234-4567-89ab-123456789abc +printf 'PARTUUID=esp /efi vfat defaults 0 2\n/efi/reliefos/instances/%s /boot none bind 0 0\n' "$uuid" > "$work/fstab" +parse_boot_fstab "$work/fstab" "$uuid" +[ "$esp_source" = PARTUUID=esp ] && [ "$esp_point" = /efi ] +[ "$boot_bind" = "/efi/reliefos/instances/$uuid" ] +if parse_boot_fstab "$work/fstab" 00000000-0000-0000-0000-000000000001; then exit 1; fi +printf 'PARTUUID=legacy /boot vfat defaults 0 2\n' > "$work/fstab" +parse_boot_fstab "$work/fstab" "$uuid" +[ "$esp_point" = /boot ] && [ -z "$boot_bind" ] +for bad in duplicate missing traversal mixed flags; do + printf 'PARTUUID=esp /efi vfat defaults 0 2\n' > "$work/fstab" + case $bad in + duplicate) printf 'PARTUUID=other /boot vfat defaults 0 2\n' >> "$work/fstab" ;; + traversal) printf '/efi/../outside /boot none bind 0 0\n' >> "$work/fstab" ;; + mixed) printf 'PARTUUID=esp /boot vfat defaults 0 2\n/efi/reliefos/instances/%s /boot none bind 0 0\n' "$uuid" > "$work/fstab" ;; + flags) printf '/efi/reliefos/instances/%s /boot none bind,ro 0 0\n' "$uuid" >> "$work/fstab" ;; + esac + if parse_boot_fstab "$work/fstab" "$uuid"; then exit 1; fi +done +mkdir "$work/root" +${HOSTCC:-cc} -std=gnu11 -O1 -g -Wall -Wextra -Werror -D_GNU_SOURCE \ + -fsanitize=address,undefined -fno-sanitize-recover=all -ffunction-sections -fdata-sections \ + -I"$src/userland/runtime/include" "$src/tests/host/test_installer_boot_layout.c" \ + -Wl,--gc-sections -o "$work/layout" +"$work/layout" "$work/root" +first=$(RELIEFOS_IMAGE_INSTANCE=first sh "$src/tools/build/image-identity.sh" root) +again=$(RELIEFOS_IMAGE_INSTANCE=first sh "$src/tools/build/image-identity.sh" root) +second=$(RELIEFOS_IMAGE_INSTANCE=second sh "$src/tools/build/image-identity.sh" root) +esp=$(RELIEFOS_IMAGE_INSTANCE=first sh "$src/tools/build/image-identity.sh" esp) +[ "$first" = "$again" ] && [ "$first" != "$second" ] && [ "$first" != "$esp" ] +if RELIEFOS_IMAGE_INSTANCE='../invalid' sh "$src/tools/build/image-identity.sh" root >/dev/null 2>&1; then exit 1; fi +printf 'boot policy: per-root binding, shared ESP, legacy layout, unsafe fstab and deterministic instance IDs passed\n' diff --git a/tests/build/test-bootloader-uefi.sh b/tests/build/test-bootloader-uefi.sh new file mode 100644 index 00000000..27a589c0 --- /dev/null +++ b/tests/build/test-bootloader-uefi.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -eu +src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +work=$(mktemp -d "${TMPDIR:-/tmp}/reliefos-uefi.XXXXXX") +trap 'rm -rf "$work"' EXIT HUP INT TERM +. "$src/tests/lib/runtime-dependencies.sh" +${HOSTCC:-cc} -std=gnu11 -O1 -g -Wall -Wextra -Werror -D_GNU_SOURCE \ + -fsanitize=address,undefined -fno-sanitize-recover=all -ffunction-sections -fdata-sections \ + -I"$work" -I"$src/userland/runtime/include" -I"$runtime_deps/usr/include" \ + "$src/tests/host/test_bootloader_uefi.c" "$src/userland/runtime/src/bootloader_uefi.c" \ + "$src/userland/runtime/src/bootloader.c" \ + "$src/userland/runtime/src/bootloader_publish.c" "$src/userland/runtime/src/kernel_bundle_io.c" \ + "$runtime_deps/usr/lib/libmbedcrypto.a" -pthread \ + -Wl,--gc-sections -o "$work/verify" +"$work/verify" "$work" diff --git a/tests/build/test-components-metadata.sh b/tests/build/test-components-metadata.sh index 99d5c275..d8189611 100644 --- a/tests/build/test-components-metadata.sh +++ b/tests/build/test-components-metadata.sh @@ -18,7 +18,7 @@ awk -F '\t' '$1=="reliefdiagram" {if($3!=1 || $4!=0 || $5!=0)exit 1;found=1} END printf '# CONFIG_RELIEFOS_COMPONENT_APP_RELIEFDIAGRAM_BUILD is not set\n' > "$w/diagram-config" "$w/components" --input "$src/configs/components.toml" --config "$w/diagram-config" --output "$w/diagram-mk" --metadata "$w/diagram-tsv" awk -F '\t' '$1=="reliefdiagram" {if($3!=0 || $4!=0 || $5!=0)exit 1;found=1} END{if(!found)exit 1}' "$w/diagram-tsv" -awk -F '\t' '$1=="reliefplayer" {if($3!=1 || $4!=1 || $5!=1 || $8!="ReliefPlayer" || $9!="Network and multimedia" || $10!=".mp3,.wav" || $11!=1)exit 1;found=1} END{if(!found)exit 1}' "$w/tsv" +awk -F '\t' '$1=="reliefplayer" {if($3!=1 || $4!=1 || $5!=1 || $8!="ReliefPlayer" || $9!="Network and multimedia" || $10!=".mp3,.wav,.mp4" || $11!=1)exit 1;found=1} END{if(!found)exit 1}' "$w/tsv" printf '# CONFIG_RELIEFOS_COMPONENT_APP_RELIEFPLAYER_IMAGE is not set\n' > "$w/player-config" "$w/components" --input "$src/configs/components.toml" --config "$w/player-config" --output "$w/player-mk" --metadata "$w/player-tsv" awk -F '\t' '$1=="reliefplayer" {if($3!=1 || $4!=0 || $5!=0)exit 1;found=1} END{if(!found)exit 1}' "$w/player-tsv" diff --git a/tests/build/test-deps.sh b/tests/build/test-deps.sh index 57323023..8ca406da 100755 --- a/tests/build/test-deps.sh +++ b/tests/build/test-deps.sh @@ -162,6 +162,10 @@ expect_output_is 'st font dependency is pinned' 2.1.5-r2 \ "$deps" --lock "$lock" --id alpine-font-liberation --print version expect_output_is 'unmarked entry defaults to base' base \ "$deps" --lock "$lock" --id alpine-openrc --print feature +expect_output_is 'optional APK keeps component feature' component:sl \ + "$deps" --lock "$lock" --id alpine-sl --print feature +expect_output_is 'runtime PNG is available in both backends' base \ + "$deps" --lock "$lock" --id alpine-libpng --print feature expect_failure 'invalid feature is rejected' \ "$deps" --lock "$(entry_lock '{"id":"a","kind":"apk","version":"1","url":"https://x/a.apk","sha256":"'$D'","directory":"a-1","license_in_source":".PKGINFO","feature":"desktop"}')" --check @@ -228,12 +232,12 @@ check 'a complete entry passes with --root' "$deps" --lock "$work/clean.json" -- # --- patch queries ------------------------------------------------------------ expect_output_is 'musl reports the digest of the patch it must apply' \ 'e336ed20b2d338dbbf88588dff043e8ce349d0d0044277d31be229d600bb93ad' \ - sh -c "$deps --lock $lock --id musl --list-patches | cut -f1" + sh -c "$deps --lock $lock --id musl --list-patches | head -n1 | cut -f1" expect_output_is 'and the path of that patch' \ 'patches/musl/0001-enforce-password-file-lock.patch' \ - sh -c "$deps --lock $lock --id musl --list-patches | cut -f2" + sh -c "$deps --lock $lock --id musl --list-patches | head -n1 | cut -f2" expect_output_is 'a dependency without patches prints nothing' '' \ - "$deps" --lock "$lock" --id zlib --list-patches + "$deps" --lock "$lock" --id alpine-zlib --list-patches expect_failure '--list-patches without --id is rejected' \ "$deps" --lock "$lock" --list-patches expect_failure '--print without --id is rejected' \ diff --git a/tests/build/test-desktop-backend.sh b/tests/build/test-desktop-backend.sh index adf84459..e77732c0 100755 --- a/tests/build/test-desktop-backend.sh +++ b/tests/build/test-desktop-backend.sh @@ -89,6 +89,17 @@ printf 'math GUI fixture\n' > "$work/out/userland/reliefmath.elf" printf 'math worker fixture\n' > "$work/out/userland/reliefmath-render.elf" printf 'reliefmathpp\tprogram-app\t1\t1\t1\t0\t0\tReliefMath++\tOffice applications\t.rmathpp\t1\n' >> "$work/metadata" printf 'geometry GUI fixture\n' > "$work/out/userland/reliefmathpp.elf" +printf 'reliefplayer\tprogram-app\t1\t1\t1\t0\t0\tReliefPlayer\tNetwork and multimedia\t.mp3,.wav,.mp4\t1\n' >> "$work/metadata" +printf 'media player fixture\n' > "$work/out/userland/reliefplayer.elf" +mkdir -p "$work/out/upstream/ffmpeg/root/usr/lib" \ + "$work/out/upstream/ffmpeg/root/usr/include/libavcodec" \ + "$work/out/upstream/ffmpeg/root/usr/share/licenses/ffmpeg" +for lib in avformat avcodec avutil swresample swscale; do + printf 'codec fixture\n' > "$work/out/upstream/ffmpeg/root/usr/lib/lib$lib-reliefplayer.so.62.1.0" + ln -s "lib$lib-reliefplayer.so.62.1.0" "$work/out/upstream/ffmpeg/root/usr/lib/lib$lib-reliefplayer.so.62" +done +printf 'development header\n' > "$work/out/upstream/ffmpeg/root/usr/include/libavcodec/avcodec.h" +printf 'LGPL fixture\n' > "$work/out/upstream/ffmpeg/root/usr/share/licenses/ffmpeg/COPYING.LGPLv2.1" printf 'audio fixture\n' > "$work/out/system/lib/libreliefos-audio.so.1" printf 'calc\tprogram-app\t0\t0\t1\t0\t0\tCalculator\tOffice applications\t\t0\n' >> "$work/metadata" printf 'ncurses\tlibrary\t1\t1\t0\t1\t0\tncurses\tLibraries\t\t0\n' >> "$work/metadata" @@ -121,11 +132,26 @@ cat > "$work/rootfs.config" < "$work/metadata-no-player" +ROOTFS_UPSTREAM_PACKAGES='sudo shadow util-linux fixture-provider ffmpeg' \ +sh "$repo_root/tools/build/rootfs-stage.sh" "$work/src" "$work/out" \ + "$work/rootfs.config" "$work/metadata-no-player" "$work/stage" "$work/layout" \ + "$work/root-no-player" "$work/no-player-manifest.json" 1700000000 +test ! -e "$work/root-no-player/usr/bin/reliefplayer" +test ! -e "$work/root-no-player/usr/lib/libavformat-reliefplayer.so.62" +test ! -e "$work/root-no-player/usr/share/licenses/ffmpeg" # Only self-authored graphical apps receive artwork, including hidden entries. for app in fileman installer leonmmcoset reliefword reliefppt reliefmath reliefmathpp; do @@ -168,7 +194,7 @@ done # Removing the selected app from component metadata excludes every private # product while leaving the shared system CJK font in the composed root. awk -F '\t' '$1!="reliefmath"' "$work/metadata" > "$work/metadata-no-reliefmath" -ROOTFS_UPSTREAM_PACKAGES='sudo shadow util-linux ncurses fixture-provider' \ +ROOTFS_UPSTREAM_PACKAGES='sudo shadow util-linux fixture-provider' \ sh "$repo_root/tools/build/rootfs-stage.sh" "$work/src" "$work/out" \ "$work/rootfs.config" "$work/metadata-no-reliefmath" "$work/stage" "$work/layout" \ "$work/root-no-reliefmath" "$work/no-reliefmath-manifest.json" 1700000000 @@ -253,11 +279,13 @@ test ! -e "$xorg_root/bin/ping" # provider; BusyBox must not claim the command. test -L "$xorg_root/sbin/mkfs.ext4" test "$(readlink "$xorg_root/sbin/mkfs.ext4")" = ../usr/sbin/mkfs.ext4 -cmp "$work/out/upstream/ncurses/root/usr/bin/clear" "$xorg_root/usr/bin/clear" -test -L "$xorg_root/usr/bin/reset" -test "$(readlink "$xorg_root/usr/bin/reset")" = clear -test -f "$xorg_root/usr/share/terminfo/fixture" -test -f "$xorg_root/etc/terminfo/fixture" +# ncurses payloads are installed later from signed APKs, never overlaid. +grep -Fx ncurses "$xorg_root/etc/reliefos/apk-components" +test ! -e "$xorg_root/usr/share/doc/reliefos/apk-transitions" +test ! -e "$xorg_root/usr/lib/reliefos/apps/sl" +test -f "$xorg_root/usr/share/licenses/ncurses/LICENSE" +test ! -e "$xorg_root/usr/share/terminfo/fixture" +test ! -e "$xorg_root/etc/terminfo/fixture" test ! -e "$xorg_root/usr/include/curses.h" cmp "$work/out/upstream/shadow/root/usr/bin/passwd" "$xorg_root/usr/bin/passwd" test ! -e "$xorg_root/bin/passwd" diff --git a/tests/build/test-image-adapters.sh b/tests/build/test-image-adapters.sh index 29c95236..70e4a632 100644 --- a/tests/build/test-image-adapters.sh +++ b/tests/build/test-image-adapters.sh @@ -92,7 +92,8 @@ for file in boot/grub/installer.cfg boot/grub/live.cfg; do done require_text "$src/boot/grub/grub.cfg" 'multiboot2 $reliefos_kernel' 'installed GRUB boots the selected checked version' require_file "$w/esp/grub/kernel-slots.cfg" 'ESP includes the stable kernel slot selector' -require_text "$src/boot/grub/grub.cfg" 'multiboot2 /leonos/kernel.sys' 'installed GRUB config retains the legacy boot entry' +require_text "$src/boot/grub/grub.cfg" 'Previous complete kernel' 'installed GRUB retains an actual complete previous kernel' +require_file "$w/esp/grub/other-systems.cfg" 'ESP includes multi-system discovery' for file in boot/grub/installer.cfg boot/grub/live.cfg; do require_text "$src/$file" 'module2 /install/root.fat reliefos-installer-root' "$file keeps the installer root module" done @@ -104,18 +105,18 @@ for file in boot/grub/grub.cfg boot/grub/installer.cfg boot/grub/live.cfg boot/g printf 'ok - %s does not reference the retired loader\n' "$file" fi done -require_text "$src/boot/grub/installer_embedded.cfg" 'reliefos-installer-iso.marker' 'embedded GRUB discovers the new ISO marker' -require_text "$src/boot/grub/installer_embedded.cfg" 'leonos-installer-iso.marker' 'embedded GRUB still discovers old ISO media' -require_text "$src/tools/build/iso.sh" 'reliefos-installer-iso.marker' 'ISO staging writes the new installer marker' -require_text "$src/tools/build/iso.sh" 'leonos-installer-iso.marker' 'ISO staging keeps the legacy marker for old loaders' +require_text "$src/boot/grub/installer_embedded.cfg" '"$cmdpath"' 'embedded GRUB binds to its own boot medium first' +require_text "$src/boot/grub/installer_embedded.cfg" '@RELIEFOS_ISO_MARKER@' 'ISO marker is specialized per medium' +require_text "$src/tools/build/iso.sh" 'marker=reliefos-$volume.marker' 'Live and installer use distinct fallback markers' +require_text "$src/tools/build/iso.sh" 's/@RELIEFOS_ISO_MARKER@/$marker/g' 'ISO stage publishes its matching embedded identity' require_text "$src/scripts/reliefos/Makefile.images" '$(O_IMAGES)/reliefos.vmdk' 'image target uses the ReliefOS VMDK name' require_text "$src/scripts/reliefos/Makefile.images" '$(O_IMAGES)/reliefos-live.iso' 'image target uses the ReliefOS live ISO name' require_text "$src/scripts/reliefos/Makefile.images" '$(O_IMAGES)/reliefos-installer.iso' 'image target uses the ReliefOS installer ISO name' require_text "$src/tools/build/disk.sh" 'name=RELIEFOS_ESP' 'GPT ESP partition has the canonical name' require_text "$src/tools/build/disk.sh" 'name=RELIEFOS_ROOT' 'GPT root partition has the canonical name' require_text "$src/tools/build/disk.sh" '-n RELIEFOS' 'FAT label fits the eight-character limit' -require_text "$src/tools/build/disk.sh" '41A3EE19-BA85-47A0-9705-A5C128374021' 'published ESP UUID is unchanged' -require_text "$src/tools/build/disk.sh" '5C13543B-732C-4F81-8652-621124484420' 'published root UUID is unchanged' +require_text "$src/tools/build/disk.sh" 'esp_uuid=$(sh "$src/tools/build/image-identity.sh" esp)' 'ESP UUID comes from the configured instance identity' +require_text "$src/tools/build/disk.sh" 'root_uuid=$(sh "$src/tools/build/image-identity.sh" root)' 'root UUID comes from the same instance identity' require_text "$src/userland/apps/installer/install_ops.h" 'TARGET_BOOT "/reliefos/kernel.sys"' 'installer detects the canonical ESP kernel path' require_text "$src/userland/apps/installer/install_ops.h" 'TARGET_BOOT "/leonos/kernel.sys"' 'installer recognizes a legacy ESP kernel path' if [ "$failures" -ne 0 ]; then exit 1; fi diff --git a/tests/build/test-installer-update-model.sh b/tests/build/test-installer-update-model.sh new file mode 100755 index 00000000..c2af7d11 --- /dev/null +++ b/tests/build/test-installer-update-model.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu +ulimit -c 0 +src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd -P) +work=$(mktemp -d "${TMPDIR:-$src/out/tmp}/installer-update-model.XXXXXX") +trap 'rm -rf "$work"' EXIT HUP INT TERM +mkdir "$work/root" +${HOSTCC:-cc} -std=c11 -D_GNU_SOURCE -Wall -Wextra -Werror \ + -fsanitize=address,undefined -fno-sanitize-recover=all -g \ + -I"$src/userland/apps/installer" \ + "$src/tools/tests/installer_update_model_test.c" \ + "$src/userland/apps/installer/installer_update_model.c" \ + "$src/userland/apps/installer/installer_update_journal.c" -o "$work/test" +ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 "$work/test" "$work/root" diff --git a/tests/build/test-reliefapk-certificates.sh b/tests/build/test-reliefapk-certificates.sh index d1de7497..a1b40271 100755 --- a/tests/build/test-reliefapk-certificates.sh +++ b/tests/build/test-reliefapk-certificates.sh @@ -4,15 +4,16 @@ src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) TMPDIR="$src/out/tmp" export TMPDIR mkdir -p "$TMPDIR" +. "$src/tests/lib/runtime-dependencies.sh" work=$(mktemp -d "$TMPDIR/reliefapk-ca.XXXXXX") trap 'rm -rf "$work"' EXIT HUP INT TERM mkdir -m 700 "$work/trust" ${HOSTCXX:-c++} -std=c++17 -Wall -Wextra -Werror -fno-exceptions -fno-rtti -nostdinc++ \ -fsanitize=address,undefined -fno-sanitize-recover=all -g \ - -I"$src/userland/apps/reliefapk" -I/usr/include/mbedtls3 \ + -I"$src/userland/apps/reliefapk" -I"$runtime_deps/usr/include" \ "$src/tools/tests/reliefapk_certificates_test.cpp" \ "$src/userland/apps/reliefapk/certificate_io.cpp" \ - -L/usr/lib/mbedtls3 -Wl,-rpath,/usr/lib/mbedtls3 -lmbedx509 -lmbedcrypto \ + "$runtime_deps/usr/lib/libmbedx509.a" "$runtime_deps/usr/lib/libmbedcrypto.a" -pthread \ -o "$work/recover" ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \ "$work/recover" "$work/trust" "$src/system/certs/cacert.pem" diff --git a/tests/build/test-reliefmail.sh b/tests/build/test-reliefmail.sh index a4361f37..2c8e8bc2 100755 --- a/tests/build/test-reliefmail.sh +++ b/tests/build/test-reliefmail.sh @@ -1,6 +1,7 @@ #!/bin/sh set -eu src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +. "$src/tests/lib/runtime-dependencies.sh" tmp="$src/out/tmp" export TMPDIR="$tmp" mkdir -p "$tmp" @@ -30,7 +31,7 @@ run_protocol() { set -- -std=c++17 -D_POSIX_C_SOURCE=200809L -Wall -Wextra -Werror \ -fno-exceptions -fno-rtti -nostdinc++ -fsanitize=address,undefined \ -fno-sanitize-recover=all -g -I"$src/userland/apps/reliefmail" \ - -I/usr/include/mbedtls3 \ + -I"$runtime_deps/usr/include" \ "$src/tests/host/test_reliefmail_protocol.cpp" \ "$src/userland/apps/reliefmail/model.cpp" \ "$src/userland/apps/reliefmail/mime.cpp" \ @@ -43,8 +44,7 @@ run_protocol() { "$src/userland/apps/reliefmail/imap.cpp" \ "$src/userland/apps/reliefmail/smtp.cpp" fi - "${HOSTCXX:-c++}" "$@" -L/usr/lib/mbedtls3 \ - -Wl,-rpath,/usr/lib/mbedtls3 -lmbedtls -lmbedx509 -lmbedcrypto \ + "${HOSTCXX:-c++}" "$@" "$runtime_deps/usr/lib/libmbedtls.a" "$runtime_deps/usr/lib/libmbedx509.a" "$runtime_deps/usr/lib/libmbedcrypto.a" -pthread \ -o "$work/reliefmail-protocol" build_worker @@ -77,44 +77,33 @@ run_protocol() { "${PYTHON:-python3}" "$fixture" --verify-audit "$audit_file" } -build_sqlite_core() { - if [ -f "$work/sqlite_core.o" ]; then return; fi - generated="$src/out/xorg-vmware-debug/upstream/app-sqlite/generated" - ${HOSTCC:-cc} -std=c11 -D_POSIX_C_SOURCE=200809L -Wall -Wextra -Werror \ - -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_OMIT_WAL=1 \ - -DSQLITE_MAX_MMAP_SIZE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -I"$generated" -c \ - "$src/userland/apps/reliefmail/sqlite_core.c" -o "$work/sqlite_core.o" -} - build_store() { - build_sqlite_core "${HOSTCXX:-c++}" -std=c++17 -D_POSIX_C_SOURCE=200809L -DRELIEFMAIL_TESTING \ -Wall -Wextra -Werror -fno-exceptions -fno-rtti -nostdinc++ \ -fsanitize=address,undefined -fno-sanitize-recover=all -g \ - -I"$src/userland/apps/reliefmail" -I"$src/out/xorg-vmware-debug/upstream/app-sqlite/generated" \ - -I/usr/include/mbedtls3 "$src/tests/host/test_reliefmail_store.cpp" \ + -I"$src/userland/apps/reliefmail" \ + -I"$runtime_deps/usr/include" "$src/tests/host/test_reliefmail_store.cpp" \ "$src/userland/apps/reliefmail/store.cpp" "$src/userland/apps/reliefmail/model.cpp" \ "$src/userland/apps/reliefmail/mime.cpp" "$src/userland/apps/reliefmail/html.cpp" \ "$src/userland/apps/reliefmail/profile_io.cpp" "$src/userland/apps/reliefmail/vault.cpp" \ - "$work/sqlite_core.o" -L/usr/lib/mbedtls3 -Wl,-rpath,/usr/lib/mbedtls3 -lmbedcrypto \ + "$runtime_deps/usr/lib/libsqlite3.a" "$runtime_deps/usr/lib/libmbedcrypto.a" -pthread \ -o "$work/reliefmail-store" } build_worker() { - build_sqlite_core "${HOSTCXX:-c++}" -std=c++17 -D_POSIX_C_SOURCE=200809L -DRELIEFMAIL_TESTING \ -Wall -Wextra -Werror -fno-exceptions -fno-rtti -nostdinc++ \ -fsanitize=address,undefined -fno-sanitize-recover=all -g \ - -I"$src/userland/apps/reliefmail" -I"$src/out/xorg-vmware-debug/upstream/app-sqlite/generated" \ - -I/usr/include/mbedtls3 "$src/tests/host/test_reliefmail_worker.cpp" \ + -I"$src/userland/apps/reliefmail" \ + -I"$runtime_deps/usr/include" "$src/tests/host/test_reliefmail_worker.cpp" \ "$src/userland/apps/reliefmail/worker.cpp" "$src/userland/apps/reliefmail/tasks.cpp" \ "$src/userland/apps/reliefmail/store.cpp" "$src/userland/apps/reliefmail/model.cpp" \ "$src/userland/apps/reliefmail/mime.cpp" "$src/userland/apps/reliefmail/html.cpp" \ "$src/userland/apps/reliefmail/reader_render.cpp" \ "$src/userland/apps/reliefmail/imap.cpp" "$src/userland/apps/reliefmail/smtp.cpp" \ "$src/userland/apps/reliefmail/transport.cpp" "$src/userland/apps/reliefmail/profile_io.cpp" \ - "$src/userland/apps/reliefmail/vault.cpp" "$work/sqlite_core.o" \ - -L/usr/lib/mbedtls3 -Wl,-rpath,/usr/lib/mbedtls3 -lmbedtls -lmbedx509 -lmbedcrypto -lpng16 -lz \ + "$src/userland/apps/reliefmail/vault.cpp" "$runtime_deps/usr/lib/libsqlite3.a" \ + "$runtime_deps/usr/lib/libmbedtls.a" "$runtime_deps/usr/lib/libmbedx509.a" "$runtime_deps/usr/lib/libmbedcrypto.a" -pthread -lpng16 -lz \ -o "$work/reliefmail-worker" } @@ -158,14 +147,14 @@ if [ "${1:-all}" = task6-adapters ]; then "${HOSTCXX:-c++}" -std=c++17 -D_POSIX_C_SOURCE=200809L -Wall -Wextra -Werror \ -fno-exceptions -fno-rtti -nostdinc++ -fsanitize=address,undefined \ -fno-sanitize-recover=all -g -I"$src/userland/apps/reliefmail" \ - -I/usr/include/mbedtls3 \ + -I"$runtime_deps/usr/include" \ "$src/tests/host/test_reliefmail_task6_adapters.cpp" \ "$src/userland/apps/reliefmail/ui_adapters.cpp" \ "$src/userland/apps/reliefmail/model.cpp" \ "$src/userland/apps/reliefmail/vault.cpp" \ "$src/userland/apps/reliefmail/profile_io.cpp" \ "$src/userland/apps/reliefmail/html.cpp" \ - -L/usr/lib/mbedtls3 -Wl,-rpath,/usr/lib/mbedtls3 -lmbedcrypto \ + "$runtime_deps/usr/lib/libmbedcrypto.a" -pthread \ -o "$work/reliefmail-task6-adapters" ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 "$work/reliefmail-task6-adapters" printf 'ReliefMail Task6 adapter suite passed\n' @@ -231,11 +220,11 @@ if [ "${1:-all}" = vault-task5 ]; then "${HOSTCXX:-c++}" -std=c++17 -D_POSIX_C_SOURCE=200809L -DRELIEFMAIL_TESTING \ -Wall -Wextra -Werror -fno-exceptions -fno-rtti -nostdinc++ \ -fsanitize=address,undefined -fno-sanitize-recover=all -g \ - -I"$src/userland/apps/reliefmail" -I/usr/include/mbedtls3 \ + -I"$src/userland/apps/reliefmail" -I"$runtime_deps/usr/include" \ "$src/tests/host/test_reliefmail_vault.cpp" \ "$src/userland/apps/reliefmail/vault.cpp" \ "$src/userland/apps/reliefmail/profile_io.cpp" \ - -L/usr/lib/mbedtls3 -Wl,-rpath,/usr/lib/mbedtls3 -lmbedcrypto \ + "$runtime_deps/usr/lib/libmbedcrypto.a" -pthread \ -o "$work/reliefmail-vault" ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \ "$work/reliefmail-vault" --task5-password-change @@ -281,11 +270,11 @@ printf 'ReliefMail store suite passed\n' ${HOSTCXX:-c++} -std=c++17 -D_POSIX_C_SOURCE=200809L -DRELIEFMAIL_TESTING \ -Wall -Wextra -Werror -fno-exceptions -fno-rtti -nostdinc++ \ -fsanitize=address,undefined -fno-sanitize-recover=all -g \ - -I"$src/userland/apps/reliefmail" -I/usr/include/mbedtls3 \ + -I"$src/userland/apps/reliefmail" -I"$runtime_deps/usr/include" \ "$src/tests/host/test_reliefmail_vault.cpp" \ "$src/userland/apps/reliefmail/vault.cpp" \ "$src/userland/apps/reliefmail/profile_io.cpp" \ - -L/usr/lib/mbedtls3 -Wl,-rpath,/usr/lib/mbedtls3 -lmbedcrypto \ + "$runtime_deps/usr/lib/libmbedcrypto.a" -pthread \ -o "$work/reliefmail-vault" ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 "$work/reliefmail-vault" printf 'ReliefMail vault suite passed\n' diff --git a/tests/build/test-reliefplayer-mp4.sh b/tests/build/test-reliefplayer-mp4.sh new file mode 100644 index 00000000..1d2e6060 --- /dev/null +++ b/tests/build/test-reliefplayer-mp4.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -eu +src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +work=$(mktemp -d "${TMPDIR:-$src/out/tmp}/reliefplayer-mp4.XXXXXX") +trap 'rm -rf "$work"' EXIT HUP INT TERM +ffmpeg -v error -nostdin -f lavfi -i testsrc2=size=320x180:rate=12:duration=2 \ + -itsoffset 0.3 -f lavfi -i sine=frequency=440:sample_rate=44100:duration=1.7 \ + -c:v libx264 -threads 1 -g 24 -bf 2 -pix_fmt yuv420p -c:a aac -ac 2 "$work/video.mp4" +ffmpeg -v error -nostdin -i "$work/video.mp4" -an -c:v copy -movflags +faststart "$work/silent.mp4" +ffmpeg -v error -nostdin -i "$work/video.mp4" -c:v copy -c:a alac "$work/unsupported.mp4" +ffmpeg -v error -nostdin -display_rotation 90 -i "$work/silent.mp4" -c copy "$work/rotated.mp4" +ffmpeg -v error -nostdin -i "$work/silent.mp4" -c:v libx265 -x265-params pools=none:frame-threads=1:log-level=error -t 0.5 "$work/hevc.mp4" +ffmpeg -v error -nostdin -i "$work/silent.mp4" -c:v mpeg4 -t 0.5 "$work/mpeg4.mp4" +head -c 100 "$work/video.mp4" > "$work/truncated.mp4" +${HOSTCXX:-c++} -std=c++17 -O2 -Wall -Wextra -Werror -nostdinc++ -fno-exceptions -fno-rtti \ + ${PLAYER_TEST_FLAGS:-} "$src/tests/host/test_reliefplayer_mp4.cpp" \ + "$src/userland/apps/reliefplayer/decoder.cpp" "$src/userland/apps/reliefplayer/media.cpp" \ + "$src/userland/apps/reliefplayer/pcm.cpp" "$src/userland/apps/reliefplayer/session.cpp" \ + "$src/userland/apps/reliefplayer/worker.cpp" -I"$src/third_party/minimp3" \ + $(pkg-config --cflags --libs libavformat libavcodec libavutil libswresample libswscale) -lm -o "$work/verify" +if [ -n "${PLAYER_TARGET_BUILD:-}" ]; then + codecs=$PLAYER_TARGET_BUILD/upstream/ffmpeg/root + musl=$PLAYER_TARGET_BUILD/sysroot/musl + ${TARGETCXX:-clang++} --target=x86_64-linux-musl --sysroot="$musl" --gcc-toolchain=/nonexistent \ + -fuse-ld=lld --rtlib=compiler-rt --unwindlib=none -std=c++17 -O2 \ + -nostdinc++ -nostdlib++ -fno-exceptions -fno-rtti -D_GNU_SOURCE -I"$src/third_party/minimp3" -I"$codecs/usr/include" \ + -I"$PLAYER_TARGET_BUILD/kernel-export/include" \ + "$src/tests/host/test_reliefplayer_mp4.cpp" "$src/userland/apps/reliefplayer/decoder.cpp" \ + "$src/userland/apps/reliefplayer/media.cpp" "$src/userland/apps/reliefplayer/pcm.cpp" \ + "$src/userland/apps/reliefplayer/session.cpp" "$src/userland/apps/reliefplayer/worker.cpp" \ + -L"$musl/lib" -L"$codecs/usr/lib" -lavformat-reliefplayer -lavcodec-reliefplayer \ + -lswresample-reliefplayer -lswscale-reliefplayer -lavutil-reliefplayer -lm -o "$work/verify-target" + "$musl/lib/libc.so" --library-path "$codecs/usr/lib:$musl/lib" "$work/verify-target" \ + "$work/video.mp4" "$work/silent.mp4" "$work/truncated.mp4" "$work/unsupported.mp4" \ + "$work/rotated.mp4" "$work/hevc.mp4" "$work/mpeg4.mp4" +fi +"$work/verify" "$work/video.mp4" "$work/silent.mp4" "$work/truncated.mp4" "$work/unsupported.mp4" \ + "$work/rotated.mp4" "$work/hevc.mp4" "$work/mpeg4.mp4" diff --git a/tests/build/test-reliefplayer-ownership.sh b/tests/build/test-reliefplayer-ownership.sh index c02225ab..8fe6972f 100644 --- a/tests/build/test-reliefplayer-ownership.sh +++ b/tests/build/test-reliefplayer-ownership.sh @@ -21,3 +21,12 @@ if ! awk -F '\t' '$2=="dir" {next} $1!="reliefos-reliefplayer" {bad=1} {count++} cat "$work/rows"; exit 1 fi printf 'ReliefPlayer ownership: executable, launcher, desktop entry and artwork travel together\n' +mkdir -p "$work/root/usr/share/licenses/ffmpeg" +printf 'LGPL fixture\n' > "$work/root/usr/share/licenses/ffmpeg/COPYING.LGPLv2.1" +for lib in avformat avcodec avutil swresample swscale; do + printf 'library fixture\n' > "$work/root/usr/lib/lib$lib-reliefplayer.so.62.1.0" + ln -s "lib$lib-reliefplayer.so.62.1.0" "$work/root/usr/lib/lib$lib-reliefplayer.so.62" +done +"$work/own" --policy "$src/configs/apk-ownership.json" --root "$work/root" --output "$work/rows-codecs" +awk -F '\t' '$2=="dir" {next} $4 ~ /-reliefplayer.so|licenses\/ffmpeg/ {if($1!="reliefos-media-codecs")exit 1;count++} END {if(count!=11)exit 1}' "$work/rows-codecs" +printf 'ReliefPlayer codec ownership: private SONAMEs, versioned links and LGPL notice grouped separately\n' diff --git a/tests/build/test-reliefplayer-session.sh b/tests/build/test-reliefplayer-session.sh index 65e6767b..c6cd721f 100644 --- a/tests/build/test-reliefplayer-session.sh +++ b/tests/build/test-reliefplayer-session.sh @@ -7,11 +7,13 @@ ${HOSTCXX:-c++} -std=c++17 -O2 -Wall -Wextra -Werror -nostdinc++ -fno-exceptions "$src/tests/host/test_reliefplayer_session.cpp" \ "$src/userland/apps/reliefplayer/decoder.cpp" "$src/userland/apps/reliefplayer/pcm.cpp" \ "$src/userland/apps/reliefplayer/worker.cpp" "$src/userland/apps/reliefplayer/session.cpp" \ - -I"$src/third_party/minimp3" -lm -o "$work/verify" + "$src/userland/apps/reliefplayer/media.cpp" \ + -I"$src/third_party/minimp3" $(pkg-config --cflags --libs libavformat libavcodec libavutil libswresample libswscale) -lm -o "$work/verify" "$work/verify" "$src/test/test.mp3" ${HOSTCXX:-c++} -std=c++17 -O2 -Wall -Wextra -Werror -nostdinc++ -fno-exceptions -fno-rtti \ "$src/tests/host/test_reliefplayer_drain.cpp" \ "$src/userland/apps/reliefplayer/decoder.cpp" "$src/userland/apps/reliefplayer/pcm.cpp" \ "$src/userland/apps/reliefplayer/worker.cpp" "$src/userland/apps/reliefplayer/session.cpp" \ - -I"$src/third_party/minimp3" -lm -Wl,--wrap=open,--wrap=close,--wrap=write,--wrap=ioctl -o "$work/drain" + "$src/userland/apps/reliefplayer/media.cpp" \ + -I"$src/third_party/minimp3" $(pkg-config --cflags --libs libavformat libavcodec libavutil libswresample libswscale) -lm -Wl,--wrap=open,--wrap=close,--wrap=write,--wrap=ioctl -o "$work/drain" "$work/drain" "$work/drain.wav" diff --git a/tests/build/test-reliefplayer.sh b/tests/build/test-reliefplayer.sh index 417c0cb8..46c4fa4a 100644 --- a/tests/build/test-reliefplayer.sh +++ b/tests/build/test-reliefplayer.sh @@ -6,5 +6,6 @@ trap 'rm -rf "$work"' EXIT HUP INT TERM ${HOSTCXX:-c++} -std=c++17 -O2 -Wall -Wextra -Werror -nostdinc++ -fno-exceptions -fno-rtti \ ${PLAYER_TEST_FLAGS:-} "$src/tests/host/test_reliefplayer.cpp" \ "$src/userland/apps/reliefplayer/decoder.cpp" "$src/userland/apps/reliefplayer/pcm.cpp" \ - -I"$src/third_party/minimp3" -lm -o "$work/verify" + "$src/userland/apps/reliefplayer/media.cpp" \ + -I"$src/third_party/minimp3" $(pkg-config --cflags --libs libavformat libavcodec libavutil libswresample libswscale) -lm -o "$work/verify" "$work/verify" "$work" "${PLAYER_MP3_FIXTURE:-$src/test/test.mp3}" diff --git a/tests/build/test-rpr-packages.sh b/tests/build/test-rpr-packages.sh index 759af709..314a579d 100644 --- a/tests/build/test-rpr-packages.sh +++ b/tests/build/test-rpr-packages.sh @@ -34,8 +34,12 @@ output= metadata= files= destination= +keys= +indexing=0 while [ "$#" -gt 0 ]; do case $1 in + mkndx) indexing=1; shift ;; + --keys-dir) keys=$2; shift 2 ;; --output) output=$2; shift 2 ;; --files) files=$2; shift 2 ;; --info) metadata="$metadata\n$2"; shift 2 ;; @@ -43,6 +47,10 @@ while [ "$#" -gt 0 ]; do *) shift ;; esac done +if [ "$indexing" = 1 ]; then + test -s "$keys/alpine-devel@lists.alpinelinux.org-6165ee59.rsa.pub" + test -s "$keys/reliefos-rpr.rsa.pub" +fi if [ -n "$destination" ]; then cp -a "$RELIEFOS_RPR_TEST_PAYLOAD/lib" "$RELIEFOS_RPR_TEST_PAYLOAD/usr" "$destination/" exit 0 @@ -103,6 +111,8 @@ test ! -e "$tmp/pages-output" # canonical filename points at the same key during the package transition. rm "$tmp/pages-apps/reliefos-broken.apk" "$tmp/pages-apps/packages.list" printf 'app package\n' > "$tmp/pages-apps/reliefos-doom-1-r0.apk" +printf 'upstream package\n' > "$tmp/pages-repository/mbedtls3-3.6.7-r0.apk" +printf 'numbered package name\n' > "$tmp/pages-repository/libatk-1.0-2.60.3-r0.apk" sh "$src/tools/build/rpr-pages.sh" "$tmp/pages-repository" "$tmp/pages-apps" \ "$tmp/kernel.sys" "$tmp/pages-kernel" \ "$tmp/pages-version" "$tmp/fake-bin/apk" "$tmp/pages-key" "$tmp/pages-output" @@ -110,6 +120,11 @@ cmp "$tmp/pages-output/apk/leonos-rpr.rsa.pub" "$tmp/pages-output/apk/reliefos-r grep -F '"public_key":"reliefos-rpr.rsa.pub"' "$tmp/pages-output/apk/repository.json" grep -F '"legacy_public_key":"leonos-rpr.rsa.pub"' "$tmp/pages-output/apk/repository.json" test -f "$tmp/pages-output/apk/reliefos-base-1-r0.apk" +cmp "$tmp/pages-repository/mbedtls3-3.6.7-r0.apk" "$tmp/pages-output/apk/mbedtls3-3.6.7-r0.apk" +grep -F 'href="mbedtls3.html"' "$tmp/pages-output/packages/index.html" +grep -F '../apk/mbedtls3-3.6.7-r0.apk' "$tmp/pages-output/packages/mbedtls3.html" +grep -F 'href="libatk-1.0.html"' "$tmp/pages-output/packages/index.html" +grep -F '
Version
2.60.3-r0
' "$tmp/pages-output/packages/libatk-1.0.html" # Kbuild invokes the producer from its output directory with relative inputs. # Exercise the real runtime verifier with that calling convention as well. diff --git a/tests/build/test-sdk-stage.sh b/tests/build/test-sdk-stage.sh index e256428f..c6137685 100644 --- a/tests/build/test-sdk-stage.sh +++ b/tests/build/test-sdk-stage.sh @@ -5,8 +5,8 @@ w=$(mktemp -d) trap 'rm -rf "$w"' EXIT mkdir -p "$w/src/include/uapi" "$w/src/include/leonos" "$w/src/include/reliefos" \ - "$w/src/userland/runtime/include/leonos" "$w/src/userland/runtime/include/reliefos" "$w/src/third_party/zlib" \ - "$w/src/third_party/libpng" "$w/sysroot/include" "$w/sysroot/lib" \ + "$w/src/userland/runtime/include/leonos" "$w/src/userland/runtime/include/reliefos" "$w/runtime-deps/usr/include" \ + "$w/runtime-deps/usr/lib" "$w/runtime-deps/usr/share/licenses" "$w/runtime-deps/usr/share/reliefos/sdk/apk" "$w/sysroot/include" "$w/sysroot/lib" \ "$w/sysroot/share/licenses/musl" "$w/pam/usr/include/security" \ "$w/pam/lib/pkgconfig" "$w/pam/share/licenses/linux-pam" \ "$w/auth/usr/include" "$w/auth/lib" "$w/auth/share/licenses/libxcrypt" \ @@ -30,25 +30,25 @@ printf 'license\n' > "$w/pam/share/licenses/linux-pam/LICENSE" printf 'license\n' > "$w/auth/share/licenses/libxcrypt/LICENSE" printf 'so\n' > "$w/pam/lib/libpam.so.0" printf 'so\n' > "$w/auth/lib/libcrypt.so.2" +printf 'archive\n' > "$w/auth/lib/libcrypt.a" printf 'reliefos runtime\n' > "$w/reliefos-runtime.so" printf 'reliefos archive\n' > "$w/reliefos-runtime.a" printf 'leonos runtime\n' > "$w/leonos-runtime.so" printf 'leonos archive\n' > "$w/leonos-runtime.a" printf 'builtins\n' > "$w/builtins.a" printf 'driver\n' > "$w/driver" -printf 'header\n' > "$w/src/third_party/zlib/zlib.h" -printf 'header\n' > "$w/src/third_party/zlib/zconf.h" -printf 'header\n' > "$w/src/third_party/libpng/png.h" -printf 'header\n' > "$w/src/third_party/libpng/pngconf.h" -printf 'header\n' > "$w/pnglibconf.h" +printf 'header\n' > "$w/runtime-deps/usr/include/pnglibconf.h" +printf 'archive\n' > "$w/runtime-deps/usr/lib/libmbedcrypto.a" +printf 'license\n' > "$w/runtime-deps/usr/share/licenses/LICENSE" +printf 'pkgname = mbedtls3\n' > "$w/runtime-deps/usr/share/reliefos/sdk/apk/mbedtls3.PKGINFO" -RUNTIME_BUILTINS="$w/builtins.a" PNG_CONFIG="$w/pnglibconf.h" \ +RUNTIME_BUILTINS="$w/builtins.a" RUNTIME_DEVELOPMENT_ROOT="$w/runtime-deps" \ sh "$root/tools/build/musl-sdk.sh" "$w/src" "$w/sysroot" \ "$w/reliefos-runtime.so" "$w/reliefos-runtime.a" \ "$w/leonos-runtime.so" "$w/leonos-runtime.a" "$w/pam" "$w/auth" "$w/driver" \ "$w/musl-sdk" 1234 "$w/export" rm "$w/musl-sdk/include/stdio.h" "$w/musl-sdk/lib/crt1.o" -RUNTIME_BUILTINS="$w/builtins.a" PNG_CONFIG="$w/pnglibconf.h" \ +RUNTIME_BUILTINS="$w/builtins.a" RUNTIME_DEVELOPMENT_ROOT="$w/runtime-deps" \ sh "$root/tools/build/musl-sdk.sh" "$w/src" "$w/sysroot" \ "$w/reliefos-runtime.so" "$w/reliefos-runtime.a" \ "$w/leonos-runtime.so" "$w/leonos-runtime.a" "$w/pam" "$w/auth" "$w/driver" \ @@ -63,3 +63,5 @@ grep -Fx 'leonos runtime' "$w/musl-sdk/lib/libleonos.so.2" grep -Fx 'reliefos archive' "$w/musl-sdk/lib/libreliefos.a" grep -Fx 'leonos archive' "$w/musl-sdk/lib/libleonos.a" echo 'ok - musl SDK stages both runtime ABIs and keeps canonical and legacy headers' +cmp "$w/runtime-deps/usr/lib/libmbedcrypto.a" "$w/musl-sdk/lib/libmbedcrypto.a" +cmp "$w/runtime-deps/usr/share/reliefos/sdk/apk/mbedtls3.PKGINFO" "$w/musl-sdk/share/reliefos/sdk/apk/mbedtls3.PKGINFO" diff --git a/tests/build/test-submodule-contract.sh b/tests/build/test-submodule-contract.sh index 1708d0f9..883b76bb 100644 --- a/tests/build/test-submodule-contract.sh +++ b/tests/build/test-submodule-contract.sh @@ -152,14 +152,9 @@ if [ -z "${other:-}" ]; then elif git clone -q --no-checkout "$repo_root/kernel/reliefnt" \ "$clone/kernel/reliefnt" 2>"$work/sub-clone.log"; then git -C "$clone/kernel/reliefnt" checkout -q --detach "$other" 2>/dev/null - # The fixture's make needs the config chain: seed the two submodules it - # parses/builds from the (already initialized) worktree checkouts. - # git >= 2.38 blocks the file transport for submodules by default. + # The release guard runs before dependency builds; no third-party source + # checkout is needed for its gitlink-mismatch fixture. cp "$repo_root/.gitmodules" "$clone/.gitmodules" - git -C "$clone" config submodule.third_party/zlib.url "$repo_root/third_party/zlib" - git -c protocol.file.allow=always -C "$clone" submodule update -q --init \ - third_party/zlib \ - 2>"$work/fixture-submodules.log" if make -s -j1 -C "$clone" O="$work/clone-out" rpr-pages \ >"$work/mismatch.log" 2>&1; then fail "make rpr-pages refuses a gitlink mismatch" 'make exited 0' diff --git a/tests/build/test-upstream-make-helper-dependencies.sh b/tests/build/test-upstream-make-helper-dependencies.sh index 0d8a6eba..2ce24622 100644 --- a/tests/build/test-upstream-make-helper-dependencies.sh +++ b/tests/build/test-upstream-make-helper-dependencies.sh @@ -6,6 +6,7 @@ RELIEFOS_SRC=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd) THIRD_PARTY=$RELIEFOS_SRC/scripts/reliefos/Makefile.third-party PAM_MAKEFILE=$RELIEFOS_SRC/scripts/reliefos/Makefile.pam UPSTREAM_MAKEFILE=$RELIEFOS_SRC/scripts/reliefos/Makefile.upstream +APK_DEPS_MAKEFILE=$RELIEFOS_SRC/scripts/reliefos/Makefile.apk-dependencies UPSTREAM_MAKE_ENV=$RELIEFOS_SRC/tools/build/upstream-make-environment.sh TMP_PARENT=${TMPDIR:-/tmp} mkdir -p "$TMP_PARENT" @@ -40,7 +41,7 @@ check_normal_edge "$THIRD_PARTY" '$(MUSL_STAMP)' musl-sysroot check_normal_edge "$THIRD_PARTY" '$(AUTH_STAMP)' auth-upstream check_normal_edge "$PAM_MAKEFILE" '$(PAM_STAMP)' pam-stage check_normal_edge "$UPSTREAM_MAKEFILE" 'upstream_$(1)_script' upstream-package -check_normal_edge "$UPSTREAM_MAKEFILE" '$(UPSTREAM_TERMINAL_SCRIPT)' upstream-terminal +check_normal_edge "$APK_DEPS_MAKEFILE" '$(RELIEFOS_SRC)/tools/build/host-runtime-development.sh' host-runtime-development check_normal_edge "$UPSTREAM_MAKEFILE" 'upstream_app_$(1)_inputs' upstream-app check_normal_edge "$UPSTREAM_MAKEFILE" '$(BUSYBOX_ELF)' upstream-busybox diff --git a/tests/host/test_bootloader_install.c b/tests/host/test_bootloader_install.c index 5fbb2266..f51de0d0 100644 --- a/tests/host/test_bootloader_install.c +++ b/tests/host/test_bootloader_install.c @@ -203,6 +203,22 @@ int main(int argc, char **argv) assert(reliefos_boot_probe(options.disk_path, options.boot_dir, &modes) == 0); unsigned selected = (options.install_bios ? 1u : 0u) | (options.install_uefi ? 2u : 0u); assert((modes & selected) == selected); + if (virtual_image && (modes & RELIEFOS_BOOT_BIOS)) { + char core[1024], saved[1024]; + snprintf(core, sizeof(core), "%s/grub/i386-pc/core.img", options.boot_dir); + snprintf(saved, sizeof(saved), "%s/grub/i386-pc/legacy-core.img", options.boot_dir); + assert(!rename(core, saved)); + assert(!reliefos_boot_probe(options.disk_path, options.boot_dir, &modes)); + int raw = __real_open(virtual_image, O_RDWR); + assert(raw >= 0); + unsigned char sector[512], invalid[8] = {0}; + assert(pread(raw, sector, sizeof(sector), 0) == sizeof(sector)); + assert(pwrite(raw, invalid, sizeof(invalid), 0x5c) == sizeof(invalid)); + assert(reliefos_boot_probe(options.disk_path, options.boot_dir, &modes) < 0 && !modes); + assert(pwrite(raw, sector + 0x5c, 8, 0x5c) == 8); + assert(!close(raw)); + assert(!rename(saved, core)); + } assert(callbacks > 0); puts("PASS shared boot publication API"); return 0; diff --git a/tests/host/test_bootloader_uefi.c b/tests/host/test_bootloader_uefi.c new file mode 100644 index 00000000..4665b000 --- /dev/null +++ b/tests/host/test_bootloader_uefi.c @@ -0,0 +1,81 @@ +#define _GNU_SOURCE 1 +#include "../../userland/runtime/src/bootloader_internal.h" +#include +#include +#include +#include +#include +#include +#include +#include +int reliefos_block_get_info(const char *p, struct reliefos_block_disk_info *out) { + (void)p; (void)out; assert(0); return -ENOSYS; +} +int reliefos_block_list_partitions(const char *p, struct reliefos_block_partition *out, uint32_t n, uint32_t *count) { + (void)p; (void)out; (void)n; (void)count; assert(0); return -ENOSYS; +} +int reliefos_block_partition_uuid(const char *p, uint32_t n, char out[37]) { + (void)p; (void)n; (void)out; assert(0); return -ENOSYS; +} +static void directory(const char *base, const char *relative) { + char path[1024]; snprintf(path, sizeof(path), "%s/%s", base, relative); + assert(!mkdir(path, 0755) || errno == EEXIST); +} +static void write_file(const char *base, const char *relative, const char *text) { + char path[1024]; snprintf(path, sizeof(path), "%s/%s", base, relative); + FILE *file = fopen(path, "w"); assert(file); + assert(fputs(text, file) >= 0); assert(!fclose(file)); +} +static void equal(const char *base, const char *relative, const char *text) { + char path[1024], bytes[2048]; snprintf(path, sizeof(path), "%s/%s", base, relative); + FILE *file = fopen(path, "r"); assert(file); + size_t n = fread(bytes, 1, sizeof(bytes) - 1, file); bytes[n] = 0; + assert(!ferror(file) && !fclose(file)); assert(!strcmp(text, bytes)); +} +int main(int argc, char **argv) { + assert(argc == 2); + const char *uuids[] = {"11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222"}; + char source[1024], esp[1024], instance[1024]; + snprintf(source, sizeof(source), "%s/source", argv[1]); assert(!mkdir(source, 0755)); + directory(source, "EFI"); directory(source, "EFI/BOOT"); + directory(source, "reliefos"); directory(source, "leonos"); directory(source, "grub"); + write_file(source, "EFI/BOOT/BOOTX64.EFI", "verified loader one"); + snprintf(esp, sizeof(esp), "%s/shared", argv[1]); assert(!mkdir(esp, 0755)); + directory(esp, "EFI"); directory(esp, "EFI/BOOT"); directory(esp, "EFI/Microsoft"); + write_file(esp, "EFI/BOOT/BOOTX64.EFI", "foreign fallback"); + write_file(esp, "EFI/BOOT/grub.cfg", "foreign config"); + write_file(esp, "EFI/Microsoft/bootmgfw.efi", "Windows loader"); + struct reliefos_boot_install_options options = {.source=source, .install_uefi=1}; + for (unsigned i=0; i<2; ++i) { + assert(!reliefos_boot_prepare_instance(esp, uuids[i], instance, sizeof(instance))); + directory(instance, "grub"); + options.boot_dir=instance; options.root_partuuid=uuids[i]; + directory(instance, "EFI"); directory(instance, "EFI/BOOT"); + write_file(instance, "EFI/BOOT/BOOTX64.EFI", "interrupted private loader publication"); + assert(!reliefos_boot_validate_target(&options)); + assert(!boot_validate_instance(&options, esp)); + assert(!boot_install_uefi(&options, esp)); + equal(esp, "EFI/BOOT/BOOTX64.EFI", "foreign fallback"); + equal(esp, "EFI/BOOT/grub.cfg", "foreign config"); + equal(esp, "EFI/Microsoft/bootmgfw.efi", "Windows loader"); + char loader[256]; snprintf(loader, sizeof(loader), "EFI/ReliefOS/%s/grubx64.efi", uuids[i]); + equal(esp, loader, "verified loader one"); + } + assert(reliefos_boot_prepare_instance(esp, "../../escape", instance, sizeof(instance)) == -EINVAL); + snprintf(esp, sizeof(esp), "%s/empty", argv[1]); assert(!mkdir(esp, 0755)); + assert(!reliefos_boot_prepare_instance(esp, uuids[0], instance, sizeof(instance))); + directory(instance, "grub"); options.boot_dir=instance; options.root_partuuid=uuids[0]; + assert(!boot_install_uefi(&options, esp)); + equal(esp, "EFI/BOOT/BOOTX64.EFI", "verified loader one"); + write_file(source, "EFI/BOOT/BOOTX64.EFI", "verified loader two"); + assert(!boot_install_uefi(&options, esp)); + equal(esp, "EFI/BOOT/BOOTX64.EFI", "verified loader two"); + write_file(esp, "EFI/BOOT/BOOTX64.EFI", "new foreign fallback"); + assert(!boot_install_uefi(&options, esp)); + equal(esp, "EFI/BOOT/BOOTX64.EFI", "new foreign fallback"); + char bad[1024]; snprintf(bad, sizeof(bad), "%s/foreign", argv[1]); assert(!mkdir(bad, 0755)); + assert(boot_validate_instance(&options, bad) < 0); + write_file(instance, "reliefos-instance.id", "not owned by ReliefOS"); + assert(reliefos_boot_prepare_instance(esp, uuids[0], instance, sizeof(instance)) < 0); + puts("UEFI: shared ESP, two instances, fallback ownership, updates and namespace refusal passed"); +} diff --git a/tests/host/test_fileman_boundary.c b/tests/host/test_fileman_boundary.c index 7840b057..bf3ded09 100644 --- a/tests/host/test_fileman_boundary.c +++ b/tests/host/test_fileman_boundary.c @@ -39,6 +39,7 @@ int main(void) assert(!strcmp(fileman_x11_default_app("/tmp/plan.rdiagram"), "/usr/bin/reliefdiagram")); assert(!strcmp(fileman_x11_default_app("/tmp/PLAN.RDIAGRAM"), "/usr/bin/reliefdiagram")); assert(!strcmp(fileman_x11_default_app("/tmp/music.mp3"), "/usr/bin/reliefplayer")); + assert(!strcmp(fileman_x11_default_app("/tmp/video.MP4"), "/usr/bin/reliefplayer")); assert(!strcmp(fileman_x11_default_app("/tmp/MUSIC.WAV"), "/usr/bin/reliefplayer")); assert(!strcmp(fileman_x11_app_for_path("/tmp/equation.rmath", "/usr/bin/custom-math"), "/usr/bin/custom-math")); diff --git a/tests/host/test_installer_boot_layout.c b/tests/host/test_installer_boot_layout.c new file mode 100644 index 00000000..fbb15c77 --- /dev/null +++ b/tests/host/test_installer_boot_layout.c @@ -0,0 +1,55 @@ +/* Exercise the updater's real fstab parser without mounting host disks. */ +#include "../../userland/apps/installer/installer_update_target.c" +#include + +static const char *identity = "12345678-1234-4567-89ab-123456789abc"; + +static int check(const char *root, const char *text, int namespaced) +{ + char path[512], source[128] = {0}; + snprintf(path, sizeof(path), "%s/etc/fstab", root); + FILE *file = fopen(path, "w"); + assert(file && fputs(text, file) >= 0 && !fclose(file)); + struct installer_update_target target = {0}; + assert(strlen(root) < sizeof(target.root)); + strcpy(target.root, root); + strcpy(target.info.root_uuid, identity); + int result = read_boot_layout(&target, source); + if (!result) { + assert(target.namespaced == namespaced); + assert(!strcmp(source, "PARTUUID=esp")); + } + return result; +} + +int main(int argc, char **argv) +{ + assert(argc == 2); + char path[512]; + snprintf(path, sizeof(path), "%s/etc", argv[1]); + assert(!mkdir(path, 0700)); + const char *physical = "PARTUUID=esp /efi vfat defaults 0 2\n"; + const char *legacy = "PARTUUID=esp /boot vfat defaults 0 2\n"; + char binding[256], combined[1024]; + snprintf(binding, sizeof(binding), "/efi/reliefos/instances/%s /boot none bind 0 0\n", identity); + snprintf(combined, sizeof(combined), "%s%s", physical, binding); + assert(!check(argv[1], combined, 1)); + assert(!check(argv[1], legacy, 0)); + assert(check(argv[1], physical, 0) == -EINVAL); + snprintf(combined, sizeof(combined), "%s%s", legacy, binding); + assert(check(argv[1], combined, 0) == -EINVAL); + snprintf(combined, sizeof(combined), "%s%s%s", physical, physical, binding); + assert(check(argv[1], combined, 0) == -ENOTUNIQ); + snprintf(combined, sizeof(combined), "%s%s%s", physical, binding, binding); + assert(check(argv[1], combined, 0) == -ENOTUNIQ); + snprintf(combined, sizeof(combined), "%s/efi/reliefos/instances/00000000-0000-0000-0000-000000000001 /boot none bind 0 0\n", physical); + assert(check(argv[1], combined, 0) == -EXDEV); + snprintf(combined, sizeof(combined), "%s/efi/../outside /boot none bind 0 0\n", physical); + assert(check(argv[1], combined, 0) == -EXDEV); + snprintf(combined, sizeof(combined), "%s/foreign /boot ext4 defaults 0 2\n", legacy); + assert(check(argv[1], combined, 0) == -EINVAL); + snprintf(combined, sizeof(combined), " # ignored comment /efi vfat\n%s", legacy); + assert(!check(argv[1], combined, 0)); + puts("updater boot layout: shared ESP, legacy, duplicate, foreign and malformed bindings passed"); + return 0; +} diff --git a/tests/host/test_motif_apps.c b/tests/host/test_motif_apps.c index 0f318396..820d570c 100644 --- a/tests/host/test_motif_apps.c +++ b/tests/host/test_motif_apps.c @@ -591,13 +591,13 @@ int main(void) int count; assert(installer_model_next(INSTALLER_PAGE_LANGUAGE, INSTALLER_MODE_FRESH) == - INSTALLER_PAGE_THANKS); - assert(installer_model_next(INSTALLER_PAGE_THANKS, INSTALLER_MODE_FRESH) == + INSTALLER_PAGE_LICENSE); + assert(installer_model_next(INSTALLER_PAGE_LICENSE, INSTALLER_MODE_FRESH) == INSTALLER_PAGE_WELCOME); assert(installer_model_next(INSTALLER_PAGE_MODE, INSTALLER_MODE_FRESH) == INSTALLER_PAGE_DISK); assert(installer_model_next(INSTALLER_PAGE_DISK, INSTALLER_MODE_UPDATE) == - INSTALLER_PAGE_BOOTLOADERS); + INSTALLER_PAGE_CONFIRM); assert(installer_model_next(INSTALLER_PAGE_DISK, INSTALLER_MODE_FRESH) == INSTALLER_PAGE_BOOTLOADERS); assert(installer_model_next(INSTALLER_PAGE_BOOTLOADERS, INSTALLER_MODE_FRESH) == @@ -613,7 +613,7 @@ int main(void) assert(installer_model_next(INSTALLER_PAGE_PROGRESS, INSTALLER_MODE_FRESH) == INSTALLER_PAGE_PROGRESS); assert(installer_model_prev(INSTALLER_PAGE_CONFIRM, INSTALLER_MODE_UPDATE) == - INSTALLER_PAGE_BOOTLOADERS); + INSTALLER_PAGE_DISK); assert(installer_model_prev(INSTALLER_PAGE_CONFIRM, INSTALLER_MODE_FRESH) == INSTALLER_PAGE_ACCOUNTS); assert(installer_model_prev(INSTALLER_PAGE_ACCOUNTS, INSTALLER_MODE_FRESH) == @@ -625,7 +625,7 @@ int main(void) assert(installer_model_prev(INSTALLER_PAGE_BOOTLOADERS, INSTALLER_MODE_UPDATE) == INSTALLER_PAGE_DISK); assert(installer_model_prev(INSTALLER_PAGE_WELCOME, INSTALLER_MODE_FRESH) == - INSTALLER_PAGE_THANKS); + INSTALLER_PAGE_LICENSE); assert(installer_model_prev(INSTALLER_PAGE_FINISH, INSTALLER_MODE_UPDATE) == INSTALLER_PAGE_DISK); assert(installer_model_prev(INSTALLER_PAGE_LANGUAGE, INSTALLER_MODE_FRESH) == @@ -648,29 +648,32 @@ int main(void) INSTALLER_ACTION_RESTART); assert(installer_model_action(INSTALLER_PAGE_FINISH, INSTALLER_MODE_FRESH, 0) == INSTALLER_ACTION_CLOSE); - assert(strcmp(installer_model_confirm_word(INSTALLER_MODE_UPDATE), "UPDATE") == 0); + assert(installer_model_action(INSTALLER_PAGE_FINISH, INSTALLER_MODE_UPDATE, 0) == + INSTALLER_ACTION_RETRY); + assert(strcmp(installer_model_confirm_word(INSTALLER_MODE_UPDATE), "") == 0); assert(strcmp(installer_model_confirm_word(INSTALLER_MODE_FRESH), "INSTALL") == 0); count = installer_model_steps(INSTALLER_MODE_FRESH, steps, INSTALLER_PAGE_COUNT); assert(count == INSTALLER_PAGE_COUNT); assert(steps[0] == INSTALLER_PAGE_LANGUAGE); - assert(steps[1] == INSTALLER_PAGE_THANKS); + assert(steps[1] == INSTALLER_PAGE_LICENSE); assert(steps[2] == INSTALLER_PAGE_WELCOME); assert(steps[4] == INSTALLER_PAGE_DISK); assert(steps[5] == INSTALLER_PAGE_BOOTLOADERS); assert(steps[6] == INSTALLER_PAGE_PARTITIONS); assert(steps[count - 1] == INSTALLER_PAGE_FINISH); count = installer_model_steps(INSTALLER_MODE_UPDATE, steps, INSTALLER_PAGE_COUNT); - assert(count == INSTALLER_PAGE_COUNT - 2); + assert(count == INSTALLER_PAGE_COUNT - 3); assert(steps[4] == INSTALLER_PAGE_DISK); - assert(steps[5] == INSTALLER_PAGE_BOOTLOADERS); - assert(steps[6] == INSTALLER_PAGE_CONFIRM); + assert(steps[5] == INSTALLER_PAGE_CONFIRM); + assert(steps[6] == INSTALLER_PAGE_PROGRESS); for (int i = 0; i < count; ++i) { assert(steps[i] != INSTALLER_PAGE_ACCOUNTS); assert(steps[i] != INSTALLER_PAGE_PARTITIONS); + assert(steps[i] != INSTALLER_PAGE_BOOTLOADERS); } count = installer_model_steps(INSTALLER_MODE_FRESH, steps, 2); - assert(count == INSTALLER_PAGE_COUNT && steps[1] == INSTALLER_PAGE_THANKS); + assert(count == INSTALLER_PAGE_COUNT && steps[1] == INSTALLER_PAGE_LICENSE); installer_model_format_disk_line(buf, sizeof(buf), 0, "vda", 2048 * 1024, 512); assert(strcmp(buf, "Disk 0 vda 1 GiB") == 0); diff --git a/tests/host/test_reliefplayer_mp4.cpp b/tests/host/test_reliefplayer_mp4.cpp new file mode 100644 index 00000000..258d9fcd --- /dev/null +++ b/tests/host/test_reliefplayer_mp4.cpp @@ -0,0 +1,139 @@ +#include "../../userland/apps/reliefplayer/decoder.h" +#include "../../userland/apps/reliefplayer/media.h" +#include "../../userland/apps/reliefplayer/session.h" +#include +#include +#include +#include +#include +using namespace rp; +static bool cancel(void *, uint64_t, uint64_t) { return false; } +static unsigned checksum(const VideoShared &s) { + unsigned value = 0; + for (size_t n = 0; n < size_t(s.width) * s.height * 3; ++n) + value = value * 33 + s.pixels[n]; + return value; +} +static void wait_state(Session &s, State desired) { + for (unsigned n = 0; n < 600; ++n) { + s.poll(); + if (s.status().state == desired) + return; + assert(s.status().state != State::Failed); + usleep(10000); + } + fprintf(stderr, "wanted state=%u actual=%u\n", unsigned(desired), + unsigned(s.status().state)); + assert(false); +} +int main(int argc, char **argv) { + assert(argc == 8); + Error error; + Decoder audio; + assert(audio.open(argv[1], error)); + assert(audio.info().kind == Kind::Mp4 && audio.info().has_audio); + assert(audio.info().width == 320 && audio.info().height == 180); + assert(audio.info().rate == 48000 && audio.info().channels == 2); + assert(audio.info().frames >= 95000 && audio.info().frames <= 100000); + int16_t samples[4096 * 2]; + uint64_t frames = 0, energy = 0; + int n; + while ((n = audio.read(samples, 4096, error)) > 0) { + for (int i = 0; i < n * 2; ++i) + energy += abs(int(samples[i])); + frames += unsigned(n); + } + assert(n == 0 && error == Error::Ok && frames == audio.info().frames && + energy > 1000000); + assert(audio.seek(48000, error)); + assert(audio.read(samples, 4096, error) == 4096 && audio.position() == 52096); + assert(audio.seek(0, error) && audio.read(samples, 4096, error) == 4096); + // The AAC track starts 300 ms after the video. Preserve the initial silence. + energy = 0; + for (int i = 0; i < 8192; ++i) + energy += abs(int(samples[i])); + assert(energy == 0); + VideoDecoder video; + assert(video.open(argv[1], error, nullptr, nullptr)); + auto *image = static_cast(calloc(1, sizeof(VideoShared))); + assert(image); + assert(video.pump(0, image, error)); + assert(image->serial && image->width && image->width <= VideoWidth && + image->height <= VideoHeight); + unsigned first = checksum(*image); + assert(video.seek(1000, error)); + for (int i = 0; i < 10; ++i) + assert(video.pump(1000, image, error)); + assert(checksum(*image) != first); + assert(video.seek(0, error) && video.pump(0, image, error)); + assert(checksum(*image) == first); + unsigned char rotated_corner[3]; + memcpy(rotated_corner, image->pixels + (image->width - 1) * 3, 3); + for (int file = 5; file < 8; ++file) { + VideoDecoder other; + assert(other.open(argv[file], error, nullptr, nullptr)); + unsigned serial = image->serial; + assert(other.pump(0, image, error) && image->serial != serial); + fprintf(stderr, "MP4 fixture=%d snapshot=%ux%u\n", file, image->width, + image->height); + assert(image->width == (file == 5 ? 180u : 320u)); + assert(image->height == (file == 5 ? 320u : 180u)); + if (file == 5) + assert(!memcmp(image->pixels, rotated_corner, 3)); + } + free(image); + Decoder silent; + assert(silent.open(argv[2], error) && !silent.info().has_audio && + silent.info().rate == 1000); + Decoder invalid; + assert(!invalid.open(argv[3], error) && error == Error::Invalid); + assert(!invalid.open(argv[4], error) && error == Error::Unsupported); + assert(!invalid.open(argv[1], error, cancel) && error == Error::Cancelled); + Session session; + assert(session.open(argv[2], 70)); + wait_state(session, State::Playing); + for (unsigned tries = 0; !session.video_serial() && tries < 200; ++tries) { + session.poll(); + usleep(10000); + } + assert(session.video_serial() && session.pixels()); + assert(session.command(Op::Play, 0)); + wait_state(session, State::Paused); + assert(session.command(Op::Seek, 1000)); + for (unsigned tries = 0; session.status().position != 1000 && tries < 200; + ++tries) { + session.poll(); + usleep(10000); + } + assert(session.status().position == 1000); + assert(session.command(Op::Seek, session.status().info.frames)); + wait_state(session, State::Ended); + assert(session.status().position == session.status().info.frames); + assert(session.command(Op::Seek, 1000)); + wait_state(session, State::Paused); + assert(session.command(Op::Play, 1)); + wait_state(session, State::Playing); + wait_state(session, State::Ended); + assert(session.status().position == session.status().info.frames); + assert(session.command(Op::Play, 1)); + wait_state(session, State::Playing); + assert(session.command(Op::Stop)); + wait_state(session, State::Stopped); + assert(session.status().position == 0); + session.shutdown(); + for (unsigned tries = 0; session.active() && tries < 400; ++tries) { + session.poll(); + usleep(10000); + } + assert(!session.active()); + assert(session.open(argv[4], 70)); + for (unsigned tries = 0; + session.status().state != State::Failed && tries < 400; ++tries) { + session.poll(); + usleep(10000); + } + assert(session.status().state == State::Failed && !session.width() && + !session.height()); + puts("ReliefPlayer MP4: H.264/HEVC/MPEG-4, AAC, rotation, delayed track, " + "B-frames, seek, silent video, controls and invalid media passed"); +} diff --git a/tests/integration/brand-abi.sh b/tests/integration/brand-abi.sh index 8472e6c5..9479bbd3 100755 --- a/tests/integration/brand-abi.sh +++ b/tests/integration/brand-abi.sh @@ -19,7 +19,9 @@ trap 'rm -rf "$work"' EXIT HUP INT TERM retired='^((leonos|reliefos)_(license_|ui_|gui_|mouse_|display_|appearance_|inputm_(note_gui_window|observe_gui_key|poll_gui_commit|take_text|take_key|set_current_context))|text_input_(note_gui_window|observe_gui_key|poll_gui_commit|take_text|take_key|set_current_context)|ui_)' awk -v retired="$retired" '$1 !~ retired' \ "$root/tests/fixtures/brand-abi/old-sdk-exports.tsv" > "$work/old.expected" -awk -v retired="$retired" '$1 !~ retired' \ +# Third-party internal exports were split into their own official libraries. +# Public first-party aliases remain the compatibility contract. +awk -v retired="$retired" '$1 !~ retired && $1 ~ /^(leonos_|reliefos_|text_input_)/' \ "$root/tests/fixtures/brand-abi/old-shared-symbols.tsv" > "$work/shared.expected" test -x "$driver" @@ -55,7 +57,8 @@ readelf -d "$new_lib" | grep -q 'Library soname: \[libreliefos.so.2\]' nm -D --defined-only --format=posix "$compat_lib" | awk '$1 ~ /^leonos_/ {print $1 "\t" $2}' | LC_ALL=C sort > "$work/old.actual" -cmp "$work/old.expected" "$work/old.actual" +join -t "$(printf '\t')" -v1 "$work/old.expected" "$work/old.actual" > "$work/old.missing" +test ! -s "$work/old.missing" nm -D --defined-only --format=posix "$compat_lib" | awk '{print $1 "\t" $2}' | LC_ALL=C sort > "$work/compat.all" awk -v retired="$retired" '$1 ~ retired {print; failed=1} END {exit failed}' \ diff --git a/tests/integration/runtime-dependencies.sh b/tests/integration/runtime-dependencies.sh new file mode 100644 index 00000000..024cec1d --- /dev/null +++ b/tests/integration/runtime-dependencies.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Run both target linkage modes after moving the actual SDK to a path with spaces. +set -eu +: "${SDK_ROOT:?fresh musl SDK required}" +: "${MUSL_LOADER:?host-compatible musl loader required}" +src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT HUP INT TERM +cp -a "$SDK_ROOT" "$work/relocated SDK" +for mode in dynamic static; do + set -- + [ "$mode" != static ] || set -- -static + "$work/relocated SDK/bin/reliefos-musl-cc" "$@" "$src/tools/tests/runtime_apk_probe.c" -lsqlite3 -o "$work/$mode" + if [ "$mode" = dynamic ]; then + "$MUSL_LOADER" --library-path "$work/relocated SDK/lib" "$work/$mode" "$work/$mode.db" + else + "$work/$mode" "$work/$mode.db" + fi +done +printf 'ok - relocated SDK runs PNG/zlib/crypto/SQLite in both linkage modes\n' diff --git a/tests/integration/test-apk-upgrade.sh b/tests/integration/test-apk-upgrade.sh index cb782b4a..5c636ed9 100644 --- a/tests/integration/test-apk-upgrade.sh +++ b/tests/integration/test-apk-upgrade.sh @@ -58,7 +58,25 @@ printf 'canonical-local-setting=yes\n' > "$work/runner/target/etc/reliefos/upgra printf 'LANG=C\nMUSL_LOCPATH=/usr/share/musl/locales\n' > "$work/runner/target/etc/leonos/locale.conf" cp "$work/runner/target/etc/leonos/locale.conf" "$work/legacy-locale-before" cp "$work/runner/target/etc/apk/world" "$work/world-before" -sed 's/^leonos-/reliefos-/' "$work/world-before" > "$work/world-after-rename" +python3 - "$work/world-before" "$work/world-after-rename" <<'PY' +from pathlib import Path +import re, sys +requests = [re.sub('^leonos-', 'reliefos-', line) + for line in Path(sys.argv[1]).read_text().splitlines()] +upstream = {'reliefos-sl': ('sl',), 'reliefos-ncurses': ('ncurses', 'ncurses-terminfo'), + 'reliefos-sqlite': ('sqlite', 'sqlite-libs')} +selected = {re.split(r'[@<>=~]', request, 1)[0] for request in requests} +expected = set() +for request in requests: + name = re.split(r'[@<>=~]', request, 1)[0] + if name not in upstream: + expected.add(request) + else: + tag = re.search(r'@([a-zA-Z0-9_.+-]+)', request) + expected.update(package + ('@' + tag[1] if tag else '') + for package in upstream[name] if package not in selected) +Path(sys.argv[2]).write_text('\n'.join(sorted(expected)) + '\n') +PY awk '/^P:/{if($0=="P:leonos-apps") found=1} END{exit !found}' \ "$work/runner/target/lib/apk/db/installed" || { echo 'legacy fixture does not contain an installed leonos-apps package' >&2 @@ -77,7 +95,7 @@ cp "$work/runner/target/etc/apk/keys/$legacy_key" "$work/old-signing-key.pub" awk '/^P:/{p=$0} /^V:/{if(p=="P:external-fixture")print}' "$work/runner/target/lib/apk/db/installed" > "$work/external-before" # Install the new command path while keeping old command aliases in the target. mkdir -p "$work/runner/usr/lib/reliefos" "$work/runner/usr/lib/leonos" -cp "$src/userland/storage/leonos-apk-update" "$work/runner/usr/lib/reliefos/reliefos-apk-update" +cp "$src/userland/storage/reliefos-apk-update" "$work/runner/usr/lib/reliefos/reliefos-apk-update" ln -sfn reliefos-apk-update "$work/runner/usr/lib/reliefos/leonos-apk-update" ln -sfn ../reliefos/reliefos-apk-update "$work/runner/usr/lib/leonos/leonos-apk-update" # pivot_root, rather than chroot, lets APK create its script sandbox on Linux. diff --git a/tests/integration/test-runtime-apk-upgrade.sh b/tests/integration/test-runtime-apk-upgrade.sh new file mode 100644 index 00000000..59580d3f --- /dev/null +++ b/tests/integration/test-runtime-apk-upgrade.sh @@ -0,0 +1,130 @@ +#!/bin/sh +# Actual signed APK transaction on a pre-migration managed root. +set -eu +[ "$#" = 2 ] || { echo 'usage: test-runtime-apk-upgrade.sh OUTPUT OLD_MANAGED_ROOT' >&2; exit 2; } +out=$(CDPATH= cd -- "$1" && pwd) +old=$(CDPATH= cd -- "$2" && pwd) +work=$(mktemp -d "$out/runtime-apk-upgrade.XXXXXX") +cleanup() { + if [ "${KEEP_UPGRADE_WORK:-0}" = 1 ]; then printf 'upgrade work: %s\n' "$work" + else rm -rf "$work" + fi +} +trap cleanup EXIT HUP INT TERM +cp -a --reflink=auto "$out/rootfs/managed" "$work/runner" +cp -a --reflink=auto "$old" "$work/runner/target" +cp -a --reflink=auto "${APK_UPGRADE_REPOSITORY:-$out/packages/apk/repository}" "$work/runner/newrepo" +printf 'migration-admin-config=yes\n' > "$work/runner/target/etc/reliefos/apk-upgrade-fixture.conf" +# Exercise legacy aliases/tags and old local version pins without making them +# constraints on the unrelated upstream version scheme. An existing official +# pin remains authoritative; other missing requests inherit the retired tag. +if [ "${APK_UPGRADE_TEST_TAGGED_WORLD:-0}" = 1 ]; then + python3 - "$work/runner/target" <<'PY' +from pathlib import Path +import sys +root = Path(sys.argv[1]) +versions = {} +for block in (root / 'lib/apk/db/installed').read_text().split('\n\n'): + fields = dict(line.split(':', 1) for line in block.splitlines() if line[:2] in ('P:', 'V:')) + if 'P' in fields: + versions[fields['P']] = fields['V'] +world = root / 'etc/apk/world' +requests = [] +for request in world.read_text().splitlines(): + name = request.split('=', 1)[0] + if name in ('sl', 'sqlite', 'sqlite-libs'): + continue + if name == 'reliefos-sl': + request = 'leonos-sl@migration=' + versions[name] + elif name in ('reliefos-ncurses', 'reliefos-sqlite'): + request = name + '@migration=' + versions[name] + requests.append(request) +world.write_text('\n'.join(requests) + '\n') +PY +fi +cp "$work/runner/target/etc/apk/world" "$work/world.before" +cp "$work/runner/target/lib/apk/db/installed" "$work/database.before" +python3 - "$work/world.before" "$work/world.expected" <<'PY' +from pathlib import Path +import re, sys +requests = Path(sys.argv[1]).read_text().splitlines() +replacements = {'reliefos-sl': ('sl',), 'reliefos-ncurses': ('ncurses', 'ncurses-terminfo'), + 'reliefos-sqlite': ('sqlite', 'sqlite-libs')} +requests = [re.sub('^leonos-', 'reliefos-', request) for request in requests] +existing = {re.split(r'[@<>=~]', request, 1)[0] for request in requests} +expected = set() +for request in requests: + name = re.split(r'[@<>=~]', request, 1)[0] + if name not in replacements: + expected.add(request) + continue + tag = re.search(r'@([a-zA-Z0-9_.+-]+)', request) + expected.update(package + ('@' + tag[1] if tag else '') + for package in replacements[name] if package not in existing) +Path(sys.argv[2]).write_text('\n'.join(sorted(expected)) + '\n') +PY +run_update() { +unshare -mp --fork sh -eu -c ' + mount --make-rprivate / + mount --bind "$1" "$1" + mkdir -p "$1/proc" "$1/dev" + mount -t proc proc "$1/proc" + for device in null zero random urandom; do + touch "$1/dev/$device" + mount --bind "/dev/$device" "$1/dev/$device" + done + cd "$1" + mkdir -p .oldroot + pivot_root . .oldroot + exec /usr/lib/reliefos/reliefos-apk-update "$2" /target "$3" +' sh "$work/runner" "$1" "${2:-/newrepo}" +} +# A signed index alone is insufficient: all newly selected command archives +# must be fetched and verified before changing the installed world/database. +cp -al "$work/runner/newrepo" "$work/runner/incomplete-repo" +rm "$work/runner/incomplete-repo/"ncurses-[0-9]*.apk +if run_update --check /incomplete-repo >"$work/missing-archive.log" 2>&1; then + echo 'updater accepted a missing replacement command archive' >&2 + exit 1 +fi +cmp "$work/world.before" "$work/runner/target/etc/apk/world" +cmp "$work/database.before" "$work/runner/target/lib/apk/db/installed" +run_update --check +root=$work/runner/target +cmp "$work/world.before" "$root/etc/apk/world" +cmp "$work/database.before" "$root/lib/apk/db/installed" +run_update --apply +LC_ALL=C sort "$root/etc/apk/world" > "$work/world.after" +cmp "$work/world.expected" "$work/world.after" +printf 'migration-admin-config=yes\n' | cmp - "$root/etc/reliefos/apk-upgrade-fixture.conf" +apk=$out/upstream/apk/apk.static +for spec in 'sl usr/bin/sl' 'ncurses usr/bin/tput' 'sqlite-libs usr/lib/libsqlite3.so.0' \ + 'mbedtls3 usr/lib/libmbedtls.so.21' 'zlib usr/lib/libz.so.1' 'libpng usr/lib/libpng16.so.16'; do + set -- $spec + unshare -Ur "$apk" --root "$root" info --who-owns "/$2" | grep -F "$1-" +done +for component in sl ncurses sqlite; do + for prefix in reliefos leonos; do + package=$prefix-$component + if unshare -Ur "$apk" --root "$root" info --exists "$package"; then + printf 'retired package remains installed: %s\n' "$package" >&2 + exit 1 + fi + for archive in "$root/usr/share/reliefos/apk/repository/$package-"[0-9]*.apk; do + test ! -e "$archive" && test ! -L "$archive" + done + done +done +test ! -e "$root/usr/lib/sqlite.so.3" +test ! -e "$root/usr/lib/reliefos/apps/sl" +test ! -e "$root/usr/share/doc/reliefos/apk-transitions" +if grep -Eq '^(leonos|reliefos)-(sl|ncurses|sqlite)([@<>=~]|$)' "$root/etc/apk/world"; then + echo 'retired world requests remain after migration' >&2 + exit 1 +fi +cp "$root/etc/apk/world" "$work/world.committed" +cp "$root/lib/apk/db/installed" "$work/database.committed" +run_update --check +cmp "$work/world.committed" "$root/etc/apk/world" +cmp "$work/database.committed" "$root/lib/apk/db/installed" +printf 'ok - signed upgrade removes retired packages/files/cache, migrates world and preserves other requests/configuration\n' diff --git a/tests/integration/upstream-products.sh b/tests/integration/upstream-products.sh index 2a88d8a4..6662a3d7 100755 --- a/tests/integration/upstream-products.sh +++ b/tests/integration/upstream-products.sh @@ -17,13 +17,14 @@ done "$root/apk/apk.static" --version printf '%s\n' 'upstream production ELF/archive checks: PASS' apps=${2:-$root/userland} -for app in busybox sl pleditor; do +for app in busybox pleditor; do test -f "$apps/$app.elf" readelf -h "$apps/$app.elf" | grep -q 'Advanced Micro Devices X86-64' if readelf -d "$apps/$app.elf" | grep -E 'libc\.so\.6|ld-linux'; then exit 1; fi done -for library in sqlite.so.3; do - readelf -d "$apps/$library" | grep -q '(SONAME)' +development=$root/runtime-development/usr/lib +for library in libsqlite3.so.0 libz.so.1 libpng16.so.16 libmbedtls.so.21 libmbedx509.so.7 libmbedcrypto.so.16; do + readelf -d "$development/$library" | grep -q '(SONAME)' done src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) sh "$src/tools/build/busybox-check.sh" "$root/busybox/build/.config" "$apps/busybox.links" diff --git a/tests/lib/runtime-dependencies.sh b/tests/lib/runtime-dependencies.sh new file mode 100644 index 00000000..6b0e1124 --- /dev/null +++ b/tests/lib/runtime-dependencies.sh @@ -0,0 +1,9 @@ +# Shared host-native test libraries matching the signature-verified target APKs. +# The caller defines src as the repository root; no download occurs here. +runtime_deps=${RELIEFOS_TEST_DEP_ROOT:-$src/out/x86_64/release/host/runtime-development} +for product in usr/include/mbedtls/ssl.h usr/include/sqlite3.h usr/lib/libmbedcrypto.a usr/lib/libsqlite3.a; do + [ -f "$runtime_deps/$product" ] || { + printf 'missing native test dependency: %s; run make host-runtime-development (or set RELIEFOS_TEST_DEP_ROOT)\n' "$runtime_deps/$product" >&2 + exit 1 + } +done diff --git a/third_party/libpng b/third_party/libpng deleted file mode 160000 index 3061454d..00000000 --- a/third_party/libpng +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3061454d980de7d53608f594194cfac722721d2a diff --git a/third_party/mbedtls b/third_party/mbedtls deleted file mode 160000 index 068ff080..00000000 --- a/third_party/mbedtls +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 068ff080b369adfac81509f9b57b2afabaf82dc5 diff --git a/third_party/ncurses b/third_party/ncurses deleted file mode 160000 index 0096bd40..00000000 --- a/third_party/ncurses +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0096bd402c4a9c8f39bd7ed266e1b8920327e4d8 diff --git a/third_party/sl b/third_party/sl deleted file mode 160000 index 923e7d7e..00000000 --- a/third_party/sl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 923e7d7ebc5c1f009755bdeb789ac25658ccce03 diff --git a/third_party/sqlite b/third_party/sqlite deleted file mode 160000 index f3d536d3..00000000 --- a/third_party/sqlite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f3d536d37825302e31ed0eddd811c689f38f85a3 diff --git a/third_party/zlib b/third_party/zlib deleted file mode 160000 index da607da7..00000000 --- a/third_party/zlib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit da607da739fa6047df13e66a2af6b8bec7c2a498 diff --git a/tools/apk_distribution.py b/tools/apk_distribution.py index 0e503dea..43e15e9a 100644 --- a/tools/apk_distribution.py +++ b/tools/apk_distribution.py @@ -333,7 +333,7 @@ def build_distribution(source, output, work, apk, key): updater = tree / "usr/lib/reliefos/reliefos-apk-update" updater.parent.mkdir(parents=True, exist_ok=True) updater.unlink(missing_ok=True) - shutil.copyfile(ROOT / "userland/storage/leonos-apk-update", updater) + shutil.copyfile(ROOT / "userland/storage/reliefos-apk-update", updater) updater.chmod(0o755) (tree / "usr/lib/reliefos/leonos-apk-update").symlink_to("reliefos-apk-update") (tree / "usr/lib/leonos").mkdir(parents=True, exist_ok=True) diff --git a/tools/apk_ownership.py b/tools/apk_ownership.py index 047ade9f..b7bd5219 100644 --- a/tools/apk_ownership.py +++ b/tools/apk_ownership.py @@ -27,6 +27,13 @@ def load_policy(policy_path: Path, components_path: Path): raise ValueError("unsupported ownership policy or invented APK registration") components = tomllib.loads(components_path.read_text())["components"] expected = {item["id"] for item in components} + upstream = policy.get("upstream_components", {}) + if not isinstance(upstream, dict) or not set(upstream) <= expected or any( + not isinstance(packages, list) or not packages or + any(not isinstance(package, str) or + not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9+_.-]*", package) for package in packages) + for packages in upstream.values()): + raise ValueError("invalid upstream component packages") owners, rules = {}, {} def claim(path, group): @@ -54,8 +61,8 @@ def claim(path, group): for stem in entry.get("library_stems", []): for suffix in (".so", ".a", ".la"): claim(stem + suffix, group) - if set(owners) != expected: - raise ValueError(f"component policy mismatch: {sorted(set(owners) ^ expected)}") + if set(owners) & set(upstream) or set(owners) | set(upstream) != expected: + raise ValueError(f"component policy mismatch: {sorted((set(owners) | set(upstream)) ^ expected)}") for path in (*UTIL_LINUX_COMMANDS, *UTIL_LINUX_LIBRARIES): claim(path, "storage-util-linux") for path in FORMATTER_COMMANDS: diff --git a/tools/build/apk-stage.sh b/tools/build/apk-stage.sh index a359fafc..86710b38 100644 --- a/tools/build/apk-stage.sh +++ b/tools/build/apk-stage.sh @@ -77,6 +77,14 @@ for archive in "$upstream"/packages/*.apk; do fi case $feature in base) ;; + component:sl|component:ncurses|component:sqlite) + [ -f "$raw/etc/reliefos/apk-components" ] && [ ! -L "$raw/etc/reliefos/apk-components" ] || { + echo 'missing APK component selection' >&2; exit 1; + } + if ! grep -Fxq "${feature#component:}" "$raw/etc/reliefos/apk-components"; then + continue + fi + ;; xorg) if [ "$backend" = xorg ]; then : @@ -225,7 +233,7 @@ cp "$apk" "$tree/sbin/apk" chmod 755 "$tree/sbin/apk" mkdir -p "$tree/usr/share/licenses/apk-tools" cp "$src/resources/licenses/apk-tools-LICENSE" "$tree/usr/share/licenses/apk-tools/LICENSE" -cp "$src/userland/storage/leonos-apk-update" "$tree/usr/lib/reliefos/reliefos-apk-update" +cp "$src/userland/storage/reliefos-apk-update" "$tree/usr/lib/reliefos/reliefos-apk-update" chmod 755 "$tree/usr/lib/reliefos/reliefos-apk-update" ln -sfn reliefos-apk-update "$tree/usr/lib/reliefos/leonos-apk-update" mkdir -p "$tree/usr/lib/leonos" diff --git a/tools/build/boot-stage.sh b/tools/build/boot-stage.sh index 155be0a0..c553794c 100644 --- a/tools/build/boot-stage.sh +++ b/tools/build/boot-stage.sh @@ -16,8 +16,9 @@ trap 'rm -rf "$work"' EXIT HUP INT TERM mkdir -p "$work/EFI/BOOT" "$work/grub/fonts" "$work/grub/theme" \ "$work/reliefos/config" "$work/leonos/config" "$work/grub/i386-pc" "${GRUB_MKSTANDALONE:-grub-mkstandalone}" -d "$modules" -O x86_64-efi -o "$work/EFI/BOOT/BOOTX64.EFI" \ - --modules='part_gpt fat iso9660 multiboot2 normal cmp search search_fs_file regexp configfile echo serial terminal video video_bochs video_cirrus efi_gop all_video font gfxterm gfxmenu hashsum gcry_sha256 crypto loadenv test' \ + --modules='part_gpt part_msdos ext2 chain ntfs linux gzio reboot search_fs_uuid search_label fat iso9660 multiboot2 normal cmp search search_fs_file regexp configfile echo serial terminal video video_bochs video_cirrus efi_gop all_video font gfxterm gfxmenu hashsum gcry_sha256 crypto loadenv test' \ "boot/grub/grub.cfg=$src/boot/grub/embedded.cfg" +printf 'set prefix=($root)/grub\nconfigfile $prefix/grub.cfg\n' > "$work/EFI/BOOT/grub.cfg" : "${RELIEFOS_KERNEL_TOOLS:?set RELIEFOS_KERNEL_TOOLS to the native Kbuild host tool}" "$RELIEFOS_KERNEL_TOOLS" boot-payload "$kernel" "$initramfs" "$work" cp "$font" "$work/grub/fonts/leonos-unicode.pf2" @@ -25,6 +26,7 @@ cp "$display" "$work/reliefos/config/display.conf" cp "$display" "$work/leonos/config/display.conf" cp "$src/boot/grub/grub.cfg" "$work/grub/grub.cfg" cp "$src/boot/grub/kernel-slots.cfg" "$work/grub/kernel-slots.cfg" +cp "$src/boot/grub/other-systems.cfg" "$work/grub/other-systems.cfg" cp "$src/boot/grub/theme/theme.txt" "$work/grub/theme/theme.txt" cp -a "$bios/." "$work/grub/i386-pc/" find "$work" -exec touch -h -d "@$epoch" {} + diff --git a/tools/build/disk.sh b/tools/build/disk.sh index 0c00480c..39edaabb 100644 --- a/tools/build/disk.sh +++ b/tools/build/disk.sh @@ -6,6 +6,10 @@ esp=$1 root=$2 raw=$3 vmdk=$4 epoch=$5 bios=$6 mkimage=$7 setup=$8 src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) export SOURCE_DATE_EPOCH=$epoch TZ=UTC LC_ALL=C +root_uuid=$(sh "$src/tools/build/image-identity.sh" root) +esp_uuid=$(sh "$src/tools/build/image-identity.sh" esp) +bios_uuid=$(sh "$src/tools/build/image-identity.sh" bios) +disk_uuid=$(sh "$src/tools/build/image-identity.sh" disk) [ "$raw" != "$vmdk" ] || { echo 'raw and VMDK outputs must differ' >&2; exit 2; } [ ! "$raw" -ef "$vmdk" ] || { echo 'raw and VMDK outputs alias one file' >&2; exit 2; } [ -f "$root" ] && [ -s "$root" ] || { echo 'root input must be a nonempty regular image' >&2; exit 1; } @@ -49,9 +53,9 @@ trap 'exit 130' INT trap 'exit 143' TERM cp -a "$esp" "$work/esp" mkdir -p "$work/esp/reliefos/config" -printf 'set reliefos_root=PARTUUID=5c13543b-732c-4f81-8652-621124484420\n' > "$work/esp/reliefos/config/root.cfg" +printf 'set reliefos_root=PARTUUID=%s\n' "$root_uuid" > "$work/esp/reliefos/config/root.cfg" printf 'version=1\nbios=1\nuefi=1\n' > "$work/esp/reliefos/config/bootloaders.conf" -marker=reliefos/config/esp-41a3ee19-ba85-47a0-9705-a5c128374021.marker +marker=reliefos/config/esp-$esp_uuid.marker printf 'ReliefOS ESP identity\n' > "$work/esp/$marker" cat > "$work/bios.cfg" < "$work/device.map" "$setup" -s --root-device=hd0,gpt2 -d "$work" -m "$work/device.map" "$work/disk.raw" +# Keep GRUB's generated core input beside its mode record, as grub-install +# does. The executable raw copy also carries embedding and RS metadata. +touch -d "@$epoch" "$work/core.img" +mcopy -o -m -i "$work/esp.fat" "$work/core.img" ::/grub/i386-pc/core.img +dd if="$work/esp.fat" of="$work/disk.raw" bs=512 seek=4096 conv=notrunc status=none sfdisk --verify "$work/disk.raw" qemu-img convert -f raw -O vmdk "$work/disk.raw" "$work/disk.vmdk" qemu-img check -f vmdk "$work/disk.vmdk" diff --git a/tools/build/ffmpeg.sh b/tools/build/ffmpeg.sh new file mode 100644 index 00000000..467d446d --- /dev/null +++ b/tools/build/ffmpeg.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Minimal LGPL MP4 decoding libraries; all input is pinned and fetched explicitly. +set -eu +MAKEFLAGS=${MAKEFLAGS-} +case ${MAKEFLAGS%% *} in *n*) exit 0;; esac +[ "$#" = 12 ] || exit 2 +pkg=$1 deps=$3 lock=$4 cache=$5 work=$6 stage=$7 musl=$8 cc=${11} target=${12} +[ "$pkg" = ffmpeg ] || exit 2 +get() { "$deps" --lock "$lock" --id "$pkg" --print "$1"; } +url=$(get url) digest=$(get sha256) directory=$(get directory) +archive=$cache/${url##*/} +[ -f "$archive" ] || { echo 'missing FFmpeg archive; run make fetch' >&2; exit 1; } +[ "$(sha256sum "$archive" | cut -d' ' -f1)" = "$digest" ] || { echo 'FFmpeg archive digest mismatch' >&2; exit 1; } +mkdir -p "$work" "$(dirname "$stage")" +tar -tf "$archive" > "$work/members" +if LC_ALL=C grep -E '(^/|(^|/)\.\.(/|$))' "$work/members"; then exit 1; fi +rm -rf "$work/source" "$work/build" +mkdir -p "$work/source" "$work/build" +tar -xf "$archive" -C "$work/source" --no-same-owner +source=$work/source/$directory +tmp=$(mktemp -d "$stage.new.XXXXXX") +trap 'rm -rf "$tmp"' EXIT HUP INT TERM +cd "$work/build" +resource=$("$cc" -print-resource-dir) +sh "$source/configure" --prefix=/usr --libdir=/usr/lib --build-suffix=-reliefplayer --arch=x86_64 --target-os=linux \ + --enable-cross-compile --cc="$cc" --ld="$cc" --ar=llvm-ar --ranlib=llvm-ranlib \ + --strip=llvm-strip --sysroot="$musl" \ + --extra-cflags="--target=$target --gcc-toolchain=/nonexistent -nostdinc -isystem $musl/include -isystem $resource/include -O2 -mno-avx -mno-avx2" \ + --extra-ldflags="--target=$target --gcc-toolchain=/nonexistent -fuse-ld=lld --rtlib=compiler-rt --unwindlib=none -L$musl/lib -Wl,-z,relro,-z,now" \ + --disable-everything --disable-autodetect --disable-network --disable-programs \ + --disable-doc --disable-debug --disable-static --enable-shared --enable-pic \ + --disable-x86asm --disable-avdevice --disable-avfilter \ + --enable-avformat --enable-avcodec --enable-avutil --enable-swscale --enable-swresample \ + --enable-demuxer=mov --enable-decoder=h264,hevc,mpeg4,aac,mp3 \ + --enable-parser=h264,hevc,mpeg4video,aac,mpegaudio +make +make DESTDIR="$tmp" install +mkdir -p "$tmp/usr/share/licenses/ffmpeg" +cp "$source/COPYING.LGPLv2.1" "$source/LICENSE.md" "$tmp/usr/share/licenses/ffmpeg/" +printf 'Upstream: %s\nCommit: %s\nSHA-256: %s\nConfiguration: %s\n' \ + "$url" "$(get commit)" "$digest" "$(cat ffbuild/config.mak | sed -n 's/^FFMPEG_CONFIGURATION=//p')" \ + > "$tmp/usr/share/licenses/ffmpeg/SOURCE" +printf '%s\n' "$pkg $digest $target" > "$tmp/.complete" +if [ -d "$stage.previous" ] && [ ! -e "$stage" ]; then mv "$stage.previous" "$stage"; fi +rm -rf "$stage.previous" +if [ -e "$stage" ]; then mv "$stage" "$stage.previous"; fi +mv "$tmp" "$stage" +rm -rf "$stage.previous" +(cd "$stage" && find . \( -type f -o -type l \) ! -name .complete -print | LC_ALL=C sort) > "$work/installed-files" diff --git a/tools/build/host-runtime-development.sh b/tools/build/host-runtime-development.sh new file mode 100644 index 00000000..3f87c76b --- /dev/null +++ b/tools/build/host-runtime-development.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Native libraries for sanitized Linux host tests; never target/SDK inputs. +set -eu +. "$(CDPATH= cd -- "$(dirname "$0")" && pwd)/upstream-make-environment.sh" +reliefos_upstream_make_environment +[ "$#" = 7 ] || { echo 'usage: host-runtime-development.sh DEPS LOCK CACHE TARGET_DEV WORK ROOT CC' >&2; exit 2; } +deps=$1 lock=$2 cache=$3 target=$4 work=$5 root=$6 cc=$7 +mkdir -p "$work" "$(dirname "$root")" +for id in host-test-mbedtls3 host-test-sqlite3; do + url=$("$deps" --lock "$lock" --id "$id" --print url) + digest=$("$deps" --lock "$lock" --id "$id" --print sha256) + archive=$cache/${url##*/} + [ -f "$archive" ] || { echo "missing $id archive; run make fetch" >&2; exit 1; } + [ "$(sha256sum "$archive" | cut -d' ' -f1)" = "$digest" ] || { echo "checksum mismatch: $id" >&2; exit 1; } + directory=$("$deps" --lock "$lock" --id "$id" --print directory) + rm -rf "$work/$directory" + tar -xf "$archive" -C "$work" +done +stage=$(mktemp -d "$root.new.XXXXXX") +trap 'rm -rf "$stage"' EXIT HUP INT TERM +mkdir -p "$stage/usr/include" "$stage/usr/lib" +cp -a "$target/usr/include/." "$stage/usr/include/" +# Preserve the official package configuration while compiling for the host libc. +make -C "$work/mbedtls-3.6.7/library" CC="$cc" AR=ar \ + CFLAGS="-O2 -fPIC -DMBEDTLS_CONFIG_FILE='\"$stage/usr/include/mbedtls/mbedtls_config.h\"'" \ + libmbedcrypto.a libmbedx509.a libmbedtls.a +cp "$work/mbedtls-3.6.7/library/"libmbed*.a "$stage/usr/lib/" +$cc -O2 -fPIC -pthread -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_RTREE \ + -DSQLITE_ENABLE_COLUMN_METADATA -c "$work/sqlite-autoconf-3530400/sqlite3.c" -o "$work/sqlite3.o" +ar rcsD "$stage/usr/lib/libsqlite3.a" "$work/sqlite3.o" +cp "$work/sqlite-autoconf-3530400/sqlite3.h" "$stage/usr/include/" +touch "$stage/.complete" +rm -rf "$root.previous" +[ ! -e "$root" ] || mv "$root" "$root.previous" +mv "$stage" "$root" +rm -rf "$root.previous" +find "$root" -mindepth 1 ! -type d -printf '%P\n' | LC_ALL=C sort >"$root.files" diff --git a/tools/build/image-identity.sh b/tools/build/image-identity.sh new file mode 100644 index 00000000..20144c26 --- /dev/null +++ b/tools/build/image-identity.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# Deterministic GPT identities for an explicitly named image instance. +set -eu +[ "$#" = 1 ] || exit 2 +instance=${RELIEFOS_IMAGE_INSTANCE:-reliefos} +case $instance in ''|*[!A-Za-z0-9._-]*) echo 'invalid IMAGE_INSTANCE_ID' >&2; exit 2;; esac +[ "${#instance}" -le 128 ] || exit 2 +case $1 in disk|bios|esp|root) ;; *) exit 2;; esac +digest=$(printf 'ReliefOS image identity v1\n%s\n%s\n' "$instance" "$1" | sha256sum) +digest=${digest%% *} +printf '%s-%s-%s-%s-%s\n' "$(printf %s "$digest" | cut -c1-8)" \ + "$(printf %s "$digest" | cut -c9-12)" "$(printf %s "$digest" | cut -c13-16)" \ + "$(printf %s "$digest" | cut -c17-20)" "$(printf %s "$digest" | cut -c21-32)" diff --git a/tools/build/iso.sh b/tools/build/iso.sh index 4ffa1000..3fec01ea 100644 --- a/tools/build/iso.sh +++ b/tools/build/iso.sh @@ -14,18 +14,19 @@ work=$(mktemp -d "$output.work.XXXXXX") trap 'rm -rf "$work"' EXIT HUP INT TERM cp -a "$esp" "$work/tree" mkdir -p "$work/tree/install" "$work/tree/boot/grub" "$work/efi/EFI/BOOT" +case $volume in RELIEFOSLIVE|RELIEFOSINST) ;; *) echo 'unsupported ISO identity' >&2; exit 2 ;; esac +marker=reliefos-$volume.marker +sed "s/@RELIEFOS_ISO_MARKER@/$marker/g" "$src/boot/grub/installer_embedded.cfg" > "$work/embedded.cfg" "${GRUB_MKSTANDALONE:-grub-mkstandalone}" -d "$modules" -O x86_64-efi -o "$work/efi/EFI/BOOT/BOOTX64.EFI" \ - --modules='part_gpt fat iso9660 multiboot2 normal cmp search search_fs_file configfile echo serial terminal video video_bochs video_cirrus efi_gop all_video font gfxterm gfxmenu' \ - "boot/grub/grub.cfg=$src/boot/grub/installer_embedded.cfg" + --modules='part_gpt part_msdos ext2 chain ntfs linux gzio reboot search_fs_uuid search_label fat iso9660 multiboot2 normal cmp search search_fs_file configfile echo serial terminal video video_bochs video_cirrus efi_gop all_video font gfxterm gfxmenu' \ + "boot/grub/grub.cfg=$work/embedded.cfg" "$mkimage" -O i386-pc-eltorito -d "$bios" -p /grub \ - -c "$src/boot/grub/installer_embedded.cfg" -o "$work/tree/boot/grub/bios.img" \ + -c "$work/embedded.cfg" -o "$work/tree/boot/grub/bios.img" \ $(cat "$src/boot/grub/bios-modules.list") cp "$work/efi/EFI/BOOT/BOOTX64.EFI" "$work/tree/EFI/BOOT/BOOTX64.EFI" cp "$root" "$work/tree/install/root.fat" cp "$config" "$work/tree/grub/grub.cfg" -printf 'ReliefOS installer ISO volume\n' > "$work/tree/reliefos-installer-iso.marker" -# Old embedded GRUB releases still locate the installer media by this marker. -printf 'ReliefOS installer ISO volume\n' > "$work/tree/leonos-installer-iso.marker" +printf 'ReliefOS %s volume\n' "$volume" > "$work/tree/$marker" find "$work/efi" "$work/tree" -exec touch -h -d "@$epoch" {} + size=$(( ($(stat -c %s "$work/efi/EFI/BOOT/BOOTX64.EFI") + 1048575) / 1048576 + 8 )) [ "$size" -ge 16 ] || size=16 diff --git a/tools/build/musl-sdk.sh b/tools/build/musl-sdk.sh index 39b428ae..08c1377c 100644 --- a/tools/build/musl-sdk.sh +++ b/tools/build/musl-sdk.sh @@ -16,7 +16,7 @@ cp -a "$src/include/leonos/." "$src/userland/runtime/include/leonos/." "$tmp/inc cp -a "$src/include/reliefos/." "$src/userland/runtime/include/reliefos/." "$tmp/include/reliefos/" cp -a "$pam/usr/include/security" "$tmp/include/" cp "$auth/usr/include/crypt.h" "$tmp/include/" -cp -P "$pam"/lib/libpam*.so* "$auth"/lib/libcrypt.so* "$tmp/lib/" +cp -P "$pam"/lib/libpam*.so* "$auth"/lib/libcrypt.so* "$auth/lib/libcrypt.a" "$tmp/lib/" cp "$reliefos_runtime" "$tmp/lib/libreliefos.so.2" cp "$reliefos_archive" "$tmp/lib/libreliefos.a" cp "$leonos_runtime" "$tmp/lib/libleonos.so.2" @@ -25,11 +25,12 @@ cp "$leonos_archive" "$tmp/lib/libleonos.a" cp "$RUNTIME_BUILTINS" "$tmp/lib/libclang_rt.builtins.a" cp "$driver" "$tmp/bin/reliefos-musl-cc" chmod 0755 "$tmp/bin/reliefos-musl-cc" -cp "$src/third_party/zlib/zlib.h" "$src/third_party/zlib/zconf.h" \ - "$src/third_party/libpng/png.h" "$src/third_party/libpng/pngconf.h" "$tmp/include/" -# Caller adds the generated libpng configuration via this script's environment. -: "${PNG_CONFIG:?generated libpng config required}" -cp "$PNG_CONFIG" "$tmp/include/pnglibconf.h" +: "${RUNTIME_DEVELOPMENT_ROOT:?verified runtime development APKs required}" +cp -a "$RUNTIME_DEVELOPMENT_ROOT/usr/include/." "$tmp/include/" +cp -P "$RUNTIME_DEVELOPMENT_ROOT"/usr/lib/lib* "$tmp/lib/" +cp -a "$RUNTIME_DEVELOPMENT_ROOT/usr/share/licenses/." "$tmp/share/licenses/" +mkdir -p "$tmp/share/reliefos/sdk" +cp -a "$RUNTIME_DEVELOPMENT_ROOT/usr/share/reliefos/sdk/apk" "$tmp/share/reliefos/sdk/" mkdir -p "$tmp/share/licenses/linux-pam" "$tmp/share/licenses/libxcrypt" cp "$pam/share/licenses/linux-pam/LICENSE" "$tmp/share/licenses/linux-pam/" cp "$auth/share/licenses/libxcrypt/LICENSE" "$tmp/share/licenses/libxcrypt/" diff --git a/tools/build/png-config.sh b/tools/build/png-config.sh deleted file mode 100644 index 97aa9058..00000000 --- a/tools/build/png-config.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -# Adjust the pinned prebuilt libpng configuration, refusing an unknown layout. -set -eu -[ "$#" = 3 ] || { echo 'usage: png-config.sh INPUT OUTPUT EMIT' >&2; exit 2; } -[ "$(grep -Fc '/* end of options */' "$1")" = 1 ] || exit 1 -tmp=$(mktemp "${2}.XXXXXX") -trap 'rm -f "$tmp"' EXIT -awk '/\/\* end of options \*\// { - print "#undef PNG_FLOATING_ARITHMETIC_SUPPORTED" - print "#undef PNG_FLOATING_POINT_SUPPORTED" - print "#undef PNG_READ_FLOAT_SUPPORTED" - print "#undef PNG_INCH_CONVERSIONS_SUPPORTED" -} { print }' "$1" > "$tmp" -# reliefos-emit consumes stdin and leaves an identical output untouched. -"$3" --output "$2" < "$tmp" diff --git a/tools/build/rootfs-stage.sh b/tools/build/rootfs-stage.sh index 04f96c5b..3ea9e733 100644 --- a/tools/build/rootfs-stage.sh +++ b/tools/build/rootfs-stage.sh @@ -19,26 +19,27 @@ link() { record l "$2" "/$1" 0777 "${3:-reliefos-base}" "${4:-unique}"; } enabled() { awk -F '\t' -v id="$1" '$1==id && $4==1 {found=1} END {exit !found}' "$metadata"; } # Every enabled upstream root is passed by the build graph. The default keeps # standalone callers compatible with the complete product package set. -rootfs_upstream_packages=${ROOTFS_UPSTREAM_PACKAGES:-'libmd libbsd util-linux sudo shadow e2fsprogs dosfstools exfatprogs coreutils alsa-lib alsa-utils nuked-opl3 ncurses'} +rootfs_upstream_packages=${ROOTFS_UPSTREAM_PACKAGES:-'libmd libbsd util-linux sudo shadow e2fsprogs dosfstools exfatprogs coreutils alsa-lib alsa-utils nuked-opl3 ffmpeg'} for package in $rootfs_upstream_packages; do + if [ "$package" = ffmpeg ]; then + if enabled reliefplayer; then + for library in "$out/upstream/ffmpeg/root/usr/lib/"*.so.*; do + if [ -L "$library" ]; then + link "usr/lib/${library##*/}" "$(readlink "$library")" reliefos-media-codecs + else + file "$library" "usr/lib/${library##*/}" 0755 reliefos-media-codecs + fi + done + tree "$out/upstream/ffmpeg/root/usr/share/licenses/ffmpeg" usr/share/licenses/ffmpeg reliefos-media-codecs + fi + continue + fi case $package in ''|*[!A-Za-z0-9._-]*) echo "rootfs-stage: invalid upstream package id: $package" >&2 exit 2 ;; esac - if [ "$package" = ncurses ]; then - if enabled "$package"; then - # Only runtime data and commands belong in the root (development static - # archives/headers are exported by SDK rules instead). - for directory in bin share; do - input=$out/upstream/$package/root/usr/$directory - if [ -d "$input" ]; then tree "$input" "usr/$directory" "$package"; fi - done - tree "$out/upstream/ncurses/root/usr/share/terminfo" etc/terminfo ncurses - fi - continue - fi for directory in bin sbin lib usr etc; do input=$out/upstream/$package/root/$directory if [ -d "$input" ]; then tree "$input" "$directory" "$package"; fi @@ -131,7 +132,7 @@ BEGIN { while ((getline app < catalog)>0) if(app!~/^#/ && app!="") artwork[app]=1; close(catalog); } -$4==1 && ($2 ~ /-app$/ || $1 ~ /^(busybox|sl)$/) { +$4==1 && ($2 ~ /-app$/ || $1 == "busybox") { terminal=0; ini=src "/userland/apps/" $1 "/" $1 ".app.ini"; while ((getline line < ini)>0) {split(line,a,"="); if(a[1]=="terminal" && a[2]~/^(1|true|yes)$/) terminal=1} close(ini); ini=src "/userland/" $1 "/" $1 ".app.ini"; @@ -190,29 +191,23 @@ if enabled doom; then file "$src/third_party/doomgeneric/freedoom1.wad" usr/lib/reliefos/apps/doom/freedoom1.wad 0644 doom file "$src/third_party/doomgeneric/FREEDOOM-COPYING.txt" usr/share/doc/reliefos/FREEDOOM-COPYING.txt 0644 doom fi -# Tool executables keep their native locations; registry entries use a local -# symlink so the registry's relative exec contract remains the same as apps. -for app in busybox sl; do +# BusyBox retains its system registry entry; upstream commands use native paths. +for app in busybox; do if enabled "$app"; then file "$work/manifests/$app.ini" "usr/lib/reliefos/apps/$app/manifest.ini" 0644 "$app" - target=/usr/bin/$app - [ "$app" != busybox ] || target=/bin/busybox - link "usr/lib/reliefos/apps/$app/$app.elf" "$target" "$app" + link "usr/lib/reliefos/apps/$app/$app.elf" /bin/busybox "$app" fi done -for app in fastfetch sl; do +for app in fastfetch; do if enabled "$app"; then file "$out/userland/$app.elf" "usr/bin/$app" 0755 "$app" override; fi done -for spec in 'sqlite sqlite.so.3'; do - set -- $spec - if enabled "$1"; then file "$out/userland/$2" "usr/lib/$2" 0755 "$1"; fi -done if enabled fastfetch; then + file "$src/userland/fastfetch/LICENSE" usr/share/licenses/fastfetch/LICENSE 0644 fastfetch file "$src/userland/fastfetch/config.jsonc" etc/fastfetch/config.jsonc 0644 fastfetch file "$src/userland/fastfetch/leonos-ascii.txt" usr/share/fastfetch/leonos-ascii.txt 0644 fastfetch file "$src/userland/fastfetch/hyfetch.json" etc/skel/.config/hyfetch.json 0644 fastfetch fi -for spec in 'busybox third_party/busybox/LICENSE' 'sl third_party/sl/LICENSE' 'pleditor third_party/pl_editor/LICENSE'; do +for spec in 'busybox third_party/busybox/LICENSE' 'pleditor third_party/pl_editor/LICENSE'; do set -- $spec if enabled "$1"; then file "$src/$2" "usr/share/licenses/$1/${2##*/}" 0644 "$1" override; fi done @@ -250,6 +245,18 @@ file "$src/system/certs/cacert.pem" etc/ssl/certs/ca-certificates.crt file "$src/system/certs/cacert.pem" usr/share/reliefos/certs/ca-certificates.crt 0644 trust file "$src/Documentation/admin-guide/packages.rst" usr/share/doc/reliefos/packages.rst file "$src/configs/apk-ownership.json" usr/share/reliefos/apk-ownership.json +: > "$work/data/apk-components" +for component in sl ncurses sqlite; do + if enabled "$component"; then + printf '%s\n' "$component" >> "$work/data/apk-components" + file "$src/resources/licenses/$component-LICENSE" "usr/share/licenses/$component/LICENSE" + fi +done +file "$work/data/apk-components" etc/reliefos/apk-components +for library in zlib libpng mbedtls3; do + file "$src/resources/licenses/$library-LICENSE" "usr/share/licenses/reliefos-runtime-dependencies/$library-LICENSE" +done + for name in simsun.ttc system.psf; do file "$src/system/fonts/$name" "usr/share/fonts/reliefos/$name"; done file "$src/system/fonts/times.ttf" usr/share/fonts/reliefos/times-new-roman.ttf for source in "$src/resources/build-art/minesweeper"/*.bmp; do file "$source" "usr/share/reliefos/resources/${source##*/}"; done diff --git a/tools/build/rpr-pages.sh b/tools/build/rpr-pages.sh index cfe004ae..24df641a 100644 --- a/tools/build/rpr-pages.sh +++ b/tools/build/rpr-pages.sh @@ -48,21 +48,29 @@ mkdir -p "$work/site/apk" "$work/site/kernel" "$work/site/packages" "$work/site/ openssl pkey -in "$key" -pubout -out "$work/site/apk/reliefos-rpr.rsa.pub" 2>/dev/null cp "$work/site/apk/reliefos-rpr.rsa.pub" "$work/site/apk/leonos-rpr.rsa.pub" chmod 644 "$work/site/apk/reliefos-rpr.rsa.pub" "$work/site/apk/leonos-rpr.rsa.pub" -for source in "$repository"/reliefos-*.apk "$apps"/reliefos-*.apk; do +# Indexing verifies the original signatures too: trust both the local signing +# key and the pinned Alpine keys, without re-signing upstream packages. +mkdir "$work/keys" +source_root=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +cp "$source_root"/system/rootfs/etc/apk/keys/*.pub "$work/keys/" +cp "$work/site/apk/reliefos-rpr.rsa.pub" "$work/keys/" +# Include the complete verified dependency closure; official APK bytes and +# signatures are preserved when publishing the local RPR tree. +for source in "$repository"/*.apk "$apps"/reliefos-*.apk; do [ -f "$source" ] || { echo "missing RPR package $source" >&2; exit 1; } name=${source##*/} case $name in *[!a-zA-Z0-9._+-]*) echo 'unsafe RPR filename' >&2; exit 1 ;; esac # apk resolves a package from its metadata as name-version.apk. A # versionless archive indexes successfully but every normal client then # requests a URL that does not exist. - printf '%s\n' "$name" | grep -Eq '^reliefos-.+-[0-9][A-Za-z0-9._+-]*\.apk$' || { + printf '%s\n' "$name" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9+_.-]*-[0-9][A-Za-z0-9._+-]*\.apk$' || { echo "RPR package filename is missing its version: $name" >&2 exit 1 } [ ! -e "$work/site/apk/$name" ] || { echo "duplicate RPR package $name" >&2; exit 1; } cp "$source" "$work/site/apk/$name" done -"$apk" mkndx --keys-dir "$work/site/apk" --sign-key "$key" --output "$work/site/apk/packages.adb" "$work/site/apk"/*.apk +"$apk" mkndx --keys-dir "$work/keys" --sign-key "$key" --output "$work/site/apk/packages.adb" "$work/site/apk"/*.apk kernel_package= for file in "$work/site/apk"/reliefos-kernel-[0-9]*.apk; do [ -f "$file" ] || continue @@ -202,11 +210,12 @@ HTML APK packages published in this release PackageVersionArchitectureSize HTML - for file in "$work/site/apk"/reliefos-*.apk; do + for file in "$work/site/apk"/*.apk; do [ -f "$file" ] || continue fname=${file##*/} - base=${fname%.apk} # reliefos---r - stem=${base%%-[0-9]*} # reliefos-: up to the first - + base=${fname%.apk} # --r + without_revision=${base%-*} + stem=${without_revision%-*} # names may contain digits: libatk-1.0 ver=${base#"$stem"-} # -r size=$(stat -c %s "$file") esc_name=$(printf '%s' "$stem" | site_html_escape) @@ -224,11 +233,12 @@ HTML # Per-package information pages: data is parsed from the validated APK filename # plus real file size and the recorded SHA256SUMS hash, so it can never drift # from what is actually published (plan §17, §21). -for file in "$work/site/apk"/reliefos-*.apk; do +for file in "$work/site/apk"/*.apk; do [ -f "$file" ] || continue fname=${file##*/} base=${fname%.apk} - stem=${base%%-[0-9]*} + without_revision=${base%-*} + stem=${without_revision%-*} ver=${base#"$stem"-} size=$(stat -c %s "$file") hash=$(site_hash_for "$work/site/apk/SHA256SUMS" "$fname") diff --git a/tools/build/runtime-development.sh b/tools/build/runtime-development.sh new file mode 100644 index 00000000..2cc37bd2 --- /dev/null +++ b/tools/build/runtime-development.sh @@ -0,0 +1,95 @@ +#!/bin/sh +# Build-only target headers and libraries; never overlay this root on the guest. +set -eu +[ "$#" = 6 ] || { printf 'usage: runtime-development.sh SRC DEPS LOCK CACHE APK ROOT\n' >&2; exit 2; } +src=$(CDPATH= cd -- "$1" && pwd) +deps=$2 +lock=$(CDPATH= cd -- "$(dirname "$3")" && pwd)/$(basename "$3") +cache=$(CDPATH= cd -- "$4" && pwd) +apk=$(CDPATH= cd -- "$(dirname "$5")" && pwd)/$(basename "$5") +root_parent_input=$(dirname "$6") +root_name=$(basename "$6") +mkdir -p "$root_parent_input" +root_parent=$(CDPATH= cd -- "$root_parent_input" && pwd) +root=$root_parent/$root_name +if [ -L "$root" ] || { [ -e "$root" ] && [ ! -d "$root" ]; }; then + printf 'development root is not a real directory: %s\n' "$root" >&2 + exit 1 +fi +stage=$(mktemp -d "$root_parent/.${root_name}.new.XXXXXX") +backup= +cleanup() { + status=$? + if [ -n "$backup" ] && [ -e "$backup" ] && + [ ! -e "$root" ] && [ ! -L "$root" ]; then + mv "$backup" "$root" || status=1 + fi + if [ -n "$stage" ] && [ -e "$stage" ]; then + rm -rf "$stage" || status=1 + fi + if [ -n "$backup" ] && [ -e "$backup" ] && + { [ -e "$root" ] || [ -L "$root" ]; }; then + rm -rf "$backup" || status=1 + fi + exit "$status" +} +trap cleanup EXIT +trap 'exit 130' HUP INT TERM +# The -dev and -static archives are build inputs, not target-root overlays. +for id in alpine-zlib runtime-dev-zlib-dev runtime-dev-zlib-static \ + alpine-libpng runtime-dev-libpng-dev runtime-dev-libpng-static \ + alpine-sqlite-libs runtime-dev-sqlite-dev runtime-dev-sqlite-static \ + alpine-mbedtls3 runtime-dev-mbedtls3-dev runtime-dev-mbedtls3-static; do + url=$("$deps" --lock "$lock" --id "$id" --print url) + digest=$("$deps" --lock "$lock" --id "$id" --print sha256) + archive=$cache/${url##*/} + [ -f "$archive" ] || { printf 'missing %s; run make fetch\n' "$archive" >&2; exit 1; } + [ "$(sha256sum "$archive" | cut -d' ' -f1)" = "$digest" ] || { printf 'checksum mismatch: %s\n' "$archive" >&2; exit 1; } + "$apk" --keys-dir "$src/system/rootfs/etc/apk/keys" verify "$archive" + tar --ignore-zeros --warning=no-unknown-keyword -tzf "$archive" >"$stage/members" + : >"$stage/selected" + while IFS= read -r member; do + case $member in */) continue;; esac + case $member in + usr/include/*|usr/lib/libz.*|usr/lib/libpng*|usr/lib/libsqlite3.*|usr/lib/libmbed*|usr/lib/libeverest.a|usr/lib/libp256m.a) + case $member in *../*|*/../*|*'\'*) printf 'unsafe archive member\n' >&2; exit 1;; esac + printf '%s\n' "$member" >>"$stage/selected" + ;; + esac + done <"$stage/members" + tar --ignore-zeros --warning=no-unknown-keyword -xzf "$archive" -C "$stage" -T "$stage/selected" + mkdir -p "$stage/usr/share/reliefos/sdk/apk" + tar --ignore-zeros --warning=no-unknown-keyword -xOf "$archive" .PKGINFO >"$stage/usr/share/reliefos/sdk/apk/$id.PKGINFO" +done +rm "$stage/members" "$stage/selected" +for header in zlib.h zconf.h png.h pngconf.h pnglibconf.h sqlite3.h mbedtls/ssl.h mbedtls/mbedtls_config.h; do + test -f "$stage/usr/include/$header" +done +for library in libz.so libz.so.1 libz.a libpng.so libpng16.so.16 libpng.a \ + libsqlite3.so libsqlite3.so.0 libsqlite3.a \ + libmbedtls.so libmbedtls.so.21 libmbedtls.a \ + libmbedx509.so libmbedx509.so.7 libmbedx509.a \ + libmbedcrypto.so libmbedcrypto.so.16 libmbedcrypto.a; do + test -f "$stage/usr/lib/$library" +done +mkdir -p "$stage/usr/share/licenses" +for name in zlib libpng sqlite mbedtls3; do + mkdir -p "$stage/usr/share/licenses/$name" + cp "$src/resources/licenses/$name-LICENSE" "$stage/usr/share/licenses/$name/LICENSE" +done +touch "$stage/.complete" + +if [ -e "$root" ]; then + backup=$(mktemp -d "$root_parent/.${root_name}.old.XXXXXX") + rmdir "$backup" + mv "$root" "$backup" +fi +if ! mv "$stage" "$root"; then + if [ -n "$backup" ]; then mv "$backup" "$root"; backup=; fi + printf 'could not install staged development root: %s\n' "$root" >&2 + exit 1 +fi +stage= +find "$root" -mindepth 1 ! -type d -printf '%P\n' | LC_ALL=C sort >"$root.files" +if [ -n "$backup" ]; then rm -rf "$backup"; backup=; fi +trap - EXIT HUP INT TERM diff --git a/tools/build/standalone-root.sh b/tools/build/standalone-root.sh index adbb35fe..4caa0915 100644 --- a/tools/build/standalone-root.sh +++ b/tools/build/standalone-root.sh @@ -28,10 +28,12 @@ printf 'leonos-standalone-test-v1\n' > "$work/etc/reliefos/test-image" printf 'test-image=1\n' > "$work/etc/reliefos/installed" printf 'LANG=%s\nMUSL_LOCPATH=/usr/share/musl/locales\n' "$language" > "$work/etc/reliefos/locale.conf" if [ "$mode" = disk ]; then - cat > "$work/etc/fstab" <<'FSTAB' + root_uuid=$(sh "$src/tools/build/image-identity.sh" root) + esp_uuid=$(sh "$src/tools/build/image-identity.sh" esp) + cat > "$work/etc/fstab" < "$work/generated/Makefile" - (cd "$work/generated" && make sqlite3.c sqlite3.h) - flags="$flags -DSQLITE_OS_OTHER=1 -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_OMIT_WAL=1 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_MAX_MMAP_SIZE=0" - compile -c "$work/generated/sqlite3.c" -o "$work/objects/sqlite3.o" - cp "$work/objects/sqlite3.o" "$output/sqlite3.o" - cp "$work/generated/sqlite3.c" "$work/generated/sqlite3.h" "$output/" - ;; *) echo "unsupported upstream app: $pkg" >&2; exit 2 ;; esac diff --git a/tools/build/upstream-terminal.sh b/tools/build/upstream-terminal.sh deleted file mode 100644 index 8a2961e8..00000000 --- a/tools/build/upstream-terminal.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh -set -eu -MAKEFLAGS=${MAKEFLAGS-} -case ${MAKEFLAGS%% *} in *n*) exit 0 ;; esac -. "$(CDPATH= cd -- "$(dirname "$0")" && pwd)/upstream-make-environment.sh" -reliefos_upstream_make_environment -[ "$#" = 9 ] || { echo 'usage: upstream-terminal.sh PACKAGE SRC DEPS LOCK WORK STAGE MUSL CC TARGET' >&2; exit 2; } -pkg=$1 src=$2 deps=$3 lock=$4 work=$5 stage=$6 musl=$7 -shift 7 -cc=$1 target=$2 -get() { "$deps" --lock "$lock" --id "$pkg" --print "$1"; } -source=$src/$(get directory) -revision=$(get commit) -[ "$(git -C "$source" rev-parse HEAD)" = "$revision" ] || { echo "$pkg: source revision mismatch" >&2; exit 1; } -mkdir -p "$work" "$(dirname "$stage")" -rm -rf "$work/source" "$work/build" -mkdir -p "$work/source" "$work/build" -# git archive consumes the pinned revision, excluding local/generated changes. -git -C "$source" archive "$revision" > "$work/source.tar" -tar -xf "$work/source.tar" -C "$work/source" -source=$work/source -resource=$("$cc" -print-resource-dir) -CC="$cc --target=$target --sysroot=$musl --gcc-toolchain=/nonexistent -fuse-ld=lld --rtlib=compiler-rt --unwindlib=none -nostdinc -isystem $musl/include -isystem $resource/include -L$musl/lib" -AR=llvm-ar RANLIB=llvm-ranlib CFLAGS='-O2 -fno-stack-protector -mno-avx -mno-avx2' CPPFLAGS= LDFLAGS=-static LIBS= -export CC AR RANLIB CFLAGS CPPFLAGS LDFLAGS LIBS -tmp=$(mktemp -d "$stage.new.XXXXXX") -trap 'rm -rf "$tmp"' EXIT -case $pkg in -ncurses) - tic=$(command -v tic); infocmp=$(command -v infocmp) - cd "$work/build" - "$source/configure" --host="$target" --prefix=/usr --with-normal --without-shared --without-debug --enable-widec --with-termlib --enable-overwrite --without-ada --without-cxx-binding --without-tests --with-default-terminfo-dir=/usr/share/terminfo --with-terminfo-dirs=/usr/share/terminfo:/etc/terminfo:/lib/terminfo --with-fallbacks=xterm,xterm-256color,linux,vt100,ansi,screen,screen-256color --with-tic-path="$tic" --with-infocmp-path="$infocmp" --enable-mixed-case - ;; -*) exit 2 ;; -esac -make -make install "DESTDIR=$tmp" -for name in xterm xterm-256color linux vt100 ansi screen screen-256color; do grep -F "/* $name */" "$work/build/ncurses/fallback.c" >/dev/null; done -# Retain the same deterministic first spelling as the former driver. -(cd "$tmp/usr/share/terminfo" && find . -type f -print | LC_ALL=C sort | LC_ALL=C awk '{ k=tolower($0); if (seen[k]++) print }') > "$work/duplicate-terminfo" -while IFS= read -r name; do rm "$tmp/usr/share/terminfo/$name"; done < "$work/duplicate-terminfo" -mkdir -p "$tmp/usr/share/licenses/$pkg" -cp "$source/COPYING" "$tmp/usr/share/licenses/$pkg/" -printf '%s\n' "$pkg $revision $target" > "$tmp/.complete" -if [ -d "$stage.previous" ] && [ ! -e "$stage" ]; then mv "$stage.previous" "$stage"; fi -rm -rf "$stage.previous" -if [ -e "$stage" ]; then mv "$stage" "$stage.previous"; fi -mv "$tmp" "$stage" -rm -rf "$stage.previous" -(cd "$stage" && find . \( -type f -o -type l \) ! -name .complete -print | LC_ALL=C sort) > "$work/installed-files" diff --git a/tools/build/verify-pages.sh b/tools/build/verify-pages.sh index f63fb110..fdd1500b 100755 --- a/tools/build/verify-pages.sh +++ b/tools/build/verify-pages.sh @@ -71,7 +71,7 @@ fi if [ -f "$site/rpr/apk/repository.json" ]; then awk -F '"' ' $0 ~ /"packages"/ { inlist=1 } - inlist { for (i=1;i<=NF;i++) if ($i ~ /^reliefos-.*\.apk$/) print $i } + inlist { for (i=1;i<=NF;i++) if ($i ~ /^[A-Za-z0-9][A-Za-z0-9+_.-]*-[0-9][A-Za-z0-9._+-]*\.apk$/) print $i } ' "$site/rpr/apk/repository.json" > "$verify_work/packages" while IFS= read -r pkg; do [ -n "$pkg" ] || continue diff --git a/tools/check_licenses.py b/tools/check_licenses.py index e2b25a96..28c9d921 100644 --- a/tools/check_licenses.py +++ b/tools/check_licenses.py @@ -22,22 +22,14 @@ from typing import Iterable, Sequence -DEFAULT_EXCLUDED_CREDITS = ("llama2.c", "TinyLlama", "karpathy") - # The first existing candidate is accepted. SUBMODULE_LICENSES: dict[str, tuple[str, ...]] = { "litehtml": ("LICENSE",), - "mbedtls": ("LICENSE",), "musl": ("COPYRIGHT",), "mimalloc": ("LICENSE",), "busybox": ("LICENSE",), - "ncurses": ("COPYING",), "pl_editor": ("LICENSE", "COPYING"), - "zlib": ("LICENSE",), - "libpng": ("LICENSE",), "stardustui": ("LICENSE",), - "sl": ("LICENSE",), - "sqlite": ("LICENSE.md", "LICENSE", "COPYING"), "portablegl": ("LICENSE",), } @@ -55,11 +47,13 @@ SDK_LICENSES: dict[str, tuple[str, ...]] = { "libncursesw.a": ("THIRD_PARTY/NCURSES-COPYING",), "libtinfow.a": ("THIRD_PARTY/NCURSES-COPYING",), - "libc.a": ("THIRD_PARTY/MUSL-COPYING",), - "libz.a": ("THIRD_PARTY/ZLIB-LICENSE",), - "libpng.a": ("THIRD_PARTY/LIBPNG-LICENSE",), - "sqlite.a": ("THIRD_PARTY/SQLITE-LICENSE",), - "sqlite.so.3": ("THIRD_PARTY/SQLITE-LICENSE",), + "libc.a": ("share/licenses/musl/COPYRIGHT", "THIRD_PARTY/MUSL-COPYING"), + "libz.a": ("share/licenses/zlib/LICENSE", "THIRD_PARTY/ZLIB-LICENSE"), + "libpng.a": ("share/licenses/libpng/LICENSE", "THIRD_PARTY/LIBPNG-LICENSE"), + "libsqlite3.a": ("share/licenses/sqlite/LICENSE",), + "libmbedcrypto.a": ("share/licenses/mbedtls3/LICENSE",), + "libmbedtls.a": ("share/licenses/mbedtls3/LICENSE",), + "libmbedx509.a": ("share/licenses/mbedtls3/LICENSE",), } @@ -202,6 +196,20 @@ def check_submodules(root: Path) -> list[Finding]: return findings +def check_binary_notices(root: Path) -> list[Finding]: + findings = [] + for name in ("sl", "ncurses", "zlib", "libpng", "sqlite", "mbedtls3"): + relative = f"resources/licenses/{name}-LICENSE" + path = root / relative + present = path.is_file() and path.stat().st_size > 0 + findings.append(result("binary-license", "pass" if present else "fail", + "info" if present else "error", name, relative, + relative if present else "missing", str(path), + "binary dependency license is present" if present else + "binary dependency license is missing")) + return findings + + def image_base(source: Path) -> Path: if source.is_dir(): for relative in ("install/root", "root/install/root", "install/esp", "root/install/esp"): @@ -249,10 +257,10 @@ def check_image(source: Path, category: str = "image-license") -> list[Finding]: "packaged program license is present" if found else "program is present but its license file is missing")) if artifact.has_prefix("usr/share/terminfo"): - notice = "usr/share/licenses/ncurses/COPYING" - found = artifact.find((notice,)) + notices = ("usr/share/licenses/ncurses/LICENSE", "usr/share/licenses/ncurses/COPYING") + found = artifact.find(notices) findings.append(result(category, "pass" if found else "fail", - "info" if found else "error", "ncurses", notice, + "info" if found else "error", "ncurses", " or ".join(notices), found or "missing", str(source), "ncurses runtime license")) api_notices = {"oschinpt": ("LICENSE", "ATTRIBUTION.txt")} for api, notices in api_notices.items(): @@ -309,33 +317,6 @@ def check_sdk(source: Path) -> list[Finding]: artifact.close() -def read_acknowledgement_source(root: Path) -> str: - source = root / "userland/apps/installer/main.cpp" - if not source.is_file(): - return "" - text = source.read_text(encoding="utf-8", errors="replace") - arrays = re.findall( - r'static\s+const\s+char\s+acknowledgements_(?:en|zh)\[\]\s*=\s*' - r'(?:"(?:[^"\\]|\\.)*"\s*)+;', text, re.DOTALL) - return "\n".join(arrays) - - -def check_excluded_credits(root: Path, excluded: Sequence[str]) -> list[Finding]: - text = read_acknowledgement_source(root) - if not text: - return [result("excluded-credit", "fail", "error", "installer acknowledgements", "source text", - "missing", "userland/apps/installer/main.cpp", "installer acknowledgement text was not found")] - findings = [] - for term in excluded: - present = re.search(re.escape(term), text, re.IGNORECASE) is not None - findings.append(result("excluded-credit", "fail" if present else "pass", - "error" if present else "info", term, "absent from installer acknowledgement page", - "present" if present else "absent", "userland/apps/installer/main.cpp", - "excluded project appears in installer acknowledgements" if present else - "excluded project is absent from installer acknowledgements")) - return findings - - def locate_default(root: Path, explicit: Path | None, names: Sequence[str]) -> Path: if explicit: return explicit if explicit.is_absolute() else root / explicit @@ -364,9 +345,6 @@ def self_test() -> int: (root / ".gitmodules").write_text('[submodule "third_party/demo"]\n\tpath = third_party/demo\n\turl = https://example.invalid/demo\n', encoding="utf-8") (root / "third_party/demo").mkdir(parents=True) (root / "third_party/demo/LICENSE").write_text("MIT\n", encoding="utf-8") - (root / "userland/apps/installer").mkdir(parents=True) - (root / "userland/apps/installer/main.cpp").write_text('static const char acknowledgements_en[] = "ok";\nstatic int text_eq(void);', encoding="utf-8") - assert check_excluded_credits(root, ("TinyLlama",))[0].status == "pass" assert check_submodules(root)[0].status == "pass" esp_program = root / "build/esp/usr/bin" esp_program.mkdir(parents=True) @@ -383,10 +361,6 @@ def self_test() -> int: (sdk / "THIRD_PARTY").mkdir() (sdk / "THIRD_PARTY/ZLIB-LICENSE").write_text("zlib\n", encoding="utf-8") assert any(f.component == "libz.a" and f.status == "pass" for f in check_sdk(sdk)) - (root / "userland/apps/installer/main.cpp").write_text( - 'static const char acknowledgements_en[] = "TinyLlama";\nstatic int text_eq(void);', - encoding="utf-8") - assert check_excluded_credits(root, ("TinyLlama",))[0].status == "fail" (root / "third_party/demo/LICENSE").unlink() assert check_submodules(root)[0].status == "fail" print("self-test: passed") @@ -399,8 +373,6 @@ def main(argv: Sequence[str] | None = None) -> int: parser.add_argument("--image", type=Path, help="ESP staging directory or ZIP") parser.add_argument("--sdk", type=Path, help="SDK directory or ZIP") parser.add_argument("--installer", type=Path, help="installer staging directory or ZIP") - parser.add_argument("--excluded-credit", action="append", dest="excluded", default=None, - help="project name forbidden in installer acknowledgements (repeatable)") parser.add_argument("--json", type=Path, dest="json_path", help="write machine-readable report") parser.add_argument("--strict", action="store_true", help="also fail when warnings are present") parser.add_argument("--self-test", action="store_true", help="run built-in tests") @@ -413,15 +385,11 @@ def main(argv: Sequence[str] | None = None) -> int: # is only license-checked when an artifact is given explicitly. sdk = args.sdk installer = locate_default(root, args.installer, ("build/install", "build/installer-iso")) - excluded = tuple(args.excluded) if args.excluded else DEFAULT_EXCLUDED_CREDITS - findings = check_submodules(root) + findings = check_submodules(root) + check_binary_notices(root) findings += check_image(image) if sdk: findings += check_sdk(sdk if sdk.is_absolute() else root / sdk) - # Installer ESP is an image staging tree; acknowledgement source is always - # checked independently so docs containing the same names cannot confuse it. findings += check_image(installer, "installer-license") - findings += check_excluded_credits(root, excluded) return report(findings, args.json_path, args.strict) diff --git a/tools/host/apk/reliefos-apk-own.c b/tools/host/apk/reliefos-apk-own.c index d6ba4949..4c15aacf 100644 --- a/tools/host/apk/reliefos-apk-own.c +++ b/tools/host/apk/reliefos-apk-own.c @@ -187,13 +187,7 @@ static int load_rules(struct state *state, const char *policy_path, json_value * {"reliefos-fastfetch", "etc/fastfetch"}, {"reliefos-fastfetch", "usr/share/fastfetch/leonos-ascii.txt"}, {"reliefos-fastfetch", "etc/skel/.config/hyfetch.json"}, {"reliefos-apps", "usr/share/licenses/pleditor"}, - {"busybox", "bin/busybox"}, {"busybox", "bin/sh"}, {"busybox", "usr/share/licenses/busybox"}, - {"ncurses", "usr/share/terminfo"}, {"ncurses", "etc/terminfo"}, - {"ncurses", "usr/share/licenses/ncurses"}, {"sl", "usr/bin/sl"}, {"sl", "usr/share/licenses/sl"} - }; - static const char *const ncurses_commands[] = { - "clear", "infocmp", "infotocap", "captoinfo", "reset", "tabs", "tic", - "toe", "tput", "tset", "ncursesw6-config" + {"busybox", "bin/busybox"}, {"busybox", "bin/sh"}, {"busybox", "usr/share/licenses/busybox"} }; static const char *const util_commands[] = { "usr/sbin/fdisk", "usr/sbin/sfdisk", "usr/sbin/runuser", "usr/sbin/fsck", @@ -210,12 +204,6 @@ static int load_rules(struct state *state, const char *policy_path, json_value * size_t item; for (item = 0; item < sizeof(tool_paths) / sizeof(tool_paths[0]); item++) if (add_rule(state, tool_paths[item].path, tool_paths[item].group) != 0) return -1; - for (item = 0; item < sizeof(ncurses_commands) / sizeof(ncurses_commands[0]); item++) { - char *path = join2("usr/bin", ncurses_commands[item]); - int result = path == NULL ? -1 : add_rule(state, path, "ncurses"); - free(path); - if (result != 0) return -1; - } for (item = 0; item < sizeof(util_commands) / sizeof(util_commands[0]); item++) if (add_rule(state, util_commands[item], "storage-util-linux") != 0) return -1; for (item = 0; item < sizeof(util_libraries) / sizeof(util_libraries[0]); item++) diff --git a/tools/host/manifest/reliefos-deps.c b/tools/host/manifest/reliefos-deps.c index 9b543fe4..3103a62d 100644 --- a/tools/host/manifest/reliefos-deps.c +++ b/tools/host/manifest/reliefos-deps.c @@ -222,8 +222,11 @@ static int validate_entry(const json_value *entry, const char *root_dir, int ind if (feature != NULL && (feature->type != JSON_STRING || (strcmp(feature->text, "base") != 0 - && strcmp(feature->text, "xorg") != 0))) { - return report("%s: \"feature\" must be base or xorg", id); + && strcmp(feature->text, "xorg") != 0 + && strcmp(feature->text, "component:sl") != 0 + && strcmp(feature->text, "component:ncurses") != 0 + && strcmp(feature->text, "component:sqlite") != 0))) { + return report("%s: \"feature\" must be base, xorg or component:sl/ncurses/sqlite", id); } } if (is_downloaded(kind)) { diff --git a/tools/host/sdk/reliefos-musl-cc.c b/tools/host/sdk/reliefos-musl-cc.c index a2aa3709..806e4089 100644 --- a/tools/host/sdk/reliefos-musl-cc.c +++ b/tools/host/sdk/reliefos-musl-cc.c @@ -98,7 +98,7 @@ int main(int argc, char **argv) fprintf(stderr, "reliefos-musl-cc: RELIEFOS_SDK_ABI must be reliefos or leonos\n"); return 2; } - char **args = calloc((size_t)argc + 48, sizeof(*args)); + char **args = calloc((size_t)argc + 64, sizeof(*args)); if (!args) { perror("SDK driver"); return 126; } size_t n = 0; args[n++] = (char *)compiler; @@ -149,11 +149,17 @@ int main(int argc, char **argv) if (static_link) { args[n++] = paths[5]; args[n++] = "-Wl,--start-group"; args[n++] = leonos_abi ? "-lleonos" : "-lreliefos"; + args[n++] = "-lpng"; args[n++] = "-lz"; + args[n++] = "-lmbedtls"; args[n++] = "-lmbedx509"; + args[n++] = "-lmbedcrypto"; args[n++] = "-lcrypt"; args[n++] = "-lc"; args[n++] = "-Wl,--end-group"; } else { args[n++] = "-l:libmimalloc.so.3"; args[n++] = leonos_abi ? "-l:libleonos.so.2" : "-l:libreliefos.so.2"; - args[n++] = "-lc"; + args[n++] = "-Wl,-rpath-link"; args[n++] = paths[6]; + args[n++] = "-lpng"; args[n++] = "-lz"; + args[n++] = "-lmbedtls"; args[n++] = "-lmbedx509"; + args[n++] = "-lmbedcrypto"; args[n++] = "-lc"; } args[n++] = "-l:libclang_rt.builtins.a"; if (!shared) args[n++] = paths[4]; diff --git a/tools/runtime_test_dependencies.py b/tools/runtime_test_dependencies.py new file mode 100644 index 00000000..7c3ed89c --- /dev/null +++ b/tools/runtime_test_dependencies.py @@ -0,0 +1,15 @@ +"""Host-test linkage to matching host-native libraries (the target uses signed APKs).""" +import os +from pathlib import Path + + +def dependency_root(): + root = Path(os.environ.get("RELIEFOS_TEST_DEP_ROOT", Path(__file__).resolve().parents[1] / "out/x86_64/release/host/runtime-development")) + if not (root / "usr/include/mbedtls/ssl.h").is_file() or not (root / "usr/lib/libmbedcrypto.a").is_file(): + raise RuntimeError("Run make host-runtime-development first, or set RELIEFOS_TEST_DEP_ROOT to its host-native test stage") + return root + + +def crypto_flags(*libraries): + root = dependency_root() + return ["-I" + str(root / "usr/include"), *[str(root / "usr/lib" / ("lib" + name + ".a")) for name in (libraries or ("mbedcrypto",))], "-pthread"] diff --git a/tools/test_bootloader_install.py b/tools/test_bootloader_install.py index c598897e..2cd793c9 100644 --- a/tools/test_bootloader_install.py +++ b/tools/test_bootloader_install.py @@ -1,5 +1,6 @@ """Host publication and refusal tests for the shared installer API.""" from pathlib import Path +from runtime_test_dependencies import crypto_flags import contextlib import os import hashlib @@ -50,24 +51,21 @@ def main(): helper.chmod(0o755) helpers[name] = helper sources = [ROOT / "userland/runtime/src" / f"bootloader{suffix}.c" - for suffix in ("", "_publish", "_process")] + for suffix in ("", "_publish", "_process", "_uefi")] sources.extend(ROOT / "userland/runtime/src" / (name + ".c") for name in ("kernel_bundle_io", "kernel_bundle_check", "kernel_bundle_slots", "kernel_bundle_publish", "kernel_bundle_sync")) - sources.extend((ROOT / "userland/runtime/shared/boot_slots.c", - ROOT / "third_party/mbedtls/library/sha256.c", - ROOT / "third_party/mbedtls/library/platform_util.c")) - (work / "qa_mbedtls.h").write_text("#define MBEDTLS_SHA256_C\n") + sources.append(ROOT / "userland/runtime/shared/boot_slots.c") subprocess.run([ "cc", "-std=gnu11", "-D_GNU_SOURCE", "-O1", "-g", "-fsanitize=address,undefined", "-Wall", "-Wextra", "-Werror", - '-DMBEDTLS_CONFIG_FILE="qa_mbedtls.h"', "-I", work, "-Ithird_party/mbedtls/include", + "-I", work, f'-DBOOT_GRUB_INSTALL="{adapter}"', *[f'-DBOOT_{name.upper().replace("-", "_")}="{path}"' for name, path in helpers.items()], "-Wl,--wrap=stat,--wrap=lstat,--wrap=open,--wrap=ioctl,--wrap=fsync,--wrap=write,--wrap=setmntent", "-Wl,--wrap=reliefos_block_get_info,--wrap=reliefos_block_list_partitions", "-Wl,--wrap=reliefos_block_partition_uuid", "-Iuserland/runtime/include", "tests/host/test_bootloader_install.c", - *sources, "-o", executable, + *sources, *crypto_flags(), "-o", executable, ], cwd=ROOT, check=True) fixture = fixtures.ModulesStage("test_real_metadata_dependency_closure_and_stale_output") fixture.setUp() @@ -82,7 +80,7 @@ def main(): source = work / "source" source.mkdir() subprocess.run([fixtures.KERNEL_TOOLS, "boot-payload", fixture.install / "kernel.sys", archive, source], check=True) - for relative in ("grub/grub.cfg", "grub/kernel-slots.cfg", + for relative in ("grub/grub.cfg", "grub/kernel-slots.cfg", "grub/other-systems.cfg", "grub/fonts/leonos-unicode.pf2", "grub/theme/theme.txt", "EFI/BOOT/BOOTX64.EFI"): file = source / relative file.parent.mkdir(parents=True, exist_ok=True) diff --git a/tools/test_documentation.py b/tools/test_documentation.py new file mode 100644 index 00000000..e6f4eaa1 --- /dev/null +++ b/tools/test_documentation.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Build and check both published manuals without building target products.""" +import argparse +from html.parser import HTMLParser +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +from urllib.parse import unquote, urlsplit + +ROOT = Path(__file__).resolve().parents[1] +MANUALS = { + "reliefos": ROOT / "Documentation", + "reliefnt": ROOT / "kernel/reliefnt/Documentation", +} + + +class Page(HTMLParser): + def __init__(self, source): + super().__init__(convert_charrefs=True) + self.anchors = set() + self.links = [] + self.errors = [] + self.feed(source) + self.close() + + def handle_starttag(self, tag, attrs): + if tag == "script": + self.errors.append("script element") + for name, value in attrs: + if name.startswith("on"): + self.errors.append(f"event handler {name}") + if value is None: + continue + if name == "id" or (tag == "a" and name == "name"): + self.anchors.add(value) + if name in {"href", "src"}: + self.links.append(value) + + handle_startendtag = handle_starttag + + +def check_site(directory): + site = directory.resolve() + errors = [] + for manual, source in MANUALS.items(): + if not (source / "index.rst").is_file(): + errors.append(f"missing documentation source: {source}") + for document in source.rglob("*.rst"): + output = site / manual / document.relative_to(source).with_suffix(".html") + if not output.is_file(): + errors.append(f"missing rendered document: {output.relative_to(site)}") + if not (site / "index.html").is_file(): + errors.append("missing documentation portal index.html") + + pages = {} + for path in sorted(site.rglob("*")): + if not path.is_file(): + continue + if path.suffix in {".js", ".mjs", ".cjs", ".md", ".rst", ".pickle"} or path.name.endswith(".rst.txt"): + errors.append(f"source, doctree or JavaScript leaked: {path.relative_to(site)}") + if path.suffix == ".html": + pages[path] = Page(path.read_text(encoding="utf-8")) + + links = 0 + for path, page in pages.items(): + name = path.relative_to(site) + errors.extend(f"{name}: {error}" for error in page.errors) + for href in page.links: + links += 1 + url = urlsplit(href) + if url.scheme.lower() == "javascript": + errors.append(f"{name}: JavaScript URL {href}") + continue + if url.scheme or url.netloc: + continue + if url.path.startswith("/"): + errors.append(f"{name}: link depends on the website root: {href}") + continue + target = (path.parent / unquote(url.path)).resolve() if url.path else path + if not target.is_relative_to(site): + errors.append(f"{name}: link escapes the documentation portal: {href}") + continue + if target.is_dir(): + target /= "index.html" + if not target.is_file(): + errors.append(f"{name}: missing local target: {href}") + elif url.fragment and target in pages and unquote(url.fragment) not in pages[target].anchors: + errors.append(f"{name}: missing HTML anchor: {href}") + + if errors: + raise RuntimeError("\n".join(errors)) + print(f"documentation: {len(pages)} HTML pages, {links} links/assets checked; " + "both manuals complete, no JavaScript or source leaks") + + +def build_and_check(): + sphinx = shutil.which(os.environ.get("SPHINXBUILD", "sphinx-build")) + if sphinx is None: + raise RuntimeError("Sphinx is required; install Documentation/requirements.txt") + temporary = ROOT / "out/tmp" + temporary.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="reliefos-docs-", dir=temporary) as work: + work = Path(work) + env = dict(os.environ, TMPDIR=str(work), SPHINXBUILD=sphinx) + for manual, source in MANUALS.items(): + subprocess.run(["make", "-C", str(source.parent), f"O={work / manual}", + f"SPHINXBUILD={sphinx}", "htmldocs"], check=True, env=env) + subprocess.run(["sh", str(ROOT / "tools/build/docs-page.sh"), + str(MANUALS["reliefos"]), str(MANUALS["reliefnt"]), + str(work / "portal")], check=True, env=env) + check_site(work / "portal") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--site", type=Path, help="check an existing documentation portal") + args = parser.parse_args() + try: + if args.site is None: + build_and_check() + else: + check_site(args.site) + except (RuntimeError, OSError, subprocess.CalledProcessError) as error: + parser.exit(1, f"documentation: {error}\n") + + +if __name__ == "__main__": + main() diff --git a/tools/test_dual_boot_qemu.py b/tools/test_dual_boot_qemu.py index fac96b38..73a31ae6 100644 --- a/tools/test_dual_boot_qemu.py +++ b/tools/test_dual_boot_qemu.py @@ -235,14 +235,17 @@ def inspect_install(disk, output, bios, uefi): """Check actual GPT and loader records after the installing VM has exited.""" table = json.loads(subprocess.check_output(["sfdisk", "--json", disk]))["partitiontable"] parts = table["partitions"] - assert len(parts) == 3 and parts[0]["start"] == 2048 and parts[0]["size"] == 2048 - assert parts[0]["type"].lower() == "21686148-6449-6e6f-744e-656564454649" + assert len(parts) == (3 if bios else 2) + if bios: + assert parts[0]["start"] == 2048 and parts[0]["size"] == 2048 + assert parts[0]["type"].lower() == "21686148-6449-6e6f-744e-656564454649" esp = next(p for p in parts if p["type"].lower() == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b") root = next(p for p in parts if p["type"].lower() == "0fc63daf-8483-4772-8e79-3d69d8477de4") source = f"{disk}@@{esp['start'] * 512}" - record = subprocess.check_output(["mtype", "-i", source, "::/reliefos/config/bootloaders.conf"], text=True) + instance = f"::/reliefos/instances/{root['uuid'].lower()}" + record = subprocess.check_output(["mtype", "-i", source, instance + "/reliefos/config/bootloaders.conf"], text=True) assert record == f"version=1\nbios={int(bios)}\nuefi={int(uefi)}\n", record - binding = subprocess.check_output(["mtype", "-i", source, "::/reliefos/config/root.cfg"], text=True) + binding = subprocess.check_output(["mtype", "-i", source, instance + "/reliefos/config/root.cfg"], text=True) assert binding.strip() == f"set reliefos_root=PARTUUID={root['uuid'].lower()}" efi = subprocess.run(["mdir", "-i", source, "::/EFI/BOOT/BOOTX64.EFI"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 diff --git a/tools/test_installer_accounts_qemu.py b/tools/test_installer_accounts_qemu.py index 394bf759..74988e42 100644 --- a/tools/test_installer_accounts_qemu.py +++ b/tools/test_installer_accounts_qemu.py @@ -431,7 +431,7 @@ def install_gui(probe, serial, process, *, skip_xorg=False, chinese=False, def install_tty(probe, serial, process, components="none", *, skip_xorg=False, install_bios=True, install_uefi=True, select_entry=True, install_timeout=2400, - swap_mib=None): + swap_mib=None, partition_commands=None): """Answer the real TTY installer, including both independent loader choices.""" if select_entry: from test_vt_qemu import ocr @@ -457,7 +457,7 @@ def install_tty(probe, serial, process, components="none", *, skip_xorg=False, ("Select disk number (r to refresh, q to quit): ", "0"), ("Install or update BIOS bootloader? [Y/n]: ", "" if install_bios else "n"), ("Install or update UEFI bootloader? [Y/n]: ", "" if install_uefi else "n"), - ("partition> ", "auto"), ("partition> ", "done"), + *[("partition> ", command) for command in (partition_commands or ("auto", "done"))], ("Username: ", "alice"), ("Password: ", USER_PASSWORD), ("Confirm password: ", USER_PASSWORD), ("root password: ", ROOT_PASSWORD), ("Confirm root password: ", ROOT_PASSWORD), @@ -477,7 +477,7 @@ def install_tty(probe, serial, process, components="none", *, skip_xorg=False, def login_tty(probe, serial, process, root, *, skip_xorg=False): # Console-profile installs expose tty1 directly. Xorg-profile installs # keep a text getty on tty2, even while a graphical greeter owns tty1. - from test_vt_qemu import wait_text + from test_vt_qemu import wait_text, ocr username = "root" if root else "alice" if skip_xorg: wait_text(probe, process, "console-tty1-login", "login:", timeout=90) @@ -485,6 +485,18 @@ def login_tty(probe, serial, process, root, *, skip_xorg=False): # Xorg takes tty1 after init completes. Switch only after that handoff # so its startup cannot steal the test's text login back from tty2. wait_log(serial, "VT graphics vt=1 enabled=1", process, timeout=120) + deadline = time.monotonic() + 120 + while time.monotonic() < deadline: + assert process.poll() is None, "QEMU exited before XDM was ready" + frame = probe.frame("xdm-ready") + # The large X logo dominates whole-screen text segmentation. + greeter = frame.crop((max(0, frame.width // 2 - 320), frame.height // 4, + min(frame.width, frame.width // 2 + 180), frame.height * 3 // 4)) + if "Login into ReliefOS" in ocr(greeter): + break + time.sleep(.5) + else: + raise AssertionError("XDM login fields did not become ready") probe.key("ctrl-alt-f2") wait_text(probe, process, "tty-login", "login:", timeout=90) probe.text(username) @@ -552,6 +564,35 @@ def login_tty(probe, serial, process, root, *, skip_xorg=False): probe.frame(("console-tty1" if skip_xorg else "tty2") + ("-root-session" if root else "-user-session")) +def shutdown_after_login(probe, process, *, root): + """Stop services cleanly so the next boot does not inherit Xorg's live lock.""" + from test_vt_qemu import wait_text + if not root: + probe.text("exit") + probe.key("ret") + wait_text(probe, process, "shutdown-login", "login:") + probe.text("root") + probe.key("ret") + wait_text(probe, process, "shutdown-password", "Password:") + probe.text(ROOT_PASSWORD) + probe.key("ret") + probe.text("echo SHUTDOWN_AUTHENTICATED") + probe.key("ret") + wait_text(probe, process, "shutdown-root", "SHUTDOWN_AUTHENTICATED") + probe.text("/sbin/poweroff") + probe.key("ret") + deadline = time.monotonic() + 60 + while time.monotonic() < deadline: + status = process.poll() + if status is not None: + assert status == 0, f"QEMU exited with status {status} during shutdown" + return + if probe.command("query-status")["status"] == "shutdown": + return + time.sleep(.2) + raise AssertionError("The installed system did not finish its normal shutdown") + + def login_desktop(probe, serial, process): wait_log(serial, "path=/usr/lib/reliefos/apps/login/login.elf", process) time.sleep(5) @@ -582,7 +623,7 @@ def login_desktop(probe, serial, process): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--iso", type=Path, default=ROOT / "build/images/reliefos-installer.iso") + parser.add_argument("--iso", type=Path, default=ROOT / "out/x86_64/release/images/reliefos-installer.iso") parser.add_argument("--output", type=Path, required=True) parser.add_argument("--installer", choices=("gui", "tty"), default="gui") parser.add_argument("--without-xorg", action="store_true", @@ -615,8 +656,10 @@ def main(): install_tty(*session, skip_xorg=args.without_xorg, swap_mib=args.swap_mib) with boot(output / "root", disk) as session: login_tty(*session, True, skip_xorg=args.without_xorg) + shutdown_after_login(session[0], session[2], root=True) with boot(output / "user", disk) as session: login_tty(*session, False, skip_xorg=args.without_xorg) + shutdown_after_login(session[0], session[2], root=False) if args.desktop: with boot(output / "desktop", disk) as session: login_desktop(*session) diff --git a/tools/test_installer_bundle_swap.py b/tools/test_installer_bundle_swap.py index b33df3fe..a99a261f 100644 --- a/tools/test_installer_bundle_swap.py +++ b/tools/test_installer_bundle_swap.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Verify swap capability from complete checksum-bound target bundles.""" +from runtime_test_dependencies import crypto_flags import argparse import hashlib import os @@ -46,21 +47,20 @@ def main(): args=parser.parse_args() if args.output:args.output.mkdir(parents=True,exist_ok=False) else:args.output=Path(tempfile.mkdtemp(prefix="installer-bundle-swap-",dir=os.environ.get("TMPDIR"))) - args.output=args.output.resolve();(args.output/"qa_mbedtls.h").write_text("#define MBEDTLS_SHA256_C\n") + args.output=args.output.resolve() cc=shutil.which("clang") if not cc:raise RuntimeError("clang is required") binary=str(args.output/"bundle") sources=["tools/tests/installer_bundle_swap_test.c","userland/runtime/src/kernel_bundle_check.c", "userland/runtime/src/kernel_bundle_io.c","userland/runtime/src/bootloader_publish.c", - "userland/runtime/shared/boot_slots.c","third_party/mbedtls/library/sha256.c", - "third_party/mbedtls/library/platform_util.c"] + "userland/runtime/shared/boot_slots.c"] if (ROOT/"userland/apps/installer/installer_swap_bundle.c").exists(): sources.append("userland/apps/installer/installer_swap_bundle.c") command=[cc,"-std=c11","-D_GNU_SOURCE","-O1","-g","-Wall","-Wextra","-Werror", "-fsanitize=address,undefined","-fno-sanitize-recover=all","-fno-pie","-no-pie", - "-ffunction-sections","-fdata-sections","-Wl,--gc-sections",'-DMBEDTLS_CONFIG_FILE="qa_mbedtls.h"', - "-I",str(args.output),"-Iuserland/runtime/include","-Ithird_party/mbedtls/include", - *sources,"-o",binary] + "-ffunction-sections","-fdata-sections","-Wl,--gc-sections", + "-I",str(args.output),"-Iuserland/runtime/include", + *sources,*crypto_flags(),"-o",binary] result=run(command) if result:return result failures=0 diff --git a/tools/test_installer_mode_switch_qemu.py b/tools/test_installer_mode_switch_qemu.py index 54397c8e..ba6b4b4f 100644 --- a/tools/test_installer_mode_switch_qemu.py +++ b/tools/test_installer_mode_switch_qemu.py @@ -5,7 +5,6 @@ import subprocess import time -from PIL import ImageChops from test_installer_accounts_qemu import boot, next_page, previous_page ROOT = Path(__file__).resolve().parents[1] @@ -16,7 +15,12 @@ def page_text(probe, name): if frame.width < 640 or frame.height < 480: return "" crop = probe.output / "content.png" - frame.crop((240, 50, min(frame.width, 1250), 320)).resize((2020, 540)).save(crop) + if probe._detect_installer_sidebar(frame) is None: + return "" + content = frame.crop((max(0, probe.content_left - 8), probe.sidebar_top + 8, + min(frame.width, probe.content_left + 790), + min(frame.height, probe.sidebar_top + 600))) + content.resize((content.width * 2, content.height * 2)).save(crop) return subprocess.check_output(["tesseract", str(crop), "stdout", "--psm", "6"], stderr=subprocess.DEVNULL, text=True) @@ -40,7 +44,7 @@ def wait_page(probe, process, text, timeout=90): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--disk", type=Path, required=True) - parser.add_argument("--iso", type=Path, default=ROOT / "build/images/reliefos-installer.iso") + parser.add_argument("--iso", type=Path, default=ROOT / "out/x86_64/release/images/reliefos-installer.iso") parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() output = args.output.resolve() @@ -62,20 +66,10 @@ def main(): probe.click(*probe.mode_option_point(update=True)) next_page(probe) next_page(probe) - wait_page(probe, process, "Choose bootloaders") - next_page(probe) - wait_page(probe, process, "Confirm Update") - before = probe.frame("update-confirm-empty") - # The confirmation field must be focused on entry without a click. - assert "Type UPDATE" in page_text(probe, "update-confirm-page") - probe.text("UPDATE") - after = probe.frame("update-confirm-typed") - button = probe.footer_button_box() - if ImageChops.difference(before.crop(button), after.crop(button)).getbbox() is None: - failures.append("Typing UPDATE did not enable the update button") - # Leave the enabled Update confirmation unsubmitted, return to Mode, - # switch to a fresh install, and carry the same disk through that path. - previous_page(probe, 5) + wait_page(probe, process, "Review system update", timeout=600) + probe.frame("update-review") + # Leave the inspected update unsubmitted, return to Mode, and carry + # the same disk through fresh installation with independent defaults. previous_page(probe, 4) previous_page(probe, 3) probe.click(*probe.mode_option_point(update=False)) @@ -98,7 +92,7 @@ def main(): failures.append(str(error)) if failures: raise AssertionError("; ".join(failures)) - print(f"PASS UPDATE input and update-to-fresh installation; {serial}", flush=True) + print(f"PASS inspected update-to-fresh installation; {serial}", flush=True) if __name__ == "__main__": diff --git a/tools/test_installer_payload.py b/tools/test_installer_payload.py index 1f86bc06..4d82e93b 100644 --- a/tools/test_installer_payload.py +++ b/tools/test_installer_payload.py @@ -16,12 +16,19 @@ def setUpClass(cls): cls.work = tempfile.TemporaryDirectory( prefix="reliefos-installer-payload-", dir=os.environ["TMPDIR"]) cls.executable = Path(cls.work.name) / "payload-check" + model = Path(cls.work.name) / "update-model.o" + subprocess.run([ + "cc", "-std=gnu11", "-O1", "-g", "-fsanitize=address,undefined", + "-ffunction-sections", "-fdata-sections", "-c", + "userland/apps/installer/installer_update_model.c", "-o", model, + ], cwd=ROOT, check=True) subprocess.run([ "cc", "-std=gnu11", "-O1", "-g", "-fsanitize=address,undefined", "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", "-idirafter", "include", "-Ikernel/reliefnt/include", "-Ikernel/reliefnt/include/uapi", "-idirafter", "userland/runtime/include", - "tools/tests/installer_payload_test.c", "-o", str(cls.executable), + "tools/tests/installer_payload_test.c", model, + "-o", str(cls.executable), ], cwd=ROOT, check=True) @classmethod @@ -38,6 +45,7 @@ def setUp(self): "usr/share/doc/reliefos", "usr/share/fonts/reliefos", "usr/share/reliefos/resources", "usr/share/licenses", "etc/ssl/certs", + "bin", "sbin", "usr/bin", "boot", ): (self.root / "install/root" / name).mkdir(parents=True, exist_ok=True) for name in ( @@ -58,11 +66,12 @@ def validate(self): def target_fixture(self, namespace): root = self.root / "target" - for name in ("bin", "sbin", "lib", "usr/bin", "usr/sbin", "usr/lib", + for name in ("bin", "sbin", "lib/apk/db", "usr/bin", "usr/sbin", "usr/lib", "usr/share", "etc/apk", f"etc/{namespace}", f"var/lib/{namespace}", f"usr/lib/{namespace}/apps", "boot/EFI"): (root / name).mkdir(parents=True, exist_ok=True) library = "libreliefos.so.2" if namespace == "reliefos" else "libleonos.so.2" + (root / "etc/os-release").write_text("ID=" + namespace + "\nVERSION_ID=5.3.0\n") for name in (f"usr/lib/{namespace}/{library}", f"boot/{namespace}/kernel.sys"): path = root / name path.parent.mkdir(parents=True, exist_ok=True) @@ -84,6 +93,7 @@ def install_profile(self, console=False): file.parent.mkdir(parents=True, exist_ok=True) file.write_text("signed package fixture\n") (root / "etc/reliefos/desktop-backend").write_text("console\n" if console else "xorg\n") + (root / "etc/os-release").write_text("ID=reliefos\nVERSION_ID=5.3.1\n") return root def validate_profile(self, mode): diff --git a/tools/test_installer_responsiveness.py b/tools/test_installer_responsiveness.py index c746afe6..3b9e7b62 100644 --- a/tools/test_installer_responsiveness.py +++ b/tools/test_installer_responsiveness.py @@ -1,7 +1,7 @@ """Check a running installer's redraw latency through an isolated QEMU QMP socket. Start the installer at its language page with no writable disk attached. This -test visits only Language, Thanks, Welcome, and Mode; it never starts a copy. +test visits only Language, License, Welcome, and Mode; it never starts a copy. Requires Pillow. Screenshots and measured response times are saved in --output. """ diff --git a/tools/test_installer_setup.py b/tools/test_installer_setup.py index dd536cbe..c2529926 100644 --- a/tools/test_installer_setup.py +++ b/tools/test_installer_setup.py @@ -8,8 +8,8 @@ import unittest ROOT = Path(__file__).resolve().parents[1] -CRYPTO = [f"third_party/mbedtls/library/{name}.c" for name in - ("md", "pkcs5", "sha1", "sha256", "sha512", "platform_util", "aes")] +from runtime_test_dependencies import crypto_flags +CRYPTO = crypto_flags() class InstallerSetupTests(unittest.TestCase): @@ -145,8 +145,7 @@ def test_legacy_accounts(self): "cc", "-std=c11", "-D_GNU_SOURCE", "-O1", "-g", "-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", - '-DMBEDTLS_CONFIG_FILE="reliefos_mbedtls_config.h"', - "-Ikernel/reliefnt/include", "-Iinclude", "-Ikernel/reliefnt/include/uapi", "-Ithird_party/mbedtls/include", + "-Ikernel/reliefnt/include", "-Iinclude", "-Ikernel/reliefnt/include/uapi", "-idirafter", "userland/runtime/include", "tools/tests/installer_accounts_test.c", "-Itools/tests/legacy_authd/include", "tools/tests/legacy_authd/accounts.c", "userland/runtime/src/auth_password.c", diff --git a/tools/test_installer_update_packages.py b/tools/test_installer_update_packages.py new file mode 100644 index 00000000..767e49e0 --- /dev/null +++ b/tools/test_installer_update_packages.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +"""Exercise offline installer preflight with signed APKs and a read-only target.""" +import hashlib +import functools +import http.server +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import threading +import unittest + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = Path(os.environ.get("RELIEFOS_TEST_OUTPUT", ROOT / "out/x86_64/release")) +APK = OUTPUT / "upstream/apk/apk.static" +BUSYBOX = OUTPUT / "userland/initramfs/busybox.elf" +UPDATER = ROOT / "userland/storage/reliefos-apk-update" + + +class UpdatePreflight(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.temporary = tempfile.TemporaryDirectory(prefix="installer-update-packages-") + cls.addClassCleanup(cls.temporary.cleanup) + cls.work = Path(cls.temporary.name) + cls.key = cls.work / "test.rsa" + subprocess.run(["openssl", "genpkey", "-algorithm", "RSA", "-pkeyopt", + "rsa_keygen_bits:2048", "-out", cls.key], + check=True, capture_output=True) + cls.key.chmod(0o600) + cls.base = cls.work / "base" + for name in ("bin", "usr/sbin", "usr/lib/reliefos", "usr/libexec/reliefos", "sbin", "tmp", "dev", "oldrepo", "newrepo", + "target/etc/apk/keys", "target/lib/apk/db", "target/etc/reliefos", + "target/home/test", "target/usr/share/external", "target/bin", + "target/usr/lib/reliefos", "target/var/lib/reliefos"): + (cls.base / name).mkdir(parents=True, exist_ok=True) + shutil.copy2(BUSYBOX, cls.base / "bin/busybox") + for name in subprocess.check_output([BUSYBOX, "--list"], text=True).splitlines(): + if name != "busybox": + (cls.base / "bin" / name).symlink_to("busybox") + shutil.copy2(APK, cls.base / "usr/sbin/apk") + shutil.copy2(OUTPUT / "userland/initramfs/kernel-bundle.elf", + cls.base / "usr/libexec/reliefos/kernel-bundle") + shutil.copy2(BUSYBOX, cls.base / "target/bin/busybox") + (cls.base / "target/bin/sh").symlink_to("busybox") + shutil.copy2(OUTPUT / "rootfs/managed/bin/bash", cls.base / "target/bin/bash") + (cls.base / "target/var/lib/reliefos/bash-default-v1").write_text("bash\n") + shutil.copy2(ROOT / "system/rootfs/usr/lib/reliefos/reliefos-migrate", + cls.base / "usr/lib/reliefos/reliefos-migrate") + shutil.copy2(ROOT / "system/rootfs/usr/lib/reliefos/migrate-shell", + cls.base / "target/usr/lib/reliefos/migrate-shell") + subprocess.run(["openssl", "pkey", "-in", cls.key, "-pubout", "-out", + cls.base / "target/etc/apk/keys/test.rsa.pub"], + check=True, capture_output=True) + (cls.base / "target/etc/apk/world").write_text("") + (cls.base / "target/etc/apk/arch").write_text("x86_64\n") + cls.package("external-library", "1.0-r0", "oldrepo") + cls.package("external-fixture", "1.0-r0", "oldrepo", ["external-library>=1.0"]) + cls.package("reliefos-fixture", "1.0-r0", "oldrepo") + cls.package("reliefos-fixture", "2.0-r0", "newrepo") + for name in ("oldrepo", "newrepo"): + cls.index(cls.base / name) + configuration = cls.work / "old.repositories" + configuration.write_text("ndx " + str(cls.base / "oldrepo/packages.adb") + "\n") + cls.apk("--root", cls.base / "target", "--repositories-file", configuration, + "--no-network", "--no-scripts", "--initdb", "add", + "reliefos-fixture", "external-fixture") + (cls.base / "target/etc/reliefos/local.conf").write_text("preserve-local=true\n") + (cls.base / "target/home/test/document.txt").write_text("用户的文件\n") + + @classmethod + def apk(cls, *arguments): + return subprocess.run(["unshare", "-Ur", APK, *map(str, arguments)], + check=True, capture_output=True, text=True) + + @classmethod + def package(cls, name, version, repository, dependencies=()): + files = cls.work / (name + "-" + version) + (files / "usr/share" / name).mkdir(parents=True, exist_ok=True) + (files / "usr/share" / name / "version").write_text(version + "\n") + arguments = ["mkpkg", "--files", files, "--output", + cls.base / repository / (name + "-" + version + ".apk"), + "--info", "name:" + name, "--info", "version:" + version, + "--info", "arch:x86_64", "--sign-key", cls.key] + for dependency in dependencies: + arguments.extend(["--info", "depends:" + dependency]) + cls.apk(*arguments) + + @classmethod + def index(cls, repository, signed=True): + args = ["mkndx", "--keys-dir", cls.base / "target/etc/apk/keys", + "--output", repository / "packages.adb"] + if signed: + args.extend(["--sign-key", cls.key]) + else: + args.insert(0, "--allow-untrusted") + cls.apk(*args, *sorted(repository.glob("*.apk"))) + + def setUp(self): + self.case = tempfile.TemporaryDirectory(prefix="case-", dir=self.work) + self.addCleanup(self.case.cleanup) + self.runner = Path(self.case.name) / "runner" + shutil.copytree(self.base, self.runner, symlinks=True) + shutil.copy2(UPDATER, self.runner / "sbin/update") + + def target_snapshot(self): + target = self.runner / "target" + return {str(path.relative_to(target)): hashlib.sha256(path.read_bytes()).hexdigest() + for path in target.rglob("*") if path.is_file() and not path.is_symlink()} + + def transaction(self, mode="--check", expected=None): + readonly = ('mount --bind "$1/target" "$1/target"; ' + 'mount -o remount,bind,ro "$1/target"; ') if mode == "--check" else "" + result = subprocess.run([ + "unshare", "-Urmp", "--fork", "sh", "-eu", "-c", + 'mount --make-rprivate /; mount --bind "$1" "$1"; ' + + readonly + + 'cd "$1"; mkdir -p .oldroot; pivot_root . .oldroot; ' + 'shift; exec /bin/sh /sbin/update "$@"', + "sh", self.runner, mode, "/target", "/newrepo", *([expected] if expected else [])], + text=True, capture_output=True, timeout=60) + return result + + def check(self): + before = self.target_snapshot() + result = self.transaction() + self.assertEqual(self.target_snapshot(), before, result.stdout + result.stderr) + return result + + def test_signed_preflight_succeeds_with_readonly_target(self): + result = self.check() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("reliefos-fixture", result.stdout) + + def test_unsigned_index_is_rejected_before_target_changes(self): + self.index(self.runner / "newrepo", signed=False) + result = self.check() + self.assertNotEqual(result.returncode, 0) + self.assertRegex(result.stdout + result.stderr, "(?i)signature|trust") + + def test_corrupt_archive_is_rejected_before_target_changes(self): + package = self.runner / "newrepo/reliefos-fixture-2.0-r0.apk" + data = bytearray(package.read_bytes()) + data[len(data) // 2] ^= 1 + package.write_bytes(data) + result = self.check() + self.assertNotEqual(result.returncode, 0) + + def test_missing_archive_is_rejected_before_target_changes(self): + (self.runner / "newrepo/reliefos-fixture-2.0-r0.apk").unlink() + result = self.check() + self.assertNotEqual(result.returncode, 0) + + def test_apply_and_retry_preserve_user_files_world_and_repository_access(self): + target = self.runner / "target" + protected = {name: (target / name).read_bytes() for name in ( + "etc/reliefos/local.conf", "home/test/document.txt", + "usr/share/external-fixture/version", "etc/apk/world")} + world_mode = (target / "etc/apk/world").stat().st_mode & 0o777 + for _ in range(2): + result = self.transaction("--apply") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + for name, data in protected.items(): + self.assertEqual((target / name).read_bytes(), data, name) + self.assertEqual((target / "etc/apk/world").stat().st_mode & 0o777, world_mode) + repository = target / "usr/share/reliefos/apk/repository" + self.assertEqual(repository.stat().st_mode & 0o777, 0o755) + self.assertEqual((repository / "packages.adb").stat().st_mode & 0o777, 0o644) + self.assertEqual((target / "usr/share/reliefos-fixture/version").read_text(), "2.0-r0\n") + self.assertFalse((target / "run/reliefos-kernel-activation.defer").exists()) + + def test_changed_index_is_rejected_before_apply(self): + before = self.target_snapshot() + result = self.transaction("--apply", "0" * 64) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(self.target_snapshot(), before) + + def test_retry_repairs_an_incomplete_system_package(self): + result = self.transaction("--apply") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + target = self.runner / "target" + (target / "usr/share/reliefos-fixture/version").unlink() + database = target / "lib/apk/db/installed" + records = database.read_text().split("\n\n") + for index, record in enumerate(records): + if "P:reliefos-fixture\n" in record: + lines = record.splitlines() + flags = next((i for i, line in enumerate(lines) if line.startswith("f:")), None) + if flags is None: + lines.insert(2, "f:f") + else: + lines[flags] += "f" + records[index] = "\n".join(lines) + database.write_text("\n\n".join(records)) + result = self.transaction("--apply") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual((target / "usr/share/reliefos-fixture/version").read_text(), "2.0-r0\n") + + def select_external_tag(self, selection="external-fixture@testing"): + """Record a real APK selection, including its installed repository tag.""" + configuration = Path(self.case.name) / "selection.repositories" + configuration.write_text("ndx @testing " + str(self.runner / "oldrepo/packages.adb") + "\n") + self.apk("--root", self.runner / "target", "--repositories-file", configuration, + "--no-network", "--no-scripts", "add", selection) + + def assert_tagged_update(self): + target = self.runner / "target" + def external_records(): + records = {} + for record in (target / "lib/apk/db/installed").read_text().split("\n\n"): + fields = {line[:1]: line[2:] for line in record.splitlines() + if line.startswith(("P:", "V:", "C:", "s:", "f:", "D:"))} + if fields.get("P", "").startswith("external-"): + records[fields["P"]] = fields + return records + metadata = external_records() + protected = {name: (target / name).read_bytes() for name in ( + "etc/apk/world", "etc/apk/repositories", "etc/reliefos/local.conf", + "usr/share/external-fixture/version", "usr/share/external-library/version", "home/test/document.txt")} + result = self.check() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("opening from cache", result.stdout + result.stderr) + result = self.transaction("--apply") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + for name, contents in protected.items(): + self.assertEqual((target / name).read_bytes(), contents, name) + self.assertEqual(external_records(), metadata) + self.assertEqual((target / "usr/share/reliefos-fixture/version").read_text(), "2.0-r0\n") + + def test_tagged_external_packages_without_index_cache(self): + self.select_external_tag() + target = self.runner / "target" + (target / "etc/apk/repositories").write_text( + "@testing https://mirrors.tuna.tsinghua.edu.cn/alpine/edge/testing\n") + for cache in (target / "var/cache/apk", target / "etc/apk/cache"): + if cache.is_symlink(): + cache.unlink() + elif cache.exists(): + shutil.rmtree(cache) + self.assert_tagged_update() + + def test_retry_after_committed_apk_failure_preserves_tagged_world(self): + self.select_external_tag() + target = self.runner / "target" + (target / "etc/apk/repositories").write_text("@testing https://offline.invalid/testing\n") + world_before = (target / "etc/apk/world").read_bytes() + apk = self.runner / "usr/sbin/apk" + apk.rename(apk.with_name("apk.real")) + apk.write_text('''#!/bin/sh +/usr/sbin/apk.real "$@" +result=$? +[ "$result" -eq 0 ] || exit "$result" +if [ "${1:-}" = --root ] && [ "${2:-}" = /target ]; then + case " $* " in + *' upgrade '*) + if [ ! -e /tmp/failed-after-commit ]; then + touch /tmp/failed-after-commit + echo 'ERROR: injected failure after APK committed' >&2 + exit 42 + fi ;; + esac +fi +exit 0 +''') + apk.chmod(0o755) + failed = self.transaction("--apply") + self.assertNotEqual(failed.returncode, 0) + self.assertIn("injected failure after APK committed", failed.stdout + failed.stderr) + self.assertEqual((target / "usr/share/reliefos-fixture/version").read_text(), "2.0-r0\n") + self.assertEqual((target / "etc/apk/world").read_bytes(), world_before) + self.assertFalse((target / "run/reliefos-kernel-activation.defer").exists()) + self.assert_tagged_update() + + def test_tagged_external_packages_with_deleted_repository_configuration(self): + self.select_external_tag() + (self.runner / "target/etc/apk/repositories").write_text("# removed remote repository\n") + self.assert_tagged_update() + + def test_multiple_tags_and_pinned_external_version(self): + self.select_external_tag("external-fixture@testing=1.0-r0") + target = self.runner / "target" + world = target / "etc/apk/world" + world.write_text(world.read_text().replace("reliefos-fixture\n", "reliefos-fixture@alpine\n")) + (target / "etc/apk/repositories").write_text( + "@testing https://offline.invalid/testing\n@alpine https://offline.invalid/alpine\n") + self.assert_tagged_update() + + def test_corrupt_external_cache_is_not_used_by_offline_update(self): + self.select_external_tag() + target = self.runner / "target" + cache = target / "var/cache/apk" + cache.mkdir(parents=True, exist_ok=True) + (cache / "APKINDEX.deadbeef.tar.gz").write_bytes(b"corrupt external index") + (target / "etc/apk/cache").symlink_to("/var/cache/apk") + (target / "etc/apk/repositories").write_text("@testing https://offline.invalid/testing\n") + self.assert_tagged_update() + + def test_missing_tagged_package_is_not_silently_ignored(self): + self.select_external_tag() + world = self.runner / "target/etc/apk/world" + world.write_text(world.read_text() + "missing-extra@testing\n") + result = self.check() + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing-extra", result.stdout + result.stderr) + + def test_unsatisfied_tagged_version_is_rejected_before_apply(self): + self.select_external_tag() + target = self.runner / "target" + world = target / "etc/apk/world" + world.write_text(world.read_text().replace("external-fixture@testing", "external-fixture@testing>=9.0-r0")) + before = self.target_snapshot() + for mode in ("--check", "--apply"): + with self.subTest(mode=mode): + result = self.transaction(mode) + self.assertNotEqual(result.returncode, 0) + self.assertIn("external-fixture", result.stdout + result.stderr) + self.assertEqual(self.target_snapshot(), before) + + def test_unavailable_new_dependency_is_rejected_before_apply(self): + self.package("reliefos-fixture", "2.0-r0", self.runner / "newrepo", ["missing-library>=2.0"]) + self.index(self.runner / "newrepo") + before = self.target_snapshot() + result = self.transaction("--apply") + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing-library", result.stdout + result.stderr) + self.assertEqual(self.target_snapshot(), before) + + def test_media_external_upgrade_is_not_selected_unnecessarily(self): + self.select_external_tag() + (self.runner / "target/etc/apk/repositories").write_text("@testing https://offline.invalid/testing\n") + self.package("external-fixture", "3.0-r0", self.runner / "newrepo", ["external-library>=1.0"]) + self.index(self.runner / "newrepo") + self.assert_tagged_update() + + def test_media_dependency_can_be_installed_without_external_index(self): + self.select_external_tag() + target = self.runner / "target" + (target / "etc/apk/repositories").write_text("@testing https://offline.invalid/testing\n") + self.package("media-library", "1.0-r0", self.runner / "newrepo") + self.package("reliefos-fixture", "2.0-r0", self.runner / "newrepo", ["media-library>=1.0"]) + self.index(self.runner / "newrepo") + self.assert_tagged_update() + self.assertEqual((target / "usr/share/media-library/version").read_text(), "1.0-r0\n") + + def test_external_package_conflict_is_rejected_before_apply(self): + self.select_external_tag() + self.package("reliefos-fixture", "2.0-r0", self.runner / "newrepo", ["!external-fixture"]) + self.index(self.runner / "newrepo") + before = self.target_snapshot() + result = self.transaction("--apply") + self.assertNotEqual(result.returncode, 0) + self.assertIn("external-fixture", result.stdout + result.stderr) + self.assertEqual(self.target_snapshot(), before) + + def test_tagged_external_packages_with_cached_indexes_and_repository_snippets(self): + target = self.runner / "target" + cache = target / "var/cache/apk" + cache.mkdir(parents=True, exist_ok=True) + handler = functools.partial(http.server.SimpleHTTPRequestHandler, + directory=str(self.runner / "oldrepo")) + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + url = f"http://127.0.0.1:{server.server_port}/packages.adb" + configuration = Path(self.case.name) / "repositories" + configuration.write_text("ndx @testing " + url + "\n") + try: + self.apk("--root", target, "--repositories-file", configuration, + "--cache-dir", cache, "update") + finally: + server.shutdown() + server.server_close() + thread.join() + world = target / "etc/apk/world" + world.write_text(world.read_text().replace("external-fixture\n", "external-fixture@testing\n")) + locations = [target / name for name in ( + "etc/apk/repositories", "etc/apk/repositories.d/testing.list", + "lib/apk/repositories.d/testing.list")] + for location in locations: + location.parent.mkdir(parents=True, exist_ok=True) + for prefix in ("ndx @testing", "@testing"): + with self.subTest(repository_syntax=prefix, configuration=location.relative_to(target)): + location.write_text(prefix + " " + url + "\n") + result = self.check() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + location.unlink() + # The commit uses the same generated repository file as preflight. + (target / "etc/apk/repositories").write_text("@testing " + url + "\n") + preserved_world = world.read_bytes() + external_version = (target / "usr/share/external-fixture/version").read_bytes() + result = self.transaction("--apply") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(world.read_bytes(), preserved_world) + self.assertEqual((target / "usr/share/external-fixture/version").read_bytes(), external_version) + self.assertEqual((target / "usr/share/reliefos-fixture/version").read_text(), "2.0-r0\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/test_installer_update_qemu.py b/tools/test_installer_update_qemu.py index 77df23cb..0f474cb0 100644 --- a/tools/test_installer_update_qemu.py +++ b/tools/test_installer_update_qemu.py @@ -6,8 +6,9 @@ import re import json import time +import hashlib -from test_installer_accounts_qemu import boot, next_page, wait_log +from test_installer_accounts_qemu import boot, next_page, wait_log, installer_finish_log from test_installer_mode_switch_qemu import wait_page ROOT = Path(__file__).resolve().parents[1] @@ -27,6 +28,16 @@ def installed_state(disk, directory): stderr=subprocess.DEVNULL, text=True) assert value, f"Missing {path}" result[name] = value + result["checkpoint"] = subprocess.check_output([ + "debugfs", "-R", "cat /var/lib/reliefos/installer-update/state", str(image)], + stderr=subprocess.DEVNULL, text=True) + result["preserved"] = {} + for path in ("/etc/passwd", "/etc/shadow", "/etc/group", "/etc/gshadow", "/etc/fstab", + "/etc/hostname", "/home/alice/installer-user-check", "/root/installer-root-check"): + content = subprocess.check_output(["debugfs", "-R", f"cat {path}", str(image)], + stderr=subprocess.DEVNULL) + if content: + result["preserved"][path] = hashlib.sha256(content).hexdigest() image.unlink() return result @@ -56,9 +67,9 @@ def main(): if disk.exists(): parser.error("output scratch.raw already exists; use a new output directory") subprocess.run(["cp", "--reflink=auto", "--sparse=always", args.disk.resolve(), disk], check=True) - before = installed_state(disk, output) if args.preserve_package else None - if before: - versions = package_versions(before["installed"]) + before = installed_state(disk, output) + versions = package_versions(before["installed"]) + if args.preserve_package: assert all(name in versions for name in args.preserve_package), "Fixture lacks required packages" with boot(output / "update", disk, args.iso.resolve()) as (probe, serial, process): wait_log(serial, "path=/usr/lib/reliefos/apps/installer/installer.elf", process) @@ -72,41 +83,33 @@ def main(): next_page(probe) probe.click(*probe.mode_option_point(update=True)) next_page(probe) - next_page(probe) - wait_page(probe, process, "Choose bootloaders") - next_page(probe) - wait_page(probe, process, "Confirm Update") - probe.click(*probe.confirm_field_point()) - probe.text("UPDATE") - probe.frame("confirmation") - next_page(probe) try: + next_page(probe) + wait_page(probe, process, "Review system update", args.timeout) + probe.frame("confirmation") + next_page(probe) wait_page(probe, process, "was updated on the selected disk", args.timeout) probe.frame("updated") finally: - log = serial.read_text(errors="replace") - match = re.search(r"installer root ready ramdisk=(0x[0-9a-f]+).*bytes=(\d+)", log) - if match: - memory = output / "installer-root.ext2" - probe.command("stop") - probe.command("pmemsave", {"val": int(match[1], 16), "size": int(match[2]), - "filename": str(memory)}) - subprocess.run(["debugfs", "-R", f"dump /var/log/installer-session.log {output / 'installer.log'}", - str(memory)], check=True) + (output / "installer.log").write_text(installer_finish_log(probe, serial, process)) log = (output / "installer.log").read_text() - assert "APK update wait_status=0" in log and "update completed successfully" in log - if before: - after = installed_state(disk, output) - updated = package_versions(after["installed"]) - expected_world = re.sub(r"(?m)^leonos-", "reliefos-", before["world"]) - # apk serializes world in lexical order after renaming dependencies. - assert sorted(expected_world.splitlines()) == sorted(after["world"].splitlines()), \ - "Update changed world dependencies beyond the package rename" + assert "update completed successfully" in log + after = installed_state(disk, output) + assert "\nphase=done\n" in after["checkpoint"], "The native update transaction did not finish" + for path, digest in before["preserved"].items(): + assert after["preserved"].get(path) == digest, f"Update changed preserved file {path}" + print("PASS preserved accounts, filesystem configuration and existing test user files") + updated = package_versions(after["installed"]) + expected_world = re.sub(r"(?m)^leonos-", "reliefos-", before["world"]) + # apk serializes world in lexical order after renaming dependencies. + assert sorted(expected_world.splitlines()) == sorted(after["world"].splitlines()), \ + "Update changed world dependencies beyond the package rename" + assert any(name.startswith(("leonos-", "reliefos-")) and + updated.get(re.sub(r"^leonos-", "reliefos-", name)) not in (None, version) + for name, version in versions.items()), "No ReliefOS package was upgraded" + if args.preserve_package: for name in args.preserve_package: assert versions[name] == updated.get(name), f"Update changed {name}" - assert any(name.startswith(("leonos-", "reliefos-")) and - updated.get(re.sub(r"^leonos-", "reliefos-", name)) not in (None, version) - for name, version in versions.items()), "No ReliefOS package was upgraded" print("PASS preserved world and packages: " + ", ".join(args.preserve_package)) print(f"PASS GUI update: confirmation, package transaction and completion; {serial}") diff --git a/tools/test_kernel_bundle.py b/tools/test_kernel_bundle.py index 4bb8109d..4083bb88 100644 --- a/tools/test_kernel_bundle.py +++ b/tools/test_kernel_bundle.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Run the actual runtime bundle validator against native production payloads.""" +from runtime_test_dependencies import crypto_flags import hashlib import json import os @@ -107,7 +108,6 @@ def setUpClass(cls): cls.build = tempfile.TemporaryDirectory(prefix="reliefos-runtime-kernel-bundle-") cls.addClassCleanup(cls.build.cleanup) work = Path(cls.build.name) - (work / "qa_mbedtls.h").write_text("#define MBEDTLS_SHA256_C\n") cls.binary = work / "bundle" sources = [ROOT / "userland/runtime/src" / (name + ".c") for name in ("kernel_bundle_io", "kernel_bundle_check", "kernel_bundle_slots", "kernel_bundle_publish", "kernel_bundle_sync", "kernel_bundle_retain", "bootloader_publish")] @@ -115,11 +115,10 @@ def setUpClass(cls): subprocess.run(["cc", "-std=gnu11", "-D_GNU_SOURCE", "-O1", "-g", "-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-Wl,--wrap=write,--wrap=pwrite,--wrap=fsync,--wrap=renameat", - '-DMBEDTLS_CONFIG_FILE="qa_mbedtls.h"', "-I", str(work), "-I", str(ROOT), - "-I", str(ROOT / "userland/runtime/include"), "-I", str(ROOT / "third_party/mbedtls/include"), + "-I", str(work), "-I", str(ROOT), + "-I", str(ROOT / "userland/runtime/include"), str(ROOT / "tests/host/test_kernel_bundle.c"), *map(str, sources), - str(ROOT / "third_party/mbedtls/library/sha256.c"), - str(ROOT / "third_party/mbedtls/library/platform_util.c"), "-o", str(cls.binary)], check=True) + *crypto_flags(), "-o", str(cls.binary)], check=True) def setUp(self): fixture = fixtures.ModulesStage("test_real_metadata_dependency_closure_and_stale_output") diff --git a/tools/test_multisystem_boot_qemu.py b/tools/test_multisystem_boot_qemu.py new file mode 100644 index 00000000..d1fd3ded --- /dev/null +++ b/tools/test_multisystem_boot_qemu.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +"""Install two systems onto one shared ESP and boot their actual native payloads.""" +import argparse +import hashlib +import json +from pathlib import Path +import shutil +import subprocess +import time +import uuid + +from test_dual_boot_qemu import boot, boot_evidence, wait_for, wait_xdm +from test_installer_accounts_qemu import install_tty +from test_vt_qemu import ocr, wait_text + +ROOT = Path(__file__).resolve().parents[1] +FOREIGN = b"foreign EFI fallback -- must survive both installations\n" +WINDOWS = b"foreign Microsoft EFI payload -- must survive\n" + + +def run(*args, **kwargs): + return subprocess.run(args, check=True, **kwargs) + + +def esp_bytes(disk, partition, name): + return subprocess.check_output(["mtype", "-i", f"{disk}@@{partition['start'] * 512}", "::/" + name]) + + +def compose_disk(output): + disk = output / "two-installations.raw" + with disk.open("xb") as stream: + stream.truncate(7 * 1024**3) + ids = {name: str(uuid.uuid4()) for name in ("esp", "first", "second")} + run("sfdisk", str(disk), input=( + "label: gpt\nunit: sectors\n\n" + "start=2048,size=2048,type=21686148-6449-6E6F-744E-656564454649,name=BIOS\n" + f"start=4096,size=1048576,type=U,uuid={ids['esp']},name=SharedESP\n" + f"size=5242880,type=L,uuid={ids['first']},name=FirstReliefOS\n" + f"size=5242880,type=L,uuid={ids['second']},name=SecondReliefOS\n" + ), text=True, stdout=subprocess.DEVNULL) + table = json.loads(subprocess.check_output(["sfdisk", "--json", disk]))["partitiontable"] + esp = table["partitions"][1] + fat = output / "shared-esp.fat" + with fat.open("xb") as stream: + stream.truncate(esp["size"] * 512) + run("mkfs.fat", "--invariant", "-F", "32", str(fat), stdout=subprocess.DEVNULL) + tree = output / "foreign" + (tree / "EFI/BOOT").mkdir(parents=True) + (tree / "EFI/Microsoft/Boot").mkdir(parents=True) + (tree / "EFI/BOOT/BOOTX64.EFI").write_bytes(FOREIGN) + (tree / "EFI/Microsoft/Boot/bootmgfw.efi").write_bytes(WINDOWS) + run("mcopy", "-s", "-i", str(fat), str(tree / "EFI"), "::/") + run("dd", f"if={fat}", f"of={disk}", "bs=512", f"seek={esp['start']}", "conv=notrunc,sparse", "status=none") + fat.unlink() + return disk, esp, ids + + +def assert_foreign(disk, esp): + assert esp_bytes(disk, esp, "EFI/BOOT/BOOTX64.EFI") == FOREIGN + assert esp_bytes(disk, esp, "EFI/Microsoft/Boot/bootmgfw.efi") == WINDOWS + + +def efi_chooser(output, disk, esp, identity): + """Use a separate boot manager to select a named EFI file without touching its disk.""" + tree = output / "chooser" + (tree / "EFI/BOOT").mkdir(parents=True) + loader = f"EFI/ReliefOS/{identity}/grubx64.efi" + (tree / "EFI/BOOT/BOOTX64.EFI").write_bytes(esp_bytes(disk, esp, loader)) + digest = hashlib.sha256((tree / "EFI/BOOT/BOOTX64.EFI").read_bytes()).hexdigest() + (tree / "EFI/BOOT/reliefos-owner").write_text(f"ReliefOS fallback v1\n{identity}\n{digest}\n") + (tree / "EFI/BOOT/grub.cfg").write_text( + f"insmod chain\nsearch --no-floppy --file /{loader} --set=root\n" + f"chainloader ($root)/{loader}\nboot\n") + image = output / "efi-selector.raw" + with image.open("xb") as stream: + stream.truncate(64 * 1024**2) + run("mkfs.fat", "--invariant", "-F", "32", str(image), stdout=subprocess.DEVNULL) + run("mcopy", "-s", "-i", str(image), str(tree / "EFI"), "::/") + return image + + +def check_update_selection(probe, serial, process, identities, timeout, after_install=True): + """Exercise the native update worker against both roots and an ambiguous choice.""" + if after_install: + probe.text("n") + probe.key("ret") + time.sleep(1) + probe.text("umount /target/boot; umount /target/efi; umount /target; echo __MULTI_UNMOUNT_$? > /dev/ttyS0") + probe.key("ret") + wait_for(serial, process, lambda text: "__MULTI_UNMOUNT_0" in text, + "release the fresh install mounts before read-only update checks", timeout) + checked = {} + for name in ("first", "second", "ambiguous"): + choice = "" if name == "ambiguous" else f" {identities[name]} {identities['esp']}" + marker = "__MULTI_UPDATE_" + name + "__" + before = serial.stat().st_size + probe.text(f"installer --update-worker check /dev/sda{choice} 2>&1|tee /dev/ttyS0; echo {marker} > /dev/ttyS0") + probe.key("ret") + wait_for(serial, process, lambda text: marker in text, "native update selection " + name, timeout) + observed = serial.read_bytes()[before:].decode(errors="replace") + (probe.output / f"update-{name}.log").write_text(observed) + if name == "ambiguous": + assert "error\t76\t" in observed, observed[-2000:] + else: + assert "error\t" not in observed, observed[-2000:] + assert "info\troot_uuid\t" + identities[name] in observed + assert "info\tesp_uuid\t" + identities["esp"] in observed + checked[name] = dict(line.split("\t", 2)[1:] for line in observed.splitlines() if line.startswith("info\t")) + print(f"PASS native updater selection: {name}", flush=True) + apply_checked_update(probe, serial, process, checked["second"], timeout) + + +def apply_checked_update(probe, serial, process, info, timeout): + """Apply immutable metadata; the native worker rechecks it before writes.""" + arguments = " ".join(info[field] for field in ("root_uuid", "esp_uuid", "index_sha256", "database_sha256", "world_sha256")) + before = serial.stat().st_size + probe.text(f"installer --update-worker apply /dev/sda {arguments} 3 2>&1|tee /dev/ttyS0; echo __MULTI_APPLY__ > /dev/ttyS0") + probe.key("ret") + wait_for(serial, process, lambda text: "__MULTI_APPLY__" in text, "native update of the second installation", timeout) + observed = serial.read_bytes()[before:].decode(errors="replace") + (probe.output / "update-apply-second.log").write_text(observed) + assert "error\t" not in observed and "System update completed successfully" in observed, observed[-4000:] + print("PASS native updater transaction on the second installation", flush=True) + + +def checked_fixture(directory, identities): + """Resume a prepared disk only after all three native check results exist.""" + text = (directory / "update-two-roots/serial.log").read_text(errors="replace") + first, rest = text.split("__MULTI_UPDATE_first__", 1) + second, rest = rest.split("__MULTI_UPDATE_second__", 1) + ambiguous = rest.split("__MULTI_UPDATE_ambiguous__", 1)[0] + assert "error\t" not in first and "info\troot_uuid\t" + identities["first"] in first + assert "error\t" not in second and "error\t76\t" in ambiguous + info = dict(line.split("\t", 2)[1:] for line in second.splitlines() if line.startswith("info\t")) + assert info["root_uuid"] == identities["second"] and info["esp_uuid"] == identities["esp"] + for field in ("index_sha256", "database_sha256", "world_sha256"): + assert len(info[field]) == 64 and all(c in "0123456789abcdef" for c in info[field]) + return info + + +def seed_second_fixture(source, disk, esp, identities, output): + """Reuse a built root for update isolation without repeating the root copy.""" + table = json.loads(subprocess.check_output(["sfdisk", "--json", source]))["partitiontable"] + primary = json.loads(subprocess.check_output(["sfdisk", "--json", disk]))["partitiontable"] + root = next(p for p in table["partitions"] if p["type"].lower() == "0fc63daf-8483-4772-8e79-3d69d8477de4") + source_esp = next(p for p in table["partitions"] if p["type"].lower() == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b") + destination = next(p for p in primary["partitions"] if p["uuid"].lower() == identities["second"]) + assert root["size"] <= destination["size"], "Root fixture must fit the second partition" + image = output / "second-root.ext4" + run("dd", f"if={source}", f"of={image}", "bs=1M", "iflag=skip_bytes,count_bytes", + f"skip={root['start'] * 512}", f"count={root['size'] * 512}", "conv=sparse", "status=none") + with image.open("r+b") as stream: + stream.truncate(destination["size"] * 512) + run("resize2fs", str(image), stdout=subprocess.DEVNULL) + fstab = output / "second-fstab" + fstab.write_text(f"/dev/disk/by-partuuid/{identities['second']} / ext4 defaults 0 1\n" + f"/dev/disk/by-partuuid/{identities['esp']} /efi vfat defaults 0 2\n" + f"/efi/reliefos/instances/{identities['second']} /boot none bind 0 0\n") + run("debugfs", "-w", "-R", "mkdir /efi", str(image), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + run("debugfs", "-w", "-R", "rm /etc/fstab", str(image), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + run("debugfs", "-w", "-R", f"write {fstab} /etc/fstab", str(image), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + assert subprocess.check_output(["debugfs", "-R", "cat /etc/fstab", str(image)], stderr=subprocess.DEVNULL) == fstab.read_bytes() + run("e2fsck", "-f", "-n", str(image), stdout=subprocess.DEVNULL) + run("dd", f"if={image}", f"of={disk}", "bs=1M", "oflag=seek_bytes", + f"seek={destination['start'] * 512}", "conv=notrunc", "status=none") + image.unlink() + tree = output / "second-boot-fixture" + tree.mkdir() + for name in ("grub", "reliefos", "leonos", "EFI"): + run("mcopy", "-s", "-i", f"{source}@@{source_esp['start'] * 512}", "::/" + name, str(tree) + "/") + (tree / "reliefos/config/root.cfg").write_text(f"set reliefos_root=PARTUUID={identities['second']}\n") + (tree / "reliefos-instance.id").write_text(f"ReliefOS instance v1\n{identities['second']}\n") + target = f"{disk}@@{esp['start'] * 512}" + relative = "::/reliefos/instances/" + identities["second"] + created = subprocess.run(["mmd", "-i", target, relative], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if created.returncode: + assert esp_bytes(disk, esp, "reliefos/instances/" + identities["second"] + "/reliefos-instance.id") == (tree / "reliefos-instance.id").read_bytes() + run("mcopy", "-o", "-s", "-i", target, *(str(p) for p in sorted(tree.iterdir())), relative + "/") + + +def enter_console(probe, serial, process): + deadline = time.monotonic() + 45 + while time.monotonic() < deadline: + probe.key("up") + if "install reliefos" in ocr(probe.frame("console-entry")).lower().replace("0", "o"): + break + else: + raise AssertionError("Installer menu did not appear") + probe.key("down"); probe.key("ret") + wait_for(serial, process, lambda text: "path=/bin/bash" in text, "installer console", 180) + + +def worker_disk(binary, output, library=None): + """Carry the current installer independently of the package-version fixture.""" + disk, fat = output / "worker.raw", output / "worker.fat" + with disk.open("xb") as stream: + stream.truncate(66 * 1024**2) + run("sfdisk", str(disk), input="label: gpt\nunit: sectors\n\nstart=2048,size=131072,type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7\n", text=True, stdout=subprocess.DEVNULL) + with fat.open("xb") as stream: + stream.truncate(64 * 1024**2) + run("mkfs.fat", "--invariant", "-F", "32", str(fat), stdout=subprocess.DEVNULL) + run("mcopy", "-i", str(fat), str(binary), "::/installer.elf") + if library: + run("mcopy", "-i", str(fat), str(library), "::/libreliefos.so.2") + run("dd", f"if={fat}", f"of={disk}", "bs=512", "seek=2048", "conv=notrunc,sparse", "status=none") + fat.unlink() + return disk + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--images", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--timeout", type=int, default=180) + parser.add_argument("--install-timeout", type=int, default=7200) + parser.add_argument("--first-installation", type=Path, help="Completed first-install test directory; only its copy is modified") + parser.add_argument("--root-fixture", type=Path, help="Built disk root reused for the second installation/update case") + parser.add_argument("--worker-binary", type=Path, help="Test the current worker against the fixed package-version fixture") + parser.add_argument("--worker-library", type=Path, help="Current runtime DSO used with the worker binary") + parser.add_argument("--checked-fixture", type=Path, help="Resume a prepared copied disk after recorded native target checks") + args = parser.parse_args() + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=False) + if bool(args.first_installation) != bool(args.root_fixture): + parser.error("--first-installation and --root-fixture are used together") + if args.first_installation: + previous = args.first_installation.resolve() + identities = json.loads((previous / "identities.json").read_text()) + disk = output / "two-installations.raw" + run("cp", "--reflink=auto", "--sparse=always", str(previous / disk.name), str(disk)) + table = json.loads(subprocess.check_output(["sfdisk", "--json", disk]))["partitiontable"] + esp = next(p for p in table["partitions"] if p["uuid"].lower() == identities["esp"]) + assert_foreign(disk, esp) + assert esp_bytes(disk, esp, f"reliefos/instances/{identities['first']}/reliefos/config/root.cfg") == f"set reliefos_root=PARTUUID={identities['first']}\n".encode() + else: + disk, esp, identities = compose_disk(output) + (output / "identities.json").write_text(json.dumps(identities, indent=2) + "\n") + first_files = None + cases = () if args.first_installation else ((2, "first"), (3, "second")) + for row, name in cases: + with boot(output / f"install-{name}", "bios", disk=disk, + iso=args.images.resolve() / "reliefos-installer.iso") as session: + install_tty(*session, skip_xorg=True, install_timeout=args.install_timeout, + partition_commands=("1", "esp", str(row), "root", "done")) + if name == "second": + check_update_selection(*session, identities, args.install_timeout) + assert_foreign(disk, esp) + identity = identities[name] + base = f"reliefos/instances/{identity}" + assert esp_bytes(disk, esp, base + "/reliefos/config/root.cfg") == f"set reliefos_root=PARTUUID={identity}\n".encode() + files = {member: hashlib.sha256(esp_bytes(disk, esp, f"reliefos/instances/{identities['first']}/{member}")).hexdigest() + for member in ("reliefos/config/root.cfg", "reliefos/state/kernel-A.env", "grub/grub.cfg")} + if first_files is None: + first_files = files + else: + assert files == first_files, "Second install changed the first installation's boot state" + print(f"PASS shared ESP installation: {name}", flush=True) + if args.first_installation: + members = ("reliefos/config/root.cfg", "reliefos/state/kernel-A.env", "grub/grub.cfg") + def first_state(): + return {p: hashlib.sha256(esp_bytes(disk, esp, f"reliefos/instances/{identities['first']}/{p}")).hexdigest() for p in members} + first_files = first_state() + previous_check = checked_fixture(args.checked_fixture.resolve(), identities) if args.checked_fixture else None + if not previous_check: + seed_second_fixture(args.root_fixture.resolve(), disk, esp, identities, output) + library = args.worker_library.resolve() if args.worker_library else None + worker = worker_disk(args.worker_binary.resolve(), output, library) if args.worker_binary else None + with boot(output / "update-two-roots", "bios", disk=disk, extra_disk=worker, + iso=args.images.resolve() / "reliefos-installer.iso") as session: + enter_console(*session) + if worker: + probe, serial, process = session + probe.text("mkdir -p /mnt/probe; mount -t vfat -o ro /dev/sdb1 /mnt/probe; cp /mnt/probe/installer.elf /tmp/installer; chmod 755 /tmp/installer; export PATH=/tmp:$PATH; echo __WORKER_READY_$? > /dev/ttyS0") + probe.key("ret") + wait_for(serial, process, lambda text: "__WORKER_READY_0" in text, "current native worker", args.timeout) + probe.text("sha256sum /tmp/installer > /dev/ttyS0") + probe.key("ret") + expected_worker = hashlib.sha256(args.worker_binary.read_bytes()).hexdigest() + wait_for(serial, process, lambda text: expected_worker in text, + "exact current installer executable checksum", args.timeout) + if library: + probe.text("cp /mnt/probe/libreliefos.so.2 /tmp/libreliefos.so.2; export LD_LIBRARY_PATH=/tmp; sha256sum /tmp/libreliefos.so.2 > /dev/ttyS0") + probe.key("ret") + expected_library = hashlib.sha256(library.read_bytes()).hexdigest() + wait_for(serial, process, lambda text: expected_library in text, + "exact current runtime library checksum", args.timeout) + if previous_check: + apply_checked_update(*session, previous_check, args.install_timeout) + else: + check_update_selection(*session, identities, args.install_timeout, after_install=False) + assert_foreign(disk, esp) + assert first_state() == first_files, "Second-root update changed the first installation" + chooser = efi_chooser(output, disk, esp, identities["first"]) + for firmware, identity in (("bios", identities["second"]), ("uefi", identities["first"])): + working = output / f"boot-{firmware}.raw" + run("cp", "--reflink=auto", "--sparse=always", str(disk), str(working)) + with boot(output / f"boot-{firmware}", firmware, disk=working, + extra_disk=chooser if firmware == "uefi" else None, + boot_extra=firmware == "uefi") as (probe, serial, process): + wait_for(serial, process, lambda text: boot_evidence(text, firmware) and identity in text, + "selected installation root and init", args.timeout) + wait_for(serial, process, lambda text: f"[initramfs] root=/dev/disk/by-partuuid/{identity} " in text, + "selected real root with its boot directory mounted", args.timeout) + if args.first_installation and identity == identities["second"]: + wait_xdm(probe, process, "updated-xdm", args.timeout) + else: + wait_text(probe, process, "console-login", "login:", timeout=args.timeout) + probe.frame("boot-complete") + print(f"PASS actual {firmware} boot of {identity}", flush=True) + selected = output / "boot-other-instance.raw" + run("cp", "--reflink=auto", "--sparse=always", str(disk), str(selected)) + with boot(output / "boot-other-instance", "bios", disk=selected) as (probe, serial, process): + deadline = time.monotonic() + 45 + while time.monotonic() < deadline: + probe.key("up") + frame = probe.frame("other-installation-menu") + menu = frame.crop((frame.width // 2, frame.height // 4, frame.width, frame.height * 3 // 4)) + if "advanced options" in ocr(menu).lower(): + break + else: + raise AssertionError("GRUB did not enumerate the other installation") + probe.key("down"); probe.key("down"); probe.key("ret") + # Selecting another GRUB config opens its own menu; let that default boot. + wait_for(serial, process, lambda text: boot_evidence(text, "bios") and identities["first"] in text, + "another installation selected through the GRUB menu", args.timeout) + wait_for(serial, process, lambda text: f"[initramfs] root=/dev/disk/by-partuuid/{identities['first']} " in text, + "the other instance's real root and boot directory", args.timeout) + wait_text(probe, process, "other-console-login", "login:", timeout=args.timeout) + probe.frame("other-installation-booted") + print("PASS GRUB menu selection of the other installation", flush=True) + assert_foreign(disk, esp) + (output / "result.json").write_text(json.dumps({"passed": True, "identities": identities, + "foreign_efi_preserved": True, "first_boot_state_preserved": True}, indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/tools/test_musl_mutex_protocol.py b/tools/test_musl_mutex_protocol.py new file mode 100644 index 00000000..d317cc95 --- /dev/null +++ b/tools/test_musl_mutex_protocol.py @@ -0,0 +1,118 @@ +"""Exercise the locked musl patch against real mutex-attribute source.""" +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +class MutexProtocolTests(unittest.TestCase): + def test_built_libc_with_unavailable_pi_syscall(self): + prefix = Path(os.environ.get("RELIEFOS_TEST_OUTPUT", ROOT / "out/x86_64/release")) / "sysroot/musl" + stamp = prefix / ".leonos-musl.json" + if not stamp.is_file() or not any("mutex-protocol" in p["path"] + for p in json.loads(stamp.read_text())["patches"]): + self.skipTest("build the musl sysroot with the PI capability patch first") + with tempfile.TemporaryDirectory(prefix="musl-pi-runtime-") as directory: + work = Path(directory) + source = work / "probe.c" + source.write_text('''#include +#include +#include +#include +#include +struct filter { unsigned short code; unsigned char jt,jf; unsigned k; }; +struct program { unsigned short count; struct filter *filters; }; +int main(void) +{ + /* An isolated Linux process models ReliefNT's unavailable FUTEX_LOCK_PI. + * All other syscalls retain their real Linux behavior. */ + struct filter filters[]={ + {0x20,0,0,0}, {0x15,0,3,202}, {0x20,0,0,24}, + {0x15,0,1,6}, {0x06,0,0,0x00050000U|ENOSYS}, {0x06,0,0,0x7fff0000U}}; + struct program program={sizeof(filters)/sizeof(filters[0]),filters}; + assert(!prctl(38,1,0,0,0)); + assert(!prctl(22,2,&program)); + pthread_mutexattr_t attr;pthread_mutex_t mutex; + assert(!pthread_mutexattr_init(&attr)); + assert(pthread_mutexattr_setprotocol(&attr,PTHREAD_PRIO_INHERIT)==ENOTSUP); + assert(!pthread_mutex_init(&mutex,&attr)); + assert(!pthread_mutex_lock(&mutex));assert(!pthread_mutex_unlock(&mutex)); + assert(!pthread_mutex_destroy(&mutex));assert(!pthread_mutexattr_destroy(&attr)); + puts("PASS rebuilt musl: missing PI syscall selects a working ordinary mutex"); + return 0; +} +''') + binary = work / "probe" + subprocess.run(["clang", "--target=x86_64-linux-musl", f"--sysroot={prefix}", + "-fuse-ld=lld", "-nostdlib", "-O2", "-Wall", "-Wextra", "-Werror", + prefix / "lib/crt1.o", prefix / "lib/crti.o", source, + "-L", prefix / "lib", "-l:libmimalloc.so.3", "-lc", prefix / "lib/crtn.o", + "-o", binary], check=True) + subprocess.run([prefix / "lib/libc.so", "--library-path", prefix / "lib", binary], + check=True, timeout=10) + + def test_optional_pi_probe_reports_posix_capability(self): + with tempfile.TemporaryDirectory(prefix="musl-pi-protocol-") as directory: + work = Path(directory) + source = work / "src/thread" + source.mkdir(parents=True) + name = "pthread_mutexattr_setprotocol.c" + shutil.copyfile(ROOT / "third_party/musl/src/thread" / name, source / name) + lock = json.loads((ROOT / "configs/dependencies.lock.json").read_text()) + musl = next(item for item in lock["dependencies"] if item["id"] == "musl") + patch = next(item for item in musl["patches"] if "mutex-protocol" in item["path"]) + patch_path = ROOT / patch["path"] + self.assertEqual(hashlib.sha256(patch_path.read_bytes()).hexdigest(), patch["sha256"]) + subprocess.run(["patch", "--batch", "--forward", "-p1", "-d", work, "-i", patch_path], + check=True, capture_output=True) + (source / "pthread_impl.h").write_text('''#include +typedef struct { unsigned __attr; } pthread_mutexattr_t; +#define PTHREAD_PRIO_NONE 0 +#define PTHREAD_PRIO_INHERIT 1 +#define PTHREAD_PRIO_PROTECT 2 +#define FUTEX_LOCK_PI 6 +#define a_store(p, v) (*(p) = (v)) +''') + (source / "syscall.h").write_text('''#define SYS_futex 202 +long probe(long, volatile int *, long, long, long); +#define __syscall probe +''') + harness = work / "test.c" + harness.write_text('''#include +#include +#include "pthread_impl.h" +int test_setprotocol(pthread_mutexattr_t *, int); +static int error, calls; +long probe(long number,volatile int *word,long operation,long value,long timeout) +{assert(number==202 && word && !*word && operation==6 && !value && !timeout);++calls;return -error;} +int main(int argc,char **argv) +{ + assert(argc==3);error=atoi(argv[1]);int expected=atoi(argv[2]); + pthread_mutexattr_t attr={0x80}; + assert(!test_setprotocol(&attr,PTHREAD_PRIO_NONE) && !calls && attr.__attr==0x80); + assert(test_setprotocol(&attr,PTHREAD_PRIO_INHERIT)==expected && calls==1); + assert(attr.__attr==(unsigned)(expected ? 0x80 : 0x88)); + assert(test_setprotocol(&attr,PTHREAD_PRIO_INHERIT)==expected && calls==1); + assert(test_setprotocol(&attr,PTHREAD_PRIO_PROTECT)==ENOTSUP && calls==1); + assert(test_setprotocol(&attr,3)==EINVAL && calls==1); + assert(!test_setprotocol(&attr,PTHREAD_PRIO_NONE) && attr.__attr==0x80); + return 0; +} +''') + binary = work / "test" + subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined", + "-fno-pie", "-no-pie", "-Dpthread_mutexattr_setprotocol=test_setprotocol", + "-I", source, source / name, harness, "-o", binary], check=True) + for error, expected in ((38, 95), (95, 95), (0, 0), (22, 22), (14, 14)): + with self.subTest(kernel_error=error): + subprocess.run([binary, str(error), str(expected)], check=True, timeout=10) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/test_oobe.py b/tools/test_oobe.py index 36267b4d..41cad8ea 100644 --- a/tools/test_oobe.py +++ b/tools/test_oobe.py @@ -6,9 +6,8 @@ from test_installer_setup import CRYPTO AUTH_SOURCES = ["userland/apps/authd/accounts.c", "userland/runtime/src/auth_password.c", *CRYPTO] -AUTH_FLAGS = ["-Ithird_party/mbedtls/include", "-idirafter", "userland/runtime/include", - "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", - '-DMBEDTLS_CONFIG_FILE="reliefos_mbedtls_config.h"'] +AUTH_FLAGS = ["-idirafter", "userland/runtime/include", + "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections"] ROOT = Path(__file__).resolve().parents[1] diff --git a/tools/test_reliefmail_ca_qemu.py b/tools/test_reliefmail_ca_qemu.py index 7a35f8a8..9b1f49bd 100755 --- a/tools/test_reliefmail_ca_qemu.py +++ b/tools/test_reliefmail_ca_qemu.py @@ -65,8 +65,7 @@ def build_probes(output): command[-3], command[-1] = str(source), str(output / (name + '.o')) command[1:1] = ['-I' + str(ROOT / 'userland/apps/reliefmail'), '-I' + str(ROOT / 'userland/runtime/include'), - '-I' + str(ROOT / 'third_party/mbedtls/include'), - '-DMBEDTLS_CONFIG_FILE="reliefos_mbedtls_config.h"'] + '-I' + str(OUT / 'upstream/runtime-development/usr/include')] run(command, log) objects.append(str(output / (name + '.o'))) command = list(template['link']) @@ -74,6 +73,8 @@ def build_probes(output): command[index:index + 1] = objects command[command.index(str(probe))] = str(output / 'tls-probe.elf') command.insert(command.index('-lc'), str(OUT / 'system/lib/libreliefos.so.2')) + for library in ('mbedtls', 'mbedx509', 'mbedcrypto'): + command.insert(command.index('-lc'), str(OUT / 'upstream/runtime-development/usr/lib' / ('lib' + library + '.so'))) run(command, log) return probe diff --git a/tools/test_reliefplayer_qemu.py b/tools/test_reliefplayer_qemu.py index 8be3db2e..d310d366 100644 --- a/tools/test_reliefplayer_qemu.py +++ b/tools/test_reliefplayer_qemu.py @@ -64,6 +64,8 @@ def prepare(image, kernel, output, build_dir=OUT): (probe, fltk.PROBE_GUEST, 0o100755), (runner, '/usr/bin/rptest', 0o100755)] for name in ('sample.wav', 'sample.mp3', 'short.wav', 'invalid.wav'): payload.append((fixtures / name, '/home/test/' + name, 0o100644)) + for library in (build_dir / 'upstream/ffmpeg/root/usr/lib').glob('*.so.*'): + payload.append((library.resolve(), '/usr/lib/' + library.name, 0o100755)) fltk.validate_target_elf(build_dir / 'userland/reliefplayer.elf', sysroot=build_dir / 'sysroot/musl', required_needed=('libfltk.so.1.3',)) metadata = fltk.prepare_disk(image, kernel, output, payload) (output / 'prepare.json').write_text(json.dumps(metadata, indent=2) + '\n') @@ -109,7 +111,7 @@ def launch(self, path=''): self.probe.key('ctrl-u') self.probe.text('rptest' + (' ' + path if path else '')) self.probe.key('ret') - self.wait('[reliefplayer.elf] FLTK audio player ready', self.launch_offset) + self.wait('[reliefplayer.elf] FLTK media player ready', self.launch_offset) log = self.serial.read_text(errors='replace')[self.launch_offset:] self.pid = int(re.search(r'RP_START pid=(\d+)', log)[1]) self.geometry = fltk.wait_client_geometry(self.serial, self.process, self.pid, self.launch_offset) diff --git a/tools/test_sudo_policy.py b/tools/test_sudo_policy.py index 557ddb96..d6bba169 100644 --- a/tools/test_sudo_policy.py +++ b/tools/test_sudo_policy.py @@ -12,6 +12,7 @@ from __future__ import annotations import subprocess +from runtime_test_dependencies import crypto_flags import sys import tempfile from pathlib import Path @@ -42,8 +43,7 @@ "userland/apps/authd/authd_sudo.c", "userland/apps/authd/accounts.c", "userland/runtime/src/auth_password.c", - *[f"third_party/mbedtls/library/{name}.c" for name in - ("md", "pkcs5", "sha1", "sha256", "sha512", "platform_util", "aes")]]), + *crypto_flags()]), ) @@ -52,10 +52,6 @@ def main() -> int: for name, sources in SUITES: executable = str(Path(tmp) / name) extra = ["-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections"] - extra += (["-Ithird_party/mbedtls/include", - "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", - '-DMBEDTLS_CONFIG_FILE="reliefos_mbedtls_config.h"'] - if name == "sudo-security" else []) if name == "sudo-client": extra += ["-Wl,--wrap=reliefos_ipc_connect,--wrap=reliefos_ipc_peer_credentials"] subprocess.run(["cc", *COMMON_FLAGS, *extra, *sources, "-o", executable], diff --git a/tools/test_terminal_packages.py b/tools/test_terminal_packages.py index e7a1167d..90b9f8c6 100644 --- a/tools/test_terminal_packages.py +++ b/tools/test_terminal_packages.py @@ -1,9 +1,5 @@ #!/usr/bin/env python3 -"""Run the built Linux ncurses binaries on the host and check their runtime data. - -Vim used to be validated here as well; it is a signed upstream Alpine package -now, so only the ReliefOS ncurses build keeps a host-side runtime check. -""" +"""Check the signed ncurses command/terminfo payload in a managed root on Linux.""" import argparse import os @@ -17,44 +13,24 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--musl", required=True, type=Path) - parser.add_argument("--ncurses", required=True, type=Path) + parser.add_argument("--root", required=True, type=Path) args = parser.parse_args() - musl, ncurses = args.musl.resolve(), args.ncurses.resolve() - database = ncurses / "share/terminfo" + musl, root = args.musl.resolve(), args.root.resolve() + database = root / "usr/share/terminfo" env = {**os.environ, "TERM": "xterm-256color", "TERMINFO": str(database), "TERMINFO_DIRS": str(database)} - clear = ncurses / "bin/clear" - infocmp = ncurses / "bin/infocmp" - for binary in (clear, infocmp, ncurses / "bin/tput"): - headers = subprocess.check_output(["readelf", "-l", "-d", str(binary)], text=True) - assert "INTERP" not in headers and "(NEEDED)" not in headers, binary - terminfo = subprocess.check_output([str(infocmp), "xterm-256color"], env=env) + loader = musl / "lib/libc.so" + def run(command, *arguments): + return subprocess.check_output([str(loader), "--library-path", str(root / "usr/lib") + ":" + str(root / "lib"), + str(root / "usr/bin" / command), *arguments], env=env) + for command in ("clear", "infocmp", "tput"): + headers = subprocess.check_output(["readelf", "-l", str(root / "usr/bin" / command)], text=True) + assert "/lib/ld-musl-x86_64.so.1" in headers + terminfo = run("infocmp", "xterm-256color") assert b"colors#0x100" in terminfo or b"colors#256" in terminfo - cleared = subprocess.check_output([str(clear)], env=env) - assert cleared.startswith(b"\x1b[") - - # ReliefOS must retain its native terminal types even if the external - # database is missing or damaged; this is the failure mode from issue #27. - fallback_env = {**env, "TERMINFO": "/nonexistent/terminfo", - "TERMINFO_DIRS": "/nonexistent/terminfo"} - fallback_env["TERM"] = "xterm" - fallback_clear = subprocess.check_output([str(clear)], env=fallback_env) - assert fallback_clear.startswith(b"\x1b[") - fallback_columns = subprocess.check_output( - [str(ncurses / "bin/tput"), "cols"], env=fallback_env) - assert int(fallback_columns) > 0 - with tempfile.TemporaryDirectory(prefix="reliefos-terminal-test-") as directory: - work = Path(directory) - probe = work / "ncurses-test" - subprocess.run([ - "clang", "--target=x86_64-linux-musl", "--gcc-toolchain=/nonexistent", - f"--sysroot={musl}", "--rtlib=compiler-rt", "--unwindlib=none", - "-fuse-ld=lld", "-static", - "-I" + str(ncurses / "include"), str(ROOT / "tools/tests/ncurses_runtime_test.c"), - "-L" + str(ncurses / "lib"), "-lncursesw", "-ltinfow", "-o", str(probe)], - check=True) - subprocess.run([str(probe)], env=env, check=True, timeout=30) - print("PASS Linux host: static ncurses runtime data and embedded fallback terminfo") + assert run("clear").startswith(b"\x1b[") + assert int(run("tput", "cols")) > 0 + print("PASS Linux host: signed dynamic ncurses tools and matching terminfo") if __name__ == "__main__": diff --git a/tools/tests/installer_mount_transition_test.c b/tools/tests/installer_mount_transition_test.c index d349a793..f4e42a44 100644 --- a/tools/tests/installer_mount_transition_test.c +++ b/tools/tests/installer_mount_transition_test.c @@ -194,35 +194,29 @@ static void assert_format_args(void) int main(void) { - assert(installer_mount_targets("/dev/sda", 0) == 0); - assert(root_mounted && esp_mounted); - assert(spawn_calls == 0); /* an update never formats */ - assert(installer_mount_targets("/dev/sda", 1) == 0); + assert(installer_mount_targets("/dev/sda") == 0); assert(root_mounted && esp_mounted && mutations == 1); assert_format_args(); - assert(installer_mount_targets("/dev/sda", 0) == 0); - assert(spawn_calls == 1); /* returning to update does not re-format */ - assert(installer_mount_targets("/dev/sda", 0) == 0); busy_esp = 1; - assert(installer_mount_targets("/dev/sda", 1) == -EBUSY); + assert(installer_mount_targets("/dev/sda") == -EBUSY); assert(root_mounted && esp_mounted && mutations == 1); busy_esp = 0; busy_root = 1; - assert(installer_mount_targets("/dev/sda", 1) == -EBUSY); + assert(installer_mount_targets("/dev/sda") == -EBUSY); assert(root_mounted && !esp_mounted && mutations == 1); busy_root = 0; - assert(installer_mount_targets("/dev/sda", 1) == 0); + assert(installer_mount_targets("/dev/sda") == 0); assert(root_mounted && esp_mounted && mutations == 2); manual_layout = 1; installer_partition_auto = 0; installer_root_partition = 8; installer_esp_partition = 3; - assert(installer_mount_targets("/dev/sda", 1) == 0); + assert(installer_mount_targets("/dev/sda") == 0); assert(!strcmp(spawn_args[spawn_argc - 1], "/dev/sda9")); assert(mutations == 2); /* A manual install never replaces the GPT. */ manual_bad_role = 1; int unchanged_formats = format_calls; - assert(installer_mount_targets("/dev/sda", 1) == -EINVAL); + assert(installer_mount_targets("/dev/sda") == -EINVAL); assert(format_calls == unchanged_formats && mutations == 2); selected_partition = 0; partition_count = 1; @@ -238,20 +232,20 @@ int main(void) assert(type_calls == previous_types); manual_bad_role = 0; capacity_bytes = 256ULL * 1024 * 1024; - assert(installer_mount_targets("/dev/sda", 1) == -ENOSPC); + assert(installer_mount_targets("/dev/sda") == -ENOSPC); assert(format_calls == previous_formats && mutations == 2); installer_partition_auto = 1; capacity_bytes = UINT64_MAX; - assert(installer_mount_targets("/dev/sda", 1) < 0); + assert(installer_mount_targets("/dev/sda") < 0); assert(format_calls == previous_formats && mutations == 2); capacity_bytes = 0; capacity_inodes = 10000000; - assert(installer_mount_targets("/dev/sda", 1) == -ENOSPC); + assert(installer_mount_targets("/dev/sda") == -ENOSPC); assert(format_calls == previous_formats && mutations == 2); capacity_inodes = 0;setup.swap_enabled = true;setup.swap_size_mib = 4096; - assert(installer_mount_targets("/dev/sda", 1) == -ENOSPC); + assert(installer_mount_targets("/dev/sda") == -ENOSPC); assert(format_calls == previous_formats && mutations == 2); installer_partition_auto = 0;setup.swap_size_mib = 512; - assert(installer_mount_targets("/dev/sda", 1) == -ENOSPC); + assert(installer_mount_targets("/dev/sda") == -ENOSPC); assert(format_calls == previous_formats && mutations == 2); setup.swap_enabled = false; installer_partition_auto = 1; @@ -259,7 +253,7 @@ int main(void) setup.skip_bios = !(modes & RELIEFOS_BOOT_BIOS); setup.skip_uefi = !(modes & RELIEFOS_BOOT_UEFI); assert(installer_boot_modes() == modes); - assert(installer_mount_targets("/dev/sda", 1) == 0); + assert(installer_mount_targets("/dev/sda") == 0); assert(!strcmp(formatted_esp, (modes & RELIEFOS_BOOT_BIOS) ? "/dev/sdap2" : "/dev/sdap1")); assert(!strcmp(spawn_args[spawn_argc - 1], (modes & RELIEFOS_BOOT_BIOS) ? "/dev/sdap3" : "/dev/sdap2")); } diff --git a/tools/tests/installer_payload_test.c b/tools/tests/installer_payload_test.c index fd61d6e3..3ac84688 100644 --- a/tools/tests/installer_payload_test.c +++ b/tools/tests/installer_payload_test.c @@ -9,6 +9,7 @@ #include "../../userland/apps/installer/install_ops.c" #undef lstat #undef fopen +#include "../../userland/apps/installer/installer_update.h" /* Map guest stat requests into a real, private host filesystem fixture. */ static const char *fixture_root; @@ -57,8 +58,21 @@ int main(int argc, char **argv) setup.skip_xorg = 1; result = check_install_payload_required(); } else if (argc == 3 && !strcmp(argv[2], "update")) { - result = check_update_payload_required(); - } else result = argc == 3 ? check_update_target_required() : check_update_payload_required(); + struct installer_update_info target = {0}, media = {0}; + char root[4096], payload[4096]; + snprintf(root, sizeof(root), "%s/target", fixture_root); + result = installer_update_read_system(root, &target); + if (!result) { + snprintf(payload, sizeof(payload), "%s/install/%s", fixture_root, + !strcmp(target.profile, "console") ? "root-console" : "root"); + result = installer_update_read_media(payload, target.profile, &media); + } + } else if (argc == 3) { + struct installer_update_info target = {0}; + char root[4096]; + snprintf(root, sizeof(root), "%s/target", fixture_root); + result = installer_update_read_system(root, &target); + } else result = check_install_payload_required(); printf("payload-check=%d\n", result); return result == 0 ? 0 : 1; } diff --git a/tools/tests/installer_swap_config_test.c b/tools/tests/installer_swap_config_test.c index 1a723a0a..0fa93073 100644 --- a/tools/tests/installer_swap_config_test.c +++ b/tools/tests/installer_swap_config_test.c @@ -105,6 +105,8 @@ int main(int argc,char **argv) bool published=success || !strcmp(which,"backup-unlink") || !strcmp(which,"dir-close"); if (published) { assert(committed && (success ? !rc : rc<0 && faulted));contents(path,"/dev/disk/by-partuuid/root-id"); + contents(path,"/dev/disk/by-partuuid/esp-id /efi vfat defaults 0 2"); + contents(path,"/efi/reliefos/instances/root-id /boot none bind 0 0"); if (enabled) contents(path,"/swapfile none swap sw 0 0"); } else {assert(rc<0 && !committed && faulted);if (!strcmp(which,"sync-close")) assert(rc==-ENOSPC && close_fault); contents(path,"old fstab");} diff --git a/tools/tests/installer_tty_input_test.c b/tools/tests/installer_tty_input_test.c index 45c405df..6d5dc86f 100644 --- a/tools/tests/installer_tty_input_test.c +++ b/tools/tests/installer_tty_input_test.c @@ -75,10 +75,35 @@ static int flow_create(uint32_t f, uint32_t n, const char *s) static int flow_number(uint32_t n) { (void)n; return flow_unused(); } static int flow_name(const char *s) { (void)s; return flow_unused(); } static int flow_format(uint32_t f, const char *s) { (void)f; (void)s; return flow_unused(); } -static void flow_probe(void) { assert(flow_stage == 1); flow_setup.skip_bios = flow_setup.skip_uefi = 0; flow_stage = 2; } +int installer_update_check(const char *disk, struct installer_update_info *info, + installer_update_observer observer, void *context) +{ + assert(flow_stage == 1 && !strcmp(disk, "/dev/sda")); + memset(info, 0, sizeof(*info)); + strcpy(info->disk, "/dev/sda"); + strcpy(info->root_uuid, "8ab5d176-6479-4ec8-b382-531fca6ea662"); + strcpy(info->installed_version, "5.3.0"); + strcpy(info->media_version, "5.3.1"); + info->boot_modes = 3; + observer(context, 100, "Update checks completed"); + flow_stage = 2; + return 0; +} static void flow_install(void) { assert(flow_stage == 3 && flow_setup.skip_xorg); flow_choices(); flow_stage = 4; } -static void flow_update(void) { assert(flow_stage == 2); flow_choices(); flow_stage = 4; } +int installer_update_apply(const struct installer_update_info *expected, unsigned modes, + struct installer_update_info *result, + installer_update_observer observer, void *context) +{ + assert(flow_stage == 2 && modes == flow_modes); + assert(!strcmp(expected->root_uuid, "8ab5d176-6479-4ec8-b382-531fca6ea662")); + flow_choices(); + flow_stage = 4; + memset(result, 0, sizeof(*result)); + strcpy(result->failure, "Fixture stops before reboot"); + observer(context, 40, "Applying signed system packages"); + return -EIO; +} int installer_setup_valid(const struct installer_setup *s) { return !strcmp(s->username, "alice") && !strcmp(s->password, "p") && !strcmp(s->password_confirm, "p") && !strcmp(s->root_password, "r") && @@ -98,14 +123,15 @@ static void test_tty_flow(int update, unsigned modes) &automatic, &selected_disk, &mode, &success, &page, flow_disks, flow_disk_line, flow_parts, flow_part_line, flow_valid, flow_auto, flow_unused, flow_create, flow_number, flow_name, flow_number, flow_unused, - flow_format, flow_probe, flow_install, flow_update, + flow_format, flow_install, }; char answers[192]; memset(&flow_setup, 0, sizeof(flow_setup)); + strcpy(disk.path, "/dev/sda"); flow_modes = modes; snprintf(answers, sizeof(answers), "%s\n0\n%s\n%s\n%s", update ? "update" : "install", modes & 1 ? "y" : "n", modes & 2 ? "y" : "n", - update ? "UPDATE\n" : "auto\ndone\nalice\np\np\nr\nr\nn\nn\nINSTALL\n"); + update ? "y\n" : "auto\ndone\nalice\np\np\nr\nr\nn\nn\nINSTALL\n"); feed(answers); /* Leave success false to return immediately after the execution callback. */ assert(installer_tty_main(&context) == 1 && flow_stage == 4); diff --git a/tools/tests/installer_update_model_test.c b/tools/tests/installer_update_model_test.c new file mode 100644 index 00000000..a918344c --- /dev/null +++ b/tools/tests/installer_update_model_test.c @@ -0,0 +1,98 @@ +#define _POSIX_C_SOURCE 200809L +#include "installer_update.h" + +#include +#include +#include +#include +#include +#include +#include + +static void write_file(const char *root, const char *relative, const char *text) +{ + char path[PATH_MAX]; + assert(snprintf(path, sizeof(path), "%s/%s", root, relative) < (int)sizeof(path)); + FILE *stream = fopen(path, "w"); + assert(stream); + assert(fputs(text, stream) >= 0); + assert(!fclose(stream)); +} + +int main(int argc, char **argv) +{ + assert(argc == 2); + const char *root = argv[1]; + const char *const directories[] = { + "etc", "etc/apk", "etc/reliefos", "lib", "lib/apk", "lib/apk/db", + "var", "var/lib", "usr", "usr/bin", "usr/lib", "usr/share", "bin", "sbin", "boot", + }; + char path[PATH_MAX]; + for (size_t i = 0; i < sizeof(directories) / sizeof(directories[0]); ++i) { + assert(snprintf(path, sizeof(path), "%s/%s", root, directories[i]) < (int)sizeof(path)); + assert(!mkdir(path, 0755)); + } + write_file(root, "etc/os-release", "ID=reliefos\nVERSION_ID=5.3.1\n"); + write_file(root, "etc/reliefos/desktop-backend", "console\n"); + struct installer_update_info info = {0}; + assert(!installer_update_read_system(root, &info)); + assert(!strcmp(info.installed_version, "5.3.1")); + assert(!strcmp(info.profile, "console")); + write_file(root, "etc/reliefos/desktop-backend", "console\nxorg\n"); + assert(installer_update_read_system(root, &info) == -EINVAL); + write_file(root, "etc/reliefos/desktop-backend", "xorg\n"); + write_file(root, "etc/os-release", "ID=reliefos\nVERSION_ID=\"$(touch /tmp/unwanted)\"\n"); + assert(installer_update_read_system(root, &info) == -EINVAL); + write_file(root, "usr/lib/os-release", "ID=reliefos\nVERSION_ID=\"5.3.1\"\n"); + snprintf(path, sizeof(path), "%s/etc/os-release", root); + assert(!unlink(path)); + assert(!symlink("../usr/lib/os-release", path)); + assert(!installer_update_read_system(root, &info)); + assert(!strcmp(info.profile, "xorg")); + assert(!unlink(path)); + assert(!symlink("/etc/os-release", path)); + assert(installer_update_read_system(root, &info) == -EINVAL); + + write_file(root, "lib/apk/db/installed", + "P:external-fixture\nV:1.0-r0\nI:999999\n\n" + "P:reliefos-base\nV:5.3.1-r1\nI:100\n\n" + "P:reliefos-kernel\nV:5.3.1-r2\nI:200\n\n"); + struct installer_update_packages packages; + assert(!installer_update_read_packages(root, &packages)); + assert(packages.installed_bytes == 300 && packages.largest_package == 200); + assert(!strcmp(packages.kernel_version, "5.3.1-r2")); + + unsigned progress = 0; + char message[128] = ""; + assert(installer_update_decode(&info, &progress, message, sizeof(message), + "progress\t40\tApplying signed packages") == 2); + assert(progress == 40 && !strcmp(message, "Applying signed packages")); + assert(installer_update_decode(&info, &progress, message, sizeof(message), + "progress\t101\tInvalid") == -EINVAL); + assert(installer_update_decode(&info, &progress, message, sizeof(message), + "info\tfree_bytes\t18446744073709551615") == 1); + assert(info.free_bytes == UINT64_MAX); + assert(installer_update_decode(&info, &progress, message, sizeof(message), + "info\tfree_bytes\t18446744073709551616") == -EINVAL); + + struct installer_update_checkpoint state = { + .phase = "packages", .root_uuid = "8ab5d176-6479-4ec8-b382-531fca6ea662", + .index_sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + .database_sha256 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + .world_sha256 = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + }, restored; + assert(!installer_update_read_checkpoint(root, &restored)); + assert(!restored.phase[0]); + assert(!installer_update_write_checkpoint(root, &state)); + assert(!installer_update_read_checkpoint(root, &restored)); + assert(!strcmp(restored.phase, "packages")); + assert(!strcmp(restored.root_uuid, state.root_uuid)); + snprintf(path, sizeof(path), "var/lib/reliefos/installer-update/.state-%ld", (long)getpid()); + write_file(root, path, "stale temporary from a previous boot\n"); + strcpy(state.phase, "boot"); + assert(!installer_update_write_checkpoint(root, &state)); + assert(!installer_update_read_checkpoint(root, &restored)); + assert(!strcmp(restored.phase, "boot")); + puts("PASS installer update metadata, protocol and durable retry state"); + return 0; +} diff --git a/tools/tests/runtime_apk_probe.c b/tools/tests/runtime_apk_probe.c new file mode 100644 index 00000000..ecf35020 --- /dev/null +++ b/tools/tests/runtime_apk_probe.c @@ -0,0 +1,72 @@ +/* Exercise the independent APK ABIs through the shipped, relocatable SDK. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int (*volatile tls_entry)(int, const char *, uint32_t, const void *, uint32_t, + const void *, uint32_t, char *, uint32_t, uint32_t *) = reliefos_tls_http_exchange; + +#define REQUIRE(expr) do { if (!(expr)) { fprintf(stderr, "APK_PROBE_FAIL line=%d %s\n", __LINE__, #expr); return 1; } } while (0) + +int main(int argc, char **argv) +{ + REQUIRE(argc == 2 && tls_entry != NULL); + const unsigned char input[] = "ReliefOS binary dependency roundtrip"; + unsigned char compressed[256], restored[256]; + uLongf compressed_size = sizeof(compressed), restored_size = sizeof(restored); + REQUIRE(compress2(compressed, &compressed_size, input, sizeof(input), 6) == Z_OK); + REQUIRE(uncompress(restored, &restored_size, compressed, compressed_size) == Z_OK); + REQUIRE(restored_size == sizeof(input) && !memcmp(input, restored, restored_size)); + + png_image image = {0}; + image.version = PNG_IMAGE_VERSION; image.width = image.height = 1; image.format = PNG_FORMAT_RGBA; + const unsigned char pixel[] = {17, 34, 51, 255}; + unsigned char png[512], decoded[4]; + png_alloc_size_t png_size = sizeof(png); + REQUIRE(png_image_write_to_memory(&image, png, &png_size, 0, pixel, 0, NULL)); + png_image_free(&image); memset(&image, 0, sizeof(image)); image.version = PNG_IMAGE_VERSION; + REQUIRE(png_image_begin_read_from_memory(&image, png, png_size)); + image.format = PNG_FORMAT_RGBA; + REQUIRE(png_image_finish_read(&image, NULL, decoded, 0, NULL)); + REQUIRE(!memcmp(decoded, pixel, sizeof(pixel))); png_image_free(&image); + + unsigned char key[32], cipher[sizeof(input)], plain[sizeof(input)], tag[16], nonce[12] = {0}; + REQUIRE(mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA256, input, sizeof(input), pixel, sizeof(pixel), 100, sizeof(key), key) == 0); + mbedtls_gcm_context gcm; mbedtls_gcm_init(&gcm); + REQUIRE(mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 256) == 0); + REQUIRE(mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, sizeof(input), nonce, sizeof(nonce), NULL, 0, input, cipher, sizeof(tag), tag) == 0); + REQUIRE(mbedtls_gcm_auth_decrypt(&gcm, sizeof(input), nonce, sizeof(nonce), NULL, 0, tag, sizeof(tag), cipher, plain) == 0); + REQUIRE(!memcmp(plain, input, sizeof(input))); tag[0] ^= 1; + REQUIRE(mbedtls_gcm_auth_decrypt(&gcm, sizeof(input), nonce, sizeof(nonce), NULL, 0, tag, sizeof(tag), cipher, plain) == MBEDTLS_ERR_GCM_AUTH_FAILED); + mbedtls_gcm_free(&gcm); + + sqlite3 *db = NULL, *other = NULL, *snapshot = NULL; + REQUIRE(sqlite3_open(argv[1], &db) == SQLITE_OK); + REQUIRE(sqlite3_exec(db, "PRAGMA mmap_size=0;PRAGMA journal_mode=DELETE;PRAGMA synchronous=FULL;CREATE TABLE IF NOT EXISTS probe(v);DELETE FROM probe;INSERT INTO probe VALUES(42);", NULL, NULL, NULL) == SQLITE_OK); + REQUIRE(sqlite3_open(argv[1], &other) == SQLITE_OK); + REQUIRE(sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) == SQLITE_OK); + REQUIRE(sqlite3_exec(other, "BEGIN IMMEDIATE", NULL, NULL, NULL) == SQLITE_BUSY); + REQUIRE(sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); + REQUIRE(sqlite3_close(other) == SQLITE_OK); + sqlite3_int64 size = 0; unsigned char *serialized = sqlite3_serialize(db, "main", &size, 0); + REQUIRE(serialized && size > 0); + REQUIRE(sqlite3_open(":memory:", &snapshot) == SQLITE_OK); + REQUIRE(sqlite3_deserialize(snapshot, "main", serialized, size, size, SQLITE_DESERIALIZE_FREEONCLOSE) == SQLITE_OK); + sqlite3_stmt *statement = NULL; + REQUIRE(sqlite3_prepare_v2(snapshot, "SELECT v FROM probe", -1, &statement, NULL) == SQLITE_OK); + REQUIRE(sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_int(statement, 0) == 42); + REQUIRE(sqlite3_finalize(statement) == SQLITE_OK); REQUIRE(sqlite3_close(snapshot) == SQLITE_OK); + REQUIRE(sqlite3_prepare_v2(db, "PRAGMA integrity_check", -1, &statement, NULL) == SQLITE_OK); + REQUIRE(sqlite3_step(statement) == SQLITE_ROW && !strcmp((const char *)sqlite3_column_text(statement, 0), "ok")); + REQUIRE(sqlite3_finalize(statement) == SQLITE_OK); REQUIRE(sqlite3_close(db) == SQLITE_OK); + REQUIRE(unlink(argv[1]) == 0); + printf("APK_PROBE_OK zlib=%s png=%s sqlite=%s crypto=pbkdf2+gcm+badtag lock=busy serialize=roundtrip\n", zlibVersion(), PNG_LIBPNG_VER_STRING, sqlite3_libversion()); + return 0; +} diff --git a/tools/tests/runtime_tls_probe.c b/tools/tests/runtime_tls_probe.c new file mode 100644 index 00000000..775990a0 --- /dev/null +++ b/tools/tests/runtime_tls_probe.c @@ -0,0 +1,27 @@ +/* Target-only TLS verification probe; sends no credentials or private data. */ +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + if (argc != 5) return 2; + int fd = reliefos_socket_tcp(); + struct reliefos_net_socket_connect connection = {0}; + if (fd < 0 || reliefos_socket_connect(fd, argv[1], (uint32_t)atoi(argv[3]), 15000, &connection) < 0) { + printf("TLS_PROBE_CONNECT_FAILED host=%s\n", argv[1]); + if (fd >= 0) close(fd); + return 2; + } + const char request[] = "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"; + char response[16384]; uint32_t length = 0; + int result = reliefos_tls_http_exchange(fd, argv[2], 15000, request, sizeof(request)-1, NULL, 0, response, sizeof(response), &length); + close(fd); + const int accepted = result == 0 && length > 0; + const int expected = !strcmp(argv[4], "accept"); + printf("TLS_PROBE host=%s expected=%s accepted=%d result=%d bytes=%u\n", argv[2], argv[4], accepted, result, length); + return accepted != expected; +} diff --git a/userland/apps/bootinstall/main.c b/userland/apps/bootinstall/main.c index e55c2894..3219fa8e 100644 --- a/userland/apps/bootinstall/main.c +++ b/userland/apps/bootinstall/main.c @@ -9,6 +9,7 @@ static void usage(void) puts("Usage: reliefos-boot-install --source DIR --boot-directory DIR [--disk DEVICE]"); puts(" [--bios=yes|no] [--uefi=yes|no] [--check] [--update]"); puts(" [--root-partuuid UUID] (default: the selected disk's unique GPT Linux root)"); + puts(" [--esp-directory DIR] (physical ESP for a per-instance --boot-directory)"); puts(" reliefos-boot-install --kernel-release RELEASE --root-directory DIR --boot-directory DIR [--check]"); puts("Kernel publication validates the complete installed package and retains the previous boot slot."); puts("BIOS requires a whole disk with a GPT BIOS Boot partition. Both modes default to yes."); @@ -32,6 +33,7 @@ int main(int argc, char **argv) .source = "/install/esp", .install_bios = 1, .install_uefi = 1, }; int check = 0; + const char *esp_directory = NULL; int firmware_options = 0; const char *kernel_release = NULL, *root_directory = "/"; for (int i = 1; i < argc; ++i) { @@ -46,13 +48,15 @@ int main(int argc, char **argv) if (mode_value(argv[i] + 7, &options.install_uefi)) { usage(); return 2; } } else if ((!strcmp(argv[i], "--source") || !strcmp(argv[i], "--boot-directory") || !strcmp(argv[i], "--disk") || !strcmp(argv[i], "--root-partuuid") || - !strcmp(argv[i], "--kernel-release") || !strcmp(argv[i], "--root-directory")) && i + 1 < argc) { + !strcmp(argv[i], "--kernel-release") || !strcmp(argv[i], "--root-directory") || + !strcmp(argv[i], "--esp-directory")) && i + 1 < argc) { const char *option = argv[i++]; if (!strcmp(option, "--source")) { options.source = argv[i]; firmware_options = 1; } else if (!strcmp(option, "--disk")) { options.disk_path = argv[i]; firmware_options = 1; } else if (!strcmp(option, "--root-partuuid")) { options.root_partuuid = argv[i]; firmware_options = 1; } else if (!strcmp(option, "--kernel-release")) kernel_release = argv[i]; else if (!strcmp(option, "--root-directory")) root_directory = argv[i]; + else if (!strcmp(option, "--esp-directory")) { esp_directory = argv[i]; firmware_options = 1; } else options.boot_dir = argv[i]; } else { usage(); return 2; } } @@ -67,10 +71,13 @@ int main(int argc, char **argv) puts(check ? "Kernel package checks passed." : "Matched kernel boot version published; reboot to activate."); return 0; } - if (!options.boot_dir || (options.install_bios && !options.disk_path)) { usage(); return 2; } - int result = check ? reliefos_boot_validate_payload(&options) : reliefos_boot_install_at_root(&options, root_directory); + if (!options.boot_dir || !options.disk_path) { usage(); return 2; } + int result = check ? reliefos_boot_validate_payload(&options) : esp_directory + ? reliefos_boot_install_instance_at_root(&options, root_directory, esp_directory) + : reliefos_boot_install_at_root(&options, root_directory); if (check && !result) result = reliefos_boot_validate_target(&options); if (check && !result) result = reliefos_boot_validate_root(&options, root_directory); + if (check && !result && esp_directory) result = reliefos_boot_validate_instance(&options, esp_directory); if (result) { fprintf(stderr, "reliefos-boot-install: %s (%d); incomplete stages are not a complete rollback\n", strerror(-result), result); diff --git a/userland/apps/fileman/boundary.c b/userland/apps/fileman/boundary.c index e685bb02..4b4af57e 100644 --- a/userland/apps/fileman/boundary.c +++ b/userland/apps/fileman/boundary.c @@ -50,7 +50,8 @@ const char *fileman_x11_default_app(const char *path) if (extension && !strcasecmp(extension, ".rmathpp")) return "/usr/bin/reliefmathpp"; if (extension && !strcasecmp(extension, ".rtodo")) return "/usr/bin/relieftodo"; if (extension && !strcasecmp(extension, ".rdiagram")) return "/usr/bin/reliefdiagram"; - if (extension && (!strcasecmp(extension, ".mp3") || !strcasecmp(extension, ".wav"))) + if (extension && (!strcasecmp(extension, ".mp3") || !strcasecmp(extension, ".wav") || + !strcasecmp(extension, ".mp4"))) return "/usr/bin/reliefplayer"; return "/usr/bin/nedit"; } diff --git a/userland/apps/installer/Kbuild b/userland/apps/installer/Kbuild index e0937085..89a24409 100644 --- a/userland/apps/installer/Kbuild +++ b/userland/apps/installer/Kbuild @@ -1,8 +1,12 @@ RELIEFOS_APP_NAME := installer -RELIEFOS_APP_OBJECTS := install_ops.o installer_capacity.o installer_partitions.o installer_setup.o installer_swap.o installer_swap_bundle.o installer_swap_config.o installer_swap_prepare.o installer_swap_storage.o installer_tty.o main.o model.o -RELIEFOS_APP_C_OBJECTS := install_ops.o installer_capacity.o installer_partitions.o installer_setup.o installer_swap.o installer_swap_bundle.o installer_swap_config.o installer_swap_prepare.o installer_swap_storage.o installer_tty.o model.o -RELIEFOS_APP_CXX_OBJECTS := main.o -RELIEFOS_APP_ASM_OBJECTS := +RELIEFOS_APP_OBJECTS := install_ops.o installer_capacity.o installer_partitions.o installer_setup.o installer_swap.o installer_swap_bundle.o installer_swap_config.o installer_swap_prepare.o installer_swap_storage.o installer_tty.o installer_update_engine.o installer_update_target.o installer_update_model.o installer_update_journal.o apache_license.o main.o model.o shared_reliefapk_process.o +RELIEFOS_APP_C_OBJECTS := install_ops.o installer_capacity.o installer_partitions.o installer_setup.o installer_swap.o installer_swap_bundle.o installer_swap_config.o installer_swap_prepare.o installer_swap_storage.o installer_tty.o installer_update_engine.o installer_update_target.o installer_update_model.o installer_update_journal.o model.o +RELIEFOS_APP_CXX_OBJECTS := main.o shared_reliefapk_process.o +RELIEFOS_APP_ASM_OBJECTS := apache_license.o ifneq ($(RELIEFOS_CAPTURE_APP_KBUILD),1) include $(srctree)/scripts/reliefos/Makefile.app-kbuild +AFLAGS_apache_license.o += -DINSTALLER_LICENSE_PATH='"$(srctree)/LICENSE"' +$(obj)/apache_license.o: $(srctree)/LICENSE +$(obj)/shared_reliefapk_process.o: $(srctree)/userland/apps/reliefapk/process.cpp FORCE + $(call if_changed_dep,cxx_o_cpp) endif diff --git a/userland/apps/installer/apache_license.S b/userland/apps/installer/apache_license.S new file mode 100644 index 00000000..b2429416 --- /dev/null +++ b/userland/apps/installer/apache_license.S @@ -0,0 +1,11 @@ +/* Embed the complete Apache License text from the repository. */ +.section .rodata.installer_license,"a",@progbits +.global installer_license_text +.hidden installer_license_text +.type installer_license_text,@object +installer_license_text: +.incbin INSTALLER_LICENSE_PATH +.byte 0 +.size installer_license_text, .-installer_license_text + +.section .note.GNU-stack,"",@progbits diff --git a/userland/apps/installer/install_ops.c b/userland/apps/installer/install_ops.c index 82fa2ff3..3a9a4cb1 100644 --- a/userland/apps/installer/install_ops.c +++ b/userland/apps/installer/install_ops.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -70,6 +71,12 @@ static uint8_t copy_buf[COPY_BUF_SIZE]; static uint8_t copy_defer_sync; static uint8_t copy_fresh_root; static char installer_root_uuid[37], installer_esp_uuid[37]; +int installer_boot_entry_path(char *path, uint32_t capacity) +{ + if (!path || !capacity || !installer_root_uuid[0]) return -EINVAL; + int n = snprintf(path, capacity, "/EFI/ReliefOS/%s/grubx64.efi", installer_root_uuid); + return n < 0 || (uint32_t)n >= capacity ? -ENAMETOOLONG : 0; +} static unsigned long boot_last_present; @@ -82,7 +89,7 @@ static void installer_boot_progress(void *context) } /** @brief Borrow current wizard choices for the common boot-install transaction. */ -static struct reliefos_boot_install_options installer_boot_options(int update) +static struct reliefos_boot_install_options installer_boot_options(void) { boot_last_present = 0; return (struct reliefos_boot_install_options){ @@ -90,7 +97,7 @@ static struct reliefos_boot_install_options installer_boot_options(int update) .disk_path = selected_disk >= 0 && (uint32_t)selected_disk < disk_count ? disks[selected_disk].path : NULL, .install_bios = !setup.skip_bios, .install_uefi = !setup.skip_uefi, - .preserve_rollback = update != 0, .progress = installer_boot_progress, + .preserve_rollback = 0, .progress = installer_boot_progress, .progress_context = &boot_last_present, .root_partuuid = installer_root_uuid, }; @@ -291,6 +298,7 @@ void refresh_partitions(void) installer_root_partition = -1; installer_esp_partition = -1; installer_partition_table_replaced = 0; + setup.format_esp = 0; partition_disk_path[0] = 0; if (selected_disk >= 0 && (uint32_t)selected_disk < disk_count) copy_text(partition_disk_path, sizeof(partition_disk_path), @@ -377,7 +385,8 @@ int installer_partition_plan_valid(void) partitions[i].sector_count >= 128ULL * 2048ULL; if ((int32_t)partitions[i].index == installer_esp_partition) esp_seen = partitions[i].gpt_type == RELIEFOS_BLOCK_GPT_ESP && - partitions[i].sector_count >= 128ULL * 2048ULL; + (setup.format_esp || partitions[i].filesystem == RELIEFOS_BLOCK_FILESYSTEM_FAT32) && + partitions[i].sector_count >= 2048u; } /* Rechecked after navigating back to firmware selection. No disk writes * here: adding BIOS to an existing two-part layout needs explicit changes. */ @@ -421,6 +430,7 @@ int installer_partition_auto_layout(void) refresh_partitions(); installer_partition_auto = 0; installer_partition_table_replaced = 1; + setup.format_esp = 1; installer_root_partition = (int32_t)plan.root.index; installer_esp_partition = (int32_t)plan.esp.index; } @@ -479,6 +489,7 @@ int installer_partition_set_type(uint32_t type) partition->index, type); if (!ret) { if (type == RELIEFOS_BLOCK_GPT_ESP) { + if (installer_esp_partition != index) setup.format_esp = 0; installer_esp_partition = index; if (installer_root_partition == index) installer_root_partition = -1; } else if (type == RELIEFOS_BLOCK_GPT_LINUX) { @@ -550,24 +561,22 @@ static int installer_format_ext4(const char *path) return 0; } -static int installer_target_partitions(const char *disk_path, int fresh, - char *esp_path, uint32_t esp_cap, - char *root_path, uint32_t root_cap, - uint32_t *root_filesystem) +static int installer_fresh_partitions(const char *disk_path, + char *esp_path, uint32_t esp_cap, + char *root_path, uint32_t root_cap, + uint32_t *root_filesystem) { struct reliefos_block_partition parts[RELIEFOS_BLOCK_MAX_PARTITIONS]; uint32_t count = 0, esp = UINT32_MAX, root = UINT32_MAX; int ret; if (!disk_path || !esp_path || !root_path || !root_filesystem) return -EINVAL; struct installer_capacity capacity = {0}; - if (fresh) { - ret = installer_capacity_measure(fresh_root_payload(), INSTALL_ESP_PAYLOAD, - !setup.skip_bios, !setup.skip_uefi, &capacity); - if (!ret && setup.swap_enabled) ret = installer_capacity_add_swap(&capacity,setup.swap_size_mib); - if (ret) return ret; - } + ret = installer_capacity_measure(fresh_root_payload(), INSTALL_ESP_PAYLOAD, + !setup.skip_bios, !setup.skip_uefi, &capacity); + if (!ret && setup.swap_enabled) ret = installer_capacity_add_swap(&capacity, setup.swap_size_mib); + if (ret) return ret; *root_filesystem = RELIEFOS_BLOCK_FILESYSTEM_UNKNOWN; - if (fresh && installer_partition_auto) { + if (installer_partition_auto) { struct reliefos_block_disk_info info; struct installer_partition_plan plan; ret = reliefos_block_get_info(disk_path, &info); @@ -594,55 +603,34 @@ static int installer_target_partitions(const char *disk_path, int fresh, printf("[installer.elf] block list partitions ret=%d count=%u disk=%s\n", ret, count, disk_path ? disk_path : "?"); if (ret < 0) return ret; - if (fresh) { - if (installer_root_partition < 0 || installer_esp_partition < 0) return -EINVAL; - for (uint32_t i = 0; i < count && i < RELIEFOS_BLOCK_MAX_PARTITIONS; ++i) { - if ((int32_t)parts[i].index == installer_esp_partition) esp = i; - if ((int32_t)parts[i].index == installer_root_partition) root = i; - } - if (esp == UINT32_MAX || root == UINT32_MAX || esp == root) return -ENOENT; - if (parts[esp].gpt_type != RELIEFOS_BLOCK_GPT_ESP || - parts[root].gpt_type != RELIEFOS_BLOCK_GPT_LINUX) return -EINVAL; - if (parts[esp].sector_count < 128ULL * 2048ULL || - parts[root].sector_count < 128ULL * 2048ULL) return -ENOSPC; - ret = installer_capacity_check(&capacity, parts[root].sector_count, parts[esp].sector_count); - if (ret) return ret; - copy_text(esp_path, esp_cap, parts[esp].path); - copy_text(root_path, root_cap, parts[root].path); - ret = reliefos_block_format(esp_path, RELIEFOS_BLOCK_FILESYSTEM_FAT32, "RELIEFOS"); - if (ret < 0) return ret; - *root_filesystem = RELIEFOS_BLOCK_FILESYSTEM_EXT4; - ret = installer_format_ext4(root_path); - if (!ret) ret = reliefos_block_partition_uuid(disk_path, parts[root].index, - installer_root_uuid); - if (!ret) ret = reliefos_block_partition_uuid(disk_path, parts[esp].index, - installer_esp_uuid); - return ret; - } + if (installer_root_partition < 0 || installer_esp_partition < 0) return -EINVAL; for (uint32_t i = 0; i < count && i < RELIEFOS_BLOCK_MAX_PARTITIONS; ++i) { - printf("[installer.elf] partition[%u] path=%s fs=%u gpt_type=%u\n", - i, parts[i].path, parts[i].filesystem, parts[i].gpt_type); - if (parts[i].gpt_type == RELIEFOS_BLOCK_GPT_ESP && - parts[i].filesystem == RELIEFOS_BLOCK_FILESYSTEM_FAT32 && esp == UINT32_MAX) esp = i; - if ((parts[i].filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXT2 || - parts[i].filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXT4 || - parts[i].filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXFAT) && root == UINT32_MAX) root = i; - } - if (esp == UINT32_MAX || root == UINT32_MAX) { - printf("[installer.elf] block partitions missing esp=%u root=%u\n", esp, root); - return -ENOENT; - } + if ((int32_t)parts[i].index == installer_esp_partition) esp = i; + if ((int32_t)parts[i].index == installer_root_partition) root = i; + } + if (esp == UINT32_MAX || root == UINT32_MAX || esp == root) return -ENOENT; + if (parts[esp].gpt_type != RELIEFOS_BLOCK_GPT_ESP || + parts[root].gpt_type != RELIEFOS_BLOCK_GPT_LINUX) return -EINVAL; + if (parts[esp].sector_count < 2048u || + parts[root].sector_count < 128ULL * 2048ULL) return -ENOSPC; + ret = installer_capacity_check(&capacity, parts[root].sector_count, parts[esp].sector_count); + if (ret) return ret; copy_text(esp_path, esp_cap, parts[esp].path); copy_text(root_path, root_cap, parts[root].path); - *root_filesystem = parts[root].filesystem; - ret = reliefos_block_partition_uuid(disk_path, parts[root].index, installer_root_uuid); - if (!ret) ret = reliefos_block_partition_uuid(disk_path, parts[esp].index, installer_esp_uuid); + if (setup.format_esp) { + ret = reliefos_block_format(esp_path, RELIEFOS_BLOCK_FILESYSTEM_FAT32, "RELIEFOS"); + if (ret < 0) return ret; + } else if (parts[esp].filesystem != RELIEFOS_BLOCK_FILESYSTEM_FAT32) return -EINVAL; + *root_filesystem = RELIEFOS_BLOCK_FILESYSTEM_EXT4; + ret = installer_format_ext4(root_path); + if (!ret) ret = reliefos_block_partition_uuid(disk_path, parts[root].index, + installer_root_uuid); + if (!ret) ret = reliefos_block_partition_uuid(disk_path, parts[esp].index, + installer_esp_uuid); return ret; } -static uint32_t installer_root_filesystem = RELIEFOS_BLOCK_FILESYSTEM_UNKNOWN; - -static int installer_mount_targets(const char *disk_path, int fresh) +static int installer_mount_targets(const char *disk_path) { char esp_path[RELIEFOS_BLOCK_PATH_LEN], root_path[RELIEFOS_BLOCK_PATH_LEN]; uint32_t root_filesystem = RELIEFOS_BLOCK_FILESYSTEM_UNKNOWN; @@ -664,9 +652,9 @@ static int installer_mount_targets(const char *disk_path, int fresh) printf("[installer.elf] mountpoint is not a directory path=%s\n", INSTALL_ROOT_MOUNT); return -ENOTDIR; } - /* Update inspection leaves these mounted. Release children first, before - * fresh installation rewrites the GPT or formats either filesystem. */ - const char *targets[] = {INSTALL_ESP_MOUNT, INSTALL_ROOT_MOUNT}; + /* Release mounts from a previous installation attempt before rewriting + * the GPT or formatting either selected filesystem. */ + const char *targets[] = {INSTALL_ESP_MOUNT, INSTALL_ESP_VOLUME, INSTALL_ROOT_MOUNT}; for (uint32_t i = 0; i < sizeof(targets) / sizeof(targets[0]); ++i) { if (umount2(targets[i], 0) < 0 && errno != ENOENT && errno != EINVAL) { ret = -errno; @@ -674,23 +662,18 @@ static int installer_mount_targets(const char *disk_path, int fresh) return ret; } } - ret = installer_target_partitions(disk_path, fresh, esp_path, sizeof(esp_path), + ret = installer_fresh_partitions(disk_path, esp_path, sizeof(esp_path), root_path, sizeof(root_path), &root_filesystem); if (ret < 0) { - printf("[installer.elf] mount target partitions failed ret=%d disk=%s fresh=%d\n", - ret, disk_path ? disk_path : "?", fresh); + printf("[installer.elf] mount target partitions failed ret=%d disk=%s\n", + ret, disk_path ? disk_path : "?"); return ret; } - installer_root_filesystem = root_filesystem; - printf("[installer.elf] mount targets disk=%s root=%s esp=%s fresh=%d root_fs=%u\n", - disk_path ? disk_path : "?", root_path, esp_path, fresh, - root_filesystem); - if (root_filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXFAT) { - root_fs_name = "exfat"; - } else if (root_filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXT2 || - root_filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXT4) { - root_fs_name = root_filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXT4 ? "ext4" : "ext2"; + printf("[installer.elf] mount targets disk=%s root=%s esp=%s root_fs=%u\n", + disk_path ? disk_path : "?", root_path, esp_path, root_filesystem); + if (root_filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXT4) { + root_fs_name = "ext4"; } else { /* An unknown root filesystem must not reach mount() with a null type. */ printf("[installer.elf] unsupported root filesystem fs=%u\n", root_filesystem); @@ -700,26 +683,51 @@ static int installer_mount_targets(const char *disk_path, int fresh) printf("[installer.elf] mount root failed path=%s errno=%d\n", root_path, errno); return -errno; } - if (mkdir(INSTALL_ESP_MOUNT, 0755) < 0 && errno != EEXIST) { + if ((mkdir(INSTALL_ESP_MOUNT, 0755) < 0 && errno != EEXIST) || + (mkdir(INSTALL_ESP_VOLUME, 0755) < 0 && errno != EEXIST)) { ret = -errno; printf("[installer.elf] mount esp mkdir failed path=%s errno=%d\n", INSTALL_ESP_MOUNT, errno); (void)umount2(INSTALL_ROOT_MOUNT, 0); return ret; } - if (mount(esp_path, INSTALL_ESP_MOUNT, "fat32", 0, NULL) < 0) { + if (mount(esp_path, INSTALL_ESP_VOLUME, "vfat", 0, NULL) < 0) { ret = -errno; printf("[installer.elf] mount esp failed path=%s errno=%d\n", esp_path, errno); (void)umount2(INSTALL_ROOT_MOUNT, 0); return ret; } + char instance[512]; + ret = reliefos_boot_prepare_instance(INSTALL_ESP_VOLUME, installer_root_uuid, instance, sizeof(instance)); + if (!ret && mount(instance, INSTALL_ESP_MOUNT, NULL, MS_BIND, NULL) < 0) ret = -errno; + if (ret) { + (void)umount2(INSTALL_ESP_VOLUME, 0); + (void)umount2(INSTALL_ROOT_MOUNT, 0); + return ret; + } + struct installer_capacity needed = {0}; + struct statvfs available; + ret = installer_capacity_measure(fresh_root_payload(), INSTALL_ESP_PAYLOAD, + !setup.skip_bios, !setup.skip_uefi, &needed); + if (!ret && statvfs(INSTALL_ESP_MOUNT, &available) < 0) ret = -errno; + if (!ret) { + uint64_t extra = 8u * 1024u * 1024u; + if (!setup.skip_uefi) { + struct stat loader; + if (stat(INSTALL_ESP_PAYLOAD "/EFI/BOOT/BOOTX64.EFI", &loader) < 0) ret = -errno; + else extra += (uint64_t)loader.st_size * 3u; + } + uint64_t unit = available.f_frsize ? available.f_frsize : available.f_bsize; + if (!ret && (!unit || (uint64_t)available.f_bavail > UINT64_MAX / unit)) ret = -EOVERFLOW; + if (!ret && (uint64_t)available.f_bavail * unit < needed.boot_bytes + extra) ret = -ENOSPC; + } + if (ret) return ret; return 0; } int confirmation_ok(void) { - return install_mode == INSTALL_MODE_UPDATE ? text_eq(confirm_text, "UPDATE") - : text_eq(confirm_text, "INSTALL"); + return install_mode == INSTALL_MODE_UPDATE || text_eq(confirm_text, "INSTALL"); } void refresh_disks(void) @@ -875,7 +883,6 @@ static int path_has_type(const char *path, uint32_t type) } /* Defined below: final-symlink-aware helpers used by traversal. */ -static int path_type_nofollow(const char *path); static int copy_symlink_path(const char *src, const char *dst); static int remove_path_recursive(const char *path) { @@ -1014,19 +1021,6 @@ static int copy_file_path(const char *src, const char *dst) return -error; } -/* Return the type of path without following a final symlink. */ -static int path_type_nofollow(const char *path) -{ - struct stat status; - if (!path || lstat(path, &status) < 0) { - return errno ? -errno : -2; - } - if (S_ISLNK(status.st_mode)) return RELIEFOS_FS_TYPE_SYMLINK; - if (S_ISDIR(status.st_mode)) return RELIEFOS_FS_TYPE_DIR; - if (S_ISREG(status.st_mode)) return RELIEFOS_FS_TYPE_FILE; - return RELIEFOS_FS_TYPE_DEVICE; -} - /* Recreate a symlink with the same literal target. The Alpine root * layout depends on symlinks staying symlinks across installation; copying * the target bytes as a directory or regular file is not equivalent. */ @@ -1184,7 +1178,7 @@ static int install_boot_after_root_sync(const char *root, int result = sync_target_root(root); if (result) return result; fprintf(stderr, "[installer.elf] root filesystem synchronized before boot publication\n"); - return reliefos_boot_install_at_root(boot, root); + return reliefos_boot_install_instance_at_root(boot, root, INSTALL_ESP_VOLUME); } /** @brief Keep the existing worker responsive while allocating the swapfile. @@ -1225,7 +1219,7 @@ static int copy_payload_ordered(void) root, INSTALL_ROOT_MOUNT, ret); return ret; } - struct reliefos_boot_install_options boot = installer_boot_options(0); + struct reliefos_boot_install_options boot = installer_boot_options(); ret = install_boot_after_root_sync(INSTALL_ROOT_MOUNT, &boot); if (ret < 0) { printf("[installer.elf] ESP payload copy failed src=%s dst=%s ret=%d\n", @@ -1273,67 +1267,6 @@ static int copy_payload_ordered(void) } } -static int check_update_target_required(void) -{ - /* Both brand namespaces use the same non-usr-merge filesystem contract. - * Accept a complete old namespace for migration, but never a usr-merged - * root or symlinked hierarchy. This preflight performs no writes. */ - static const char *const required_dirs[] = { - "/etc", "/var", "/var/lib", "/bin", "/sbin", "/lib", - "/usr", "/usr/bin", "/usr/sbin", "/usr/lib", - }; - for (uint32_t i = 0; i < sizeof(required_dirs) / sizeof(required_dirs[0]); ++i) { - char path[RELIEFOS_FS_PATH_LEN]; - if (path_join(path, sizeof(path), INSTALL_ROOT_MOUNT, required_dirs[i]) < 0) - return -ENAMETOOLONG; - if (path_type_nofollow(path) != RELIEFOS_FS_TYPE_DIR) { - set_status(T("Unsupported root layout; use a fresh ext4 installation"), path); - return -EINVAL; - } - } - { - static const char *const namespaces[][5] = { - {"/etc/reliefos", "/var/lib/reliefos", "/usr/lib/reliefos", - "/usr/lib/reliefos/apps", "/usr/lib/reliefos/libreliefos.so.2"}, - {"/etc/leonos", "/var/lib/leonos", "/usr/lib/leonos", - "/usr/lib/leonos/apps", "/usr/lib/leonos/libleonos.so.2"}, - }; - int found = 0; - for (uint32_t n = 0; n < sizeof(namespaces) / sizeof(namespaces[0]); ++n) { - int complete = 1; - for (uint32_t i = 0; i < 5; ++i) { - char path[RELIEFOS_FS_PATH_LEN]; - int required = i == 4 ? RELIEFOS_FS_TYPE_FILE : RELIEFOS_FS_TYPE_DIR; - if (path_join(path, sizeof(path), INSTALL_ROOT_MOUNT, namespaces[n][i]) < 0) - return -ENAMETOOLONG; - if (path_type_nofollow(path) != required) { complete = 0; break; } - } - if (complete) { found = 1; break; } - } - if (!found) { - set_status(T("Existing ReliefOS was not detected"), INSTALL_ROOT_MOUNT); - return -EINVAL; - } - } - { - static const char *const esp_dirs[] = {"boot"}; - for (uint32_t i = 0; i < sizeof(esp_dirs) / sizeof(esp_dirs[0]); ++i) { - char path[RELIEFOS_FS_PATH_LEN]; - if (path_join(path, sizeof(path), INSTALL_ROOT_MOUNT, esp_dirs[i]) < 0 || - path_has_type(path, RELIEFOS_FS_TYPE_DIR) < 0) { - set_status(T("Existing ReliefOS boot partition was not detected"), path); - return -2; - } - } - } - if (path_has_type(TARGET_ESP_KERNEL, RELIEFOS_FS_TYPE_FILE) < 0 && - path_has_type(TARGET_ESP_KERNEL_LEGACY, RELIEFOS_FS_TYPE_FILE) < 0) { - set_status(T("Existing ReliefOS boot partition was not detected"), TARGET_ESP_KERNEL); - return -2; - } - return 0; -} - /* Return the exact installed profile; the marker is data, never shell code. */ static int desktop_profile(const char *root) { @@ -1354,16 +1287,6 @@ static int desktop_profile(const char *root) return -EINVAL; } -static const char *update_root_payload(void) -{ - /* A missing legacy marker keeps the old desktop update path. Damaged or - * unreadable markers must not convert a console target back to Xorg. */ - int profile = desktop_profile(INSTALL_ROOT_MOUNT); - if (profile < 0 && profile != -ENOENT) return NULL; - return profile == 1 - ? INSTALL_CONSOLE_ROOT_PAYLOAD : INSTALL_ROOT_PAYLOAD; -} - static int check_payload_paths(const char *root, const char *const *paths, uint32_t count, int type) { @@ -1429,17 +1352,6 @@ static int check_profile_payload_required(const char *root, int skip_xorg) return ret; } -static int check_update_payload_required(void) -{ - const char *root = update_root_payload(); - if (!root) { - set_status(T("Existing system check failed"), - INSTALL_ROOT_MOUNT "/etc/reliefos/desktop-backend"); - return -EINVAL; - } - return check_profile_payload_required(root, !strcmp(root, INSTALL_CONSOLE_ROOT_PAYLOAD)); -} - static int check_install_payload_required(void) { return check_profile_payload_required(fresh_root_payload(), setup.skip_xorg); @@ -1494,155 +1406,6 @@ static void finish_install(int ret, } } -void perform_install(void); -void perform_update(void); -void prepare_update_target(void); - -/* Helpers for updates within the current rootfs layout. */ - -static int installer_mkdir_p(const char *path) -{ - char buffer[RELIEFOS_FS_PATH_LEN]; - uint32_t i; - if (!path || !path[0] || strlen(path) >= sizeof(buffer)) { - return path && path[0] ? -36 : -22; - } - copy_text(buffer, sizeof(buffer), path); - for (i = 1; buffer[i]; ++i) { - if (buffer[i] == '/') { - buffer[i] = 0; - int ret = installer_mkdir(buffer); - if (ret < 0 && ret != -17) { - return ret; - } - buffer[i] = '/'; - } - } - return installer_mkdir(buffer); -} - -/* Require real hierarchy components. The standard links are copied from - * the same layout table as fresh images; no old-root transformation occurs. */ -static int apply_runtime_root_paths(void) -{ - static const struct { const char *path; const char *target; } links[] = { -#define ROOT_LINK(path, target) {INSTALL_ROOT_MOUNT path, target}, - RELIEFOS_ROOTFS_SYMLINKS(ROOT_LINK) -#undef ROOT_LINK - }; - for (uint32_t i = 0; i < sizeof(links) / sizeof(links[0]); ++i) { - int type = path_type_nofollow(links[i].path); - if (type == -ENOENT) { - if (symlink(links[i].target, links[i].path) < 0) return -errno; - } else if (type == RELIEFOS_FS_TYPE_SYMLINK) { - char target[RELIEFOS_FS_PATH_LEN]; - ssize_t length = readlink(links[i].path, target, sizeof(target) - 1); - if (length < 0) return -errno; - target[length] = 0; - if (strcmp(target, links[i].target)) return -EEXIST; - } else { - return type < 0 ? type : -EEXIST; - } - } - return 0; -} -static int ensure_runtime_layout_dirs(void) -{ - static const struct { - const char *path; - uint32_t mode; - } dirs[] = { -#define ROOT_DIR(path, mode) {INSTALL_ROOT_MOUNT path, mode}, - RELIEFOS_ROOTFS_DIRECTORIES(ROOT_DIR) -#undef ROOT_DIR - }; - for (uint32_t i = 0; i < sizeof(dirs) / sizeof(dirs[0]); ++i) { - int ret = installer_mkdir_p(dirs[i].path); - if (ret < 0 && ret != -17) return ret; - if (chown(dirs[i].path, 0, 0) < 0 || - chmod(dirs[i].path, dirs[i].mode) < 0) return -errno; - } - return 0; -} - -/* Copy new payload paths over an existing tree without deleting unrelated - * target entries. System files are refreshed; user-created commands and - * packages survive. */ -static int sync_system_payload(void) -{ - pid_t child; - char repository[RELIEFOS_FS_PATH_LEN]; - const char *root = update_root_payload(); - if (!root) return -EINVAL; - if (path_join(repository, sizeof(repository), root, - "/usr/share/reliefos/apk/repository") < 0) return -ENAMETOOLONG; - char *const argv[] = {"sh", "/usr/lib/reliefos/reliefos-apk-update", INSTALL_ROOT_MOUNT, - repository, NULL}; - char *const envp[] = {"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "LC_ALL=C", "HOME=/root", NULL}; - int ret = posix_spawn(&child, "/bin/sh", NULL, NULL, argv, envp); - if (ret) return -ret; - int status; - for (;;) { - pid_t done = waitpid(child, &status, WNOHANG); - if (done == child) break; - if (done < 0 && errno != EINTR) return -errno; - refresh_ui(); - usleep(50000); - } - printf("[installer.elf] APK update wait_status=%d\n", status); - return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : -EIO; -} -void prepare_update_target(void) -{ - int ret; - if (selected_disk < 0 || (uint32_t)selected_disk >= disk_count) { - return; - } - page = PAGE_PROGRESS; - progress_value = 0; - show_progress(5, - T("Mounting target filesystems"), - T("Root: /target; ESP: /target/boot")); - ret = installer_mount_targets(disks[selected_disk].path, 0); - if (ret < 0) { - finish_install(ret, T("Mount failed")); - return; - } - if (reliefos_account_legacy_check(INSTALL_ROOT_MOUNT) < 0) { - finish_install(-errno, - T("Legacy accounts require recovery before updating")); - return; - } - uint32_t modes; - ret = reliefos_boot_probe(disks[selected_disk].path, INSTALL_ESP_MOUNT, &modes); - if (ret < 0) { finish_install(ret, T("Boot installation checks failed")); return; } - setup.skip_bios = !(modes & RELIEFOS_BOOT_BIOS); - setup.skip_uefi = !(modes & RELIEFOS_BOOT_UEFI); - show_progress(18, - T("Checking existing ReliefOS"), - T("Target: /target")); - ret = check_update_payload_required(); - if (ret < 0) { - finish_install(ret, T("Payload check failed")); - return; - } - ret = check_update_target_required(); - if (ret < 0) { - finish_install(ret, T("Existing system check failed")); - return; - } - if (path_type_nofollow(INSTALL_ROOT_MOUNT "/lib/apk/db/installed") != RELIEFOS_FS_TYPE_FILE) { - finish_install(-ENOENT, - T("No APK database: reinstall from this media")); - return; - } - set_status(T("Update installed ReliefOS packages"), - T("Alpine packages and local configuration are preserved.")); - page = PAGE_BOOTLOADERS; - reset_confirm(); - refresh_ui(); -} - void perform_install(void) { int ret; @@ -1679,14 +1442,14 @@ void perform_install(void) if (ret) {finish_install(ret,T("Swap requires an AHCI disk"));return;} } - struct reliefos_boot_install_options boot = installer_boot_options(0); + struct reliefos_boot_install_options boot = installer_boot_options(); ret = reliefos_boot_validate_payload(&boot); if (!ret) ret = reliefos_boot_validate_disk(&boot, !installer_partition_auto); if (ret < 0) { finish_install(ret, T("Boot installation checks failed")); return; } show_progress(2, "Preparing target disk", ""); - show_progress(22, "Mounting target filesystems", "Root: /target (ext4 for new installs; ext4 or exFAT for updates), ESP: /target/boot (FAT32)"); - ret = installer_mount_targets(disks[selected_disk].path, 1); + show_progress(22, "Mounting target filesystems", "Root: /target (ext4), ESP: /target/boot (FAT32)"); + ret = installer_mount_targets(disks[selected_disk].path); if (ret < 0) { finish_install(ret, "Mount failed"); return; @@ -1727,74 +1490,3 @@ void perform_install(void) show_progress(100, T("Installation completed successfully"), T("Target disk is ready.")); finish_install(0, ""); } - -void perform_update(void) -{ - int ret; - if (selected_disk < 0 || (uint32_t)selected_disk >= disk_count) { - return; - } - install_running = 1; - install_success = 0; - page = PAGE_PROGRESS; - copy_total = 0; - copy_done = 0; - copy_total_bytes = 0; - copy_done_bytes = 0; - - show_progress(2, T("Mounting target filesystems"), - T("Root: /target; ESP: /target/boot")); - ret = installer_mount_targets(disks[selected_disk].path, 0); - if (ret < 0) { - finish_install(ret, T("Mount failed")); - return; - } - - show_progress(10, T("Checking existing ReliefOS"), - T("Target: /target")); - ret = check_update_payload_required(); - if (ret < 0) { - finish_install(ret, T("Payload check failed")); - return; - } - ret = check_update_target_required(); - if (ret < 0) { - finish_install(ret, T("Existing system check failed")); - return; - } - if (installer_root_filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXFAT) { - /* The current root contract needs real symlinks; exFAT cannot store - * them, so an update would silently produce an unusable namespace. - * Refuse instead of materializing directory copies. */ - finish_install(-38, - T("exFAT root cannot carry the current symlink layout; use a fresh ext4 install")); - return; - } - - struct reliefos_boot_install_options boot = installer_boot_options(1); - ret = reliefos_boot_validate_payload(&boot); - if (!ret) ret = reliefos_boot_validate_target(&boot); - if (ret < 0) { finish_install(ret, T("Boot installation checks failed")); return; } - - ret = ensure_runtime_layout_dirs(); - if (ret >= 0) ret = apply_runtime_root_paths(); - if (ret < 0) { - finish_install(ret, T("Invalid root layout")); - return; - } - - show_progress(35, T("Upgrading signed ReliefOS packages"), - T("Checking dependencies and preserving local configuration")); - ret = sync_system_payload(); - if (ret < 0) { - finish_install(ret, T("Core update failed")); - return; - } - - ret = install_boot_after_root_sync(INSTALL_ROOT_MOUNT, &boot); - if (ret < 0) { finish_install(ret, T("Boot update failed")); return; } - - show_progress(100, T("Update completed successfully"), - T("Target disk is ready.")); - finish_install(0, ""); -} diff --git a/userland/apps/installer/install_ops.h b/userland/apps/installer/install_ops.h index 93c41f18..31d05950 100644 --- a/userland/apps/installer/install_ops.h +++ b/userland/apps/installer/install_ops.h @@ -1,4 +1,4 @@ -/* Installer install/update operations shared by the FLTK and TTY frontends. */ +/* Fresh installation operations and wizard state shared by both frontends. */ #ifndef RELIEFOS_INSTALLER_OPS_H #define RELIEFOS_INSTALLER_OPS_H @@ -32,6 +32,7 @@ extern "C" { #define INSTALL_ESP_PAYLOAD "/install/esp" #define INSTALL_ROOT_MOUNT "/target" #define INSTALL_ESP_MOUNT "/target/boot" +#define INSTALL_ESP_VOLUME "/target/efi" /* Target-root path contract. The installer payload uses the same relative * paths as the guest root, so each payload path is INSTALL_ROOT_PAYLOAD plus @@ -105,6 +106,7 @@ void format_partition_line(char *buf, uint32_t cap, const struct reliefos_block_partition *partition); int installer_partition_plan_valid(void); uint32_t installer_boot_modes(void); +int installer_boot_entry_path(char *path, uint32_t capacity); int installer_partition_initialize(void); int installer_partition_auto_layout(void); int installer_partition_create(uint32_t filesystem, uint32_t size_mib, @@ -116,9 +118,7 @@ int installer_partition_delete(void); int installer_partition_format(uint32_t filesystem, const char *label); /* Long-running operations; they drive `page` to PROGRESS then FINISH. */ -void prepare_update_target(void); void perform_install(void); -void perform_update(void); #ifdef __cplusplus } diff --git a/userland/apps/installer/installer_setup.c b/userland/apps/installer/installer_setup.c index 0907271a..3c827866 100644 --- a/userland/apps/installer/installer_setup.c +++ b/userland/apps/installer/installer_setup.c @@ -11,7 +11,7 @@ int installer_setup_valid(const struct installer_setup *setup) { - if (!setup || setup->skip_xorg > 1 || setup->skip_bios > 1 || setup->skip_uefi > 1) return 0; + if (!setup || setup->skip_xorg > 1 || setup->skip_bios > 1 || setup->skip_uefi > 1 || setup->format_esp > 1) return 0; if (!reliefos_account_name_valid(setup->username, sizeof(setup->username)) || !strcmp(setup->username, "root") || !strcmp(setup->username, "nobody") || !strcmp(setup->username, "wheel")) return 0; diff --git a/userland/apps/installer/installer_setup.h b/userland/apps/installer/installer_setup.h index 6bcb83c1..ca34eab4 100644 --- a/userland/apps/installer/installer_setup.h +++ b/userland/apps/installer/installer_setup.h @@ -13,6 +13,7 @@ struct installer_setup { uint8_t skip_xorg; uint8_t skip_bios; uint8_t skip_uefi; + uint8_t format_esp; bool swap_enabled; uint64_t swap_size_mib; }; diff --git a/userland/apps/installer/installer_swap_config.c b/userland/apps/installer/installer_swap_config.c index 9ce4999c..17aa517c 100644 --- a/userland/apps/installer/installer_swap_config.c +++ b/userland/apps/installer/installer_swap_config.c @@ -179,8 +179,9 @@ int installer_swap_configure(const char *root,const char *root_uuid,const char * int n=snprintf(fstab,sizeof(fstab), "# \n" "/dev/disk/by-partuuid/%s / ext4 defaults 0 1\n" - "/dev/disk/by-partuuid/%s /boot vfat defaults 0 2\n%s", - root_uuid,esp_uuid,enabled ? "/swapfile none swap sw 0 0\n" : ""); + "/dev/disk/by-partuuid/%s /efi vfat defaults 0 2\n" + "/efi/reliefos/instances/%s /boot none bind 0 0\n%s", + root_uuid,esp_uuid,root_uuid,enabled ? "/swapfile none swap sw 0 0\n" : ""); if (n<0 || (size_t)n>=sizeof(fstab)) return -EOVERFLOW; int rootfd=-1,etc=-1,conf=-1,levels=-1,run=-1,rc=0; bool published=false; diff --git a/userland/apps/installer/installer_tty.c b/userland/apps/installer/installer_tty.c index 536979f5..0c19bf8c 100644 --- a/userland/apps/installer/installer_tty.c +++ b/userland/apps/installer/installer_tty.c @@ -10,6 +10,7 @@ #include #include "installer_tty.h" +#include "installer_update.h" #include "installer_capacity.h" #include "model.h" @@ -255,8 +256,17 @@ static int tty_partition_menu(const struct installer_tty_context *context) for (;;) { tty_print_partitions(context); puts("Commands: auto, init, create SIZE FS NAME, resize SIZE, rename NAME,"); - puts("format FS LABEL, bios, esp, root, data, delete, refresh, done. FS raw creates an unformatted partition."); + puts("format FS LABEL, format-esp yes|no, bios, esp, root, data, delete, refresh, done. FS raw creates an unformatted partition."); + printf("ESP: %s\n", context->setup->format_esp ? "format (erase EFI files)" : "preserve existing FAT32 files"); if (!tty_read_line("partition> ", input, sizeof(input))) return 0; + if (tty_line_is(input, "format-esp yes")) { + context->setup->format_esp = 1; + continue; + } + if (tty_line_is(input, "format-esp no")) { + context->setup->format_esp = 0; + continue; + } if (tty_line_is(input, "done")) { if (context->partition_plan_valid()) return 1; puts(context->setup->skip_bios @@ -312,6 +322,12 @@ static int tty_partition_menu(const struct installer_tty_context *context) } } +static void tty_update_progress(void *context, unsigned progress, const char *message) +{ + (void)context; + printf("[%3u%%] %s\n", progress, T(message)); +} + int installer_tty_main(const struct installer_tty_context *context) { char input[32]; @@ -326,8 +342,7 @@ int installer_tty_main(const struct installer_tty_context *context) !context->partition_create || !context->partition_resize || !context->partition_rename || !context->partition_set_type || !context->partition_delete || !context->partition_format || - !context->prepare_update || - !context->perform_install || !context->perform_update) { + !context->perform_install) { return 1; } @@ -356,17 +371,51 @@ int installer_tty_main(const struct installer_tty_context *context) } if (*context->install_mode == INSTALLER_TTY_MODE_UPDATE) { - context->prepare_update(); - if (*context->page == INSTALLER_PAGE_FINISH && !*context->install_success) { + struct installer_update_info inspected, result; + const char *disk = context->disks[*context->selected_disk].path; + char root_uuid[37] = {0}, esp_uuid[37] = {0}; + context->refresh_partitions(); + tty_print_partitions(context); + char *end; + for (int role = 0; role < 2; ++role) { + if (!tty_read_line(role ? T("ESP partition number (blank: use fstab): ") + : T("ReliefOS root partition number (blank: auto): "), input, sizeof(input))) return 1; + if (!input[0]) continue; + errno = 0; + unsigned long index = strtoul(input, &end, 10); + if (errno || *end || index >= *context->partition_count) return 1; + const struct reliefos_block_partition *part = &context->partitions[index]; + if (part->gpt_type != (role ? RELIEFOS_BLOCK_GPT_ESP : RELIEFOS_BLOCK_GPT_LINUX)) return 1; + if (reliefos_block_partition_uuid(disk, part->index, role ? esp_uuid : root_uuid)) return 1; + } + int error = installer_update_check_target(disk, root_uuid, esp_uuid, &inspected, tty_update_progress, NULL); + if (error) { + fprintf(stderr, "%s: %s\n", T(inspected.failure), strerror(-error)); return 1; } + printf("\n%s: %s -> %s\n%s: %s\n%s: %s\n%s: %s\n", + T("System version"), inspected.installed_version, inspected.media_version, + T("Target disk"), inspected.disk, T("System partition"), inspected.root_device, + T("Boot partition"), inspected.esp_device); + printf("%s: %llu MiB / %llu MiB\n", T("Free / required space"), + (unsigned long long)(inspected.free_bytes / (1024u * 1024u)), + (unsigned long long)((inspected.required_bytes + 1024u * 1024u - 1) / (1024u * 1024u))); + puts(T("Accounts, personal files, installed software and local configuration are preserved.")); + context->setup->skip_bios = !(inspected.boot_modes & 1u); + context->setup->skip_uefi = !(inspected.boot_modes & 2u); if (!tty_bootloaders(context->setup)) return 1; - if (!tty_read_line("Type UPDATE to confirm an in-place update: ", - input, sizeof(input)) || !tty_line_is(input, "UPDATE")) { + if (!tty_read_line(T("Proceed with system update? [y/N]: "), input, sizeof(input)) || + (!tty_line_is(input, "y") && !tty_line_is(input, "Y") && !tty_line_is(input, "yes"))) { puts("Update not confirmed. Installation cancelled."); return 1; } - context->perform_update(); + error = installer_update_apply(&inspected, + (context->setup->skip_bios ? 0u : 1u) | (context->setup->skip_uefi ? 0u : 2u), + &result, tty_update_progress, NULL); + if (!error && !context->setup->skip_uefi) printf("UEFI: %s\n", result.boot_file); + *context->install_success = error == 0; + *context->page = INSTALLER_PAGE_FINISH; + if (error) fprintf(stderr, "%s: %s\n", T(result.failure), strerror(-error)); } else { if (!tty_bootloaders(context->setup)) return 1; context->refresh_partitions(); @@ -383,12 +432,19 @@ int installer_tty_main(const struct installer_tty_context *context) return 1; } if (!installer_tty_select_swap(context->setup)) return 1; + puts(*context->partition_auto ? T("Automatic installation erases the selected disk.") : + context->setup->format_esp ? T("The selected root and ESP will be formatted.") : + T("Only the selected root is formatted; existing ESP files are preserved.")); if (!tty_read_line("Type INSTALL to confirm the partition layout and installation: ", input, sizeof(input)) || !tty_line_is(input, "INSTALL")) { puts("Installation not confirmed. Installation cancelled."); return 1; } context->perform_install(); + if (*context->install_success && !context->setup->skip_uefi && context->boot_entry_path) { + char entry[128]; + if (!context->boot_entry_path(entry, sizeof(entry))) printf("UEFI: %s\n", entry); + } } if (!*context->install_success) { diff --git a/userland/apps/installer/installer_tty.h b/userland/apps/installer/installer_tty.h index c551acde..6f3789dc 100644 --- a/userland/apps/installer/installer_tty.h +++ b/userland/apps/installer/installer_tty.h @@ -45,9 +45,8 @@ struct installer_tty_context { int (*partition_set_type)(uint32_t type); int (*partition_delete)(void); int (*partition_format)(uint32_t filesystem, const char *label); - void (*prepare_update)(void); void (*perform_install)(void); - void (*perform_update)(void); + int (*boot_entry_path)(char *path, uint32_t capacity); }; #ifdef __cplusplus diff --git a/userland/apps/installer/installer_update.h b/userland/apps/installer/installer_update.h new file mode 100644 index 00000000..b29c6ea7 --- /dev/null +++ b/userland/apps/installer/installer_update.h @@ -0,0 +1,65 @@ +#ifndef RELIEFOS_INSTALLER_UPDATE_H +#define RELIEFOS_INSTALLER_UPDATE_H + +#include +#include + +/* Immutable inspection results travel from the worker to either frontend. + * Applying an update requires the exact inspected disk, partition identities, + * signed index and installed database, so a stale confirmation cannot retarget. */ +struct installer_update_info { + char disk[64], root_device[64], esp_device[64]; + char boot_file[128]; + char root_uuid[37], esp_uuid[37]; + char installed_version[64], media_version[64], profile[16]; + char installed_kernel[96], media_kernel[96]; + char index_sha256[65], database_sha256[65], world_sha256[65], pending[32]; + char failure[128]; + uint64_t free_bytes, required_bytes; + uint32_t boot_modes; + int error_number; +}; + +struct installer_update_packages { + uint64_t installed_bytes, largest_package; + char kernel_version[96]; +}; + +struct installer_update_checkpoint { + char phase[32], root_uuid[37], index_sha256[65], database_sha256[65], world_sha256[65]; +}; + +#ifdef __cplusplus +extern "C" { +#endif + +/* Read data files without sourcing shell text or following hierarchy links. + * All functions return zero or a negative errno. */ +int installer_update_read_system(const char *root, struct installer_update_info *info); +int installer_update_read_media(const char *root, const char *profile, struct installer_update_info *info); +int installer_update_read_packages(const char *root, struct installer_update_packages *packages); +int installer_update_read_checkpoint(const char *root, struct installer_update_checkpoint *checkpoint); +int installer_update_write_checkpoint(const char *root, const struct installer_update_checkpoint *checkpoint); + +typedef void (*installer_update_observer)(void *context, unsigned progress, const char *message); +int installer_update_check(const char *disk, struct installer_update_info *info, + installer_update_observer observer, void *context); +int installer_update_check_target(const char *disk, const char *root_uuid, const char *esp_uuid, + struct installer_update_info *info, + installer_update_observer observer, void *context); +int installer_update_apply(const struct installer_update_info *expected, unsigned boot_modes, + struct installer_update_info *result, + installer_update_observer observer, void *context); + +/* Decode a complete bounded worker line. Returns 1 for metadata, 2 for + * progress, 0 for an unrelated line, or a negative errno for invalid data. */ +int installer_update_decode(struct installer_update_info *info, unsigned *progress, + char *message, size_t capacity, const char *line); + +/* Run the isolated native backend before any FLTK initialization. */ +int installer_update_worker(int argc, char **argv); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/userland/apps/installer/installer_update_engine.c b/userland/apps/installer/installer_update_engine.c new file mode 100644 index 00000000..fa7c7f94 --- /dev/null +++ b/userland/apps/installer/installer_update_engine.c @@ -0,0 +1,454 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif +#include "installer_update_internal.h" +#include "../../auth/standard_accounts.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define N_(text) (text) + +#define UPDATE_ESP "/install/esp" +#define UPDATE_PACKAGES "/usr/share/reliefos/apk/repository" +#define UPDATE_HELPER "/usr/lib/reliefos/reliefos-apk-update" +#define UPDATE_LOG "/var/log/installer-update.log" + +struct update_run { + installer_update_observer observer; + void *context; + FILE *log; + unsigned progress; + const char *stage; + struct timespec presented; +}; + +static void report(struct update_run *run, unsigned progress, const char *stage) +{ + run->progress = progress; + run->stage = stage; + fprintf(stderr, "[installer-update] %u%% %s\n", progress, stage); + if (run->log) { fprintf(run->log, "[installer-update] %u%% %s\n", progress, stage); fflush(run->log); } + if (run->observer) run->observer(run->context, progress, stage); +} + +/* Each command is owned by the isolated worker. APK output is streamed to + * the frontend's log pipe and the installer session's persistent log. */ +static int command(struct update_run *run, const char *const arguments[], char *output, size_t capacity) +{ + int descriptors[2]; + if (pipe(descriptors) < 0) return -errno; + if (fcntl(descriptors[0], F_SETFD, FD_CLOEXEC) < 0 || fcntl(descriptors[1], F_SETFD, FD_CLOEXEC) < 0) { + int result = -errno; + close(descriptors[0]); close(descriptors[1]); + return result; + } + posix_spawn_file_actions_t actions; + int result = posix_spawn_file_actions_init(&actions); + if (result) { close(descriptors[0]); close(descriptors[1]); return -result; } + result = posix_spawn_file_actions_addclose(&actions, descriptors[0]); + if (!result) result = posix_spawn_file_actions_adddup2(&actions, descriptors[1], STDOUT_FILENO); + if (!result) result = posix_spawn_file_actions_adddup2(&actions, descriptors[1], STDERR_FILENO); + if (!result) result = posix_spawn_file_actions_addclose(&actions, descriptors[1]); + char *const environment[] = {"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "LC_ALL=C", "LANG=C", "HOME=/root", NULL}; + pid_t child = -1; + if (!result) result = posix_spawn(&child, arguments[0], &actions, NULL, + (char *const *)arguments, environment); + posix_spawn_file_actions_destroy(&actions); + close(descriptors[1]); + if (result) { close(descriptors[0]); return -result; } + size_t used = 0; + char buffer[4096]; + int error = 0; + for (;;) { + ssize_t length = read(descriptors[0], buffer, sizeof(buffer)); + if (length < 0 && errno == EINTR) continue; + if (length < 0) { error = -errno; break; } + if (!length) break; + if (output) { + if ((size_t)length >= capacity - used) error = -EOVERFLOW; + else { memcpy(output + used, buffer, (size_t)length); used += (size_t)length; } + } else { + (void)fwrite(buffer, 1, (size_t)length, stderr); + if (run->log && fwrite(buffer, 1, (size_t)length, run->log) != (size_t)length) error = -EIO; + } + } + close(descriptors[0]); + if (output) output[used] = 0; + if (run->log && fflush(run->log) && !error) error = -errno; + int status; + while (waitpid(child, &status, 0) < 0) { + if (errno == EINTR) continue; + return -errno; + } + if (error) return error; + if (!WIFEXITED(status) || WEXITSTATUS(status)) { + fprintf(stderr, "[installer-update] command %s wait_status=%d\n", arguments[0], status); + return -EPROTO; + } + return 0; +} + +static int hash_file(struct update_run *run, const char *path, char digest[65]) +{ + const char *const arguments[] = {"/bin/busybox", "sha256sum", path, NULL}; + char output[PATH_MAX + 80]; + int result = command(run, arguments, output, sizeof(output)); + if (result) return result; + if (strlen(output) < 66 || output[64] != ' ' || strspn(output, "0123456789abcdef") != 64) return -EPROTO; + memcpy(digest, output, 64); + digest[64] = 0; + return 0; +} + +static int sync_root(const char *root) +{ + int fd = open(root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return -errno; + int result = syncfs(fd) < 0 ? -errno : 0; + if (close(fd) < 0 && !result) result = -errno; + return result; +} + +static int package_state(struct update_run *run, const char *root, struct installer_update_info *info) +{ + char path[PATH_MAX]; + int length = snprintf(path, sizeof(path), "%s/lib/apk/db/installed", root); + if (length < 0 || (size_t)length >= sizeof(path)) return -ENAMETOOLONG; + int result = hash_file(run, path, info->database_sha256); + if (result) return result; + length = snprintf(path, sizeof(path), "%s/etc/apk/world", root); + if (length < 0 || (size_t)length >= sizeof(path)) return -ENAMETOOLONG; + return hash_file(run, path, info->world_sha256); +} + +static int repository_bytes(const char *repository, uint64_t *bytes) +{ + DIR *directory = opendir(repository); + if (!directory) return -errno; + struct dirent *entry; + int result = 0; + *bytes = 0; + errno = 0; + while ((entry = readdir(directory))) { + size_t length = strlen(entry->d_name); + if (length < 4 || strcmp(entry->d_name + length - 4, ".apk")) continue; + struct stat status; + if (fstatat(dirfd(directory), entry->d_name, &status, AT_SYMLINK_NOFOLLOW) < 0) { result = -errno; break; } + if (!S_ISREG(status.st_mode) || status.st_size < 0) { result = -EINVAL; break; } + if (UINT64_MAX - *bytes < (uint64_t)status.st_size) { result = -EOVERFLOW; break; } + *bytes += (uint64_t)status.st_size; + errno = 0; + } + if (!result && errno) result = -errno; + if (closedir(directory) < 0 && !result) result = -errno; + return result; +} + +static int check_media(struct update_run *run, struct installer_update_target *target, + const char *media, char repository[PATH_MAX]) +{ + struct installer_update_info source = {0}; + struct installer_update_packages installed, incoming; + int result = installer_update_read_media(media, target->info.profile, &source); + if (!result) result = installer_update_read_packages(target->root, &installed); + if (!result) result = installer_update_read_packages(media, &incoming); + if (result) return result; + if (!incoming.kernel_version[0]) return -ENOENT; + strcpy(target->info.media_version, source.installed_version); + strcpy(target->info.installed_kernel, installed.kernel_version); + strcpy(target->info.media_kernel, incoming.kernel_version); + const char *old_version = installed.kernel_version[0] ? installed.kernel_version : target->info.installed_version; + const char *new_version = installed.kernel_version[0] ? incoming.kernel_version : source.installed_version; + const char *const compare[] = {"/sbin/apk", "version", "-t", old_version, new_version, NULL}; + char relation[16]; + result = command(run, compare, relation, sizeof(relation)); + if (result) return result; + if (!strcmp(relation, ">\n")) { + report(run, run->progress, N_("Installation media is older than the installed system")); + return -EPERM; + } + if (strcmp(relation, "<\n") && strcmp(relation, "=\n")) return -EPROTO; + int length = snprintf(repository, PATH_MAX, "%s%s", media, UPDATE_PACKAGES); + if (length < 0 || length >= PATH_MAX) return -ENAMETOOLONG; + char path[PATH_MAX]; + length = snprintf(path, sizeof(path), "%s/packages.adb", repository); + if (length < 0 || (size_t)length >= sizeof(path)) return -ENAMETOOLONG; + result = hash_file(run, path, target->info.index_sha256); + if (result) return result; + result = package_state(run, target->root, &target->info); + if (result) return result; + struct statvfs space; + if (statvfs(target->root, &space) < 0) return -errno; + uint64_t unit = space.f_frsize ? space.f_frsize : space.f_bsize; + if (!unit || space.f_bavail > UINT64_MAX / unit) return -EOVERFLOW; + target->info.free_bytes = (uint64_t)space.f_bavail * unit; + uint64_t archives; + result = repository_bytes(repository, &archives); + if (result) return result; + uint64_t growth = incoming.installed_bytes > installed.installed_bytes + ? incoming.installed_bytes - installed.installed_bytes : 0; + uint64_t reserve = 64u * 1024u * 1024u; + if (UINT64_MAX - reserve < archives || UINT64_MAX - reserve - archives < growth || + UINT64_MAX - reserve - archives - growth < incoming.largest_package) return -EOVERFLOW; + target->info.required_bytes = reserve + archives + growth + incoming.largest_package; + return target->info.free_bytes < target->info.required_bytes ? -ENOSPC : 0; +} + +static void boot_progress(void *context) +{ + struct update_run *run = context; + struct timespec now; + if (!clock_gettime(CLOCK_MONOTONIC, &now) && + (now.tv_sec > run->presented.tv_sec || now.tv_nsec - run->presented.tv_nsec >= 500000000L)) { + run->presented = now; + if (run->observer) run->observer(run->context, run->progress, run->stage); + } +} + +static int save_checkpoint(struct installer_update_target *target, struct installer_update_checkpoint *state, + const char *phase) +{ + snprintf(state->phase, sizeof(state->phase), "%s", phase); + strcpy(state->root_uuid, target->info.root_uuid); + strcpy(state->index_sha256, target->info.index_sha256); + strcpy(state->database_sha256, target->info.database_sha256); + strcpy(state->world_sha256, target->info.world_sha256); + return installer_update_write_checkpoint(target->root, state); +} + +static int recover_pending(struct update_run *run, struct installer_update_target *target, + const struct installer_update_checkpoint *checkpoint) +{ + if (!checkpoint->phase[0] || !strcmp(checkpoint->phase, "done")) return 0; + report(run, 32, N_("Recovering the previous update attempt")); + const char *const restore[] = {"/usr/libexec/reliefos/kernel-bundle", "--restore-all", target->root, NULL}; + int result = command(run, restore, NULL, 0); + if (result) return result; + char path[PATH_MAX], text[96]; + snprintf(path, sizeof(path), "%s/run/reliefos-kernel-activation.defer", target->root); + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return errno == ENOENT ? 0 : -errno; + struct stat status; + if (fstat(fd, &status) < 0) { result = -errno; close(fd); return result; } + if (!S_ISREG(status.st_mode) || status.st_uid || status.st_nlink != 1 || status.st_size >= (off_t)sizeof(text)) { + close(fd); + return -EINVAL; + } + size_t used = 0; + while (used < sizeof(text) - 1) { + ssize_t length = read(fd, text + used, sizeof(text) - 1 - used); + if (length < 0 && errno == EINTR) continue; + if (length < 0) { result = -errno; break; } + if (!length) break; + used += (size_t)length; + } + if (close(fd) < 0 && !result) result = -errno; + if (result) return result; + text[used] = 0; + if (strcmp(text, "ReliefOS installer package transaction\n")) return -EBUSY; + return unlink(path) < 0 ? -errno : 0; +} + +static int execute(const char *disk, const struct installer_update_info *expected, unsigned modes, + struct installer_update_info *out, installer_update_observer observer, void *context, + const char *root_uuid, const char *esp_uuid) +{ + struct installer_update_target target; + struct installer_update_checkpoint checkpoint = {0}; + struct update_run run = {.observer = observer, .context = context, .stage = N_("Checking update target")}; + int applying = expected != NULL; + memset(out, 0, sizeof(*out)); + if (geteuid()) { out->error_number = EACCES; strcpy(out->failure, N_("Administrator privileges are required")); return -EACCES; } + int log_fd = open(UPDATE_LOG, O_WRONLY | O_APPEND | O_CREAT | O_NOFOLLOW | O_CLOEXEC, 0600); + if (log_fd >= 0) { + run.log = fdopen(log_fd, "a"); + if (!run.log) { int saved = errno; close(log_fd); errno = saved; } + } + if (!run.log) { + out->error_number = errno; + strcpy(out->failure, N_("Writing the update log")); + return -out->error_number; + } + report(&run, 2, N_("Identifying the installed system")); + int result = installer_update_target_open(&target, disk, expected ? expected->root_uuid : root_uuid, + expected ? expected->esp_uuid : esp_uuid); + if (result) goto done; + if (reliefos_account_legacy_check(target.root) < 0) { result = -errno; goto done; } + const char *media = !strcmp(target.info.profile, "console") ? "/install/root-console" : "/install/root"; + char repository[PATH_MAX]; + report(&run, 10, N_("Checking installation media and free space")); + result = check_media(&run, &target, media, repository); + if (result) goto done; + result = installer_update_read_checkpoint(target.root, &checkpoint); + if (result) goto done; + if (checkpoint.phase[0] && strcmp(checkpoint.phase, "done")) { + if (strcmp(checkpoint.root_uuid, target.info.root_uuid)) { result = -EXDEV; goto done; } + strcpy(target.info.pending, checkpoint.phase); + } + if (expected && (strcmp(expected->disk, target.info.disk) || strcmp(expected->esp_uuid, target.info.esp_uuid) || + strcmp(expected->root_uuid, target.info.root_uuid) || strcmp(expected->index_sha256, target.info.index_sha256) || + strcmp(expected->database_sha256, target.info.database_sha256) || + strcmp(expected->world_sha256, target.info.world_sha256))) { + report(&run, run.progress, N_("Target or media changed. Check the update again.")); + result = -ESTALE; + goto done; + } + struct reliefos_boot_install_options boot = { + .source = UPDATE_ESP, .boot_dir = target.boot, .disk_path = disk, + .install_bios = ((applying ? modes : target.info.boot_modes) & RELIEFOS_BOOT_BIOS) != 0, + .install_uefi = ((applying ? modes : target.info.boot_modes) & RELIEFOS_BOOT_UEFI) != 0, + .preserve_rollback = 1, .root_partuuid = target.info.root_uuid, + .progress = boot_progress, .progress_context = &run, + }; + report(&run, 16, N_("Checking bootloader compatibility")); + result = target.namespaced ? reliefos_boot_validate_instance(&boot, target.esp) : 0; + if (!result) result = reliefos_boot_validate_payload(&boot); + if (!result) result = reliefos_boot_validate_target(&boot); + if (result) goto done; + report(&run, 22, N_("Verifying signed packages and dependencies")); + const char *const check[] = {"/bin/sh", UPDATE_HELPER, "--check", target.root, repository, NULL}; + result = command(&run, check, NULL, 0); + if (result) goto done; + if (!applying) { report(&run, 100, N_("Update checks completed")); goto done; } + + int resume_boot = (!strcmp(checkpoint.phase, "root-ready") || !strcmp(checkpoint.phase, "boot") || + !strcmp(checkpoint.phase, "boot-failed")) && + !strcmp(checkpoint.root_uuid, target.info.root_uuid) && + !strcmp(checkpoint.index_sha256, target.info.index_sha256) && + !strcmp(checkpoint.database_sha256, target.info.database_sha256) && + !strcmp(checkpoint.world_sha256, target.info.world_sha256); + report(&run, 30, N_("Preparing the installed system for update")); + result = installer_update_target_writable(&target); + if (result) goto done; + result = recover_pending(&run, &target, &checkpoint); + if (result) goto done; + if (!resume_boot) { + result = save_checkpoint(&target, &checkpoint, "packages"); + if (result) goto done; + report(&run, 38, N_("Applying signed system packages")); + const char *const apply[] = {"/bin/sh", UPDATE_HELPER, "--apply", target.root, repository, target.info.index_sha256, NULL}; + result = command(&run, apply, NULL, 0); + if (result) { (void)save_checkpoint(&target, &checkpoint, "packages-failed"); goto done; } + result = package_state(&run, target.root, &target.info); + if (result) goto done; + report(&run, 78, N_("Synchronizing the updated root filesystem")); + result = sync_root(target.root); + if (!result) result = save_checkpoint(&target, &checkpoint, "root-ready"); + if (result) goto done; + } + report(&run, 86, resume_boot ? N_("Resuming bootloader publication") : N_("Publishing kernel and bootloader files")); + result = save_checkpoint(&target, &checkpoint, "boot"); + if (!result) result = target.namespaced + ? reliefos_boot_install_instance_at_root(&boot, target.root, target.esp) + : reliefos_boot_install_at_root(&boot, target.root); + if (result) { (void)save_checkpoint(&target, &checkpoint, "boot-failed"); goto done; } + result = sync_root(target.root); + if (!result) result = save_checkpoint(&target, &checkpoint, "done"); + if (!result) report(&run, 98, N_("Finishing the system update")); +done: + *out = target.info; + int cleanup = installer_update_target_close(&target); + if (!result) result = cleanup; + if (result) { + out->error_number = -result; + snprintf(out->failure, sizeof(out->failure), "%s", run.stage); + fprintf(stderr, "[installer-update] %s failed with %s (%d)\n", run.stage, strerror(-result), result); + if (run.log) fprintf(run.log, "[installer-update] %s failed with %s (%d)\n", run.stage, strerror(-result), result); + } else if (applying) report(&run, 100, N_("System update completed successfully")); + if (run.log && fclose(run.log) && !result) { + result = -errno; + out->error_number = -result; + strcpy(out->failure, N_("Writing the update log")); + } + return result; +} + +int installer_update_check(const char *disk, struct installer_update_info *info, + installer_update_observer observer, void *context) +{ + if (!disk || !info) { + if (info) { memset(info, 0, sizeof(*info)); info->error_number = EINVAL; } + return -EINVAL; + } + return execute(disk, NULL, 0, info, observer, context, NULL, NULL); +} + +int installer_update_check_target(const char *disk, const char *root_uuid, const char *esp_uuid, + struct installer_update_info *info, + installer_update_observer observer, void *context) +{ + if (!disk || !info || (root_uuid && *root_uuid && strlen(root_uuid) != 36) || + (esp_uuid && *esp_uuid && strlen(esp_uuid) != 36)) return -EINVAL; + return execute(disk, NULL, 0, info, observer, context, root_uuid, esp_uuid); +} + +int installer_update_apply(const struct installer_update_info *expected, unsigned modes, + struct installer_update_info *result, + installer_update_observer observer, void *context) +{ + if (!expected || !result || !expected->root_uuid[0] || !expected->esp_uuid[0] || + strlen(expected->index_sha256) != 64 || strlen(expected->database_sha256) != 64 || + strlen(expected->world_sha256) != 64 || modes > 3) { + if (result) { memset(result, 0, sizeof(*result)); result->error_number = EINVAL; } + return -EINVAL; + } + return execute(expected->disk, expected, modes, result, observer, context, NULL, NULL); +} + +static void worker_progress(void *context, unsigned progress, const char *message) +{ + (void)context; + printf("progress\t%u\t%s\n", progress, message); + fflush(stdout); +} + +static void worker_info(const struct installer_update_info *info) +{ +#define FIELD(name) printf("info\t%s\t%s\n", #name, info->name) + FIELD(disk); FIELD(root_device); FIELD(esp_device); FIELD(root_uuid); FIELD(esp_uuid); + FIELD(boot_file); + FIELD(installed_version); FIELD(media_version); FIELD(profile); FIELD(installed_kernel); + FIELD(media_kernel); FIELD(index_sha256); FIELD(database_sha256); FIELD(world_sha256); FIELD(pending); +#undef FIELD + printf("info\tfree_bytes\t%llu\ninfo\trequired_bytes\t%llu\ninfo\tboot_modes\t%u\n", + (unsigned long long)info->free_bytes, (unsigned long long)info->required_bytes, info->boot_modes); +} + +int installer_update_worker(int argc, char **argv) +{ + struct installer_update_info info = {0}; + int result; + setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1); + if (argc == 4 && !strcmp(argv[2], "check")) { + result = installer_update_check(argv[3], &info, worker_progress, NULL); + } else if (argc == 6 && !strcmp(argv[2], "check")) { + result = installer_update_check_target(argv[3], argv[4], argv[5], &info, worker_progress, NULL); + } else if (argc == 10 && !strcmp(argv[2], "apply")) { + struct installer_update_info expected = {0}; + if (strlen(argv[3]) >= sizeof(expected.disk) || strlen(argv[4]) != 36 || strlen(argv[5]) != 36 || + strlen(argv[6]) != 64 || strlen(argv[7]) != 64 || strlen(argv[8]) != 64 || + strlen(argv[9]) != 1 || *argv[9] < '0' || *argv[9] > '3') return 2; + strcpy(expected.disk, argv[3]); strcpy(expected.root_uuid, argv[4]); strcpy(expected.esp_uuid, argv[5]); + strcpy(expected.index_sha256, argv[6]); strcpy(expected.database_sha256, argv[7]); + strcpy(expected.world_sha256, argv[8]); + result = installer_update_apply(&expected, (unsigned)(*argv[9] - '0'), &info, worker_progress, NULL); + } else return 2; + worker_info(&info); + if (result) printf("error\t%d\t%s\n", -result, info.failure[0] ? info.failure : N_("Update worker failed")); + fflush(stdout); + return result ? 1 : 0; +} diff --git a/userland/apps/installer/installer_update_internal.h b/userland/apps/installer/installer_update_internal.h new file mode 100644 index 00000000..68b5b619 --- /dev/null +++ b/userland/apps/installer/installer_update_internal.h @@ -0,0 +1,18 @@ +#ifndef RELIEFOS_INSTALLER_UPDATE_INTERNAL_H +#define RELIEFOS_INSTALLER_UPDATE_INTERNAL_H +#include "installer_update.h" + +struct installer_update_target { + struct installer_update_info info; + char workspace[256], root[256], boot[256], esp[256]; + const char *filesystem; + int lock_fd, root_mounted, boot_mounted, esp_mounted, namespaced; +}; + +/* Only the worker owns these mounts and the advisory installer lock. */ +int installer_update_target_open(struct installer_update_target *target, + const char *disk, const char *expected_root, const char *expected_esp); +int installer_update_target_writable(struct installer_update_target *target); +int installer_update_target_close(struct installer_update_target *target); + +#endif diff --git a/userland/apps/installer/installer_update_journal.c b/userland/apps/installer/installer_update_journal.c new file mode 100644 index 00000000..d47004eb --- /dev/null +++ b/userland/apps/installer/installer_update_journal.c @@ -0,0 +1,120 @@ +#define _POSIX_C_SOURCE 200809L +#include "installer_update.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static int state_directory(const char *root, int create) +{ + static const char *const components[] = {"var", "lib", "reliefos", "installer-update"}; + int fd = open(root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return -errno; + for (size_t i = 0; i < sizeof(components) / sizeof(components[0]); ++i) { + if (create && i >= 2) { + int made = mkdirat(fd, components[i], i == 3 ? 0700 : 0755); + if ((made < 0 && errno != EEXIST) || (!made && fsync(fd) < 0)) { + int result = -errno; + close(fd); + return result; + } + } + int child = openat(fd, components[i], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + int result = child < 0 ? -errno : 0; + close(fd); + if (result) return result; + fd = child; + } + return fd; +} + +static int valid_field(const char *text, size_t capacity) +{ + return memchr(text, 0, capacity) && text[0] && + strspn(text, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_") == strlen(text); +} + +int installer_update_write_checkpoint(const char *root, const struct installer_update_checkpoint *state) +{ + if (!root || !state || !valid_field(state->phase, sizeof(state->phase)) || + !valid_field(state->root_uuid, sizeof(state->root_uuid)) || + !valid_field(state->index_sha256, sizeof(state->index_sha256)) || + !valid_field(state->database_sha256, sizeof(state->database_sha256)) || + !valid_field(state->world_sha256, sizeof(state->world_sha256))) return -EINVAL; + int directory = state_directory(root, 1); + if (directory < 0) return directory; + char path[PATH_MAX]; + int length = snprintf(path, sizeof(path), "%s/var/lib/reliefos/installer-update/.state-XXXXXX", root); + if (length < 0 || (size_t)length >= sizeof(path)) { close(directory); return -ENAMETOOLONG; } + int fd = mkstemp(path); + if (fd < 0) { int result = -errno; close(directory); return result; } + const char *temporary = strrchr(path, '/') + 1; + if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) { + int result = -errno; + close(fd); + (void)unlinkat(directory, temporary, 0); + close(directory); + return result; + } + FILE *stream = fdopen(fd, "w"); + int result = stream ? 0 : -errno; + if (stream) { + if (fprintf(stream, "format=1\nphase=%s\nroot_uuid=%s\nindex_sha256=%s\ndatabase_sha256=%s\nworld_sha256=%s\n", + state->phase, state->root_uuid, state->index_sha256, state->database_sha256, state->world_sha256) < 0 || + fflush(stream) || fsync(fd)) result = -errno; + if (fclose(stream) && !result) result = -errno; + } else close(fd); + if (!result && renameat(directory, temporary, directory, "state") < 0) result = -errno; + if (!result && fsync(directory) < 0) result = -errno; + if (result) (void)unlinkat(directory, temporary, 0); + if (close(directory) < 0 && !result) result = -errno; + return result; +} + +int installer_update_read_checkpoint(const char *root, struct installer_update_checkpoint *state) +{ + if (!root || !state) return -EINVAL; + memset(state, 0, sizeof(*state)); + int directory = state_directory(root, 0); + if (directory == -ENOENT) return 0; + if (directory < 0) return directory; + int fd = openat(directory, "state", O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + int result = fd < 0 ? -errno : 0; + close(directory); + if (result) return result == -ENOENT ? 0 : result; + FILE *stream = fdopen(fd, "r"); + if (!stream) { result = -errno; close(fd); return result; } + char line[160]; + unsigned fields = 0; + while (fgets(line, sizeof(line), stream)) { + if (!strchr(line, '\n')) { result = -EINVAL; break; } + line[strcspn(line, "\n")] = 0; + char *value = strchr(line, '='); + if (!value) { result = -EINVAL; break; } + *value++ = 0; + char *out = NULL; + size_t capacity = 0; + unsigned bit = 0; + if (!strcmp(line, "format") && !strcmp(value, "1")) bit = 1; + else if (!strcmp(line, "phase")) { out = state->phase; capacity = sizeof(state->phase); bit = 2; } + else if (!strcmp(line, "root_uuid")) { out = state->root_uuid; capacity = sizeof(state->root_uuid); bit = 4; } + else if (!strcmp(line, "index_sha256")) { out = state->index_sha256; capacity = sizeof(state->index_sha256); bit = 8; } + else if (!strcmp(line, "database_sha256")) { out = state->database_sha256; capacity = sizeof(state->database_sha256); bit = 16; } + else if (!strcmp(line, "world_sha256")) { out = state->world_sha256; capacity = sizeof(state->world_sha256); bit = 32; } + else { result = -EINVAL; break; } + if ((fields & bit) || (out && (strlen(value) >= capacity || !valid_field(value, strlen(value) + 1)))) { + result = -EINVAL; + break; + } + if (out) strcpy(out, value); + fields |= bit; + } + if (!result && (fields != 63 || ferror(stream))) result = -EINVAL; + if (fclose(stream) && !result) result = -errno; + return result; +} diff --git a/userland/apps/installer/installer_update_model.c b/userland/apps/installer/installer_update_model.c new file mode 100644 index 00000000..67c0ea43 --- /dev/null +++ b/userland/apps/installer/installer_update_model.c @@ -0,0 +1,270 @@ +#define _POSIX_C_SOURCE 200809L +#include "installer_update.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static int join_path(char *path, size_t capacity, const char *root, const char *relative) +{ + int length = snprintf(path, capacity, "%s/%s", root, relative); + return length < 0 || (size_t)length >= capacity ? -ENAMETOOLONG : 0; +} + +static int copy_field(char *out, size_t capacity, const char *value) +{ + size_t length = strlen(value); + if (length >= capacity || strpbrk(value, "\r\n\t")) return -EINVAL; + memcpy(out, value, length + 1); + return 0; +} + +static int parse_number(const char *text, uint64_t *number) +{ + char *end; + if (!*text || *text < '0' || *text > '9') return -EINVAL; + errno = 0; + unsigned long long value = strtoull(text, &end, 10); + if (errno || *end) return -EINVAL; + *number = value; + return 0; +} + +static int unquote(char *value) +{ + size_t length = strlen(value); + if (length && (value[0] == '"' || value[0] == '\'')) { + if (length < 2 || value[length - 1] != value[0]) return -EINVAL; + memmove(value, value + 1, length - 2); + value[length - 2] = 0; + } + if (!*value || strspn(value, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._+-") != strlen(value)) + return -EINVAL; + return 0; +} + +/* os-release permits the standard vendor-file symlink. Resolve that one + * inside the mounted target instead of following an absolute host path. */ +static FILE *open_release(const char *root) +{ + char path[PATH_MAX], link[PATH_MAX]; + struct stat status; + int result = join_path(path, sizeof(path), root, "etc/os-release"); + if (result) { errno = -result; return NULL; } + if (lstat(path, &status) < 0) return NULL; + if (S_ISLNK(status.st_mode)) { + ssize_t length = readlink(path, link, sizeof(link) - 1); + if (length < 0) return NULL; + link[length] = 0; + if (strcmp(link, "../usr/lib/os-release") && strcmp(link, "/usr/lib/os-release")) { + errno = EINVAL; + return NULL; + } + result = join_path(path, sizeof(path), root, "usr/lib/os-release"); + if (result) { errno = -result; return NULL; } + } + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return NULL; + FILE *stream = fdopen(fd, "r"); + if (!stream) { int saved = errno; close(fd); errno = saved; } + return stream; +} + +int installer_update_read_system(const char *root, struct installer_update_info *info) +{ + static const char *const directories[] = { + "etc", "etc/apk", "lib", "lib/apk", "lib/apk/db", "var", "var/lib", + "usr", "usr/bin", "usr/lib", "usr/share", "bin", "sbin", "boot", + }; + char path[PATH_MAX], line[512], id[32] = ""; + struct stat status; + if (!root || !info) return -EINVAL; + for (size_t i = 0; i < sizeof(directories) / sizeof(directories[0]); ++i) { + int result = join_path(path, sizeof(path), root, directories[i]); + if (result) return result; + if (lstat(path, &status) < 0) return -errno; + if (!S_ISDIR(status.st_mode)) return -ENOTDIR; + } + static const char *const namespaces[] = { + "etc/reliefos", "etc/leonos", "var/lib/reliefos", "var/lib/leonos", + "usr/lib/reliefos", "usr/lib/leonos", + }; + for (size_t i = 0; i < sizeof(namespaces) / sizeof(namespaces[0]); ++i) { + int error = join_path(path, sizeof(path), root, namespaces[i]); + if (error) return error; + if (lstat(path, &status) < 0) { if (errno == ENOENT) continue; return -errno; } + if (!S_ISDIR(status.st_mode)) return -ENOTDIR; + } + FILE *stream = open_release(root); + if (!stream) return -errno; + int result = 0; + info->installed_version[0] = 0; + while (fgets(line, sizeof(line), stream)) { + if (!strchr(line, '\n') && !feof(stream)) { result = -EOVERFLOW; break; } + line[strcspn(line, "\r\n")] = 0; + char *value = strchr(line, '='); + if (!value) continue; + *value++ = 0; + if (strcmp(line, "ID") && strcmp(line, "VERSION_ID")) continue; + result = unquote(value); + if (!result) result = !strcmp(line, "ID") + ? copy_field(id, sizeof(id), value) + : copy_field(info->installed_version, sizeof(info->installed_version), value); + if (result) break; + } + if (ferror(stream) && !result) result = -EIO; + if (fclose(stream) && !result) result = -errno; + if (result) return result; + if (strcmp(id, "reliefos") && strcmp(id, "leonos")) return -ENODEV; + if (!info->installed_version[0]) return -EINVAL; + if (join_path(path, sizeof(path), root, "etc/reliefos/desktop-backend")) return -ENAMETOOLONG; + int fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0 && errno == ENOENT) { + if (join_path(path, sizeof(path), root, "etc/leonos/desktop-backend")) return -ENAMETOOLONG; + fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + } + if (fd < 0) { + if (errno != ENOENT) return -errno; + strcpy(info->profile, "xorg"); + return 0; + } + size_t used = 0; + while (used < sizeof(line) - 1) { + ssize_t length = read(fd, line + used, sizeof(line) - 1 - used); + if (length < 0 && errno == EINTR) continue; + if (length < 0) { result = -errno; break; } + if (!length) break; + used += (size_t)length; + } + if (close(fd) < 0 && !result) result = -errno; + if (result) return result; + if ((used == 4 && !memcmp(line, "xorg", 4)) || + (used == 5 && !memcmp(line, "xorg\n", 5))) strcpy(info->profile, "xorg"); + else if ((used == 7 && !memcmp(line, "console", 7)) || + (used == 8 && !memcmp(line, "console\n", 8))) strcpy(info->profile, "console"); + else return -EINVAL; + return 0; +} + +int installer_update_read_media(const char *root, const char *profile, struct installer_update_info *info) +{ + if (!root || !profile || !info) return -EINVAL; + int result = installer_update_read_system(root, info); + if (result) return result; + if (strcmp(profile, info->profile)) return -EINVAL; + const char *const metadata[] = {"etc/apk/world", "lib/apk/db/installed", + "usr/share/reliefos/apk/repository/packages.adb"}; + for (size_t i = 0; i < sizeof(metadata) / sizeof(metadata[0]); ++i) { + char path[PATH_MAX]; + struct stat status; + result = join_path(path, sizeof(path), root, metadata[i]); + if (result) return result; + if (lstat(path, &status) < 0) return -errno; + if (!S_ISREG(status.st_mode) || status.st_size <= 0) return -EINVAL; + } + return 0; +} + +static int package_record(struct installer_update_packages *out, const char *name, + const char *version, uint64_t bytes) +{ + if (strncmp(name, "reliefos-", 9) && strncmp(name, "leonos-", 7)) return 0; + if (UINT64_MAX - out->installed_bytes < bytes) return -EOVERFLOW; + out->installed_bytes += bytes; + if (out->largest_package < bytes) out->largest_package = bytes; + if (!strcmp(name, "reliefos-kernel") || !strcmp(name, "leonos-kernel")) + return copy_field(out->kernel_version, sizeof(out->kernel_version), version); + return 0; +} + +int installer_update_read_packages(const char *root, struct installer_update_packages *out) +{ + char path[PATH_MAX], line[8192], name[128] = "", version[96] = ""; + uint64_t bytes = 0; + if (!root || !out) return -EINVAL; + memset(out, 0, sizeof(*out)); + int result = join_path(path, sizeof(path), root, "lib/apk/db/installed"); + if (result) return result; + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return -errno; + FILE *stream = fdopen(fd, "r"); + if (!stream) { result = -errno; close(fd); return result; } + while (fgets(line, sizeof(line), stream)) { + if (!strchr(line, '\n') && !feof(stream)) { result = -EOVERFLOW; break; } + line[strcspn(line, "\r\n")] = 0; + if (!line[0]) { + result = package_record(out, name, version, bytes); + name[0] = version[0] = 0; + bytes = 0; + } else if (!strncmp(line, "P:", 2)) result = copy_field(name, sizeof(name), line + 2); + else if (!strncmp(line, "V:", 2)) result = copy_field(version, sizeof(version), line + 2); + else if (!strncmp(line, "I:", 2)) result = parse_number(line + 2, &bytes); + if (result) break; + } + if (!result) result = ferror(stream) ? -EIO : package_record(out, name, version, bytes); + if (fclose(stream) && !result) result = -errno; + return result; +} + +int installer_update_decode(struct installer_update_info *info, unsigned *progress, + char *message, size_t capacity, const char *line) +{ + if (!info || !progress || !message || !capacity || !line) return -EINVAL; + if (!strncmp(line, "error\t", 6)) { + char *end; + errno = 0; + long value = strtol(line + 6, &end, 10); + if (errno || value < 1 || value > 4095 || *end != '\t') return -EINVAL; + int result = copy_field(message, capacity, end + 1); + if (!result) result = copy_field(info->failure, sizeof(info->failure), end + 1); + if (result) return result; + info->error_number = (int)value; + return 3; + } + struct field { const char *key; char *value; size_t size; }; +#define FIELD(name) {#name, info->name, sizeof(info->name)} + struct field fields[] = { + FIELD(disk), FIELD(root_device), FIELD(esp_device), FIELD(root_uuid), FIELD(esp_uuid), + FIELD(boot_file), + FIELD(installed_version), FIELD(media_version), FIELD(profile), FIELD(installed_kernel), + FIELD(media_kernel), FIELD(index_sha256), FIELD(database_sha256), FIELD(world_sha256), FIELD(pending), + }; +#undef FIELD + if (!strncmp(line, "progress\t", 9)) { + const char *tab = strchr(line + 9, '\t'); + if (!tab || (size_t)(tab - line - 9) >= 4) return -EINVAL; + char text[4]; + memcpy(text, line + 9, (size_t)(tab - line - 9)); + text[tab - line - 9] = 0; + uint64_t value; + int result = parse_number(text, &value); + if (result || value > 100) return -EINVAL; + result = copy_field(message, capacity, tab + 1); + if (result) return result; + *progress = (unsigned)value; + return 2; + } + if (strncmp(line, "info\t", 5)) return 0; + const char *tab = strchr(line + 5, '\t'); + if (!tab) return -EINVAL; + size_t length = (size_t)(tab - line - 5); + for (size_t i = 0; i < sizeof(fields) / sizeof(fields[0]); ++i) { + if (strlen(fields[i].key) == length && !memcmp(fields[i].key, line + 5, length)) { + int result = copy_field(fields[i].value, fields[i].size, tab + 1); + return result ? result : 1; + } + } + uint64_t value; + int result = parse_number(tab + 1, &value); + if (result) return result; + if (length == 10 && !memcmp(line + 5, "free_bytes", length)) info->free_bytes = value; + else if (length == 14 && !memcmp(line + 5, "required_bytes", length)) info->required_bytes = value; + else if (length == 10 && !memcmp(line + 5, "boot_modes", length) && value <= 3) info->boot_modes = (uint32_t)value; + else return -EINVAL; + return 1; +} diff --git a/userland/apps/installer/installer_update_target.c b/userland/apps/installer/installer_update_target.c new file mode 100644 index 00000000..707e6873 --- /dev/null +++ b/userland/apps/installer/installer_update_target.c @@ -0,0 +1,258 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif +#include "installer_update_internal.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int mounted_elsewhere(const char *path) +{ + struct stat device; + if (stat(path, &device) < 0) return -errno; + if (!S_ISBLK(device.st_mode)) return -ENOTBLK; + FILE *stream = setmntent("/proc/self/mounts", "r"); + if (!stream) return -errno; + int result = 0; + struct mntent *entry; + while ((entry = getmntent(stream))) { + struct stat source; + if (!strcmp(entry->mnt_fsname, path) || + (!stat(entry->mnt_fsname, &source) && S_ISBLK(source.st_mode) && source.st_rdev == device.st_rdev)) { + result = -EBUSY; + break; + } + } + if (!result && ferror(stream)) result = -EIO; + endmntent(stream); + return result; +} + +static int acquire_lock(void) +{ + struct stat status; + if (mkdir("/run/reliefos", 0755) < 0 && errno != EEXIST) return -errno; + if (lstat("/run/reliefos", &status) < 0) return -errno; + if (!S_ISDIR(status.st_mode) || status.st_uid || (status.st_mode & 0022)) return -EPERM; + int fd = open("/run/reliefos/installer-update.lock", O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC, 0600); + if (fd < 0) return -errno; + if (fstat(fd, &status) < 0) { + int result = -errno; + close(fd); + return result; + } + if (!S_ISREG(status.st_mode) || status.st_uid || status.st_nlink != 1) { close(fd); return -EPERM; } + struct flock lock = {.l_type = F_WRLCK, .l_whence = SEEK_SET}; + if (fcntl(fd, F_SETLK, &lock) < 0) { + int result = errno == EACCES || errno == EAGAIN ? -EBUSY : -errno; + close(fd); + return result; + } + return fd; +} + +int installer_update_target_close(struct installer_update_target *target) +{ + int result = 0; + if (target->boot_mounted) { + if (umount2(target->boot, 0) < 0) result = -errno; + else target->boot_mounted = 0; + } + if (target->esp_mounted && !target->boot_mounted) { + if (umount2(target->esp, 0) < 0 && !result) result = -errno; + else if (!result) target->esp_mounted = 0; + } + if (target->root_mounted && !target->boot_mounted && !target->esp_mounted) { + if (umount2(target->root, 0) < 0 && !result) result = -errno; + else if (!result) target->root_mounted = 0; + } + if (!target->root_mounted && target->root[0]) { + if (rmdir(target->root) < 0 && !result) result = -errno; + if (rmdir(target->workspace) < 0 && !result) result = -errno; + } + if (target->lock_fd >= 0) { + if (close(target->lock_fd) < 0 && !result) result = -errno; + target->lock_fd = -1; + } + return result; +} + +/** @brief Resolve only the selected root's declarative ESP and bind layout. */ +static int read_boot_layout(struct installer_update_target *target, char source[128]) +{ + int root = open(target->root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + int etc = root < 0 ? -1 : openat(root, "etc", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + int fd = etc < 0 ? -1 : openat(etc, "fstab", O_RDONLY | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC); + int saved = errno; + if (etc >= 0) close(etc); + if (root >= 0) close(root); + if (fd < 0) return -saved; + struct stat status; + if (fstat(fd, &status) < 0) { saved = errno; close(fd); return -saved; } + if (!S_ISREG(status.st_mode) || status.st_size < 0 || status.st_size > 65536) { close(fd); return -EINVAL; } + FILE *stream = fdopen(fd, "r"); + if (!stream) { saved = errno; close(fd); return -saved; } + char line[1024], device[128], point[128], type[32], flags[128], bind[128] = {0}; + int result = 0, physical = 0, logical = 0, physical_namespace = 0; + while (fgets(line, sizeof(line), stream)) { + if (!strchr(line, '\n') && !feof(stream)) { result = -EOVERFLOW; break; } + int fields = sscanf(line, "%127s %127s %31s %127s", device, point, type, flags); + if (fields < 1 || device[0] == '#') continue; + if (fields < 2 || (strcmp(point, "/boot") && strcmp(point, "/efi"))) continue; + if (fields != 4) { result = -EINVAL; break; } + if (!strcmp(point, "/boot") && !strcmp(type, "none") && !strcmp(flags, "bind")) { + if (logical++) { result = -ENOTUNIQ; break; } + snprintf(bind, sizeof(bind), "/efi/reliefos/instances/%s", target->info.root_uuid); + if (strcmp(device, bind)) { result = -EXDEV; break; } + } else if ((!strcmp(point, "/boot") || !strcmp(point, "/efi")) && + (!strcmp(type, "vfat") || !strcmp(type, "fat32"))) { + if (physical++) { result = -ENOTUNIQ; break; } + strcpy(source, device); + physical_namespace = !strcmp(point, "/efi"); + } else { result = -EINVAL; break; } + } + if (!result && ferror(stream)) result = -EIO; + fclose(stream); + if (!result && (!physical || logical != physical_namespace)) result = -EINVAL; + if (!result) target->namespaced = physical_namespace; + return result; +} + +/** @brief Mount the chosen physical ESP and its optional per-root boot view. */ +static int mount_boot(struct installer_update_target *target, unsigned long flags) +{ + const char *physical = target->namespaced ? target->esp : target->boot; + struct stat point; + if (lstat(physical, &point) < 0) return -errno; + if (!S_ISDIR(point.st_mode)) return -EINVAL; + if (target->namespaced) { + if (lstat(target->boot, &point) < 0) return -errno; + if (!S_ISDIR(point.st_mode)) return -EINVAL; + } + if (mount(target->info.esp_device, physical, "vfat", flags, NULL) < 0) return -errno; + if (!target->namespaced) { target->boot_mounted = 1; return 0; } + target->esp_mounted = 1; + char instance[512]; + snprintf(instance, sizeof(instance), "%s/reliefos/instances/%s", target->esp, target->info.root_uuid); + struct stat status; + if (lstat(instance, &status) < 0) return -errno; + if (!S_ISDIR(status.st_mode)) return -EINVAL; + if (mount(instance, target->boot, NULL, MS_BIND, NULL) < 0) return -errno; + target->boot_mounted = 1; + return 0; +} + +int installer_update_target_open(struct installer_update_target *target, + const char *disk, const char *expected_root, const char *expected_esp) +{ + struct reliefos_block_partition parts[RELIEFOS_BLOCK_MAX_PARTITIONS]; + uint32_t count = 0; + int result, esp = -1, found = -1; + memset(target, 0, sizeof(*target)); + target->lock_fd = -1; + if (!disk || strncmp(disk, "/dev/", 5) || strlen(disk) >= sizeof(target->info.disk)) return -EINVAL; + target->lock_fd = acquire_lock(); + if (target->lock_fd < 0) return target->lock_fd; + result = reliefos_block_list_partitions(disk, parts, RELIEFOS_BLOCK_MAX_PARTITIONS, &count); + if (result) return result; + if (count > RELIEFOS_BLOCK_MAX_PARTITIONS) return -EOVERFLOW; + strcpy(target->workspace, "/run/reliefos/update-XXXXXX"); + if (!mkdtemp(target->workspace)) return -errno; + char *paths[] = {target->root, target->boot, target->esp}; + const char *suffixes[] = {"/root", "/root/boot", "/root/efi"}; + for (unsigned i = 0; i < 3; ++i) { + int n = snprintf(paths[i], sizeof(target->root), "%s%s", target->workspace, suffixes[i]); + if (n < 0 || n >= (int)sizeof(target->root)) return -ENAMETOOLONG; + } + if (mkdir(target->root, 0700) < 0) return -errno; + for (uint32_t i = 0; i < count; ++i) { + const char *filesystem = parts[i].filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXT4 ? "ext4" + : parts[i].filesystem == RELIEFOS_BLOCK_FILESYSTEM_EXT2 ? "ext2" : NULL; + if (!filesystem || parts[i].gpt_type != RELIEFOS_BLOCK_GPT_LINUX) continue; + char uuid[37]; + result = reliefos_block_partition_uuid(disk, parts[i].index, uuid); + if (result) return result; + if (expected_root && *expected_root && strcmp(expected_root, uuid)) continue; + result = mounted_elsewhere(parts[i].path); + if (result) return result; + if (mount(parts[i].path, target->root, filesystem, MS_RDONLY | MS_NOSUID | MS_NODEV, NULL) < 0) + return -errno; + target->root_mounted = 1; + struct installer_update_info candidate = {0}; + result = installer_update_read_system(target->root, &candidate); + if (umount2(target->root, 0) < 0) return -errno; + target->root_mounted = 0; + if (result) { + fprintf(stderr, "[installer-update] root inspection %s returned %d\n", parts[i].path, result); + continue; + } + if (found >= 0) return -ENOTUNIQ; + found = (int)i; + target->info = candidate; + strcpy(target->info.root_uuid, uuid); + target->filesystem = filesystem; + } + if (found < 0) return -ENODEV; + strcpy(target->info.disk, disk); + snprintf(target->info.root_device, sizeof(target->info.root_device), "%s", parts[found].path); + if (mount(target->info.root_device, target->root, target->filesystem, MS_RDONLY | MS_NOSUID | MS_NODEV, NULL) < 0) + return -errno; + target->root_mounted = 1; + char source[128] = {0}; + result = read_boot_layout(target, source); + if (result) return result; + for (uint32_t i = 0; i < count; ++i) { + if (parts[i].gpt_type != RELIEFOS_BLOCK_GPT_ESP || parts[i].filesystem != RELIEFOS_BLOCK_FILESYSTEM_FAT32) continue; + char uuid[37]; + result = reliefos_block_partition_uuid(disk, parts[i].index, uuid); + if (result) return result; + const char *id = !strncmp(source, "PARTUUID=", 9) ? source + 9 : + !strncmp(source, "/dev/disk/by-partuuid/", 22) ? source + 22 : NULL; + int matches = id ? !strcasecmp(id, uuid) : !strcmp(source, parts[i].path); + if (!id && !matches) { + struct stat alias, partition; + matches = !stat(source, &alias) && S_ISBLK(alias.st_mode) && + !stat(parts[i].path, &partition) && alias.st_rdev == partition.st_rdev; + } + if (!matches) continue; + if (expected_esp && *expected_esp && strcmp(expected_esp, uuid)) return -EXDEV; + if (esp >= 0) return -ENOTUNIQ; + esp = (int)i; + strcpy(target->info.esp_uuid, uuid); + } + if (esp < 0) return -ENODEV; + snprintf(target->info.esp_device, sizeof(target->info.esp_device), "%s", parts[esp].path); + result = mounted_elsewhere(target->info.esp_device); + if (result) return result; + result = mount_boot(target, MS_RDONLY | MS_NOSUID | MS_NODEV); + if (result) return result; + if (target->namespaced) + snprintf(target->info.boot_file, sizeof(target->info.boot_file), "/EFI/ReliefOS/%s/grubx64.efi", target->info.root_uuid); + else strcpy(target->info.boot_file, "/EFI/BOOT/BOOTX64.EFI"); + return reliefos_boot_probe(disk, target->boot, &target->info.boot_modes); +} + +int installer_update_target_writable(struct installer_update_target *target) +{ + if (!target->root_mounted || !target->boot_mounted) return -EINVAL; + if (umount2(target->boot, 0) < 0) return -errno; + target->boot_mounted = 0; + if (target->esp_mounted) { + if (umount2(target->esp, 0) < 0) return -errno; + target->esp_mounted = 0; + } + if (umount2(target->root, 0) < 0) return -errno; + target->root_mounted = 0; + if (mount(target->info.root_device, target->root, target->filesystem, 0, NULL) < 0) return -errno; + target->root_mounted = 1; + return mount_boot(target, MS_NOSUID | MS_NODEV); +} diff --git a/userland/apps/installer/main.cpp b/userland/apps/installer/main.cpp index 9b50cf8b..1be1a934 100644 --- a/userland/apps/installer/main.cpp +++ b/userland/apps/installer/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include extern "C" { #include @@ -21,14 +22,15 @@ extern "C" { #include #include #include +#include #include #include #include -#include #include #include #include -#include +#include +#include #include #include #include @@ -39,6 +41,8 @@ extern "C" { #include "installer_capacity.h" #include "installer_partitions.h" #include "installer_tty.h" +#include "installer_update.h" +#include "../reliefapk/process.h" #include "model.h" enum partition_prompt_action { @@ -72,6 +76,8 @@ static Fl_Return_Button *primary_button; static Fl_Button *cancel_button; static Fl_Hold_Browser *disk_browser; static Fl_Hold_Browser *partition_browser; +static Fl_Choice *update_root_choice, *update_esp_choice; +static Fl_Check_Button *format_esp_checkbox; static Fl_Input *confirm_input; static Fl_Check_Button *xorg_checkbox; static Fl_Input *swap_size_input; @@ -92,92 +98,16 @@ static int back_pending; static int render_pending; static int account_input_overflow[5]; static int confirm_input_overflow; +static ra::Process update_process; +static struct installer_update_info update_info; +static Fl_Text_Buffer *update_log_buffer; +static Fl_Text_Display *update_log_display; +static size_t update_stdout_offset, update_stderr_offset; +static int update_job, update_checked, update_protocol_error; +static char update_message[256]; -static const char acknowledgements_en[] = - "Acknowledgements\n" - "\n" - "ReliefOS gratefully acknowledges the creators, contributors, and maintainers " - "of the public resources and open-source projects used by this release.\n" - "\n" - "Runtime, Toolchain, and Applications\n" - "- GNU GRUB 2 - GPL-3.0-or-later\n" - "- Mbed TLS 3.6.7 - Apache License 2.0\n" - "- musl 1.2.6 - MIT\n" - "- mimalloc 3.5.1 - MIT\n" - "- zlib 1.3.2, the bundled compression library - zlib License\n" - "- libpng 1.6.58, the bundled PNG decoder library - libpng License\n" - "- SQLite 3.46.1 - public-domain dedication and blessing\n" - "- BusyBox 1.36.1 - GPL-2.0-only\n" - "- less (Alpine APK) - GPL-3.0-or-later OR BSD-2-Clause\n" - "- Vim (Alpine APK) - Vim License\n" - "- Fastfetch 2.68.1 - MIT\n" - "- sl - permissive upstream license\n" - "- Leonmmcoset/pl_editor (modified PL Editor fork) - MIT\n" - "- file and libmagic - BSD-2-Clause\n" - "- StardustUI - MIT\n" - "- DoomGeneric - GPL-2.0-only\n" - "- Freedoom - BSD-3-Clause\n" - "- rime-pinyin-simp dictionary - Apache License 2.0\n" - "- minimp3 - CC0-1.0\n" - "\n" - "Fonts and Visual Resources\n" - "- Noto Sans Mono - SIL Open Font License 1.1\n" - "- Droid Sans Fallback - Apache License 2.0\n" - "- Noto Sans CJK - SIL Open Font License 1.1\n" - "- Microsoft fonts used by the distribution - subject to the applicable " - "Microsoft license terms\n" - "- NASA Image and Video Library, PIA18033 - used in accordance with NASA " - "Media Usage Guidelines\n" - "\n" - "Recorded Browser Source Dependencies\n" - "- litehtml - BSD-3-Clause\n" - "- Gumbo HTML Parser - Apache License 2.0\n" - "\n" - "These browser sources are retained for future compatibility work and are " - "not linked into the current browser runtime.\n" - "Complete license texts and attribution notices are preserved with the " - "corresponding source, SDK, and/or installed program package.\n"; - -static const char acknowledgements_zh[] = - "感谢\n" - "\n" - "ReliefOS 诚挚感谢本发行版所使用的公共资源与开源项目的创作者、贡献者和维护者。\n" - "\n" - "运行时、开发工具链与应用程序\n" - "- GNU GRUB 2 - GPL-3.0-or-later\n" - "- Mbed TLS 3.6.7 - Apache License 2.0\n" - "- musl 1.2.6 - MIT\n" - "- mimalloc 3.5.1 - MIT\n" - "- zlib 1.3.2(内置压缩库)- zlib License\n" - "- libpng 1.6.58(内置 PNG 解码库)- libpng License\n" - "- SQLite 3.46.1 - 公共领域声明与许可祝福文本\n" - "- BusyBox 1.36.1 - GPL-2.0-only\n" - "- less (Alpine APK) - GPL-3.0-or-later OR BSD-2-Clause\n" - "- Vim (Alpine APK) - Vim License\n" - "- Fastfetch 2.68.1 - MIT\n" - "- sl - 上游宽松许可\n" - "- Leonmmcoset/pl_editor(PL Editor 修改版)- MIT\n" - "- file 与 libmagic - BSD-2-Clause\n" - "- StardustUI - MIT\n" - "- DoomGeneric - GPL-2.0-only\n" - "- Freedoom - BSD-3-Clause\n" - "- rime-pinyin-simp 中文词库 - Apache License 2.0\n" - "- minimp3 - CC0-1.0\n" - "\n" - "字体与视觉资源\n" - "- Noto Sans Mono - SIL Open Font License 1.1\n" - "- Droid Sans Fallback - Apache License 2.0\n" - "- Noto Sans CJK - SIL Open Font License 1.1\n" - "- 发行版使用的 Microsoft 字体 - 受相应 Microsoft 许可条款约束\n" - "- NASA Image and Video Library,PIA18033 - 遵循 NASA 媒体使用指南\n" - "\n" - "已记录的浏览器源码依赖\n" - "- litehtml - BSD-3-Clause\n" - "- Gumbo HTML Parser - Apache License 2.0\n" - "\n" - "这些浏览器源码为后续兼容性工作保留,当前浏览器运行时尚未链接它们。\n" - "完整许可证文本与归属声明保留在相应源代码、开发套件和/或已安装程序包中。\n"; - +/* Embedded verbatim from the repository LICENSE by apache_license.S. */ +extern "C" const char installer_license_text[]; static Fl_Box *make_label(const char *text, int x, int y, int w, int h, int size, int wrap) @@ -215,7 +145,7 @@ static const char *page_label(enum installer_page target) { switch (target) { case INSTALLER_PAGE_LANGUAGE: return T("Language"); - case INSTALLER_PAGE_THANKS: return T("Thanks"); + case INSTALLER_PAGE_LICENSE: return T("License"); case INSTALLER_PAGE_WELCOME: return T("Welcome"); case INSTALLER_PAGE_MODE: return T("Mode"); case INSTALLER_PAGE_DISK: return T("Disk"); @@ -236,9 +166,12 @@ static const char *primary_label(void) install_success)) { case INSTALLER_ACTION_INSTALL: return T("Install"); case INSTALLER_ACTION_UPDATE: return T("Update"); + case INSTALLER_ACTION_RETRY: return T("Check Again"); case INSTALLER_ACTION_RESTART: return T("Restart"); case INSTALLER_ACTION_CLOSE: return T("Close"); - case INSTALLER_ACTION_NEXT: return T("Next"); + case INSTALLER_ACTION_NEXT: + return page == INSTALLER_PAGE_DISK && install_mode == INSTALLER_MODE_UPDATE + ? T("Check Update") : T("Next"); } return T("Next"); } @@ -255,6 +188,8 @@ static int primary_disabled(void) return selected_disk < 0 || (uint32_t)selected_disk >= disk_count; if (page == INSTALLER_PAGE_PARTITIONS) return !installer_partition_plan_valid(); + if (page == INSTALLER_PAGE_CONFIRM && install_mode == INSTALLER_MODE_UPDATE) + return !update_checked; if (page == INSTALLER_PAGE_CONFIRM) return confirm_input_overflow || !confirmation_ok() || (install_mode==INSTALLER_MODE_FRESH && setup.swap_enabled && swap_size_invalid); @@ -326,6 +261,130 @@ static void schedule_render(void) Fl::add_timeout(0.0, render_deferred); } +static void update_pump(void *) +{ + update_process.step(16384); + const char *output = update_process.output(); + size_t length = update_process.output_size(); + while (update_stdout_offset < length) { + const char *line = output + update_stdout_offset; + const char *end = static_cast(memchr(line, '\n', length - update_stdout_offset)); + if (!end) break; + size_t bytes = static_cast(end - line); + char text[512]; + if (bytes < sizeof(text)) { + memcpy(text, line, bytes); + text[bytes] = 0; + unsigned value = progress_value; + int result = installer_update_decode(&update_info, &value, update_message, sizeof(update_message), text); + if (result < 0) update_protocol_error = 1; + if (result == 2 || result == 3) { + progress_value = value; + copy_text(progress_text, sizeof(progress_text), T(update_message)); + copy_text(status_text, sizeof(status_text), T(update_message)); + } + } + update_stdout_offset += bytes + 1; + } + size_t errors = update_process.errors_size(); + if (errors > update_stderr_offset) { + const char *logs = update_process.errors(); + fwrite(logs + update_stderr_offset, 1, errors - update_stderr_offset, stderr); + update_stderr_offset = errors; + if (update_log_buffer) { + size_t start = errors > 65536 ? errors - 65536 : 0; + while (start < errors && (static_cast(logs[start]) & 0xc0) == 0x80) ++start; + update_log_buffer->text(logs + start); + if (update_log_display) { + update_log_display->insert_position(update_log_buffer->length()); + update_log_display->show_insert_position(); + } + } + } + refresh_status_widgets(); + if (update_process.running()) { Fl::repeat_timeout(0.08, update_pump); return; } + int checking = update_job == 1; + update_job = 0; + ui_busy = 0; + install_running = 0; + if (update_process.exit_code() == 0 && !update_protocol_error && !update_info.error_number) { + if (checking) { + update_checked = 1; + setup.skip_bios = !(update_info.boot_modes & 1u); + setup.skip_uefi = !(update_info.boot_modes & 2u); + page = INSTALLER_PAGE_CONFIRM; + set_status(T("Ready to update"), T("Review the detected system and start the update.")); + } else { + page = INSTALLER_PAGE_FINISH; + install_success = 1; + progress_value = 100; + set_status(T("System update completed successfully"), T("Remove the installation media, then restart.")); + fprintf(stderr, "[installer.elf] update completed successfully\n"); + } + } else { + page = INSTALLER_PAGE_FINISH; + install_success = 0; + int error = update_info.error_number ? update_info.error_number + : update_process.exec_error() ? update_process.exec_error() : EIO; + char detail[128]; + snprintf(detail, sizeof(detail), "%s (%d)", strerror(error), error); + set_status(update_info.failure[0] ? T(update_info.failure) : T("Update worker failed"), detail); + } + show_page(); +} + +static void start_update_job(int applying) +{ + if (update_job || selected_disk < 0 || static_cast(selected_disk) >= disk_count) return; + if (applying && !update_checked) return; + if (!applying) memset(&update_info, 0, sizeof(update_info)); + update_checked = 0; + update_protocol_error = 0; + update_stdout_offset = update_stderr_offset = 0; + if (!update_log_buffer) update_log_buffer = new Fl_Text_Buffer(); + update_log_buffer->text(""); + char executable[PATH_MAX], modes[2] = { + static_cast('0' + (setup.skip_bios ? 0 : 1) + (setup.skip_uefi ? 0 : 2)), 0}; + ssize_t bytes = readlink("/proc/self/exe", executable, sizeof(executable) - 1); + if (bytes < 0 || static_cast(bytes) >= sizeof(executable) - 1) { + page = INSTALLER_PAGE_FINISH; + install_success = 0; + set_status(T("Cannot start the update worker"), strerror(bytes < 0 ? errno : ENAMETOOLONG)); + show_page(); + return; + } + executable[bytes] = 0; + char root_uuid[37] = {0}, esp_uuid[37] = {0}; + if (!applying) { + int error = 0; + if (installer_root_partition >= 0) + error = reliefos_block_partition_uuid(disks[selected_disk].path, installer_root_partition, root_uuid); + if (!error && installer_esp_partition >= 0) + error = reliefos_block_partition_uuid(disks[selected_disk].path, installer_esp_partition, esp_uuid); + if (error) { set_error_status(T("Cannot identify the selected partitions"), error); return; } + } + const char *check[] = {executable, "--update-worker", "check", disks[selected_disk].path, root_uuid, esp_uuid, NULL}; + const char *apply[] = {executable, "--update-worker", "apply", update_info.disk, + update_info.root_uuid, update_info.esp_uuid, update_info.index_sha256, update_info.database_sha256, + update_info.world_sha256, modes, NULL}; + if (!update_process.start(applying ? apply : check, false, ra::AuthMode::Sudo, false)) { + page = INSTALLER_PAGE_FINISH; + install_success = 0; + set_status(T("Cannot start the update worker"), strerror(update_process.exec_error() ? update_process.exec_error() : EIO)); + show_page(); + return; + } + update_job = applying ? 2 : 1; + ui_busy = 1; + install_running = 1; + install_success = 0; + page = INSTALLER_PAGE_PROGRESS; + progress_value = 0; + copy_text(progress_text, sizeof(progress_text), applying ? T("Preparing the system update") : T("Checking the selected system")); + show_page(); + Fl::add_timeout(0.0, update_pump); +} + static void close_now(void) { exit(0); @@ -384,13 +443,10 @@ static void go_primary_now(void *) show_page(); return; case INSTALLER_ACTION_UPDATE: - ui_busy = 1; - page = INSTALLER_PAGE_PROGRESS; - show_page(); - Fl::flush(); - perform_update(); - ui_busy = 0; - show_page(); + start_update_job(1); + return; + case INSTALLER_ACTION_RETRY: + start_update_job(0); return; case INSTALLER_ACTION_RESTART: fprintf(stderr, "[installer.elf] restart requested from completion page\n"); @@ -409,14 +465,7 @@ static void go_primary_now(void *) } if (page == INSTALLER_PAGE_DISK && install_mode == INSTALLER_MODE_UPDATE) { - ui_busy = 1; - confirm_input_overflow = 0; - page = INSTALLER_PAGE_PROGRESS; - show_page(); - Fl::flush(); - prepare_update_target(); - ui_busy = 0; - show_page(); + start_update_job(0); return; } page = (uint8_t)installer_model_next((enum installer_page)page, @@ -476,22 +525,17 @@ static void build_language_page(void) CONTENT_X, 238, CONTENT_W, 32, 15, 1); } -static void build_thanks_page(void) +static void build_license_page(void) { - make_heading(T("Thank You"), 20); - make_label(T("Acknowledgements for public resources and open-source projects."), - CONTENT_X, 58, CONTENT_W, 32, 15, 1); - Fl_Scroll *scroll = new Fl_Scroll(CONTENT_X, 98, CONTENT_W, 500); - scroll->box(FL_DOWN_BOX); - scroll->begin(); - Fl_Multiline_Output *text = - new Fl_Multiline_Output(CONTENT_X, 98, CONTENT_W - 18, 900); - text->value(language_selection() == 1 ? acknowledgements_zh - : acknowledgements_en); + make_heading("Apache License 2.0", 20); + static Fl_Text_Buffer license_buffer; + if (!license_buffer.length()) license_buffer.text(installer_license_text); + Fl_Text_Display *text = new Fl_Text_Display(CONTENT_X, 62, CONTENT_W, 536); + text->buffer(license_buffer); + text->textfont(FL_COURIER); text->textsize(13); - text->wrap(1); + text->wrap_mode(Fl_Text_Display::WRAP_AT_BOUNDS, 0); reliefos_fltk::style(text); - scroll->end(); } static void build_welcome_page(void) @@ -540,10 +584,46 @@ static void build_mode_page(void) CONTENT_X, 278, CONTENT_W, 42, 15, 1); } +static void update_partition_selected(Fl_Widget *widget, void *) +{ + auto *choice = static_cast(widget); + int index = choice->mvalue() ? int((intptr_t)choice->mvalue()->user_data()) - 1 : -1; + if (widget == update_root_choice) installer_root_partition = index; + else installer_esp_partition = index; + update_checked = 0; +} + +static void refresh_update_partitions(void) +{ + if (!update_root_choice || !update_esp_choice) return; + refresh_partitions(); + update_root_choice->clear(); update_esp_choice->clear(); + update_root_choice->add(T("Automatically identify a single ReliefOS installation")); + update_esp_choice->add(T("Use the selected installation's fstab")); + update_root_choice->value(0); update_esp_choice->value(0); + for (uint32_t i = 0; i < partition_count; ++i) { + char line[256]; format_partition_line(line, sizeof(line), &partitions[i]); + Fl_Choice *choice = partitions[i].gpt_type == RELIEFOS_BLOCK_GPT_ESP ? update_esp_choice : + partitions[i].gpt_type == RELIEFOS_BLOCK_GPT_LINUX ? update_root_choice : nullptr; + if (!choice) continue; + char escaped[sizeof(line) * 2]; size_t used = 0; + for (const char *p = line; *p; ++p) { + if (*p == '/' || *p == '\\' || *p == '&') escaped[used++] = '\\'; + escaped[used++] = *p; + } + escaped[used] = 0; + choice->add(escaped, 0, nullptr, (void *)(intptr_t)(partitions[i].index + 1)); + int selected = choice == update_root_choice ? installer_root_partition : installer_esp_partition; + if (selected == (int)partitions[i].index) choice->value(choice->size() - 2); + } +} + static void select_disk(Fl_Widget *, void *) { if (disk_browser && disk_browser->value() > 0) selected_disk = disk_browser->value() - 1; + setup.format_esp = 0; + refresh_update_partitions(); update_footer(); } @@ -558,6 +638,7 @@ static void refresh_disk_list(void) } if (selected_disk >= 0 && (uint32_t)selected_disk < disk_count) disk_browser->value(selected_disk + 1); + refresh_update_partitions(); update_footer(); refresh_status_widgets(); } @@ -582,11 +663,21 @@ static void build_disk_page(void) CONTENT_X, 62, 600, 32, 15, 1); make_button(T("Refresh"), CONTENT_X + 650, 22, 130, 34, refresh_pressed, NULL); make_label(T("Available disks"), CONTENT_X, 102, CONTENT_W, 26, 14, 0); - disk_browser = new Fl_Hold_Browser(CONTENT_X, 132, CONTENT_W, 330); + disk_browser = new Fl_Hold_Browser(CONTENT_X, 132, CONTENT_W, + install_mode == INSTALLER_MODE_UPDATE ? 190 : 330); disk_browser->callback(select_disk); disk_browser->textsize(14); disk_browser->selection_color(COLOR_SIDEBAR); reliefos_fltk::style(disk_browser); + if (install_mode == INSTALLER_MODE_UPDATE) { + make_label(T("ReliefOS system partition"), CONTENT_X, 330, CONTENT_W, 24, 14, 0); + update_root_choice = new Fl_Choice(CONTENT_X, 356, CONTENT_W, 32); + make_label(T("EFI system partition"), CONTENT_X, 396, CONTENT_W, 24, 14, 0); + update_esp_choice = new Fl_Choice(CONTENT_X, 422, CONTENT_W, 32); + update_root_choice->callback(update_partition_selected); + update_esp_choice->callback(update_partition_selected); + reliefos_fltk::style(update_root_choice); reliefos_fltk::style(update_esp_choice); + } status_box = make_label(status_text, CONTENT_X, 478, CONTENT_W, 34, 14, 1); detail_box = make_label(detail_text, CONTENT_X, 516, CONTENT_W, 48, 14, 1); refresh_disk_list(); @@ -595,6 +686,7 @@ static void build_disk_page(void) static void refresh_partition_list(void) { if (!partition_browser) return; + if (format_esp_checkbox) format_esp_checkbox->value(setup.format_esp); char line[640]; if (partition_hint) { struct installer_partition_plan plan; @@ -822,6 +914,12 @@ static void partition_role_pressed(Fl_Widget *, void *data) partition_result(ret, T("Partition role updated.")); } +static void format_esp_changed(Fl_Widget *widget, void *) +{ + setup.format_esp = static_cast(widget)->value() != 0; + update_footer(); +} + static void build_partition_page(void) { static const char *const labels[] = { @@ -870,10 +968,18 @@ static void build_partition_page(void) detail_box = make_label(detail_text, CONTENT_X, 512, CONTENT_W, 38, 13, 1); make_label(T("Create: size in MiB, filesystem, name. Resize only changes the trailing edge."), CONTENT_X, 552, CONTENT_W, 26, 12, 1); - make_label((installer_partition_auto || installer_partition_table_replaced) + make_label(installer_partition_auto ? T("Final confirmation formats a FAT32 ESP and ext4 root on the selected disk.") - : T("Final confirmation formats only the selected ESP and root; other partitions are preserved."), + : T("The selected root is formatted. An existing FAT32 ESP is preserved by default."), CONTENT_X, 578, CONTENT_W, 32, 12, 1); + if (!installer_partition_auto) { + auto *format = new Fl_Check_Button(CONTENT_X, 613, CONTENT_W, 28, + T("Format selected ESP (erase all EFI boot files on it)")); + format->value(setup.format_esp); + format_esp_checkbox = format; + format->callback(format_esp_changed); + reliefos_fltk::style(format); + } refresh_partition_list(); } @@ -992,8 +1098,6 @@ static void swap_install_changed(Fl_Widget *widget,void *) /** @brief Apply one firmware choice and explain the effect of skipping both. */ static const char *bootloader_description(void) { - if (install_mode == INSTALLER_MODE_UPDATE) - return T("Existing partitions are preserved. BIOS requires an unformatted BIOS Boot partition. Skipped existing bootloaders are preserved."); if (setup.skip_bios && setup.skip_uefi) return T("No bootloader selected. A new installation will need a separately configured bootloader."); return setup.skip_bios @@ -1029,20 +1133,54 @@ static void build_bootloader_page(void) reliefos_fltk::style(uefi); bootloader_hint = make_label(bootloader_description(), CONTENT_X, 244, CONTENT_W, 100, 14, 1); - if (install_mode == INSTALLER_MODE_UPDATE) - make_label(T("The initial selection matches the bootloaders found on this disk."), - CONTENT_X, 362, CONTENT_W, 54, 14, 1); } static void build_confirm_page(void) { + if (install_mode == INSTALLER_MODE_UPDATE) { + char text[256]; + make_heading(T("Review system update"), 16); + make_label(T("Installed version"), CONTENT_X, 66, 200, 26, 14, 0); + make_label(update_info.installed_version, CONTENT_X + 215, 66, 220, 26, 14, 0); + make_label(T("Media version"), CONTENT_X + 445, 66, 170, 26, 14, 0); + make_label(update_info.media_version, CONTENT_X + 620, 66, 160, 26, 14, 0); + make_label(T("Target disk"), CONTENT_X, 112, 200, 26, 14, 0); + make_label(update_info.disk, CONTENT_X + 215, 112, 560, 26, 14, 0); + make_label(T("System partition"), CONTENT_X, 150, 200, 26, 14, 0); + make_label(update_info.root_device, CONTENT_X + 215, 150, 560, 26, 14, 0); + make_label(T("Boot partition"), CONTENT_X, 188, 200, 26, 14, 0); + make_label(update_info.esp_device, CONTENT_X + 215, 188, 560, 26, 14, 0); + make_label(T("Desktop environment"), CONTENT_X, 226, 200, 26, 14, 0); + make_label(!strcmp(update_info.profile, "console") ? T("Console") : "Xorg / IceWM", + CONTENT_X + 215, 226, 560, 26, 14, 0); + make_label(T("Free / required space"), CONTENT_X, 264, 200, 26, 14, 0); + snprintf(text, sizeof(text), "%llu MiB / %llu MiB", + (unsigned long long)(update_info.free_bytes / (1024u * 1024u)), + (unsigned long long)((update_info.required_bytes + 1024u * 1024u - 1) / (1024u * 1024u))); + make_label(text, CONTENT_X + 215, 264, 560, 26, 14, 0); + make_label(T("Update bootloader software"), CONTENT_X, 310, CONTENT_W, 26, 14, 0); + Fl_Check_Button *bios = new Fl_Check_Button(CONTENT_X, 346, 175, 30, "BIOS"); + bios->value(!setup.skip_bios); + bios->callback(bootloader_install_changed, (void *)(uintptr_t)1); + reliefos_fltk::style(bios); + Fl_Check_Button *uefi = new Fl_Check_Button(CONTENT_X + 190, 346, 175, 30, "UEFI"); + uefi->value(!setup.skip_uefi); + uefi->callback(bootloader_install_changed, (void *)(uintptr_t)2); + reliefos_fltk::style(uefi); + make_label(T("Accounts, personal files, installed software and local configuration are preserved."), + CONTENT_X, 398, CONTENT_W, 46, 14, 1); + if (update_info.pending[0]) + make_label(T("A previous update is incomplete. This attempt will verify the system and recover the pending update stages."), + CONTENT_X, 464, CONTENT_W, 60, 14, 1); + else + make_label(T("Signed packages are checked before changes. Boot files are published after the package transaction succeeds."), + CONTENT_X, 464, CONTENT_W, 60, 14, 1); + return; + } char line[160]; char target[192]; - make_heading(install_mode == INSTALLER_MODE_UPDATE - ? T("Confirm Update") : T("Confirm Installation"), 16); - make_label(install_mode == INSTALLER_MODE_UPDATE - ? T("Installed ReliefOS packages will be upgraded.") - : T("This operation is destructive."), + make_heading(T("Confirm Installation"), 16); + make_label(T("This operation is destructive."), CONTENT_X, 54, CONTENT_W, 32, 15, 1); if (selected_disk >= 0 && (uint32_t)selected_disk < disk_count) format_disk_line(line, sizeof(line), &disks[selected_disk]); @@ -1050,15 +1188,13 @@ static void build_confirm_page(void) line[0] = 0; snprintf(target, sizeof(target), "%s %s", T("Target:"), line); make_label(target, CONTENT_X, 98, CONTENT_W, 34, 14, 1); - make_label(install_mode == INSTALLER_MODE_UPDATE - ? T("Alpine packages and local configuration are retained. Boot files are updated after the package transaction succeeds.") - : (installer_partition_auto || installer_partition_table_replaced) + make_label(installer_partition_auto ? T("The selected disk will be erased and formatted with a FAT32 ESP and ext4 system root.") - : T("Only the selected ESP and root partitions will be formatted; other partitions are preserved."), + : setup.format_esp + ? T("The selected root and ESP will be formatted. All EFI boot files on that ESP will be erased.") + : T("Only the selected root will be formatted. Existing ESP files and other partitions are preserved."), CONTENT_X, 142, CONTENT_W, 64, 14, 1); - make_label(install_mode == INSTALLER_MODE_UPDATE - ? T("Type UPDATE to enable the Update button.") - : T("Type INSTALL to enable the Install button."), + make_label(T("Type INSTALL to enable the Install button."), CONTENT_X, 224, CONTENT_W, 30, 14, 1); confirm_input = new Fl_Input(CONTENT_X, 260, 330, 36); confirm_input->value(confirm_text); @@ -1066,29 +1202,27 @@ static void build_confirm_page(void) confirm_input->when(FL_WHEN_CHANGED | FL_WHEN_ENTER_KEY_ALWAYS); confirm_input->callback(confirm_changed); reliefos_fltk::style(confirm_input); - if (install_mode != INSTALLER_MODE_UPDATE) { - xorg_checkbox = new Fl_Check_Button(CONTENT_X, 318, 375, 32, - T("Install Xorg desktop environment")); - xorg_checkbox->value(setup.skip_xorg ? 0 : 1); - xorg_checkbox->callback(xorg_install_changed); - reliefos_fltk::style(xorg_checkbox); - if (!swap_size_initialized) { - if (!setup.swap_size_mib) setup.swap_size_mib=installer_swap_recommend_system(); - snprintf(swap_size_text,sizeof(swap_size_text),"%llu",(unsigned long long)setup.swap_size_mib); - swap_size_initialized=1; - } - Fl_Check_Button *swap=new Fl_Check_Button(CONTENT_X+390,318,220,32,T("Swapfile (MiB)")); - swap->value(setup.swap_enabled ? 1 : 0); - swap->callback(swap_install_changed); - reliefos_fltk::style(swap); - swap_size_input=new Fl_Input(CONTENT_X+615,318,130,32); - swap_size_input->value(swap_size_text); - swap_size_input->maximum_size(20); - swap_size_input->when(FL_WHEN_CHANGED); - swap_size_input->callback(swap_size_changed); - if (!setup.swap_enabled) swap_size_input->deactivate(); - reliefos_fltk::style(swap_size_input); + xorg_checkbox = new Fl_Check_Button(CONTENT_X, 318, 375, 32, + T("Install Xorg desktop environment")); + xorg_checkbox->value(setup.skip_xorg ? 0 : 1); + xorg_checkbox->callback(xorg_install_changed); + reliefos_fltk::style(xorg_checkbox); + if (!swap_size_initialized) { + if (!setup.swap_size_mib) setup.swap_size_mib=installer_swap_recommend_system(); + snprintf(swap_size_text,sizeof(swap_size_text),"%llu",(unsigned long long)setup.swap_size_mib); + swap_size_initialized=1; } + Fl_Check_Button *swap=new Fl_Check_Button(CONTENT_X+390,318,220,32,T("Swapfile (MiB)")); + swap->value(setup.swap_enabled ? 1 : 0); + swap->callback(swap_install_changed); + reliefos_fltk::style(swap); + swap_size_input=new Fl_Input(CONTENT_X+615,318,130,32); + swap_size_input->value(swap_size_text); + swap_size_input->maximum_size(20); + swap_size_input->when(FL_WHEN_CHANGED); + swap_size_input->callback(swap_size_changed); + if (!setup.swap_enabled) swap_size_input->deactivate(); + reliefos_fltk::style(swap_size_input); snprintf(line, sizeof(line), "%s BIOS: %s UEFI: %s", T("Bootloaders:"), setup.skip_bios ? T("Skip") : T("Install"), setup.skip_uefi ? T("Skip") : T("Install")); @@ -1096,9 +1230,24 @@ static void build_confirm_page(void) make_label(bootloader_description(), CONTENT_X, 414, CONTENT_W, 100, 13, 1); } +static void build_update_log(int y, int height) +{ + make_label(T("Update details"), CONTENT_X, y, CONTENT_W, 26, 14, 0); + if (!update_log_buffer) update_log_buffer = new Fl_Text_Buffer(); + update_log_display = new Fl_Text_Display(CONTENT_X, y + 32, CONTENT_W, height); + update_log_display->buffer(update_log_buffer); + update_log_display->textfont(FL_HELVETICA); + update_log_display->textsize(13); + update_log_display->wrap_mode(Fl_Text_Display::WRAP_AT_BOUNDS, 0); + reliefos_fltk::style(update_log_display); + update_log_display->insert_position(update_log_buffer->length()); + update_log_display->show_insert_position(); + make_label("/var/log/installer-update.log", CONTENT_X, y + height + 40, CONTENT_W, 22, 12, 0); +} + static void build_progress_page(void) { - make_heading(mode_progress_title(), 20); + make_heading(update_job == 1 ? T("Checking system update") : mode_progress_title(), 20); make_label(T("Do not turn off this machine."), CONTENT_X, 62, CONTENT_W, 30, 15, 1); progress_text_box = make_label(progress_text, CONTENT_X, 112, @@ -1115,6 +1264,7 @@ static void build_progress_page(void) snprintf(percent, sizeof(percent), "%u%%", (unsigned)value); progress_percent_box = make_label(percent, CONTENT_X + CONTENT_W - 98, 162, 72, 30, 14, 0); + if (install_mode == INSTALLER_MODE_UPDATE) build_update_log(222, 290); } static void build_finish_page(void) @@ -1129,25 +1279,41 @@ static void build_finish_page(void) CONTENT_X, 62, CONTENT_W, 32, 15, 1); make_label(T("Remove the installation media, then restart."), CONTENT_X, 108, CONTENT_W, 32, 15, 1); + if (!setup.skip_uefi) { + char entry[128]; + int error = install_mode == INSTALLER_MODE_UPDATE + ? (snprintf(entry, sizeof(entry), "%s", update_info.boot_file), 0) + : installer_boot_entry_path(entry, sizeof(entry)); + if (!error) { + make_label(T("UEFI boot file (add it in firmware if no automatic entry appears):"), + CONTENT_X, 148, CONTENT_W, 28, 13, 1); + make_label(entry, CONTENT_X, 180, CONTENT_W, 28, 13, 1); + } + } if (reboot_error) { - make_label(T("Restart failed"), CONTENT_X, 154, + make_label(T("Restart failed"), CONTENT_X, 212, CONTENT_W, 28, 14, 1); snprintf(message, sizeof(message), "%s", strerror(reboot_error)); - make_label(message, CONTENT_X, 184, CONTENT_W, 36, 14, 1); + make_label(message, CONTENT_X, 242, CONTENT_W, 36, 14, 1); } } else { make_heading(install_mode == INSTALLER_MODE_UPDATE ? T("Update Failed") : T("Installation Failed"), 20); - make_label(T("No writes will continue after this error."), + make_label(install_mode == INSTALLER_MODE_UPDATE + ? T("Review the details, then check the update again to retry.") + : T("No writes will continue after this error."), CONTENT_X, 62, CONTENT_W, 32, 15, 1); make_label(status_text, CONTENT_X, 108, CONTENT_W, 42, 14, 1); make_label(detail_text, CONTENT_X, 154, CONTENT_W, 60, 14, 1); } + if (install_mode == INSTALLER_MODE_UPDATE) build_update_log(reboot_error ? 290 : 236, reboot_error ? 210 : 264); } static void clear_page_widgets(void) { disk_browser = NULL; + update_root_choice = update_esp_choice = NULL; + format_esp_checkbox = NULL; partition_browser = NULL; confirm_input = NULL; xorg_checkbox = NULL; @@ -1160,6 +1326,7 @@ static void clear_page_widgets(void) progress_text_box = NULL; progress_percent_box = NULL; progress_bar = NULL; + update_log_display = NULL; for (int i = 0; i < 5; ++i) account_inputs[i] = NULL; } @@ -1171,7 +1338,7 @@ static void show_page(void) content_group->begin(); switch ((enum installer_page)page) { case INSTALLER_PAGE_LANGUAGE: build_language_page(); break; - case INSTALLER_PAGE_THANKS: build_thanks_page(); break; + case INSTALLER_PAGE_LICENSE: build_license_page(); break; case INSTALLER_PAGE_WELCOME: build_welcome_page(); break; case INSTALLER_PAGE_MODE: build_mode_page(); break; case INSTALLER_PAGE_DISK: build_disk_page(); break; @@ -1289,6 +1456,10 @@ static int run_graphical(void) int main(int argc, char **argv) { + if (argc >= 2 && !strcmp(argv[1], "--update-worker")) { + setvbuf(stdout, NULL, _IOLBF, 0); + return installer_update_worker(argc, argv); + } struct installer_tty_context tty_context; int graphical = argc == 2 && strcmp(argv[1], "--graphical") == 0; @@ -1329,9 +1500,8 @@ int main(int argc, char **argv) tty_context.partition_set_type = installer_partition_set_type; tty_context.partition_delete = installer_partition_delete; tty_context.partition_format = installer_partition_format; - tty_context.prepare_update = prepare_update_target; tty_context.perform_install = perform_install; - tty_context.perform_update = perform_update; + tty_context.boot_entry_path = installer_boot_entry_path; return installer_tty_main(&tty_context); } puts("[installer.elf] starting installer wizard"); diff --git a/userland/apps/installer/model.c b/userland/apps/installer/model.c index 4543de91..09ea8908 100644 --- a/userland/apps/installer/model.c +++ b/userland/apps/installer/model.c @@ -72,15 +72,16 @@ enum installer_page installer_model_next(enum installer_page page, { switch (page) { case INSTALLER_PAGE_LANGUAGE: - return INSTALLER_PAGE_THANKS; - case INSTALLER_PAGE_THANKS: + return INSTALLER_PAGE_LICENSE; + case INSTALLER_PAGE_LICENSE: return INSTALLER_PAGE_WELCOME; case INSTALLER_PAGE_WELCOME: return INSTALLER_PAGE_MODE; case INSTALLER_PAGE_MODE: return INSTALLER_PAGE_DISK; case INSTALLER_PAGE_DISK: - return INSTALLER_PAGE_BOOTLOADERS; + return mode == INSTALLER_MODE_UPDATE ? INSTALLER_PAGE_CONFIRM + : INSTALLER_PAGE_BOOTLOADERS; case INSTALLER_PAGE_BOOTLOADERS: return mode == INSTALLER_MODE_UPDATE ? INSTALLER_PAGE_CONFIRM : INSTALLER_PAGE_PARTITIONS; @@ -99,10 +100,10 @@ enum installer_page installer_model_prev(enum installer_page page, enum installer_mode mode) { switch (page) { - case INSTALLER_PAGE_THANKS: + case INSTALLER_PAGE_LICENSE: return INSTALLER_PAGE_LANGUAGE; case INSTALLER_PAGE_WELCOME: - return INSTALLER_PAGE_THANKS; + return INSTALLER_PAGE_LICENSE; case INSTALLER_PAGE_MODE: return INSTALLER_PAGE_WELCOME; case INSTALLER_PAGE_DISK: @@ -112,7 +113,7 @@ enum installer_page installer_model_prev(enum installer_page page, case INSTALLER_PAGE_BOOTLOADERS: return INSTALLER_PAGE_DISK; case INSTALLER_PAGE_CONFIRM: - return mode == INSTALLER_MODE_UPDATE ? INSTALLER_PAGE_BOOTLOADERS + return mode == INSTALLER_MODE_UPDATE ? INSTALLER_PAGE_DISK : INSTALLER_PAGE_ACCOUNTS; case INSTALLER_PAGE_ACCOUNTS: return INSTALLER_PAGE_PARTITIONS; @@ -144,6 +145,7 @@ enum installer_action installer_model_action(enum installer_page page, : INSTALLER_ACTION_INSTALL; } if (page == INSTALLER_PAGE_FINISH) { + if (!install_success && mode == INSTALLER_MODE_UPDATE) return INSTALLER_ACTION_RETRY; return install_success ? INSTALLER_ACTION_RESTART : INSTALLER_ACTION_CLOSE; } return INSTALLER_ACTION_NEXT; @@ -151,7 +153,7 @@ enum installer_action installer_model_action(enum installer_page page, const char *installer_model_confirm_word(enum installer_mode mode) { - return mode == INSTALLER_MODE_UPDATE ? "UPDATE" : "INSTALL"; + return mode == INSTALLER_MODE_UPDATE ? "" : "INSTALL"; } int installer_model_steps(enum installer_mode mode, enum installer_page *out, @@ -160,7 +162,8 @@ int installer_model_steps(enum installer_mode mode, enum installer_page *out, int count = 0; enum installer_page page; for (page = INSTALLER_PAGE_LANGUAGE; page < INSTALLER_PAGE_COUNT; ++page) { - if ((page == INSTALLER_PAGE_ACCOUNTS || page == INSTALLER_PAGE_PARTITIONS) && + if ((page == INSTALLER_PAGE_ACCOUNTS || page == INSTALLER_PAGE_PARTITIONS || + page == INSTALLER_PAGE_BOOTLOADERS) && mode == INSTALLER_MODE_UPDATE) continue; if (count < cap) diff --git a/userland/apps/installer/model.h b/userland/apps/installer/model.h index cc1066b7..5d8f26a2 100644 --- a/userland/apps/installer/model.h +++ b/userland/apps/installer/model.h @@ -6,7 +6,7 @@ enum installer_page { INSTALLER_PAGE_LANGUAGE = 0, - INSTALLER_PAGE_THANKS, + INSTALLER_PAGE_LICENSE, INSTALLER_PAGE_WELCOME, INSTALLER_PAGE_MODE, INSTALLER_PAGE_DISK, @@ -28,6 +28,7 @@ enum installer_action { INSTALLER_ACTION_NEXT = 0, INSTALLER_ACTION_INSTALL, INSTALLER_ACTION_UPDATE, + INSTALLER_ACTION_RETRY, INSTALLER_ACTION_RESTART, INSTALLER_ACTION_CLOSE, }; @@ -36,12 +37,12 @@ enum installer_action { extern "C" { #endif -/* Both modes select firmware after the disk. Fresh installs then configure - * partitions/accounts; updates keep their partition layout and confirm. */ +/* Fresh installs configure firmware, partitions and accounts. Updates inspect + * the selected system and show firmware choices inside their review page. */ enum installer_page installer_model_next(enum installer_page page, enum installer_mode mode); -/* Backward transition; update confirmation returns to firmware selection. */ +/* Backward transition; update review returns to target selection. */ enum installer_page installer_model_prev(enum installer_page page, enum installer_mode mode); diff --git a/userland/apps/reliefmail/Kbuild b/userland/apps/reliefmail/Kbuild index c9a0bf90..4df6d43f 100644 --- a/userland/apps/reliefmail/Kbuild +++ b/userland/apps/reliefmail/Kbuild @@ -1,6 +1,6 @@ RELIEFOS_APP_NAME := reliefmail -RELIEFOS_APP_OBJECTS := compose_state.o compose_window.o dialogs.o editor.o html.o imap.o main.o mime.o model.o profile_io.o reader.o reader_render.o smtp.o sqlite_core.o store.o tasks.o tools.o transport.o ui_adapters.o ui_widgets.o vault.o window.o window_accounts.o window_actions.o window_query.o window_reader.o window_vault.o worker.o shared_reliefword_layout.o shared_reliefword_model.o -RELIEFOS_APP_C_OBJECTS := sqlite_core.o +RELIEFOS_APP_OBJECTS := compose_state.o compose_window.o dialogs.o editor.o html.o imap.o main.o mime.o model.o profile_io.o reader.o reader_render.o smtp.o store.o tasks.o tools.o transport.o ui_adapters.o ui_widgets.o vault.o window.o window_accounts.o window_actions.o window_query.o window_reader.o window_vault.o worker.o shared_reliefword_layout.o shared_reliefword_model.o +RELIEFOS_APP_C_OBJECTS := RELIEFOS_APP_CXX_OBJECTS := compose_state.o compose_window.o dialogs.o editor.o html.o imap.o main.o mime.o model.o profile_io.o reader.o reader_render.o smtp.o store.o tasks.o tools.o transport.o ui_adapters.o ui_widgets.o vault.o window.o window_accounts.o window_actions.o window_query.o window_reader.o window_vault.o worker.o shared_reliefword_layout.o shared_reliefword_model.o RELIEFOS_APP_ASM_OBJECTS := ifneq ($(RELIEFOS_CAPTURE_APP_KBUILD),1) diff --git a/userland/apps/reliefmail/sqlite_core.c b/userland/apps/reliefmail/sqlite_core.c deleted file mode 100644 index 332373cb..00000000 --- a/userland/apps/reliefmail/sqlite_core.c +++ /dev/null @@ -1,22 +0,0 @@ -/* ReliefMail owns a POSIX-VFS build of the same generated SQLite amalgamation. */ -#define SQLITE_THREADSAFE 0 -#define SQLITE_OMIT_LOAD_EXTENSION 1 -#define SQLITE_OMIT_WAL 1 -#define SQLITE_MAX_MMAP_SIZE 0 -#define SQLITE_DEFAULT_MEMSTATUS 0 - -/* Keep generated-amalgamation diagnostics scoped to its include boundary. */ -#if defined(__clang__) -#pragma clang diagnostic ignored "-Wunused-parameter" -#pragma clang diagnostic ignored "-Wunused-function" -#elif defined(__GNUC__) -#pragma GCC diagnostic ignored "-Wunused-parameter" -#pragma GCC diagnostic ignored "-Wunused-function" -#pragma GCC diagnostic ignored "-Wunused-but-set-variable" -#pragma GCC diagnostic ignored "-Wdiscarded-qualifiers" -#endif - -#include "sqlite3.c" - -/* This wrapper translation unit contains only the generated amalgamation; keep - * these narrow suppressions active through compiler end-of-unit diagnostics. */ diff --git a/userland/apps/reliefmail/store.cpp b/userland/apps/reliefmail/store.cpp index 0151188c..2aef9402 100644 --- a/userland/apps/reliefmail/store.cpp +++ b/userland/apps/reliefmail/store.cpp @@ -1023,7 +1023,7 @@ bool Store::open(const ProfilePaths &paths, StoreOpenMode mode, Text &error) { database_ = db; sqlite3_busy_timeout(db, 150); sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, nullptr); - if (!exec_sql(db, "PRAGMA journal_mode=DELETE;PRAGMA synchronous=FULL;PRAGMA foreign_keys=ON;" + if (!exec_sql(db, "PRAGMA journal_mode=DELETE;PRAGMA synchronous=FULL;PRAGMA foreign_keys=ON;PRAGMA mmap_size=0;" "PRAGMA trusted_schema=OFF;PRAGMA secure_delete=ON;", error, "cannot configure durable profile database") || !initialize_schema(db, data_fd_, error)) { close(); @@ -4737,7 +4737,7 @@ bool Store::restore_profile_impl(const char *source_path, SecretBytes backup_mas database_ = db; sqlite3_busy_timeout(db, 150); sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, nullptr); - if (!exec_sql(db, "PRAGMA journal_mode=DELETE;PRAGMA synchronous=FULL;PRAGMA foreign_keys=ON;" + if (!exec_sql(db, "PRAGMA journal_mode=DELETE;PRAGMA synchronous=FULL;PRAGMA foreign_keys=ON;PRAGMA mmap_size=0;" "PRAGMA trusted_schema=OFF;PRAGMA secure_delete=ON;", error, "cannot configure restored profile database") || !initialize_schema(db, data_fd_, error)) { sqlite3_close(db); diff --git a/userland/apps/reliefplayer/Kbuild b/userland/apps/reliefplayer/Kbuild index b568e1dd..1440cb2c 100644 --- a/userland/apps/reliefplayer/Kbuild +++ b/userland/apps/reliefplayer/Kbuild @@ -1,7 +1,7 @@ RELIEFOS_APP_NAME := reliefplayer -RELIEFOS_APP_OBJECTS := decoder.o main.o pcm.o session.o window.o worker.o +RELIEFOS_APP_OBJECTS := decoder.o main.o media.o pcm.o session.o window.o worker.o RELIEFOS_APP_C_OBJECTS := -RELIEFOS_APP_CXX_OBJECTS := decoder.o main.o pcm.o session.o window.o worker.o +RELIEFOS_APP_CXX_OBJECTS := decoder.o main.o media.o pcm.o session.o window.o worker.o RELIEFOS_APP_ASM_OBJECTS := ifneq ($(RELIEFOS_CAPTURE_APP_KBUILD),1) include $(srctree)/scripts/reliefos/Makefile.app-kbuild diff --git a/userland/apps/reliefplayer/decoder.cpp b/userland/apps/reliefplayer/decoder.cpp index 62a23f9d..81aad272 100644 --- a/userland/apps/reliefplayer/decoder.cpp +++ b/userland/apps/reliefplayer/decoder.cpp @@ -1,4 +1,5 @@ #include "decoder.h" +#include "media.h" #include "../relieftodo/storage.h" #include #include @@ -41,6 +42,7 @@ struct Frame { }; } // namespace struct Decoder::Impl { + MediaAudio media; int fd = -1; Info info; uint64_t size = 0, data = 0, position = 0; @@ -66,13 +68,13 @@ const char *error_text(Error error) { case Error::Cancelled: return "Cancelled"; case Error::Io: - return "Could not read the audio file"; + return "Could not read the media file"; case Error::Invalid: - return "Invalid or truncated audio file"; + return "Invalid or truncated media file"; case Error::Unsupported: - return "Unsupported audio format"; + return "Unsupported media format or codec"; case Error::TooLarge: - return "Audio file exceeds the supported limit"; + return "Media exceeds the supported size or resolution"; case Error::Memory: return "Not enough memory"; case Error::Device: @@ -80,11 +82,11 @@ const char *error_text(Error error) { case Error::Busy: return "Audio device is busy"; case Error::Playback: - return "Audio playback failed"; + return "Media playback failed"; case Error::Worker: - return "Audio process stopped unexpectedly"; + return "Playback process stopped unexpectedly"; } - return "Audio playback failed"; + return "Media playback failed"; } bool Decoder::Impl::wav(Error &error, Progress progress, void *opaque) { uint64_t end = uint64_t(le32(input + 4)) + 8, at = 12; @@ -294,8 +296,12 @@ bool Decoder::open(const char *path, Error &error, Progress progress, return false; } bool wav = !memcmp(p_->input, "RIFF", 4) && !memcmp(p_->input + 8, "WAVE", 4); - if (!(wav ? p_->wav(error, progress, opaque) - : p_->scan(error, progress, opaque))) { + const char *extension = strrchr(path, '.'); + bool mp4 = !memcmp(p_->input + 4, "ftyp", 4) || + (extension && !strcasecmp(extension, ".mp4")); + if (!(mp4 ? p_->media.open(path, p_->info, error, progress, opaque) + : wav ? p_->wav(error, progress, opaque) + : p_->scan(error, progress, opaque))) { close(); return false; } @@ -306,7 +312,9 @@ const Info &Decoder::info() const { static const Info empty; return p_ ? p_->info : empty; } -uint64_t Decoder::position() const { return p_ ? p_->position : 0; } +uint64_t Decoder::position() const { + return !p_ ? 0 : p_->info.kind == Kind::Mp4 ? p_->media.position() : p_->position; +} int Decoder::Impl::frame(Error &error) { if (next >= index.size()) return 0; @@ -347,6 +355,8 @@ bool Decoder::seek(uint64_t target, Error &error) { } if (target > p_->info.frames) target = p_->info.frames; + if (p_->info.kind == Kind::Mp4) + return p_->media.seek(target, error); p_->pcm_at = p_->pcm_count = p_->failures = 0; if (p_->info.kind == Kind::Wav) { p_->position = target; @@ -382,6 +392,8 @@ int Decoder::read(int16_t *stereo, unsigned frames, Error &error) { error = Error::Invalid; return -1; } + if (p_->info.kind == Kind::Mp4) + return p_->media.read(stereo, frames > 4096 ? 4096 : frames, error); if (!frames || p_->position >= p_->info.frames) return 0; if (frames > 4096) diff --git a/userland/apps/reliefplayer/decoder.h b/userland/apps/reliefplayer/decoder.h index 363e4b3f..ed937b0a 100644 --- a/userland/apps/reliefplayer/decoder.h +++ b/userland/apps/reliefplayer/decoder.h @@ -16,11 +16,13 @@ enum class Error : uint32_t { Playback, Worker }; -enum class Kind : uint32_t { Wav, Mp3 }; +enum class Kind : uint32_t { Wav, Mp3, Mp4 }; struct Info { Kind kind = Kind::Wav; uint32_t rate = 0, channels = 0, bits = 0; uint64_t frames = 0; + uint32_t width = 0, height = 0; + bool has_audio = true; }; using Progress = bool (*)(void *, uint64_t, uint64_t); const char *error_text(Error error); diff --git a/userland/apps/reliefplayer/main.cpp b/userland/apps/reliefplayer/main.cpp index 869123fb..f3b97247 100644 --- a/userland/apps/reliefplayer/main.cpp +++ b/userland/apps/reliefplayer/main.cpp @@ -17,7 +17,7 @@ void ready(void *p) { auto &start = *static_cast(p); start.window->redraw(); Fl::flush(); - puts("[reliefplayer.elf] FLTK audio player ready"); + puts("[reliefplayer.elf] FLTK media player ready"); fflush(stdout); if (start.path) start.window->open_path(start.path); @@ -28,7 +28,7 @@ int main(int argc, char **argv) { bool literal = false; for (int n = 1; n < argc; ++n) { if (!literal && !strcmp(argv[n], "--help")) { - puts("Usage: reliefplayer [--] [audio.mp3|audio.wav]\nNative FLTK audio " + puts("Usage: reliefplayer [--] [audio.mp3|audio.wav|video.mp4]\nNative FLTK media " "player."); return 0; } diff --git a/userland/apps/reliefplayer/media.cpp b/userland/apps/reliefplayer/media.cpp new file mode 100644 index 00000000..e6c5f4a8 --- /dev/null +++ b/userland/apps/reliefplayer/media.cpp @@ -0,0 +1,550 @@ +#include "media.h" +#include "../relieftodo/storage.h" +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include +#include +#include +#include +#include +#include +#include +} +namespace rp { +namespace { +constexpr AVRational Milliseconds{1, 1000}, Samples{1, 48000}; +Error media_error(int code) { + if (code == AVERROR_EXIT) + return Error::Cancelled; + if (code == AVERROR(ENOMEM)) + return Error::Memory; + if (code == AVERROR_DECODER_NOT_FOUND) + return Error::Unsupported; + if (code == AVERROR(EIO) || code == AVERROR(EACCES)) + return Error::Io; + return Error::Invalid; +} +bool failure(const char *stage, int code, Error &error) { + char text[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(code, text, sizeof(text)); + fprintf(stderr, "[reliefplayer.elf] media stage=%s code=%d detail=%s\n", + stage, code, text); + error = media_error(code); + return false; +} +// Custom AVIO confines the demuxer to one already-open regular file. No URL, +// network protocol, external data reference or shell command is evaluated. +struct Input { + int fd = -1, stream = -1; + int64_t size = 0, origin = 0; + AVFormatContext *format = nullptr; + AVIOContext *io = nullptr; + AVCodecContext *codec = nullptr; + AVPacket *packet = nullptr; + AVFrame *frame = nullptr; + Progress progress = nullptr; + void *opaque = nullptr; + bool flushed = false, ready = false; + ~Input() { + av_frame_free(&frame); + av_packet_free(&packet); + avcodec_free_context(&codec); + avformat_close_input(&format); + if (io) + av_freep(&io->buffer); + avio_context_free(&io); + if (fd >= 0) + ::close(fd); + } + static int interrupt(void *p) { + auto &s = *static_cast(p); + return s.progress && !s.progress(s.opaque, s.ready ? 1 : 0, 1); + } + static int read(void *p, uint8_t *bytes, int count) { + auto &s = *static_cast(p); + if (interrupt(p)) + return AVERROR_EXIT; + ssize_t n; + do { + n = ::read(s.fd, bytes, count); + } while (n < 0 && errno == EINTR); + return n > 0 ? int(n) : n == 0 ? AVERROR_EOF : AVERROR(errno); + } + static int64_t seek(void *p, int64_t offset, int whence) { + auto &s = *static_cast(p); + if (whence & AVSEEK_SIZE) + return s.size; + off_t result = lseek(s.fd, offset, whence & ~AVSEEK_FORCE); + return result < 0 ? AVERROR(errno) : result; + } + bool open(const char *path, AVMediaType type, Error &error, Progress cb, + void *ctx) { + progress = cb; + opaque = ctx; + fd = ::open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + struct stat st{}; + if (fd < 0 || fstat(fd, &st) < 0) { + error = Error::Io; + return false; + } + if (!S_ISREG(st.st_mode) || st.st_size < 12) { + error = Error::Invalid; + return false; + } + size = st.st_size; + format = avformat_alloc_context(); + auto *buffer = static_cast(av_malloc(32768)); + if (!format || !buffer) { + av_free(buffer); + error = Error::Memory; + return false; + } + io = avio_alloc_context(buffer, 32768, 0, this, read, nullptr, seek); + if (!io) { + av_free(buffer); + error = Error::Memory; + return false; + } + format->pb = io; + format->flags |= AVFMT_FLAG_CUSTOM_IO; + format->interrupt_callback = {interrupt, this}; + format->probesize = 4 * 1024 * 1024; + format->max_analyze_duration = 5 * AV_TIME_BASE; + AVDictionary *options = nullptr; + av_dict_set(&options, "enable_drefs", "0", 0); + av_dict_set(&options, "use_absolute_path", "0", 0); + int n = avformat_open_input(&format, nullptr, av_find_input_format("mov"), + &options); + av_dict_free(&options); + if (n < 0) + return failure("open MP4", n, error); + n = avformat_find_stream_info(format, nullptr); + if (n < 0) + return failure("read MP4 streams", n, error); + origin = format->start_time == AV_NOPTS_VALUE ? 0 : format->start_time; + const AVCodec *decoder = nullptr; + stream = av_find_best_stream(format, type, -1, -1, &decoder, 0); + ready = true; + if (stream == AVERROR_STREAM_NOT_FOUND) + return true; + if (stream < 0) + return failure("find codec", stream, error); + auto *parameters = format->streams[stream]->codecpar; + // Keep host validation and target builds on the same supported codec set. + bool supported = type == AVMEDIA_TYPE_VIDEO + ? parameters->codec_id == AV_CODEC_ID_H264 || + parameters->codec_id == AV_CODEC_ID_HEVC || + parameters->codec_id == AV_CODEC_ID_MPEG4 + : parameters->codec_id == AV_CODEC_ID_AAC || + parameters->codec_id == AV_CODEC_ID_MP3; + if (!supported) { + error = Error::Unsupported; + return false; + } + codec = avcodec_alloc_context3(decoder); + packet = av_packet_alloc(); + frame = av_frame_alloc(); + if (!codec || !packet || !frame) { + error = Error::Memory; + return false; + } + n = avcodec_parameters_to_context(codec, parameters); + if (n < 0) + return failure("codec parameters", n, error); + codec->thread_count = 1; + codec->max_pixels = 4096 * 2160; + codec->pkt_timebase = format->streams[stream]->time_base; + n = avcodec_open2(codec, decoder, nullptr); + return n >= 0 || failure("open codec", n, error); + } + int next(Error &error) { + for (unsigned tries = 0; tries < 4096; ++tries) { + if (interrupt(this)) { + error = Error::Cancelled; + return -1; + } + av_frame_unref(frame); + int n = avcodec_receive_frame(codec, frame); + if (n >= 0) + return 1; + if (n == AVERROR_EOF) + return 0; + if (n != AVERROR(EAGAIN)) { + failure("decode", n, error); + return -1; + } + if (flushed) { + error = Error::Invalid; + return -1; + } + do { + av_packet_unref(packet); + n = av_read_frame(format, packet); + } while (n >= 0 && packet->stream_index != stream && ++tries < 4096); + if (tries >= 4096) + break; + if (n < 0 && n != AVERROR_EOF) { + failure("read packet", n, error); + return -1; + } + if (n == AVERROR_EOF) + flushed = true; + n = avcodec_send_packet(codec, flushed ? nullptr : packet); + av_packet_unref(packet); + if (n < 0) { + failure("submit packet", n, error); + return -1; + } + } + error = Error::Invalid; + return -1; + } + int64_t timestamp(AVRational units, int64_t fallback) const { + if (frame->best_effort_timestamp == AV_NOPTS_VALUE) + return fallback; + return av_rescale_q(frame->best_effort_timestamp, + format->streams[stream]->time_base, units) - + av_rescale_q(origin, AV_TIME_BASE_Q, units); + } + bool reposition(uint64_t ms, Error &error) { + int64_t at = int64_t(ms) * 1000 + origin; + int n = av_seek_frame(format, -1, at, AVSEEK_FLAG_BACKWARD); + if (n < 0) + return failure("seek", n, error); + avcodec_flush_buffers(codec); + av_packet_unref(packet); + av_frame_unref(frame); + flushed = false; + return true; + } +}; +template T *allocate(Error &error) { + void *memory = malloc(sizeof(T)); + if (!memory) { + error = Error::Memory; + return nullptr; + } + return new (memory, rt::Placement{}) T; +} +template void release(T *&p) { + if (p) { + p->~T(); + free(p); + p = nullptr; + } +} +} // namespace +struct MediaAudio::Impl { + Input input; + SwrContext *resample = nullptr; + rt::Array pcm; + uint64_t position = 0, total = 0; + int64_t start = 0, next = 0; + unsigned count = 0; + bool eof = false; + ~Impl() { swr_free(&resample); } +}; +MediaAudio::~MediaAudio() { release(p_); } +bool MediaAudio::open(const char *path, Info &info, Error &error, Progress cb, + void *ctx) { + release(p_); + p_ = allocate(error); + if (!p_ || !p_->input.open(path, AVMEDIA_TYPE_AUDIO, error, cb, ctx)) + return false; + auto &in = p_->input; + int video = + av_find_best_stream(in.format, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); + if (video < 0) { + error = Error::Unsupported; + return false; + } + auto *v = in.format->streams[video]->codecpar; + if (v->width <= 0 || v->height <= 0 || v->width > 4096 || v->height > 2160) { + error = Error::TooLarge; + return false; + } + info.kind = Kind::Mp4; + info.width = v->width; + info.height = v->height; + info.has_audio = in.stream >= 0; + info.rate = info.has_audio ? 48000 : 1000; + info.channels = info.has_audio ? 2 : 0; + info.bits = info.has_audio ? 16 : 0; + if (in.format->duration <= 0 || + in.format->duration > int64_t(7 * 86400) * AV_TIME_BASE) { + error = Error::Invalid; + return false; + } + info.frames = + av_rescale_q(in.format->duration, AV_TIME_BASE_Q, {1, int(info.rate)}); + p_->total = info.frames; + if (!info.has_audio) + return true; + auto *c = in.codec; + if (c->sample_rate < 8000 || c->sample_rate > 192000 || + c->ch_layout.nb_channels < 1 || c->ch_layout.nb_channels > 8) { + error = Error::Unsupported; + return false; + } + AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO; + int n = swr_alloc_set_opts2(&p_->resample, &stereo, AV_SAMPLE_FMT_S16, 48000, + &c->ch_layout, c->sample_fmt, c->sample_rate, 0, + nullptr); + if (n >= 0) + n = swr_init(p_->resample); + return n >= 0 || failure("audio conversion", n, error); +} +uint64_t MediaAudio::position() const { return p_ ? p_->position : 0; } +bool MediaAudio::seek(uint64_t frame, Error &error) { + if (!p_) { + error = Error::Invalid; + return false; + } + p_->position = frame > p_->total ? p_->total : frame; + p_->count = 0; + p_->eof = p_->position == p_->total; + p_->next = int64_t(p_->position); + if (!p_->input.codec || p_->eof) + return true; + if (!p_->input.reposition(p_->position * 1000 / 48000, error)) + return false; + swr_close(p_->resample); + int n = swr_init(p_->resample); + return n >= 0 || failure("reset audio conversion", n, error); +} +int MediaAudio::read(int16_t *out, unsigned frames, Error &error) { + if (!p_ || !p_->resample) { + error = Error::Invalid; + return -1; + } + if (frames > p_->total - p_->position) + frames = unsigned(p_->total - p_->position); + unsigned written = 0; + while (written < frames) { + if (p_->count && int64_t(p_->position) >= p_->start + p_->count) + p_->count = 0; + if (!p_->count && !p_->eof) { + int n = p_->input.next(error); + if (n < 0) + return -1; + auto *f = p_->input.frame; + auto *c = p_->input.codec; + if (n && + (f->sample_rate != c->sample_rate || f->format != c->sample_fmt || + av_channel_layout_compare(&f->ch_layout, &c->ch_layout))) { + error = Error::Unsupported; + return -1; + } + int capacity = swr_get_out_samples(p_->resample, n ? f->nb_samples : 0); + if (capacity < 0 || capacity > 1048576) { + error = Error::TooLarge; + return -1; + } + if (!p_->pcm.resize(size_t(capacity) * 2 + 2)) { + error = Error::Memory; + return -1; + } + auto *dest = reinterpret_cast(p_->pcm.data()); + int64_t delay = swr_get_delay(p_->resample, 48000); + p_->start = n ? p_->input.timestamp(Samples, p_->next) - delay : p_->next; + int samples = swr_convert( + p_->resample, &dest, capacity, + n ? const_cast(f->extended_data) : nullptr, + n ? f->nb_samples : 0); + if (samples < 0) { + failure("resample", samples, error); + return -1; + } + p_->count = unsigned(samples); + p_->next = p_->start + samples; + if (!n) + p_->eof = true; + if (!p_->count && !p_->eof) + continue; + } + unsigned n = frames - written; + if (!p_->count || int64_t(p_->position) < p_->start) { + if (p_->count && uint64_t(p_->start) - p_->position < n) + n = unsigned(p_->start - p_->position); + memset(out + written * 2, 0, n * 4); + } else { + unsigned offset = unsigned(int64_t(p_->position) - p_->start); + if (offset >= p_->count) { + p_->count = 0; + continue; + } + if (n > p_->count - offset) + n = p_->count - offset; + memcpy(out + written * 2, p_->pcm.data() + offset * 2, n * 4); + } + p_->position += n; + written += n; + } + return int(written); +} +struct VideoDecoder::Impl { + Input input; + SwsContext *scale = nullptr; + rt::Array rgb; + bool pending = false, eof = false; + int rotation = 0; + rt::Array rotated; + int64_t at = 0, next = 0; + ~Impl() { sws_freeContext(scale); } +}; +VideoDecoder::~VideoDecoder() { release(p_); } +bool VideoDecoder::open(const char *path, Error &error, Progress cb, + void *ctx) { + release(p_); + p_ = allocate(error); + if (!p_ || !p_->input.open(path, AVMEDIA_TYPE_VIDEO, error, cb, ctx)) + return false; + if (p_->input.stream < 0) { + error = Error::Unsupported; + return false; + } + auto *parameters = p_->input.format->streams[p_->input.stream]->codecpar; + const auto *matrix = av_packet_side_data_get(parameters->coded_side_data, + parameters->nb_coded_side_data, + AV_PKT_DATA_DISPLAYMATRIX); + if (matrix && matrix->size >= 9 * sizeof(int32_t)) { + double angle = av_display_rotation_get( + reinterpret_cast(matrix->data)); + if (isfinite(angle)) { + int turn = int(lround(angle / 90)); + if (fabs(angle - turn * 90) < 1) + p_->rotation = (turn % 4 + 4) % 4; + } + } + return true; +} +bool VideoDecoder::seek(uint64_t ms, Error &error) { + if (!p_ || !p_->input.reposition(ms, error)) + return false; + p_->pending = p_->eof = false; + p_->next = int64_t(ms); + return true; +} +bool VideoDecoder::pump(uint64_t ms, VideoShared *shared, Error &error) { + if (!p_ || !shared) { + error = Error::Memory; + return false; + } + auto &s = *p_; + // A bounded batch permits control commands even when decoding a long GOP. + for (unsigned frames = 0; frames < 12 && !s.eof; ++frames) { + if (!s.pending) { + int n = s.input.next(error); + if (n < 0) + return false; + if (!n) { + s.eof = true; + break; + } + s.at = s.input.timestamp(Milliseconds, s.next); + AVRational fps = av_guess_frame_rate( + s.input.format, s.input.format->streams[s.input.stream], + s.input.frame); + int64_t step = fps.num > 0 && fps.den > 0 + ? av_rescale_q(1, av_inv_q(fps), Milliseconds) + : 40; + s.next = s.at + (step > 0 ? step : 1); + s.pending = true; + } + if (s.at > int64_t(ms)) + break; + auto *f = s.input.frame; + if (f->width <= 0 || f->height <= 0 || f->width > 4096 || + f->height > 2160) { + error = Error::TooLarge; + return false; + } + // Apply pixel aspect ratio before fitting the RGB snapshot into its limit. + AVRational aspect = av_guess_sample_aspect_ratio( + s.input.format, s.input.format->streams[s.input.stream], f); + double ratio = double(f->width) / f->height; + if (aspect.num > 0 && aspect.den > 0) + ratio *= av_q2d(aspect); + if (ratio < .05 || ratio > 20) { + error = Error::Unsupported; + return false; + } + unsigned w = unsigned(f->height * ratio); + if (w > VideoWidth) + w = VideoWidth; + unsigned h = unsigned(w / ratio); + if (h > VideoHeight) { + h = VideoHeight; + w = unsigned(h * ratio); + } + if (!w) + w = 1; + if (!h) + h = 1; + if (!s.rgb.resize(size_t(w) * h * 3)) { + error = Error::Memory; + return false; + } + s.scale = sws_getCachedContext( + s.scale, f->width, f->height, AVPixelFormat(f->format), int(w), int(h), + AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); + if (!s.scale) { + error = Error::Memory; + return false; + } + int colorspace = + f->colorspace == AVCOL_SPC_BT709 ? SWS_CS_ITU709 : SWS_CS_ITU601; + const int *coefficients = sws_getCoefficients(colorspace); + sws_setColorspaceDetails(s.scale, coefficients, + f->color_range == AVCOL_RANGE_JPEG, coefficients, + 1, 0, 1 << 16, 1 << 16); + uint8_t *dest[4] = {s.rgb.data(), nullptr, nullptr, nullptr}; + int strides[4] = {int(w * 3), 0, 0, 0}; + int n = + sws_scale(s.scale, f->data, f->linesize, 0, f->height, dest, strides); + if (n != int(h)) { + error = Error::Playback; + return false; + } + unsigned output_w = (s.rotation & 1) ? h : w; + unsigned output_h = (s.rotation & 1) ? w : h; + const unsigned char *pixels = s.rgb.data(); + if (s.rotation) { + if (!s.rotated.resize(size_t(w) * h * 3)) { + error = Error::Memory; + return false; + } + for (unsigned y = 0; y < h; ++y) + for (unsigned x = 0; x < w; ++x) { + unsigned dx = s.rotation == 1 ? y + : s.rotation == 2 ? w - x - 1 + : h - y - 1; + unsigned dy = s.rotation == 1 ? w - x - 1 + : s.rotation == 2 ? h - y - 1 + : x; + memcpy(s.rotated.data() + (dy * output_w + dx) * 3, + s.rgb.data() + (y * w + x) * 3, 3); + } + pixels = s.rotated.data(); + } + // Portrait frames fit the same maximum byte count; validate both sides at + // the reader using the larger dimension rather than assuming landscape. + if (!__atomic_exchange_n(&shared->lock, 1u, __ATOMIC_ACQUIRE)) { + memcpy(shared->pixels, pixels, size_t(output_w) * output_h * 3); + shared->width = output_w; + shared->height = output_h; + ++shared->serial; + __atomic_store_n(&shared->lock, 0u, __ATOMIC_RELEASE); + } else + return true; // Retry this frame if a paused seek races a GUI copy. + s.pending = false; + } + return true; +} +} // namespace rp diff --git a/userland/apps/reliefplayer/media.h b/userland/apps/reliefplayer/media.h new file mode 100644 index 00000000..23caf12a --- /dev/null +++ b/userland/apps/reliefplayer/media.h @@ -0,0 +1,41 @@ +#ifndef RELIEFPLAYER_MEDIA_H +#define RELIEFPLAYER_MEDIA_H +#include "decoder.h" +namespace rp { +// RGB snapshots are bounded independently of the source resolution. The worker +// and GUI only try the lock; neither waits for the other process. +constexpr unsigned VideoWidth = 1280, VideoHeight = 720; +constexpr unsigned VideoBytes = VideoWidth * VideoHeight * 3; +struct VideoShared { + uint32_t lock, serial, width, height; + unsigned char pixels[VideoBytes]; +}; +class MediaAudio { + struct Impl; + Impl *p_ = nullptr; + +public: + MediaAudio() = default; + MediaAudio(const MediaAudio &) = delete; + MediaAudio &operator=(const MediaAudio &) = delete; + ~MediaAudio(); + bool open(const char *, Info &, Error &, Progress, void *); + bool seek(uint64_t, Error &); + int read(int16_t *, unsigned, Error &); + uint64_t position() const; +}; +class VideoDecoder { + struct Impl; + Impl *p_ = nullptr; + +public: + VideoDecoder() = default; + VideoDecoder(const VideoDecoder &) = delete; + VideoDecoder &operator=(const VideoDecoder &) = delete; + ~VideoDecoder(); + bool open(const char *, Error &, Progress, void *); + bool seek(uint64_t milliseconds, Error &); + bool pump(uint64_t milliseconds, VideoShared *, Error &); +}; +} // namespace rp +#endif diff --git a/userland/apps/reliefplayer/reliefplayer.desktop b/userland/apps/reliefplayer/reliefplayer.desktop index ad4432a3..09dc669a 100644 --- a/userland/apps/reliefplayer/reliefplayer.desktop +++ b/userland/apps/reliefplayer/reliefplayer.desktop @@ -1,14 +1,14 @@ [Desktop Entry] Type=Application Name=ReliefPlayer -GenericName=Audio Player -GenericName[zh_CN]=音频播放器 -Comment=Play local MP3 and WAV audio -Comment[zh_CN]=播放本地 MP3 和 WAV 音频,支持暂停、定位和音量控制 +GenericName=Media Player +GenericName[zh_CN]=媒体播放器 +Comment=Play local MP3 and WAV audio and MP4 video +Comment[zh_CN]=播放本地 MP3、WAV 音频和 MP4 视频 Exec=/usr/bin/reliefplayer %f TryExec=/usr/bin/reliefplayer Icon=reliefos-reliefplayer Terminal=false -Categories=AudioVideo;Audio;Player; -MimeType=audio/mpeg;audio/x-wav;audio/wav;audio/vnd.wave; +Categories=AudioVideo;Audio;Video;Player; +MimeType=audio/mpeg;audio/x-wav;audio/wav;audio/vnd.wave;video/mp4; StartupWMClass=reliefos-reliefplayer diff --git a/userland/apps/reliefplayer/session.cpp b/userland/apps/reliefplayer/session.cpp index 4efcff62..544bf314 100644 --- a/userland/apps/reliefplayer/session.cpp +++ b/userland/apps/reliefplayer/session.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include namespace rp { @@ -22,6 +24,7 @@ Session::~Session() { } } close_pipes(); + free(pixels_); } void Session::close_pipes() { if (commands_ >= 0) @@ -30,10 +33,26 @@ void Session::close_pipes() { close(statuses_); commands_ = statuses_ = -1; queued_ = received_ = 0; + if (video_) + munmap(video_, sizeof(VideoShared)); + video_ = nullptr; } void Session::start() { pending_open_ = false; closing_ = false; + frame_failed_ = false; + free(pixels_); + pixels_ = nullptr; + width_ = height_ = serial_ = 0; + int frame_fd = memfd_create("reliefplayer-video", MFD_CLOEXEC); + if (frame_fd >= 0 && ftruncate(frame_fd, sizeof(VideoShared)) == 0) { + void *memory = mmap(nullptr, sizeof(VideoShared), PROT_READ | PROT_WRITE, + MAP_SHARED, frame_fd, 0); + if (memory != MAP_FAILED) + video_ = static_cast(memory); + } + if (frame_fd >= 0) + close(frame_fd); int command_pipe[2] = {-1, -1}, status_pipe[2] = {-1, -1}; if (pipe(command_pipe) < 0 || pipe(status_pipe) < 0 || !nonblocking(command_pipe[0]) || !nonblocking(command_pipe[1]) || @@ -46,6 +65,7 @@ void Session::start() { close(fd); status_.state = State::Failed; status_.error = Error::Worker; + close_pipes(); return; } child_ = fork(); @@ -55,7 +75,7 @@ void Session::start() { signal(SIGTERM, SIG_DFL); signal(SIGINT, SIG_DFL); int result = - worker(path_, command_pipe[0], status_pipe[1], initial_volume_); + worker(path_, command_pipe[0], status_pipe[1], initial_volume_, video_); close(command_pipe[0]); close(status_pipe[1]); _exit(result); @@ -67,6 +87,7 @@ void Session::start() { close(status_pipe[0]); status_.state = State::Failed; status_.error = Error::Worker; + close_pipes(); return; } commands_ = command_pipe[1]; @@ -137,10 +158,31 @@ void Session::poll() { received_ += unsigned(n); if (received_ == sizeof(PlaybackStatus)) { received_ = 0; - if (partial_.magic == Protocol && !closing_) + if (partial_.magic == Protocol && !closing_ && !frame_failed_) status_ = partial_; } } + if (video_ && !closing_ && !frame_failed_ && + !__atomic_exchange_n(&video_->lock, 1u, __ATOMIC_ACQUIRE)) { + if (video_->serial != serial_ && video_->width && video_->height && + video_->width <= VideoWidth && video_->height <= VideoWidth && + uint64_t(video_->width) * video_->height * 3 <= VideoBytes) { + if (!pixels_) + pixels_ = static_cast(malloc(VideoBytes)); + if (pixels_) { + width_ = video_->width; + height_ = video_->height; + memcpy(pixels_, video_->pixels, size_t(width_) * height_ * 3); + serial_ = video_->serial; + } else { + frame_failed_ = true; + status_.state = State::Failed; + status_.error = Error::Memory; + command(Op::Quit); + } + } + __atomic_store_n(&video_->lock, 0u, __ATOMIC_RELEASE); + } int result = 0; pid_t reaped = waitpid(child_, &result, WNOHANG); if (reaped == child_ || (reaped < 0 && errno == ECHILD)) { diff --git a/userland/apps/reliefplayer/session.h b/userland/apps/reliefplayer/session.h index dc23f2f7..60a98842 100644 --- a/userland/apps/reliefplayer/session.h +++ b/userland/apps/reliefplayer/session.h @@ -1,17 +1,22 @@ #ifndef RELIEFPLAYER_SESSION_H #define RELIEFPLAYER_SESSION_H #include "protocol.h" +#include "media.h" #include namespace rp { class Session { int commands_ = -1, statuses_ = -1; pid_t child_ = -1; bool closing_ = false, pending_open_ = false; + bool frame_failed_ = false; uint64_t deadline_ = 0; char path_[4096]{}; unsigned initial_volume_ = 70, received_ = 0, queued_ = 0; Command queue_[16]; PlaybackStatus partial_, status_; + VideoShared *video_ = nullptr; + unsigned char *pixels_ = nullptr; + uint32_t serial_ = 0, width_ = 0, height_ = 0; void start(); void close_pipes(); void flush(); @@ -27,6 +32,10 @@ class Session { void shutdown(); bool active() const { return child_ > 0 || pending_open_; } const PlaybackStatus &status() const { return status_; } + const unsigned char *pixels() const { return pixels_; } + unsigned width() const { return width_; } + unsigned height() const { return height_; } + uint32_t video_serial() const { return serial_; } }; } // namespace rp #endif diff --git a/userland/apps/reliefplayer/window.cpp b/userland/apps/reliefplayer/window.cpp index 782721d2..7d7d3091 100644 --- a/userland/apps/reliefplayer/window.cpp +++ b/userland/apps/reliefplayer/window.cpp @@ -1,17 +1,56 @@ #include "window.h" #include +#include #include #include namespace rp { namespace { +class VideoView : public Fl_Widget { + const Session &session_; + int drawn_width_ = 1, drawn_height_ = 1; + static void row(void *opaque, int x, int y, int width, unsigned char *out) { + auto &s = *static_cast(opaque); + unsigned source_y = + unsigned(y) * s.session_.height() / unsigned(s.drawn_height_); + const unsigned char *line = + s.session_.pixels() + source_y * s.session_.width() * 3; + for (int n = 0; n < width; ++n) { + unsigned source_x = + unsigned(x + n) * s.session_.width() / unsigned(s.drawn_width_); + memcpy(out + n * 3, line + source_x * 3, 3); + } + } + void draw() override { + fl_push_clip(x(), y(), w(), h()); + fl_color(FL_BLACK); + fl_rectf(x(), y(), w(), h()); + if (session_.pixels() && session_.width() && session_.height()) { + drawn_width_ = w(); + drawn_height_ = int(int64_t(w()) * session_.height() / session_.width()); + if (drawn_height_ > h()) { + drawn_height_ = h(); + drawn_width_ = int(int64_t(h()) * session_.width() / session_.height()); + } + if (drawn_width_ > 0 && drawn_height_ > 0) + fl_draw_image(row, this, x() + (w() - drawn_width_) / 2, + y() + (h() - drawn_height_) / 2, drawn_width_, + drawn_height_, 3); + } + fl_pop_clip(); + } + +public: + explicit VideoView(const Session &session) + : Fl_Widget(0, 0, 1, 1), session_(session) {} +}; const char *state_label(State state) { switch (state) { case State::Idle: - return T("Open an MP3 or WAV file to begin"); + return T("Open an MP3, WAV or MP4 file to begin"); case State::Loading: - return T("Reading audio information..."); + return T("Reading media information..."); case State::Playing: - return T("Playing audio"); + return T("Playing"); case State::Paused: return T("Paused"); case State::Stopped: @@ -19,7 +58,7 @@ const char *state_label(State state) { case State::Ended: return T("Playback complete"); case State::Failed: - return T("Audio playback failed"); + return T("Media playback failed"); } return ""; } @@ -45,16 +84,18 @@ int ProgressSlider::handle(int event) { PlayerWindow::PlayerWindow() : Fl_Double_Window(720, 350, "ReliefPlayer") { xclass("reliefos-reliefplayer"); begin(); - open_ = new Fl_Button(0, 0, 1, 1, T("Open audio...")); + open_ = new Fl_Button(0, 0, 1, 1, T("Open media...")); help_ = new Fl_Button(0, 0, 1, 1, T("Help")); panel_ = new Fl_Box(0, 0, 1, 1); panel_->box(FL_ENGRAVED_BOX); - title_ = new Fl_Box(0, 0, 1, 1, T("No audio file selected")); + title_ = new Fl_Box(0, 0, 1, 1, T("No media file selected")); title_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_CLIP); title_->labelfont(FL_HELVETICA_BOLD); title_->labelsize(19); - detail_ = new Fl_Box(0, 0, 1, 1, "MP3 / WAV"); + detail_ = new Fl_Box(0, 0, 1, 1, "MP3 / WAV / MP4"); detail_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_CLIP); + video_ = new VideoView(session_); + video_->hide(); elapsed_ = new Fl_Box(0, 0, 1, 1, "00:00"); elapsed_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); duration_ = new Fl_Box(0, 0, 1, 1, "00:00"); @@ -89,7 +130,7 @@ PlayerWindow::PlayerWindow() : Fl_Double_Window(720, 350, "ReliefPlayer") { callback(action, this); end(); resizable(this); - size_range(600, 330); + size_range(600, 350); layout(); update(); Fl::add_timeout(.03, tick, this); @@ -102,6 +143,7 @@ void PlayerWindow::layout() { title_->resize(28, 73, w() - 56, 29); detail_->resize(28, 108, w() - 56, 24); int y = h() - 190; + video_->resize(16, 156, w() - 32, y > 166 ? y - 166 : 1); elapsed_->resize(16, y, 120, 24); duration_->resize(w() - 136, y, 120, 24); seek_->resize(16, y + 27, w() - 32, 25); @@ -126,6 +168,10 @@ void PlayerWindow::open_path(const char *path) { } if (!session_.open(path, mute_->value() ? 0 : unsigned(volume_->value()))) return; + video_->hide(); + video_serial_ = 0; + video_layout_ = false; + size_range(600, 350); if (path != path_) memcpy(path_, path, strlen(path) + 1); const char *base = strrchr(path_, '/'); @@ -138,9 +184,9 @@ void PlayerWindow::open_path(const char *path) { update(); } void PlayerWindow::choose() { - const char *path = - fl_file_chooser(T("Open audio"), T("Audio files (*.{mp3,MP3,wav,WAV})"), - path_[0] ? path_ : nullptr, 0); + const char *path = fl_file_chooser( + T("Open media"), T("Media files (*.{mp3,MP3,wav,WAV,mp4,MP4})"), + path_[0] ? path_ : nullptr, 0); if (path) open_path(path); } @@ -179,11 +225,52 @@ void PlayerWindow::close_player() { if (!session_.active()) hide(); else - status_->copy_label(T("Stopping audio...")); + status_->copy_label(T("Stopping playback...")); } void PlayerWindow::update() { const auto &s = session_.status(); char text[256], elapsed[40], total[40]; + if (s.info.width && s.state != State::Loading) { + if (!video_layout_) { + video_layout_ = true; + size_range(600, 500); + if (h() < 600) { + int sx, sy, sw, sh; + Fl::screen_work_area(sx, sy, sw, sh, x(), y()); + int width = w() < 800 ? 800 : w(), height = 640; + if (width > sw - 32) + width = sw - 32; + if (height > sh - 60) + height = sh - 60; + if (width < 600) + width = 600; + if (height < 500) + height = 500; + int left = x(), top = y(); + if (left + width > sx + sw) + left = sx + sw - width; + if (top + height > sy + sh - 30) + top = sy + sh - height - 30; + if (left < sx) + left = sx; + if (top < sy + 30) + top = sy + 30; + resize(left, top, width, height); + } + video_->show(); + layout(); + redraw(); + } + if (video_serial_ != session_.video_serial()) { + video_serial_ = session_.video_serial(); + video_->redraw(); + } + } else if (video_layout_) { + video_layout_ = false; + video_->hide(); + size_range(600, 350); + redraw(); + } play_->copy_label(s.state == State::Playing ? T("Pause") : T("Play")); bool loaded = s.info.frames && s.state != State::Loading && session_.active() && !closing_; @@ -209,13 +296,17 @@ void PlayerWindow::update() { if (!seek_->dragging) seek_->value(s.info.frames ? double(s.position) / double(s.info.frames) : 0); - if (s.info.rate) + if (s.info.width) + snprintf(text, sizeof(text), "MP4 · %u × %u · %s", s.info.width, + s.info.height, + s.info.has_audio ? T("Stereo audio") : T("No audio track")); + else if (s.info.rate) snprintf(text, sizeof(text), "%s · %u Hz · %s · %u %s", s.info.kind == Kind::Mp3 ? "MP3" : "WAV", s.info.rate, - s.info.channels == 1 ? T("Mono audio") : T("Stereo audio"), s.info.bits, - T("bit")); + s.info.channels == 1 ? T("Mono audio") : T("Stereo audio"), + s.info.bits, T("bit")); else - snprintf(text, sizeof(text), "MP3 / WAV"); + snprintf(text, sizeof(text), "MP3 / WAV / MP4"); detail_->copy_label(text); if (closing_) return; @@ -266,11 +357,12 @@ void PlayerWindow::action(Fl_Widget *widget, void *p) { else if (widget == window.help_) fl_message( "%s", - T("ReliefPlayer\nSimple MP3 and WAV audio player\n\nCtrl+O: open " - "audio\nSpace: play or pause\nLeft / Right: seek 10 seconds\nHome: " + T("ReliefPlayer\nMP3 / WAV audio and MP4 video player\n\nCtrl+O: open " + "media\nSpace: play or pause\nLeft / Right: seek 10 seconds\nHome: " "return to the beginning\n\nVolume changes affect this player " "only.\nSupports PCM WAV 8/16/24/32-bit and float32, mono or " - "stereo.\nLocal files only; no network streams or media library.")); + "stereo.\nMP4: H.264, MPEG-4 or HEVC video; AAC or MP3 audio.\n" + "Local files only; no network streams or media library.")); } bool PlayerWindow::hotkey() { if (!closing_ && !Fl::modal()) { diff --git a/userland/apps/reliefplayer/window.h b/userland/apps/reliefplayer/window.h index c1fc656a..bd7b3005 100644 --- a/userland/apps/reliefplayer/window.h +++ b/userland/apps/reliefplayer/window.h @@ -19,6 +19,9 @@ class PlayerWindow : public Fl_Double_Window { Session session_; char path_[4096]{}; bool closing_ = false; + bool video_layout_ = false; + uint32_t video_serial_ = 0; + Fl_Widget *video_; Fl_Button *open_, *help_, *play_, *stop_, *back_, *forward_; Fl_Box *panel_, *title_, *detail_, *elapsed_, *duration_, *status_; Fl_Check_Button *mute_; diff --git a/userland/apps/reliefplayer/worker.cpp b/userland/apps/reliefplayer/worker.cpp index c5ae196d..205b968e 100644 --- a/userland/apps/reliefplayer/worker.cpp +++ b/userland/apps/reliefplayer/worker.cpp @@ -1,6 +1,7 @@ #include "worker.h" #include "../relieftodo/storage.h" #include "pcm.h" +#include "media.h" #include "protocol.h" #include #include @@ -319,7 +320,8 @@ class Audio { } }; } // namespace -int worker(const char *path, int commands, int statuses, unsigned volume) { +int worker(const char *path, int commands, int statuses, unsigned volume, + VideoShared *video) { signal(SIGPIPE, SIG_IGN); Control control; control.fd = commands; @@ -339,10 +341,23 @@ int worker(const char *path, int commands, int statuses, unsigned volume) { report.send(true); return 1; } + VideoDecoder picture; + bool has_video = decoder.info().kind == Kind::Mp4; + if (has_video && (!video || !picture.open(path, error, loading, &progress))) { + if (control.quit) + return 0; + report.status.state = State::Failed; + report.status.error = video ? error : Error::Memory; + report.send(true); + return 1; + } report.status.info = decoder.info(); report.status.progress = 100; Audio audio; uint64_t target = 0; + uint64_t clock_base = 0, clock_started = 0; + bool silent_running = false; + bool has_audio = decoder.info().has_audio; State state = State::Stopped; unsigned recoveries = 0; while (!control.quit) { @@ -356,8 +371,23 @@ int worker(const char *path, int commands, int statuses, unsigned volume) { : control.target; else if (audio.active()) target = audio.position(); + else if (silent_running) { + target = clock_base + monotonic_ms() - clock_started; + if (target > decoder.info().frames) + target = decoder.info().frames; + } + uint64_t preview_ms = target * 1000 / decoder.info().rate; + if (target == decoder.info().frames && preview_ms) + --preview_ms; + bool seek_failed = + has_video && control.seeking && !picture.seek(preview_ms, error); audio.close(); - if (target == decoder.info().frames && !control.play_requested) { + silent_running = false; + if (seek_failed) { + report.status.error = error; + state = State::Failed; + control.playing = false; + } else if (target == decoder.info().frames && !control.play_requested) { state = State::Ended; control.playing = false; } else @@ -365,16 +395,29 @@ int worker(const char *path, int commands, int statuses, unsigned volume) { control.reset = control.seeking = false; control.play_requested = false; } - if (control.playing && !audio.active()) { - if (target >= decoder.info().frames) + if (control.playing && !audio.active() && !silent_running) { + if (target >= decoder.info().frames) { target = 0; + if (has_video && !picture.seek(0, error)) { + report.status.error = error; + state = State::Failed; + control.playing = false; + } + } errno = 0; - if (!audio.open(decoder, target, error)) { + if (!control.playing) { + // Seeking/replay errors remain visible until the user retries. + } else if (has_audio && !audio.open(decoder, target, error)) { report.status.error = error; report.status.system_error = errno; state = State::Failed; control.playing = false; } else { + if (!has_audio) { + silent_running = true; + clock_base = target; + clock_started = monotonic_ms(); + } state = State::Playing; report.status.error = Error::Ok; report.status.system_error = 0; @@ -413,6 +456,25 @@ int worker(const char *path, int commands, int statuses, unsigned volume) { } } } + if (control.playing && silent_running) { + target = clock_base + monotonic_ms() - clock_started; + if (target >= decoder.info().frames) { + target = decoder.info().frames; + silent_running = false; + control.playing = false; + state = State::Ended; + } + } + if (has_video && state != State::Failed && + !picture.pump(target * 1000 / decoder.info().rate, video, error)) { + if (control.quit) + break; + audio.close(); + silent_running = false; + control.playing = false; + state = State::Failed; + report.status.error = error; + } bool changed = report.status.state != state; report.status.state = state; report.status.position = target; diff --git a/userland/apps/reliefplayer/worker.h b/userland/apps/reliefplayer/worker.h index 2243ed30..397f086d 100644 --- a/userland/apps/reliefplayer/worker.h +++ b/userland/apps/reliefplayer/worker.h @@ -1,6 +1,8 @@ #ifndef RELIEFPLAYER_WORKER_H #define RELIEFPLAYER_WORKER_H namespace rp { -int worker(const char *path, int commands, int statuses, unsigned volume); +struct VideoShared; +int worker(const char *path, int commands, int statuses, unsigned volume, + VideoShared *video = nullptr); } #endif diff --git a/userland/busybox/block_storage.c b/userland/busybox/block_storage.c index fd4c3c16..538353e8 100644 --- a/userland/busybox/block_storage.c +++ b/userland/busybox/block_storage.c @@ -22,9 +22,6 @@ //config:config LEONOS_UMOUNT //config: bool "umount" //config: default y -//config:config LEONOS_GRUB_INSTALLER -//config: bool "leonos-grub-installer (copy EFI payload)" -//config: default y //config:config LEONOS_FSCK //config: bool "fsck.*" //config: default y @@ -44,7 +41,6 @@ //applet:IF_LEONOS_MKFS_EXFAT(APPLET_ODDNAME(mkfs.exfat, leonos_mkfs_exfat, BB_DIR_SBIN, BB_SUID_DROP, leonos_mkfs_exfat)) //applet:IF_LEONOS_MOUNT(APPLET(mount, BB_DIR_BIN, BB_SUID_DROP)) //applet:IF_LEONOS_UMOUNT(APPLET(umount, BB_DIR_SBIN, BB_SUID_DROP)) -//applet:IF_LEONOS_GRUB_INSTALLER(APPLET_ODDNAME(leonos-grub-installer, leonos_grub_installer, BB_DIR_SBIN, BB_SUID_DROP, leonos_grub_installer)) //applet:IF_LEONOS_FSCK(APPLET_ODDNAME(fsck.fat, leonos_fsck_fat, BB_DIR_SBIN, BB_SUID_DROP, leonos_fsck_fat)) //applet:IF_LEONOS_FSCK(APPLET_ODDNAME(fsck.fat32, leonos_fsck_fat, BB_DIR_SBIN, BB_SUID_DROP, leonos_fsck_fat)) //applet:IF_LEONOS_FSCK(APPLET_ODDNAME(fsck.vfat, leonos_fsck_fat, BB_DIR_SBIN, BB_SUID_DROP, leonos_fsck_fat)) @@ -69,8 +65,6 @@ //usage:#define mount_full_usage "\n\nMount a FAT32, exFAT, ext4, or legacy ext2 block device\n" //usage:#define umount_trivial_usage "DIR" //usage:#define umount_full_usage "\n\nUnmount a filesystem by mount point\n" -//usage:#define leonos_grub_installer_trivial_usage "ESP-MOUNTPOINT" -//usage:#define leonos_grub_installer_full_usage "\n\nInstall the ReliefOS EFI/GRUB payload into an ESP mount\n" //usage:#define leonos_fsck_fat_trivial_usage "BLOCKDEV" //usage:#define leonos_fsck_fat_full_usage "\n\nCheck a FAT32 filesystem signature\n" //usage:#define leonos_fsck_ext2_trivial_usage "BLOCKDEV" @@ -123,7 +117,6 @@ static int print_partitions(const char *disk) } return 0; } - static int print_all_partitions(void) { struct leonos_block_disk_info disks[LEONOS_BLOCK_MAX_DISKS]; @@ -387,55 +380,3 @@ int lsblk_main(int argc, char **argv) } return 0; } - -static int grub_copy_file(const char *source, const char *destination) -{ - FILE *in = fopen(source, "rb"); FILE *out; unsigned char buffer[8192]; size_t got; - if (!in || !(out = fopen(destination, "wb"))) { if (in) fclose(in); return -1; } - while ((got = fread(buffer, 1, sizeof(buffer), in)) != 0) - if (fwrite(buffer, 1, got, out) != got) { fclose(in); fclose(out); return -1; } - fclose(in); return fclose(out); -} -static int grub_copy_tree(const char *source, const char *destination) -{ - DIR *dir = opendir(source); struct dirent *entry; - if (!dir) return -1; - (void)mkdir(destination, 0777); - while ((entry = readdir(dir)) != NULL) { - char src[LEONOS_FS_PATH_LEN], dst[LEONOS_FS_PATH_LEN]; - if (snprintf(src, sizeof(src), "%s/%s", source, entry->d_name) < 0 || - snprintf(dst, sizeof(dst), "%s/%s", destination, entry->d_name) < 0 || - (entry->d_type == DT_DIR ? grub_copy_tree(src, dst) : grub_copy_file(src, dst)) < 0) { - closedir(dir); return -1; - } - } - closedir(dir); return 0; -} -int leonos_grub_installer_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; -int leonos_grub_installer_main(int argc, char **argv) -{ - const char *esp = argc == 2 ? argv[1] : NULL; - static const char *const files[] = {"EFI/BOOT/BOOTX64.EFI", "reliefos/kernel.sys", "leonos/kernel.sys", NULL}; - if (!esp) bb_show_usage(); - { - char path[LEONOS_FS_PATH_LEN]; - snprintf(path, sizeof(path), "%s/EFI", esp); (void)mkdir(path, 0777); - snprintf(path, sizeof(path), "%s/EFI/BOOT", esp); (void)mkdir(path, 0777); - snprintf(path, sizeof(path), "%s/reliefos", esp); (void)mkdir(path, 0777); - snprintf(path, sizeof(path), "%s/leonos", esp); (void)mkdir(path, 0777); - } - for (uint32_t i = 0; files[i]; ++i) { - char source[LEONOS_FS_PATH_LEN], destination[LEONOS_FS_PATH_LEN]; - snprintf(source, sizeof(source), "/install/esp/%s", files[i]); - snprintf(destination, sizeof(destination), "%s/%s", esp, files[i]); - if (grub_copy_file(source, destination) < 0) { bb_error_msg("cannot install %s", files[i]); return 1; } - } - { - char source[LEONOS_FS_PATH_LEN], destination[LEONOS_FS_PATH_LEN]; - snprintf(source, sizeof(source), "/install/esp/grub"); - snprintf(destination, sizeof(destination), "%s/grub", esp); - if (grub_copy_tree(source, destination) < 0) { bb_error_msg("cannot install GRUB files"); return 1; } - } - printf("ReliefOS GRUB payload installed to %s.\n", esp); - return 0; -} diff --git a/userland/busybox/leonos_shim.c b/userland/busybox/leonos_shim.c index 7e80fa76..a7a2da64 100644 --- a/userland/busybox/leonos_shim.c +++ b/userland/busybox/leonos_shim.c @@ -106,7 +106,7 @@ const char *reliefos_shell_command_path(const char *name) strcmp(name, "fsck.fat32") == 0 || strcmp(name, "fsck.vfat") == 0 || strcmp(name, "fsck.ext2") == 0 || strcmp(name, "fsck.exfat") == 0 || strcmp(name, "blkid") == 0 || strcmp(name, "lsblk") == 0 || - strcmp(name, "leonos-grub-installer") == 0 || strcmp(name, "sync") == 0) + strcmp(name, "sync") == 0) return "/bin/busybox"; if (reliefos_app_registry_resolve(name, resolved, sizeof(resolved)) == 0) return resolved; diff --git a/userland/runtime/include/leonos_mbedtls_config.h b/userland/runtime/include/leonos_mbedtls_config.h index 6c620246..cb7c11c8 100644 --- a/userland/runtime/include/leonos_mbedtls_config.h +++ b/userland/runtime/include/leonos_mbedtls_config.h @@ -1,7 +1,6 @@ -/* Transitional compatibility forwarder (ReliefOS / ReliefNT rename). - * Canonical runtime configuration: . */ +/* Compatibility include: use the configuration shipped with mbedtls3-dev. + * Custom ALT settings are incompatible with the precompiled library ABI. */ #ifndef LEONOS_MBEDTLS_CONFIG_H #define LEONOS_MBEDTLS_CONFIG_H -#include -#define leonos_mbedtls_time reliefos_mbedtls_time -#endif /* LEONOS_MBEDTLS_CONFIG_H */ +#include +#endif diff --git a/userland/runtime/include/reliefos/bootloader.h b/userland/runtime/include/reliefos/bootloader.h index 0bba1ce6..8d2e0175 100644 --- a/userland/runtime/include/reliefos/bootloader.h +++ b/userland/runtime/include/reliefos/bootloader.h @@ -39,9 +39,24 @@ int reliefos_boot_validate_root(const struct reliefos_boot_install_options *opti * Existing boot slots and versioned payloads are never copied from the media. */ int reliefos_boot_install_at_root(const struct reliefos_boot_install_options *options, const char *root); +/** @brief Install into a per-instance boot tree and publish its EFI files on esp. + * @param options Original ABI options; boot_dir names the instance directory. + * @param root Final installed root. @param esp Physical mounted ESP root. + * @return Zero after publication, negative errno on failure. Other EFI loaders + * and existing fallback executables are preserved; no implicit firmware writes. + */ +int reliefos_boot_install_instance_at_root(const struct reliefos_boot_install_options *options, + const char *root, const char *esp); +/** @brief Create/open a UUID-named instance below a held physical ESP, rejecting links. + * @param esp Absolute physical ESP. @param uuid Canonical root PARTUUID. + * @param path Output absolute directory. @param capacity Output size. + * @return Zero or negative errno; existing unrelated directories survive. + */ +int reliefos_boot_prepare_instance(const char *esp, const char *uuid, char *path, unsigned capacity); +/** @brief Check physical ESP and per-instance ownership without creating files. */ +int reliefos_boot_validate_instance(const struct reliefos_boot_install_options *options, const char *esp); /** @brief Install using this system's final root (/); return negative errno on failure. - * The original structure and symbol ABI remain unchanged. Offline installers - * use reliefos_boot_install_at_root to name their mounted destination root. + * Offline installers use reliefos_boot_install_at_root for their destination. */ int reliefos_boot_install(const struct reliefos_boot_install_options *options); /** @brief Read installed mode bits; legacy media without a record can identify UEFI only. */ diff --git a/userland/runtime/include/reliefos_mbedtls_config.h b/userland/runtime/include/reliefos_mbedtls_config.h index 555cd36a..ea9ac145 100644 --- a/userland/runtime/include/reliefos_mbedtls_config.h +++ b/userland/runtime/include/reliefos_mbedtls_config.h @@ -1,76 +1,6 @@ +/* Compatibility include: use the configuration shipped with mbedtls3-dev. + * Custom ALT settings are incompatible with the precompiled library ABI. */ #ifndef RELIEFOS_MBEDTLS_CONFIG_H #define RELIEFOS_MBEDTLS_CONFIG_H - -#include - -int64_t reliefos_mbedtls_time(int64_t *seconds); - -#define MBEDTLS_HAVE_ASM -#define MBEDTLS_HAVE_TIME -#define MBEDTLS_HAVE_TIME_DATE -#define MBEDTLS_PLATFORM_C -#define MBEDTLS_PLATFORM_TIME_TYPE_MACRO int64_t -#define MBEDTLS_PLATFORM_TIME_MACRO reliefos_mbedtls_time -#define MBEDTLS_PLATFORM_GMTIME_R_ALT -#define MBEDTLS_NO_PLATFORM_ENTROPY -#define MBEDTLS_ENTROPY_HARDWARE_ALT - -#define MBEDTLS_CIPHER_MODE_CBC -#define MBEDTLS_CIPHER_MODE_CTR -#define MBEDTLS_PKCS1_V15 -#define MBEDTLS_PKCS1_V21 -#define MBEDTLS_ECP_DP_SECP256R1_ENABLED -#define MBEDTLS_ECP_DP_SECP384R1_ENABLED -#define MBEDTLS_ECP_DP_SECP521R1_ENABLED -#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED -#define MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED -#define MBEDTLS_SSL_PROTO_TLS1_2 -#define MBEDTLS_SSL_SERVER_NAME_INDICATION - -#define MBEDTLS_AES_C -#define MBEDTLS_ASN1_PARSE_C -#define MBEDTLS_ASN1_WRITE_C -#define MBEDTLS_BASE64_C -#define MBEDTLS_BIGNUM_C -#define MBEDTLS_CIPHER_C -#define MBEDTLS_CTR_DRBG_C -#define MBEDTLS_ECDH_C -#define MBEDTLS_ECDSA_C -#define MBEDTLS_ECP_C -#define MBEDTLS_ENTROPY_C -#define MBEDTLS_GCM_C -#define MBEDTLS_MD_C -#define MBEDTLS_PKCS5_C -#define MBEDTLS_OID_C -#define MBEDTLS_PEM_PARSE_C -#define MBEDTLS_PK_C -#define MBEDTLS_PK_PARSE_C -#define MBEDTLS_RSA_C -#define MBEDTLS_SHA1_C -#define MBEDTLS_SHA256_C -#define MBEDTLS_SHA512_C -#define MBEDTLS_SSL_CLI_C -#define MBEDTLS_SSL_TLS_C -#define MBEDTLS_X509_CRT_PARSE_C -#define MBEDTLS_X509_RSASSA_PSS_SUPPORT -#define MBEDTLS_X509_USE_C - -#define MBEDTLS_AES_ROM_TABLES -#define MBEDTLS_NO_UDBL_DIVISION -#define MBEDTLS_MPI_MAX_SIZE 512 -#define MBEDTLS_ECP_WINDOW_SIZE 2 -#define MBEDTLS_ECP_FIXED_POINT_OPTIM 0 -#define MBEDTLS_ECP_NIST_OPTIM -#define MBEDTLS_ENTROPY_MAX_SOURCES 1 -#define MBEDTLS_SSL_IN_CONTENT_LEN 16384 -#define MBEDTLS_SSL_OUT_CONTENT_LEN 16384 -#define MBEDTLS_SSL_CIPHERSUITES \ - MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, \ - MBEDTLS_TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, \ - MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, \ - MBEDTLS_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, \ - MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, \ - MBEDTLS_TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, \ - 0 - +#include #endif diff --git a/userland/runtime/src/Kbuild b/userland/runtime/src/Kbuild index 3061de78..afd87878 100644 --- a/userland/runtime/src/Kbuild +++ b/userland/runtime/src/Kbuild @@ -2,6 +2,8 @@ RELIEFOS_RUNTIME_SRC_OBJECTS := admin.o ansi_curses.o api.o app_registry.o auth_ RELIEFOS_RUNTIME_SRC_C_OBJECTS := admin.o ansi_curses.o api.o app_registry.o auth_accounts.o auth_password.o blockdev.o bootloader.o bootloader_publish.o bootloader_process.o devmand_client.o devmgr_service.o environment.o gpu.o gpu_sdk.o fb.o ini.o inputm.o launch.o libc.o locale_conf.o net_service.o netsock.o ntp.o openrc.o pam_session.o png.o procsys.o sessiond_client.o sudo_client.o tar.o text_encoding.o tls.o unix_ipc.o RELIEFOS_RUNTIME_SRC_OBJECTS += xdg.o desktop_entry.o RELIEFOS_RUNTIME_SRC_C_OBJECTS += xdg.o desktop_entry.o +RELIEFOS_RUNTIME_SRC_OBJECTS += bootloader_uefi.o +RELIEFOS_RUNTIME_SRC_C_OBJECTS += bootloader_uefi.o RELIEFOS_RUNTIME_SRC_ASM_OBJECTS := syscall.o RELIEFOS_RUNTIME_SRC_OBJECTS += fs_directory.o RELIEFOS_RUNTIME_SRC_C_OBJECTS += fs_directory.o diff --git a/userland/runtime/src/bootloader.c b/userland/runtime/src/bootloader.c index c3889487..509ef90b 100644 --- a/userland/runtime/src/bootloader.c +++ b/userland/runtime/src/bootloader.c @@ -115,7 +115,7 @@ int reliefos_boot_validate_payload(const struct reliefos_boot_install_options *o int root = boot_open_directory(options->source); if (root < 0) return root; close(root); - const char *const common[] = {"grub/grub.cfg", "grub/kernel-slots.cfg", + const char *const common[] = {"grub/grub.cfg", "grub/kernel-slots.cfg", "grub/other-systems.cfg", "grub/fonts/leonos-unicode.pf2", "grub/theme/theme.txt"}; for (unsigned i = 0; i < sizeof(common) / sizeof(common[0]); ++i) { int result = payload_file(options, common[i]); @@ -310,7 +310,45 @@ int reliefos_boot_validate_root(const struct reliefos_boot_install_options *opti return checked_root(options, root, NULL); } -/** @brief Decode the versioned mode record, checking actual loader files too. */ +/** @brief Recognize an embedded GRUB core when an older image lacks its ESP copy. + * The stage-one LBA and diskboot blocklist must remain inside a GPT BIOS Boot + * extent. A mode record or arbitrary nonzero MBR alone is insufficient. + */ +static int embedded_bios_core(const char *path, int fd, const unsigned char mbr[512]) +{ + struct reliefos_block_disk_info info; + int result = reliefos_block_get_info(path, &info); + if (result) return result; + if (info.sector_size != 512) return -ENOTSUP; + uint64_t lba = 0; + /* GRUB_BOOT_MACHINE_KERNEL_SECTOR in the standard i386-pc boot protocol. */ + for (unsigned i = 0; i < 8; ++i) lba |= (uint64_t)mbr[0x5c + i] << (8 * i); + if (!lba || lba >= info.sector_count || lba > INT64_MAX / 512) return -EINVAL; + struct reliefos_block_partition parts[RELIEFOS_BLOCK_MAX_PARTITIONS]; + uint32_t count = 0; + result = reliefos_block_list_partitions(path, parts, RELIEFOS_BLOCK_MAX_PARTITIONS, &count); + if (result) return result; + if (count > RELIEFOS_BLOCK_MAX_PARTITIONS) return -EOVERFLOW; + for (uint32_t i = 0; i < count; ++i) { + if (parts[i].gpt_type != RELIEFOS_BLOCK_GPT_BIOS_BOOT || + parts[i].first_lba > info.sector_count || + parts[i].sector_count > info.sector_count - parts[i].first_lba) continue; + uint64_t end = parts[i].first_lba + parts[i].sector_count; + if (lba < parts[i].first_lba || lba >= end) continue; + unsigned char core[512]; + ssize_t got; + do { got = pread(fd, core, sizeof(core), (off_t)(lba * 512)); } while (got < 0 && errno == EINTR); + if (got != sizeof(core)) return got < 0 ? -errno : -EIO; + uint64_t next = 0; + for (unsigned j = 0; j < 8; ++j) next |= (uint64_t)core[500 + j] << (8 * j); + unsigned sectors = core[508] | (unsigned)core[509] << 8; + unsigned segment = core[510] | (unsigned)core[511] << 8; + return next > lba && next < end && sectors && sectors <= end - next && segment == 0x820 ? 0 : -EINVAL; + } + return -ENOENT; +} + +/** @brief Decode installed modes, including legacy raw-only BIOS cores. */ int reliefos_boot_probe(const char *disk_path, const char *boot_dir, uint32_t *modes) { if (!modes) return -EINVAL; @@ -346,7 +384,11 @@ int reliefos_boot_probe(const char *disk_path, const char *boot_dir, uint32_t *m } if (!result && !found) result = -EINVAL; if (!result && (*modes & RELIEFOS_BOOT_UEFI)) result = installed_file(root, "EFI/BOOT/BOOTX64.EFI"); - if (!result && (*modes & RELIEFOS_BOOT_BIOS)) result = installed_file(root, "grub/i386-pc/core.img"); + int embedded_only = 0; + if (!result && (*modes & RELIEFOS_BOOT_BIOS)) { + result = installed_file(root, "grub/i386-pc/core.img"); + if (result == -ENOENT && disk_path) { embedded_only = 1; result = 0; } + } if (!result && (*modes & RELIEFOS_BOOT_BIOS) && disk_path) { int disk = open(disk_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (disk < 0) result = -errno; @@ -359,6 +401,7 @@ int reliefos_boot_probe(const char *disk_path, const char *boot_dir, uint32_t *m unsigned code = 0; for (unsigned i = 0; i < 440; ++i) code |= sector[i]; if (!code || sector[510] != 0x55 || sector[511] != 0xaa) result = -EINVAL; + else if (embedded_only) result = embedded_bios_core(disk_path, disk, sector); } close(disk); } @@ -400,10 +443,23 @@ int reliefos_boot_validate_target(const struct reliefos_boot_install_options *op uint32_t previous; if (!result) result = reliefos_boot_probe(options->disk_path, options->boot_dir, &previous); if (!result) result = boot_walk_payload(options, root, 0); - if (!result && options->disk_path) { + if (!result) { int file = boot_open_file(root, "reliefos/config/root.cfg"); if (file >= 0) close(file); else if (file != -ENOENT) result = file; + else if (options->install_uefi) { + int existing = boot_open_file(root, "EFI/BOOT/BOOTX64.EFI"); + if (existing >= 0) { + close(existing); + char uuid[37], expected[96], marker[96]; + result = root_uuid_text(options->root_partuuid, uuid); + int length = result ? 0 : snprintf(expected, sizeof(expected), "ReliefOS instance v1\n%s\n", uuid); + if (!result) result = kernel_bundle_read_fixed(root, "reliefos-instance.id", marker, (size_t)length); + if (!result && memcmp(marker, expected, (size_t)length)) result = -EXDEV; + if (result == -ENOENT) result = -EEXIST; + } + else if (existing != -ENOENT) result = existing; + } } close(root); return result; @@ -415,7 +471,8 @@ int reliefos_boot_validate_target(const struct reliefos_boot_install_options *op * serializes installs. Errors can leave completed file or raw-disk stages in * place; they never report a failed GRUB invocation as a successful install. */ -int reliefos_boot_install_at_root(const struct reliefos_boot_install_options *options, const char *installed_root) +static int install_at_root(const struct reliefos_boot_install_options *options, + const char *installed_root, const char *esp) { int result = reliefos_boot_validate_payload(options); if (!result) result = reliefos_boot_validate_target(options); @@ -429,12 +486,33 @@ int reliefos_boot_install_at_root(const struct reliefos_boot_install_options *op } int root = boot_open_directory(options->boot_dir); if (root < 0) return root; - result = reliefos_kernel_bundle_publish(installed_root, options->boot_dir, identity.release); + if (esp && !options->preserve_rollback) { + const char *slots[] = {"reliefos/state/kernel-A.env", "reliefos/state/kernel-A.sha256", + "reliefos/state/kernel-B.env", "reliefos/state/kernel-B.sha256"}; + for (unsigned i = 0; i < 4 && !result; ++i) { + char leaf[256]; int state = boot_open_parent(root, slots[i], 0, leaf); + if (state == -ENOENT) continue; + if (state < 0) { result = state; break; } + if (unlinkat(state, leaf, 0) < 0 && errno != ENOENT) result = -errno; + if (!result && fsync(state) < 0) result = -errno; + close(state); + } + } + if (!result) result = reliefos_kernel_bundle_publish(installed_root, options->boot_dir, identity.release); if (!result) result = boot_walk_payload(options, root, 1); if (!result && options->install_bios) { fprintf(stderr, "[boot-install] installing BIOS loader on %s\n", options->disk_path); - result = boot_install_bios(options); + struct reliefos_boot_install_options bios_options = *options; + char physical_boot[BOOT_PATH_MAX]; + if (esp) { + int n = snprintf(physical_boot, sizeof(physical_boot), "%s/reliefos/instances/%s", esp, options->root_partuuid); + if (n < 0 || n >= (int)sizeof(physical_boot)) result = -ENAMETOOLONG; + else bios_options.boot_dir = physical_boot; + } + if (!result) result = boot_install_bios(&bios_options); } + if (!result && options->install_uefi && esp) + result = boot_install_uefi(options, esp); if (!result && fsync(root) < 0) result = -errno; if (!result && options->disk_path) { char uuid[37], config[96]; @@ -457,6 +535,20 @@ int reliefos_boot_install_at_root(const struct reliefos_boot_install_options *op return result; } +int reliefos_boot_install_at_root(const struct reliefos_boot_install_options *options, const char *root) +{ + return install_at_root(options, root, NULL); +} + +int reliefos_boot_install_instance_at_root(const struct reliefos_boot_install_options *options, + const char *root, const char *esp) +{ + if (!esp || esp[0] != '/' || !options || !options->root_partuuid) return -EINVAL; + int result = boot_validate_instance(options, esp); + if (result) return result; + return install_at_root(options, root, esp); +} + int reliefos_boot_install(const struct reliefos_boot_install_options *options) { return reliefos_boot_install_at_root(options, "/"); diff --git a/userland/runtime/src/bootloader_internal.h b/userland/runtime/src/bootloader_internal.h index 8f164e98..3d48c12e 100644 --- a/userland/runtime/src/bootloader_internal.h +++ b/userland/runtime/src/bootloader_internal.h @@ -51,4 +51,8 @@ int boot_publish_descriptor(const struct reliefos_boot_install_options *options, int boot_retire_legacy(int rootfd); /** @brief Run fixed-path upstream grub-install, pumping the caller's optional UI callback. */ int boot_install_bios(const struct reliefos_boot_install_options *options); +/** @brief Publish an instance EFI entry, preserving an unrelated fallback loader. */ +int boot_install_uefi(const struct reliefos_boot_install_options *options, const char *esp); +/** @brief Check physical ESP, namespace inode and ownership marker without mutation. */ +int boot_validate_instance(const struct reliefos_boot_install_options *options, const char *esp); #endif diff --git a/userland/runtime/src/bootloader_process.c b/userland/runtime/src/bootloader_process.c index 1eb7e0e2..50eec31e 100644 --- a/userland/runtime/src/bootloader_process.c +++ b/userland/runtime/src/bootloader_process.c @@ -19,7 +19,7 @@ int boot_install_bios(const struct reliefos_boot_install_options *options) if (n < 0 || (size_t)n >= sizeof(boot_directory)) return -ENAMETOOLONG; char *const argv[] = {BOOT_GRUB_INSTALL, "--target=i386-pc", directory, boot_directory, "--recheck", "--force-file-id", "--fonts=", "--themes=", "--locales=", - "--modules=part_gpt fat normal multiboot2 cmp search search_fs_file configfile serial terminal all_video font gfxterm gfxmenu hashsum gcry_sha256 crypto loadenv regexp test", + "--modules=part_gpt part_msdos ext2 chain ntfs linux gzio reboot search_fs_uuid search_label fat normal multiboot2 cmp search search_fs_file configfile serial terminal all_video font gfxterm gfxmenu hashsum gcry_sha256 crypto loadenv regexp test", "--", (char *)options->disk_path, NULL}; char *const environment[] = {"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "LC_ALL=C", "HOME=/root", NULL}; diff --git a/userland/runtime/src/bootloader_publish.c b/userland/runtime/src/bootloader_publish.c index b6ee1ea4..fb3cc2ce 100644 --- a/userland/runtime/src/bootloader_publish.c +++ b/userland/runtime/src/bootloader_publish.c @@ -323,8 +323,22 @@ static int walk(const struct reliefos_boot_install_options *options, int root, int boot_walk_payload(const struct reliefos_boot_install_options *options, int root, int publish_files) { const char *const trees[] = {"reliefos", "leonos", "grub", "EFI"}; + int preserve_efi = 0; + if (publish_files && options->install_uefi) { + int marker = boot_open_file(root, "reliefos-instance.id"); + if (marker >= 0) close(marker); + else if (marker != -ENOENT) return marker; + else { + int existing = boot_open_file(root, "EFI/BOOT/BOOTX64.EFI"); + if (existing >= 0) { + close(existing); + preserve_efi = 1; + fprintf(stderr, "[boot-install] preserving unverified legacy EFI executable and sibling configuration\n"); + } else if (existing != -ENOENT) return existing; + } + } for (unsigned i = 0; i < 4; ++i) { - if (i == 3 && !options->install_uefi) continue; + if (i == 3 && (!options->install_uefi || preserve_efi)) continue; int result = walk(options, root, trees[i], 0, publish_files); if (result) return result; } diff --git a/userland/runtime/src/bootloader_uefi.c b/userland/runtime/src/bootloader_uefi.c new file mode 100644 index 00000000..03673f65 --- /dev/null +++ b/userland/runtime/src/bootloader_uefi.c @@ -0,0 +1,191 @@ +/* Per-installation UEFI entry publication; never take over a foreign fallback. */ +#include "bootloader_internal.h" +#include "kernel_bundle_internal.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Require a canonical nonzero UUID before placing it in paths or GRUB data. */ +static int instance_uuid(const char *uuid) +{ + if (!uuid || strlen(uuid) != 36) return 0; + unsigned nonzero = 0; + for (unsigned i = 0; i < 36; ++i) { + if (i == 8 || i == 13 || i == 18 || i == 23) { if (uuid[i] != '-') return 0; } + else { if (!isdigit((unsigned char)uuid[i]) && (uuid[i] < 'a' || uuid[i] > 'f')) return 0; nonzero |= uuid[i] != '0'; } + } + return nonzero; +} + +/** @brief Read an exact small ownership record, rejecting links and extra bytes. */ +static int read_record(int root, const char *path, char *text, size_t size) +{ + int fd = boot_open_file(root, path); + if (fd < 0) return fd; + size_t used = 0; + int result = 0; + while (used < size) { + ssize_t n = read(fd, text + used, size - used); + if (n < 0 && errno == EINTR) continue; + if (n < 0) { result = -errno; break; } + if (!n) break; + used += (size_t)n; + } + char extra; + ssize_t tail; + do { tail = read(fd, &extra, 1); } while (tail < 0 && errno == EINTR); + if (!result && tail != 0) result = tail < 0 ? -errno : -EINVAL; + close(fd); + return result ? result : used == size ? 0 : -EINVAL; +} + +int reliefos_boot_prepare_instance(const char *esp, const char *uuid, char *path, unsigned capacity) +{ + if (!esp || !path || !instance_uuid(uuid)) return -EINVAL; + char relative[192], leaf[256]; + int n = snprintf(path, capacity, "%s/reliefos/instances/%s", esp, uuid); + if (n < 0 || (unsigned)n >= capacity) return -ENAMETOOLONG; + int root = boot_open_directory(esp); + if (root < 0) return root; + snprintf(relative, sizeof(relative), "reliefos/instances/%s/entry", uuid); + int parent = boot_open_parent(root, relative, 1, leaf); + if (parent < 0) { close(root); return parent; } + char marker[96], existing[96]; + int length = snprintf(marker, sizeof(marker), "ReliefOS instance v1\n%s\n", uuid); + int result = read_record(parent, "reliefos-instance.id", existing, (size_t)length); + if (result == -ENOENT) { + int copy = fcntl(parent, F_DUPFD_CLOEXEC, 3); + DIR *directory = copy < 0 ? NULL : fdopendir(copy); + if (!directory) { if (copy >= 0) close(copy); result = -errno; } + else { + struct dirent *entry; + result = 0; + errno = 0; + while ((entry = readdir(directory))) { + if (strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) { result = -EEXIST; break; } + } + if (!result && errno) result = -errno; + closedir(directory); + if (!result) { struct reliefos_boot_install_options options = {0}; + result = boot_publish_record(&options, parent, "reliefos-instance.id", marker, (size_t)length); } + } + } else if (!result && memcmp(marker, existing, (size_t)length)) result = -EEXIST; + close(root); + if (result) { close(parent); return result; } + result = fsync(parent) < 0 ? -errno : 0; + close(parent); + return result; +} + +/** @brief Reject a mismatched physical ESP or instance view before any boot writes. */ +int boot_validate_instance(const struct reliefos_boot_install_options *options, const char *esp) +{ + if (!options || !instance_uuid(options->root_partuuid) || !esp) return -EINVAL; + char expected[BOOT_PATH_MAX]; + int n = snprintf(expected, sizeof(expected), "%s/reliefos/instances/%s", esp, options->root_partuuid); + if (n < 0 || n >= (int)sizeof(expected)) return -ENAMETOOLONG; + int a = boot_open_directory(expected), b = boot_open_directory(options->boot_dir); + struct stat sa, sb; + int result = a < 0 ? a : b < 0 ? b : fstat(a, &sa) || fstat(b, &sb) ? -errno : + sa.st_dev != sb.st_dev || sa.st_ino != sb.st_ino ? -EXDEV : 0; + char text[96], marker[96]; + n = snprintf(text, sizeof(text), "ReliefOS instance v1\n%s\n", options->root_partuuid); + if (!result) result = read_record(a, "reliefos-instance.id", marker, (size_t)n); + if (!result && memcmp(text, marker, (size_t)n)) result = -EXDEV; + if (a >= 0) close(a); + if (b >= 0) close(b); + return result; +} + +int boot_install_uefi(const struct reliefos_boot_install_options *options, const char *esp) +{ + if (!instance_uuid(options->root_partuuid)) return -EINVAL; + int root = boot_open_directory(esp), instance = boot_open_directory(options->boot_dir); + if (root < 0 || instance < 0) { if (root >= 0) close(root); if (instance >= 0) close(instance); return root < 0 ? root : instance; } + char path[256], expected[BOOT_PATH_MAX], record[160], digest[65] = {0}; + snprintf(path, sizeof(path), "reliefos/instances/%s/grub/entry", options->root_partuuid); + char leaf[256]; int parent = boot_open_parent(root, path, 0, leaf); + struct stat a, b; + int result = parent < 0 ? parent : fstat(instance, &a) ? -errno : 0; + if (!result) { + snprintf(expected, sizeof(expected), "%s/reliefos/instances/%s", esp, options->root_partuuid); + int actual = boot_open_directory(expected); + if (actual < 0) result = actual; + else { if (fstat(actual, &b) || a.st_dev != b.st_dev || a.st_ino != b.st_ino) result = -EXDEV; close(actual); } + } + if (parent >= 0) close(parent); + close(instance); + if (result) { close(root); return result; } + int fallback = boot_open_file(root, "EFI/BOOT/BOOTX64.EFI"); + int fallback_missing = fallback == -ENOENT; + int owned = 0; + if (fallback >= 0) { + result = kernel_bundle_hash_fd(fallback, digest); + close(fallback); + int size = snprintf(expected, sizeof(expected), "ReliefOS fallback v1\n%s\n%s\n", options->root_partuuid, digest); + if (!result && !read_record(root, "EFI/BOOT/reliefos-owner", record, (size_t)size)) + owned = !memcmp(expected, record, (size_t)size); + } else if (!fallback_missing) { close(root); return fallback; } + int source_root = boot_open_directory(options->source); + int source = source_root < 0 ? source_root : boot_open_file(source_root, "EFI/BOOT/BOOTX64.EFI"); + if (source_root >= 0) close(source_root); + if (source < 0) { close(root); return source; } + snprintf(path, sizeof(path), "EFI/ReliefOS/%s/grubx64.efi", options->root_partuuid); + result = boot_publish_descriptor(options, root, path, source); + char config[512]; + int n = snprintf(config, sizeof(config), "# ReliefOS installation %s\nset prefix=($root)/reliefos/instances/%s/grub\nconfigfile $prefix/grub.cfg\n", options->root_partuuid, options->root_partuuid); + snprintf(path, sizeof(path), "EFI/ReliefOS/%s/grub.cfg", options->root_partuuid); + if (!result) result = boot_publish_record(options, root, path, config, (size_t)n); + // An existing sibling config without an executable is still foreign data. + int sibling = boot_open_file(root, "EFI/BOOT/grub.cfg"); + int can_create = fallback_missing && sibling == -ENOENT; + if (sibling >= 0) close(sibling); + if (!result && (can_create || owned)) { + result = boot_publish_record(options, root, "EFI/BOOT/grub.cfg", config, (size_t)n); + if (!result && lseek(source, 0, SEEK_SET) < 0) result = -errno; + if (!result) result = boot_publish_descriptor(options, root, "EFI/BOOT/BOOTX64.EFI", source); + if (!result && lseek(source, 0, SEEK_SET) < 0) result = -errno; + if (!result) result = kernel_bundle_hash_fd(source, digest); + n = snprintf(record, sizeof(record), "ReliefOS fallback v1\n%s\n%s\n", options->root_partuuid, digest); + if (!result) result = boot_publish_record(options, root, "EFI/BOOT/reliefos-owner", record, (size_t)n); + } else if (!result) { + fprintf(stderr, "[boot-install] preserved existing EFI fallback; firmware entry: /EFI/ReliefOS/%s/grubx64.efi\n", options->root_partuuid); + } + // A conventional vendor entry lets another boot manager discover ReliefOS + // without guessing an installation UUID. Its selected default is retained + // when a different installation on the shared ESP is updated. + int vendor = boot_open_file(root, "EFI/ReliefOS/grubx64.efi"); + int vendor_missing = vendor == -ENOENT, vendor_owned = 0; + if (vendor >= 0) { + int error = kernel_bundle_hash_fd(vendor, digest); + close(vendor); + int length = snprintf(expected, sizeof(expected), "ReliefOS dispatcher v1\n%s\n%s\n", options->root_partuuid, digest); + if (!error && !read_record(root, "EFI/ReliefOS/dispatcher-owner", record, (size_t)length)) + vendor_owned = !memcmp(expected, record, (size_t)length); + } + int vendor_config = boot_open_file(root, "EFI/ReliefOS/grub.cfg"); + int vendor_create = vendor_missing && vendor_config == -ENOENT; + if (vendor_config >= 0) close(vendor_config); + if (!result && (vendor_create || vendor_owned)) { + n = snprintf(config, sizeof(config), "# ReliefOS installation %s\nset prefix=($root)/reliefos/instances/%s/grub\nconfigfile $prefix/grub.cfg\n", options->root_partuuid, options->root_partuuid); + result = boot_publish_record(options, root, "EFI/ReliefOS/grub.cfg", config, (size_t)n); + if (!result && lseek(source, 0, SEEK_SET) < 0) result = -errno; + if (!result) result = boot_publish_descriptor(options, root, "EFI/ReliefOS/grubx64.efi", source); + if (!result && lseek(source, 0, SEEK_SET) < 0) result = -errno; + if (!result) result = kernel_bundle_hash_fd(source, digest); + n = snprintf(record, sizeof(record), "ReliefOS dispatcher v1\n%s\n%s\n", options->root_partuuid, digest); + if (!result) result = boot_publish_record(options, root, "EFI/ReliefOS/dispatcher-owner", record, (size_t)n); + } + close(source); close(root); + return result; +} + +int reliefos_boot_validate_instance(const struct reliefos_boot_install_options *options, const char *esp) +{ + return boot_validate_instance(options, esp); +} diff --git a/userland/runtime/src/kernel_bundle_publish.c b/userland/runtime/src/kernel_bundle_publish.c index 079f9624..37da1fc5 100644 --- a/userland/runtime/src/kernel_bundle_publish.c +++ b/userland/runtime/src/kernel_bundle_publish.c @@ -248,6 +248,27 @@ int reliefos_kernel_bundle_publish(const char *root_path, const char *boot, cons struct stat source_status, target_status; if (fstat(root, &target_status) || fstat(bundle.root, &source_status)) { result = -errno; goto out; } if (source_status.st_dev == target_status.st_dev && source_status.st_ino == target_status.st_ino) { result = -EINVAL; goto out; } + static const char instance_header[] = "ReliefOS instance v1\n"; + char marker[sizeof(instance_header) - 1 + 36 + 1], uuid[37] = {0}, alias[128]; + result = kernel_bundle_read_fixed(root, "reliefos-instance.id", marker, sizeof(marker)); + if (!result) { + if (memcmp(marker, instance_header, sizeof(instance_header) - 1) || marker[sizeof(marker) - 1] != '\n') { + result = -EINVAL; goto out; + } + memcpy(uuid, marker + sizeof(instance_header) - 1, 36); + for (unsigned i = 0; i < 36; ++i) { + if (i == 8 || i == 13 || i == 18 || i == 23) { + if (uuid[i] != '-') { result = -EINVAL; goto out; } + } else if ((uuid[i] < '0' || uuid[i] > '9') && (uuid[i] < 'a' || uuid[i] > 'f')) { + result = -EINVAL; goto out; + } + } + snprintf(alias, sizeof(alias), "/dev/disk/by-partuuid/%s", uuid); + struct stat device; + if (stat(alias, &device) < 0) { result = -errno; goto out; } + if (!S_ISBLK(device.st_mode) || device.st_rdev != source_status.st_dev) { result = -EXDEV; goto out; } + } else if (result == -ENOENT) result = 0; + else goto out; stage = "synchronize installed root products"; result = kernel_bundle_sync_root(&bundle); if (result) goto out; diff --git a/userland/runtime/src/tls.c b/userland/runtime/src/tls.c index 8cb73c5a..06217487 100644 --- a/userland/runtime/src/tls.c +++ b/userland/runtime/src/tls.c @@ -52,11 +52,6 @@ static void reliefos_tls_log_verify_failure(const char *hostname, uint32_t flags (flags & MBEDTLS_X509_BADCERT_BAD_MD) != 0); } -static int reliefos_tls_is_leap_year(int year) -{ - return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; -} - static int reliefos_tls_header_name_equal(const char *line, const char *name) { while (*name && *line) { @@ -120,102 +115,8 @@ int64_t reliefos_mbedtls_time(int64_t *seconds) return value; } -struct tm *mbedtls_platform_gmtime_r(const int64_t *seconds, struct tm *out) -{ - static const int month_days[] = { - 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, - }; - int64_t days; - int64_t remainder; - int year = 1970; - int month = 0; - if (!seconds || !out || *seconds < 0) { - return 0; - } - days = *seconds / 86400; - remainder = *seconds % 86400; - while (days >= (reliefos_tls_is_leap_year(year) ? 366 : 365)) { - days -= reliefos_tls_is_leap_year(year) ? 366 : 365; - ++year; - } - while (month < 12) { - int count = month_days[month]; - if (month == 1 && reliefos_tls_is_leap_year(year)) { - ++count; - } - if (days < count) { - break; - } - days -= count; - ++month; - } - out->tm_sec = (int)(remainder % 60); - out->tm_min = (int)((remainder / 60) % 60); - out->tm_hour = (int)(remainder / 3600); - out->tm_mday = (int)days + 1; - out->tm_mon = month; - out->tm_year = year - 1900; - out->tm_wday = (int)(((*seconds / 86400) + 4) % 7); - out->tm_yday = (int)((*seconds / 86400) - - ((int64_t)(year - 1970) * 365)); - for (int current = 1970; current < year; ++current) { - if (reliefos_tls_is_leap_year(current)) { - --out->tm_yday; - } - } - out->tm_isdst = 0; - return out; -} - -static int reliefos_tls_rdrand_available(void) -{ - uint32_t eax = 1; - uint32_t ebx; - uint32_t ecx; - uint32_t edx; - __asm__ volatile("cpuid" - : "+a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) - : "c"(0)); - (void)ebx; - (void)edx; - return (ecx & (1U << 30)) != 0; -} - -static int reliefos_tls_rdrand64(uint64_t *value) -{ - unsigned char ready; - __asm__ volatile("rdrand %0; setc %1" - : "=r"(*value), "=qm"(ready)); - return ready != 0; -} - -int mbedtls_hardware_poll(void *data, unsigned char *output, - size_t len, size_t *olen) -{ - size_t written = 0; - (void)data; - if (olen) { - *olen = 0; - } - if (!output || !olen || !reliefos_tls_rdrand_available()) { - return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED; - } - while (written < len) { - uint64_t random = 0; - int ready = 0; - for (int attempt = 0; attempt < 10 && !ready; ++attempt) { - ready = reliefos_tls_rdrand64(&random); - } - if (!ready) { - return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED; - } - for (uint32_t index = 0; index < sizeof(random) && written < len; ++index) { - output[written++] = (unsigned char)(random >> (index * 8U)); - } - } - *olen = written; - return 0; -} +/* Official mbedTLS uses musl time/gmtime and its platform entropy source. + * Do not interpose ALT hooks compiled for the retired private configuration. */ static int reliefos_tls_load_roots(void) { diff --git a/userland/runtime/vendor/libpng/Kbuild b/userland/runtime/vendor/libpng/Kbuild deleted file mode 100644 index 550708bd..00000000 --- a/userland/runtime/vendor/libpng/Kbuild +++ /dev/null @@ -1,42 +0,0 @@ -RELIEFOS_LIBPNG_OBJECTS := png.o pngerror.o pngget.o pngmem.o pngpread.o pngread.o pngrio.o pngrtran.o pngrutil.o pngset.o pngtrans.o pngwio.o pngwrite.o pngwtran.o pngwutil.o -RELIEFOS_LIBPNG_C_OBJECTS := png.o pngerror.o pngget.o pngmem.o pngpread.o pngread.o pngrio.o pngrtran.o pngrutil.o pngset.o pngtrans.o pngwio.o pngwrite.o pngwtran.o pngwutil.o -RELIEFOS_LIBPNG_ASM_OBJECTS := -ifneq ($(RELIEFOS_CAPTURE_RUNTIME_KBUILD),1) -obj-y := $(RELIEFOS_LIBPNG_OBJECTS) -ccflags-y += $(RUNTIME_FLAGS) $(RUNTIME_CFLAGS) -include $(AUTOCONF_H) -asflags-y += --target=$(TRIPLE_USER) -fPIC -I$(HEADER_EXPORT_INCLUDE) -$(addprefix $(obj)/,$(RELIEFOS_LIBPNG_C_OBJECTS)): $(AUTOCONF_H) $(GBK_TABLE) $(PNG_CONFIG) $(MUSL_STAMP) $(RUNTIME_HEADERS) $(HEADER_EXPORT_MANIFEST) $(RELIEFOS_CC_ID) -$(addprefix $(obj)/,$(RELIEFOS_LIBPNG_ASM_OBJECTS)): $(AUTOCONF_H) $(HEADER_EXPORT_MANIFEST) $(RELIEFOS_CC_ID) - -ccflags-y += -DRELIEFOS_LIBPNG_FIXED_POINT=3 -$(obj)/png.o: $(srctree)/third_party/libpng/png.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngerror.o: $(srctree)/third_party/libpng/pngerror.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngget.o: $(srctree)/third_party/libpng/pngget.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngmem.o: $(srctree)/third_party/libpng/pngmem.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngpread.o: $(srctree)/third_party/libpng/pngpread.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngread.o: $(srctree)/third_party/libpng/pngread.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngrio.o: $(srctree)/third_party/libpng/pngrio.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngrtran.o: $(srctree)/third_party/libpng/pngrtran.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngrutil.o: $(srctree)/third_party/libpng/pngrutil.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngset.o: $(srctree)/third_party/libpng/pngset.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngtrans.o: $(srctree)/third_party/libpng/pngtrans.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngwio.o: $(srctree)/third_party/libpng/pngwio.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngwrite.o: $(srctree)/third_party/libpng/pngwrite.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngwtran.o: $(srctree)/third_party/libpng/pngwtran.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pngwutil.o: $(srctree)/third_party/libpng/pngwutil.c FORCE - $(call if_changed_dep,cc_o_c) -endif diff --git a/userland/runtime/vendor/mbedtls/Kbuild b/userland/runtime/vendor/mbedtls/Kbuild deleted file mode 100644 index 7142c962..00000000 --- a/userland/runtime/vendor/mbedtls/Kbuild +++ /dev/null @@ -1,98 +0,0 @@ -RELIEFOS_MBEDTLS_OBJECTS := aes.o asn1parse.o asn1write.o base64.o bignum.o bignum_core.o bignum_mod.o bignum_mod_raw.o block_cipher.o cipher.o cipher_wrap.o constant_time.o ctr_drbg.o ecdh.o ecdsa.o ecp.o ecp_curves.o entropy.o entropy_poll.o error.o gcm.o md.o oid.o pem.o pk.o pk_ecc.o pk_wrap.o pkcs5.o pkparse.o pkwrite.o platform.o platform_util.o rsa.o rsa_alt_helpers.o sha1.o sha256.o sha512.o ssl_ciphersuites.o ssl_client.o ssl_msg.o ssl_tls.o ssl_tls12_client.o x509.o x509_crt.o -RELIEFOS_MBEDTLS_C_OBJECTS := aes.o asn1parse.o asn1write.o base64.o bignum.o bignum_core.o bignum_mod.o bignum_mod_raw.o block_cipher.o cipher.o cipher_wrap.o constant_time.o ctr_drbg.o ecdh.o ecdsa.o ecp.o ecp_curves.o entropy.o entropy_poll.o error.o gcm.o md.o oid.o pem.o pk.o pk_ecc.o pk_wrap.o pkcs5.o pkparse.o pkwrite.o platform.o platform_util.o rsa.o rsa_alt_helpers.o sha1.o sha256.o sha512.o ssl_ciphersuites.o ssl_client.o ssl_msg.o ssl_tls.o ssl_tls12_client.o x509.o x509_crt.o -RELIEFOS_MBEDTLS_ASM_OBJECTS := -ifneq ($(RELIEFOS_CAPTURE_RUNTIME_KBUILD),1) -obj-y := $(RELIEFOS_MBEDTLS_OBJECTS) -ccflags-y += $(RUNTIME_FLAGS) $(RUNTIME_CFLAGS) -include $(AUTOCONF_H) -asflags-y += --target=$(TRIPLE_USER) -fPIC -I$(HEADER_EXPORT_INCLUDE) -$(addprefix $(obj)/,$(RELIEFOS_MBEDTLS_C_OBJECTS)): $(AUTOCONF_H) $(GBK_TABLE) $(PNG_CONFIG) $(MUSL_STAMP) $(RUNTIME_HEADERS) $(HEADER_EXPORT_MANIFEST) $(RELIEFOS_CC_ID) -$(addprefix $(obj)/,$(RELIEFOS_MBEDTLS_ASM_OBJECTS)): $(AUTOCONF_H) $(HEADER_EXPORT_MANIFEST) $(RELIEFOS_CC_ID) -$(obj)/aes.o: $(srctree)/third_party/mbedtls/library/aes.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/asn1parse.o: $(srctree)/third_party/mbedtls/library/asn1parse.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/asn1write.o: $(srctree)/third_party/mbedtls/library/asn1write.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/base64.o: $(srctree)/third_party/mbedtls/library/base64.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/bignum.o: $(srctree)/third_party/mbedtls/library/bignum.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/bignum_core.o: $(srctree)/third_party/mbedtls/library/bignum_core.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/bignum_mod.o: $(srctree)/third_party/mbedtls/library/bignum_mod.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/bignum_mod_raw.o: $(srctree)/third_party/mbedtls/library/bignum_mod_raw.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/block_cipher.o: $(srctree)/third_party/mbedtls/library/block_cipher.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/cipher.o: $(srctree)/third_party/mbedtls/library/cipher.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/cipher_wrap.o: $(srctree)/third_party/mbedtls/library/cipher_wrap.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/constant_time.o: $(srctree)/third_party/mbedtls/library/constant_time.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ctr_drbg.o: $(srctree)/third_party/mbedtls/library/ctr_drbg.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ecdh.o: $(srctree)/third_party/mbedtls/library/ecdh.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ecdsa.o: $(srctree)/third_party/mbedtls/library/ecdsa.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ecp.o: $(srctree)/third_party/mbedtls/library/ecp.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ecp_curves.o: $(srctree)/third_party/mbedtls/library/ecp_curves.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/entropy.o: $(srctree)/third_party/mbedtls/library/entropy.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/entropy_poll.o: $(srctree)/third_party/mbedtls/library/entropy_poll.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/error.o: $(srctree)/third_party/mbedtls/library/error.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/gcm.o: $(srctree)/third_party/mbedtls/library/gcm.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/md.o: $(srctree)/third_party/mbedtls/library/md.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/oid.o: $(srctree)/third_party/mbedtls/library/oid.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pem.o: $(srctree)/third_party/mbedtls/library/pem.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pk.o: $(srctree)/third_party/mbedtls/library/pk.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pk_ecc.o: $(srctree)/third_party/mbedtls/library/pk_ecc.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pk_wrap.o: $(srctree)/third_party/mbedtls/library/pk_wrap.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pkcs5.o: $(srctree)/third_party/mbedtls/library/pkcs5.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pkparse.o: $(srctree)/third_party/mbedtls/library/pkparse.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/pkwrite.o: $(srctree)/third_party/mbedtls/library/pkwrite.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/platform.o: $(srctree)/third_party/mbedtls/library/platform.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/platform_util.o: $(srctree)/third_party/mbedtls/library/platform_util.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/rsa.o: $(srctree)/third_party/mbedtls/library/rsa.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/rsa_alt_helpers.o: $(srctree)/third_party/mbedtls/library/rsa_alt_helpers.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/sha1.o: $(srctree)/third_party/mbedtls/library/sha1.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/sha256.o: $(srctree)/third_party/mbedtls/library/sha256.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/sha512.o: $(srctree)/third_party/mbedtls/library/sha512.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ssl_ciphersuites.o: $(srctree)/third_party/mbedtls/library/ssl_ciphersuites.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ssl_client.o: $(srctree)/third_party/mbedtls/library/ssl_client.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ssl_msg.o: $(srctree)/third_party/mbedtls/library/ssl_msg.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ssl_tls.o: $(srctree)/third_party/mbedtls/library/ssl_tls.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/ssl_tls12_client.o: $(srctree)/third_party/mbedtls/library/ssl_tls12_client.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/x509.o: $(srctree)/third_party/mbedtls/library/x509.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/x509_crt.o: $(srctree)/third_party/mbedtls/library/x509_crt.c FORCE - $(call if_changed_dep,cc_o_c) -endif diff --git a/userland/runtime/vendor/zlib/Kbuild b/userland/runtime/vendor/zlib/Kbuild deleted file mode 100644 index 54370f5a..00000000 --- a/userland/runtime/vendor/zlib/Kbuild +++ /dev/null @@ -1,34 +0,0 @@ -RELIEFOS_ZLIB_OBJECTS := adler32.o compress.o crc32.o deflate.o infback.o inffast.o inflate.o inftrees.o trees.o uncompr.o zutil.o -RELIEFOS_ZLIB_C_OBJECTS := adler32.o compress.o crc32.o deflate.o infback.o inffast.o inflate.o inftrees.o trees.o uncompr.o zutil.o -RELIEFOS_ZLIB_ASM_OBJECTS := -ifneq ($(RELIEFOS_CAPTURE_RUNTIME_KBUILD),1) -obj-y := $(RELIEFOS_ZLIB_OBJECTS) -ccflags-y += $(RUNTIME_FLAGS) $(RUNTIME_CFLAGS) -include $(AUTOCONF_H) -asflags-y += --target=$(TRIPLE_USER) -fPIC -I$(HEADER_EXPORT_INCLUDE) -$(addprefix $(obj)/,$(RELIEFOS_ZLIB_C_OBJECTS)): $(AUTOCONF_H) $(GBK_TABLE) $(PNG_CONFIG) $(MUSL_STAMP) $(RUNTIME_HEADERS) $(HEADER_EXPORT_MANIFEST) $(RELIEFOS_CC_ID) -$(addprefix $(obj)/,$(RELIEFOS_ZLIB_ASM_OBJECTS)): $(AUTOCONF_H) $(HEADER_EXPORT_MANIFEST) $(RELIEFOS_CC_ID) - -ccflags-y += -DZ_SOLO -include stddef.h -$(obj)/adler32.o: $(srctree)/third_party/zlib/adler32.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/compress.o: $(srctree)/third_party/zlib/compress.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/crc32.o: $(srctree)/third_party/zlib/crc32.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/deflate.o: $(srctree)/third_party/zlib/deflate.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/infback.o: $(srctree)/third_party/zlib/infback.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/inffast.o: $(srctree)/third_party/zlib/inffast.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/inflate.o: $(srctree)/third_party/zlib/inflate.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/inftrees.o: $(srctree)/third_party/zlib/inftrees.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/trees.o: $(srctree)/third_party/zlib/trees.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/uncompr.o: $(srctree)/third_party/zlib/uncompr.c FORCE - $(call if_changed_dep,cc_o_c) -$(obj)/zutil.o: $(srctree)/third_party/zlib/zutil.c FORCE - $(call if_changed_dep,cc_o_c) -endif diff --git a/userland/sqlite/Kbuild b/userland/sqlite/Kbuild deleted file mode 100644 index 2dc6142e..00000000 --- a/userland/sqlite/Kbuild +++ /dev/null @@ -1,7 +0,0 @@ -RELIEFOS_SQLITE_VFS_OBJECTS := leonos_sqlite_vfs.o -RELIEFOS_SQLITE_VFS_C_OBJECTS := $(RELIEFOS_SQLITE_VFS_OBJECTS) -ifneq ($(RELIEFOS_CAPTURE_SQLITE_VFS_KBUILD),1) -obj-y := $(RELIEFOS_SQLITE_VFS_OBJECTS) -ccflags-y += $(UPSTREAM_SQLITE_VFS_FLAGS) -$(addprefix $(obj)/,$(RELIEFOS_SQLITE_VFS_C_OBJECTS)): $(RELIEFOS_CC_ID) -endif diff --git a/userland/sqlite/leonos_sqlite_vfs.c b/userland/sqlite/leonos_sqlite_vfs.c deleted file mode 100644 index de1b62e6..00000000 --- a/userland/sqlite/leonos_sqlite_vfs.c +++ /dev/null @@ -1,280 +0,0 @@ -/* - * ReliefOS VFS adapter for SQLite. - * - * SQLite is built with SQLITE_OS_OTHER, so this file is the complete file - * and clock boundary. ReliefOS currently exposes no cross-process advisory - * locking primitive to userland; locking callbacks therefore serialize only - * within SQLite's single-threaded connection and WAL is disabled by the - * build. Do not use this VFS for concurrent writers until that contract is - * extended. - */ - -#include "sqlite3.h" -#include -#include -#include -#include -#include -#include - -struct ReliefosStatRaw { - uint32_t type; - uint32_t reserved; - uint64_t size; -}; - -extern int reliefos_stat_raw_call(const char *path, struct ReliefosStatRaw *status) - __asm__("stat"); -extern int reliefos_fstat_raw_call(int fd, struct ReliefosStatRaw *status) - __asm__("fstat"); -extern int reliefos_ftruncate_call(int fd, long length) __asm__("ftruncate"); -extern int sleep_ms(unsigned long milliseconds); - -typedef struct ReliefosFile { - sqlite3_file base; - int fd; - int readonly; -} ReliefosFile; - -static int reliefos_close(sqlite3_file *file) -{ - ReliefosFile *handle = (ReliefosFile *)file; - int result = close(handle->fd); - handle->fd = -1; - return result == 0 ? SQLITE_OK : SQLITE_IOERR_CLOSE; -} - -static int reliefos_read(sqlite3_file *file, void *buffer, int amount, sqlite3_int64 offset) -{ - ReliefosFile *handle = (ReliefosFile *)file; - long position; - long received; - position = lseek(handle->fd, (long)offset, SEEK_SET); - if (position < 0) { - return SQLITE_IOERR_SEEK; - } - received = read(handle->fd, buffer, (size_t)amount); - if (received < 0) { - return SQLITE_IOERR_READ; - } - if (received < amount) { - memset((unsigned char *)buffer + received, 0, (size_t)amount - (size_t)received); - return SQLITE_IOERR_SHORT_READ; - } - return SQLITE_OK; -} - -static int reliefos_write(sqlite3_file *file, const void *buffer, int amount, sqlite3_int64 offset) -{ - ReliefosFile *handle = (ReliefosFile *)file; - long position; - long written; - if (handle->readonly) { - return SQLITE_READONLY; - } - position = lseek(handle->fd, (long)offset, SEEK_SET); - if (position < 0) { - return SQLITE_IOERR_SEEK; - } - written = write(handle->fd, buffer, (size_t)amount); - return written == amount ? SQLITE_OK : SQLITE_IOERR_WRITE; -} - -static int reliefos_truncate(sqlite3_file *file, sqlite3_int64 size) -{ - ReliefosFile *handle = (ReliefosFile *)file; - if (handle->readonly || reliefos_ftruncate_call(handle->fd, (long)size) != 0) { - return handle->readonly ? SQLITE_READONLY : SQLITE_IOERR_TRUNCATE; - } - return SQLITE_OK; -} - -static int reliefos_sync(sqlite3_file *file, int flags) -{ - (void)file; - (void)flags; - /* ReliefOS filesystem writes commit metadata as part of the write syscall. */ - return SQLITE_OK; -} - -static int reliefos_size(sqlite3_file *file, sqlite3_int64 *size) -{ - struct ReliefosStatRaw status; - ReliefosFile *handle = (ReliefosFile *)file; - if (reliefos_fstat_raw_call(handle->fd, &status) != 0) { - return SQLITE_IOERR_FSTAT; - } - *size = (sqlite3_int64)status.size; - return SQLITE_OK; -} - -static int reliefos_lock(sqlite3_file *file, int lock) -{ - (void)file; - (void)lock; - return SQLITE_OK; -} - -static int reliefos_unlock(sqlite3_file *file, int lock) -{ - (void)file; - (void)lock; - return SQLITE_OK; -} - -static int reliefos_reserved(sqlite3_file *file, int *reserved) -{ - (void)file; - *reserved = 0; - return SQLITE_OK; -} - -static int reliefos_file_control(sqlite3_file *file, int operation, void *argument) -{ - (void)file; - (void)operation; - (void)argument; - return SQLITE_NOTFOUND; -} - -static int reliefos_sector_size(sqlite3_file *file) -{ - (void)file; - return 512; -} - -static int reliefos_device_characteristics(sqlite3_file *file) -{ - (void)file; - return 0; -} - -static const sqlite3_io_methods reliefos_io = { - .iVersion = 1, - .xClose = reliefos_close, - .xRead = reliefos_read, - .xWrite = reliefos_write, - .xTruncate = reliefos_truncate, - .xSync = reliefos_sync, - .xFileSize = reliefos_size, - .xLock = reliefos_lock, - .xUnlock = reliefos_unlock, - .xCheckReservedLock = reliefos_reserved, - .xFileControl = reliefos_file_control, - .xSectorSize = reliefos_sector_size, - .xDeviceCharacteristics = reliefos_device_characteristics, -}; - -static int reliefos_open(sqlite3_vfs *vfs, const char *path, sqlite3_file *file, - int flags, int *out_flags) -{ - ReliefosFile *handle = (ReliefosFile *)file; - int native_flags; - int fd; - (void)vfs; - memset(handle, 0, sizeof(*handle)); - handle->fd = -1; - if (flags & SQLITE_OPEN_READWRITE) { - native_flags = O_RDWR; - if (flags & SQLITE_OPEN_CREATE) { - native_flags |= O_CREAT; - } - } else { - native_flags = O_RDONLY; - } - fd = open(path, native_flags, 0666); - if (fd < 0 && (flags & SQLITE_OPEN_CREATE) && (flags & SQLITE_OPEN_READWRITE)) { - return SQLITE_CANTOPEN; - } - if (fd < 0) { - return SQLITE_CANTOPEN; - } - handle->fd = fd; - handle->readonly = (native_flags & O_ACCMODE) == O_RDONLY; - handle->base.pMethods = &reliefos_io; - if (out_flags) { - *out_flags = flags; - } - return SQLITE_OK; -} - -static int reliefos_delete(sqlite3_vfs *vfs, const char *path, int sync_dir) -{ - (void)vfs; - (void)sync_dir; - return unlink(path) == 0 ? SQLITE_OK : SQLITE_IOERR_DELETE; -} - -static int reliefos_access(sqlite3_vfs *vfs, const char *path, int flags, int *result) -{ - struct ReliefosStatRaw status; - (void)vfs; - (void)flags; - *result = reliefos_stat_raw_call(path, &status) == 0; - return SQLITE_OK; -} - -static int reliefos_full_pathname(sqlite3_vfs *vfs, const char *path, int length, char *output) -{ - (void)vfs; - if (length <= 0) { - return SQLITE_CANTOPEN; - } - strncpy(output, path, (size_t)length - 1U); - output[length - 1] = '\0'; - return SQLITE_OK; -} - -static int reliefos_randomness(sqlite3_vfs *vfs, int length, char *output) -{ - static uint32_t state = 0x9e3779b9U; - int index; - (void)vfs; - for (index = 0; index < length; ++index) { - state ^= state << 13; - state ^= state >> 17; - state ^= state << 5; - output[index] = (char)(state >> 24); - } - return length; -} - -static int reliefos_sleep(sqlite3_vfs *vfs, int microseconds) -{ - (void)vfs; - sleep_ms((unsigned long)((microseconds + 999) / 1000)); - return microseconds; -} - -static int reliefos_current_time(sqlite3_vfs *vfs, double *now) -{ - (void)vfs; - /* 2440587.5 is the Julian day at Unix epoch. */ - *now = 2440587.5 + ((double)time(NULL) / 86400.0); - return SQLITE_OK; -} - -static sqlite3_vfs reliefos_vfs = { - .iVersion = 1, - .szOsFile = sizeof(ReliefosFile), - .mxPathname = 256, - /* Public VFS selection name retained for existing applications. */ - .zName = "leonos", - .xOpen = reliefos_open, - .xDelete = reliefos_delete, - .xAccess = reliefos_access, - .xFullPathname = reliefos_full_pathname, - .xRandomness = reliefos_randomness, - .xSleep = reliefos_sleep, - .xCurrentTime = reliefos_current_time, -}; - -int sqlite3_os_init(void) -{ - return sqlite3_vfs_register(&reliefos_vfs, 1); -} - -int sqlite3_os_end(void) -{ - return sqlite3_vfs_unregister(&reliefos_vfs); -} diff --git a/userland/storage/leonos-apk-update b/userland/storage/leonos-apk-update deleted file mode 100644 index 8a91535d..00000000 --- a/userland/storage/leonos-apk-update +++ /dev/null @@ -1,196 +0,0 @@ -#!/bin/sh -# The installer runs this from its own root, against the mounted target. -set -eu -PATH=/usr/sbin:/usr/bin:/sbin:/bin -LC_ALL=C -export PATH LC_ALL -if [ "$#" -ne 2 ]; then - echo "usage: reliefos-apk-update TARGET_ROOT SIGNED_REPOSITORY" >&2 - exit 2 -fi -target=$1 -repository=$2 -case "$target:$repository" in - /*:/*) ;; - *) echo "target and repository must be absolute paths" >&2; exit 2 ;; -esac -if [ ! -s "$target/lib/apk/db/installed" ]; then - echo "Target has no APK database; reinstall from APK-enabled media." >&2 - exit 1 -fi -if [ ! -f "$repository/packages.adb" ]; then - echo "Missing signed ReliefOS repository index." >&2 - exit 1 -fi -# Retain only repository tags used by world, plus the signed update source. -# Unused network repositories must not make a fresh system's offline update fail. -temporary=/tmp/reliefos-apk-update.$$ -(umask 077; /bin/busybox mkdir "$temporary") -restore_world=0 -retention_started=0 -activation_deferred=0 -kernel_bundle=${RELIEFOS_KERNEL_BUNDLE_HELPER:-/usr/libexec/reliefos/kernel-bundle} -activation_marker=$target/run/reliefos-kernel-activation.defer -cleanup() { - status=$? - trap - EXIT HUP INT TERM - set +e - # APK may have removed old version-owned files even on a failed commit. - # A killed transaction is additionally recovered by the static early tool. - if [ "$retention_started" -eq 1 ]; then - "$kernel_bundle" --restore-all "$target" || status=1 - fi - if [ "$restore_world" -eq 1 ]; then - if /bin/busybox cp "$temporary/world-before" "$target/etc/apk/world.restore.$$" && - /bin/busybox mv "$target/etc/apk/world.restore.$$" "$target/etc/apk/world"; then : - else status=1 - fi - fi - if [ "$activation_deferred" -eq 1 ]; then - /bin/busybox rm -f "$activation_marker" || status=1 - fi - /bin/busybox rm -rf "$temporary" - exit "$status" -} -trap cleanup EXIT HUP INT TERM -( /bin/busybox mkdir "$temporary/seen" ) -required_tags=' ' -while IFS= read -r dependency || [ -n "$dependency" ]; do - case "$dependency" in - *@*) tag=${dependency#*@}; tag=${tag%%[\<\>\=\~]*}; required_tags="$required_tags$tag " ;; - esac -done < "$target/etc/apk/world" -configuration=$temporary/repositories -printf 'ndx %s/packages.adb\n' "$repository" > "$configuration" -append_repositories() { - [ -f "$1" ] || return 0 - while IFS= read -r line || [ -n "$line" ]; do - read -r first second third rest <> "$configuration"; continue ;; - @*) kind=; tag=$first; url=$second; components="$third $rest" ;; - v2|v3|ndx) kind=$first; tag=$second; url=$third; components=$rest ;; - *) continue ;; - esac - case "$tag" in @*) ;; *) continue ;; esac - case "$required_tags" in *" ${tag#@} "*) ;; *) continue ;; esac - # Local paths in the target's configuration are in its namespace. - case "$url" in - /*) url=$target$url ;; - file:///*) url=$target${url#file://} ;; - esac - printf '%s %s %s %s\n' "$kind" "$tag" "$url" "$components" >> "$configuration" - done < "$1" -} -append_repositories "$target/etc/apk/repositories" -for directory in "$target/etc/apk/repositories.d" "$target/lib/apk/repositories.d"; do - for snippet in "$directory"/*.list; do - [ -f "$snippet" ] || continue - name=${snippet##*/} - [ ! -e "$temporary/seen/$name" ] || continue - : > "$temporary/seen/$name" - append_repositories "$snippet" - done -done -# Target trust anchors and cached signed indexes remain authoritative. Missing -# metadata for an in-use tag is an error, never a reason to rewrite world. -# apk 3 counts unchanged broken packages in the transaction's exit status. -# Parse installed-db records (P/V/C/f), retaining identity and the exact flags; -# uppercase S is a digest-format flag, not a broken script (lowercase s). -broken_packages() { - /bin/busybox awk -v exclude_system="$1" ' - function emit() { - if (flags ~ /[fs]/ && (!exclude_system || name !~ /^(leonos|reliefos)-/)) - print name "\t" version "\t" checksum "\t" flags - name = version = checksum = flags = "" - } - /^P:/ { name = substr($0, 3) } - /^V:/ { version = substr($0, 3) } - /^C:/ { checksum = substr($0, 3) } - /^f:/ { flags = substr($0, 3) } - /^$/ { emit() } - END { emit() } - ' "$target/lib/apk/db/installed" > "$temporary/records" - /bin/busybox sort "$temporary/records" -} -# Download and validate every ReliefOS package before changing world or letting -# apk remove any legacy package. The signed index binds each archive's digest. -preflight_repository=$temporary/preflight -/bin/busybox mkdir "$preflight_repository" -status=0 -apk --root "$target" --repositories-file "$configuration" --no-network fetch --recursive \ - --output "$preflight_repository" 'reliefos-*' > "$temporary/preflight.log" 2>&1 || status=$? -/bin/busybox cat "$temporary/preflight.log" -if [ "$status" -ne 0 ]; then - exit "$status" -fi -/bin/busybox cp "$repository/packages.adb" "$preflight_repository/packages.adb" -preflight_configuration=$temporary/repositories.preflight -/bin/busybox awk -v repo_index="$preflight_repository/packages.adb" \ - 'NR == 1 { print "ndx " repo_index; next } { print }' \ - "$configuration" > "$preflight_configuration" -configuration=$preflight_configuration -# Copy known old-brand configuration before apk introduces canonical defaults. -# The migrator never overwrites an existing canonical file and verifies bytes, -# mode and ownership before publishing; a failure prevents the transaction. -/bin/sh /usr/lib/reliefos/reliefos-migrate "$target" -# The caller is the offline installer, which publishes its selected BIOS/UEFI -# resources only after this entire signed package transaction and root sync. -# Retain old modules first, then defer the kernel package's activation script -# within this invocation. Normal system APK operations have no defer marker. -case "$kernel_bundle" in /*) ;; *) echo 'kernel bundle helper must be absolute' >&2; exit 1 ;; esac -[ -x "$kernel_bundle" ] || { echo 'Missing native kernel retention helper.' >&2; exit 1; } -"$kernel_bundle" --retain-installed "$target" -retention_started=1 -[ ! -L "$target/run" ] && { [ ! -e "$target/run" ] || [ -d "$target/run" ]; } || exit 1 -/bin/busybox mkdir -p "$target/run" -[ ! -e "$activation_marker" ] && [ ! -L "$activation_marker" ] || { - echo 'An offline kernel activation is already pending.' >&2; exit 1; -} -(umask 077; set -C; printf '%s\n' 'ReliefOS offline installer boot publication pending' > "$activation_marker") -activation_deferred=1 -broken_packages 1 > "$temporary/broken-before" -world=$target/etc/apk/world -/bin/busybox cp "$world" "$temporary/world-before" -sed 's/^leonos-/reliefos-/' "$temporary/world-before" > "$temporary/world-after" -# Move the desired package names first so apk's own solver removes each old -# package through the replacement metadata in the signed ReliefOS index. -/bin/busybox cp "$temporary/world-after" "$target/etc/apk/world.reliefos-update.$$" -/bin/busybox mv "$target/etc/apk/world.reliefos-update.$$" "$world" -restore_world=1 -status=0 -apk --root "$target" --repositories-file "$configuration" --no-network upgrade 'reliefos-*' \ - > "$temporary/transaction.log" 2>&1 || status=$? -/bin/busybox cat "$temporary/transaction.log" -if [ "$status" -ne 0 ]; then - broken_packages 0 > "$temporary/broken-after" - count=$(/bin/busybox awk 'END { print NR }' "$temporary/broken-before") - # Require a completed commit, no new diagnostics, unchanged external - # broken records/world, and exactly their known error contribution. - # Solver, trust, extraction, script and database-write failures remain fatal. - if [ "$count" -gt 0 ] && [ "$count" -lt 125 ] && [ "$status" -eq "$count" ] && - /bin/busybox cmp -s "$temporary/broken-before" "$temporary/broken-after" && - /bin/busybox cmp -s "$temporary/world-after" "$target/etc/apk/world" && - ! /bin/busybox grep -q '^ERROR:' "$temporary/transaction.log" && - /bin/busybox tail -n 1 "$temporary/transaction.log" | - /bin/busybox grep -Eq "^$status errors?; .+ in [0-9]+ packages$"; then - echo "WARNING: ReliefOS packages updated; pre-existing external package errors remain:" >&2 - /bin/busybox cut -f 1 "$temporary/broken-before" >&2 - else - exit "$status" - fi -fi -restore_world=0 -/bin/busybox chroot "$target" /bin/sh /usr/lib/reliefos/migrate-shell -# Publish the offline repository only after the package transaction succeeded. -destination=$target/usr/share/reliefos/apk/repository -/bin/busybox mkdir -p "$destination" -for package in "$repository"/*.apk; do - /bin/busybox cp "$package" "$destination/$(/bin/busybox basename "$package").new" - /bin/busybox mv "$destination/$(/bin/busybox basename "$package").new" "$destination/$(/bin/busybox basename "$package")" -done -/bin/busybox cp "$repository/packages.adb" "$destination/packages.adb.new" -/bin/busybox mv "$destination/packages.adb.new" "$destination/packages.adb" -/bin/busybox sync diff --git a/userland/storage/leonos-grub-installer b/userland/storage/leonos-grub-installer index b028eb50..fafb17a7 100755 --- a/userland/storage/leonos-grub-installer +++ b/userland/storage/leonos-grub-installer @@ -1,17 +1,5 @@ #!/bin/sh -# Compatibility spelling for the shared UEFI-only boot publisher. +# Compatibility name; all validation/publication uses the native installer. +# The old ESP-only copy interface cannot identify a root safely. set -eu -PATH=/usr/sbin:/usr/bin:/sbin:/bin -export PATH -source=/install/esp -if [ "${1-}" = --source ]; then - [ "$#" -ge 3 ] || { printf '%s\n' 'Usage: reliefos-grub-installer [--source DIR] ESP-MOUNTPOINT' >&2; exit 2; } - source=$2 - shift 2 -fi -[ "$#" -eq 1 ] && [ -d "$1" ] || { - printf '%s\n' 'Usage: reliefos-grub-installer [--source DIR] ESP-MOUNTPOINT' >&2 - exit 2 -} -exec /usr/sbin/reliefos-boot-install --source "$source" --boot-directory "$1" \ - --bios=no --uefi=yes --update +exec /usr/sbin/reliefos-boot-install --bios=no --uefi=yes --update "$@" diff --git a/userland/storage/reliefos-apk-update b/userland/storage/reliefos-apk-update new file mode 100755 index 00000000..d707abf3 --- /dev/null +++ b/userland/storage/reliefos-apk-update @@ -0,0 +1,298 @@ +#!/bin/sh +# Signed offline package transactions for the installer. Inspection uses a +# private database and accepts a read-only installed root. +set -eu +PATH=/usr/sbin:/usr/bin:/sbin:/bin +LC_ALL=C +export PATH LC_ALL +umask 077 + +die() { printf 'update: %s\n' "$*" >&2; exit 1; } +mode=apply +expected_index= +case "$#:${1:-}" in + 2:*) target=$1; repository=$2 ;; + 3:--check|3:--apply|4:--apply) + mode=${1#--}; target=$2; repository=$3; expected_index=${4:-} ;; + *) echo 'usage: reliefos-apk-update [--check|--apply] TARGET_ROOT SIGNED_REPOSITORY [INDEX_SHA256]' >&2; exit 2 ;; +esac +case "$target:$repository" in /*:/*) ;; *) die 'absolute target and repository paths are required' ;; esac +[ ! -L "$target" ] && [ ! -L "$repository" ] || die 'target and repository must be real directories' +target=$(CDPATH= cd -- "$target" && pwd -P) +repository=$(CDPATH= cd -- "$repository" && pwd -P) +[ "$target" != / ] || die 'boot the installation media to update an offline system' +for file in "$target/etc/apk/world" "$target/lib/apk/db/installed" "$repository/packages.adb"; do + [ -s "$file" ] && [ ! -L "$file" ] || die "missing or unsafe package metadata: $file" +done +[ -d "$target/etc/apk/keys" ] && [ ! -L "$target/etc/apk/keys" ] || die 'installed package trust keys are missing' +index_sha=$(/bin/busybox sha256sum "$repository/packages.adb") +index_sha=${index_sha%% *} +[ -z "$expected_index" ] || [ "$index_sha" = "$expected_index" ] || die 'installation media changed after inspection; check the update again' + +temporary=$(/bin/busybox mktemp -d /tmp/reliefos-update.XXXXXX) +world_changed=0 +retention_started=0 +activation_deferred=0 +kernel_bundle=${RELIEFOS_KERNEL_BUNDLE_HELPER:-/usr/libexec/reliefos/kernel-bundle} +activation_marker=$target/run/reliefos-kernel-activation.defer +cleanup() { + result=$? + trap - EXIT HUP INT TERM + set +e + if [ "$retention_started" -eq 1 ]; then + "$kernel_bundle" --restore-all "$target" || result=1 + fi + # Only roll world back when APK did not modify its database. A partially + # committed database must keep APK's actual world for a consistent retry. + if [ "$result" -ne 0 ] && [ "$world_changed" -eq 1 ] && + /bin/busybox cmp -s "$temporary/installed-before" "$target/lib/apk/db/installed"; then + if /bin/busybox cp -p "$temporary/world-before" "$target/etc/apk/world.restore.$$" && + /bin/busybox mv "$target/etc/apk/world.restore.$$" "$target/etc/apk/world"; then : + else result=1 + fi + fi + if [ "$activation_deferred" -eq 1 ]; then + /bin/busybox rm -f "$activation_marker" || result=1 + fi + /bin/busybox rm -rf "$temporary" + exit "$result" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +plan=$temporary/root +/bin/busybox mkdir -p "$plan/etc/apk" "$plan/lib/apk/db" "$plan/var/cache/apk" "$temporary/repository" +/bin/busybox cp "$target/lib/apk/db/installed" "$plan/lib/apk/db/installed" +/bin/busybox cp "$target/lib/apk/db/installed" "$temporary/installed-before" +/bin/busybox cp -p "$target/etc/apk/world" "$temporary/world-before" +# Retired local commands are selected directly from their official packages. +# Local generation/version constraints cannot describe upstream versions. Keep +# existing upstream requests (including their tags/pins) and all unrelated +# world entries, using an inherited tag only when adding a missing request. +# Editing the private plan lets APK remove the old owners in the same signed +# transaction that installs their replacements; --check never edits the target. +: > "$temporary/upstream-packages" +/bin/busybox awk -v upstream_packages="$temporary/upstream-packages" ' + function canonical(request) { + sub(/^leonos-/, "reliefos-", request) + return request + } + function package_name(request) { + sub(/[@<>=~].*$/, "", request) + return request + } + function emit(request) { + if (!seen[request]++) print request + } + function upstream(name, tag) { + if (!(name in present)) emit(name tag) + print name >> upstream_packages + } + NR == FNR { present[package_name(canonical($0))] = 1; next } + { + request = canonical($0) + name = package_name(request) + if (name !~ /^reliefos-(sl|ncurses|sqlite)$/) { emit(request); next } + tag = "" + if (index(request, "@")) { + tag = request + sub(/^[^@]*@/, "", tag) + sub(/[<>=~].*$/, "", tag) + tag = "@" tag + } + if (name == "reliefos-sl") upstream("sl", tag) + if (name == "reliefos-ncurses") { + upstream("ncurses", tag) + upstream("ncurses-terminfo", tag) + } + if (name == "reliefos-sqlite") { + upstream("sqlite", tag) + upstream("sqlite-libs", tag) + } + } +' "$temporary/world-before" "$temporary/world-before" > "$plan/etc/apk/world" +if [ -f "$target/etc/apk/arch" ]; then + [ ! -L "$target/etc/apk/arch" ] || die 'unsafe installed architecture file' + /bin/busybox cp "$target/etc/apk/arch" "$plan/etc/apk/arch" +fi + +# APK requires each world tag to have an available repository even when its +# packages are already installed. Register those tags against the same signed +# offline media for this transaction only. Installed packages still satisfy +# world/dependency constraints; new archives can only come from the media. +# Do not depend on online repositories or their possibly absent/stale caches, +# and never strip tags from the installed world or rewrite repository settings. +: > "$temporary/tags" +while IFS= read -r dependency || [ -n "$dependency" ]; do + case "$dependency" in + *@*) + tag=${dependency#*@}; tag=${tag%%[\<\>\=\~]*} + case "$tag" in ''|*[!a-zA-Z0-9_.+-]*) die 'invalid repository tag in installed world' ;; esac + printf '%s\n' "$tag" >> "$temporary/tags" + ;; + esac +done < "$plan/etc/apk/world" +/bin/busybox sort -u "$temporary/tags" > "$temporary/required-tags" +write_repositories() { + source_repository=$1 + tag_repositories=$2 + printf 'ndx %s/packages.adb\n' "$source_repository" + number=0 + while IFS= read -r tag; do + number=$((number + 1)) + tagged_repository=$tag_repositories/$number + /bin/busybox mkdir -p "$tagged_repository" + /bin/busybox cp "$source_repository/packages.adb" "$tagged_repository/packages.adb" + for archive in "$source_repository"/*.apk; do + [ -f "$archive" ] || continue + /bin/busybox ln -s "$archive" "$tagged_repository/${archive##*/}" + done + # Distinct index URLs keep APK's default/tagged repository masks + # separate, preserving the installed origin tag of retained extras. + printf 'ndx @%s %s/packages.adb\n' "$tag" "$tagged_repository" + done < "$temporary/required-tags" +} +configuration=$temporary/repositories +write_repositories "$repository" "$temporary/tagged-media" > "$configuration" + +broken_packages() { + /bin/busybox awk -v exclude_system="$2" ' + function emit() { + if (flags ~ /[fs]/ && (!exclude_system || name !~ /^(leonos|reliefos)-/)) + print name "\t" version "\t" checksum "\t" flags + name = version = checksum = flags = "" + } + /^P:/ { name = substr($0, 3) } + /^V:/ { version = substr($0, 3) } + /^C:/ { checksum = substr($0, 3) } + /^f:/ { flags = substr($0, 3) } + /^$/ { emit() } + END { emit() } + ' "$1/lib/apk/db/installed" | /bin/busybox sort +} + +# Preserve APK's exact exit status while streaming its output through tee. +# The status file is written before the pipeline closes, including on errors. +run_transaction() { + run_root=$1 + run_log=$2 + operation=$3 + shift 3 + exclude_system=1 + [ "$#" -eq 0 ] || exclude_system=0 + broken_packages "$run_root" "$exclude_system" > "$temporary/broken-before" + ( + set +e + if [ "$operation" = fix ]; then + apk --root "$run_root" --keys-dir "$target/etc/apk/keys" \ + --repositories-file "$configuration" --no-network "$@" fix --reinstall --upgrade $repair_packages + else + apk --root "$run_root" --keys-dir "$target/etc/apk/keys" \ + --repositories-file "$configuration" --no-network "$@" upgrade $packages + fi + printf '%s\n' "$?" > "$temporary/apk-status" + ) 2>&1 | /bin/busybox tee "$run_log" + status=$(/bin/busybox cat "$temporary/apk-status") + [ "$status" -ne 0 ] || return 0 + broken_packages "$run_root" 0 > "$temporary/broken-after" + count=$(/bin/busybox awk 'END { print NR }' "$temporary/broken-before") + if [ "$count" -gt 0 ] && [ "$count" -lt 125 ] && [ "$status" -eq "$count" ] && + /bin/busybox cmp -s "$temporary/broken-before" "$temporary/broken-after" && + ! /bin/busybox grep -q '^ERROR:' "$run_log" && + /bin/busybox tail -n 1 "$run_log" | + /bin/busybox grep -Eq "^$status errors?; .+ in [0-9]+ packages$"; then + echo 'WARNING: pre-existing external package errors remain:' >&2 + /bin/busybox cut -f 1 "$temporary/broken-before" >&2 + return 0 + fi + return "$status" +} + +echo 'Checking signed repository and package archives' +packages=$(apk --root "$plan" --keys-dir "$target/etc/apk/keys" \ + --repositories-file "$configuration" --no-network search --quiet 'reliefos-*') +[ -n "$packages" ] || die 'the signed repository has no ReliefOS packages' +for name in $packages; do + case "$name" in reliefos-*) ;; *) die 'invalid system package name' ;; esac + case "$name" in *[!a-zA-Z0-9+_.-]*) die 'invalid system package name' ;; esac +done +repair_packages=$(broken_packages "$plan" 0 | /bin/busybox awk -F '\t' \ + '$1 ~ /^reliefos-/ && $1 !~ /^reliefos-(sl|ncurses|sqlite)$/ { print $1 }') +for name in $repair_packages; do + case "$name" in *[!a-zA-Z0-9+_.-]*) die 'invalid repair package name' ;; esac +done +# Replacement commands are world selections, no longer dependencies of local +# wrappers. Include them in the verified archive set before the solver runs. +upstream_packages=$(/bin/busybox sort -u "$temporary/upstream-packages") +apk --root "$plan" --keys-dir "$target/etc/apk/keys" \ + --repositories-file "$configuration" --no-network fetch --recursive \ + --output "$temporary/repository" $packages $repair_packages $upstream_packages +apk --root "$plan" --keys-dir "$target/etc/apk/keys" --no-network \ + verify "$temporary/repository/"*.apk +/bin/busybox cp "$repository/packages.adb" "$temporary/repository/packages.adb" +write_repositories "$temporary/repository" "$temporary/tagged-verified" > "$temporary/verified.repositories" +configuration=$temporary/verified.repositories +echo 'Calculating the offline package transaction' +if [ -n "$repair_packages" ]; then + run_transaction "$plan" "$temporary/repair-plan.log" fix --simulate +fi +run_transaction "$plan" "$temporary/plan.log" upgrade --simulate +printf 'RELIEFOS_UPDATE_INDEX_SHA256=%s\n' "$index_sha" +[ "$mode" != check ] || exit 0 + +echo 'Preserving installed kernel modules and local configuration' +case "$kernel_bundle" in /*) ;; *) die 'kernel retention helper must have an absolute path' ;; esac +[ -x "$kernel_bundle" ] || die 'native kernel retention helper is missing' +/bin/sh /usr/lib/reliefos/reliefos-migrate "$target" +"$kernel_bundle" --retain-installed "$target" +retention_started=1 +[ ! -L "$target/run" ] && { [ ! -e "$target/run" ] || [ -d "$target/run" ]; } || die 'unsafe target run directory' +/bin/busybox mkdir -p "$target/run" +[ ! -e "$activation_marker" ] && [ ! -L "$activation_marker" ] || die 'kernel activation is already pending' +(set -C; printf '%s\n' 'ReliefOS installer package transaction' > "$activation_marker") +activation_deferred=1 + +# A concurrent administrator edit must be inspected again before committing. +/bin/busybox cmp -s "$temporary/world-before" "$target/etc/apk/world" || die 'installed package selection changed during preflight' +/bin/busybox cmp -s "$temporary/installed-before" "$target/lib/apk/db/installed" || die 'installed package database changed during preflight' +/bin/busybox cp -p "$temporary/world-before" "$target/etc/apk/world.reliefos-update.$$" +/bin/busybox cat "$plan/etc/apk/world" > "$target/etc/apk/world.reliefos-update.$$" +/bin/busybox mv "$target/etc/apk/world.reliefos-update.$$" "$target/etc/apk/world" +world_changed=1 +echo 'Applying the signed package transaction' +if [ -n "$repair_packages" ]; then + run_transaction "$target" "$temporary/repair.log" fix +fi +run_transaction "$target" "$temporary/transaction.log" upgrade +/bin/busybox chroot "$target" /bin/sh /usr/lib/reliefos/migrate-shell + +echo 'Publishing the verified offline repository' +for relative in usr usr/share usr/share/reliefos usr/share/reliefos/apk usr/share/reliefos/apk/repository; do + path=$target/$relative + [ ! -L "$path" ] && { [ ! -e "$path" ] || [ -d "$path" ]; } || die "unsafe repository directory: $path" + [ -d "$path" ] || /bin/busybox mkdir -m 0755 "$path" +done +destination=$target/usr/share/reliefos/apk/repository +for package in "$temporary/repository"/*.apk; do + name=${package##*/} + /bin/busybox cp "$package" "$destination/$name.new" + /bin/busybox chmod 0644 "$destination/$name.new" + /bin/busybox mv "$destination/$name.new" "$destination/$name" +done +/bin/busybox cp "$temporary/repository/packages.adb" "$destination/packages.adb.new" +/bin/busybox chmod 0644 "$destination/packages.adb.new" +/bin/busybox mv "$destination/packages.adb.new" "$destination/packages.adb" +# Old offline archives must not keep the removed wrappers installable. Limit +# cleanup to these retired package names; retain unrelated administrator files. +for component in sl ncurses sqlite; do + for prefix in reliefos leonos; do + for archive in "$destination/$prefix-$component-"[0-9]*.apk; do + [ -e "$archive" ] || [ -L "$archive" ] || continue + [ -f "$archive" ] && [ ! -L "$archive" ] || die "unsafe retired package archive: $archive" + /bin/busybox rm "$archive" + done + done +done +/bin/busybox sync