From 47f323b9b6e7ad6b2d5e47671e44997284610d85 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 15 Oct 2026 01:19:47 +0900 Subject: [PATCH] feat(system): system.admin_step declares a step only an administrator may take, never runs it -- the host asks once, through its one door, or records it when nobody can answer (0.0.63) --- docs/architecture.md | 16 ++++++++++++++++ mcpp.toml | 2 +- src/lua-stdlib/xim/libxpkg/system.lua | 19 +++++++++++++++++++ src/xpkg-executor.cppm | 5 +++-- tests/test_executor.cpp | 22 ++++++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 9f57bb9..62e12ef 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -136,6 +136,22 @@ system.run_in_script(script, true) -- admin(sudo)同理 旧版本的 `system.exec` 忽略 `tty`,而它本来就是直通终端,所以 recipe 可以无条件采用。直通用的原始 `os.execute` 只以内部名 `_LIBXPKG_EXEC_INHERIT` 暴露,不是公开的 `os.*` 函数。 +**需要管理员权限的步骤:声明,不执行(0.0.63)。** recipe 不自己 `sudo`:无人值守的安装里没有终端可问, +等待会一直挂住;有人在场时,问谁、何时问、记录在哪里,应由宿主统一决定。用 `system.admin_step(argv, why)` +声明一步(`argv` 是命令的词列表,`why` 是一行给人看的理由),它作为延后请求交给宿主 +(`install_requests()` 里 `op == "admin"`,`target` 是 why 加上各个词,以 ASCII 单元分隔符 0x1F 连接)。 +宿主在安装结束后一次性询问并执行,没人可问时记为待办。旧版本没有这个函数,recipe 先判断再用: + +```lua +if system.admin_step then + system.admin_step({"chown", "root:root", sandbox}, "Chromium's sandbox helper must be root's") +else + log.warn("run as root: chown root:root %s && chmod 4755 %s", sandbox, sandbox) +end +``` + +`system.run_in_script(script, true)` 仍直接经 `sudo` 执行,只为已有 recipe 保留;新 recipe 用 `admin_step`。 + #### `pkginfo.build_dep` 的名字规范化(0.0.60) xlings 导出 `XLINGS_BUILDDEP__PATH` 时先去掉 `@版本`,再去掉命名空间前缀,再把字母数字转大写、其余转 `_`。`build_dep` 现在按同一规则拼变量名,`build_dep("xim:7zip")`、`build_dep("7zip")`、`build_dep("xim:7zip@26.02")` 读的是同一个 `XLINGS_BUILDDEP_7ZIP_PATH`。之前带命名空间的写法查的是 `XLINGS_BUILDDEP_XIM_7ZIP_PATH`,永远查不到,退回 `dep_install_dir`(它不记录 build 依赖),最后返回 nil。查不到变量时仍退回 `dep_install_dir`。 diff --git a/mcpp.toml b/mcpp.toml index 1691f00..8465cce 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "xpkg" -version = "0.0.62" +version = "0.0.63" description = "C++23 reference implementation of the xpkg V2 spec (multi-arch)" license = "Apache-2.0" repo = "https://github.com/openxlings/libxpkg" diff --git a/src/lua-stdlib/xim/libxpkg/system.lua b/src/lua-stdlib/xim/libxpkg/system.lua index b2af645..f4399ec 100644 --- a/src/lua-stdlib/xim/libxpkg/system.lua +++ b/src/lua-stdlib/xim/libxpkg/system.lua @@ -27,6 +27,25 @@ function M.exec(cmd, opt) end end +-- A step only an administrator may take (chown root a sandbox helper, a +-- setuid bit), DECLARED, never run here: the host decides who is asked, when +-- and whether anyone can be (a recipe's own `sudo` waits on a terminal that +-- an unattended install does not have). `argv` is the command as a list, +-- `why` one line a person reads before saying yes. It reaches the host as a +-- deferred request: op "admin", target = why, then each word, separated by +-- the ASCII unit separator (0x1F). +function M.admin_step(argv, why) + if type(argv) ~= "table" or #argv == 0 then error("system.admin_step: argv is a non-empty list") end + local words = { tostring(why or "") } + for _, w in ipairs(argv) do + w = tostring(w) + if w:find("\31", 1, true) then error("system.admin_step: a word holds the separator") end + table.insert(words, w) + end + if not _INSTALL_REQUESTS then _INSTALL_REQUESTS = {} end + table.insert(_INSTALL_REQUESTS, {op = "admin", target = table.concat(words, "\31")}) +end + function M.rundir() return _RUNTIME and _RUNTIME.run_dir or nil end function M.xpkgdir() return _RUNTIME and _RUNTIME.xpkg_dir or nil end function M.bindir() return _RUNTIME and _RUNTIME.bin_dir or nil end diff --git a/src/xpkg-executor.cppm b/src/xpkg-executor.cppm index d44f34b..36be9f1 100644 --- a/src/xpkg-executor.cppm +++ b/src/xpkg-executor.cppm @@ -191,8 +191,9 @@ struct XvmOp { }; struct InstallRequest { - std::string op; // "install" | "remove" - std::string target; // e.g. "scode:linux-headers@5.11.1" + std::string op; // "install" | "remove" | "admin" (system.admin_step) + std::string target; // e.g. "scode:linux-headers@5.11.1"; for "admin": why, then + // each word of the command, joined by '\x1f' }; enum class HookType { Installed, Build, Install, Config, Uninstall }; diff --git a/tests/test_executor.cpp b/tests/test_executor.cpp index 9938bd1..413a4c6 100644 --- a/tests/test_executor.cpp +++ b/tests/test_executor.cpp @@ -3760,6 +3760,28 @@ TEST(ExecutionBoundaryTest, TheLegacyFactoryStillLoadsAndRunsLocally) { fs::remove_all(temp); } +TEST(ExecutorTest, AnAdminStepIsDeclaredNotRun) { + const auto temp = make_temp_dir("libxpkg-admin-step-"); + const auto recipe = temp / "admin.lua"; + const auto marker = temp / "ran"; + write_text(recipe, std::string(R"LUA( + import('xim.libxpkg.system') + function install() + system.admin_step({'touch', ')LUA") + marker.string() + R"LUA('}, 'a file only root may make') + return true + end + )LUA"); + auto exec = create_executor(recipe); + ASSERT_TRUE(exec) << exec.error(); + ASSERT_TRUE(exec->run_hook(HookType::Install, make_context(temp / "payload", "linux")).success); + EXPECT_FALSE(fs::exists(marker)) << "declared, never run by the recipe"; + auto requests = exec->install_requests(); + ASSERT_EQ(requests.size(), 1u); + EXPECT_EQ(requests[0].op, "admin"); + EXPECT_EQ(requests[0].target, "a file only root may make\x1ftouch\x1f" + marker.string()); + fs::remove_all(temp); +} + TEST(ExecutionBoundaryTest, OneWorkerRetainsRecipeStateAndCumulativeEffectsAcrossHooks) { const auto temp = make_temp_dir("libxpkg-boundary-effects-"); const auto recipe = temp / "effects.lua";