Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/scripts/check-deps-and-qt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,27 @@ cmake_consumer() {
echo "ok: a file added to the subproject re-ran its installation"
}

# A BUILD FOR ANOTHER TARGET (0.21.0): the subproject is compiled with the
# compilers and words of the engine's contract for the target, read when the
# action runs, and not with the machine's toolset. On a glibc machine,
# `--target x86_64-linux-musl` must give a subproject compiled against musl.
cmake_cross() {
cd "$ROOT/tests/cmake-cross"
rm -rf target
mkdir -p target/ci
"$MCPP" build --target x86_64-linux-musl > target/ci/build.log 2>&1 || { cat target/ci/build.log; fail "the cross build failed"; }
script=$(find target -path '*deps-cmake/answer/answer.cmake' | head -1)
[ -n "$script" ] || fail "no deps-cmake script was written"
grep -q "the engine's contract for x86_64-linux-musl" "$script" || { cat "$script"; fail "the script does not read the contract"; }
cache=$(find target -path '*deps-cmake/answer/build-*/CMakeCache.txt' | head -1)
grep -q '^CMAKE_SYSTEM_NAME:.*=Linux' "$cache" || { grep CMAKE_SYSTEM_NAME "$cache"; fail "CMAKE_SYSTEM_NAME is not Linux"; }
grep -q '^CMAKE_TRY_COMPILE_TARGET_TYPE:.*=STATIC_LIBRARY' "$cache" || fail "the try-compile links"
"$MCPP" run --target x86_64-linux-musl | tee target/ci/run.log
grep -q '^cmake-cross: the subproject was compiled against not glibc$' target/ci/run.log ||
fail "the subproject was compiled with the machine's C library"
echo "ok: a subproject of a cross build is compiled from the engine's contract"
}

# AN INSTALLATION IS BUILT ONCE (0.18.0). deps-cmake keeps what a subproject
# installed under a key that holds no path of the machine, and copies it into
# the prefix of any later build with the same key. Only a toolset the engine
Expand Down Expand Up @@ -673,6 +694,7 @@ case "${1:-}" in
vcpkg-workspace) vcpkg_workspace ;;
cmake-consumer) cmake_consumer ;;
cmake-cache) cmake_cache ;;
cmake-cross) cmake_cross ;;
qt-consumer) qt_consumer ;;
qt-widgets-consumer) qt_widgets_consumer ;;
qt-sdk-consumer) qt_sdk_consumer ;;
Expand All @@ -682,5 +704,5 @@ case "${1:-}" in
vcpkg-managed) vcpkg_managed ;;
vcpkg-msbuild-refused) vcpkg_msbuild_refused ;;
vcpkg-make-port) vcpkg_make_port ;;
*) echo "usage: $0 vcpkg-consumer|vcpkg-libcxx|archive-consumer|vcpkg-workspace|cmake-consumer|cmake-cache|qt-consumer|qt-widgets-consumer|qt-sdk-consumer|qt-import-only|plugin-logic|tool-sources|vcpkg-crt|vcpkg-managed|vcpkg-msbuild-refused|vcpkg-make-port"; exit 2 ;;
*) echo "usage: $0 vcpkg-consumer|vcpkg-libcxx|archive-consumer|vcpkg-workspace|cmake-consumer|cmake-cache|cmake-cross|qt-consumer|qt-widgets-consumer|qt-sdk-consumer|qt-import-only|plugin-logic|tool-sources|vcpkg-crt|vcpkg-managed|vcpkg-msbuild-refused|vcpkg-make-port"; exit 2 ;;
esac
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1206,7 +1206,7 @@ jobs:
set -e
[ "$rc" = 2 ] || { echo "FAIL: --format bogus exited $rc, expected 2"; exit 1; }
[ -z "$so" ] || { echo "FAIL: the refusal wrote to stdout"; exit 1; }
grep -q 'available in this build: tar, dir, appimage' <<<"$se" \
grep -qE 'available in this build: .*\bappimage\b' <<<"$se" \
|| { echo "FAIL: the refusal does not name appimage"; echo "$se"; exit 1; }

"$MCPP" pack --format appimage | tee pack.log
Expand Down Expand Up @@ -1756,6 +1756,11 @@ jobs:
- name: deps-cmake keeps an installation and takes it for the same key
run: bash .github/scripts/check-deps-and-qt.sh cmake-cache

# 0.21.0: a subproject of a build for another target is compiled from the
# engine's contract for it (mcpp 2026.10.30.1+): musl from a glibc machine.
- name: deps-cmake compiles a cross build's subproject from the engine's contract
run: bash .github/scripts/check-deps-and-qt.sh cmake-cross

- name: rules-qt runs moc, rcc, lupdate and lrelease, with the SDK the project declares
run: bash .github/scripts/check-deps-and-qt.sh qt-consumer

Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module name the member declares, and configures it there.

```toml
[build-dependencies.mcpp]
plugins = { version = "0.20.0", features = ["rules-spirv"], host-module = true }
plugins = { version = "0.21.0", features = ["rules-spirv"], host-module = true }
```

```cpp
Expand Down Expand Up @@ -51,9 +51,11 @@ naming it to avoid a download, and the scenarios on both sides -- is in

## Members

From 0.20.0 the package as a whole needs mcpp 2026.10.27.1 (`[package] mcpp`),
whose protocol 17 states a platform's fields and whose `mcpp pack` writes
`mcpp-package.json` beside every staged tree (0.19.0 needed 2026.10.1.3, whose
From 0.21.0 the package as a whole needs mcpp 2026.10.30.1 (`[package] mcpp`),
which writes the configuration's contract that `deps-cmake` compiles a cross
build from (0.20.0 needed 2026.10.27.1, whose protocol 17 states a platform's
fields and whose `mcpp pack` writes `mcpp-package.json` beside every staged
tree; 0.19.0 needed 2026.10.1.3, whose
protocol 15 states where each tool and payload comes from); the column states
the release each member's behaviour first needed.

Expand All @@ -76,7 +78,7 @@ the release each member's behaviour first needed.
| `dist-web` | `mcpp.dist.web` | 2026.9.13.1 | Copies a `wasm32-emscripten` program and its files into a web directory with an `index.html`. | [dist](docs/dist.md#dist-web) |
| `dist-apk` | `mcpp.dist.apk` | 2026.10.1.3 | Packs the native closure into a signed APK or App Bundle, with Java, Kotlin and Maven libraries. | [dist-apk](docs/dist-apk.md) |
| `deps-vcpkg` | `mcpp.deps.vcpkg` | 2026.10.1.3 | Installs a `vcpkg.json` manifest as an action with the toolset mcpp resolved, and maps the prefix into the build. | [deps](docs/deps.md#deps-vcpkg) |
| `deps-cmake` | `mcpp.deps.cmake` | 2026.10.1.3 | Builds and installs a CMake subproject as an action with Ninja and the toolset mcpp resolved, keeps the installation for any build with the same key, and maps the prefix into the build. | [deps](docs/deps.md#deps-cmake) |
| `deps-cmake` | `mcpp.deps.cmake` | 2026.10.1.3; a cross build 2026.10.30.1 | Builds and installs a CMake subproject as an action with Ninja and the toolset mcpp resolved -- for another target, the compilers and words of the engine's contract -- keeps the installation for any build with the same key, and maps the prefix into the build. | [deps](docs/deps.md#deps-cmake) |
| `deps-archive` | `mcpp.deps.archive` | 2026.10.1.3 | Extracts a zip archive the project keeps and places its tree beside the program. | [deps](docs/deps.md#deps-archive) |

Some features add a sub-capability to a member: `dist-apk-kotlin` and
Expand Down
113 changes: 100 additions & 13 deletions deps/cmake.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,62 @@ struct options {
// `~/.cache/mcpp-plugins/deps-cmake`). Read when the action runs; the place
// is no part of what is built. See docs/deps.md.
std::string cache;
// `CMAKE_SYSTEM_NAME` of a build for another target (0.21.0). Empty
// derives it from the target's system: Linux, Android, Darwin, iOS,
// Windows, Emscripten, Generic for bare metal, and Linux for an ok image,
// whose C library is POSIX at the source level.
std::string system_name;
};

// The `CMAKE_SYSTEM_NAME` of a target system (`mcpp::target_os()`).
inline std::string cmake_system_name(std::string_view os) {
if (os == "linux" || os == "openkal") return "Linux";
if (os == "android") return "Android";
if (os == "macos") return "Darwin";
if (os == "ios") return "iOS";
if (os == "windows") return "Windows";
if (os == "emscripten") return "Emscripten";
return "Generic";
}

// The CMake code that reads the engine's contract for this configuration
// (`${mcpp.contract}`, mcpp 2026.10.30.1+) into variables when the action
// runs: `_mcpp_<c|cxx|asm>` and `_mcpp_ld` the programs, `_mcpp_<...>_flags`
// their words joined (a word with a space quoted, as CMake splits a flags
// string), `_mcpp_ar` the archiver. Read then and not when this
// program plans: the engine writes the file after build programs have run.
inline std::string contract_reader() {
return R"(if(NOT EXISTS "${MCPP_CONTRACT}")
message(FATAL_ERROR "mcpp.deps.cmake: a build for another target reads the engine's contract, "
"and '${MCPP_CONTRACT}' does not exist; it is written by mcpp 2026.10.30.1 and later")
endif()
file(READ "${MCPP_CONTRACT}" _mcpp_contract)
function(_mcpp_words out)
string(JSON _n LENGTH "${_mcpp_contract}" ${ARGN})
set(_w "")
if(_n GREATER 0)
math(EXPR _last "${_n} - 1")
foreach(_i RANGE ${_last})
string(JSON _a GET "${_mcpp_contract}" ${ARGN} ${_i})
if(_a MATCHES " ")
set(_a "\"${_a}\"")
endif()
string(APPEND _w " ${_a}")
endforeach()
endif()
string(STRIP "${_w}" _w)
set(${out} "${_w}" PARENT_SCOPE)
endfunction()
foreach(_lang c cxx asm)
string(JSON _mcpp_${_lang} GET "${_mcpp_contract}" compile ${_lang} program)
_mcpp_words(_mcpp_${_lang}_flags compile ${_lang} args)
endforeach()
string(JSON _mcpp_ld GET "${_mcpp_contract}" link program)
string(JSON _mcpp_ar GET "${_mcpp_contract}" link archiver)
_mcpp_words(_mcpp_ld_flags link args)
)";
}

// The variable a `-D<name>[:<type>]=<value>` argument defines; empty for any
// other argument.
inline std::string_view defined_variable(std::string_view arg) {
Expand Down Expand Up @@ -376,27 +430,37 @@ inline prefix use(const options& opt) {
const bool chosen = chooses_compiler(opt.cache_args);
const bool generatorChosen = chooses_generator(opt.cache_args);
const bool visualStudio = opt.generator == options::generator_kind::visual_studio;
auto tools = visualStudio ? ts::resolve(opt.toolset) : ts::resolve_named(opt.toolset);
if (!tools) {
std::cerr << std::format("{}: {}\n", who, tools.error());
// A BUILD FOR ANOTHER TARGET IS COMPILED AS THE ENGINE COMPILES IT (0.21.0):
// the compilers, target words and archiver of the engine's contract, which
// the action reads when it runs. The toolset this program resolves is the
// machine's, and a cross build linked with it would not be the target's.
const std::string targetTriple = mcpp::target(), hostTriple = mcpp::host();
const bool contract = !chosen && !targetTriple.empty() && !hostTriple.empty()
&& targetTriple != hostTriple;
auto resolvedTools = visualStudio ? ts::resolve(opt.toolset) : ts::resolve_named(opt.toolset);
if (!resolvedTools && !contract) {
std::cerr << std::format("{}: {}\n", who, resolvedTools.error());
return {};
}
const ts::resolved_tools noTools{};
const ts::resolved_tools* tools = resolvedTools ? &*resolvedTools : &noTools;
const std::string crt = opt.crt_linkage.empty() ? tools->crt : opt.crt_linkage;
if (!crt.empty() && crt != "static" && crt != "dynamic") {
std::cerr << std::format("{}: options::crt_linkage is '{}'; it is \"static\" or \"dynamic\".\n",
who, crt);
return {};
}
const std::string ninja = mcpp::ninja_program();
const bool named = !chosen && tools->how == ts::mechanism::chain && !tools->cxx.empty();
const bool useNinja = named && !generatorChosen && !ninja.empty();
const bool instance = !chosen && !generatorChosen && tools->how == ts::mechanism::instance;
const bool named = !contract && !chosen && tools->how == ts::mechanism::chain && !tools->cxx.empty();
const bool useNinja = (named || contract) && !generatorChosen && !ninja.empty();
const bool instance = !contract && !chosen && !generatorChosen && tools->how == ts::mechanism::instance;

// One build directory per toolset statement: CMake refuses a cache made
// with another generator or instance. What 0.16.0 configured stays in
// `build/`.
std::string key;
if (useNinja) key = "ninja;" + tools->identity + ";" + tools->cxx;
if (contract) key = "contract;" + targetTriple;
else if (useNinja) key = "ninja;" + tools->identity + ";" + tools->cxx;
else if (named) key = "named;" + tools->identity + ";" + tools->cxx;
else if (instance) key = "instance;" + tools->instance_dir + ";" + tools->toolset_version;
const std::string name = opt.name.empty() ? source.filename().string() : opt.name;
Expand All @@ -409,8 +473,9 @@ inline prefix use(const options& opt) {
p.include = mcpp::deps::generic(root / opt.dirs.include);
p.lib = mcpp::deps::generic(root / opt.dirs.lib);
p.bin = mcpp::deps::generic(root / opt.dirs.bin);
p.mechanism = std::string(ts::name(named ? ts::mechanism::chain
: instance ? ts::mechanism::instance : ts::mechanism::detected));
p.mechanism = contract ? std::string("contract")
: std::string(ts::name(named ? ts::mechanism::chain
: instance ? ts::mechanism::instance : ts::mechanism::detected));

const auto tool = cmake_exe(opt);
const std::string cmake = tool.program;
Expand Down Expand Up @@ -486,21 +551,41 @@ inline prefix use(const options& opt) {
+ (crt == "dynamic" ? "DLL" : ""));
}
for (auto const& x : opt.cache_args) same(x);
if (contract) {
same("-DCMAKE_SYSTEM_NAME=" + (opt.system_name.empty() ? cmake_system_name(mcpp::target_os())
: opt.system_name));
// A try-compile links nothing: the target's startup files and C
// library reach a program only through the engine's link.
same("-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY");
keyed.push_back("contract " + targetTriple);
}

std::string steps = "execute_process(COMMAND ${CMAKE_COMMAND}";
std::string steps = contract ? contract_reader() : std::string();
steps += "execute_process(COMMAND ${CMAKE_COMMAND}";
for (auto const& x : configure) steps += "\n " + bracket(x);
if (contract)
steps += "\n \"-DCMAKE_C_COMPILER=${_mcpp_c}\" \"-DCMAKE_CXX_COMPILER=${_mcpp_cxx}\""
"\n \"-DCMAKE_ASM_COMPILER=${_mcpp_asm}\" \"-DCMAKE_AR=${_mcpp_ar}\""
"\n \"-DCMAKE_C_FLAGS=${_mcpp_c_flags}\" \"-DCMAKE_CXX_FLAGS=${_mcpp_cxx_flags}\""
"\n \"-DCMAKE_ASM_FLAGS=${_mcpp_asm_flags}\""
"\n \"-DCMAKE_EXE_LINKER_FLAGS=${_mcpp_ld_flags}\""
"\n \"-DCMAKE_SHARED_LINKER_FLAGS=${_mcpp_ld_flags}\"";
steps += "\n RESULT_VARIABLE rc)\n"
"if(NOT rc EQUAL 0)\n message(FATAL_ERROR \"configure exited ${rc}\")\nendif()\n"
"execute_process(COMMAND ${CMAKE_COMMAND} --build " + bracket(buildS) +
" --config " + bracket(opt.config) + " --target install --parallel\n RESULT_VARIABLE rc)\n"
"if(NOT rc EQUAL 0)\n message(FATAL_ERROR \"build and install exited ${rc}\")\nendif()\n";
std::string text = "# Written by mcpp.deps.cmake: configure, build and install " + name + ".\n"
"# toolset: " + ts::describe(*tools) + "\n";
"# toolset: " + (contract ? std::string("the engine's contract for ") + targetTriple
: ts::describe(*tools)) + "\n";
const auto files = mcpp::deps::files_under(source);
// A key states the compilers through the toolset's identity. On the
// MSVC ABI that is the MSVC toolset, which a row compiler (clang-cl,
// `compiler::row`) is not, so such an installation is not kept.
const bool identified = instance || (named && (!tools->msvc_abi || opt.toolset.cc == ts::compiler::abi_native));
// A contract build is not kept either: its compilers are named by the
// contract when the action runs, after this key is written.
const bool identified = !contract
&& (instance || (named && (!tools->msvc_abi || opt.toolset.cc == ts::compiler::abi_native)));
if (!identified || opt.cache == "off") {
text += steps;
} else {
Expand All @@ -520,7 +605,9 @@ inline prefix use(const options& opt) {
a.id = id.c_str();
a.role = mcpp::roles::prepare;
a.description = desc.c_str();
a.arg(cmake.c_str()).arg("-P").arg(scriptS.c_str());
a.arg(cmake.c_str());
if (contract) a.arg("-DMCPP_CONTRACT=${mcpp.contract}");
a.arg("-P").arg(scriptS.c_str());
// Named tools run with the environment the engine runs them with, and
// their directories first on PATH.
if (named) {
Expand Down
21 changes: 21 additions & 0 deletions docs/deps.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,27 @@ Each toolset statement configures its own build directory, because CMake
refuses a cache made with another generator or instance; the directory 0.16.0
configured stays `build/`.

**A build for another target (0.21.0, mcpp 2026.10.30.1+).** When the build's
target is not the machine (`mcpp build --target x86_64-linux-musl`, an ok image
`--target x86_64-openkal`), the toolset above is the machine's and is not used.
The subproject is compiled as the engine compiles the target: the action reads
the engine's contract for the configuration (`${mcpp.contract}`, see mcpp's
docs/30) when it runs, and configures with its compilers
(`CMAKE_<LANG>_COMPILER`), its words (`CMAKE_<LANG>_FLAGS`: target, sysroot,
target-side headers, platform and profile flags), its link words and its
archiver, under Ninja. `CMAKE_SYSTEM_NAME` is the target's system (Linux,
Android, Darwin, iOS, Windows, Emscripten, `Generic` for bare metal, and Linux
for an ok image, whose C library is POSIX at the source level), or
`options::system_name`; `CMAKE_TRY_COMPILE_TARGET_TYPE` is `STATIC_LIBRARY`,
because a program reaches the target's startup files and C library only through
the engine's link. The libraries the subproject installs are linked into the
program by that link. A tool the subproject runs at build time for the machine
(LLVM's `llvm-tblgen`) is built in the host graph, as a build dependency's tool,
and named to the subproject with a `cache_args` entry. An installation of a
cross build is not kept in the cache below: its compilers are named by the
contract when the action runs, after the key is written. Compilers or a
toolchain file in `cache_args` decide here too.

### An installation is built once (0.18.0)

The action keeps what it installed in a cache outside the package, and a later
Expand Down
2 changes: 1 addition & 1 deletion docs/plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ mcpp-index refuses such a package.

```toml
[build-dependencies.mcpp]
plugins = { version = "0.20.0", features = ["plugins-core"], host-module = true, reexport = true }
plugins = { version = "0.21.0", features = ["plugins-core"], host-module = true, reexport = true }
```

`reexport = true` is needed when the plugin's consumers import L2 modules in
Expand Down
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[package]
name = "plugins"
namespace = "mcpp"
version = "0.20.0"
version = "0.21.0"
# The first mcpp release that speaks protocol 17 (SPEC-013, mcpp#847): the
# platform's directives `mcpp.plugins.platform` states (`link_driver`,
# `link_input`, `artifact_name`, `seal`, `run_host`), the `platform` phase, and
# `mcpp-package.json` beside every staged tree, which `mcpp::plugins::package`
# reads. It carries protocol 15 (mcpp#755): `xpkg_source`, `xpkg_program`,
# `xpkg_request`, `mcpp::decision`, `mcpp::toolchain`, `mcpp::phase`, and the
# `provision = "on-request"` entries below.
mcpp = ">=2026.10.27.1"
mcpp = ">=2026.10.30.1"
description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
2 changes: 1 addition & 1 deletion src/plugins.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export namespace mcpp::plugins {
//
// One package, one version: the number lives in mcpp.toml, and the CI step
// `the collection states its own version` compares the two.
inline constexpr std::string_view version = "0.20.0";
inline constexpr std::string_view version = "0.21.0";

} // namespace mcpp::plugins

Expand Down
Loading
Loading