From 8613f2525d8f57dc049a8f7e8daa89b195400789 Mon Sep 17 00:00:00 2001 From: NopeCharon <115794254+NopeCharon@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:30:54 +0530 Subject: [PATCH] fix: a missing mcpp.lock is named by `emit sbom` and `--locked` mcpp::pm::load returns an empty Lockfile when the file does not exist, and two callers took a successful load to mean the file was there, so the branches they wrote for a missing lock were never taken. `mcpp emit sbom` wrote a root-only document, exit 0, without the note its own comment requires: that document is true of a project with no dependencies and false of one that was never built. It now asks whether the file exists before loading it. The note goes to the narration stream, so standard output stays the document. `mcpp build --locked` reported every resolved package as "(not in the lock)" under "this resolution differs from mcpp.lock", naming a file that is not there. It now gives the refusal that says the lock is missing and how to record one. `load` keeps its meaning: the build's own readers want an empty lock from a project that has none. A lock that exists behaves as before in both commands, and one that cannot be read is still an error. Tests: tests/e2e/333 runs `--locked` and `emit sbom` with the lock moved aside. docs/51 states the note in both languages. Closes #891 --- docs/51-supported-versions.md | 5 +++- docs/zh/51-supported-versions.md | 3 ++- src/build/prepare/records.cpp | 4 ++- src/cli/cmd_sbom.cppm | 14 ++++++---- .../e2e/333_named_runners_locked_and_sbom.sh | 27 +++++++++++++++++++ 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/docs/51-supported-versions.md b/docs/51-supported-versions.md index fef07b562..57ea29b0b 100644 --- a/docs/51-supported-versions.md +++ b/docs/51-supported-versions.md @@ -113,7 +113,10 @@ mcpp emit sbom -o sbom.json It reads `mcpp.lock` rather than resolving again, because a document describing a different graph from the one that was built is worse than no document. A component whose licence mcpp does not know is emitted as `NOASSERTION` rather -than omitted: an absent field reads as "not examined". +than omitted: an absent field reads as "not examined". With no `mcpp.lock` the +document names the root package alone, and a note on standard error says that +nothing is recorded: that is the whole truth for a project with no dependencies, +and not for one that has never been built. ## Offline and air-gapped use diff --git a/docs/zh/51-supported-versions.md b/docs/zh/51-supported-versions.md index aefe888fb..aaf6a404a 100644 --- a/docs/zh/51-supported-versions.md +++ b/docs/zh/51-supported-versions.md @@ -97,7 +97,8 @@ mcpp emit sbom -o sbom.json 它读取 `mcpp.lock` 而不是重新解析,因为一份描述了与实际构建不同的依赖图的 文档,比没有文档更糟。许可证 mcpp 无法识别的组件会被写成 `NOASSERTION` 而不是 -省略:缺失的字段读起来像「未检查」。 +省略:缺失的字段读起来像「未检查」。没有 `mcpp.lock` 时,文档只列出根包,并在标准 +错误上提示没有任何记录:这对没有依赖的工程是全部事实,对从未构建过的工程则不是。 ## 离线与隔网使用 diff --git a/src/build/prepare/records.cpp b/src/build/prepare/records.cpp index aea64a660..5ec032291 100644 --- a/src/build/prepare/records.cpp +++ b/src/build/prepare/records.cpp @@ -426,8 +426,10 @@ std::expected step13_lockfile(PrepareState& state, BuildConte // true and useless; which package moved, from which version to // which, is what the reader does something about. if (mcpp::invocation::get("MCPP_LOCKED").value_or("") == "1") { + // `load` reads a missing file as an empty lock, which would + // report every package as drift from a file that is not there. auto prior = mcpp::lockfile::load(lockPath); - if (!prior) { + if (!prior || !std::filesystem::exists(lockPath)) { return std::unexpected(std::format( "--locked was given and there is no readable mcpp.lock at {}\n" " Run the same command without --locked once to record " diff --git a/src/cli/cmd_sbom.cppm b/src/cli/cmd_sbom.cppm index ad37d59df..1867990b9 100644 --- a/src/cli/cmd_sbom.cppm +++ b/src/cli/cmd_sbom.cppm @@ -102,12 +102,16 @@ int cmd_sbom(const mcpplibs::cmdline::ParsedArgs& parsed) { const auto lockPath = *root / "mcpp.lock"; std::vector pkgs; - if (auto lock = mcpp::lockfile::load(lockPath)) { + // Asked before loading: `load` reads a file that is not there as an empty + // lock, so its answer cannot tell "nothing recorded" from "no dependencies". + if (std::filesystem::exists(lockPath)) { + auto lock = mcpp::lockfile::load(lockPath); + if (!lock) { + mcpp::ui::error(std::format( + "mcpp.lock exists but could not be read: {}", lock.error().message)); + return 2; + } pkgs = lock->packages; - } else if (std::filesystem::exists(lockPath)) { - mcpp::ui::error(std::format( - "mcpp.lock exists but could not be read: {}", lock.error().message)); - return 2; } else { // NOT AN ERROR, AND NOT SILENCE EITHER. A project with no // dependencies has no lock and its bill of materials is one component, diff --git a/tests/e2e/333_named_runners_locked_and_sbom.sh b/tests/e2e/333_named_runners_locked_and_sbom.sh index 2b658805d..7efedbc41 100755 --- a/tests/e2e/333_named_runners_locked_and_sbom.sh +++ b/tests/e2e/333_named_runners_locked_and_sbom.sh @@ -101,6 +101,21 @@ esac echo " ok --locked names the package that moved and both versions" mv mcpp.lock.bak mcpp.lock +# A LOCK THAT IS NOT THERE IS NOT A LOCK THAT DIFFERS. The refusal says what to +# do about it, and writes no lock for the next run to pass against. +mv mcpp.lock mcpp.lock.bak +if out=$("$MCPP" build --locked 2>&1); then + echo "FAIL: --locked accepted a project with no mcpp.lock" + echo "$out" | tail -3; exit 1 +fi +case "$out" in + *"no readable mcpp.lock"*) ;; + *) echo "FAIL: the refusal does not say the lock is missing"; echo "$out"; exit 1 ;; +esac +test ! -e mcpp.lock || { echo "FAIL: a refused --locked wrote mcpp.lock"; exit 1; } +mv mcpp.lock.bak mcpp.lock +echo " ok --locked says a missing lock is missing" + # ── E: the bill of materials describes the RECORDED resolution ───────────── "$MCPP" emit sbom -o sbom.json >/dev/null 2>&1 || { echo "FAIL: mcpp emit sbom"; exit 1; } python3 - <<'PY' || exit 1 @@ -135,6 +150,18 @@ print(" ok sbom reads the lock rather than re-resolving") PY mv mcpp.lock.bak mcpp.lock +# NOTHING RECORDED IS SAID ON THE NARRATION STREAM, AND THE DOCUMENT STAYS VALID. +# A root-only bill of materials is true of a project with no dependencies and +# false of one that was never built, and only the reader knows which this is. +mv mcpp.lock mcpp.lock.bak +"$MCPP" emit sbom > sbom3.json 2> sbom3.err || { echo "FAIL: mcpp emit sbom with no lock"; exit 1; } +grep -q 'no mcpp.lock' sbom3.err \ + || { echo "FAIL: sbom did not say that nothing was recorded"; cat sbom3.err; exit 1; } +python3 -c 'import json; assert json.load(open("sbom3.json"))["components"] == []' \ + || { echo "FAIL: sbom with no lock is not a root-only document"; exit 1; } +mv mcpp.lock.bak mcpp.lock +echo " ok sbom says when no lock records the resolution" + # ── B: one board package, two environments, chosen by the consumer ───────── # # THE EMULATOR/HARDWARE AXIS IS A FEATURE, NOT A FORK. A board reached