Skip to content

Commit 6dc3490

Browse files
authored
0.18.1: rules-qt and deps leave out another build system's output directory when they walk a source tree (#40)
1 parent 0601d4b commit 6dc3490

10 files changed

Lines changed: 145 additions & 21 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module name the member declares, and configures it there.
99

1010
```toml
1111
[build-dependencies.mcpp]
12-
plugins = { version = "0.18.0", features = ["rules-spirv"], host-module = true }
12+
plugins = { version = "0.18.1", features = ["rules-spirv"], host-module = true }
1313
```
1414

1515
```cpp

‎deps/deps.cppm‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ inline std::filesystem::path find_upward(std::filesystem::path start, std::strin
9999
}
100100

101101
// Every regular file under `dir`, for an action's inputs. Version-control and
102-
// build-output directories are skipped: they change on every build and are no
103-
// part of what the installer reads.
102+
// build-output directories are skipped, and so is the output directory of any
103+
// other build system (`mcpp::plugins::tree`, 0.18.1): they change on every
104+
// build and are no part of what the installer reads.
104105
inline std::vector<std::string> files_under(const std::filesystem::path& dir) {
105106
std::vector<std::string> out;
106107
std::error_code ec;
@@ -112,7 +113,8 @@ inline std::vector<std::string> files_under(const std::filesystem::path& dir) {
112113
const auto name = it->path().filename().string();
113114
if (it->is_directory(ec)) {
114115
if (name == ".git" || name == "target" || name == "out" || name == "build" ||
115-
name == "Install" || name == "vcpkg_installed")
116+
name == "Install" || name == "vcpkg_installed" ||
117+
mcpp::plugins::tree::build_output(it->path()))
116118
it.disable_recursion_pending();
117119
continue;
118120
}

‎docs/deps.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Module `mcpp.deps.archive`; engine floor: 2026.9.26.2.
2424

2525
```toml
2626
[build-dependencies.mcpp]
27-
plugins = { version = "0.18.0", features = ["deps-vcpkg"], host-module = true }
27+
plugins = { version = "0.18.1", features = ["deps-vcpkg"], host-module = true }
2828
```
2929

3030
```cpp
@@ -197,7 +197,13 @@ One `prepare` action, `cmake -P` over a script the member writes
197197
(`<out dir>/deps-cmake/<name>.cmake`), configures, builds and installs the
198198
subproject into `<out dir>/deps-cmake/<name>/install`, its declared output
199199
directory; its inputs are the script and the subproject's files, so an edit to
200-
the subproject rebuilds it. The prefix is mapped as `deps-vcpkg` maps its own.
200+
the subproject rebuilds it. The subproject's files leave out `.git`, `target`,
201+
`out`, `build`, `Install`, `vcpkg_installed`, and from 0.18.1 the output
202+
directory of another build system inside it: one holding `CMakeCache.txt` (an
203+
IDE's `cmake-build-debug`), `.qmake.stash`, a `CACHEDIR.TAG` with the standard
204+
signature, or a `<name>.tlog` directory (MSBuild). A build made there by hand
205+
changes neither the inputs nor the key. The prefix is mapped as `deps-vcpkg`
206+
maps its own.
201207
`layout` names install directories other than `include/`, `lib/` and `bin/`;
202208
`prefix_path` becomes `CMAKE_PREFIX_PATH` (`mcpp::rules::qt::root()` for a
203209
subproject that finds Qt); `cache_args` carries `-D…`, `-G …` and a toolchain

‎docs/plugin-development.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ mcpp-index refuses such a package.
5252

5353
```toml
5454
[build-dependencies.mcpp]
55-
plugins = { version = "0.18.0", features = ["plugins-core"], host-module = true, reexport = true }
55+
plugins = { version = "0.18.1", features = ["plugins-core"], host-module = true, reexport = true }
5656
```
5757

5858
`reexport = true` is needed when the plugin's consumers import L2 modules in

‎docs/rules-qt.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
```toml
88
[build-dependencies.mcpp]
9-
plugins = { version = "0.18.0", features = ["rules-qt"], host-module = true }
9+
plugins = { version = "0.18.1", features = ["rules-qt"], host-module = true }
1010

1111
# The SDK and its version are the project's declaration.
1212
[target.'cfg(any(windows, linux, macos))'.xlings.workspace]
@@ -69,7 +69,7 @@ A package that enables `rules-qt` only to import `mcpp.rules.qt`, and has no `bu
6969
| `private_modules` | modules whose private headers are included |
7070
| `moc`, `moc_headers` | `moc_scan::project_headers` (default) scans the package's headers by content; `moc_scan::listed` takes `moc_headers` only |
7171
| `forms`, `resources` | `.ui` and `.qrc` files beside those in `[build] sources` |
72-
| `i18n` | `.ts` files beside those in `[build] sources`; `update_sources` runs `lupdate` as an action whose output is the `.ts` it rewrites; `tr_function_alias`; `deploy_to` (default `translations`); `out_dir` (default `<out dir>/qt/translations`); `qt_languages`: the linked modules' catalogs combined by `lconvert` into `qt_<language>.qm`, the file windeployqt writes |
72+
| `i18n` | `.ts` files beside those in `[build] sources`; `update_sources` runs `lupdate` as an action whose output is the `.ts` it rewrites, reading `sources`, or when that is empty the package's C++ sources and headers, found by the walk that finds moc's headers; `tr_function_alias`; `deploy_to` (default `translations`); `out_dir` (default `<out dir>/qt/translations`); `qt_languages`: the linked modules' catalogs combined by `lconvert` into `qt_<language>.qm`, the file windeployqt writes |
7373
| `deploy_plugins` | plugin directories placed beside the program; default `platforms` |
7474
| `deploy_software_gl` | Windows: `opengl32sw.dll` and `d3dcompiler_47.dll` beside the program |
7575
| `root`, `extra_roots` | level 1 of the SDK lookup |
@@ -79,6 +79,7 @@ A package that enables `rules-qt` only to import `mcpp.rules.qt`, and has no `bu
7979
## Behaviour
8080

8181
- `moc` runs for every header under the package root that declares `Q_OBJECT`, `Q_GADGET` or `Q_NAMESPACE`, and for a source that includes its own `<stem>.moc`; `uic` for `.ui`, `rcc` for `.qrc`, `lrelease` for `.ts`. Each is a `role = "source"` action with declared inputs; the outputs are under `<out dir>/qt/`.
82+
- The walk of the package root that finds those headers, and `lupdate`'s sources, leaves out `target`, `.git`, `mcpp-generated`, `vcpkg_installed`, `node_modules`, and from 0.18.1 the output directory of another build system, known by what it leaves there: `CMakeCache.txt` (a CMake build directory), `.qmake.stash` (qmake), a `CACHEDIR.TAG` with the standard signature, or a `<name>.tlog` directory (an MSBuild intermediate directory such as `x64/Release`, where a Visual Studio project writes its moc and uic output). Such a directory is known by what it holds, never by its name, so an `x64/` of sources is read.
8283
- The SDK's `bin/` (Windows) or `lib/` is a runtime search directory: the program's run path, `mcpp run`'s load path, `mcpp pack`'s closure, and on Windows the Qt DLLs placed beside the program. The plugin directories `deploy_plugins` names are deployed beside the program.
8384
- Under an MSVC compiler the rule adds `/Zc:__cplusplus` and `/permissive-`; on Linux, `-fPIC`. On macOS the modules are frameworks under `lib/`, compiled with `-F<root>/lib` and linked by their binaries' full paths. A resource compiled into a static library is registered with `Q_INIT_RESOURCE(<stem>)`.
8485
- A missing SDK, module or tool is a warning: the plan states what it can, and the build is where the absence fails.

‎mcpp.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "plugins"
33
namespace = "mcpp"
4-
version = "0.18.0"
4+
version = "0.18.1"
55
# The first mcpp release that states the build information (protocol 14),
66
# places files in one edge, and renders structured diagnostics (mcpp#734).
77
mcpp = ">=2026.9.28.3"

‎rules/qt.cppm‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ inline std::string tool(std::span<const std::filesystem::path> roots, const char
187187
}
188188

189189
// Files under the package root with one of `exts`, skipping build output and
190-
// version control. Sorted, so the plan does not depend on directory order.
190+
// version control, and the output directories of another build system
191+
// (`mcpp::plugins::tree`, 0.18.1). Sorted, so the plan does not depend on
192+
// directory order.
191193
inline std::vector<std::filesystem::path> project_files(std::initializer_list<std::string_view> exts) {
192194
std::vector<std::filesystem::path> out;
193195
const std::filesystem::path root = mcpp::manifest_dir();
@@ -199,7 +201,8 @@ inline std::vector<std::filesystem::path> project_files(std::initializer_list<st
199201
const auto name = it->path().filename().string();
200202
if (it->is_directory(ec)) {
201203
if (name == "target" || name == ".git" || name == "mcpp-generated" ||
202-
name == "vcpkg_installed" || name == "node_modules")
204+
name == "vcpkg_installed" || name == "node_modules" ||
205+
mcpp::plugins::tree::build_output(it->path()))
203206
it.disable_recursion_pending();
204207
continue;
205208
}

‎src/plugins.cppm‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export namespace mcpp::plugins {
4949
//
5050
// One package, one version: the number lives in mcpp.toml, and the CI step
5151
// `the collection states its own version` compares the two.
52-
inline constexpr std::string_view version = "0.18.0";
52+
inline constexpr std::string_view version = "0.18.1";
5353

5454
} // namespace mcpp::plugins
5555

@@ -1448,3 +1448,47 @@ inline std::string module_root_for(std::string_view package_name,
14481448
}
14491449

14501450
} // namespace mcpp::plugins::surface
1451+
1452+
// mcpp::plugins::tree -- the directories a walk of a source tree leaves out.
1453+
//
1454+
// SHARED BECAUSE TWO MEMBERS WALK A SOURCE TREE BY DEFAULT. `rules-qt` finds
1455+
// the headers moc reads and the sources lupdate reads under the package, and
1456+
// `deps` takes every file of a subproject as an input of its installation, and
1457+
// `deps-cmake` as part of the installation's key. Neither may take what ANOTHER
1458+
// build system generated there: a Visual Studio project built in the same
1459+
// checkout leaves its moc and uic output under `x64/Release/`, and lupdate
1460+
// given those files rewrites the translations, and a CMake build directory an
1461+
// IDE made inside a subproject changes the key on every build.
1462+
//
1463+
// Such a directory is known by what its build system leaves in it, not by its
1464+
// name: `x64/` is as often a directory of architecture-specific sources.
1465+
export namespace mcpp::plugins::tree {
1466+
1467+
// Whether `dir` is an output directory of another build system:
1468+
//
1469+
// CMakeCache.txt a CMake build tree (an IDE's `cmake-build-*`, `out/build/*`)
1470+
// .qmake.stash a qmake build tree
1471+
// CACHEDIR.TAG a directory its tool tags as a cache (https://bford.info/cachedir/)
1472+
// <name>.tlog/ an MSBuild intermediate directory (`x64/Release/`), where a
1473+
// Visual Studio project writes the sources it generates
1474+
//
1475+
// A walk asks this of the directories under its root, never of the root: a
1476+
// subproject configured in its own source directory holds CMakeCache.txt too.
1477+
inline bool build_output(const std::filesystem::path& dir) {
1478+
namespace fs = std::filesystem;
1479+
std::error_code ec;
1480+
if (fs::is_regular_file(dir / "CMakeCache.txt", ec) || fs::is_regular_file(dir / ".qmake.stash", ec))
1481+
return true;
1482+
if (fs::is_regular_file(dir / "CACHEDIR.TAG", ec)) {
1483+
std::ifstream in(dir / "CACHEDIR.TAG", std::ios::binary);
1484+
std::string head(43, '\0');
1485+
in.read(head.data(), 43);
1486+
if (head == "Signature: 8a477f597d28d172789f06886806bc55") return true;
1487+
}
1488+
for (auto it = fs::directory_iterator(dir, fs::directory_options::skip_permission_denied, ec);
1489+
!ec && it != fs::directory_iterator(); it.increment(ec))
1490+
if (it->path().extension() == ".tlog" && it->is_directory(ec)) return true;
1491+
return false;
1492+
}
1493+
1494+
} // namespace mcpp::plugins::tree

‎tests/plugin-logic/build.mcpp‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// The deps members' decisions, one case per decision (mcpp#734, design §6).
1+
// The deps members' decisions, and the files rules-qt reads by default, one
2+
// case per decision (mcpp#734, design §6).
23
// Each case states a build context, runs the member, and reads the action it
34
// declared and the files it wrote. Nothing is installed or run.
45
import std;
@@ -8,6 +9,7 @@ import mcpp.plugins.toolset;
89
import mcpp.deps; // the 0.16.0 names, for the compatibility case
910
import mcpp.deps.vcpkg;
1011
import mcpp.deps.cmake;
12+
import mcpp.rules.qt;
1113

1214
namespace t = mcpp::plugins::testing;
1315
namespace ts = mcpp::plugins::toolset;
@@ -35,6 +37,28 @@ t::context with_subproject(t::context c) {
3537
.file("pkg/greet/CMakeLists.txt", "project(greet C)\n");
3638
}
3739

40+
// Output directories of other build systems inside a tree `at`: an MSBuild
41+
// intermediate directory, a CMake build directory, a tagged cache directory and
42+
// one whose tag lacks the signature. `x64/` itself is a directory of sources.
43+
t::context with_foreign_output(t::context c, const std::string& at, const std::string& ext) {
44+
return c.file(at + "/x64/simd" + ext, "class Simd { Q_OBJECT };\n")
45+
.file(at + "/x64/Release/app.tlog/app.lastbuildstate")
46+
.file(at + "/x64/Release/qt/uic/ui_form" + ext, "class Ui { Q_OBJECT };\n")
47+
.file(at + "/cmake-build-debug/CMakeCache.txt")
48+
.file(at + "/cmake-build-debug/gen/stale" + ext, "class Stale { Q_OBJECT };\n")
49+
.file(at + "/cache/CACHEDIR.TAG", "Signature: 8a477f597d28d172789f06886806bc55\n")
50+
.file(at + "/cache/blob" + ext)
51+
.file(at + "/notag/CACHEDIR.TAG", "not a tag\n")
52+
.file(at + "/notag/kept" + ext);
53+
}
54+
55+
// A stand-in Qt SDK: the tools rules-qt looks for, never run.
56+
t::context with_qt(t::context c) {
57+
for (auto const* tool : { "moc", "lupdate", "lrelease" })
58+
c = c.file(std::string("qt/bin/") + tool).file(std::string("qt/bin/") + tool + ".exe");
59+
return c.file("pkg/widget.h", "class Widget { Q_OBJECT };\n").file("pkg/app.ts");
60+
}
61+
3862
int vcpkg_use(mcpp::deps::vcpkg::options o = {}) {
3963
o.libraries = {"fmt"};
4064
return mcpp::deps::vcpkg::use(o) ? 0 : 1;
@@ -378,6 +402,50 @@ int main(int argc, char** argv) {
378402
"the script does not consult the cache");
379403
} },
380404

405+
{ "cmake: another build system's output directory in a subproject is not part of the key",
406+
with_foreign_output(with_subproject(t::row::windows_managed()), "pkg/greet", ".c"),
407+
[] { return cmake_use(); },
408+
[](const t::result& r, t::checker& c) {
409+
std::string script;
410+
for (auto const& f : r.files_under("out/deps-cmake/greet"))
411+
if (f.ends_with("greet.cmake")) script = r.file(f);
412+
const auto from = script.find("set(listed [==[");
413+
const auto listed = from == std::string::npos ? std::string()
414+
: script.substr(from, script.find("]==])", from) - from);
415+
c.expect(listed.find("\nx64/simd.c\n") != std::string::npos, "a directory named x64 is listed");
416+
c.expect(listed.find("\nnotag/kept.c\n") != std::string::npos,
417+
"a CACHEDIR.TAG without the signature marks nothing");
418+
c.expect(listed.find("x64/Release") == std::string::npos, "an MSBuild intermediate directory is not");
419+
c.expect(listed.find("cmake-build-debug") == std::string::npos, "a CMake build directory is not");
420+
c.expect(listed.find("cache/") == std::string::npos, "a tagged cache directory is not");
421+
} },
422+
423+
// ── rules-qt ────────────────────────────────────────────────────────
424+
{ "qt: moc and lupdate read the package's files, not another build system's output",
425+
with_foreign_output(with_qt(t::row::linux_gcc()), "pkg", ".h"),
426+
[] {
427+
mcpp::rules::qt::options o;
428+
o.root = "../qt";
429+
o.modules = {"Core"};
430+
o.i18n.ts = {"app.ts"};
431+
o.i18n.update_sources = true;
432+
return mcpp::rules::qt::compile(o) ? 0 : 1;
433+
},
434+
[](const t::result& r, t::checker& c) {
435+
c.expect(r.exit_code == 0, "the member succeeds");
436+
const auto u = r.line("mcpp:action=", {"qt:lupdate:app"});
437+
c.expect(u.find("pkg/widget.h") != std::string::npos && u.find("pkg/x64/simd.h") != std::string::npos
438+
&& u.find("pkg/notag/kept.h") != std::string::npos,
439+
"lupdate reads the package's sources, a directory named x64 among them");
440+
c.expect(u.find("x64/Release") == std::string::npos && u.find("cmake-build-debug") == std::string::npos
441+
&& u.find("pkg/cache/") == std::string::npos,
442+
"lupdate reads nothing another build system wrote");
443+
c.expect(r.has_line("mcpp:action=", {"qt:moc:", "widget"}) && r.has_line("mcpp:action=", {"qt:moc:", "simd"}),
444+
"moc reads the package's headers");
445+
c.expect(!r.has_line("mcpp:action=", {"qt:moc:", "ui_form"}) && !r.has_line("mcpp:action=", {"qt:moc:", "stale"}),
446+
"moc reads no header another build system wrote");
447+
} },
448+
381449
// ── compatibility ───────────────────────────────────────────────────
382450
{ "compat: program_compilers keeps its 0.16.0 answer and says until when",
383451
t::row::linux_libcxx().file("llvm/bin/clang").file("llvm/bin/clang++"),

‎tests/plugin-logic/mcpp.toml‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# Fixture: the logic of the deps members, tested with `mcpp.plugins.testing`
2-
# against stated build contexts -- a Visual Studio row, a managed MSVC row, a
3-
# Linux GCC row and a Linux libc++ row -- on every host, with no foreign tool
4-
# installed or run. The build program is the test: a failed case fails the
5-
# build and prints the report; `target/**/plugins-testing/results.txt` holds
6-
# one verdict per case.
1+
# Fixture: the logic of the deps members and the files rules-qt reads by
2+
# default, tested with `mcpp.plugins.testing` against stated build contexts --
3+
# a Visual Studio row, a managed MSVC row, a Linux GCC row and a Linux libc++
4+
# row -- on every host, with no foreign tool installed or run. The build
5+
# program is the test: a failed case fails the build and prints the report;
6+
# `target/**/plugins-testing/results.txt` holds one verdict per case.
77
[package]
88
name = "plugin-logic"
99
version = "0.1.0"
@@ -17,7 +17,7 @@ modules = true
1717
import_std = true
1818

1919
[build-dependencies.mcpp]
20-
plugins = { path = "../..", features = ["deps-vcpkg", "deps-cmake", "plugins-testing"], host-module = true }
20+
plugins = { path = "../..", features = ["deps-vcpkg", "deps-cmake", "rules-qt", "plugins-testing"], host-module = true }
2121

2222
[targets.plugin-logic]
2323
kind = "bin"

0 commit comments

Comments
 (0)