From 45cc32d7227b6c047fda8a4410df5290a6494f58 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:22:42 +0800 Subject: [PATCH] the distributable is the terminal artifact, and a library-form application stages its runtime files (#622) --- CHANGELOG.md | 11 +++++++++ docs/10-pack-and-release.md | 8 +++++++ docs/zh/10-pack-and-release.md | 6 +++++ src/build/execute.cppm | 15 ++++++++++++ src/pack/pack.cppm | 9 +++++++ src/pack/pipeline.cppm | 24 +++++++++++++++++-- ...lication_on_android_is_a_shared_library.sh | 8 +++++++ ...n_hands_the_distributable_to_the_runner.sh | 23 +++++++++++++++++- 8 files changed, 101 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8189b4..4d08ba0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -173,6 +173,17 @@ Emscripten 自己的 CMake 工具链(`CMAKE_EXECUTABLE_SUFFIX ".js"`)与 Rust 的宿主 spec(用户写的 `[toolchain]` 或机器默认),构建程序按它解析;行 pin 没有替换任何 东西时行为不变。`tests/e2e/657`。 +### 修复:发布物是终端产物;库形态的应用也带上运行期文件(#622) + +- `mcpp pack --format ` 与 `mcpp run --format ` 报告的产物改为请求引入的 + artifact 动作中**没有被其他引入动作当作输入**的输出(终端产物)。此前取第一个输出: + `dist-apk` 提交的是一条链(link、加库、对齐、签名),`adb-run` 拿到的是未签名的 `base.apk`, + `adb install` 拒绝安装。中间产物仍逐个核验存在,只是不再以 `Packed` 报告;`mcpp run` 在终端 + 产物不止一个时按句拒绝并列出它们。`tests/e2e/656`。 +- 在应用形态为共享库的行上(`*-linux-android` 的 `kind = "app"`),`mcpp pack` 此前只暂存 + `lib/.so`,`deploy` 放置的运行期文件没有进暂存树,于是 dist-apk 的 `assets/` 为空。 + 现在与其他行一致,按 `bin//...` 的相对路径暂存。`tests/e2e/652b`。 + ## [2026.9.12.2] - 2026-09-12 2026.9.12.1 未单独发布,其条目并入本版本。 diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index 6113f287..172e22b0 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -212,6 +212,14 @@ then runs the artifact the pack reported, through the runner resolved for a program: the project's `[target.] runner`, then a dependency's `mcpp::runner(...)`, then the payload descriptor's. +The artifact a pack reports is the request's **terminal** one: among the +`artifact` actions the request introduced, the output no other introduced +action consumes. A provider is often a chain (`dist-apk`: link, add the +libraries, align, sign), and every output in it is verified to exist, but only +the last is the distributable and only it is printed as `Packed`. A format +whose chain ends in two files is refused by `mcpp run --format`, naming both, +because a runner takes one operand. + An unknown `` is refused naming the format set the resolved graph provides, the same set `mcpp pack --format bogus` reports. `--format` together with `--no-runner` is refused — an `.apk` or an installed `.app` cannot be diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index a7f8f59b..8b825bd7 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -171,6 +171,12 @@ mcpp run --target aarch64-ios-sim --format app 报出的那个产物,经由为一个程序解析出的 runner:项目的 `[target.] runner`, 其次依赖的 `mcpp::runner(...)`,再次载荷描述文件的。 +打包报出的产物是这次请求的**终端**产物:在请求引入的 `artifact` 动作中,没有被其他 +引入动作当作输入的那个输出。提供者常常是一条链(`dist-apk`:链接、加库、对齐、签名), +链上每个输出都会被核验存在,但只有最后一个是发布物,也只有它以 `Packed` 报出。链的 +末端有两个文件的格式会被 `mcpp run --format` 拒绝并点名两者,因为 runner 只接受一个 +操作数。 + 未知的 `` 会被拒绝,点名已解析图提供的格式集合,与 `mcpp pack --format bogus` 报出的是同一个集合。`--format` 与 `--no-runner` 同时出现会被拒绝——一个 `.apk` 或已安装的 `.app` 无法被直接执行。在 `kind = "app"` 的形态是一个库的那一行上, diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 0d8683f5..592d0cb2 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -1886,6 +1886,21 @@ export int build_run_target(const std::optional& targetName, if (auto rc = run_build_plan(*ctx2, /*verbose=*/false, no_cache, target_triple); rc != 0) return rc; + // ONE DISTRIBUTABLE, OR A SENTENCE. The pack pipeline reports the + // terminal artifacts of the request (outputs no other introduced + // action consumes); a format that ends in two files has no single + // operand a runner can take. + if (outcome.artifacts.size() != 1) { + std::string names; + for (auto const& a : outcome.artifacts) { + if (!names.empty()) names += ", "; + names += a.string(); + } + std::println(stderr, + "error: --format {} produced {} distributables ({}); mcpp run needs " + "exactly one to hand to the runner", format, outcome.artifacts.size(), names); + return 1; + } return run_artifact_via_runner(*ctx2, outcome.artifacts.front(), passthrough, no_runner, runner_name); } diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 035f5b1c..66724f40 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -1170,6 +1170,15 @@ run_shared_program(const Plan& plan) if (ec) return std::unexpected(Error{std::format( "copy binary failed: {}", ec.message())}); + // THE RUNTIME FILES TRAVEL AS ON EVERY OTHER ROW. `deploy` placed them + // under `bin//` beside the built library; they are staged at the same + // relative path under `bin/`, which is where a provider that maps them + // into its own layout (`dist-apk`: `assets/`) reads them. Measured + // 2026-09-12: without this the Android staged tree carried the library + // alone and a deploy'd resource never reached the APK. + if (!plan.opts.runtimeFiles.empty()) + if (auto r = stage_runtime_files(plan, plan.stagingRoot / "bin"); !r) return r; + copy_if_exists(plan.projectRoot / "README.md", plan.stagingRoot); copy_if_exists(plan.projectRoot / "LICENSE", plan.stagingRoot); diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index ecbe2fff..cf3c87f3 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -433,11 +433,27 @@ export PackOutcome build_and_pack(Options opts, bool modeFromUser, // Identity is (package, id): an id is unique within the package that // declared it and nothing more. std::vector distOutputs; + // THE DISTRIBUTABLE IS THE TERMINAL ARTIFACT. A provider may submit a + // chain (`dist-apk`: link, add libraries, align, sign); every output + // is verified below, but the thing a user installs, and the operand + // `mcpp run --format` hands the runner, is an output no other + // introduced action consumes. Measured 2026-09-12: with the first + // output taken as the operand, `adb-run` received the unsigned + // `base.apk` and `adb install` refused it. + std::vector distInputs; for (auto const& a : distCtx->plan.actions) { if (a.role != mcpp::manifest::BuildAction::Role::Artifact) continue; if (preexistingArtifacts.contains({a.packageName, a.id})) continue; for (auto const& o : a.outputs) distOutputs.push_back(o); + for (auto const& i : a.inputs) distInputs.push_back(i); } + auto absolute_of = [&](std::string const& p) { + auto q = std::filesystem::path(p).is_absolute() + ? std::filesystem::path(p) : distCtx->plan.outputDir / p; + return q.lexically_normal(); + }; + std::set consumed; + for (auto const& i : distInputs) consumed.insert(absolute_of(i)); // DECLARED AND THEN SUBMITTED NOTHING. The half of the contract a // member is most likely to get wrong is the gate, and a member whose // gate never opens leaves a pass that succeeds and produces no @@ -484,9 +500,9 @@ export PackOutcome build_and_pack(Options opts, bool modeFromUser, // nothing. std::error_code ec; std::vector reported; + std::vector intermediate; for (auto const& o : distOutputs) { - auto abs = std::filesystem::path(o).is_absolute() - ? std::filesystem::path(o) : distCtx->plan.outputDir / o; + auto abs = absolute_of(o); if (!std::filesystem::is_regular_file(abs, ec) && !std::filesystem::is_directory(abs, ec)) { mcpp::ui::error(std::format( @@ -494,9 +510,13 @@ export PackOutcome build_and_pack(Options opts, bool modeFromUser, opts.formatName, abs.string())); return PackOutcome{1}; } + if (consumed.contains(abs)) { intermediate.push_back(std::move(abs)); continue; } mcpp::ui::status("Packed", mcpp::ui::shorten_path(abs, pathCtx)); reported.push_back(std::move(abs)); } + // Every output consumed by another: a cycle a provider should not + // write, reported as all outputs rather than as nothing. + if (reported.empty()) reported = std::move(intermediate); return PackOutcome{0, std::move(reported)}; } diff --git a/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh b/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh index 5ad56049..511bdca5 100755 --- a/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh +++ b/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh @@ -94,6 +94,7 @@ echo "the refusal does not fire for a bin target OK" # adding it earlier would change what check 3's refusal lists (it asserts # "one of: none declared", which is only true while this package provides # no format at all). +printf "resource-1\n" > res.txt cat > copy.sh <<'EOF' #!/usr/bin/env bash set -e @@ -116,6 +117,11 @@ int main() { std::fclose(f); } + // A deploy'd file (#622 A4) travels with the library: staged under + // `bin//` on this row as on every other, where a provider that maps + // it into its own layout (dist-apk: assets/) reads it. + mcpp::deploy((std::string(mcpp::manifest_dir()) + "/res.txt").c_str(), "myres"); + mcpp::provides_pack_format("blob"); if (std::string_view(mcpp::pack_format()) != "blob") return 0; @@ -144,6 +150,8 @@ staged=$(ls -d target/dist/myapp-0.1.0-*/ 2>/dev/null | head -1) [ -n "$staged" ] || fail "no staged tree under target/dist for the Android pack" pack.log [ -f "${staged}lib/libmyapp.so" ] \ || fail "the staged tree has no lib/libmyapp.so" pack.log +[ -f "${staged}bin/myres/res.txt" ] \ + || fail "the deploy'd file was not staged under bin/myres/ on the Android row" pack.log [ -n "$(find target -name 'myapp.blob' 2>/dev/null)" ] \ || fail "the reported artifact myapp.blob does not exist" pack.log echo "mcpp pack --format blob on Android stages lib/libmyapp.so OK" diff --git a/tests/e2e/656_run_hands_the_distributable_to_the_runner.sh b/tests/e2e/656_run_hands_the_distributable_to_the_runner.sh index d0198e4f..7975d1e5 100755 --- a/tests/e2e/656_run_hands_the_distributable_to_the_runner.sh +++ b/tests/e2e/656_run_hands_the_distributable_to_the_runner.sh @@ -90,6 +90,21 @@ int main() { .input("${mcpp.target_file:app}") .output(out.c_str()) .submit(); + // A second step that consumes the first: the format's distributable is + // the TERMINAL artifact (the output no other introduced action consumes), + // which is what the runner must receive. A provider such as dist-apk is + // a chain of this shape (link, add libraries, align, sign). + const std::string fin = std::string(mcpp::out_dir()) + "/app.final"; + mcpp::action b; + b.id = "final"; + b.role = "artifact"; + b.description = "final"; + b.arg((root + "/copy.sh").c_str()) + .arg(out.c_str()) + .arg(fin.c_str()) + .input(out.c_str()) + .output(fin.c_str()) + .submit(); return 0; } EOF @@ -130,8 +145,14 @@ printf '\n[target.%s]\nrunner = ["%s"]\n' "$HOST" "$TMP/runner.sh" >> mcpp.toml # ── 1. `mcpp run --format blob` hands the runner the distributable ──────── out=$("$MCPP" run --format blob 2>&1) || fail "mcpp run --format blob failed" <(echo "$out") +grep -q "RUNNER: .*/app\.final$" <<<"$out" \ + || fail "the runner's operand was not the terminal artifact app.final" <(echo "$out") grep -q "RUNNER: .*/app\.blob$" <<<"$out" \ - || fail "the runner's operand was not app.blob" <(echo "$out") + && fail "the runner received the intermediate app.blob" <(echo "$out") +grep -q "Packed .*app\.final" <<<"$out" \ + || fail "the Packed line does not name the terminal artifact" <(echo "$out") +grep -q "Packed .*app\.blob" <<<"$out" \ + && fail "the intermediate app.blob was reported as Packed" <(echo "$out") grep -q "1-2-3" <<<"$out" \ || fail "the program's marker did not print through the runner" <(echo "$out") echo "mcpp run --format blob hands the runner the distributable OK"