/` to itself.
+ cp.linkResourceDir = sdk.resourceDir;
+
+ // Header + library search order is the whole upgrade mechanism: a
+ // purpose-built target libc++ (with a std module) goes FIRST, the SDK's
+ // patched libc++ 15 is the always-available fallback. Both lib dirs stay
+ // on the line — the overlay does not ship a usable libunwind for this
+ // target, so the platform's is what actually resolves.
+ if (auto ov = detect_libcxx_overlay(t)) {
+ cp.cxxIncludes.push_back(ov->include);
+ cp.libDirs.push_back(ov->lib);
+ cp.stdModuleSource = ov->stdModule;
+ cp.provider += " + external libc++";
+ } else if (!sdk.libcxxInclude.empty()) {
+ cp.cxxIncludes.push_back(sdk.libcxxInclude);
+ } else {
+ return std::unexpected(std::format(
+ "{} has no OHOS libc++ headers (expected "
+ "llvm/include/libcxx-ohos/include/c++/v1)", sdk.display()));
+ }
+ cp.libDirs.push_back(sdkLib);
+ return cp;
+}
+
+} // namespace mcpp::toolchain::ohos
diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm
index fff4e79f..7026396c 100644
--- a/src/toolchain/registry.cppm
+++ b/src/toolchain/registry.cppm
@@ -23,6 +23,7 @@ import mcpp.toolchain.gcc;
import mcpp.toolchain.llvm;
import mcpp.toolchain.model;
import mcpp.toolchain.msvc;
+import mcpp.toolchain.ohos;
import mcpp.toolchain.triple;
export namespace mcpp::toolchain {
@@ -326,6 +327,15 @@ bool is_system_toolchain(const ToolchainSpec& spec) {
bool host_can_serve(const triple::Triple& target) {
if (target.empty()) return true; // host target
+ // HarmonyOS: served by mcpp's own clang plus the platform SDK, so the
+ // question is "is the SDK here", not "does a payload exist for this
+ // hos
arch pair". Checked before the linux branch below, which would
+ // otherwise reject aarch64-linux-ohos on an x86_64 host for a reason that
+ // does not apply (no arch-specific payload is involved — one clang serves
+ // every target).
+ if (target.is_ohos())
+ return mcpp::toolchain::ohos::detect_installation().has_value();
+
if (target.os == "linux") {
if constexpr (mcpp::platform::is_linux) {
// musl payloads are self-contained, so any arch is reachable; a
diff --git a/src/toolchain/triple.cppm b/src/toolchain/triple.cppm
index 676ae858..be9f66d3 100644
--- a/src/toolchain/triple.cppm
+++ b/src/toolchain/triple.cppm
@@ -11,8 +11,15 @@
// abi_profile, model.cppm's is_*_target, registry's musl signals) consumes
// this module now. Vocabulary: os ∈ {linux, macos, windows} (never "darwin"),
// arch is the GNU spelling ({x86_64, aarch64, riscv64, ...} — never "arm64"),
-// env ∈ {gnu, musl, msvc} (empty on macos). `static` is NOT part of a triple:
-// it is a target's default linkage property, flipped via [build].
+// env ∈ {gnu, musl, msvc, ohos} (empty on macos). `static` is NOT part of a
+// triple: it is a target's default linkage property, flipped via [build].
+//
+// `ohos` (HarmonyOS / OpenHarmony) is an ENV, not an OS — that is upstream
+// LLVM's own model (`llvm::Triple::OpenHOS`), and it is the right one: the
+// kernel is Linux, so `cfg(unix)` / `cfg(os = "linux")` must keep matching,
+// while the libc (a musl FORK with its own soname layout) and the whole
+// runtime ABI differ. Spelling it as a new OS would have silently excluded
+// every `cfg(os = "linux")` block a portable package already ships.
//
// The known-target table below is the closed vocabulary `--target` validates
// against (with an escape hatch for explicit [target.X] manifest sections)
@@ -32,7 +39,7 @@ export namespace mcpp::toolchain::triple {
struct Triple {
std::string arch; // "x86_64" | "aarch64" | "riscv64" | ... (GNU spelling)
std::string os; // "linux" | "macos" | "windows"
- std::string env; // "gnu" | "musl" | "msvc" | "" (always empty on macos)
+ std::string env; // "gnu" | "musl" | "msvc" | "ohos" | "" (empty on macos)
bool empty() const { return arch.empty() && os.empty(); }
@@ -46,6 +53,13 @@ struct Triple {
bool is_musl() const { return env == "musl"; }
bool is_msvc_env() const { return env == "msvc"; }
+ // HarmonyOS / OpenHarmony. Its libc is a musl FORK, but `is_musl()`
+ // deliberately stays FALSE: mcpp's musl answer everywhere (payload
+ // selection, `ld-musl-
.so.1`, the `abi:musl` capability) means the
+ // upstream musl those payloads were built against, and an OHOS artifact
+ // is not interchangeable with one. Callers that mean "no glibc" must ask
+ // for that, not for musl.
+ bool is_ohos() const { return env == "ohos"; }
bool is_windows_gnu() const { return os == "windows" && env == "gnu"; }
bool is_pe() const { return os == "windows"; }
@@ -106,6 +120,14 @@ inline constexpr TargetInfo kKnownTargets[] = {
{ "x86_64-windows-gnu", "verified", "PE", "gcc@16.1.0", true },
{ "x86_64-windows-msvc", "verified", "PE", "", false },
{ "aarch64-macos", "verified", "", "", false },
+ // HarmonyOS / OpenHarmony. The pin is an LLVM one and that is structural,
+ // not a preference: GCC has no `ohos` target at all, so config2 ("mcpp
+ // brings the compiler, the platform brings the sysroot") can only be
+ // spelled with clang here. Must stay equal to `pins::kOhosLlvm` below —
+ // test_ohos_target.cpp enforces it.
+ { "aarch64-linux-ohos", "verified", "", "llvm@20.1.7", true },
+ { "x86_64-linux-ohos", "planned", "", "llvm@20.1.7", true },
+ { "arm-linux-ohos", "planned", "", "llvm@20.1.7", true },
{ "riscv64-linux-musl", "planned", "", "", true },
{ "aarch64-linux-gnu", "planned", "", "", false },
{ "x86_64-macos", "planned", "", "", false },
@@ -167,6 +189,13 @@ namespace pins {
inline constexpr std::string_view kFirstRunWinGnuTarget = "x86_64-windows-gnu";
inline constexpr std::string_view kFirstRunLinuxX86_64 = "gcc@16.1.0";
inline constexpr std::string_view kFirstRunLinuxOther = "gcc@15.1.0-musl";
+ // HarmonyOS/OpenHarmony convention toolchain. LLVM is not a preference
+ // here but the only option: GCC has no `ohos` target, so the compiler
+ // half of config2 can only be clang. Must stay equal to the
+ // `*-linux-ohos` rows' `pin` in kKnownTargets above — test_ohos_target.cpp
+ // enforces it. Floor is clang ≥ 17 (`-fmodule-output`); the SDK's own
+ // bundled clang is 15.0.4 even in SDK 6.1 and can never be used.
+ inline constexpr std::string_view kOhosLlvm = "llvm@20.1.7";
// Suggested install spellings used by help / MCPP_NO_AUTO_INSTALL errors.
inline constexpr std::string_view kSuggestLlvm = "llvm 20.1.7";
inline constexpr std::string_view kSuggestGccMusl = "gcc 15.1.0-musl";
@@ -311,6 +340,11 @@ std::optional parse(std::string_view s) {
if (t.os != "macos") {
if (k == "musl" || starts_with(k, "musleabi")) { t.env = "musl"; continue; }
if (k == "gnu" || starts_with(k, "gnueabi")) { t.env = "gnu"; continue; }
+ // HarmonyOS/OpenHarmony. `ohos` is the canonical spelling and the
+ // one the OHOS NDK's own driver uses ("aarch64-linux-ohos");
+ // "openhos" is upstream LLVM's long form for the same environment.
+ if (k == "ohos" || starts_with(k, "ohoseabi")
+ || k == "openhos") { t.env = "ohos"; continue; }
// starts_with: clang effective triples can carry a version suffix
// on the env segment ("...-windows-msvc19.44.35211").
if (starts_with(k, "msvc")) { t.env = "msvc"; continue; }
diff --git a/tests/e2e/103_harmonyos_cross_qemu.sh b/tests/e2e/103_harmonyos_cross_qemu.sh
new file mode 100755
index 00000000..e70467f3
--- /dev/null
+++ b/tests/e2e/103_harmonyos_cross_qemu.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+# requires: ohos-sdk qemu-aarch64
+# Linux → HarmonyOS/OpenHarmony cross: build a C++23 named-module project for
+# aarch64-linux-ohos with mcpp's own clang against the platform SDK's sysroot,
+# assert the artefact really is a static aarch64 OHOS ELF, and RUN it under
+# qemu-aarch64. Part C of .agents/docs/2026-08-04-harmonyos-target-design.md.
+#
+# "Linked" has never implied "runs" (the elfpatch incident, and §6.1 of
+# 2026年08月03日-windows-host-linux-cross-design.md), which is why the execution
+# step is the point of this test and the assertions around it are secondary.
+set -e
+
+TMP=$(mktemp -d)
+trap "rm -rf $TMP" EXIT
+cd "$TMP"
+
+TRIPLE=aarch64-linux-ohos
+
+"$MCPP" new ohosdemo
+cd ohosdemo
+
+# `import std` deliberately NOT used here: a stock OpenHarmony SDK carries
+# libc++ 15.0.4, which ships no std module. This test covers the shape that
+# works against an unmodified SDK — see 104 for the import-std tier.
+cat> src/banner.cppm <<'eof' +module; +#include
+export module ohosdemo.banner;
+export std::string banner() {
+ return "hello from aarch64-linux-ohos, running under qemu";
+}
+EOF
+
+cat> src/main.cpp <<'eof' +#include
+import ohosdemo.banner;
+int main() {
+#if !defined(__OHOS__)
+#error "__OHOS__ not defined — this was not built for HarmonyOS"
+#endif
+ std::printf("%s\n", banner().c_str());
+ std::printf("harmonyos cross named-module OK\n");
+ return 0;
+}
+EOF
+
+# The vocabulary pin (triple.cppm) selects an LLVM toolchain by itself, so no
+# [target.*] section is written: this asserts the CONVENTION works, not just
+# that an explicit override does.
+"$MCPP" build --target "$TRIPLE"
+
+BIN=$(find "target/$TRIPLE" -type f -path '*/bin/*' -name ohosdemo | head -1)
+[ -n "$BIN" ] || { echo "FAIL: no artefact under target/$TRIPLE"; find target -type f | head -20; exit 1; }
+
+echo "== file =="
+file "$BIN"
+# Positive `grep -q` on purpose: `! cmd | grep` is exempt from errexit and can
+# never fail (build-mcpp-helper-self-containment).
+file "$BIN" | grep -q "ELF 64-bit LSB"
+file "$BIN" | grep -q "ARM aarch64"
+file "$BIN" | grep -q "statically linked"
+
+# Stronger than `file`'s wording and independent of it: a fully static ELF has
+# no PT_INTERP at all. HarmonyOS's loader is /lib/ld-musl-aarch64.so.1, which
+# no CI runner has — so an accidentally-dynamic artefact would be unrunnable
+# here and this is what would catch it.
+if command -v readelf &>/dev/null; then
+ readelf -l "$BIN"> hdrs.txt
+ if grep -q "INTERP" hdrs.txt; then
+ echo "FAIL: artefact has PT_INTERP — not statically linked"
+ grep -A2 "INTERP" hdrs.txt
+ exit 1
+ fi
+fi
+
+QEMU=$(command -v qemu-aarch64 || command -v qemu-aarch64-static)
+echo "== run under $QEMU =="
+OUT=$("$QEMU" "$BIN")
+echo "$OUT"
+echo "$OUT" | grep -q "harmonyos cross named-module OK" \
+ || { echo "FAIL: artefact did not produce expected output"; exit 1; }
+
+echo "OK: HarmonyOS cross artefact builds and executes"
diff --git a/tests/e2e/104_harmonyos_import_std.sh b/tests/e2e/104_harmonyos_import_std.sh
new file mode 100755
index 00000000..fbae1cbf
--- /dev/null
+++ b/tests/e2e/104_harmonyos_import_std.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# requires: ohos-sdk qemu-aarch64 ohos-libcxx
+# The `import std` tier of the HarmonyOS target: with a libc++ built FOR
+# aarch64-linux-ohos supplied through $MCPP_OHOS_LIBCXX, mcpp builds the std
+# module for the target and `import std;` works — the same experience mcpp
+# gives on every other platform.
+#
+# Split from 103 because the two tiers fail differently and a combined test
+# could not say which one broke. 103 is the floor (stock SDK, named modules
+# only); this is the upgrade, and the capability gate is what keeps it from
+# being a false red on a machine that only has the SDK.
+set -e
+
+TMP=$(mktemp -d)
+trap "rm -rf $TMP" EXIT
+cd "$TMP"
+
+TRIPLE=aarch64-linux-ohos
+
+"$MCPP" new ohosstd
+cd ohosstd
+
+cat> src/main.cpp <<'eof' +import std; +int main() { + std::vector parts{"import", "std", "on", "HarmonyOS"};
+ std::string joined;
+ for (auto const& p : parts) { if (!joined.empty()) joined += ' '; joined += p; }
+ std::println("{}", joined);
+ std::println("total={}", std::accumulate(parts.begin(), parts.end(), std::size_t{0},
+ [](std::size_t a, auto const& s) { return a + s.size(); }));
+ return 0;
+}
+EOF
+
+"$MCPP" build --target "$TRIPLE"
+
+BIN=$(find "target/$TRIPLE" -type f -path '*/bin/*' -name ohosstd | head -1)
+[ -n "$BIN" ] || { echo "FAIL: no artefact under target/$TRIPLE"; exit 1; }
+
+file "$BIN" | grep -q "ARM aarch64"
+file "$BIN" | grep -q "statically linked"
+
+QEMU=$(command -v qemu-aarch64 || command -v qemu-aarch64-static)
+OUT=$("$QEMU" "$BIN")
+echo "$OUT"
+echo "$OUT" | grep -q "import std on HarmonyOS" \
+ || { echo "FAIL: unexpected output"; exit 1; }
+echo "$OUT" | grep -q "total=20" \
+ || { echo "FAIL: std::accumulate over the target's std module misbehaved"; exit 1; }
+
+echo "OK: import std works on HarmonyOS"
diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh
index 5f10700e..84430a59 100755
--- a/tests/e2e/run_all.sh
+++ b/tests/e2e/run_all.sh
@@ -62,6 +62,33 @@ case "$OS" in
fi
# wine: run cross-built Windows PE artifacts on the Linux host.
command -v wine &>/dev/null && CAPS+=(wine)
+ # ohos-sdk: the OpenHarmony native SDK, which mcpp consumes as a
+ # sysroot only (its bundled clang is 15.0.4 and cannot build modules).
+ # Probed the same way mcpp probes it, and by the same two files
+ # mcpp's own looks_like_native_sdk() requires — a directory that
+ # merely exists must not enable a test that then fails inside a
+ # compile command.
+ for _ohos in "${OHOS_NDK_HOME:-}" "${OHOS_SDK_NATIVE:-}" \
+ "${OHOS_SDK_HOME:-}/native" "$HOME/ohos-sdk/native" \
+ /opt/ohos-sdk/native; do
+ [[ -n "$_ohos" ]] || continue
+ if [[ -f "$_ohos/sysroot/usr/include/stdlib.h" && -d "$_ohos/llvm/lib" ]]; then
+ CAPS+=(ohos-sdk); break
+ fi
+ done
+ unset _ohos
+ # ohos-libcxx: a libc++ built FOR the ohos target, which is what makes
+ # `import std` available there (the SDK's own libc++ 15 has no std
+ # module). Separate capability from ohos-sdk because the two tiers
+ # fail differently and only one of them needs this.
+ if [[ -n "${MCPP_OHOS_LIBCXX:-}" \
+ && -f "${MCPP_OHOS_LIBCXX}/share/libc++/v1/std.cppm" ]]; then
+ CAPS+=(ohos-libcxx)
+ fi
+ # qemu-aarch64: run cross-built aarch64 artifacts on an x86_64 host.
+ # Both spellings exist in the wild (qemu-user vs qemu-user-static).
+ { command -v qemu-aarch64 &>/dev/null \
+ || command -v qemu-aarch64-static &>/dev/null; } && CAPS+=(qemu-aarch64)
# pack capability: ELF + patchelf both required
if [[ " ${CAPS[*]} " == *" patchelf "* ]]; then
CAPS+=(pack)
diff --git a/tests/unit/test_ohos_target.cpp b/tests/unit/test_ohos_target.cpp
new file mode 100644
index 00000000..9cea7bde
--- /dev/null
+++ b/tests/unit/test_ohos_target.cpp
@@ -0,0 +1,276 @@
+// HarmonyOS / OpenHarmony target identity + driver-retargeting model.
+//
+// Everything here is host-independent by construction: the triple language is
+// pure data, and the link/driver models take a Toolchain by value. That is
+// deliberate — the target these tests describe cannot be BUILT anywhere
+// without a vendor SDK, so if the model were only testable where the SDK is
+// installed it would effectively be untested. See
+// .agents/docs/2026-08-04-harmonyos-target-design.md.
+
+#include
+
+import std;
+import mcpp.toolchain.triple;
+import mcpp.toolchain.abi;
+import mcpp.toolchain.model;
+import mcpp.toolchain.linkmodel;
+import mcpp.toolchain.hostflags;
+
+using namespace mcpp::toolchain;
+using mcpp::toolchain::triple::parse;
+
+// ── triple language ─────────────────────────────────────────────────────────
+
+TEST(OhosTriple, OhosIsAnEnvNotAnOs) {
+ auto t = parse("aarch64-linux-ohos");
+ ASSERT_TRUE(t.has_value());
+ EXPECT_EQ(t->arch, "aarch64");
+ // The OS stays linux. A package that cfg-gates on `os = "linux"` (or on
+ // `family = "unix"`) must keep matching on HarmonyOS — the kernel really
+ // is Linux, and spelling ohos as an OS would silently exclude every such
+ // block.
+ EXPECT_EQ(t->os, "linux");
+ EXPECT_EQ(t->env, "ohos");
+ EXPECT_EQ(t->family(), "unix");
+ EXPECT_EQ(t->str(), "aarch64-linux-ohos");
+ EXPECT_TRUE(t->is_ohos());
+}
+
+TEST(OhosTriple, IsNotReportedAsMusl) {
+ // OHOS libc IS a musl fork, and that is exactly why this assertion
+ // exists: `is_musl()` drives payload selection and the `abi:musl`
+ // capability, both of which mean UPSTREAM musl. An OHOS artifact is not
+ // interchangeable with one.
+ auto t = parse("aarch64-linux-ohos");
+ ASSERT_TRUE(t.has_value());
+ EXPECT_FALSE(t->is_musl());
+ EXPECT_FALSE(t->is_pe());
+ EXPECT_FALSE(t->is_msvc_env());
+}
+
+TEST(OhosTriple, AcceptsTheSpellingsTheNdkAndLlvmUse) {
+ // The SDK's own driver wrappers are named `aarch64-unknown-linux-ohos-*`,
+ // and upstream LLVM's long environment name is OpenHOS. Both normalize.
+ for (auto s : {"aarch64-unknown-linux-ohos", "aarch64-linux-openhos"}) {
+ auto t = parse(s);
+ ASSERT_TRUE(t.has_value()) << s; + EXPECT_EQ(t->str(), "aarch64-linux-ohos") << s; + } + auto arm = parse("arm-linux-ohos"); + ASSERT_TRUE(arm.has_value()); + EXPECT_EQ(arm->str(), "arm-linux-ohos");
+}
+
+TEST(OhosTriple, ArtifactNamingIsPlainElf) {
+ auto t = parse("aarch64-linux-ohos");
+ ASSERT_TRUE(t.has_value());
+ // Host naming deliberately set to the PE convention: if the answer were
+ // taken from the host rather than the target, this would come back
+ // `.exe`/`.lib` and the assertion would catch it (that is the B3 bug
+ // class, 2026年08月03日-b3-target-aware-artifact-naming.md).
+ triple::ArtifactNaming pe{".exe", "", ".lib", ".dll", true};
+ auto n = triple::artifact_naming(*t, pe);
+ EXPECT_EQ(n.exeSuffix, "");
+ EXPECT_EQ(n.libPrefix, "lib");
+ EXPECT_EQ(n.staticLibExt, ".a");
+ EXPECT_EQ(n.sharedLibExt, ".so");
+ EXPECT_FALSE(n.sharedNeedsImportLib);
+}
+
+TEST(OhosTriple, KnownTargetPinsAnLlvmToolchain) {
+ auto t = parse("aarch64-linux-ohos");
+ ASSERT_TRUE(t.has_value());
+ auto* info = triple::find_known_target(*t);
+ ASSERT_NE(info, nullptr);
+ EXPECT_EQ(info->tier, "verified");
+ // Structural, not stylistic: GCC has no `ohos` target, so a gcc@ pin here
+ // would be unbuildable by construction.
+ EXPECT_EQ(info->pin, triple::pins::kOhosLlvm);
+ EXPECT_TRUE(std::string_view(info->pin).starts_with("llvm@"));
+ // musl-shaped libc + no system loader to rely on ⇒ static by default,
+ // the same call the musl rows make.
+ EXPECT_TRUE(info->defaultStatic);
+
+ for (auto s : {"x86_64-linux-ohos", "arm-linux-ohos"}) {
+ auto o = parse(s);
+ ASSERT_TRUE(o.has_value()) << s; + auto* oi = triple::find_known_target(*o); + ASSERT_NE(oi, nullptr) << s; + EXPECT_EQ(oi->pin, triple::pins::kOhosLlvm) << s; + } +} + +TEST(OhosTriple, FullStaticIsAllowedAndDoesNotConsultTheHost) { + // hostCapability=false models a macOS/Windows host: a cross target's + // linkage must not be decided by the build machine (the B2 bug class). + EXPECT_TRUE(target_supports_full_static("aarch64-linux-ohos", false)); +} + +TEST(OhosTriple, LoaderKeepsMuslNaming) { + // Measured from an SDK-produced binary's PT_INTERP: HarmonyOS kept musl's + // loader file name even though its libc is a fork. + EXPECT_EQ(loader_filename("aarch64-linux-ohos"), "ld-musl-aarch64.so.1"); + EXPECT_EQ(distro_loader_path("aarch64-linux-ohos"), "/lib/ld-musl-aarch64.so.1"); +} + +// ── ABI dimensions ────────────────────────────────────────────────────────── + +TEST(OhosAbi, LibcIsItsOwnDimensionValue) { + Toolchain tc; + tc.compiler = CompilerId::Clang; + tc.targetTriple = "aarch64-linux-ohos"; + tc.stdlibId = "libc++"; + auto p = abi_profile(tc); + EXPECT_EQ(p.libc, "ohos"); // NOT "musl" — see IsNotReportedAsMusl + EXPECT_EQ(p.os, "linux"); + EXPECT_EQ(p.arch, "aarch64"); + EXPECT_EQ(p.cxxAbi, "itanium"); + EXPECT_EQ(p.cxxStdlib, "libc++"); + + // A package declaring plain `abi:musl` must NOT be considered compatible. + auto c = parse_abi_capability("abi:musl", "some-pkg"); + ASSERT_TRUE(c.has_value()); + EXPECT_FALSE(abi_check(p, {*c}).empty()); +} + +// ── driver retargeting ────────────────────────────────────────────────────── + +namespace { + +Toolchain cross_clang() { + Toolchain tc; + tc.compiler = CompilerId::Clang; + tc.version = "20.1.7"; + tc.binaryPath = "/xpkgs/llvm/20.1.7/bin/clang++"; + tc.targetTriple = "aarch64-linux-ohos"; + tc.stdlibId = "libc++"; + // A host glibc payload deliberately left reachable: the point of several + // assertions below is that the cross path must ignore it. + tc.payloadPaths = PayloadPaths{"/xpkgs/glibc/include", "/xpkgs/glibc/lib64", + "/xpkgs/linux-headers/include"}; + CrossTarget ct; + ct.triple = "aarch64-linux-ohos"; + ct.sysroot = "/sdk/native/sysroot"; + ct.cxxIncludes = {"/sdk/native/llvm/include/libcxx-ohos/include/c++/v1"}; + ct.libDirs = {"/sdk/native/llvm/lib/aarch64-linux-ohos"}; + ct.linkResourceDir = "/sdk/native/llvm/lib/clang/15.0.4"; + ct.provider = "OpenHarmony SDK 6.1.0.31 (API 23)"; + tc.crossTarget = ct; + return tc; +} + +bool has(const std::vector& v, std::string_view s) {
+ return std::ranges::find(v, s) != v.end();
+}
+
+bool any_contains(const std::vector& v, std::string_view needle) {
+ return std::ranges::any_of(v, [&](auto const& t) {
+ return t.find(needle) != std::string::npos; });
+}
+
+} // namespace
+
+TEST(OhosCross, SysrootModeIgnoresTheHostPayload) {
+ auto lm = resolve_link_model(cross_clang());
+ EXPECT_EQ(lm.mode, CLibMode::Sysroot);
+ EXPECT_EQ(lm.sysroot, std::filesystem::path("/sdk/native/sysroot"));
+ // PayloadFirst would have put the host glibc's lib dir on -L and its
+ // loader on --dynamic-linker. Aiming a cross link at the build machine's
+ // libc is the failure this ordering exists to prevent.
+ EXPECT_TRUE(lm.libDirs.empty());
+ EXPECT_TRUE(lm.crtDir.empty());
+ EXPECT_TRUE(lm.loader.empty());
+}
+
+TEST(OhosCross, TargetFlagIsFirstOnBothSides) {
+ auto dm = resolve_clang_driver(cross_clang());
+ ASSERT_TRUE(dm.isCross());
+ auto esc = no_escape;
+
+ auto c = dm.compile_tokens(esc);
+ ASSERT_FALSE(c.empty());
+ // First, because it selects the toolchain object everything later is
+ // interpreted against.
+ EXPECT_EQ(c.front(), "--target=aarch64-linux-ohos");
+
+ auto l = dm.link_tokens(esc);
+ ASSERT_FALSE(l.empty());
+ EXPECT_EQ(l.front(), "--target=aarch64-linux-ohos");
+}
+
+TEST(OhosCross, CompileSideTakesTargetLibcxxAndNotTheHosts) {
+ auto dm = resolve_clang_driver(cross_clang());
+ auto c = dm.compile_tokens(no_escape);
+ EXPECT_TRUE(has(c, "-nostdinc++"));
+ EXPECT_TRUE(has(c, "-isystem/sdk/native/llvm/include/libcxx-ohos/include/c++/v1"));
+ // The host llvm root is /../ = /xpkgs/llvm/20.1.7. Its libc++ headers
+ // must never appear: they would compile without complaint and produce a
+ // BMI configured for the wrong platform.
+ EXPECT_FALSE(any_contains(c, "/xpkgs/llvm/20.1.7/include"));
+}
+
+TEST(OhosCross, ResourceDirIsLinkSideOnly) {
+ auto dm = resolve_clang_driver(cross_clang());
+ // On the link line: it is where the target's libclang_rt.builtins.a and
+ // clang_rt.crt{begin,end}.o live.
+ EXPECT_TRUE(has(dm.link_tokens(no_escape),
+ "-resource-dir=/sdk/native/llvm/lib/clang/15.0.4"));
+ // NOT on the compile line: that dir belongs to clang 15 and carries clang
+ // 15's intrinsic headers, which the actual (much newer) compiler must not
+ // read.
+ EXPECT_FALSE(any_contains(dm.compile_tokens(no_escape), "-resource-dir="));
+}
+
+TEST(OhosCross, NoRpathIntoBuildHostPaths) {
+ auto l = resolve_clang_driver(cross_clang()).link_tokens(no_escape);
+ EXPECT_TRUE(has(l, "-L/sdk/native/llvm/lib/aarch64-linux-ohos"));
+ // The device will not have the build machine's directory layout, and a
+ // static link has nowhere to use an rpath anyway.
+ EXPECT_FALSE(any_contains(l, "-Wl,-rpath,"));
+}
+
+TEST(OhosCross, HostFlagProducerBypassesTheCfgEvenWithoutOne) {
+ // The bundled-LLVM cfg is generated at install time for the HOST triple,
+ // so a retargeted driver must bypass it. `cross_clang()`'s binary path
+ // does not exist, hence hasCfg == false — and the tokens must still be
+ // emitted, which is exactly the case a `dm.hasCfg &&` gate would drop.
+ auto tc = cross_clang();
+ HostFlagOptions opt;
+ auto c = host_compile_tokens(tc, opt, no_escape);
+ EXPECT_TRUE(has(c, "--target=aarch64-linux-ohos"));
+ EXPECT_TRUE(has(c, "--no-default-config"));
+ EXPECT_TRUE(has(c, "--sysroot=/sdk/native/sysroot"));
+
+ auto l = host_link_tokens(tc, opt, no_escape);
+ EXPECT_TRUE(has(l, "--target=aarch64-linux-ohos"));
+ EXPECT_TRUE(has(l, "--sysroot=/sdk/native/sysroot"));
+}
+
+TEST(OhosCross, NoStdModuleUnlessTheProviderSuppliesOne) {
+ // The default (stock SDK, libc++ 15) has no std module, and mcpp must say
+ // so rather than reach for the driver's own — which is the HOST's, and
+ // would compile into a wrong-platform BMI without erroring.
+ auto tc = cross_clang();
+ EXPECT_TRUE(tc.crossTarget->stdModuleSource.empty());
+ EXPECT_FALSE(tc.hasImportStd);
+}
+
+TEST(OhosCross, HostToolchainIsUntouchedByTheseChanges) {
+ // Regression guard: everything above keys on tc.crossTarget, so an
+ // ordinary host toolchain must resolve exactly as before — payload-first,
+ // no --target, no -resource-dir.
+ Toolchain tc;
+ tc.compiler = CompilerId::Clang;
+ tc.binaryPath = "/xpkgs/llvm/20.1.7/bin/clang++";
+ tc.targetTriple = "x86_64-linux-gnu";
+ tc.payloadPaths = PayloadPaths{"/xpkgs/glibc/include", "/xpkgs/glibc/lib64",
+ "/xpkgs/linux-headers/include"};
+ auto dm = resolve_clang_driver(tc);
+ EXPECT_FALSE(dm.isCross());
+ EXPECT_TRUE(dm.target_tokens().empty());
+ EXPECT_TRUE(dm.cross_link_tokens(no_escape).empty());
+
+ auto lm = resolve_link_model(tc);
+ EXPECT_EQ(lm.mode, CLibMode::PayloadFirst);
+ EXPECT_EQ(lm.crtDir, std::filesystem::path("/xpkgs/glibc/lib64"));
+}