|
| 1 | +#!/usr/bin/env bash |
| 2 | +# 191_link_scale.sh — a link edge with more objects than a command line can hold. |
| 3 | +# |
| 4 | +# WHAT THIS COVERS THAT NOTHING ELSE DID |
| 5 | +# |
| 6 | +# mcpp#346: no CI job builds a package of the opencv/ffmpeg magnitude. Every |
| 7 | +# job builds either mcpp itself (tens of TUs) or a synthetic e2e project |
| 8 | +# (single digits). Link-line length, the response-file path, and ninja graph |
| 9 | +# size over a large object set therefore had ZERO coverage, and the whole |
| 10 | +# command-length defect family surfaced in the ecosystem rather than in CI: |
| 11 | +# |
| 12 | +# #274 ninja goals argv 50781 chars vs cmd.exe 8191 |
| 13 | +# #247 Windows CreateProcess 32 KiB |
| 14 | +# #345 POSIX MAX_ARG_STRLEN 128 KiB (ninja spawns `sh -c "<whole cmd>"`) |
| 15 | +# #360 link.exe LNK1170, response-file LINE capped at 128 KiB |
| 16 | +# |
| 17 | +# 190 asserts the SHAPE of the generated rule (`rspfile_content = $in_newline`, |
| 18 | +# 25 objects). This asserts the SCALE: it builds a link edge whose object list |
| 19 | +# does not fit in a command line on any supported platform, and requires it to |
| 20 | +# link and run. A regression that puts objects back on the command line fails |
| 21 | +# here for the same reason opencv-module failed in the ecosystem — except in |
| 22 | +# 20 seconds, with no external package, and with a diagnostic that names the |
| 23 | +# cause. |
| 24 | +# |
| 25 | +# WHY THE SIZE ASSERTION IS PART OF THE TEST |
| 26 | +# |
| 27 | +# The regime is what gives this test its value, and the regime depends on |
| 28 | +# incidental things — object naming, the disambiguation prefix, how many files |
| 29 | +# the loop below writes. If any of them shrinks the object list back under the |
| 30 | +# ceiling, the test would keep passing while covering nothing. So the response |
| 31 | +# file's size is asserted directly: below the ceiling the test reports that it |
| 32 | +# has stopped covering the axis, rather than passing quietly. |
| 33 | +# |
| 34 | +# VERIFIED TO FAIL WITHOUT THE FIX |
| 35 | +# |
| 36 | +# Reverting the generated `cxx_link` rule to its pre-#345 inline form on this |
| 37 | +# exact project reproduces the original symptom verbatim: |
| 38 | +# |
| 39 | +# ninja: fatal: posix_spawn: Argument list too long |
| 40 | +# |
| 41 | +# COST |
| 42 | +# |
| 43 | +# C sources, one trivial function each. Measured at 1.3s wall on a developer |
| 44 | +# machine (1400 TUs, parallel). Windows is the expensive leg — per-process |
| 45 | +# spawn cost dominates there — and is bounded by the suite's 600s per-test |
| 46 | +# timeout. |
| 47 | +# |
| 48 | +# The names are padded so each object path is ~100 bytes, which is what |
| 49 | +# reaches the 128 KiB ceiling at a file count this small: padding trades |
| 50 | +# compile time (expensive) for path length (free). Padding is bounded on the |
| 51 | +# other side by Windows MAX_PATH — 100 bytes relative, plus the temp directory |
| 52 | +# and the build directory, stays near 200 of the 260 available. |
| 53 | +set -e |
| 54 | + |
| 55 | +TMP=$(mktemp -d) |
| 56 | +trap "rm -rf $TMP" EXIT |
| 57 | +cd "$TMP" |
| 58 | + |
| 59 | +# ── The ceiling this test has to clear ──────────────────────────────────── |
| 60 | +# POSIX MAX_ARG_STRLEN, the largest of the command-line limits in |
| 61 | +# src/build/cmdlimits.cppm — clearing the largest clears all of them, so one |
| 62 | +# number works for every platform the suite runs on. |
| 63 | +CEILING=$((128 * 1024)) |
| 64 | + |
| 65 | +PAD=$(printf 'x%.0s' $(seq 1 88)) |
| 66 | +N=1400 |
| 67 | + |
| 68 | +mkdir -p scale/src |
| 69 | +cat > scale/mcpp.toml <<'EOF' |
| 70 | +[package] |
| 71 | +name = "scale" |
| 72 | +version = "0.1.0" |
| 73 | +
|
| 74 | +[build] |
| 75 | +c_standard = "c11" |
| 76 | +EOF |
| 77 | + |
| 78 | +i=1 |
| 79 | +while [ "$i" -le "$N" ]; do |
| 80 | + printf 'int f%04d(void) { return %d; }\n' "$i" "$i" > "scale/src/f${i}_${PAD}.c" |
| 81 | + i=$((i + 1)) |
| 82 | +done |
| 83 | +# The bin target is inferred from src/main.cpp; the objects it links are the C |
| 84 | +# TUs above. Referencing the FIRST and the LAST of them is what makes a |
| 85 | +# truncated object list a link error rather than a smaller binary: a response |
| 86 | +# file cut short at any point drops one of these two. |
| 87 | +cat > scale/src/main.cpp <<EOF |
| 88 | +extern "C" int f0001(void); |
| 89 | +extern "C" int f$(printf '%04d' "$N")(void); |
| 90 | +int main() { return (f0001() == 1 && f$(printf '%04d' "$N")() == $N) ? 0 : 1; } |
| 91 | +EOF |
| 92 | + |
| 93 | +cd scale |
| 94 | +"$MCPP" build > b.log 2>&1 || { tail -40 b.log; echo "FAIL: build $N objects"; exit 1; } |
| 95 | +echo " ok: built $((N + 1)) objects" |
| 96 | + |
| 97 | +ninja_file=$(find target -name build.ninja | head -1) |
| 98 | +[ -n "$ninja_file" ] || { echo "FAIL: no build.ninja"; exit 1; } |
| 99 | +bdir=$(dirname "$ninja_file") |
| 100 | +bin_rel=$(cd "$bdir" && ls bin/ 2>/dev/null | head -1) |
| 101 | +[ -n "$bin_rel" ] || { echo "FAIL: nothing was linked"; exit 1; } |
| 102 | + |
| 103 | +# Relink with the response file kept, so its real contents can be inspected — |
| 104 | +# the same technique as 190, at a size that matters. |
| 105 | +(cd "$bdir" && rm -f "bin/$bin_rel" && ninja -d keeprsp "bin/$bin_rel" > relink.log 2>&1) || { |
| 106 | + tail -20 "$bdir/relink.log"; echo "FAIL: relink under -d keeprsp"; exit 1; } |
| 107 | + |
| 108 | +rsp=$(find "$bdir" -name '*.rsp' | head -1) |
| 109 | +[ -n "$rsp" ] || { echo "FAIL: -d keeprsp left no response file"; exit 1; } |
| 110 | + |
| 111 | +bytes=$(wc -c < "$rsp" | tr -d ' ') |
| 112 | +objects=$(( $(wc -l < "$rsp" | tr -d ' ') + 1 )) |
| 113 | + |
| 114 | +# 1. NON-VACUITY: the object list must not fit in a command line. Without |
| 115 | +# this, everything below could pass on a link edge small enough that the |
| 116 | +# inline form would have worked too. |
| 117 | +[ "$bytes" -gt "$CEILING" ] || { |
| 118 | + echo "response file is $bytes bytes over $objects objects; the ceiling is $CEILING" |
| 119 | + echo "FAIL: this test no longer reaches the regime it exists to cover." |
| 120 | + echo " Raise N or PAD until the object list exceeds the ceiling again." |
| 121 | + exit 1; } |
| 122 | +echo " ok: object list is $bytes bytes ($objects objects), past the ${CEILING}-byte command-line ceiling" |
| 123 | + |
| 124 | +# 2. No single line approaches link.exe's per-line cap (the LNK1170 axis) — |
| 125 | +# asserted here at real scale rather than 190's 25 objects. |
| 126 | +longest=$(awk '{ if (length($0) > m) m = length($0) } END { print m+0 }' "$rsp") |
| 127 | +[ "$longest" -lt 4096 ] || { |
| 128 | + echo "FAIL: longest response-file line is $longest chars — objects are not one per line" |
| 129 | + exit 1; } |
| 130 | +echo " ok: longest response-file line $longest chars" |
| 131 | + |
| 132 | +# 3. The objects were really consumed. main.cpp calls into both ends of the |
| 133 | +# object list, so a truncated response file cannot reach this point — it |
| 134 | +# fails at link time with an undefined reference. Running the binary closes |
| 135 | +# the remaining gap: that the values arriving at runtime are the ones the |
| 136 | +# two TUs return. |
| 137 | +"./$bdir/bin/$bin_rel" || { echo "FAIL: linked binary did not run cleanly"; exit 1; } |
| 138 | +echo " ok: linked binary runs" |
| 139 | + |
| 140 | +echo "OK" |
0 commit comments