Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/04-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ own — dependencies are [05](05-dependencies.md), features are

`mcpp.toml` is the project configuration file for the mcpp build tool, analogous to Cargo's `Cargo.toml` or Node's `package.json`. Place it in the project root; `mcpp build` discovers and reads it automatically.

The file is TOML. An inline table may span several lines, carry comments and
end with a comma after its last pair, as TOML 1.1 allows; each `key = value`
pair stays on one line (#928). The same holds for every TOML file mcpp reads
(`mcpp.lock`, `config.toml`, template metadata).

```toml
[dependencies]
widget = {
version = "2.0.0",
features = ["gui"], # a comment inside the table
}
```

## 1. Minimal Examples

mcpp is designed around **convention over configuration** — most fields have sensible defaults, so the simplest `mcpp.toml` is just a few lines:
Expand Down
3 changes: 2 additions & 1 deletion docs/09-commands-by-scenario.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ of any run that read such an index, including runs that then succeeded.
fails rather than fetch. `--locked` fails when resolution differs from
`mcpp.lock` instead of rewriting it, which is the shape a CI job wants.
`mcpp index pin <name> <rev>` records a commit for a custom index in
`mcpp.toml`; `mcpp index unpin` removes it.
`mcpp.toml`; `mcpp index unpin` removes it. Both edit an `[indices]` entry
written on one line, and refuse one whose inline table spans several lines.

## Download progress

Expand Down
12 changes: 12 additions & 0 deletions docs/zh/04-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
`mcpp.toml` 是 mcpp 构建工具的工程配置文件,类似于 Cargo 的 `Cargo.toml` 或
Node 的 `package.json`。把它放在工程根目录;`mcpp build` 会自动发现并读取它。

该文件是 TOML。内联表可以跨多行书写、包含注释,并在最后一对之后带一个逗号,
这与 TOML 1.1 的规定一致;每一对 `key = value` 仍写在一行之内(#928)。mcpp
读取的每个 TOML 文件(`mcpp.lock`、`config.toml`、模板元数据)都遵循同一规则。

```toml
[dependencies]
widget = {
version = "2.0.0",
features = ["gui"], # 表内的注释
}
```

## 1. 最小化示例

mcpp 按**约定优于配置**设计——大多数字段都有合理的默认值,因此最简单的
Expand Down
3 changes: 2 additions & 1 deletion docs/zh/09-commands-by-scenario.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ tip: the refreshed package index `mcpplibs` requires a newer mcpp; this run used
`--offline`(或 `MCPP_OFFLINE=1`)在单次调用中禁止网络,宁可失败也不拉取。
`--locked` 在解析结果与 `mcpp.lock` 不一致时失败,而不是改写它,这正是 CI
作业需要的形状。`mcpp index pin <name> <rev>` 把一个自定义索引的某个
commit 记进 `mcpp.toml`;`mcpp index unpin` 移除它。
commit 记进 `mcpp.toml`;`mcpp index unpin` 移除它。二者编辑写在一行之内的
`[indices]` 条目;内联表跨多行的条目会被拒绝。

## 下载进度

Expand Down
14 changes: 9 additions & 5 deletions modules/libs/src/toml.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// - Booleans: key = true / false
// - Integers
// - Arrays of strings: key = ["a", "b"]
// - Inline tables (rudimentary): key = { version = "..." }
// - Inline tables: key = { version = "..." }, across lines as TOML 1.1 allows
// Comments: # to end of line. Multiline strings ("""/''') supported; no datetime.

export module mcpp.libs.toml;
Expand Down Expand Up @@ -379,14 +379,18 @@ inline std::expected<Value, ParseError> read_array(Lexer& L) {
return Value{std::move(out)};
}

// `{ key = value, ... }`, as TOML 1.1 writes it: newlines and comments may
// stand between the braces and the pairs, and a comma may follow the last
// pair. A pair itself stays on one line (`key = value`), as at the top level.
inline std::expected<Value, ParseError> read_inline_table(Lexer& L) {
if (L.peek() != '{') return std::unexpected(ParseError{"expected '{'", L.position()});
const Position open = L.position();
L.advance();
Table tab;
L.skip_inline_whitespace();
if (L.peek() == '}') { L.advance(); return Value{tab}; }
while (true) {
L.skip_inline_whitespace();
L.skip_whitespace_and_comments();
if (L.peek() == '}') { L.advance(); break; }
if (L.eof()) return std::unexpected(ParseError{"unterminated inline table", open});
auto k = read_key(L);
if (!k) return std::unexpected(k.error());
L.skip_inline_whitespace();
Expand All @@ -398,7 +402,7 @@ inline std::expected<Value, ParseError> read_inline_table(Lexer& L) {
auto v = read_value(L);
if (!v) return std::unexpected(v.error());
tab[*k] = std::move(*v);
L.skip_inline_whitespace();
L.skip_whitespace_and_comments();
if (L.peek() == ',') { L.advance(); continue; }
if (L.peek() == '}') { L.advance(); break; }
return std::unexpected(ParseError{"expected ',' or '}' in inline table", L.position()});
Expand Down
35 changes: 34 additions & 1 deletion modules/manifest/src/toml.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,14 @@ std::expected<std::string, std::string>
upsert_dependency_text(std::string_view source,
const DependencyTextEdit& edit);

// Where the `key = value` line that begins at `lineStart` ends: the newline
// after its value, or the end of `text`. An inline table or an array may
// continue over the following lines (TOML 1.1 inline tables, any array), so
// an editor that replaced or erased the first line alone would leave the rest
// of the value behind. Brackets are counted outside strings and comments; the
// value ends at the first newline where none is open.
std::size_t assignment_end(std::string_view text, std::size_t lineStart);

// Every path the lib-root convention would accept, in extension-table order
// (`.cppm` first, then whatever `[build] module_extensions` declares). An
// explicit `[lib] path` collapses this to that one entry.
Expand Down Expand Up @@ -5774,6 +5782,31 @@ license = "Apache-2.0"
)", packageName);
}

std::size_t assignment_end(std::string_view text, std::size_t lineStart) {
int depth = 0;
char quote = '\0';
bool comment = false;
for (std::size_t i = lineStart; i < text.size(); ++i) {
const char c = text[i];
if (c == '\n') {
comment = false;
if (depth <= 0 && quote == '\0') return i;
continue;
}
if (comment) continue;
if (quote != '\0') {
if (c == '\\' && quote == '"') ++i;
else if (c == quote) quote = '\0';
continue;
}
if (c == '"' || c == '\'') quote = c;
else if (c == '#') comment = true;
else if (c == '{' || c == '[') ++depth;
else if (c == '}' || c == ']') --depth;
}
return text.size();
}

std::expected<std::string, std::string>
upsert_dependency_text(std::string_view source,
const DependencyTextEdit& edit) {
Expand Down Expand Up @@ -5916,7 +5949,7 @@ upsert_dependency_text(std::string_view source,
};
if (!line.starts_with('#')
&& (assignment_for(key) || assignment_for(quotedKey))) {
return std::pair{lineStart, lineEnd};
return std::pair{lineStart, assignment_end(text, lineStart)};
}
if (newline == std::string_view::npos
|| newline >= section.sectionEnd) break;
Expand Down
5 changes: 3 additions & 2 deletions src/pm/commands.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -420,11 +420,12 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) {
}

bool changed = false;
// The entry's whole value goes: an inline table written over several
// lines (TOML 1.1) ends where its braces close, not at the first newline.
auto erase_line_at = [&](std::size_t p) {
auto bol = text.rfind('\n', p);
auto eol = text.find('\n', p);
if (bol == std::string::npos) bol = 0; else ++bol;
if (eol == std::string::npos) eol = text.size();
const auto eol = mcpp::manifest::assignment_end(text, bol);
text.erase(bol, (eol - bol) + (eol < text.size() ? 1 : 0));
changed = true;
};
Expand Down
8 changes: 6 additions & 2 deletions src/pm/index_management.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,9 @@ export int index_pin(const std::string& name, std::string rev) {
// Inline table. Find the closing brace and insert/update rev.
auto braceClose = lineContent.find('}', braceOpen);
if (braceClose == std::string::npos) {
mcpp::ui::error("malformed inline table in mcpp.toml");
mcpp::ui::error(std::format(
"the inline table of index `{}` does not close on its line in mcpp.toml; "
"`mcpp index pin` edits an entry written on one line", name));
return 1;
}
auto tableContent = lineContent.substr(braceOpen + 1, braceClose - braceOpen - 1);
Expand Down Expand Up @@ -521,7 +523,9 @@ export int index_unpin(const std::string& name) {
if (braceOpen != std::string::npos) {
auto braceClose = lineContent.find('}', braceOpen);
if (braceClose == std::string::npos) {
mcpp::ui::error("malformed inline table in mcpp.toml");
mcpp::ui::error(std::format(
"the inline table of index `{}` does not close on its line in mcpp.toml; "
"`mcpp index unpin` edits an entry written on one line", name));
return 1;
}
auto tableContent = lineContent.substr(braceOpen + 1, braceClose - braceOpen - 1);
Expand Down
13 changes: 12 additions & 1 deletion tests/e2e/23_remove_update.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# requires:
# 23_remove_update.sh — M4 #3: mcpp remove / mcpp update.
# 23_remove_update.sh — M4 #3: mcpp remove / mcpp update; a multi-line entry
# is removed whole (#928).
set -e

TMP=$(mktemp -d)
Expand All @@ -17,6 +18,10 @@ cat >> mcpp.toml <<'EOF'

[dependencies]
foo = "1.0.0"
baz = {
version = "3.0.0", # an inline table over several lines (TOML 1.1)
features = ["x"],
}
bar = "2.0.0"
EOF

Expand All @@ -26,6 +31,12 @@ grep -q 'Removing' /tmp/_r.log || { cat /tmp/_r.log; exit 1; }
if grep -qE '^("foo"|foo) = ' mcpp.toml; then echo "foo not actually removed"; cat mcpp.toml; exit 1; fi
grep -qE '^("bar"|bar) = "2\.0\.0"' mcpp.toml || { echo "bar accidentally removed"; cat mcpp.toml; exit 1; }

# remove baz: its whole value goes, not only the line that opens it (#928)
"$MCPP" remove baz > /tmp/_r.log 2>&1
grep -q 'Removing' /tmp/_r.log || { cat /tmp/_r.log; exit 1; }
if grep -qE 'baz|3\.0\.0|features|^\}' mcpp.toml; then echo "baz not wholly removed"; cat mcpp.toml; exit 1; fi
grep -qE '^("bar"|bar) = "2\.0\.0"' mcpp.toml || { echo "bar accidentally removed"; cat mcpp.toml; exit 1; }

# remove non-existent → exit code 1
rc=0
"$MCPP" remove nonexistent > /tmp/_r2.log 2>&1 || rc=$?
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/test_manifest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,51 @@ widget = "1.0.0"
expect(ne(invalid.error().find("invalid manifest"), std::string::npos));
}};

// An entry written as an inline table over several lines (TOML 1.1) is
// replaced whole: replacing its first line alone left the rest of the old
// value behind, and the edit was refused as an invalid manifest.
UTest replaces_an_entry_written_over_several_lines{"ManifestEditor/ReplacesAnEntryWrittenOverSeveralLines", [] {
constexpr std::string_view source = R"([package]
name = "app"
version = "0.1.0"

[dependencies.acme]
widget = {
version = "1.0.0", # the brace } in a comment is no close
features = ["gui",
"a]b"],
}
gadget = "1.0.0"
)";
auto edited = mcpp::manifest::upsert_dependency_text(source, {
.namespace_ = "acme",
.shortName = "widget",
.version = "2.0.0",
});
if (auto&& failed = expect(fatal(edited.has_value())); !failed) failed << edited.error();
expect(ne(edited->find("[dependencies.acme]\nwidget = \"2.0.0\"\ngadget = \"1.0.0\"\n"), std::string::npos));
auto parsed = mcpp::manifest::parse_string(*edited);
if (auto&& failed = expect(fatal(parsed.has_value())); !failed) failed << parsed.error().format();
expect(fatal(eq(parsed->dependencies.size(), 2u)));
expect(eq(parsed->dependencies.at("acme.widget").version, "2.0.0"));
expect(eq(parsed->dependencies.at("acme.widget").features, std::vector<std::string>{}));
expect(eq(parsed->dependencies.at("acme.gadget").version, "1.0.0"));
}};

// The end of an assignment is the newline after its value, wherever the value
// closes: brackets inside strings and comments do not count.
UTest an_assignment_ends_where_its_value_closes{"ManifestEditor/AnAssignmentEndsWhereItsValueCloses", [] {
using mcpp::manifest::assignment_end;
constexpr std::string_view one = "a = \"1\"\nb = 2\n";
expect(eq(assignment_end(one, 0), one.find('\n')));
expect(eq(assignment_end(one, 8), one.size() - 1));

constexpr std::string_view many = "a = {\n x = \"}\", # ]\n y = ['{',\n \"\\\"[\"],\n}\nb = 2";
expect(eq(assignment_end(many, 0), many.find("\nb = 2")));
expect(eq(assignment_end("a = { x = 1 }", 0), 13u));
expect(eq(assignment_end("a = {\n", 0), 6u)); // never closes: the end of the text
}};

UTest multiple_entries_across_platforms{"ListXpkgVersions/MultipleEntriesAcrossPlatforms", [] {
constexpr auto src = R"(
package = {
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/test_toml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,50 @@ UTest a_unicode_escape_names_a_scalar_value{"Toml/AUnicodeEscapeNamesAScalarValu
if (auto&& failed = expect(ne(bad.error().message.find("\\uDFFF"), std::string::npos)); !failed) failed << bad.error().message;
}};

// TOML 1.1: an inline table may span lines, hold comments, and end with a
// comma. Each was refused with `expected key`, so the multi-line `runner = {...}`
// of docs/35 could not be read.
UTest an_inline_table_spans_lines{"Toml/AnInlineTableSpansLines", [] {
auto d = t::parse(R"(
runner = { # a comment after the brace
cmd = ["dirun", "--", "{}"],
hosts = ["linux-x86_64",
"macos-aarch64"],
# a comment line between pairs
env = { A = "1", B = { C = 2, }, },
}
after = true
)");
if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message;
expect(eq(*d->get_string_array("runner.cmd"), std::vector<std::string>({"dirun", "--", "{}"})));
expect(eq(*d->get_string_array("runner.hosts"), std::vector<std::string>({"linux-x86_64", "macos-aarch64"})));
expect(eq(d->get_string("runner.env.A").value_or(""), "1"));
expect(eq(d->get_int("runner.env.B.C").value_or(-1), 2));
expect(eq(d->get_bool("after").value_or(false), true));

d = t::parse("empty = {\n}\nx = { a = 1, }");
if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message;
expect(fatal(d->get_table("empty") != nullptr));
expect(d->get_table("empty")->empty());
expect(eq(d->get_int("x.a").value_or(-1), 1));
}};

// What TOML 1.1 still refuses in an inline table: a pair split across lines,
// a missing separator, an empty element, and a table that never closes.
UTest an_inline_table_keeps_each_pair_on_one_line{"Toml/AnInlineTableKeepsEachPairOnOneLine", [] {
expect(!(t::parse("x = { a\n= 1 }").has_value()));
expect(!(t::parse("x = { a =\n1 }").has_value()));
expect(!(t::parse("x = { a = 1\nb = 2 }").has_value()));
expect(!(t::parse("x = { a = 1,, }").has_value()));
expect(!(t::parse("x = { , }").has_value()));

auto open = t::parse("x = {\n a = 1,\n");
expect(fatal(!open.has_value()));
if (auto&& failed = expect(eq(open.error().message, "unterminated inline table")); !failed) failed << open.error().message;
expect(eq(open.error().where.line, 1u));
expect(eq(open.error().where.column, 5u));
}};

UTest reject_unterminated_string{"Toml/RejectUnterminatedString", [] {
auto d = t::parse(R"(x = "no end)");
expect(!d.has_value());
Expand Down
Loading