From e640cce910052c0eae8d38e81bd6a12b4169519a Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sat, 7 Nov 2026 21:55:12 +0800 Subject: [PATCH 1/9] fix(kernel): update ReliefNT for ppoll signal masks Advance ReliefNT to df8af1b97a50aa1752389ba230b7a043cbf7515e, which implements temporary ppoll signal masks and restores them across completion, cancellation, and signal delivery. This fixes the unsupported ppoll path used by SSH clients. --- kernel/reliefnt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/reliefnt b/kernel/reliefnt index f1129c75..df8af1b9 160000 --- a/kernel/reliefnt +++ b/kernel/reliefnt @@ -1 +1 @@ -Subproject commit f1129c7582c8207114ef99e5ee17157ead980132 +Subproject commit df8af1b97a50aa1752389ba230b7a043cbf7515e From cb1934261c0ff2845d572e5dc90fe9a1f951d573 Mon Sep 17 00:00:00 2001 From: VasilyZa <450270247@qq.com> Date: Sun, 8 Nov 2026 12:25:49 +0800 Subject: [PATCH 2/9] feat(installer): rewrite offline system update workflow Share an isolated update engine across the FLTK and TTY frontends. Check target identities, signed packages, dependencies and free space before applying updates. Preserve local state and record durable transaction checkpoints for retry and boot publication. Update packaging, translations, documentation and regression coverage. --- Documentation/admin-guide/installation.rst | 24 +- configs/nls/po/leonos.pot | 192 +++++++- configs/nls/po/zh_CN.po | 195 ++++++-- scripts/reliefos/Makefile.apk | 2 +- tests/build/brand-allowlist.tsv | 3 +- tests/build/test-installer-update-model.sh | 14 + tests/host/test_motif_apps.c | 15 +- tests/integration/test-apk-upgrade.sh | 2 +- tools/apk_distribution.py | 2 +- tools/build/apk-stage.sh | 2 +- tools/test_installer_accounts_qemu.py | 47 +- tools/test_installer_mode_switch_qemu.py | 30 +- tools/test_installer_payload.py | 14 +- tools/test_installer_update_packages.py | 223 +++++++++ tools/test_installer_update_qemu.py | 67 +-- tools/tests/installer_mount_transition_test.c | 30 +- tools/tests/installer_payload_test.c | 18 +- tools/tests/installer_tty_input_test.c | 34 +- tools/tests/installer_update_model_test.c | 98 ++++ userland/apps/installer/Kbuild | 8 +- userland/apps/installer/install_ops.c | 437 ++---------------- userland/apps/installer/install_ops.h | 4 +- userland/apps/installer/installer_tty.c | 38 +- userland/apps/installer/installer_tty.h | 2 - userland/apps/installer/installer_update.h | 61 +++ .../apps/installer/installer_update_engine.c | 437 ++++++++++++++++++ .../installer/installer_update_internal.h | 18 + .../apps/installer/installer_update_journal.c | 120 +++++ .../apps/installer/installer_update_model.c | 269 +++++++++++ .../apps/installer/installer_update_target.c | 165 +++++++ userland/apps/installer/main.cpp | 286 +++++++++--- userland/apps/installer/model.c | 11 +- userland/apps/installer/model.h | 7 +- userland/storage/leonos-apk-update | 196 -------- userland/storage/reliefos-apk-update | 265 +++++++++++ 35 files changed, 2525 insertions(+), 811 deletions(-) create mode 100755 tests/build/test-installer-update-model.sh create mode 100644 tools/test_installer_update_packages.py create mode 100644 tools/tests/installer_update_model_test.c create mode 100644 userland/apps/installer/installer_update.h create mode 100644 userland/apps/installer/installer_update_engine.c create mode 100644 userland/apps/installer/installer_update_internal.h create mode 100644 userland/apps/installer/installer_update_journal.c create mode 100644 userland/apps/installer/installer_update_model.c create mode 100644 userland/apps/installer/installer_update_target.c delete mode 100644 userland/storage/leonos-apk-update create mode 100755 userland/storage/reliefos-apk-update diff --git a/Documentation/admin-guide/installation.rst b/Documentation/admin-guide/installation.rst index b5196ca7..7de7ba8b 100644 --- a/Documentation/admin-guide/installation.rst +++ b/Documentation/admin-guide/installation.rst @@ -66,13 +66,31 @@ AHCI SATA 设备上的专用文件或未挂载分区,再使用 ``mkswap`` 初 更新不自动重分区。旧两分区 UEFI 磁盘保持原有布局,缺少 BIOS Boot 空间时增加 BIOS 引导的请求应在写入前失败。先备份用户文件和引导分区, -检查已安装 APK 数据库、信任密钥及所选更新介质。 -磁盘探测完成后进入引导方式页,默认选中已检测到的引导器,再进入更新确认页; -返回引导方式页不会覆盖用户刚修改的选择。未选中的现有引导器会保留。 +检查已安装 APK 数据库、信任密钥及所选更新介质。正常关闭已安装系统后, +再从安装介质启动。 +在安装器中选择更新和目标磁盘后,独立的后台进程只读检查已安装系统、桌面 +类型、软件包数据库、安装介质和空间。确认页列出当前版本、介质版本、根分区、 +ESP 和空间需求,并默认选中已检测到的 BIOS/UEFI 引导器。核对后点击更新。 +TTY 入口使用相同的后端,并在显示摘要和引导选项后通过 ``y/N`` 确认。 + +检查过程使用私有 APK 数据库和已安装系统的信任密钥,验证签名索引、完整 +软件包归档以及离线依赖。确认后的执行会再次核对磁盘、分区 UUID、索引和 +已安装包数据库及 world 选择;目标已经变化时需要重新检查。已被其他路径挂载的分区、 +无法唯一识别的根分区或 ESP,以及早于已安装系统的介质会在写入前被拒绝。 根文件系统使用 APK 更新,保留外部包、world 选择和受保护配置;新默认配置 可能以 ``.apk-new`` 出现。根事务成功后才发布引导文件。 更新保留现有 fstab 和 swap 设置,不会自动添加或更改交换文件。 +更新详情显示软件包和引导操作的实际输出,完整日志位于安装器运行环境的 +``/var/log/installer-update.log``。失败页面提供重新检查入口。 + +目标系统的 ``/var/lib/reliefos/installer-update/state`` 持久记录软件包、根同步、 +引导提交和完成阶段。同一介质的引导提交失败后,重新检查会核对已完成的根 +事务,再继续引导提交;软件包阶段失败则重新预检并执行包事务。旧内核模块 +保留与 A/B 内核发布继续使用共同的原生引导接口。更新期间由独立进程持有 +安装器更新锁,释放目标挂载后才报告完成。APK 已记录为损坏的系统包会先通过 +已验证的归档执行定向修复,再执行升级;已有的外部包错误保留并在日志中说明。 + 单文件原子替换不等于全部包、ESP 和 BIOS 写入的整体断电原子事务。 旧回滚内核可能不认识 PARTUUID,不能假定所有旧菜单都能自动恢复。 diff --git a/configs/nls/po/leonos.pot b/configs/nls/po/leonos.pot index 60648060..a4a03534 100644 --- a/configs/nls/po/leonos.pot +++ b/configs/nls/po/leonos.pot @@ -778,10 +778,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 +2346,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 +5990,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 "" @@ -12574,10 +12562,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 +15014,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 +15038,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 +15437,171 @@ 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 "" diff --git a/configs/nls/po/zh_CN.po b/configs/nls/po/zh_CN.po index c003b024..3f585489 100644 --- a/configs/nls/po/zh_CN.po +++ b/configs/nls/po/zh_CN.po @@ -807,10 +807,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 +2384,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 +6071,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 "已安装大小" @@ -12734,10 +12722,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 +15538,6 @@ msgstr "工作进程错误" #~ msgid "System information (logo unavailable)" #~ msgstr "系统版本信息(Logo 不可用)" -#~ msgid "System version" -#~ msgstr "系统版本" - #~ msgid "Tar extract failed" #~ msgstr "Tar解压失败" @@ -16822,10 +16803,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 +16827,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 +17263,171 @@ 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] " diff --git a/scripts/reliefos/Makefile.apk b/scripts/reliefos/Makefile.apk index 3c2c2a5d..918a563c 100644 --- a/scripts/reliefos/Makefile.apk +++ b/scripts/reliefos/Makefile.apk @@ -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/tests/build/brand-allowlist.tsv b/tests/build/brand-allowlist.tsv index 219735f1..2876de38 100644 --- a/tests/build/brand-allowlist.tsv +++ b/tests/build/brand-allowlist.tsv @@ -305,7 +305,8 @@ main compatibility userland/runtime/src/text_encoding.c 所有当前命中均为 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-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/host/test_motif_apps.c b/tests/host/test_motif_apps.c index 0f318396..47e92c4f 100644 --- a/tests/host/test_motif_apps.c +++ b/tests/host/test_motif_apps.c @@ -597,7 +597,7 @@ int main(void) 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) == @@ -648,7 +648,9 @@ 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); @@ -661,13 +663,14 @@ int main(void) 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); diff --git a/tests/integration/test-apk-upgrade.sh b/tests/integration/test-apk-upgrade.sh index cb782b4a..6d6a8f19 100644 --- a/tests/integration/test-apk-upgrade.sh +++ b/tests/integration/test-apk-upgrade.sh @@ -77,7 +77,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/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/build/apk-stage.sh b/tools/build/apk-stage.sh index a359fafc..16576e90 100644 --- a/tools/build/apk-stage.sh +++ b/tools/build/apk-stage.sh @@ -225,7 +225,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/test_installer_accounts_qemu.py b/tools/test_installer_accounts_qemu.py index 394bf759..3a5d83ac 100644 --- a/tools/test_installer_accounts_qemu.py +++ b/tools/test_installer_accounts_qemu.py @@ -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_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_update_packages.py b/tools/test_installer_update_packages.py new file mode 100644 index 00000000..201a3e86 --- /dev/null +++ b/tools/test_installer_update_packages.py @@ -0,0 +1,223 @@ +#!/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-fixture", "1.0-r0", "oldrepo") + 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): + files = cls.work / (name + "-" + version) + (files / "usr/share" / name).mkdir(parents=True) + (files / "usr/share" / name / "version").write_text(version + "\n") + cls.apk("mkpkg", "--files", files, "--output", + cls.base / repository / (name + "-" + version + ".apk"), + "--info", "name:" + name, "--info", "version:" + version, + "--info", "arch:x86_64", "--sign-key", cls.key) + + @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 test_tagged_external_packages_use_installed_cached_indexes_offline(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 @addon " + url + "\n") + try: + self.apk("--root", target, "--repositories-file", configuration, + "--cache-dir", cache, "update") + finally: + server.shutdown() + server.server_close() + thread.join() + (target / "etc/apk/repositories").write_text(configuration.read_text()) + world = target / "etc/apk/world" + world.write_text(world.read_text().replace("external-fixture\n", "external-fixture@addon\n")) + result = self.check() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + +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/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_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/userland/apps/installer/Kbuild b/userland/apps/installer/Kbuild index e0937085..3e181a5f 100644 --- a/userland/apps/installer/Kbuild +++ b/userland/apps/installer/Kbuild @@ -1,8 +1,10 @@ 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_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 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 := ifneq ($(RELIEFOS_CAPTURE_APP_KBUILD),1) include $(srctree)/scripts/reliefos/Makefile.app-kbuild +$(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/install_ops.c b/userland/apps/installer/install_ops.c index 82fa2ff3..9b902a4a 100644 --- a/userland/apps/installer/install_ops.c +++ b/userland/apps/installer/install_ops.c @@ -82,7 +82,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 +90,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, }; @@ -550,24 +550,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 +592,32 @@ 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 < 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); - *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); + 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; } -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,8 +639,8 @@ 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. */ + /* Release mounts from a previous installation attempt before rewriting + * the GPT or formatting either selected filesystem. */ const char *targets[] = {INSTALL_ESP_MOUNT, 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) { @@ -674,23 +649,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); @@ -718,8 +688,7 @@ static int installer_mount_targets(const char *disk_path, int fresh) 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 +844,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 +982,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. */ @@ -1225,7 +1180,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 +1228,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 +1248,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 +1313,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 +1367,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 +1403,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 +1451,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..5d4eb0fc 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 @@ -116,9 +116,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_tty.c b/userland/apps/installer/installer_tty.c index 536979f5..14c32c4b 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" @@ -312,6 +313,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 +333,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 +362,35 @@ 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; + int error = installer_update_check(disk, &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); + *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(); diff --git a/userland/apps/installer/installer_tty.h b/userland/apps/installer/installer_tty.h index c551acde..df62157e 100644 --- a/userland/apps/installer/installer_tty.h +++ b/userland/apps/installer/installer_tty.h @@ -45,9 +45,7 @@ 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); }; #ifdef __cplusplus diff --git a/userland/apps/installer/installer_update.h b/userland/apps/installer/installer_update.h new file mode 100644 index 00000000..6a986298 --- /dev/null +++ b/userland/apps/installer/installer_update.h @@ -0,0 +1,61 @@ +#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 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_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..9c33b7bd --- /dev/null +++ b/userland/apps/installer/installer_update_engine.c @@ -0,0 +1,437 @@ +#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) +{ + 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 : NULL); + 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 = 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 = 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); +} + +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); +} + +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(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 == 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..7d3b1413 --- /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]; + const char *filesystem; + int lock_fd, root_mounted, boot_mounted; +}; + +/* 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); +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..9f5f6025 --- /dev/null +++ b/userland/apps/installer/installer_update_model.c @@ -0,0 +1,269 @@ +#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(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..421632af --- /dev/null +++ b/userland/apps/installer/installer_update_target.c @@ -0,0 +1,165 @@ +#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->root_mounted && !target->boot_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; +} + +int installer_update_target_open(struct installer_update_target *target, + const char *disk, const char *expected_root) +{ + 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; + for (uint32_t i = 0; i < count; ++i) { + result = mounted_elsewhere(parts[i].path); + if (result) return result; + if (parts[i].gpt_type == RELIEFOS_BLOCK_GPT_ESP && parts[i].filesystem == RELIEFOS_BLOCK_FILESYSTEM_FAT32) { + if (esp >= 0) return -ENOTUNIQ; + esp = (int)i; + } + } + if (esp < 0) return -ENOENT; + strcpy(target->workspace, "/run/reliefos/update-XXXXXX"); + if (!mkdtemp(target->workspace)) return -errno; + snprintf(target->root, sizeof(target->root), "%s/root", target->workspace); + snprintf(target->boot, sizeof(target->boot), "%s/root/boot", target->workspace); + 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; + 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); + snprintf(target->info.esp_device, sizeof(target->info.esp_device), "%s", parts[esp].path); + result = reliefos_block_partition_uuid(disk, parts[esp].index, target->info.esp_uuid); + if (result) return result; + if (mount(target->info.root_device, target->root, target->filesystem, MS_RDONLY | MS_NOSUID | MS_NODEV, NULL) < 0) + return -errno; + target->root_mounted = 1; + if (mount(target->info.esp_device, target->boot, "vfat", MS_RDONLY | MS_NOSUID | MS_NODEV, NULL) < 0) + return -errno; + target->boot_mounted = 1; + 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 (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; + if (mount(target->info.esp_device, target->boot, "vfat", MS_NOSUID | MS_NODEV, NULL) < 0) return -errno; + target->boot_mounted = 1; + return 0; +} diff --git a/userland/apps/installer/main.cpp b/userland/apps/installer/main.cpp index 9b50cf8b..226eb81f 100644 --- a/userland/apps/installer/main.cpp +++ b/userland/apps/installer/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include extern "C" { #include @@ -29,6 +30,8 @@ extern "C" { #include #include #include +#include +#include #include #include #include @@ -39,6 +42,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 { @@ -92,6 +97,13 @@ 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" @@ -236,9 +248,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 +270,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 +343,121 @@ 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; + const char *check[] = {executable, "--update-worker", "check", disks[selected_disk].path, 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 +516,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 +538,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, @@ -992,8 +1114,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 +1149,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 +1204,11 @@ 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 || installer_partition_table_replaced) ? 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."), 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 +1216,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 +1244,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 +1278,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) @@ -1138,11 +1302,14 @@ static void build_finish_page(void) } 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(236, 264); } static void clear_page_widgets(void) @@ -1160,6 +1327,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; } @@ -1289,6 +1457,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 +1501,7 @@ 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; 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..1e9b7e79 100644 --- a/userland/apps/installer/model.c +++ b/userland/apps/installer/model.c @@ -80,7 +80,8 @@ enum installer_page installer_model_next(enum installer_page page, 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; @@ -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..ee676b89 100644 --- a/userland/apps/installer/model.h +++ b/userland/apps/installer/model.h @@ -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/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/reliefos-apk-update b/userland/storage/reliefos-apk-update new file mode 100755 index 00000000..253243ed --- /dev/null +++ b/userland/storage/reliefos-apk-update @@ -0,0 +1,265 @@ +#!/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" "$temporary/seen" +/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" +/bin/busybox sed 's/^leonos-/reliefos-/' "$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 + +# Reuse cached signed indexes for world-selected repository tags, without +# copying package archives into memory or writing the installed cache. +copy_index_cache() { + cache_source=$1 + if [ -L "$cache_source" ]; then + link=$(/bin/busybox readlink "$cache_source") + case "$link" in + /*) cache_source=$target$link ;; + *) cache_source=${cache_source%/*}/$link ;; + esac + fi + [ -d "$cache_source" ] || return 0 + cache_source=$(CDPATH= cd -- "$cache_source" && pwd -P) + case "$cache_source" in "$target"/*) ;; *) die 'package cache escapes the installed root' ;; esac + for index in "$cache_source"/*; do + case "${index##*/}" in *.adb|APKINDEX.*.tar.gz) ;; *) continue ;; esac + [ -f "$index" ] && [ ! -L "$index" ] || continue + /bin/busybox cp "$index" "$plan/var/cache/apk/" + done +} +copy_index_cache "$target/var/cache/apk" +copy_index_cache "$target/etc/apk/cache" + +# Only repositories explicitly selected by world tags participate. Absolute +# local URLs are interpreted in the installed root's namespace. +required_tags=' ' +while IFS= read -r dependency || [ -n "$dependency" ]; do + case "$dependency" in + *@*) tag=${dependency#*@}; tag=${tag%%[\<\>\=\~]*}; required_tags="$required_tags$tag " ;; + esac +done < "$plan/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 + 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 + +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-/ { print $1 }') +for name in $repair_packages; do + case "$name" in *[!a-zA-Z0-9+_.-]*) die 'invalid repair package name' ;; esac +done +apk --root "$plan" --keys-dir "$target/etc/apk/keys" \ + --repositories-file "$configuration" --no-network fetch --recursive \ + --output "$temporary/repository" $packages $repair_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" +/bin/busybox awk -v repo_index="$temporary/repository/packages.adb" \ + 'NR == 1 { print "ndx " repo_index; next } { print }' "$configuration" > "$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" +/bin/busybox sync From a854de52bb9b09800031b43295ab71ddd3b268fc Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sun, 8 Nov 2026 14:30:52 +0800 Subject: [PATCH 3/9] fix(installer): preserve tagged packages in offline updates --- Documentation/admin-guide/installation.rst | 5 + tools/test_installer_update_packages.py | 201 +++++++++++++++++++-- userland/storage/reliefos-apk-update | 97 ++++------ 3 files changed, 229 insertions(+), 74 deletions(-) diff --git a/Documentation/admin-guide/installation.rst b/Documentation/admin-guide/installation.rst index 7de7ba8b..7f923f76 100644 --- a/Documentation/admin-guide/installation.rst +++ b/Documentation/admin-guide/installation.rst @@ -80,6 +80,11 @@ TTY 入口使用相同的后端,并在显示摘要和引导选项后通过 ``y 根文件系统使用 APK 更新,保留外部包、world 选择和受保护配置;新默认配置 可能以 ``.apk-new`` 出现。根事务成功后才发布引导文件。 +离线更新只从安装介质的签名仓库取得新软件包。``world`` 中已有的 +``@testing``、``@alpine`` 等标签在本次事务的私有配置中注册到该仓库, +使已安装的外部软件包可以继续满足依赖,不要求原仓库在线或保留索引缓存。 +原有仓库配置和标签选择保持不变;介质和已安装软件包无法满足的版本约束 +仍会在预检查时拒绝,不会强制忽略依赖。 更新保留现有 fstab 和 swap 设置,不会自动添加或更改交换文件。 更新详情显示软件包和引导操作的实际输出,完整日志位于安装器运行环境的 ``/var/log/installer-update.log``。失败页面提供重新检查入口。 diff --git a/tools/test_installer_update_packages.py b/tools/test_installer_update_packages.py index 201a3e86..767e49e0 100644 --- a/tools/test_installer_update_packages.py +++ b/tools/test_installer_update_packages.py @@ -55,7 +55,8 @@ def setUpClass(cls): 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-fixture", "1.0-r0", "oldrepo") + 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"): @@ -74,14 +75,17 @@ def apk(cls, *arguments): check=True, capture_output=True, text=True) @classmethod - def package(cls, name, version, repository): + def package(cls, name, version, repository, dependencies=()): files = cls.work / (name + "-" + version) - (files / "usr/share" / name).mkdir(parents=True) + (files / "usr/share" / name).mkdir(parents=True, exist_ok=True) (files / "usr/share" / name / "version").write_text(version + "\n") - cls.apk("mkpkg", "--files", files, "--output", - cls.base / repository / (name + "-" + version + ".apk"), - "--info", "name:" + name, "--info", "version:" + version, - "--info", "arch:x86_64", "--sign-key", cls.key) + 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): @@ -193,7 +197,163 @@ def test_retry_repairs_an_incomplete_system_package(self): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual((target / "usr/share/reliefos-fixture/version").read_text(), "2.0-r0\n") - def test_tagged_external_packages_use_installed_cached_indexes_offline(self): + 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) @@ -204,7 +364,7 @@ def test_tagged_external_packages_use_installed_cached_indexes_offline(self): thread.start() url = f"http://127.0.0.1:{server.server_port}/packages.adb" configuration = Path(self.case.name) / "repositories" - configuration.write_text("ndx @addon " + url + "\n") + configuration.write_text("ndx @testing " + url + "\n") try: self.apk("--root", target, "--repositories-file", configuration, "--cache-dir", cache, "update") @@ -212,11 +372,28 @@ def test_tagged_external_packages_use_installed_cached_indexes_offline(self): server.shutdown() server.server_close() thread.join() - (target / "etc/apk/repositories").write_text(configuration.read_text()) world = target / "etc/apk/world" - world.write_text(world.read_text().replace("external-fixture\n", "external-fixture@addon\n")) - result = self.check() + 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__": diff --git a/userland/storage/reliefos-apk-update b/userland/storage/reliefos-apk-update index 253243ed..c2990ce4 100755 --- a/userland/storage/reliefos-apk-update +++ b/userland/storage/reliefos-apk-update @@ -63,7 +63,7 @@ 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" "$temporary/seen" +/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" @@ -73,70 +73,44 @@ if [ -f "$target/etc/apk/arch" ]; then /bin/busybox cp "$target/etc/apk/arch" "$plan/etc/apk/arch" fi -# Reuse cached signed indexes for world-selected repository tags, without -# copying package archives into memory or writing the installed cache. -copy_index_cache() { - cache_source=$1 - if [ -L "$cache_source" ]; then - link=$(/bin/busybox readlink "$cache_source") - case "$link" in - /*) cache_source=$target$link ;; - *) cache_source=${cache_source%/*}/$link ;; - esac - fi - [ -d "$cache_source" ] || return 0 - cache_source=$(CDPATH= cd -- "$cache_source" && pwd -P) - case "$cache_source" in "$target"/*) ;; *) die 'package cache escapes the installed root' ;; esac - for index in "$cache_source"/*; do - case "${index##*/}" in *.adb|APKINDEX.*.tar.gz) ;; *) continue ;; esac - [ -f "$index" ] && [ ! -L "$index" ] || continue - /bin/busybox cp "$index" "$plan/var/cache/apk/" - done -} -copy_index_cache "$target/var/cache/apk" -copy_index_cache "$target/etc/apk/cache" - -# Only repositories explicitly selected by world tags participate. Absolute -# local URLs are interpreted in the installed root's namespace. -required_tags=' ' +# 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%%[\<\>\=\~]*}; required_tags="$required_tags$tag " ;; + *@*) + 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" -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 - 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" +/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" } -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 +configuration=$temporary/repositories +write_repositories "$repository" "$temporary/tagged-media" > "$configuration" broken_packages() { /bin/busybox awk -v exclude_system="$2" ' @@ -209,8 +183,7 @@ apk --root "$plan" --keys-dir "$target/etc/apk/keys" \ 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" -/bin/busybox awk -v repo_index="$temporary/repository/packages.adb" \ - 'NR == 1 { print "ndx " repo_index; next } { print }' "$configuration" > "$temporary/verified.repositories" +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 From 93ab4050aed2a8d22e2c3e3514628c107bb979a5 Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sun, 8 Nov 2026 15:32:42 +0800 Subject: [PATCH 4/9] fix(runtime): handle unavailable PI mutexes and update ReliefNT --- Documentation/kbuild/third-party.rst | 5 + configs/dependencies.lock.json | 4 + kernel/reliefnt | 2 +- ...report-unsupported-pi-mutex-protocol.patch | 14 +++ tools/test_musl_mutex_protocol.py | 118 ++++++++++++++++++ 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 patches/musl/0002-report-unsupported-pi-mutex-protocol.patch create mode 100644 tools/test_musl_mutex_protocol.py diff --git a/Documentation/kbuild/third-party.rst b/Documentation/kbuild/third-party.rst index aaa3aa4b..b3dcfb57 100644 --- a/Documentation/kbuild/third-party.rst +++ b/Documentation/kbuild/third-party.rst @@ -22,6 +22,11 @@ musl、BusyBox、SQLite、FLTK、Xorg/Mesa、GRUB、认证库等保留各自原 移植边界。目标程序与开发 SDK 的 staging 不可混用。上游 README、许可证、 COPYING、归属和字体许可是来源资料,保留原文,不属于待清理的过程报告。 +musl 的互斥锁属性探测补丁将内核缺少 PI futex 的 ``ENOSYS`` 转换为 +POSIX 的 ``ENOTSUP``,以便 PulseAudio 等调用者回退到普通互斥锁。 +该补丁不伪装成已实现优先级继承,也不改变其他 futex 错误。补丁及摘要由 +依赖锁管理;已有镜像需要重新构建 musl 和系统包才会取得此修正。 + 移植与打包 ========== diff --git a/configs/dependencies.lock.json b/configs/dependencies.lock.json index a0dad10b..e7ada147 100644 --- a/configs/dependencies.lock.json +++ b/configs/dependencies.lock.json @@ -2616,6 +2616,10 @@ { "path": "patches/musl/0001-enforce-password-file-lock.patch", "sha256": "e336ed20b2d338dbbf88588dff043e8ce349d0d0044277d31be229d600bb93ad" + }, + { + "path": "patches/musl/0002-report-unsupported-pi-mutex-protocol.patch", + "sha256": "6454c5786fe36c513e5dbc69eb221cfdde840f90e49e00139da4c3e55cece039" } ] }, diff --git a/kernel/reliefnt b/kernel/reliefnt index df8af1b9..bd0d364b 160000 --- a/kernel/reliefnt +++ b/kernel/reliefnt @@ -1 +1 @@ -Subproject commit df8af1b97a50aa1752389ba230b7a043cbf7515e +Subproject commit bd0d364bb7516ba5f7c02e3a040785e6078c67a3 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/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() From 01343077b2e31f0990fd40d1c64639ad5470af6c Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sun, 8 Nov 2026 22:36:56 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E4=B8=80=E5=A0=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Documentation/admin-guide/installation.rst | 61 +- Documentation/applications/reliefplayer.rst | 30 +- Documentation/kbuild/building.rst | 6 + Documentation/kbuild/third-party.rst | 7 + Documentation/userspace-api/abi.rst | 12 + Kconfig | 8 + boot/grub/bios-modules.list | 2 +- boot/grub/embedded.cfg | 1 + boot/grub/grub.cfg | 121 ++-- boot/grub/installer.cfg | 23 +- boot/grub/installer_embedded.cfg | 9 +- boot/grub/kernel-slots.cfg | 16 +- boot/grub/live.cfg | 16 +- boot/grub/other-systems.cfg | 92 +++ configs/apk-ownership.json | 8 +- configs/components.toml | 2 +- configs/dependencies.lock.json | 12 + configs/nls/messages.c | 12 +- configs/nls/po/leonos.pot | 120 ++++ configs/nls/po/zh_CN.po | 120 ++++ scripts/reliefos/Makefile.images | 29 +- scripts/reliefos/Makefile.upstream | 4 +- scripts/reliefos/Makefile.userland | 5 +- system/initramfs/functions | 30 + system/initramfs/init | 22 +- system/rootfs/etc/fstab | 3 +- tests/build/test-boot-mount-policy.sh | 38 ++ tests/build/test-bootloader-uefi.sh | 15 + tests/build/test-components-metadata.sh | 2 +- tests/build/test-desktop-backend.sh | 28 +- tests/build/test-image-adapters.sh | 15 +- tests/build/test-reliefplayer-mp4.sh | 38 ++ tests/build/test-reliefplayer-ownership.sh | 9 + tests/build/test-reliefplayer-session.sh | 6 +- tests/build/test-reliefplayer.sh | 3 +- tests/host/test_bootloader_install.c | 16 + tests/host/test_bootloader_uefi.c | 81 +++ tests/host/test_fileman_boundary.c | 1 + tests/host/test_installer_boot_layout.c | 55 ++ tests/host/test_reliefplayer_mp4.cpp | 139 +++++ tools/build/boot-stage.sh | 4 +- tools/build/disk.sh | 21 +- tools/build/ffmpeg.sh | 49 ++ tools/build/image-identity.sh | 13 + tools/build/iso.sh | 13 +- tools/build/rootfs-stage.sh | 15 +- tools/build/standalone-root.sh | 8 +- tools/test_bootloader_install.py | 4 +- tools/test_dual_boot_qemu.py | 11 +- tools/test_installer_accounts_qemu.py | 4 +- tools/test_multisystem_boot_qemu.py | 337 +++++++++++ tools/test_reliefplayer_qemu.py | 4 +- tools/tests/installer_swap_config_test.c | 2 + userland/apps/bootinstall/main.c | 13 +- userland/apps/fileman/boundary.c | 3 +- userland/apps/installer/install_ops.c | 55 +- userland/apps/installer/install_ops.h | 2 + userland/apps/installer/installer_setup.c | 2 +- userland/apps/installer/installer_setup.h | 1 + .../apps/installer/installer_swap_config.c | 5 +- userland/apps/installer/installer_tty.c | 36 +- userland/apps/installer/installer_tty.h | 1 + userland/apps/installer/installer_update.h | 4 + .../apps/installer/installer_update_engine.c | 29 +- .../installer/installer_update_internal.h | 6 +- .../apps/installer/installer_update_model.c | 1 + .../apps/installer/installer_update_target.c | 137 ++++- userland/apps/installer/main.cpp | 108 +++- userland/apps/reliefplayer/Kbuild | 4 +- userland/apps/reliefplayer/decoder.cpp | 32 +- userland/apps/reliefplayer/decoder.h | 4 +- userland/apps/reliefplayer/main.cpp | 4 +- userland/apps/reliefplayer/media.cpp | 550 ++++++++++++++++++ userland/apps/reliefplayer/media.h | 41 ++ .../apps/reliefplayer/reliefplayer.desktop | 12 +- userland/apps/reliefplayer/session.cpp | 46 +- userland/apps/reliefplayer/session.h | 9 + userland/apps/reliefplayer/window.cpp | 130 ++++- userland/apps/reliefplayer/window.h | 3 + userland/apps/reliefplayer/worker.cpp | 72 ++- userland/apps/reliefplayer/worker.h | 4 +- userland/busybox/block_storage.c | 59 -- userland/busybox/leonos_shim.c | 2 +- .../runtime/include/reliefos/bootloader.h | 19 +- userland/runtime/src/Kbuild | 2 + userland/runtime/src/bootloader.c | 106 +++- userland/runtime/src/bootloader_internal.h | 4 + userland/runtime/src/bootloader_process.c | 2 +- userland/runtime/src/bootloader_publish.c | 16 +- userland/runtime/src/bootloader_uefi.c | 191 ++++++ userland/runtime/src/kernel_bundle_publish.c | 21 + userland/storage/leonos-grub-installer | 18 +- 92 files changed, 3063 insertions(+), 393 deletions(-) create mode 100644 boot/grub/other-systems.cfg create mode 100644 tests/build/test-boot-mount-policy.sh create mode 100644 tests/build/test-bootloader-uefi.sh create mode 100644 tests/build/test-reliefplayer-mp4.sh create mode 100644 tests/host/test_bootloader_uefi.c create mode 100644 tests/host/test_installer_boot_layout.c create mode 100644 tests/host/test_reliefplayer_mp4.cpp create mode 100644 tools/build/ffmpeg.sh create mode 100644 tools/build/image-identity.sh create mode 100644 tools/test_multisystem_boot_qemu.py create mode 100644 userland/apps/reliefplayer/media.cpp create mode 100644 userland/apps/reliefplayer/media.h create mode 100644 userland/runtime/src/bootloader_uefi.c diff --git a/Documentation/admin-guide/installation.rst b/Documentation/admin-guide/installation.rst index 7f923f76..6d870684 100644 --- a/Documentation/admin-guide/installation.rst +++ b/Documentation/admin-guide/installation.rst @@ -61,6 +61,57 @@ 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 身份,不依赖预制镜像的实例标识。 + + 更新已有系统 ============ @@ -71,6 +122,14 @@ AHCI SATA 设备上的专用文件或未挂载分区,再使用 ``mkswap`` 初 在安装器中选择更新和目标磁盘后,独立的后台进程只读检查已安装系统、桌面 类型、软件包数据库、安装介质和空间。确认页列出当前版本、介质版本、根分区、 ESP 和空间需求,并默认选中已检测到的 BIOS/UEFI 引导器。核对后点击更新。 +同盘有多个安装时,可在目标磁盘页选择明确的根分区和 ESP;留空 ESP 时读取 +所选根的 fstab,不能简单取磁盘上第一个 ESP。显式选择与该根的 fstab 不一致时 +拒绝更新,避免把引导文件写到其他系统的分区。旧的 ``/boot`` 直接挂载 FAT32 +布局继续兼容,更新不强制改变现有挂载布局。 +旧布局中未能通过所有权验证的现有 EFI 可执行文件及相邻配置会保留, +不会因系统更新而覆盖。兼容命令 ``reliefos-grub-installer`` 现直接转交原生 +CLI,需要明确的 ``--disk``、``--boot-directory`` 和根分区身份; +不再接受无法绑定安装实例的旧式单个 ESP 路径复制操作。 TTY 入口使用相同的后端,并在显示摘要和引导选项后通过 ``y/N`` 确认。 检查过程使用私有 APK 数据库和已安装系统的信任密钥,验证签名索引、完整 @@ -97,6 +156,6 @@ TTY 入口使用相同的后端,并在显示摘要和引导选项后通过 ``y 已验证的归档执行定向修复,再执行升级;已有的外部包错误保留并在日志中说明。 单文件原子替换不等于全部包、ESP 和 BIOS 写入的整体断电原子事务。 -旧回滚内核可能不认识 PARTUUID,不能假定所有旧菜单都能自动恢复。 +回退条目只使用通过完整性检查的匹配内核与 initramfs,不把当前内核的别名当作回退。 运行验证和未覆盖范围见 :doc:`limitations`。 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/third-party.rst b/Documentation/kbuild/third-party.rst index b3dcfb57..664d8f29 100644 --- a/Documentation/kbuild/third-party.rst +++ b/Documentation/kbuild/third-party.rst @@ -27,6 +27,13 @@ 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 命令行程序或开发头文件。 + 移植与打包 ========== diff --git a/Documentation/userspace-api/abi.rst b/Documentation/userspace-api/abi.rst index c293b578..7252a3ac 100644 --- a/Documentation/userspace-api/abi.rst +++ b/Documentation/userspace-api/abi.rst @@ -62,3 +62,15 @@ 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`` 的系统继续使用旧接口,不改变其现有文件布局。 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/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..d60cdb4d 100644 --- a/configs/apk-ownership.json +++ b/configs/apk-ownership.json @@ -37,7 +37,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", 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 e7ada147..091264b5 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", 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 a4a03534..58ee5a32 100644 --- a/configs/nls/po/leonos.pot +++ b/configs/nls/po/leonos.pot @@ -15605,3 +15605,123 @@ 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 3f585489..013b77ec 100644 --- a/configs/nls/po/zh_CN.po +++ b/configs/nls/po/zh_CN.po @@ -17431,3 +17431,123 @@ 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/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.upstream b/scripts/reliefos/Makefile.upstream index b69eb75e..93f7f038 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 @@ -18,6 +18,8 @@ upstream_alsa-lib_outputs := usr/lib/libasound.so.2 usr/lib/libasound.a usr/incl 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 +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) ncurses,$(eval upstream_$(package)_primary := $(upstream_$(package)_outputs))) upstream_libbsd_deps := libmd upstream_shadow_deps := libmd libbsd diff --git a/scripts/reliefos/Makefile.userland b/scripts/reliefos/Makefile.userland index 3a2bf911..5fd640fa 100644 --- a/scripts/reliefos/Makefile.userland +++ b/scripts/reliefos/Makefile.userland @@ -47,7 +47,10 @@ 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 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/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..63d523c4 --- /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 +printf '#define MBEDTLS_SHA256_C\n' > "$work/qa_mbedtls.h" +${HOSTCC:-cc} -std=gnu11 -O1 -g -Wall -Wextra -Werror -D_GNU_SOURCE \ + -fsanitize=address,undefined -fno-sanitize-recover=all -ffunction-sections -fdata-sections \ + -DMBEDTLS_CONFIG_FILE='"qa_mbedtls.h"' -I"$work" -I"$src/userland/runtime/include" -I"$src/third_party/mbedtls/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" \ + "$src/third_party/mbedtls/library/sha256.c" "$src/third_party/mbedtls/library/platform_util.c" \ + -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-desktop-backend.sh b/tests/build/test-desktop-backend.sh index adf84459..0ad79d01 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 ncurses 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 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-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/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_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/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/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/rootfs-stage.sh b/tools/build/rootfs-stage.sh index 04f96c5b..6148df48 100644 --- a/tools/build/rootfs-stage.sh +++ b/tools/build/rootfs-stage.sh @@ -19,8 +19,21 @@ 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 ncurses'} 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 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" < ", "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), 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_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/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/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/install_ops.c b/userland/apps/installer/install_ops.c index 9b902a4a..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; @@ -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) { @@ -600,14 +611,16 @@ static int installer_fresh_partitions(const char *disk_path, 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 || + 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); - ret = reliefos_block_format(esp_path, RELIEFOS_BLOCK_FILESYSTEM_FAT32, "RELIEFOS"); - if (ret < 0) return ret; + 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, @@ -641,7 +654,7 @@ static int installer_mount_targets(const char *disk_path) } /* Release mounts from a previous installation attempt before rewriting * the GPT or formatting either selected filesystem. */ - const char *targets[] = {INSTALL_ESP_MOUNT, INSTALL_ROOT_MOUNT}; + 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; @@ -670,19 +683,45 @@ static int installer_mount_targets(const char *disk_path) 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; } @@ -1139,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. diff --git a/userland/apps/installer/install_ops.h b/userland/apps/installer/install_ops.h index 5d4eb0fc..31d05950 100644 --- a/userland/apps/installer/install_ops.h +++ b/userland/apps/installer/install_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, 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 14c32c4b..0c19bf8c 100644 --- a/userland/apps/installer/installer_tty.c +++ b/userland/apps/installer/installer_tty.c @@ -256,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 @@ -364,7 +373,22 @@ int installer_tty_main(const struct installer_tty_context *context) if (*context->install_mode == INSTALLER_TTY_MODE_UPDATE) { struct installer_update_info inspected, result; const char *disk = context->disks[*context->selected_disk].path; - int error = installer_update_check(disk, &inspected, tty_update_progress, NULL); + 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; @@ -388,6 +412,7 @@ int installer_tty_main(const struct installer_tty_context *context) 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)); @@ -407,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 df62157e..6f3789dc 100644 --- a/userland/apps/installer/installer_tty.h +++ b/userland/apps/installer/installer_tty.h @@ -46,6 +46,7 @@ struct installer_tty_context { int (*partition_delete)(void); int (*partition_format)(uint32_t filesystem, const char *label); void (*perform_install)(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 index 6a986298..b29c6ea7 100644 --- a/userland/apps/installer/installer_update.h +++ b/userland/apps/installer/installer_update.h @@ -9,6 +9,7 @@ * 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]; @@ -43,6 +44,9 @@ int installer_update_write_checkpoint(const char *root, const struct installer_u 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); diff --git a/userland/apps/installer/installer_update_engine.c b/userland/apps/installer/installer_update_engine.c index 9c33b7bd..fa7c7f94 100644 --- a/userland/apps/installer/installer_update_engine.c +++ b/userland/apps/installer/installer_update_engine.c @@ -264,7 +264,8 @@ static int recover_pending(struct update_run *run, struct installer_update_targe } 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) + 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}; @@ -283,7 +284,8 @@ static int execute(const char *disk, const struct installer_update_info *expecte return -out->error_number; } report(&run, 2, N_("Identifying the installed system")); - int result = installer_update_target_open(&target, disk, expected ? expected->root_uuid : NULL); + 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"; @@ -313,7 +315,8 @@ static int execute(const char *disk, const struct installer_update_info *expecte .progress = boot_progress, .progress_context = &run, }; report(&run, 16, N_("Checking bootloader compatibility")); - result = reliefos_boot_validate_payload(&boot); + 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")); @@ -349,7 +352,9 @@ static int execute(const char *disk, const struct installer_update_info *expecte } report(&run, 86, resume_boot ? N_("Resuming bootloader publication") : N_("Publishing kernel and bootloader files")); result = save_checkpoint(&target, &checkpoint, "boot"); - if (!result) result = reliefos_boot_install_at_root(&boot, target.root); + 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"); @@ -379,7 +384,16 @@ int installer_update_check(const char *disk, struct installer_update_info *info, if (info) { memset(info, 0, sizeof(*info)); info->error_number = EINVAL; } return -EINVAL; } - return execute(disk, NULL, 0, info, observer, context); + 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, @@ -392,7 +406,7 @@ int installer_update_apply(const struct installer_update_info *expected, unsigne if (result) { memset(result, 0, sizeof(*result)); result->error_number = EINVAL; } return -EINVAL; } - return execute(expected->disk, expected, modes, result, observer, context); + return execute(expected->disk, expected, modes, result, observer, context, NULL, NULL); } static void worker_progress(void *context, unsigned progress, const char *message) @@ -406,6 +420,7 @@ 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 @@ -420,6 +435,8 @@ int installer_update_worker(int argc, char **argv) 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 || diff --git a/userland/apps/installer/installer_update_internal.h b/userland/apps/installer/installer_update_internal.h index 7d3b1413..68b5b619 100644 --- a/userland/apps/installer/installer_update_internal.h +++ b/userland/apps/installer/installer_update_internal.h @@ -4,14 +4,14 @@ struct installer_update_target { struct installer_update_info info; - char workspace[256], root[256], boot[256]; + char workspace[256], root[256], boot[256], esp[256]; const char *filesystem; - int lock_fd, root_mounted, boot_mounted; + 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 *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); diff --git a/userland/apps/installer/installer_update_model.c b/userland/apps/installer/installer_update_model.c index 9f5f6025..67c0ea43 100644 --- a/userland/apps/installer/installer_update_model.c +++ b/userland/apps/installer/installer_update_model.c @@ -230,6 +230,7 @@ int installer_update_decode(struct installer_update_info *info, unsigned *progre #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), }; diff --git a/userland/apps/installer/installer_update_target.c b/userland/apps/installer/installer_update_target.c index 421632af..707e6873 100644 --- a/userland/apps/installer/installer_update_target.c +++ b/userland/apps/installer/installer_update_target.c @@ -67,7 +67,11 @@ int installer_update_target_close(struct installer_update_target *target) if (umount2(target->boot, 0) < 0) result = -errno; else target->boot_mounted = 0; } - if (target->root_mounted && !target->boot_mounted) { + 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; } @@ -82,8 +86,73 @@ int installer_update_target_close(struct installer_update_target *target) 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 *disk, const char *expected_root, const char *expected_esp) { struct reliefos_block_partition parts[RELIEFOS_BLOCK_MAX_PARTITIONS]; uint32_t count = 0; @@ -96,19 +165,14 @@ int installer_update_target_open(struct installer_update_target *target, result = reliefos_block_list_partitions(disk, 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) { - result = mounted_elsewhere(parts[i].path); - if (result) return result; - if (parts[i].gpt_type == RELIEFOS_BLOCK_GPT_ESP && parts[i].filesystem == RELIEFOS_BLOCK_FILESYSTEM_FAT32) { - if (esp >= 0) return -ENOTUNIQ; - esp = (int)i; - } - } - if (esp < 0) return -ENOENT; strcpy(target->workspace, "/run/reliefos/update-XXXXXX"); if (!mkdtemp(target->workspace)) return -errno; - snprintf(target->root, sizeof(target->root), "%s/root", target->workspace); - snprintf(target->boot, sizeof(target->boot), "%s/root/boot", target->workspace); + 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" @@ -118,6 +182,8 @@ int installer_update_target_open(struct installer_update_target *target, 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; @@ -138,15 +204,40 @@ int installer_update_target_open(struct installer_update_target *target, if (found < 0) return -ENODEV; strcpy(target->info.disk, disk); snprintf(target->info.root_device, sizeof(target->info.root_device), "%s", parts[found].path); - snprintf(target->info.esp_device, sizeof(target->info.esp_device), "%s", parts[esp].path); - result = reliefos_block_partition_uuid(disk, parts[esp].index, target->info.esp_uuid); - if (result) return result; if (mount(target->info.root_device, target->root, target->filesystem, MS_RDONLY | MS_NOSUID | MS_NODEV, NULL) < 0) return -errno; target->root_mounted = 1; - if (mount(target->info.esp_device, target->boot, "vfat", MS_RDONLY | MS_NOSUID | MS_NODEV, NULL) < 0) - return -errno; - target->boot_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); } @@ -155,11 +246,13 @@ 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; - if (mount(target->info.esp_device, target->boot, "vfat", MS_NOSUID | MS_NODEV, NULL) < 0) return -errno; - target->boot_mounted = 1; - return 0; + return mount_boot(target, MS_NOSUID | MS_NODEV); } diff --git a/userland/apps/installer/main.cpp b/userland/apps/installer/main.cpp index 226eb81f..019f9062 100644 --- a/userland/apps/installer/main.cpp +++ b/userland/apps/installer/main.cpp @@ -22,6 +22,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -77,6 +78,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; @@ -436,7 +439,16 @@ static void start_update_job(int applying) return; } executable[bytes] = 0; - const char *check[] = {executable, "--update-worker", "check", disks[selected_disk].path, NULL}; + 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}; @@ -662,10 +674,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(); } @@ -680,6 +728,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(); } @@ -704,11 +753,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(); @@ -717,6 +776,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; @@ -944,6 +1004,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[] = { @@ -992,10 +1058,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(); } @@ -1204,9 +1278,11 @@ 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((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(T("Type INSTALL to enable the Install button."), CONTENT_X, 224, CONTENT_W, 30, 14, 1); @@ -1293,11 +1369,22 @@ 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 @@ -1309,12 +1396,14 @@ static void build_finish_page(void) 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(236, 264); + 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; @@ -1502,6 +1591,7 @@ int main(int argc, char **argv) tty_context.partition_delete = installer_partition_delete; tty_context.partition_format = installer_partition_format; tty_context.perform_install = perform_install; + 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/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/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/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/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 "$@" From a152a89f347f4dd258294c1bb6ab7fdc1f527a70 Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sun, 8 Nov 2026 22:45:13 +0800 Subject: [PATCH 6/9] fix(boot): update ReliefNT for shared ESP bind mounts --- kernel/reliefnt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/reliefnt b/kernel/reliefnt index bd0d364b..6e5966f6 160000 --- a/kernel/reliefnt +++ b/kernel/reliefnt @@ -1 +1 @@ -Subproject commit bd0d364bb7516ba5f7c02e3a040785e6078c67a3 +Subproject commit 6e5966f6088b45752f7825d4e5e2be85e8f67761 From f9a7245e7a0f32d72df94e8e3c3311c9282bf5a6 Mon Sep 17 00:00:00 2001 From: LeonMMcoset Date: Tue, 10 Nov 2026 11:43:23 +0000 Subject: [PATCH 7/9] refactor(packages): replace six source submodules with signed APKs Use official Alpine APKs for sl, ncurses, zlib, libpng, SQLite and mbedTLS 3, including matching SDK headers and libraries. Remove the retired local wrapper packages and migrate existing world requests and offline caches through a real APK transaction. Validation: production artifacts rebuilt, signed repository audited, three offline upgrade scenarios and QEMU runtime checks passed. Full make test still stops on the same 32 pre-existing brand assertions. --- .gitmodules | 18 - Documentation/admin-guide/packages.rst | 17 + Documentation/kbuild/sdk.rst | 8 +- Documentation/kbuild/third-party.rst | 33 +- Documentation/userspace-api/abi.rst | 9 + Kbuild | 1 + NOTICE | 20 +- configs/apk-ownership.json | 24 +- configs/dependencies.lock.json | 264 +++++++-- resources/licenses/libpng-LICENSE | 134 +++++ resources/licenses/mbedtls3-LICENSE | 553 ++++++++++++++++++ resources/licenses/ncurses-LICENSE | 29 + resources/licenses/sl-LICENSE | 6 + resources/licenses/sqlite-LICENSE | 6 + resources/licenses/zlib-LICENSE | 22 + scripts/doctor.sh | 5 - scripts/reliefos/Makefile.apk | 8 +- scripts/reliefos/Makefile.apk-dependencies | 44 ++ scripts/reliefos/Makefile.rootfs | 10 +- scripts/reliefos/Makefile.rpr | 2 +- scripts/reliefos/Makefile.runtime | 35 +- scripts/reliefos/Makefile.sdk | 8 +- scripts/reliefos/Makefile.tests | 4 +- scripts/reliefos/Makefile.third-party | 2 +- scripts/reliefos/Makefile.upstream | 74 +-- scripts/reliefos/Makefile.userland | 12 +- tests/build/brand-allowlist.tsv | 7 +- tests/build/test-apk-ownership.sh | 5 + tests/build/test-apk-stage-selection.sh | 18 + tests/build/test-bootloader-uefi.sh | 6 +- tests/build/test-deps.sh | 10 +- tests/build/test-desktop-backend.sh | 18 +- tests/build/test-reliefapk-certificates.sh | 5 +- tests/build/test-reliefmail.sh | 43 +- tests/build/test-rpr-packages.sh | 15 + tests/build/test-sdk-stage.sh | 20 +- tests/build/test-submodule-contract.sh | 9 +- .../test-upstream-make-helper-dependencies.sh | 3 +- tests/integration/brand-abi.sh | 7 +- tests/integration/runtime-dependencies.sh | 20 + tests/integration/test-apk-upgrade.sh | 20 +- tests/integration/test-runtime-apk-upgrade.sh | 130 ++++ tests/integration/upstream-products.sh | 7 +- tests/lib/runtime-dependencies.sh | 9 + third_party/libpng | 1 - third_party/mbedtls | 1 - third_party/ncurses | 1 - third_party/sl | 1 - third_party/sqlite | 1 - third_party/zlib | 1 - tools/apk_ownership.py | 11 +- tools/build/apk-stage.sh | 8 + tools/build/host-runtime-development.sh | 37 ++ tools/build/musl-sdk.sh | 13 +- tools/build/png-config.sh | 15 - tools/build/rootfs-stage.sh | 46 +- tools/build/rpr-pages.sh | 26 +- tools/build/runtime-development.sh | 95 +++ tools/build/upstream-app.sh | 12 - tools/build/upstream-terminal.sh | 50 -- tools/build/verify-pages.sh | 2 +- tools/check_licenses.py | 40 +- tools/host/apk/reliefos-apk-own.c | 14 +- tools/host/manifest/reliefos-deps.c | 7 +- tools/host/sdk/reliefos-musl-cc.c | 10 +- tools/runtime_test_dependencies.py | 15 + tools/test_bootloader_install.py | 10 +- tools/test_installer_bundle_swap.py | 12 +- tools/test_installer_setup.py | 7 +- tools/test_kernel_bundle.py | 9 +- tools/test_oobe.py | 5 +- tools/test_reliefmail_ca_qemu.py | 5 +- tools/test_sudo_policy.py | 8 +- tools/test_terminal_packages.py | 54 +- tools/tests/runtime_apk_probe.c | 72 +++ tools/tests/runtime_tls_probe.c | 27 + userland/apps/installer/main.cpp | 12 +- userland/apps/reliefmail/Kbuild | 4 +- userland/apps/reliefmail/sqlite_core.c | 22 - userland/apps/reliefmail/store.cpp | 4 +- .../runtime/include/leonos_mbedtls_config.h | 9 +- .../runtime/include/reliefos_mbedtls_config.h | 76 +-- userland/runtime/src/tls.c | 103 +--- userland/runtime/vendor/libpng/Kbuild | 42 -- userland/runtime/vendor/mbedtls/Kbuild | 98 ---- userland/runtime/vendor/zlib/Kbuild | 34 -- userland/sqlite/Kbuild | 7 - userland/sqlite/leonos_sqlite_vfs.c | 280 --------- userland/storage/reliefos-apk-update | 66 ++- 89 files changed, 1873 insertions(+), 1200 deletions(-) create mode 100644 resources/licenses/libpng-LICENSE create mode 100644 resources/licenses/mbedtls3-LICENSE create mode 100644 resources/licenses/ncurses-LICENSE create mode 100644 resources/licenses/sl-LICENSE create mode 100644 resources/licenses/sqlite-LICENSE create mode 100644 resources/licenses/zlib-LICENSE create mode 100644 scripts/reliefos/Makefile.apk-dependencies create mode 100644 tests/integration/runtime-dependencies.sh create mode 100644 tests/integration/test-runtime-apk-upgrade.sh create mode 100644 tests/lib/runtime-dependencies.sh delete mode 160000 third_party/libpng delete mode 160000 third_party/mbedtls delete mode 160000 third_party/ncurses delete mode 160000 third_party/sl delete mode 160000 third_party/sqlite delete mode 160000 third_party/zlib create mode 100644 tools/build/host-runtime-development.sh delete mode 100644 tools/build/png-config.sh create mode 100644 tools/build/runtime-development.sh delete mode 100644 tools/build/upstream-terminal.sh create mode 100644 tools/runtime_test_dependencies.py create mode 100644 tools/tests/runtime_apk_probe.c create mode 100644 tools/tests/runtime_tls_probe.c delete mode 100644 userland/apps/reliefmail/sqlite_core.c delete mode 100644 userland/runtime/vendor/libpng/Kbuild delete mode 100644 userland/runtime/vendor/mbedtls/Kbuild delete mode 100644 userland/runtime/vendor/zlib/Kbuild delete mode 100644 userland/sqlite/Kbuild delete mode 100644 userland/sqlite/leonos_sqlite_vfs.c 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/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/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 664d8f29..698dcefc 100644 --- a/Documentation/kbuild/third-party.rst +++ b/Documentation/kbuild/third-party.rst @@ -18,7 +18,7 @@ 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、归属和字体许可是来源资料,保留原文,不属于待清理的过程报告。 @@ -41,3 +41,34 @@ ReliefPlayer 的 MP4 解码使用官方 FFmpeg 8.1.3 源码,以 LGPL 配置构 动态库依赖、桌面入口及许可证。不要仅为安装成功关闭上游验证。 标准控制台工具没有必要伪装成自研图形应用或分配桌面图标。 上游源码修订先在其仓库提交并推送,再更新父仓引用。 + +预编译基础依赖 +============== + +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/userspace-api/abi.rst b/Documentation/userspace-api/abi.rst index 7252a3ac..6a72660b 100644 --- a/Documentation/userspace-api/abi.rst +++ b/Documentation/userspace-api/abi.rst @@ -74,3 +74,12 @@ X11 应用通过 Xorg 通信。未知或旧兼容路径不能当作新的公开 目录遍历拒绝 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/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/configs/apk-ownership.json b/configs/apk-ownership.json index d60cdb4d..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", @@ -215,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": [], @@ -228,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/dependencies.lock.json b/configs/dependencies.lock.json index 091264b5..0a55e88e 100644 --- a/configs/dependencies.lock.json +++ b/configs/dependencies.lock.json @@ -1211,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" }, @@ -2387,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" }, @@ -2521,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", @@ -2597,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", @@ -2646,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", @@ -2685,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", @@ -2747,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", @@ -2830,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/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 918a563c..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 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.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 93f7f038..8a2a932d 100644 --- a/scripts/reliefos/Makefile.upstream +++ b/scripts/reliefos/Makefile.upstream @@ -13,14 +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 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) ncurses,$(eval upstream_$(package)_primary := $(upstream_$(package)_outputs))) +$(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 @@ -54,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) @@ -111,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)) @@ -171,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 5fd640fa..b024a107 100644 --- a/scripts/reliefos/Makefile.userland +++ b/scripts/reliefos/Makefile.userland @@ -41,8 +41,8 @@ $(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) @@ -84,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) @@ -108,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/tests/build/brand-allowlist.tsv b/tests/build/brand-allowlist.tsv index 2876de38..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,7 +306,6 @@ 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/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 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-bootloader-uefi.sh b/tests/build/test-bootloader-uefi.sh index 63d523c4..27a589c0 100644 --- a/tests/build/test-bootloader-uefi.sh +++ b/tests/build/test-bootloader-uefi.sh @@ -3,13 +3,13 @@ set -eu src=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) work=$(mktemp -d "${TMPDIR:-/tmp}/reliefos-uefi.XXXXXX") trap 'rm -rf "$work"' EXIT HUP INT TERM -printf '#define MBEDTLS_SHA256_C\n' > "$work/qa_mbedtls.h" +. "$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 \ - -DMBEDTLS_CONFIG_FILE='"qa_mbedtls.h"' -I"$work" -I"$src/userland/runtime/include" -I"$src/third_party/mbedtls/include" \ + -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" \ - "$src/third_party/mbedtls/library/sha256.c" "$src/third_party/mbedtls/library/platform_util.c" \ + "$runtime_deps/usr/lib/libmbedcrypto.a" -pthread \ -Wl,--gc-sections -o "$work/verify" "$work/verify" "$work" 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 0ad79d01..e77732c0 100755 --- a/tests/build/test-desktop-backend.sh +++ b/tests/build/test-desktop-backend.sh @@ -132,7 +132,7 @@ cat > "$work/rootfs.config" < "$work/metadata-no-player" -ROOTFS_UPSTREAM_PACKAGES='sudo shadow util-linux ncurses fixture-provider ffmpeg' \ +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 @@ -194,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 @@ -279,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-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-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/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 6d6a8f19..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 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_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 16576e90..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 : 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/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 6148df48..3ea9e733 100644 --- a/tools/build/rootfs-stage.sh +++ b/tools/build/rootfs-stage.sh @@ -19,7 +19,7 @@ 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 ffmpeg 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 @@ -40,18 +40,6 @@ for package in $rootfs_upstream_packages; do 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 @@ -144,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"; @@ -203,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 @@ -263,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/upstream-app.sh b/tools/build/upstream-app.sh index 1d927261..6dca50e2 100644 --- a/tools/build/upstream-app.sh +++ b/tools/build/upstream-app.sh @@ -52,17 +52,5 @@ pleditor) cp "$work/generated/src/main.c" "$work/generated/src/pleditor.c" \ "$work/generated/src/syntax.c" "$output/pleditor-generated/src/" ;; -sl) - compile -D_DEFAULT_SOURCE -fPIE -c "$source/sl.c" -o "$work/objects/sl.o" - executable "$work/objects/sl.o" - ;; -sqlite) - sed "s@TOP = ../sqlite@TOP = $source@" "$source/Makefile.linux-gcc" > "$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..1de8c4f8 100644 --- a/tools/check_licenses.py +++ b/tools/check_licenses.py @@ -27,17 +27,11 @@ # 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 +49,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 +198,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 +259,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(): @@ -414,7 +424,7 @@ def main(argv: Sequence[str] | None = None) -> int: 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) 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 b3feb6f1..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 @@ -53,21 +54,18 @@ def main(): 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() 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_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_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_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_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/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/installer/main.cpp b/userland/apps/installer/main.cpp index 019f9062..d78d5723 100644 --- a/userland/apps/installer/main.cpp +++ b/userland/apps/installer/main.cpp @@ -119,9 +119,9 @@ static const char acknowledgements_en[] = "- 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" + "- zlib 1.4 (signed Alpine APK) - zlib License\n" + "- libpng 1.6.59 (signed Alpine APK) - libpng License\n" + "- SQLite 3.53.4 - 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" @@ -163,9 +163,9 @@ static const char acknowledgements_zh[] = "- 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" + "- zlib 1.4(签名 Alpine APK)- zlib License\n" + "- libpng 1.6.59(签名 Alpine APK)- libpng License\n" + "- SQLite 3.53.4 - 公共领域声明与许可祝福文本\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" 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/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_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/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/reliefos-apk-update b/userland/storage/reliefos-apk-update index c2990ce4..d707abf3 100755 --- a/userland/storage/reliefos-apk-update +++ b/userland/storage/reliefos-apk-update @@ -67,7 +67,52 @@ plan=$temporary/root /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" -/bin/busybox sed 's/^leonos-/reliefos-/' "$temporary/world-before" > "$plan/etc/apk/world" +# 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" @@ -173,13 +218,17 @@ 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-/ { print $1 }') +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 + --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" @@ -235,4 +284,15 @@ 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 From 6743eef500165ee4b808148eecd509b3f0d3cbd2 Mon Sep 17 00:00:00 2001 From: LeonMMcoset Date: Tue, 10 Nov 2026 11:49:20 +0000 Subject: [PATCH 8/9] fix(docs): validate both manuals before building Pages Update ReliefNT to the corrected mixed-width headings. Add an independent Sphinx 5.3/7.2.6/9.x workflow and check both manuals before the Pages image build. Validate rendered pages, local links and anchors, assets, URL prefixes and absence of script/source leaks. Validation: all 108 HTML pages build without warnings on Sphinx 5.3.0, 7.2.6 and 9.2.0; Pages integration, 12 negative cases and actionlint pass. --- .github/workflows/build-pages.yml | 3 + .github/workflows/check-docs.yml | 53 ++++++++++ Documentation/process/documentation.rst | 15 +++ kernel/reliefnt | 2 +- tools/test_documentation.py | 130 ++++++++++++++++++++++++ 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/check-docs.yml create mode 100644 tools/test_documentation.py 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/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/kernel/reliefnt b/kernel/reliefnt index 6e5966f6..80edb7e4 160000 --- a/kernel/reliefnt +++ b/kernel/reliefnt @@ -1 +1 @@ -Subproject commit 6e5966f6088b45752f7825d4e5e2be85e8f67761 +Subproject commit 80edb7e421b20a45f37fc1eb5168175277245654 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() From 8b16ac89040def534376daeed4d43fd9a55de78d Mon Sep 17 00:00:00 2001 From: LeonMMcoset Date: Tue, 10 Nov 2026 14:54:49 +0000 Subject: [PATCH 9/9] feat(installer): show the full Apache 2.0 license Replace the acknowledgement step with a scrollable license page in both locales. Embed the repository LICENSE directly and remove obsolete credit strings and credit-only checks while retaining third-party license packaging validation. Validated installer and VMDK/ISO builds, wizard and staging fixtures, localization, documentation, license packaging, and English/Chinese QEMU scrolling and navigation. The full make test run stops at the same 32 existing brand assertions. --- Documentation/admin-guide/installation.rst | 3 + configs/nls/po/leonos.pot | 13 +-- configs/nls/po/zh_CN.po | 13 +-- tests/host/test_motif_apps.c | 10 +- tools/check_licenses.py | 42 -------- tools/test_installer_responsiveness.py | 2 +- userland/apps/installer/Kbuild | 6 +- userland/apps/installer/apache_license.S | 11 ++ userland/apps/installer/main.cpp | 114 +++------------------ userland/apps/installer/model.c | 8 +- userland/apps/installer/model.h | 2 +- 11 files changed, 43 insertions(+), 181 deletions(-) create mode 100644 userland/apps/installer/apache_license.S diff --git a/Documentation/admin-guide/installation.rst b/Documentation/admin-guide/installation.rst index 6d870684..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 布局根据所选引导方式生成: diff --git a/configs/nls/po/leonos.pot b/configs/nls/po/leonos.pot index 58ee5a32..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 "" @@ -6579,6 +6575,7 @@ msgid "Library" msgstr "" #: userland/apps/reliefapk/window.cpp:684 +#: userland/apps/installer/main.cpp msgid "License" msgstr "" @@ -11802,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 "" diff --git a/configs/nls/po/zh_CN.po b/configs/nls/po/zh_CN.po index 013b77ec..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 "英亩" @@ -6665,6 +6661,7 @@ msgid "Library" msgstr "公式库" #: userland/apps/reliefapk/window.cpp:684 +#: userland/apps/installer/main.cpp msgid "License" msgstr "许可证" @@ -11954,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 文件。" diff --git a/tests/host/test_motif_apps.c b/tests/host/test_motif_apps.c index 47e92c4f..820d570c 100644 --- a/tests/host/test_motif_apps.c +++ b/tests/host/test_motif_apps.c @@ -591,8 +591,8 @@ 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); @@ -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) == @@ -656,7 +656,7 @@ int main(void) 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); @@ -673,7 +673,7 @@ int main(void) 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/tools/check_licenses.py b/tools/check_licenses.py index 1de8c4f8..28c9d921 100644 --- a/tools/check_licenses.py +++ b/tools/check_licenses.py @@ -22,8 +22,6 @@ 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",), @@ -319,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 @@ -374,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) @@ -393,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") @@ -409,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") @@ -423,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) + 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/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/userland/apps/installer/Kbuild b/userland/apps/installer/Kbuild index 3e181a5f..89a24409 100644 --- a/userland/apps/installer/Kbuild +++ b/userland/apps/installer/Kbuild @@ -1,10 +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 installer_update_engine.o installer_update_target.o installer_update_model.o installer_update_journal.o main.o model.o shared_reliefapk_process.o +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 := +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/main.cpp b/userland/apps/installer/main.cpp index d78d5723..1be1a934 100644 --- a/userland/apps/installer/main.cpp +++ b/userland/apps/installer/main.cpp @@ -26,11 +26,9 @@ extern "C" { #include #include #include -#include #include #include #include -#include #include #include #include @@ -108,91 +106,8 @@ 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.4 (signed Alpine APK) - zlib License\n" - "- libpng 1.6.59 (signed Alpine APK) - libpng License\n" - "- SQLite 3.53.4 - 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.4(签名 Alpine APK)- zlib License\n" - "- libpng 1.6.59(签名 Alpine APK)- libpng License\n" - "- SQLite 3.53.4 - 公共领域声明与许可祝福文本\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) @@ -230,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"); @@ -610,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) @@ -1428,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; diff --git a/userland/apps/installer/model.c b/userland/apps/installer/model.c index 1e9b7e79..09ea8908 100644 --- a/userland/apps/installer/model.c +++ b/userland/apps/installer/model.c @@ -72,8 +72,8 @@ 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; @@ -100,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: diff --git a/userland/apps/installer/model.h b/userland/apps/installer/model.h index ee676b89..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,