Skip to content

Commit c2c4f85

Browse files
committed
Merge branch 'feat/622-t3-app' into feat/622-ui-framework-on-three-rows
2 parents 4735908 + 9f8e375 commit c2c4f85

15 files changed

Lines changed: 546 additions & 25 deletions

‎modules/buildmcpp/src/directives.cppm‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -878,21 +878,24 @@ void apply(mcpp::manifest::Manifest& m, const Directives& d) {
878878

879879
// A named executable's subsystem and entry. `target_directive_error` has
880880
// refused every value that names no executable, so the conditions below
881-
// only keep this function total.
881+
// only keep this function total. #622 A3: `is_program()`, not `Binary`
882+
// alone — `windows_subsystem`/`windows_entry` accept `app` exactly as
883+
// they accept `bin` (PE has no Android row, so this never meets an
884+
// `Application` whose form is a shared object in practice).
882885
for (auto const& entry : d.at(Slot::WindowsSubsystem)) {
883886
const auto sep = entry.rfind(':');
884887
if (sep == std::string::npos) continue;
885888
const auto name = entry.substr(0, sep);
886889
for (auto& t : m.targets)
887-
if (t.name == name && t.kind == mcpp::manifest::Target::Binary)
890+
if (t.name == name && t.is_program())
888891
t.windowsSubsystem = entry.substr(sep + 1);
889892
}
890893
for (auto const& entry : d.at(Slot::WindowsEntry)) {
891894
const auto sep = entry.rfind(':');
892895
if (sep == std::string::npos) continue;
893896
const auto name = entry.substr(0, sep);
894897
for (auto& t : m.targets)
895-
if (t.name == name && t.kind == mcpp::manifest::Target::Binary)
898+
if (t.name == name && t.is_program())
896899
t.windowsEntry = entry.substr(sep + 1);
897900
}
898901

@@ -967,10 +970,12 @@ static std::string named_target_error(const mcpp::manifest::Manifest& m,
967970
wire, entry, m.package.name, name,
968971
names.empty() ? std::string("none") : names);
969972
}
970-
if (target->kind != mcpp::manifest::Target::Binary)
973+
// #622 A3: `is_program()` — a program, not literally `Binary` — so an
974+
// `app` accepts these exactly as a `bin` does.
975+
if (!target->is_program())
971976
return std::format(
972977
"build.mcpp emitted `mcpp:{}={}`, and `{}` applies to an executable "
973-
"(`kind = \"bin\"`); target `{}` is not one.",
978+
"(`kind = \"bin\"` or `\"app\"`); target `{}` is not one.",
974979
wire, entry, key, name);
975980
const std::string& declared =
976981
subsystem ? target->windowsSubsystem : target->windowsEntry;

‎modules/manifest/src/toml.cppm‎

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,14 +1106,28 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
11061106
else if (kind_s == "bin" || kind_s == "binary") t.kind = Target::Binary;
11071107
else if (kind_s == "shared" || kind_s == "dylib"
11081108
|| kind_s == "so" || kind_s == "shlib") t.kind = Target::SharedLibrary;
1109+
// #622 A3: "the thing a user launches", on every row. Its link form
1110+
// is a function of the row alone (`toolchain::triple::
1111+
// application_form`) -- identical to `bin` everywhere except
1112+
// `*-linux-android`, where it is the shared library the platform
1113+
// loads. An older engine refuses this name (`toml.cppm:1110` on
1114+
// 2026.9.12.2 lists three kinds), which is correct: a root that
1115+
// names a form the engine cannot produce must not build.
1116+
else if (kind_s == "app" || kind_s == "application") t.kind = Target::Application;
11091117
else return std::unexpected(error(origin,
1110-
std::format("targets.{}.kind must be 'bin', 'lib' or 'shared'; got '{}'", tname, kind_s)));
1111-
1112-
if (t.kind == Target::Binary) {
1118+
std::format("targets.{}.kind must be 'bin', 'app', 'lib' or 'shared'; got '{}'", tname, kind_s)));
1119+
1120+
// `main` is required for `bin` and for `app`: on every row but
1121+
// Android it is the executable's entry, exactly as it is for `bin`;
1122+
// on Android it is a translation unit compiled INTO the shared
1123+
// library (its actual entry is `ANativeActivity_onCreate` or the
1124+
// JNI exports, which is the platform's contract and not mcpp's to
1125+
// rename -- see `is_program()` and `application_form`).
1126+
if (t.is_program()) {
11131127
auto mit = tt.find("main");
11141128
if (mit == tt.end() || !mit->second.is_string()) {
11151129
return std::unexpected(error(origin,
1116-
std::format("targets.{} (kind=bin) requires 'main' field", tname)));
1130+
std::format("targets.{} (kind={}) requires 'main' field", tname, kind_s)));
11171131
}
11181132
t.main = mit->second.as_string();
11191133
}
@@ -1219,12 +1233,16 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
12191233
return std::unexpected(r.error());
12201234
// An executable's property. A library has no subsystem, and a GUI
12211235
// subsystem on anything a test runner executes is the defect #618
1222-
// describes, so both are refused naming the key.
1236+
// describes, so both are refused naming the key. `app` is accepted
1237+
// exactly as `bin` is (#622 A3: PE has no Android row, so this key
1238+
// never meets `application_form`'s SharedObject form in practice) --
1239+
// `is_program()` is "is this the program", which is what the PE
1240+
// subsystem attaches to; `TestBinary` stays refused on purpose.
12231241
if ((!t.windowsSubsystem.empty() || !t.windowsEntry.empty())
1224-
&& t.kind != Target::Binary)
1242+
&& !t.is_program())
12251243
return std::unexpected(error(origin, std::format(
1226-
"targets.{}.{} applies to an executable (`kind = \"bin\"`), and this "
1227-
"target is not one", tname,
1244+
"targets.{}.{} applies to an executable (`kind = \"bin\"` or "
1245+
"`\"app\"`), and this target is not one", tname,
12281246
t.windowsSubsystem.empty() ? "windows_entry" : "windows_subsystem")));
12291247
// Guard: -std=... belongs to [package].standard, not per-target flags
12301248
// (same rule as [build].cxxflags). Reject early with a clear message.
@@ -3404,6 +3422,12 @@ void apply_defaults_and_infer(Manifest& m, const std::filesystem::path& root) {
34043422
const bool hasModuleInterface = !moduleInterfaceExt.empty();
34053423

34063424
if (hasMain) {
3425+
// #622 A3: inference stays `Binary`, deliberately. `app` is a
3426+
// platform fact an author states on purpose (an Android build is
3427+
// never an accident), never a default this engine guesses from a
3428+
// bare `src/main.cpp` -- guessing wrong here would silently link
3429+
// a library where the author's `[targets]`-free project expected
3430+
// an executable.
34073431
Target t;
34083432
t.name = m.package.name;
34093433
t.kind = Target::Binary;

‎modules/manifest/src/types.cppm‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,26 @@ inline std::string windows_choice_problem(bool subsystem, std::string_view value
133133

134134
struct Target {
135135
std::string name;
136-
enum Kind { Library, Binary, SharedLibrary, TestBinary } kind;
137-
std::string main; // for binary / test
136+
// `Application` (#622 A3, `kind = "app"`) is "the thing a user launches",
137+
// and its link form is a property of the ROW, not of this enum: on every
138+
// row but Android it links exactly as `Binary` does; on `*-linux-android`
139+
// it links as `SharedLibrary` does, because that is the only form an
140+
// Android application has. See `toolchain::triple::application_form`,
141+
// which is the one function that answers this — the manifest carries no
142+
// predicate for it.
143+
enum Kind { Library, Binary, SharedLibrary, TestBinary, Application } kind;
144+
std::string main; // for binary / test / app
145+
146+
// Whether this target is a PROGRAM -- something `mcpp run` can be asked
147+
// for and `mcpp pack` treats as the application pipeline's subject --
148+
// as opposed to a library. True for `Binary` and `Application`.
149+
//
150+
// Every call site that used to ask `kind == Binary` to mean "is this the
151+
// program" now asks this instead; a site that means "is this literally an
152+
// executable link" (a PE subsystem, a host build tool that must be exec'd
153+
// directly) keeps comparing against `Binary` and, where an `app` can
154+
// stand in for it, adds `Application` guarded by the row's actual form.
155+
bool is_program() const { return kind == Binary || kind == Application; }
138156
std::string soname; // ABI name for shared libraries, e.g. libfoo.so.1
139157
// WHICH SYMBOLS THIS ARTIFACT PUBLISHES. Empty = every symbol, which is
140158
// what both platforms do today (ELF default visibility; PE gets an

‎modules/manifest/src/xpkg.cppm‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,10 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
15661566
else if (k == "bin" || k == "binary") t.kind = Target::Binary;
15671567
else if (k == "shared" || k == "dylib"
15681568
|| k == "so" || k == "shlib") t.kind = Target::SharedLibrary;
1569+
// #622 A3: a descriptor's target can be `app` too --
1570+
// read exactly like `mcpp.toml` (`toml.cppm`), so a
1571+
// Form B package can publish an application.
1572+
else if (k == "app" || k == "application") t.kind = Target::Application;
15691573
} else if (sub == "main") {
15701574
t.main = cur.read_string();
15711575
} else if (sub == "soname") {

‎modules/toolchain-model/src/triple.cppm‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,31 @@ inline ArtifactNaming artifact_naming(const Triple& t, const ArtifactNaming& hos
11581158
return hostNaming;
11591159
}
11601160

1161+
// ── The link form of `kind = "app"` ──────────────────────────────────────────
1162+
//
1163+
// #622 A3. An application is "the thing a user launches" on every row, but
1164+
// what that MEANS is not the same file shape everywhere: on Android there is
1165+
// no executable form of an application at all -- the platform loads a shared
1166+
// library into a Java process (`System.loadLibrary`, `android:name` in the
1167+
// manifest) -- while on ELF, PE, Mach-O and the Emscripten row it is an
1168+
// ordinary executable, exactly like `kind = "bin"`.
1169+
//
1170+
// This is a property of the ROW ALONE, the same reason `artifact_naming` sits
1171+
// here rather than in the manifest: the manifest says what the author means
1172+
// (`app`), and the engine answers how THIS row spells it. A manifest that
1173+
// tried to say this itself would need a predicate over `[targets.<name>]`,
1174+
// which is exactly the shape §2.3 of the #622 design record rejects.
1175+
enum class ApplicationForm { Executable, SharedObject };
1176+
1177+
inline ApplicationForm application_form(const Triple& t) {
1178+
// `env == "android"` is the same test `is_android()` uses (and the same
1179+
// one `artifact_naming` would use for an Android row, if it had one) --
1180+
// deliberately not `os == "linux"`, which would also catch a plain
1181+
// ELF/glibc or musl row that has an ordinary executable `main`.
1182+
return t.env == "android" ? ApplicationForm::SharedObject
1183+
: ApplicationForm::Executable;
1184+
}
1185+
11611186
} // namespace mcpp::toolchain::triple
11621187

11631188
namespace mcpp::toolchain::triple {

‎src/build/execute.cppm‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,33 @@ export int build_run_target(const std::optional<std::string>& targetName,
17151715
if (targetName) break;
17161716
}
17171717
if (!chosen) {
1718+
// #622 A3/A10: an `app` whose form on THIS row is a shared library
1719+
// never becomes a `LinkUnit::Binary`, so the loop above cannot find
1720+
// it — that is correct (there is no executable to exec), but "no
1721+
// binary target 'myapp' found" would blame the user for a name that
1722+
// does exist. Read the manifest directly and, when that is exactly
1723+
// why the search came up empty, name the actual reason and the way
1724+
// out (`--format`, §2.10) instead.
1725+
for (auto const& t : ctx->manifest.targets) {
1726+
if (t.kind != mcpp::manifest::Target::Application) continue;
1727+
if (targetName && t.name != *targetName) continue;
1728+
auto triple = mcpp::toolchain::triple::parse(ctx->tc.targetTriple);
1729+
if (mcpp::toolchain::triple::application_form(
1730+
triple ? *triple : mcpp::toolchain::triple::Triple{})
1731+
!= mcpp::toolchain::triple::ApplicationForm::SharedObject)
1732+
continue;
1733+
std::string formats;
1734+
for (auto const& f : ctx->plan.providedPackFormats)
1735+
formats += (formats.empty() ? "" : ", ") + f;
1736+
std::println(stderr,
1737+
"error: '{}' is an application, and on {} an application is "
1738+
"a shared library that a package installs. Run it through a "
1739+
"distributable: mcpp run --format <name>, where <name> is "
1740+
"one of: {}",
1741+
t.name, ctx->tc.targetTriple,
1742+
formats.empty() ? std::string("none declared") : formats);
1743+
return 2;
1744+
}
17181745
std::println(stderr, "error: no binary target {}",
17191746
targetName ? std::format("'{}' found", *targetName) : "in this package");
17201747
return 2;

‎src/build/plan.cppm‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -482,13 +482,22 @@ mcpp::toolchain::triple::ArtifactNaming naming_for(const mcpp::toolchain::Toolch
482482
// Reading the host constants here made ninja declare an output the compiler
483483
// never writes (Linux -> PE: declared `bin/foo`, produced `bin/foo.exe`), so
484484
// the link edge could never be satisfied and reran on every build.
485+
//
486+
// `asSharedObject` (#622 A3) is set by the caller for an `Application` target
487+
// exactly when `toolchain::triple::application_form(triple)` answered
488+
// `SharedObject` — the file name derives from the link FORM, and this
489+
// function has no triple of its own to ask, by design (it is called on the
490+
// dependency-target axis too, where the naming has already been resolved).
491+
// It is inert for every other kind.
485492
std::filesystem::path target_output(const mcpp::manifest::Target& t,
486-
const mcpp::toolchain::triple::ArtifactNaming& n) {
493+
const mcpp::toolchain::triple::ArtifactNaming& n,
494+
bool asSharedObject = false) {
487495
if (t.kind == mcpp::manifest::Target::Library) {
488496
return std::filesystem::path("bin") /
489497
std::format("{}{}{}", n.libPrefix, t.name, n.staticLibExt);
490498
}
491-
if (t.kind == mcpp::manifest::Target::SharedLibrary) {
499+
if (t.kind == mcpp::manifest::Target::SharedLibrary
500+
|| (t.kind == mcpp::manifest::Target::Application && asSharedObject)) {
492501
return std::filesystem::path("bin") /
493502
std::format("{}{}{}", n.libPrefix, t.name, n.sharedLibExt);
494503
}
@@ -1370,7 +1379,11 @@ make_plan(const mcpp::manifest::Manifest& manifest,
13701379
// disambiguate the common single-binary project).
13711380
for (auto& t : manifest.targets) {
13721381
if (t.main.empty()) continue;
1373-
if (t.kind != mcpp::manifest::Target::Binary
1382+
// `is_program()` (#622 A3): an `app` also has an entry `main` --
1383+
// compiled as an ordinary executable entry on every row but Android,
1384+
// and as a translation unit of the shared library there -- so it
1385+
// occupies an object address in this census exactly as `bin` does.
1386+
if (!t.is_program()
13741387
&& t.kind != mcpp::manifest::Target::TestBinary) continue;
13751388
auto entry = projectRoot / t.main;
13761389
if (scannedSources.contains(entry)) continue;
@@ -1792,7 +1805,26 @@ make_plan(const mcpp::manifest::Manifest& manifest,
17921805
lu.kind = LinkUnit::TestBinary;
17931806
lu.output = target_output(t, naming);
17941807
if (!t.main.empty()) lu.entryMain = projectRoot / t.main;
1808+
} else if (t.kind == mcpp::manifest::Target::Application
1809+
&& mcpp::toolchain::triple::application_form(targetTriple)
1810+
== mcpp::toolchain::triple::ApplicationForm::SharedObject) {
1811+
// #622 A3: on `*-linux-android` an `app` links as `shared` does --
1812+
// this row is the one case the form function answers
1813+
// `SharedObject` for. `main` still names the file compiled INTO
1814+
// the library (its actual entry is the platform's JNI/
1815+
// NativeActivity contract, not mcpp's), so it is synthesized the
1816+
// same way a `Binary`'s entry is, below.
1817+
lu.kind = LinkUnit::SharedLibrary;
1818+
lu.output = target_output(t, naming, /*asSharedObject=*/true);
1819+
lu.importLibrary = import_library_for(t, naming);
1820+
lu.soname = t.soname;
1821+
lu.exportPatterns = t.exportPatterns;
1822+
lu.runtimeAliases = runtime_aliases_for_target(t, naming);
1823+
if (!t.main.empty()) lu.entryMain = projectRoot / t.main;
17951824
} else {
1825+
// `Binary`, or an `Application` whose row form is `Executable`
1826+
// (every row this record verifies except Android) -- identical
1827+
// to `bin` in every respect, which is the point of the kind.
17961828
lu.kind = LinkUnit::Binary;
17971829
lu.output = target_output(t, naming);
17981830
if (!t.main.empty()) lu.entryMain = projectRoot / t.main;
@@ -1817,7 +1849,13 @@ make_plan(const mcpp::manifest::Manifest& manifest,
18171849
// false → inline (the pre-archive behavior, always provides the entry).
18181850
bool entryDefinesMain = lu.entryMain && source_defines_main(*lu.entryMain);
18191851

1820-
if ((lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary) && lu.entryMain) {
1852+
// #622 A3: a `SharedLibrary` unit only ever carries `entryMain` when
1853+
// it is an `app` on the Android row (an ordinary `kind = "shared"`
1854+
// target never sets it) -- so this condition widens to synthesize
1855+
// that translation unit exactly as `Binary`'s is, without touching
1856+
// an ordinary shared library's compile set.
1857+
if ((lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary
1858+
|| lu.kind == LinkUnit::SharedLibrary) && lu.entryMain) {
18211859
// Synthesize the entry main's compile unit. Its object path is
18221860
// NOT computed here — it comes from the shared `object_for`
18231861
// disambiguator below, so the link input matches the compile edge

0 commit comments

Comments
 (0)