Skip to content

Commit 094195f

Browse files
committed
Bounded tool-build paths: owner-relative object addresses, a short sub-build scratch, extended-length opens on Windows (#641 item 3; M7, M8, M9)
M7. A source outside the package that declares it (a build program's mcpp::source under a dependency's root) is addressed as obj/<declaring>/__pkg/<owning package>/<path inside it>, or obj/<declaring>/__ext/<8-hex hash of its directory>/ when no package contains it. The mirror of the declaring-package-relative path, with one __up per level, is gone for these sources; sources inside their declaring package, root-owned flat addresses, the #233 census and backstop, and the cache address (packageRel) are unchanged. In the root census branch the escaping form applies only where the mirrored prefix was used (a basename collision), so the flat obj/<name> of a generated OUT_DIR source (#239) is unchanged. M8. The host-tool sub-build scratch moves from <entry>/build-<hash> to <cache>/tool/.build/<hash of entry and consuming work root> (tool_store::scratch_dir), keeping per-consumer isolation, re-run reuse and removal after publish; the entry layout is unchanged. M9. mcpp::platform::fs::extended_length (identity outside Windows; absolute, normal, backslash, \\?\ or \\?\UNC\ form on Windows, idempotent) with the pure spelling rule windows_extended_length_spelling. Call sites, enumerated from the $mcpp rules of ninja_backend.cppm and the __action-stamp wrapper: src/cli/cmd_build.cppm cmd_dyndep: --output; the --expect-* ddi read; emit_dyndep_single input; emit_dyndep_from_files inputs src/cli/cmd_build.cppm cmd_stage: source and --output (stage_file) src/cli/cmd_build.cppm cmd_bmi_equal: both paths (bmi_equivalent) src/cli/cmd_build.cppm cmd_coff_def: each object read; the .def write src/cli/cmd_build.cppm cmd_bmi_compile: --bmi --slot --sem --command-file --dep-from --dep-to (not --self, an executable) src/cli/cmd_build.cppm cmd_bmi_supervise: --slot --token --command-file src/cli/cmd_build.cppm cmd_bmi_await: --slot --object src/cli.cppm __action-stamp: each stamp Downstream opens receive the converted paths. Two follow-ons: the detached compile's log opens with CreateFileW (CreateFileA does not honour the prefix past MAX_PATH), and CompileRequest::bmiTarget keeps ninja's spelling of the BMI for the copied depfile's target, which must not change. Tests: unit ObjectAddress.ASourceUnderAnotherPackageIsAddressedByThatPackage, ObjectAddress.AnUnownedEscapingSourceIsAddressedByAHashOfItsDirectory (both fail on the previous object_for), ToolStoreScratch.IsShortPerConsumerAndStable, PlatformFs.ExtendedLength* (four); e2e 698 (leg A fails on 2026.9.15.1 with obj/installer/__up/__up/dep/platform/impl/helper.o). Docs: 30-build-mcpp and its Chinese mirror.
1 parent 69fae26 commit 094195f

13 files changed

Lines changed: 675 additions & 30 deletions

‎docs/30-build-mcpp.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,20 @@ left `mcpp run` printing the previous answer until the version was bumped.
11711171
Entries accumulate as a tree is edited; `mcpp cache clean` empties the store,
11721172
which lives at `<mcpp cache dir>/tool/<index>/<name>@<version>[+<source>]/`.
11731173

1174+
**The paths a tool build writes are bounded (2026.9.15.2+).** The sub-build
1175+
runs in `<mcpp cache dir>/tool/.build/<hash>/`, a directory named by the entry
1176+
and the consuming project rather than nested inside the entry, and it is
1177+
removed once the binary is published. A source that a build program selects
1178+
from outside its own package, such as a file under a dependency's root named
1179+
with `mcpp::source`, is compiled to
1180+
`obj/<package>/__pkg/<owning package>/<path inside that package>`, or to
1181+
`obj/<package>/__ext/<hash of its directory>/` when no package contains it, so
1182+
its address does not grow with the distance between the two packages. On
1183+
Windows, the engine's own build steps (`dyndep`, `stage`, the BMI edges,
1184+
`coff-def` and check stamps) open their files through extended-length paths.
1185+
Before these changes a tool's scan output reached 271 characters on a Windows
1186+
runner and could not be read (#641).
1187+
11741188
The action's own tracking is separate from the store's key. An action that
11751189
declares the tool among its inputs re-runs when that file's bytes change,
11761190
measured by overwriting the binary in the store: the artifact followed.

‎docs/zh/30-build-mcpp.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,16 @@ grpc = { version = "1.83.0", tools = ["grpc_cpp_plugin"] }
984984
版本被抬高。树被编辑时条目会累积;`mcpp cache clean` 清空 store,路径是
985985
`<mcpp cache dir>/tool/<index>/<name>@<version>[+<source>]/`。
986986

987+
**工具构建写出的路径长度有界(2026.9.15.2+)。** 子构建在
988+
`<mcpp cache dir>/tool/.build/<hash>/` 中进行,这个目录由条目与消费工程命名,不嵌套在
989+
条目之内,二进制发布后即被删除。构建程序从自身包之外选入的源文件(例如用
990+
`mcpp::source` 选入依赖根下的文件)编译到
991+
`obj/<包>/__pkg/<所属包>/<在该包内的路径>`;没有任何包包含它时,编译到
992+
`obj/<包>/__ext/<其目录的哈希>/`。因此地址不随两个包之间的距离增长。在 Windows 上,
993+
引擎自己的构建步骤(`dyndep`、`stage`、BMI 相关的边、`coff-def` 与检查戳)以
994+
extended-length 路径打开文件。在这些改动之前,一个工具的扫描产物在 Windows runner
995+
上达到 271 个字符而无法读取(#641)。
996+
987997
action 自己的跟踪与 store 的键是两回事。把工具列进 action 输入的规则,会在那个文件的
988998
字节变化时重跑 —— 实测直接覆盖 store 里的二进制,产物随之改变。
989999

‎modules/buildmcpp/src/tool_store.cppm‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,20 @@ std::string tree_stamp(const std::filesystem::path& root);
122122

123123
// <cacheRoot>/tool/<index>/<pkg>@<ver>/<keyHex>/
124124
std::filesystem::path entry_dir(const std::filesystem::path& cacheRoot, const Key& k);
125+
126+
// <cacheRoot>/tool/.build/<16 hex>/ -- where one consumer builds one entry.
127+
//
128+
// A SIBLING OF THE ENTRIES, NOT A CHILD OF ONE. The sub-build's own layout
129+
// (`target/<triple>/<fingerprint>/obj/...`) is appended to this path, and the
130+
// entry directory already spends its length on the index, the package, its
131+
// version with the source stamp and the key: 133 characters on a Windows
132+
// runner before the sub-build added its own (mcpp#641, item 3). The scratch
133+
// needs none of those names, only an identity per (entry, consumer) pair, so
134+
// that two projects building one tool at once do not share a ninja tree and a
135+
// re-run reuses its own.
136+
std::filesystem::path scratch_dir(const std::filesystem::path& cacheRoot,
137+
const std::filesystem::path& entryDir,
138+
const std::filesystem::path& consumerRoot);
125139
std::filesystem::path bin_path(const std::filesystem::path& entryDir,
126140
std::string_view toolName,
127141
std::string_view exeSuffix);
@@ -228,6 +242,18 @@ fs::path entry_dir(const fs::path& cacheRoot, const Key& k) {
228242
/ key_hex(k);
229243
}
230244

245+
fs::path scratch_dir(const fs::path& cacheRoot, const fs::path& entryDir,
246+
const fs::path& consumerRoot) {
247+
// Identities, not narrow spellings: a home or project directory whose name
248+
// the Windows code page cannot spell must not throw here.
249+
const auto entry = entryDir.lexically_normal().generic_u8string();
250+
const auto consumer = consumerRoot.lexically_normal().generic_u8string();
251+
std::string joined(reinterpret_cast<const char*>(entry.data()), entry.size());
252+
joined += '\n';
253+
joined.append(reinterpret_cast<const char*>(consumer.data()), consumer.size());
254+
return cacheRoot / "tool" / ".build" / mcpp::toolchain::hash_string(joined);
255+
}
256+
231257
fs::path bin_path(const fs::path& entryDir, std::string_view toolName,
232258
std::string_view exeSuffix) {
233259
return entryDir / "bin" / (std::string(toolName) + std::string(exeSuffix));

‎modules/platform/src/fs.cppm‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ std::filesystem::path self_exe_path();
5252
// POSIX: `command -v <name>`
5353
std::optional<std::filesystem::path> which(std::string_view binary_name);
5454

55+
// ── extended_length ───────────────────────────────────────────────────────
56+
//
57+
// The form of a path that a file operation may open whatever its length.
58+
//
59+
// A Win32 path that is not in the extended-length form is limited to 259
60+
// characters, and a relative path counts the working directory against that
61+
// limit. The engine subcommands ninja invokes (`mcpp dyndep`, `mcpp stage`,
62+
// the `bmi-*` edges, `coff-def`, the check stamps) receive paths relative to a
63+
// build directory, and in a host-tool sub-build those paths crossed the limit:
64+
// `clang-scan-deps` wrote a `.ddi` that `mcpp dyndep` then could not read
65+
// (mcpp#641, item 3).
66+
//
67+
// Windows the absolute, lexically normal path with backslashes, prefixed
68+
// `\\?\` (a UNC path `\\server\share\x` becomes
69+
// `\\?\UNC\server\share\x`); a path already in that form is
70+
// returned unchanged.
71+
// POSIX the argument, unchanged. No limit of this kind exists.
72+
std::filesystem::path extended_length(const std::filesystem::path& p);
73+
74+
// The Windows spelling rule of `extended_length`, as a pure function of an
75+
// absolute, generic (forward-slash or backslash) path string, so that the rule
76+
// is tested on every host. An empty or relative input is returned unchanged.
77+
std::string windows_extended_length_spelling(std::string_view absolutePath);
78+
5579
// 不先删除目标文件,避免发布失败时丢失最后一份可用的编译数据库。
5680
bool replace_file(const std::filesystem::path& source,
5781
const std::filesystem::path& destination,
@@ -141,6 +165,71 @@ std::optional<std::filesystem::path> which(std::string_view binary_name) {
141165
return std::filesystem::path(out);
142166
}
143167

168+
std::string windows_extended_length_spelling(std::string_view in) {
169+
std::string s(in);
170+
for (auto& c : s) if (c == '/') c = '\\';
171+
// Already extended-length (`\\?\`) or a device path (`\\.\`): the
172+
// prefix disables every further interpretation, so nothing is rewritten.
173+
if (s.starts_with("\\\\?\\") || s.starts_with("\\\\.\\")) return s;
174+
175+
std::string prefix;
176+
std::string rest;
177+
if (s.starts_with("\\\\")) {
178+
prefix = "\\\\?\\UNC\\";
179+
rest = s.substr(2);
180+
} else if (s.size() >= 3 && std::isalpha(static_cast<unsigned char>(s[0]))
181+
&& s[1] == ':' && s[2] == '\\') {
182+
prefix = "\\\\?\\";
183+
rest = s;
184+
} else {
185+
return std::string(in); // relative or rootless: not this function's input
186+
}
187+
188+
// The prefix turns off `.`/`..` processing, so the components are resolved
189+
// here, lexically. A `..` never climbs above the drive or the share.
190+
std::vector<std::string> parts;
191+
std::size_t keep = prefix.ends_with("UNC\\") ? 2 : 1; // share or drive
192+
std::size_t start = 0;
193+
while (start <= rest.size()) {
194+
auto end = rest.find('\\', start);
195+
if (end == std::string::npos) end = rest.size();
196+
std::string comp = rest.substr(start, end - start);
197+
start = end + 1;
198+
if (comp.empty() || comp == ".") continue;
199+
if (comp == "..") {
200+
if (parts.size() > keep) parts.pop_back();
201+
continue;
202+
}
203+
parts.push_back(std::move(comp));
204+
}
205+
std::string out = prefix;
206+
for (std::size_t i = 0; i < parts.size(); ++i) {
207+
if (i) out += '\\';
208+
out += parts[i];
209+
}
210+
// A bare drive keeps its root separator: `\\?\C:\`, not `\\?\C:`.
211+
if (parts.size() == 1 && keep == 1) out += '\\';
212+
return out;
213+
}
214+
215+
std::filesystem::path extended_length(const std::filesystem::path& p) {
216+
#if defined(_WIN32)
217+
if (p.empty()) return p;
218+
std::error_code ec;
219+
auto abs = std::filesystem::absolute(p, ec);
220+
if (ec) return p;
221+
// Through UTF-8, so that a name the ANSI code page cannot spell survives:
222+
// the path narrowing rule (mcpp-contributing) forbids `.string()` here.
223+
const auto u8 = abs.generic_u8string();
224+
std::string narrow(reinterpret_cast<const char*>(u8.data()), u8.size());
225+
const auto spelled = windows_extended_length_spelling(narrow);
226+
return std::filesystem::path(std::u8string(
227+
reinterpret_cast<const char8_t*>(spelled.data()), spelled.size()));
228+
#else
229+
return p;
230+
#endif
231+
}
232+
144233
bool replace_file(const std::filesystem::path& source,
145234
const std::filesystem::path& destination,
146235
std::error_code& ec) {

‎src/build/plan.cppm‎

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,7 +1360,12 @@ make_plan(const mcpp::manifest::Manifest& manifest,
13601360
// match would file the member's sources under the outer package. Index 0 is
13611361
// the root project; `packages.size()` means "outside every known root",
13621362
// which is treated as root-owned and never cached.
1363-
auto owner_of = [&](const std::filesystem::path& src) -> std::size_t {
1363+
//
1364+
// `container_of` keeps the distinction `owner_of` folds away: no package
1365+
// contains the source at all. The object address of a source outside its
1366+
// declaring package needs it (see `outside_prefix` below).
1367+
auto container_of = [&](const std::filesystem::path& src)
1368+
-> std::optional<std::size_t> {
13641369
std::size_t best = 0;
13651370
std::size_t bestLen = 0;
13661371
bool found = false;
@@ -1372,7 +1377,11 @@ make_plan(const mcpp::manifest::Manifest& manifest,
13721377
auto len = packages[p].root.generic_string().size();
13731378
if (!found || len > bestLen) { best = p; bestLen = len; found = true; }
13741379
}
1375-
return found ? best : 0;
1380+
if (!found) return std::nullopt;
1381+
return best;
1382+
};
1383+
auto owner_of = [&](const std::filesystem::path& src) -> std::size_t {
1384+
return container_of(src).value_or(0);
13761385
};
13771386

13781387
std::set<std::filesystem::path> scannedSources;
@@ -1442,6 +1451,47 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14421451
return pkg.empty() ? safe
14431452
: std::filesystem::path(sanitize(pkg)) / safe;
14441453
};
1454+
// A SOURCE OUTSIDE ITS DECLARING PACKAGE IS ADDRESSED BY WHERE IT IS, NOT
1455+
// BY HOW FAR IT IS FROM THAT PACKAGE (mcpp#641, item 3).
1456+
//
1457+
// `relPath` is relative to the package whose manifest or build program
1458+
// named the source. When a build program names a file under another
1459+
// package's root, mirroring that relPath spells one `__up` per directory
1460+
// between the two roots, so the address grew with a distance that has
1461+
// nothing to do with the file. Reported on windows-2022: a host tool's
1462+
// `.ddi` reached 271 characters, 90 of them this mirror, and `mcpp dyndep`
1463+
// could not open it.
1464+
//
1465+
// The address is instead the owning package's slug under a `__pkg`
1466+
// component, followed by the path inside that package; a source that no
1467+
// package contains is filed under `__ext/<hash of its directory>`. Both
1468+
// are downward and shell-safe by construction, and the marker components
1469+
// keep them apart from the declaring package's own mirrored directories.
1470+
// A source inside its declaring package does not reach this function, so
1471+
// its address is unchanged.
1472+
auto escapes_declaring_package = [](const std::filesystem::path& relPath) {
1473+
if (relPath.empty()) return false;
1474+
if (relPath.is_absolute() || relPath.has_root_name()) return true;
1475+
return *relPath.begin() == "..";
1476+
};
1477+
auto outside_prefix = [&](const std::filesystem::path& src)
1478+
-> std::filesystem::path {
1479+
if (auto c = container_of(src)) {
1480+
std::error_code ec;
1481+
auto rel = std::filesystem::relative(src, packages[*c].root, ec);
1482+
if (!ec && !rel.empty())
1483+
return std::filesystem::path("__pkg")
1484+
/ sanitize(qualified_package_name(packages[*c].manifest))
1485+
/ safe_object_prefix({}, rel.parent_path());
1486+
}
1487+
// The identity of a directory, not its spelling on this code page
1488+
// (see the path narrowing rule in mcpp-contributing).
1489+
const auto dir = src.parent_path().lexically_normal().generic_u8string();
1490+
const auto digest = mcpp::toolchain::hash_string(std::string_view(
1491+
reinterpret_cast<const char*>(dir.data()), dir.size()));
1492+
return std::filesystem::path("__ext") / digest.substr(0, 8);
1493+
};
1494+
14451495
// mcpp#233/#240/#344: the single source of truth for a compile unit's
14461496
// object addresses — scanned units AND the synthesized entry main go
14471497
// through here, so neither the link input nor the cache address can
@@ -1456,12 +1506,20 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14561506
std::size_t owner) -> ObjectAddress
14571507
{
14581508
const auto fname = object_filename_for(src, objExt);
1509+
const bool escapes = escapes_declaring_package(relPath);
14591510
if (owner == 0) {
14601511
// Root project: never cached, historical layout preserved.
1461-
if (rootBasenameCount[fname] > 1)
1512+
if (rootBasenameCount[fname] > 1) {
1513+
if (escapes)
1514+
return { std::filesystem::path("obj")
1515+
/ (pkg.empty() ? std::filesystem::path{}
1516+
: std::filesystem::path(sanitize(pkg)))
1517+
/ outside_prefix(src) / fname,
1518+
{} };
14621519
return { std::filesystem::path("obj")
14631520
/ safe_object_prefix(pkg, relPath.parent_path()) / fname,
14641521
{} };
1522+
}
14651523
return { std::filesystem::path("obj") / fname, {} };
14661524
}
14671525

@@ -1471,7 +1529,8 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14711529
auto mirrored = safe_object_prefix({}, relPath.parent_path()) / fname;
14721530

14731531
ObjectAddress addr;
1474-
addr.object = std::filesystem::path("obj") / slug / mirrored;
1532+
addr.object = std::filesystem::path("obj") / slug
1533+
/ (escapes ? outside_prefix(src) / fname : mirrored);
14751534

14761535
// The cache address additionally has to be MACHINE-independent: another
14771536
// machine computes the same key and reads the same entry. A relPath

‎src/build/prepare.cppm‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9355,8 +9355,13 @@ prepare_build(bool print_fingerprint,
93559355
// Hashed rather than random so a re-run reuses its own
93569356
// scratch (ninja stays incremental if the publish step
93579357
// never got to delete it).
9358-
sub.work_dir = entry / std::format("build-{}",
9359-
mcpp::toolchain::hash_string(workRoot.string()));
9358+
//
9359+
// Beside the entries rather than inside one: every
9360+
// directory name of the entry is repeated in each object
9361+
// path the sub-build writes, and on Windows those paths
9362+
// crossed the 260-character limit (mcpp#641, item 3).
9363+
sub.work_dir = mcpp::build::tool_store::scratch_dir(
9364+
cacheRoot, entry, workRoot);
93609365
sub.target_triple = ""; // HOST — the whole point
93619366
sub.profile = "release";
93629367
sub.cache_mode = overrides.cache_mode;

‎src/build/schedule/detach_codegen.cppm‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ struct CompileRequest {
123123
// itself reran.
124124
std::filesystem::path depFrom;
125125
std::filesystem::path depTo;
126+
// The BMI as ninja spells it, which is what the copied depfile must name.
127+
// `bmi` is the path this edge OPENS, and on Windows it is the absolute,
128+
// extended-length form (mcpp::platform::fs::extended_length); ninja treats
129+
// an edge whose depfile names anything but its own output spelling as
130+
// permanently dirty. Empty means `bmi` is already that spelling.
131+
std::string bmiTarget;
126132
};
127133

128134
// Phase 1 — returns 0 as soon as the BMI is published, leaving code generation
@@ -372,7 +378,10 @@ int run_to_completion(std::string_view command,
372378
line += command;
373379

374380
SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE};
375-
HANDLE log = ::CreateFileA(logPath.string().c_str(), GENERIC_WRITE,
381+
// The wide API: an extended-length log path (`bmi-supervise` receives one,
382+
// see mcpp::platform::fs::extended_length) is honoured only by it, and a
383+
// narrowed spelling would also throw on a name the code page cannot spell.
384+
HANDLE log = ::CreateFileW(logPath.c_str(), GENERIC_WRITE,
376385
FILE_SHARE_READ, &sa, CREATE_ALWAYS,
377386
FILE_ATTRIBUTE_NORMAL, nullptr);
378387
// The command, in the log, before it runs. Phase 2 replays this file, so a
@@ -523,7 +532,8 @@ int compile_release_at_bmi(const CompileRequest& req) {
523532
// For a unit with no previous BMI that reduces to "it now exists".
524533
if (!req.bmi.empty() && bmi_identity(req.bmi) != before) {
525534
settle_bmi(req.bmi);
526-
copy_first_rule(req.depFrom, req.depTo, req.bmi.string());
535+
copy_first_rule(req.depFrom, req.depTo,
536+
req.bmiTarget.empty() ? req.bmi.string() : req.bmiTarget);
527537
return 0; // importers may proceed
528538
}
529539
if (const auto rc = read_rc(req.slot)) {
@@ -553,7 +563,8 @@ int compile_release_at_bmi(const CompileRequest& req) {
553563
// in an importer — reproducible at `-j1`, so it was never a
554564
// race between compilers, only between this loop's two checks.
555565
settle_bmi(req.bmi);
556-
copy_first_rule(req.depFrom, req.depTo, req.bmi.string());
566+
copy_first_rule(req.depFrom, req.depTo,
567+
req.bmiTarget.empty() ? req.bmi.string() : req.bmiTarget);
557568
}
558569
return *rc;
559570
}

‎src/cli.cppm‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import mcpp.wire;
3232
import mcpp.cli.cmd_sbom;
3333
import mcpp.platform.env; // --offline → MCPP_OFFLINE
3434
import mcpp.platform.process; // __action-stamp runs the checked command
35+
import mcpp.platform.fs; // __action-stamp writes its stamps
3536
import mcpp.platform.runtime_search; // linker-wrapper path-injection opt-out
3637
import mcpp.ui;
3738
import mcpp.log;
@@ -1003,7 +1004,9 @@ int run(int argc, char** argv) {
10031004
if (r != 0) return r;
10041005
for (auto const& s : stamps) {
10051006
std::error_code ec;
1006-
std::filesystem::path p{s};
1007+
// Relative to the build directory, which can be deep enough to
1008+
// take the stamp past the Windows path limit (mcpp#641, item 3).
1009+
const auto p = mcpp::platform::fs::extended_length(std::filesystem::path{s});
10071010
if (!p.parent_path().empty())
10081011
std::filesystem::create_directories(p.parent_path(), ec);
10091012
if (std::filesystem::exists(p, ec)) continue;

0 commit comments

Comments
 (0)