Skip to content

Commit c576628

Browse files
fix: a source whose main no command compiles is no beginning of a program; the quoted-key warning names the key
Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 154fac0 commit c576628

7 files changed

Lines changed: 256 additions & 5 deletions

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@
103103

104104
### Fixed
105105

106+
- A dependency's source whose `main` is compiled only under a switch no command
107+
defines -- zlib's `crc32.c` (`#ifdef MAKECRCH`), QuickJS's `libregexp.c`
108+
(`#ifdef TEST`) -- is linked into a test that has its own `main`. Which
109+
sources begin a program is read from the branches the preprocessor may
110+
compile: a name the compiler or platform predefines is unknown, any other is
111+
undefined; 2026.11.7.2 left such a source out, and the test's link failed
112+
with undefined references (measured in Sunrisepeak/mui, #874; e2e 1037 L).
113+
- The warning on a quoted dotted dependency key names the key to write,
114+
`xrender.graph = ...`, not an example (e2e 1032).
106115
- A configuration group's lock keeps the entries of the other groups (#903;
107116
e2e 1027).
108117
- A project's state directory `.mcpp/` ignores itself in a repository (e2e 1028).

‎src/build/plan.cppm‎

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,6 +1609,204 @@ bool path_is_under_any(const std::filesystem::path& p,
16091609
return false;
16101610
}
16111611

1612+
// A preprocessor condition's value as far as a source's text decides it: true,
1613+
// false, or unknown (nullopt). A name the compiler or the platform predefines
1614+
// (one beginning with `_`) is unknown; every other name is not defined, as no
1615+
// command defines the switches a library guards its own tools with (zlib's
1616+
// MAKECRCH, QuickJS's TEST).
1617+
namespace main_scan {
1618+
1619+
struct Expr {
1620+
std::string_view text;
1621+
std::size_t at = 0;
1622+
bool failed = false;
1623+
1624+
void blank() { while (at < text.size() && (text[at] == ' ' || text[at] == '\t')) ++at; }
1625+
bool take(std::string_view op) {
1626+
blank();
1627+
if (text.substr(at, op.size()) != op) return false;
1628+
at += op.size();
1629+
return true;
1630+
}
1631+
std::string name() {
1632+
blank();
1633+
std::size_t from = at;
1634+
while (at < text.size() && (std::isalnum(static_cast<unsigned char>(text[at])) || text[at] == '_')) ++at;
1635+
return std::string(text.substr(from, at - from));
1636+
}
1637+
static bool reserved(std::string_view n) { return !n.empty() && n.front() == '_'; }
1638+
1639+
std::optional<long long> primary() {
1640+
blank();
1641+
if (take("(")) {
1642+
auto v = ternary();
1643+
if (!take(")")) failed = true;
1644+
return v;
1645+
}
1646+
if (take("!")) {
1647+
auto v = primary();
1648+
return v ? std::optional<long long>(!*v) : std::nullopt;
1649+
}
1650+
if (take("-")) {
1651+
auto v = primary();
1652+
return v ? std::optional<long long>(-*v) : std::nullopt;
1653+
}
1654+
if (at < text.size() && std::isdigit(static_cast<unsigned char>(text[at]))) {
1655+
long long v = 0;
1656+
const bool hex = text.substr(at, 2) == "0x" || text.substr(at, 2) == "0X";
1657+
if (hex) at += 2;
1658+
const auto from = at;
1659+
while (at < text.size() && std::isxdigit(static_cast<unsigned char>(text[at]))) ++at;
1660+
std::from_chars(text.data() + from, text.data() + at, v, hex ? 16 : 10);
1661+
while (at < text.size() && std::isalpha(static_cast<unsigned char>(text[at]))) ++at; // 1L, 2u
1662+
return v;
1663+
}
1664+
const auto n = name();
1665+
if (n.empty()) { failed = true; return std::nullopt; }
1666+
if (n == "defined") {
1667+
const bool paren = take("(");
1668+
const auto m = name();
1669+
if (paren && !take(")")) failed = true;
1670+
return reserved(m) ? std::nullopt : std::optional<long long>(0);
1671+
}
1672+
blank();
1673+
if (at < text.size() && text[at] == '(') { // a function-like macro: its value is the macro's
1674+
int depth = 0;
1675+
do {
1676+
depth += text[at] == '(' ? 1 : text[at] == ')' ? -1 : 0;
1677+
++at;
1678+
} while (at < text.size() && depth > 0);
1679+
return std::nullopt;
1680+
}
1681+
return reserved(n) ? std::nullopt : std::optional<long long>(0);
1682+
}
1683+
template <class Next, class Apply>
1684+
std::optional<long long> binary(Next next, std::initializer_list<std::string_view> ops, Apply apply) {
1685+
auto lhs = next();
1686+
for (bool more = true; more && !failed;) {
1687+
more = false;
1688+
for (auto op : ops) {
1689+
blank();
1690+
// `&&` is not `&`, `<=` is not `<`: the longer operators are listed first.
1691+
if (!take(op)) continue;
1692+
lhs = apply(op, lhs, next());
1693+
more = true;
1694+
break;
1695+
}
1696+
}
1697+
return lhs;
1698+
}
1699+
static std::optional<long long> arith(std::string_view op, std::optional<long long> a, std::optional<long long> b) {
1700+
if (!a || !b) return std::nullopt;
1701+
if (op == "*") return *a * *b;
1702+
if (op == "/") return *b ? std::optional<long long>(*a / *b) : std::nullopt;
1703+
if (op == "+") return *a + *b;
1704+
if (op == "-") return *a - *b;
1705+
if (op == "<=") return *a <= *b;
1706+
if (op == ">=") return *a >= *b;
1707+
if (op == "<") return *a < *b;
1708+
if (op == ">") return *a > *b;
1709+
if (op == "==") return *a == *b;
1710+
return *a != *b;
1711+
}
1712+
std::optional<long long> product() { return binary([&] { return primary(); }, {"*", "/"}, arith); }
1713+
std::optional<long long> sum() { return binary([&] { return product(); }, {"+", "-"}, arith); }
1714+
std::optional<long long> relation() { return binary([&] { return sum(); }, {"<=", ">=", "<", ">"}, arith); }
1715+
std::optional<long long> equality() { return binary([&] { return relation(); }, {"==", "!="}, arith); }
1716+
std::optional<long long> conjunction() {
1717+
return binary([&] { return equality(); }, {"&&"}, [](auto, auto a, auto b) -> std::optional<long long> {
1718+
if ((a && !*a) || (b && !*b)) return 0;
1719+
if (a && b) return 1;
1720+
return std::nullopt;
1721+
});
1722+
}
1723+
std::optional<long long> disjunction() {
1724+
return binary([&] { return conjunction(); }, {"||"}, [](auto, auto a, auto b) -> std::optional<long long> {
1725+
if ((a && *a) || (b && *b)) return 1;
1726+
if (a && b) return 0;
1727+
return std::nullopt;
1728+
});
1729+
}
1730+
std::optional<long long> ternary() {
1731+
auto c = disjunction();
1732+
if (!take("?")) return c;
1733+
auto t = ternary();
1734+
if (!take(":")) failed = true;
1735+
auto f = ternary();
1736+
if (!c) return t == f ? t : std::nullopt;
1737+
return *c ? t : f;
1738+
}
1739+
};
1740+
1741+
// The value of `#if <text>`; unknown when the text is not an expression this reads.
1742+
std::optional<bool> condition(std::string_view text) {
1743+
Expr e{text};
1744+
auto v = e.ternary();
1745+
e.blank();
1746+
if (e.failed || e.at != text.size() || !v) return std::nullopt;
1747+
return *v != 0;
1748+
}
1749+
1750+
// One `#if` group: whether its enclosing region may be compiled, whether a
1751+
// branch of it was taken for certain, and whether one may have been.
1752+
struct Group {
1753+
bool parent = true;
1754+
bool certainlyTaken = false;
1755+
bool current = true; // the current branch may be compiled
1756+
};
1757+
1758+
// The code of a source the preprocessor may compile: the lines of branches
1759+
// whose conditions are not false, directives dropped.
1760+
std::string compiled_lines(std::string_view code) {
1761+
std::string out;
1762+
std::vector<Group> groups;
1763+
auto active = [&] { return groups.empty() || (groups.back().parent && groups.back().current); };
1764+
auto open = [&](std::optional<bool> c) {
1765+
const bool parent = active();
1766+
groups.push_back({parent, c.value_or(false), c.value_or(true)});
1767+
};
1768+
auto next_branch = [&](std::optional<bool> c) {
1769+
if (groups.empty()) return;
1770+
auto& g = groups.back();
1771+
g.current = !g.certainlyTaken && c.value_or(true);
1772+
g.certainlyTaken = g.certainlyTaken || c.value_or(false);
1773+
};
1774+
std::size_t at = 0;
1775+
while (at < code.size()) {
1776+
auto end = code.find('\n', at);
1777+
if (end == std::string_view::npos) end = code.size();
1778+
std::string line(code.substr(at, end - at));
1779+
at = end + 1;
1780+
while (!line.empty() && line.back() == '\\' && at < code.size()) { // a continued directive
1781+
line.pop_back();
1782+
end = code.find('\n', at);
1783+
if (end == std::string_view::npos) end = code.size();
1784+
line += code.substr(at, end - at);
1785+
at = end + 1;
1786+
}
1787+
const auto first = line.find_first_not_of(" \t");
1788+
if (first == std::string::npos || line[first] != '#') {
1789+
if (active()) out += line + '\n';
1790+
continue;
1791+
}
1792+
Expr d{std::string_view(line).substr(first + 1)};
1793+
const auto word = d.name();
1794+
const auto rest = std::string_view(line).substr(first + 1 + d.at);
1795+
if (word == "if") open(condition(rest));
1796+
else if (word == "ifdef" || word == "ifndef") {
1797+
Expr n{rest};
1798+
const auto m = n.name();
1799+
std::optional<bool> isDefined = Expr::reserved(m) ? std::nullopt : std::optional<bool>(false);
1800+
open(word == "ifdef" ? isDefined : (isDefined ? std::optional<bool>(!*isDefined) : std::nullopt));
1801+
} else if (word == "elif") next_branch(condition(rest));
1802+
else if (word == "else") next_branch(true);
1803+
else if (word == "endif" && !groups.empty()) groups.pop_back();
1804+
}
1805+
return out;
1806+
}
1807+
1808+
} // namespace main_scan
1809+
16121810
bool source_defines_main(const std::filesystem::path& src) {
16131811
std::ifstream is(src);
16141812
if (!is) return false;
@@ -1648,6 +1846,7 @@ bool source_defines_main(const std::filesystem::path& src) {
16481846
break;
16491847
}
16501848
}
1849+
code = main_scan::compiled_lines(code);
16511850
auto isws = [](char c) {
16521851
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
16531852
};

‎src/build/prepare/graph.cpp‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,8 +671,9 @@ step4b_resolve_identity(PrepareState& state, WorklistItemCtx& ctx) {
671671
// COMPAT(quoted-dotted-dependency-key): one TOML key, read as the selector its dots spell (mcpp#904);
672672
// not reported where the quoted key is the only spelling of a package beside its parent or child.
673673
if (!spec.quotingRequired)
674-
state.reportCompatUse("quoted-dotted-dependency-key", packageIndexForConsumer(item.consumerDepIndex),
675-
std::format("{} \"{}\"", spec.declaredIn, name));
674+
state.reportCompatUseInstead("quoted-dotted-dependency-key", packageIndexForConsumer(item.consumerDepIndex),
675+
std::format("{} \"{}\"", spec.declaredIn, name),
676+
std::format("the key unquoted, `{} = ...`", name));
676677
spec.candidates = {{
677678
.namespace_ = spec.namespace_,
678679
.shortName = spec.shortName,

‎src/build/prepare/graph_closures.cpp‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ void step4b_define_lookup_closures(PrepareState& state) {
107107
return !mcpp::build::path_is_under_any(pkg.root, state.storeRoots)
108108
&& !mcpp::build::path_is_under_any(pkg.root, std::vector{mcpp::home::root()});
109109
};
110-
state.reportCompatUse = [&](std::string_view id, std::size_t i, std::string detail) {
110+
state.reportCompatUseInstead = [&](std::string_view id, std::size_t i, std::string detail,
111+
std::string instead) {
111112
auto const* d = mcpp::pm::compat::find_deprecation(id);
112113
if (!d) return;
113114
const bool warn = state.editablePackage(i);
@@ -119,7 +120,10 @@ void step4b_define_lookup_closures(PrepareState& state) {
119120
if (!warn) return;
120121
mcpp::diag::warning(d->domain, std::format(
121122
"{}: {} is {}. Write {} instead.", where, detail, mcpp::pm::compat::deprecation_clause(*d),
122-
d->canonical));
123+
instead.empty() ? std::string(d->canonical) : instead));
124+
};
125+
state.reportCompatUse = [&](std::string_view id, std::size_t i, std::string detail) {
126+
state.reportCompatUseInstead(id, i, std::move(detail), {});
123127
};
124128

125129

‎src/build/prepare/state.cppm‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,10 @@ struct PrepareState {
755755
// recorded for `mcpp why compat` either way.
756756
std::function<void(std::string_view id, std::size_t i, std::string detail)>
757757
reportCompatUse;
758+
// The same, naming what to write instead in the use's own words (the
759+
// registry's replacement is an example).
760+
std::function<void(std::string_view id, std::size_t i, std::string detail, std::string instead)>
761+
reportCompatUseInstead;
758762
// Whether the manifest of package `i` is one the person running the build
759763
// can edit (SPEC-008 §4: a diagnostic is given to the package that can act
760764
// on it).

‎tests/e2e/1032_a_quoted_dotted_dependency_key_is_deprecated.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ printf '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\n"acme.l
3131
"$MCPP" build > quoted.log 2>&1 || fail "the quoted key no longer builds" quoted.log
3232
grep -q '"acme.lib" is deprecated since mcpp 2026.11.8.1 and removed from mcpp 2027.2' quoted.log \
3333
|| fail "no deprecation warning for the quoted key" quoted.log
34-
grep -q 'mcpplibs.cmdline = "0.0.2"' quoted.log || fail "the warning does not show the unquoted spelling" quoted.log
34+
grep -q 'Write the key unquoted, `acme.lib = ...` instead' quoted.log || fail "the warning does not show the unquoted spelling of its own key" quoted.log
3535
"$MCPP" why compat > why.log 2>&1 || true
3636
grep -q 'quoted-dotted-dependency-key: \[dependencies\] "acme.lib"' why.log || fail "mcpp why compat does not list the use" why.log
3737

‎tests/e2e/1037_a_skeleton_tests_with_mtest_and_a_test_needs_no_main.sh‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
# K. mtest reaches the test units only: the skeleton's `mcpp build` and `mcpp test` share one
2525
# build directory, switching between them compiles the program's source once, its command
2626
# carries no `__OPENKAL__`, and the compile database holds the test.
27+
# L. A dev-dependency's source whose `main` is compiled only under a switch no command defines
28+
# (zlib's `#ifdef MAKECRCH`) is linked into a test that has its own `main`; one whose `main`
29+
# is in the branch an unknown condition leaves (gtest_main.cc's `#else`) still begins a test.
2730
set -e
2831
TMP=$(mktemp -d)
2932
trap 'rm -rf "$TMP"' EXIT
@@ -189,4 +192,35 @@ for e in json.load(open(sys.argv[1])):
189192
sys.exit(1)
190193
PY
191194

195+
# L
196+
mkdir -p lib-tools/src lapp/tests
197+
cat > lib-tools/mcpp.toml <<'TOML'
198+
[package]
199+
name = "lib-tools"
200+
version = "0.1.0"
201+
202+
[build]
203+
sources = ["src/*.c"]
204+
TOML
205+
cat > lib-tools/src/table.c <<'C'
206+
int table_value(void) { return 7; }
207+
#ifdef MAKE_TABLE
208+
#include <stdio.h>
209+
int main(void) { printf("%d\n", table_value()); return 0; }
210+
#endif
211+
C
212+
cat > lapp/mcpp.toml <<'TOML'
213+
[package]
214+
name = "lapp"
215+
version = "0.1.0"
216+
217+
[dev-dependencies]
218+
lib-tools = { path = "../lib-tools" }
219+
TOML
220+
printf 'extern "C" int table_value(void);
221+
int main() { return table_value() == 7 ? 0 : 1; }
222+
' > lapp/tests/own.cpp
223+
(cd lapp && "$MCPP" test > ../l.log 2>&1) || fail "L: a source whose main no command compiles was left out of the link" l.log
224+
grep -q 'own \.\.\. ok' l.log || fail "L: the test did not pass" l.log
225+
192226
echo "PASS: 1037 a skeleton tests with mtest, and a test needs no main"

0 commit comments

Comments
 (0)