Skip to content

Commit 45cc32d

Browse files
committed
the distributable is the terminal artifact, and a library-form application stages its runtime files (#622)
1 parent c765c8c commit 45cc32d

8 files changed

Lines changed: 101 additions & 3 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,17 @@ Emscripten 自己的 CMake 工具链(`CMAKE_EXECUTABLE_SUFFIX ".js"`)与 Rust
173173
的宿主 spec(用户写的 `[toolchain]` 或机器默认),构建程序按它解析;行 pin 没有替换任何
174174
东西时行为不变。`tests/e2e/657`
175175

176+
### 修复:发布物是终端产物;库形态的应用也带上运行期文件(#622)
177+
178+
- `mcpp pack --format <name>``mcpp run --format <name>` 报告的产物改为请求引入的
179+
artifact 动作中**没有被其他引入动作当作输入**的输出(终端产物)。此前取第一个输出:
180+
`dist-apk` 提交的是一条链(link、加库、对齐、签名),`adb-run` 拿到的是未签名的 `base.apk`,
181+
`adb install` 拒绝安装。中间产物仍逐个核验存在,只是不再以 `Packed` 报告;`mcpp run` 在终端
182+
产物不止一个时按句拒绝并列出它们。`tests/e2e/656`
183+
- 在应用形态为共享库的行上(`*-linux-android``kind = "app"`),`mcpp pack` 此前只暂存
184+
`lib/<name>.so`,`deploy` 放置的运行期文件没有进暂存树,于是 dist-apk 的 `assets/` 为空。
185+
现在与其他行一致,按 `bin/<to>/...` 的相对路径暂存。`tests/e2e/652b`
186+
176187
## [2026.9.12.2] - 2026-09-12
177188

178189
2026.9.12.1 未单独发布,其条目并入本版本。

docs/10-pack-and-release.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,14 @@ then runs the artifact the pack reported, through the runner resolved for a
212212
program: the project's `[target.<triple>] runner`, then a dependency's
213213
`mcpp::runner(...)`, then the payload descriptor's.
214214

215+
The artifact a pack reports is the request's **terminal** one: among the
216+
`artifact` actions the request introduced, the output no other introduced
217+
action consumes. A provider is often a chain (`dist-apk`: link, add the
218+
libraries, align, sign), and every output in it is verified to exist, but only
219+
the last is the distributable and only it is printed as `Packed`. A format
220+
whose chain ends in two files is refused by `mcpp run --format`, naming both,
221+
because a runner takes one operand.
222+
215223
An unknown `<name>` is refused naming the format set the resolved graph
216224
provides, the same set `mcpp pack --format bogus` reports. `--format` together
217225
with `--no-runner` is refused — an `.apk` or an installed `.app` cannot be

docs/zh/10-pack-and-release.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ mcpp run --target aarch64-ios-sim --format app
171171
报出的那个产物,经由为一个程序解析出的 runner:项目的 `[target.<triple>] runner`,
172172
其次依赖的 `mcpp::runner(...)`,再次载荷描述文件的。
173173

174+
打包报出的产物是这次请求的**终端**产物:在请求引入的 `artifact` 动作中,没有被其他
175+
引入动作当作输入的那个输出。提供者常常是一条链(`dist-apk`:链接、加库、对齐、签名),
176+
链上每个输出都会被核验存在,但只有最后一个是发布物,也只有它以 `Packed` 报出。链的
177+
末端有两个文件的格式会被 `mcpp run --format` 拒绝并点名两者,因为 runner 只接受一个
178+
操作数。
179+
174180
未知的 `<name>` 会被拒绝,点名已解析图提供的格式集合,与 `mcpp pack --format
175181
bogus` 报出的是同一个集合。`--format``--no-runner` 同时出现会被拒绝——一个
176182
`.apk` 或已安装的 `.app` 无法被直接执行。在 `kind = "app"` 的形态是一个库的那一行上,

src/build/execute.cppm

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,21 @@ export int build_run_target(const std::optional<std::string>& targetName,
18861886
if (auto rc = run_build_plan(*ctx2, /*verbose=*/false, no_cache, target_triple);
18871887
rc != 0)
18881888
return rc;
1889+
// ONE DISTRIBUTABLE, OR A SENTENCE. The pack pipeline reports the
1890+
// terminal artifacts of the request (outputs no other introduced
1891+
// action consumes); a format that ends in two files has no single
1892+
// operand a runner can take.
1893+
if (outcome.artifacts.size() != 1) {
1894+
std::string names;
1895+
for (auto const& a : outcome.artifacts) {
1896+
if (!names.empty()) names += ", ";
1897+
names += a.string();
1898+
}
1899+
std::println(stderr,
1900+
"error: --format {} produced {} distributables ({}); mcpp run needs "
1901+
"exactly one to hand to the runner", format, outcome.artifacts.size(), names);
1902+
return 1;
1903+
}
18891904
return run_artifact_via_runner(*ctx2, outcome.artifacts.front(),
18901905
passthrough, no_runner, runner_name);
18911906
}

src/pack/pack.cppm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,15 @@ run_shared_program(const Plan& plan)
11701170
if (ec) return std::unexpected(Error{std::format(
11711171
"copy binary failed: {}", ec.message())});
11721172

1173+
// THE RUNTIME FILES TRAVEL AS ON EVERY OTHER ROW. `deploy` placed them
1174+
// under `bin/<to>/` beside the built library; they are staged at the same
1175+
// relative path under `bin/`, which is where a provider that maps them
1176+
// into its own layout (`dist-apk`: `assets/`) reads them. Measured
1177+
// 2026-09-12: without this the Android staged tree carried the library
1178+
// alone and a deploy'd resource never reached the APK.
1179+
if (!plan.opts.runtimeFiles.empty())
1180+
if (auto r = stage_runtime_files(plan, plan.stagingRoot / "bin"); !r) return r;
1181+
11731182
copy_if_exists(plan.projectRoot / "README.md", plan.stagingRoot);
11741183
copy_if_exists(plan.projectRoot / "LICENSE", plan.stagingRoot);
11751184

src/pack/pipeline.cppm

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,11 +433,27 @@ export PackOutcome build_and_pack(Options opts, bool modeFromUser,
433433
// Identity is (package, id): an id is unique within the package that
434434
// declared it and nothing more.
435435
std::vector<std::string> distOutputs;
436+
// THE DISTRIBUTABLE IS THE TERMINAL ARTIFACT. A provider may submit a
437+
// chain (`dist-apk`: link, add libraries, align, sign); every output
438+
// is verified below, but the thing a user installs, and the operand
439+
// `mcpp run --format` hands the runner, is an output no other
440+
// introduced action consumes. Measured 2026-09-12: with the first
441+
// output taken as the operand, `adb-run` received the unsigned
442+
// `base.apk` and `adb install` refused it.
443+
std::vector<std::string> distInputs;
436444
for (auto const& a : distCtx->plan.actions) {
437445
if (a.role != mcpp::manifest::BuildAction::Role::Artifact) continue;
438446
if (preexistingArtifacts.contains({a.packageName, a.id})) continue;
439447
for (auto const& o : a.outputs) distOutputs.push_back(o);
448+
for (auto const& i : a.inputs) distInputs.push_back(i);
440449
}
450+
auto absolute_of = [&](std::string const& p) {
451+
auto q = std::filesystem::path(p).is_absolute()
452+
? std::filesystem::path(p) : distCtx->plan.outputDir / p;
453+
return q.lexically_normal();
454+
};
455+
std::set<std::filesystem::path> consumed;
456+
for (auto const& i : distInputs) consumed.insert(absolute_of(i));
441457
// DECLARED AND THEN SUBMITTED NOTHING. The half of the contract a
442458
// member is most likely to get wrong is the gate, and a member whose
443459
// gate never opens leaves a pass that succeeds and produces no
@@ -484,19 +500,23 @@ export PackOutcome build_and_pack(Options opts, bool modeFromUser,
484500
// nothing.
485501
std::error_code ec;
486502
std::vector<std::filesystem::path> reported;
503+
std::vector<std::filesystem::path> intermediate;
487504
for (auto const& o : distOutputs) {
488-
auto abs = std::filesystem::path(o).is_absolute()
489-
? std::filesystem::path(o) : distCtx->plan.outputDir / o;
505+
auto abs = absolute_of(o);
490506
if (!std::filesystem::is_regular_file(abs, ec)
491507
&& !std::filesystem::is_directory(abs, ec)) {
492508
mcpp::ui::error(std::format(
493509
"--format {} reported success and produced nothing at {}",
494510
opts.formatName, abs.string()));
495511
return PackOutcome{1};
496512
}
513+
if (consumed.contains(abs)) { intermediate.push_back(std::move(abs)); continue; }
497514
mcpp::ui::status("Packed", mcpp::ui::shorten_path(abs, pathCtx));
498515
reported.push_back(std::move(abs));
499516
}
517+
// Every output consumed by another: a cycle a provider should not
518+
// write, reported as all outputs rather than as nothing.
519+
if (reported.empty()) reported = std::move(intermediate);
500520
return PackOutcome{0, std::move(reported)};
501521
}
502522

tests/e2e/652b_an_application_on_android_is_a_shared_library.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ echo "the refusal does not fire for a bin target OK"
9494
# adding it earlier would change what check 3's refusal lists (it asserts
9595
# "one of: none declared", which is only true while this package provides
9696
# no format at all).
97+
printf "resource-1\n" > res.txt
9798
cat > copy.sh <<'EOF'
9899
#!/usr/bin/env bash
99100
set -e
@@ -116,6 +117,11 @@ int main() {
116117
std::fclose(f);
117118
}
118119
120+
// A deploy'd file (#622 A4) travels with the library: staged under
121+
// `bin/<to>/` on this row as on every other, where a provider that maps
122+
// it into its own layout (dist-apk: assets/) reads it.
123+
mcpp::deploy((std::string(mcpp::manifest_dir()) + "/res.txt").c_str(), "myres");
124+
119125
mcpp::provides_pack_format("blob");
120126
if (std::string_view(mcpp::pack_format()) != "blob") return 0;
121127
@@ -144,6 +150,8 @@ staged=$(ls -d target/dist/myapp-0.1.0-*/ 2>/dev/null | head -1)
144150
[ -n "$staged" ] || fail "no staged tree under target/dist for the Android pack" pack.log
145151
[ -f "${staged}lib/libmyapp.so" ] \
146152
|| fail "the staged tree has no lib/libmyapp.so" pack.log
153+
[ -f "${staged}bin/myres/res.txt" ] \
154+
|| fail "the deploy'd file was not staged under bin/myres/ on the Android row" pack.log
147155
[ -n "$(find target -name 'myapp.blob' 2>/dev/null)" ] \
148156
|| fail "the reported artifact myapp.blob does not exist" pack.log
149157
echo "mcpp pack --format blob on Android stages lib/libmyapp.so OK"

tests/e2e/656_run_hands_the_distributable_to_the_runner.sh

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ int main() {
9090
.input("${mcpp.target_file:app}")
9191
.output(out.c_str())
9292
.submit();
93+
// A second step that consumes the first: the format's distributable is
94+
// the TERMINAL artifact (the output no other introduced action consumes),
95+
// which is what the runner must receive. A provider such as dist-apk is
96+
// a chain of this shape (link, add libraries, align, sign).
97+
const std::string fin = std::string(mcpp::out_dir()) + "/app.final";
98+
mcpp::action b;
99+
b.id = "final";
100+
b.role = "artifact";
101+
b.description = "final";
102+
b.arg((root + "/copy.sh").c_str())
103+
.arg(out.c_str())
104+
.arg(fin.c_str())
105+
.input(out.c_str())
106+
.output(fin.c_str())
107+
.submit();
93108
return 0;
94109
}
95110
EOF
@@ -130,8 +145,14 @@ printf '\n[target.%s]\nrunner = ["%s"]\n' "$HOST" "$TMP/runner.sh" >> mcpp.toml
130145

131146
# ── 1. `mcpp run --format blob` hands the runner the distributable ────────
132147
out=$("$MCPP" run --format blob 2>&1) || fail "mcpp run --format blob failed" <(echo "$out")
148+
grep -q "RUNNER: .*/app\.final$" <<<"$out" \
149+
|| fail "the runner's operand was not the terminal artifact app.final" <(echo "$out")
133150
grep -q "RUNNER: .*/app\.blob$" <<<"$out" \
134-
|| fail "the runner's operand was not app.blob" <(echo "$out")
151+
&& fail "the runner received the intermediate app.blob" <(echo "$out")
152+
grep -q "Packed .*app\.final" <<<"$out" \
153+
|| fail "the Packed line does not name the terminal artifact" <(echo "$out")
154+
grep -q "Packed .*app\.blob" <<<"$out" \
155+
&& fail "the intermediate app.blob was reported as Packed" <(echo "$out")
135156
grep -q "1-2-3" <<<"$out" \
136157
|| fail "the program's marker did not print through the runner" <(echo "$out")
137158
echo "mcpp run --format blob hands the runner the distributable OK"

0 commit comments

Comments
 (0)