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