Skip to content

Commit 068f23e

Browse files
committed
Merge W3: bounded object addresses, a short tool scratch, extended-length paths (#641 item 3)
2 parents b156481 + 094195f commit 068f23e

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
@@ -1387,7 +1387,12 @@ make_plan(const mcpp::manifest::Manifest& manifest,
13871387
// match would file the member's sources under the outer package. Index 0 is
13881388
// the root project; `packages.size()` means "outside every known root",
13891389
// which is treated as root-owned and never cached.
1390-
auto owner_of = [&](const std::filesystem::path& src) -> std::size_t {
1390+
//
1391+
// `container_of` keeps the distinction `owner_of` folds away: no package
1392+
// contains the source at all. The object address of a source outside its
1393+
// declaring package needs it (see `outside_prefix` below).
1394+
auto container_of = [&](const std::filesystem::path& src)
1395+
-> std::optional<std::size_t> {
13911396
std::size_t best = 0;
13921397
std::size_t bestLen = 0;
13931398
bool found = false;
@@ -1399,7 +1404,11 @@ make_plan(const mcpp::manifest::Manifest& manifest,
13991404
auto len = packages[p].root.generic_string().size();
14001405
if (!found || len > bestLen) { best = p; bestLen = len; found = true; }
14011406
}
1402-
return found ? best : 0;
1407+
if (!found) return std::nullopt;
1408+
return best;
1409+
};
1410+
auto owner_of = [&](const std::filesystem::path& src) -> std::size_t {
1411+
return container_of(src).value_or(0);
14031412
};
14041413

14051414
std::set<std::filesystem::path> scannedSources;
@@ -1469,6 +1478,47 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14691478
return pkg.empty() ? safe
14701479
: std::filesystem::path(sanitize(pkg)) / safe;
14711480
};
1481+
// A SOURCE OUTSIDE ITS DECLARING PACKAGE IS ADDRESSED BY WHERE IT IS, NOT
1482+
// BY HOW FAR IT IS FROM THAT PACKAGE (mcpp#641, item 3).
1483+
//
1484+
// `relPath` is relative to the package whose manifest or build program
1485+
// named the source. When a build program names a file under another
1486+
// package's root, mirroring that relPath spells one `__up` per directory
1487+
// between the two roots, so the address grew with a distance that has
1488+
// nothing to do with the file. Reported on windows-2022: a host tool's
1489+
// `.ddi` reached 271 characters, 90 of them this mirror, and `mcpp dyndep`
1490+
// could not open it.
1491+
//
1492+
// The address is instead the owning package's slug under a `__pkg`
1493+
// component, followed by the path inside that package; a source that no
1494+
// package contains is filed under `__ext/<hash of its directory>`. Both
1495+
// are downward and shell-safe by construction, and the marker components
1496+
// keep them apart from the declaring package's own mirrored directories.
1497+
// A source inside its declaring package does not reach this function, so
1498+
// its address is unchanged.
1499+
auto escapes_declaring_package = [](const std::filesystem::path& relPath) {
1500+
if (relPath.empty()) return false;
1501+
if (relPath.is_absolute() || relPath.has_root_name()) return true;
1502+
return *relPath.begin() == "..";
1503+
};
1504+
auto outside_prefix = [&](const std::filesystem::path& src)
1505+
-> std::filesystem::path {
1506+
if (auto c = container_of(src)) {
1507+
std::error_code ec;
1508+
auto rel = std::filesystem::relative(src, packages[*c].root, ec);
1509+
if (!ec && !rel.empty())
1510+
return std::filesystem::path("__pkg")
1511+
/ sanitize(qualified_package_name(packages[*c].manifest))
1512+
/ safe_object_prefix({}, rel.parent_path());
1513+
}
1514+
// The identity of a directory, not its spelling on this code page
1515+
// (see the path narrowing rule in mcpp-contributing).
1516+
const auto dir = src.parent_path().lexically_normal().generic_u8string();
1517+
const auto digest = mcpp::toolchain::hash_string(std::string_view(
1518+
reinterpret_cast<const char*>(dir.data()), dir.size()));
1519+
return std::filesystem::path("__ext") / digest.substr(0, 8);
1520+
};
1521+
14721522
// mcpp#233/#240/#344: the single source of truth for a compile unit's
14731523
// object addresses — scanned units AND the synthesized entry main go
14741524
// through here, so neither the link input nor the cache address can
@@ -1483,12 +1533,20 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14831533
std::size_t owner) -> ObjectAddress
14841534
{
14851535
const auto fname = object_filename_for(src, objExt);
1536+
const bool escapes = escapes_declaring_package(relPath);
14861537
if (owner == 0) {
14871538
// Root project: never cached, historical layout preserved.
1488-
if (rootBasenameCount[fname] > 1)
1539+
if (rootBasenameCount[fname] > 1) {
1540+
if (escapes)
1541+
return { std::filesystem::path("obj")
1542+
/ (pkg.empty() ? std::filesystem::path{}
1543+
: std::filesystem::path(sanitize(pkg)))
1544+
/ outside_prefix(src) / fname,
1545+
{} };
14891546
return { std::filesystem::path("obj")
14901547
/ safe_object_prefix(pkg, relPath.parent_path()) / fname,
14911548
{} };
1549+
}
14921550
return { std::filesystem::path("obj") / fname, {} };
14931551
}
14941552

@@ -1498,7 +1556,8 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14981556
auto mirrored = safe_object_prefix({}, relPath.parent_path()) / fname;
14991557

15001558
ObjectAddress addr;
1501-
addr.object = std::filesystem::path("obj") / slug / mirrored;
1559+
addr.object = std::filesystem::path("obj") / slug
1560+
/ (escapes ? outside_prefix(src) / fname : mirrored);
15021561

15031562
// The cache address additionally has to be MACHINE-independent: another
15041563
// 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
@@ -9349,8 +9349,13 @@ prepare_build(bool print_fingerprint,
93499349
// Hashed rather than random so a re-run reuses its own
93509350
// scratch (ninja stays incremental if the publish step
93519351
// never got to delete it).
9352-
sub.work_dir = entry / std::format("build-{}",
9353-
mcpp::toolchain::hash_string(workRoot.string()));
9352+
//
9353+
// Beside the entries rather than inside one: every
9354+
// directory name of the entry is repeated in each object
9355+
// path the sub-build writes, and on Windows those paths
9356+
// crossed the 260-character limit (mcpp#641, item 3).
9357+
sub.work_dir = mcpp::build::tool_store::scratch_dir(
9358+
cacheRoot, entry, workRoot);
93549359
sub.target_triple = ""; // HOST — the whole point
93559360
sub.profile = "release";
93569361
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;
@@ -1005,7 +1006,9 @@ int run(int argc, char** argv) {
10051006
if (r != 0) return r;
10061007
for (auto const& s : stamps) {
10071008
std::error_code ec;
1008-
std::filesystem::path p{s};
1009+
// Relative to the build directory, which can be deep enough to
1010+
// take the stamp past the Windows path limit (mcpp#641, item 3).
1011+
const auto p = mcpp::platform::fs::extended_length(std::filesystem::path{s});
10091012
if (!p.parent_path().empty())
10101013
std::filesystem::create_directories(p.parent_path(), ec);
10111014
if (std::filesystem::exists(p, ec)) continue;

0 commit comments

Comments
 (0)