Skip to content
Merged
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
2 changes: 1 addition & 1 deletion 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.21.0", features = ["rules-spirv"], host-module = true }
plugins = { version = "0.22.0", features = ["rules-spirv"], host-module = true }
```

```cpp
Expand Down
127 changes: 106 additions & 21 deletions dist/web.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ struct options {
// Where the produced directory lands. Empty means `<out_dir>/web`.
std::string output;
std::string out_dir = std::string(mcpp::out_dir());

// A runner named `web` (0.22.0): `mcpp run --pkg-format web` serves the
// directory on this machine and opens the page in a browser, through the
// `emrun` the Emscripten toolchain carries, until the operator ends it
// (a long-lived runner). It listens on `localhost` only; a machine that
// wants another browser, port or address replaces the runner in its
// `config.toml` (`[target.wasm32-emscripten.runners] web = [...]`).
bool runner = true;
};

// ─── The plan ──────────────────────────────────────────────────────────────
Expand All @@ -146,6 +154,7 @@ struct plan {
std::string reason;
std::string web_dir; // <out_dir>/web
std::string stage_bin; // ${mcpp.stage_dir}/bin, kept for the floor check
std::string page; // the page, the format's distributable
std::vector<step> steps;
explicit operator bool() const { return applies; }
};
Expand Down Expand Up @@ -244,27 +253,27 @@ inline plan plan_for(options opt = {}) {
// something that does not exist there, and the engine has already
// accepted the value because the graph declared it.
if (const std::string os = mcpp::target_os(); os != "emscripten") {
std::cerr << std::format(
mcpp::warning(std::format(
"mcpp.dist.web: a web directory wraps a wasm32-emscripten "
"program, and this build targets '{}'.\n"
" use: --format tar, or build for --target wasm32-emscripten",
os.empty() ? "unknown" : os) << '\n';
"program, and this build targets '{}'; use --pkg-format tar, or "
"build for --target wasm32-emscripten",
os.empty() ? "unknown" : os).c_str());
p.reason = "not the emscripten target";
return p;
}

const std::string stage = mcpp::pack_stage_dir();
if (stage.empty()) {
std::cerr << "mcpp.dist.web: mcpp reported no staged tree. This "
"member needs mcpp 2026.9.12.2 or newer.\n";
mcpp::warning("mcpp.dist.web: mcpp reported no staged tree. This "
"member needs mcpp 2026.9.12.2 or newer.");
p.reason = "no staged tree";
return p;
}

const std::string target = target_for(opt);
if (target.empty()) {
std::cerr << "mcpp.dist.web: no target to wrap. Set "
"`options::target` to the program target's name.\n";
mcpp::warning("mcpp.dist.web: no target to wrap. Set "
"`options::target` to the program target's name.");
p.reason = "no target";
return p;
}
Expand All @@ -277,18 +286,17 @@ inline plan plan_for(options opt = {}) {
const std::string stageBin = stage + "/bin";
const std::string launcher = stageBin + "/" + target + ".js";
if (!is_file(launcher)) {
std::cerr << std::format(
mcpp::warning(std::format(
"mcpp.dist.web: the staged tree at {0} carries no launcher for "
"target '{1}'.\n"
" expected: {0}/{1}.js",
stageBin, target) << '\n';
"target '{1}' (expected {0}/{1}.js); name the program with "
"`options::target`",
stageBin, target).c_str());
p.reason = "no launcher in the staged tree";
return p;
}

const auto pageFile = page_name(opt);
if (!pageFile) {
std::cerr << "mcpp.dist.web: " << pageFile.error() << '\n';
mcpp::warning(("mcpp.dist.web: " + pageFile.error()).c_str());
p.reason = "page name refused";
return p;
Expand All @@ -298,8 +306,8 @@ inline plan plan_for(options opt = {}) {
const std::string tpl =
(std::filesystem::path(mcpp::manifest_dir()) / opt.template_file).string();
if (!is_file(tpl)) {
std::cerr << std::format(
"mcpp.dist.web: the template {} was not found", tpl) << '\n';
mcpp::warning(std::format(
"mcpp.dist.web: the template {} was not found", tpl).c_str());
p.reason = "template not found";
return p;
}
Expand All @@ -325,12 +333,14 @@ inline plan plan_for(options opt = {}) {
: (std::filesystem::path(opt.out_dir) / "web").string();

// The side file carries the page name, so two packs of one program with
// different pages do not overwrite each other's rendering; the default
// keeps the name 0.11 wrote (`<target>-index.html`).
// different pages do not overwrite each other's rendering. It is the
// page's source, not a page (0.22.0): it lives in a hidden directory and
// ends in `.in`, because one written beside the build's outputs as
// `<target>-index.html` was opened as the product and loaded nothing.
const std::string indexSrc =
(std::filesystem::path(opt.out_dir) / (target + "-" + *pageFile)).string();
(std::filesystem::path(opt.out_dir) / ".dist-web" / (target + "-" + *pageFile + ".in")).string();
if (!write_if_different(indexSrc, templateBytes)) {
std::cerr << std::format("mcpp.dist.web: cannot write {}", indexSrc) << '\n';
mcpp::warning(std::format("mcpp.dist.web: cannot write {}", indexSrc).c_str());
p.reason = std::format("cannot write {}", *pageFile);
return p;
}
Expand Down Expand Up @@ -362,8 +372,8 @@ inline plan plan_for(options opt = {}) {
// assertion, because a member's job on a broken invariant is to say
// so, not to crash the build program that is packaging someone
// else's project.
std::cerr << std::format(
"mcpp.dist.web: {} carries no files to stage", stageBin) << '\n';
mcpp::warning(std::format(
"mcpp.dist.web: {} carries no files to stage", stageBin).c_str());
p.reason = "staged bin/ is empty";
return p;
}
Expand Down Expand Up @@ -394,15 +404,23 @@ inline plan plan_for(options opt = {}) {
p.steps.push_back(std::move(s));
}

// THE PAGE IS THE DIRECTORY'S ONE DISTRIBUTABLE (0.22.0). A distributable
// is an output no other action consumes; each copied file was one, so the
// format had as many as the directory has files and `mcpp run --pkg-format
// web` refused to choose. The page's step takes every copied file as an
// input: it is ordered after them, and it is what a runner is handed.
step page;
page.id = "mcpp.dist.web.index";
page.role = "artifact";
page.description = "INDEX.HTML";
page.argv = { "${mcpp.self}", "stage", "--verify", "content", "--output",
webDir + "/" + *pageFile, indexSrc };
page.inputs = { indexSrc };
for (auto const& s : p.steps)
page.inputs.insert(page.inputs.end(), s.outputs.begin(), s.outputs.end());
page.outputs = { webDir + "/" + *pageFile };
p.steps.push_back(std::move(page));
p.page = webDir + "/" + *pageFile;

p.applies = true;
return p;
Expand Down Expand Up @@ -437,6 +455,17 @@ inline bool submit(const plan& p) {
// catch "nothing was linked or staged", not to police a size budget. A
// wasm launcher plus its module is comfortably larger than this on any
// program that links `import std`.
// HOW THE DIRECTORY IS OPENED, said once it is declared: a browser loads
// the module only over HTTP, and a page opened from disk loads nothing
// without saying why.
{
static std::string message, hint;
message = std::format("the web directory is {}", p.web_dir);
hint = std::format("open it over HTTP: `mcpp run --pkg-format web` serves it and opens {}, "
"or serve the directory with any static server; a page opened from disk "
"cannot load its .wasm", std::filesystem::path(p.page).filename().string());
mcpp::report({.severity = "note", .message = message.c_str(), .hint = hint.c_str()});
}
if (bytes < 4u * 1024u) {
static char msg[512];
std::snprintf(msg, sizeof msg,
Expand All @@ -448,14 +477,70 @@ inline bool submit(const plan& p) {
return true;
}

// ─── The runner ────────────────────────────────────────────────────────────

// THE RUNNER NAMED FOR THE FORMAT (0.22.0). `mcpp run --pkg-format web` hands
// the format's distributable -- the page -- to the runner named `web`. emrun,
// which the Emscripten toolchain carries, serves the page's directory with the
// types a browser requires (`application/wasm`), opens the page, and serves
// until the operator ends it; it does not end by itself, so the runner is
// declared long-lived. `--hostname localhost`: emrun listens on every address
// unless told otherwise, and a development server is not offered to the
// network by default.
//
// Declared on every pass of the wasm32-emscripten row, as the format itself
// is: `mcpp run` reads it from a build that is not packaging. On Windows the
// toolchain carries the script as `emrun.bat`; the program is named by its
// path because it is not in the bin/ of a payload, where the engine looks for
// a runner's bare name.
inline void declare_runner() {
if (mcpp::target_os() != std::string_view("emscripten")) return;
const char* root = mcpp::toolchain_dir();
if (!root || !*root) return;
const std::string host = mcpp::host();
const bool windows = host.find("windows") != std::string::npos;
static std::string program;
program = (std::filesystem::path(root) / "emscripten" / (windows ? "emrun.bat" : "emrun"))
.generic_string();
if (!is_file(program)) {
static std::string why;
why = std::format("mcpp.dist.web: the toolchain at {} carries no {}, so `mcpp run "
"--pkg-format web` has no runner", root,
std::filesystem::path(program).filename().string());
mcpp::warning(why.c_str());
return;
}
for (const char* tok : {program.c_str(), "--hostname", "localhost", "{}"})
mcpp::runner("web", tok);
mcpp::runner_longlived("web");
}

// ─── The one call a consumer makes ─────────────────────────────────────────

// THE PROGRAM THE DIRECTORY WRAPS, STATED TO THE ENGINE (0.22.1; mcpp
// 2026.11.16.1, protocol 19). `mcpp pack --pkg-format web` with no program
// named packs the program `options::target` names, as `mcpp pack <program>`
// would: a package of several programs packed its first, which this format
// then did not find. Stated when the engine's module has the call; an older
// engine keeps its own choice, which `mcpp pack <program>` overrides.
namespace pack_program_probe {
using namespace mcpp;
template <class S>
concept stated = requires(S s) { pack_program(s); };
template <class S>
void state(S program) {
if constexpr (stated<S>) pack_program(program);
}
} // namespace pack_program_probe

// DECLARE UNCONDITIONALLY, SUBMIT CONDITIONALLY -- and both halves are here
// so a consumer cannot do one without the other. See `dist/appimage.cppm`'s
// own comment on this function for why the declaration cannot be gated the
// same way `plan_for` gates its submission.
inline bool generate(options opt = {}) {
mcpp::provides_pack_format("web");
if (!opt.target.empty()) pack_program_probe::state(opt.target.c_str());
if (opt.runner) declare_runner();
return submit(plan_for(std::move(opt)));
}

Expand Down
6 changes: 6 additions & 0 deletions docs/dist.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ Module `mcpp.dist.wix`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.11.1
Module `mcpp.dist.web`; engine floor: 2026.9.13.1, the release that carries `${mcpp.self}` and `mcpp stage`'s argument shape as an engine contract (`stage --verify content --output <dst> <src>`) -- what lets this member's copy run on every host mcpp does, Windows included, in place of the `cp` this member used through 0.8.0.

**Needs and behaviour.** nothing beyond mcpp: `wasm32-emscripten` only. Copies `${mcpp.stage_dir}/bin/` -- the `.js` launcher, the implicit `.wasm`, the `.data` when present, and every `mcpp::deploy`'d file, all of which #622 A5 and A4 already stage there -- to `<out_dir>/web/`, dropping the `bin/` prefix a browser has no use for, and writes an `index.html` rendered from a project template or a built-in default that loads the script with a plain `<script src>`. Each staged file and the rendered page are copied with `${mcpp.self} stage --verify content --output <dst> <src>`, the same copier every `stage_file` edge in `build.ninja` already runs -- no host-specific copy tool, no plan-time `create_directories` (`stage` creates the destination's parent), and a second `mcpp pack --format web` with nothing changed copies nothing. From 0.12.0 `options::page` names the page (`index.html` by default), for a project whose other build names it after the target; a name that is not a bare `*.html` file name is refused at plan time

**The page, the runner and where the directory is (0.22.0).** The page is the format's one distributable: its step takes every copied file as an input, so no copied file is an output nothing consumes, and `mcpp run --pkg-format web` is handed the page (0.21.0 declared each copied file a distributable, and the engine refused to choose among them). The page's source is written to `<out_dir>/.dist-web/<target>-<page>.in`, not beside the build's outputs as `<target>-index.html`, where it was opened as the product and loaded nothing. When `mcpp pack --pkg-format web` declares the directory, a note names it and says how it is opened: a browser loads the module only over HTTP, and a page opened from disk cannot load its `.wasm`. When the member declines a request (another target, no launcher for the program it wraps, a page name it refuses), it states the reason as a warning, which a build program's successful run shows and its standard error does not; mcpp 2026.11.16.1 and later repeat it in the refusal of a pack that produced nothing. With mcpp 2026.11.16.1 and later (protocol 19) it also states the program `options::target` names to the engine (`mcpp::pack_program`), so `mcpp pack --pkg-format web` in a package with several programs packs that one without the program being named; an older engine keeps its own choice.

`options::runner` (default true) declares a long-lived runner named `web`: `emrun` from the resolved Emscripten toolchain (`<toolchain_dir>/emscripten/emrun`, `emrun.bat` on Windows), given `--hostname localhost` and the page. `mcpp run --pkg-format web` therefore serves the directory on this machine with the types a browser requires (`application/wasm`), opens the page in the default browser, and serves until the operator ends it. The server listens on `localhost` only; a machine that wants another browser, port or address replaces the runner in its `config.toml` (`[target.wasm32-emscripten.runners] web = ["<emsdk>/emscripten/emrun", "--port", "8080", "{}"]`). Measured on Linux: the module is served as `application/wasm`; on a machine with no browser emrun says so and keeps serving.

**Link arguments are not a function of the package format.** A build program that branches on `mcpp::pack_format()` to change link arguments (`-sSINGLE_FILE=1` for one format and not another) changes nothing: `mcpp pack` stages the program the build already linked, and its second pass does not link again. A link difference is a profile's (`[profile.web] ldflags`, selected with `--profile web`), which also names the build directory, so the two links do not overwrite each other.
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.21.0", features = ["plugins-core"], host-module = true, reexport = true }
plugins = { version = "0.22.0", features = ["plugins-core"], host-module = true, reexport = true }
```

`reexport = true` is needed when the plugin's consumers import L2 modules in
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "plugins"
namespace = "mcpp"
version = "0.21.0"
version = "0.22.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
Expand Down
2 changes: 1 addition & 1 deletion src/plugins.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,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.21.0";
inline constexpr std::string_view version = "0.22.0";

} // namespace mcpp::plugins

Expand Down
33 changes: 33 additions & 0 deletions tests/web-consumer/check-web-plan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,39 @@ out=$(node "$WEB/web-consumer.js")
[ "$out" = "1-2-3" ] || fail "node did not print 1-2-3 (got: $out)"
echo "ok: node $WEB/web-consumer.js prints 1-2-3"

echo "== the page is the one distributable, and its source is not mistaken for it (0.22.0) =="
grep -q "Packed .*/web/index.html" pack.log || fail "mcpp pack did not name index.html as the distributable" pack.log
grep -q "note: .*the web directory is" pack.log || fail "mcpp pack did not say where the web directory is" pack.log
grep -q "open it over HTTP" pack.log || fail "mcpp pack did not say how the directory is opened" pack.log
stray=$(find target -name '*-index.html' -not -path '*/.dist-web/*')
[ -z "$stray" ] || fail "a page source lies beside the build's outputs as a page: $stray"
echo "ok: one distributable (index.html); the page's source is .dist-web/*.html.in"

echo "== mcpp run --pkg-format web serves the page through the runner named web (0.22.0) =="
"$MCPP" run --target wasm32-emscripten --list-runners > runners.log 2>&1 || fail "--list-runners failed" runners.log
grep -E '^ +web +.*emrun.*long-lived' runners.log || fail "no long-lived runner named web" runners.log
# No browser on a CI machine: emrun says so and keeps serving, which is what
# is measured -- the page and the module, with the type a browser requires.
# mcpp becomes the runner (its image replaced on POSIX), so the started
# process is the server, and an interrupt is the operator ending it.
"$MCPP" run --pkg-format web --target wasm32-emscripten > serve.log 2>&1 &
server=$!
served=
for i in $(seq 1 45); do
if curl -fsS -o target/served.wasm -w '%{content_type}' http://localhost:6931/web-consumer.wasm > target/served.type 2>/dev/null; then served=1; break; fi
sleep 1
done
[ -n "$served" ] || fail "nothing was served on localhost:6931" serve.log
grep -q 'application/wasm' target/served.type || fail "the module was not served as application/wasm" target/served.type
curl -fsS http://localhost:6931/index.html | grep -q 'web-consumer.js' || fail "the page was not served" serve.log
grep -q 'localhost' serve.log || fail "the server does not say it listens on localhost" serve.log
# A background job of a script ignores SIGINT (POSIX), so the operator's end
# is stated as SIGTERM here.
kill -TERM "$server"
for i in $(seq 1 20); do kill -0 "$server" 2>/dev/null || break; sleep 1; done
kill -0 "$server" 2>/dev/null && { kill -KILL "$server"; fail "the server did not end when interrupted" serve.log; }
echo "ok: the page and its module are served on localhost (application/wasm)"

echo "== options::page names the page, and refuses a name that is not a bare *.html file =="
# A copy of this fixture beside it, so its path dependency on the collection
# (`../..`) still resolves; the copy's build program sets `options::page`.
Expand Down
Loading