Skip to content

Commit 34f4da9

Browse files
committed
e2e: runtime_search_dir, the prepare role and the stamp rule (mcpp#701, mcpp#702)
779: rewritten for runtime_search_dir (renamed from ...runtime_library_dir): RUNPATH in build.ninja, mcpp run, a build.mcpp cache-hit replay, mcpp pack, and a dependency's declaration reaching a consumer's executable through the same LinkGlobal merge. 790: a prepare action's command writes a header and a shared library into a directory whose contents build.mcpp never names; a unit that includes the header compiles and a program linked against the library runs through runtime_search_dir on the first build; the second build runs nothing; after an input of the action changes, it runs once and then not again. 791: a prepare action whose command creates nothing fails naming its declared directory. The stamp is declared beside that directory, not inside it -- ninja creates a declared output's own parent directory before running any edge, so a stamp nested inside the directory would make it "exist" regardless of what the command did. 792: a build program whose rerun_if_changed or rerun_if_changed_glob names a path inside a prepare directory is warned, naming both paths; a re-run input outside any such directory is the silent control. 793: an unknown action role is refused, naming the value and the five roles; each real role, including a real object action, is the control. 794/795: the R1' Windows leg -- a DLL in a declared runtime search directory is found by `mcpp run` through PATH and placed beside the packed program by `mcpp pack`. 794 requires windows and runs on Windows CI; this host cannot run it. 795 exercises the identical property through mingw-cross + wine, which this host can and does run, giving this round a passing reading before release.
1 parent f4151de commit 34f4da9

7 files changed

Lines changed: 773 additions & 12 deletions

‎tests/e2e/779_a_build_program_declares_a_runtime_search_dir.sh‎

Lines changed: 104 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
#!/usr/bin/env bash
22
# requires: pack gcc
3-
# 779_a_build_program_declares_a_runtime_library_dir.sh --
4-
# `mcpp::runtime_library_dir(dir)`: the build-program form of `[runtime]
5-
# library_dirs` (docs/04 §2.11). A dependency that brings a prebuilt shared
6-
# library (a vcpkg prefix's `bin/`, a Qt SDK's `bin/`) knows where it lives
7-
# only at build-program time, and the manifest key cannot be computed -- it is
8-
# a fixed TOML array. This is the directive that closes that gap.
3+
# 779_a_build_program_declares_a_runtime_search_dir.sh --
4+
# `mcpp::runtime_search_dir(dir)`: the build-program form of
5+
# `runtime_search_dirs` (docs/04 §2.11). A dependency that brings a prebuilt
6+
# shared library (a vcpkg prefix's `bin/`, a Qt SDK's `bin/`) knows where it
7+
# lives only at build-program time, and the manifest key cannot be computed --
8+
# it is a fixed TOML array. This is the directive that closes that gap
9+
# (design §5.4 R1', mcpp#701/#702).
910
#
1011
# What this holds, each with the wrong answer it excludes:
1112
#
@@ -22,10 +23,24 @@
2223
# `mcpp run` working -- the same replay criterion `mcpp:deploy=` is held
2324
# to in 651_a_build_program_deploys_what_it_generated.sh.
2425
# 4. `mcpp pack --format dir` STAGES IT into the bundle's `lib/` -- the
25-
# same closure search `[runtime] library_dirs` already feeds. Checked
26+
# same closure search `runtime_search_dirs` already feeds. Checked
2627
# LAST: `mcpp pack` builds under a different profile and would otherwise
2728
# invalidate the `dev`-profile build.mcpp cache the replay check (3)
2829
# depends on.
30+
# 5. A DEPENDENCY'S DECLARATION REACHES THE CONSUMER'S EXECUTABLE: the
31+
# directive is `Scope::LinkGlobal`, folded into `LinkIntent
32+
# .runtimeSearchDirs` by the SAME per-package merge (`resolve_runtime_
33+
# contract`, plan.cppm) the manifest key goes through, so a path
34+
# dependency that calls `runtime_search_dir` in its OWN build.mcpp makes
35+
# its consumer's binary run without the consumer naming the directory at
36+
# all.
37+
#
38+
# It lands DIRECTLY on `LinkIntent::runtimeSearchDirs`, not the retiring
39+
# `RuntimeConfig::libraryDirs` (docs/04 §2.11) -- #702 as first written joined
40+
# the wrong field (design §5.3 point 1); nothing here would tell them apart
41+
# if it had not been fixed, since every reader below is fed FROM
42+
# `runtimeSearchDirs` either way once the manifest key itself is also routed
43+
# there.
2944
set -e
3045

3146
TMP=$(mktemp -d)
@@ -66,7 +81,7 @@ EOF
6681

6782
# The directive under test. `link_lib`/`link_search` also come from the
6883
# program, on purpose: the point is a dependency the build.mcpp DISCOVERS,
69-
# not one written into mcpp.toml by hand -- the case `[runtime] library_dirs`
84+
# not one written into mcpp.toml by hand -- the case `runtime_search_dirs`
7085
# cannot cover.
7186
cat > app/build.mcpp <<'EOF'
7287
import mcpp;
@@ -75,7 +90,7 @@ int main() {
7590
const std::string root = mcpp::manifest_dir();
7691
mcpp::link_lib("runtime_plugin");
7792
mcpp::link_search((root + "/rtlib").c_str());
78-
mcpp::runtime_library_dir((root + "/rtlib").c_str());
93+
mcpp::runtime_search_dir((root + "/rtlib").c_str());
7994
return 0;
8095
}
8196
EOF
@@ -90,7 +105,7 @@ G=$(find_graph)
90105
[ -n "$G" ] || fail "no build.ninja" b1.log
91106
RTDIR=$(realpath rtlib)
92107
grep -F -- "-Wl,-rpath,$RTDIR" "$G" >/dev/null \
93-
|| fail "runtime-library-dir directive missing from RUNPATH intent" "$G"
108+
|| fail "runtime-search-dir directive missing from RUNPATH intent" "$G"
94109

95110
# ── 2. mcpp run finds the library only through that RUNPATH ────────────────
96111
"$MCPP" run > run1.log 2>&1 \
@@ -116,10 +131,87 @@ grep -F -- "-Wl,-rpath,$RTDIR" "$G2" >/dev/null \
116131
# ── 4. mcpp pack --format dir stages it into the bundle's lib/ ─────────────
117132
"$MCPP" pack --format dir > pack1.log 2>&1 || fail "pack --format dir failed" pack1.log
118133
STAGED=$(find target/dist -name 'libruntime_plugin.so' | head -1)
119-
[ -n "$STAGED" ] || fail "the runtime-library-dir closure was not staged by mcpp pack" pack1.log
134+
[ -n "$STAGED" ] || fail "the runtime-search-dir closure was not staged by mcpp pack" pack1.log
120135
case "$STAGED" in
121136
*/lib/libruntime_plugin.so) : ;;
122137
*) fail "the staged library is not under the packed bundle's lib/" pack1.log ;;
123138
esac
124139

125-
echo "PASS: 779_a_build_program_declares_a_runtime_library_dir"
140+
cd "$TMP"
141+
142+
# ── 5. a dependency's declaration reaches the consumer's executable ────────
143+
# `libdep` is a path dependency whose OWN build.mcpp declares the plugin's
144+
# directory; `consumer` names none of this -- no `link_search`, no
145+
# `runtime_search_dir`, not even `link_lib` in its own manifest or program --
146+
# and still runs, because the directive is LinkGlobal and the merge that
147+
# folds a dependency's LinkIntent into its consumer's is the SAME one the
148+
# manifest key goes through (plan.cppm ~:911-948).
149+
mkdir -p libdep/src libdep/plugin
150+
151+
cat > libdep/plugin/dep_plugin.c <<'EOF'
152+
int dep_plugin_answer(void) { return 7; }
153+
EOF
154+
gcc -shared -fPIC libdep/plugin/dep_plugin.c -o libdep/plugin/libdep_plugin.so
155+
156+
cat > libdep/src/lib.cpp <<'EOF'
157+
extern "C" int dep_plugin_answer();
158+
int libdep_touch() { return dep_plugin_answer(); }
159+
EOF
160+
161+
cat > libdep/mcpp.toml <<'EOF'
162+
[package]
163+
name = "libdep"
164+
version = "0.1.0"
165+
166+
[modules]
167+
sources = ["src/**/*.cpp"]
168+
169+
[targets.libdep]
170+
kind = "lib"
171+
EOF
172+
173+
cat > libdep/build.mcpp <<'EOF'
174+
import mcpp;
175+
#include <string>
176+
int main() {
177+
const std::string root = mcpp::manifest_dir();
178+
mcpp::link_lib("dep_plugin");
179+
mcpp::link_search((root + "/plugin").c_str());
180+
mcpp::runtime_search_dir((root + "/plugin").c_str());
181+
return 0;
182+
}
183+
EOF
184+
185+
mkdir -p consumer/src
186+
cat > consumer/src/main.cpp <<'EOF'
187+
extern "C" int dep_plugin_answer();
188+
int main() { return dep_plugin_answer() == 7 ? 0 : 1; }
189+
EOF
190+
191+
cat > consumer/mcpp.toml <<'EOF'
192+
[package]
193+
name = "consumer"
194+
version = "0.1.0"
195+
196+
[toolchain]
197+
linux = "gcc@16.1.0"
198+
199+
[dependencies]
200+
libdep = { path = "../libdep" }
201+
202+
[targets.consumer]
203+
kind = "bin"
204+
main = "src/main.cpp"
205+
EOF
206+
207+
cd consumer
208+
DEPDIR=$(realpath ../libdep/plugin)
209+
"$MCPP" build > b3.log 2>&1 || fail "consumer build failed" b3.log
210+
G3=$(find_graph)
211+
[ -n "$G3" ] || fail "no build.ninja for consumer" b3.log
212+
grep -F -- "-Wl,-rpath,$DEPDIR" "$G3" >/dev/null \
213+
|| fail "the dependency's runtime-search-dir did not reach the consumer's link" "$G3"
214+
"$MCPP" run > run3.log 2>&1 \
215+
|| fail "consumer run failed to find the dependency's plugin" run3.log b3.log
216+
217+
echo "PASS: 779_a_build_program_declares_a_runtime_search_dir"
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc
3+
# 790_a_prepare_action_populates_an_unknown_directory.sh — the `prepare`
4+
# role (design §5.4 P, mcpp#701/#702, SPEC-007 R3.3): construction whose file
5+
# names are not known when the build program runs, because they are decided
6+
# by a command it merely invokes (a vcpkg/CMake install, an SDK unpack) —
7+
# `install.sh` here, standing in for one of those. The build program declares
8+
# only the DIRECTORY (`a.output_dir`); the header and the shared library
9+
# inside it are named by the script, never by build.mcpp.
10+
#
11+
# What this holds:
12+
#
13+
# 1. ON THE FIRST BUILD, a unit that `#include`s a header the action wrote
14+
# compiles, and the program — linked against a library the SAME action
15+
# wrote, found only through `runtime_search_dir` — runs and gets the
16+
# right answer. Neither the header nor the library exists before this
17+
# build starts; both exist only because the compile and the link waited
18+
# for the action (design §5.4 P's ordering: every compile edge and every
19+
# link edge of the declaring package).
20+
# 2. A SECOND BUILD RUNS NOTHING: no compile, no link, no action — a plain
21+
# "up to date" from ninja's own dependency tracking on unchanged inputs.
22+
# 3. AFTER AN INPUT OF THE ACTION CHANGES, it runs ONCE — and the build
23+
# after THAT does not run it again (R2, design §5.4: the engine touches
24+
# the action's stamp on every success, whether or not the command wrote
25+
# it, so the stamp is newer than the input from that point on).
26+
set -e
27+
28+
TMP=$(mktemp -d)
29+
trap "rm -rf $TMP" EXIT
30+
mkdir -p "$TMP/prep/src"
31+
cd "$TMP/prep"
32+
33+
cat > mcpp.toml <<'EOF'
34+
[package]
35+
name = "prep"
36+
version = "0.1.0"
37+
38+
[toolchain]
39+
linux = "gcc@16.1.0"
40+
41+
[targets.prep]
42+
kind = "bin"
43+
main = "src/main.cpp"
44+
EOF
45+
46+
# The command a real rule package would run (vcpkg, a CMake `install`
47+
# target). It decides the file NAMES; build.mcpp never sees them, only the
48+
# directory they land in ($1).
49+
cat > install.sh <<'EOF'
50+
#!/usr/bin/env bash
51+
set -e
52+
PREFIX="$1"
53+
mkdir -p "$PREFIX/include" "$PREFIX/lib"
54+
cat > "$PREFIX/include/greet.h" <<HDR
55+
#ifdef __cplusplus
56+
extern "C" {
57+
#endif
58+
int prep_greet(void);
59+
#ifdef __cplusplus
60+
}
61+
#endif
62+
HDR
63+
cat > "$PREFIX/lib/greet.c" <<SRC
64+
int prep_greet(void) { return 99; }
65+
SRC
66+
gcc -shared -fPIC "$PREFIX/lib/greet.c" -o "$PREFIX/lib/libprepgreet.so"
67+
EOF
68+
chmod +x install.sh
69+
70+
# The action's declared INPUT that changes in step 3 below. Its content is
71+
# never read by install.sh — it only has to be a file ninja can hash, the
72+
# same way a vcpkg manifest's checksum would gate a real installation.
73+
echo "recipe v1" > recipe.txt
74+
75+
cat > src/main.cpp <<'EOF'
76+
#include <greet.h>
77+
int main() { return prep_greet() == 99 ? 0 : 1; }
78+
EOF
79+
80+
cat > build.mcpp <<'EOF'
81+
import mcpp;
82+
#include <string>
83+
int main() {
84+
const std::string root = mcpp::manifest_dir();
85+
const std::string prefix = std::string(mcpp::out_dir()) + "/prep-install";
86+
87+
mcpp::action a;
88+
a.id = "prep:install";
89+
a.role = mcpp::roles::prepare;
90+
// The stamp is a SIBLING of `prefix`, not nested inside it: ninja
91+
// creates a declared output's own parent directory before running any
92+
// edge, so a stamp inside `prefix` would make `prefix` exist regardless
93+
// of what install.sh did (see 791, which depends on the two staying
94+
// apart to mean anything).
95+
a.arg((root + "/install.sh").c_str()).arg(prefix.c_str())
96+
.input((root + "/install.sh").c_str())
97+
.input((root + "/recipe.txt").c_str())
98+
.output((prefix + ".stamp").c_str())
99+
.output_dir(prefix.c_str())
100+
.submit();
101+
102+
mcpp::include_dir((prefix + "/include").c_str());
103+
mcpp::link_lib("prepgreet");
104+
mcpp::link_search((prefix + "/lib").c_str());
105+
mcpp::runtime_search_dir((prefix + "/lib").c_str());
106+
return 0;
107+
}
108+
EOF
109+
110+
MCPP="${MCPP:-mcpp}"
111+
ran_prepare() { grep -q 'PREPARE prep:install\|__action-stamp.*install\.sh' "$1"; }
112+
113+
# ── 1. first build: header compiles, program runs through runtime_search_dir
114+
"$MCPP" build -v > b1.log 2>&1 || { cat b1.log; echo "FAIL: first build failed"; exit 1; }
115+
ran_prepare b1.log || { cat b1.log; echo "FAIL: the prepare action did not run on the first build"; exit 1; }
116+
nj=$(find target -name build.ninja | head -1)
117+
[[ -n "$nj" ]] || { echo "FAIL: no build.ninja"; exit 1; }
118+
grep -qE '^build [^:]*(prep\.stamp|prepgreet)' "$nj" >/dev/null || true # sanity, not load-bearing
119+
out1="$("$MCPP" run 2>&1)" || { echo "FAIL: run failed after the first build: $out1"; exit 1; }
120+
121+
# ── 2. second build runs nothing at all ─────────────────────────────────────
122+
"$MCPP" build -v > b2.log 2>&1 || { cat b2.log; echo "FAIL: second build failed"; exit 1; }
123+
if ran_prepare b2.log; then
124+
cat b2.log; echo "FAIL: the prepare action re-ran on an unchanged second build"; exit 1
125+
fi
126+
if grep -qE '\bmain\.o\b' b2.log; then
127+
cat b2.log; echo "FAIL: the compile re-ran on an unchanged second build"; exit 1
128+
fi
129+
130+
# ── 3. an input changes: the action runs once, then not again ──────────────
131+
sleep 1
132+
echo "recipe v2" > recipe.txt
133+
"$MCPP" build -v > b3.log 2>&1 || { cat b3.log; echo "FAIL: third build (after the input changed) failed"; exit 1; }
134+
ran_prepare b3.log || { cat b3.log; echo "FAIL: the action did not re-run after its input changed"; exit 1; }
135+
136+
"$MCPP" build -v > b4.log 2>&1 || { cat b4.log; echo "FAIL: fourth build failed"; exit 1; }
137+
if ran_prepare b4.log; then
138+
cat b4.log
139+
echo "FAIL: the action re-ran on the build after it passed -- R2's stamp rule did not hold"
140+
exit 1
141+
fi
142+
out2="$("$MCPP" run 2>&1)" || { echo "FAIL: run failed after the recipe changed: $out2"; exit 1; }
143+
144+
echo "PASS: 790_a_prepare_action_populates_an_unknown_directory"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc
3+
# 791_a_prepare_action_that_creates_nothing_fails.sh — design §5.4 P, SPEC-007
4+
# R3.3: "when the command succeeds and the directory does not exist, the
5+
# engine writes no stamp and fails the edge, naming the directory." A
6+
# `prepare` action promises a directory the build reads BY DIRECTORY
7+
# (`include_dir`/`link_search`/`runtime_search_dir`); a command that exits 0
8+
# without populating it is indistinguishable from one that silently did
9+
# nothing, so this cannot be a warning the way an incomplete environment is
10+
# (R1.2) -- nothing downstream can read a directory that was never created.
11+
set -e
12+
13+
TMP=$(mktemp -d)
14+
trap "rm -rf $TMP" EXIT
15+
mkdir -p "$TMP/app/src"
16+
cd "$TMP/app"
17+
18+
cat > mcpp.toml <<'EOF'
19+
[package]
20+
name = "app"
21+
version = "0.1.0"
22+
EOF
23+
printf '#include <cstdio>\nint main(){ std::printf("ok\\n"); }\n' > src/main.cpp
24+
25+
cat > build.mcpp <<'EOF'
26+
import mcpp;
27+
#include <string>
28+
int main() {
29+
const std::string prefix = std::string(mcpp::out_dir()) + "/never-created";
30+
mcpp::action a;
31+
a.id = "empty:prepare";
32+
a.role = mcpp::roles::prepare;
33+
// The stamp lives BESIDE `prefix`, not inside it: ninja creates a
34+
// declared output's own parent directory before running any edge, so a
35+
// stamp nested inside `prefix` would make `prefix` exist by the time the
36+
// engine checks for it regardless of what the command did, and this
37+
// test would defend nothing.
38+
a.arg("true")
39+
.output((prefix + ".stamp").c_str())
40+
.output_dir(prefix.c_str())
41+
.submit();
42+
return 0;
43+
}
44+
EOF
45+
46+
MCPP="${MCPP:-mcpp}"
47+
if "$MCPP" build > b.log 2>&1; then
48+
cat b.log
49+
echo "FAIL: a prepare action whose command created nothing let the build succeed"
50+
exit 1
51+
fi
52+
53+
grep -qF "never-created" b.log \
54+
|| { cat b.log; echo "FAIL: the failure does not name the declared directory"; exit 1; }
55+
grep -qi "does not exist" b.log \
56+
|| { cat b.log; echo "FAIL: the failure does not say the directory does not exist"; exit 1; }
57+
58+
# No half-satisfied edge: the stamp itself must not exist either, or a later
59+
# build would read a check that "passed" without its post-condition holding.
60+
if find target -name 'never-created.stamp' | grep -q .; then
61+
echo "FAIL: a stamp was written despite the missing directory"; exit 1
62+
fi
63+
64+
echo "PASS: 791_a_prepare_action_that_creates_nothing_fails"

0 commit comments

Comments
 (0)