feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
|
|
|
|
// Wires Crafter.Debug into a consumer's build. In the consumer's project.cpp:
|
|
|
|
|
//
|
|
|
|
|
// #pragma crafter dependency "https://forgejo.catcrafts.net/Catcrafts/Crafter.Debug.git" project "tooling/project.cpp"
|
|
|
|
|
// import Crafter.Debug.Build;
|
|
|
|
|
// ...
|
2026-11-01 03:21:41 +01:00
|
|
|
// const bool debugApi = Debug::Use(cfg, { .enabled = opts.Has("--debug-api"), .flag = "--debug-api",
|
|
|
|
|
// .define = "MYAPP_DEBUG_API", .args = depArgs });
|
|
|
|
|
// if (debugApi) { /* add the debug-only sources */ }
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
//
|
|
|
|
|
// crafter-build builds this module for the host and links it into the
|
|
|
|
|
// consumer's project.so, the way Crafter.LintRules gets in, so Use runs each
|
|
|
|
|
// time crafter-build loads the consumer: for every verb, before anything is
|
|
|
|
|
// built. With the consumer's flag on, Use adds the Crafter.Debug library from
|
|
|
|
|
// the checkout this module was built from, and writes build/crafter-debug, the
|
|
|
|
|
// launcher for the client that library builds. The client is one of the
|
|
|
|
|
// library's dependencies, so the build that follows compiles it in the same
|
|
|
|
|
// pass as the app, from the same sources.
|
|
|
|
|
//
|
|
|
|
|
// The launcher is a POSIX sh script, because what it starts has to run where
|
|
|
|
|
// it was built. A client built inside the CrafterBuild Flatpak links the libc++
|
|
|
|
|
// in that Flatpak's /app/lib against the runtime's glibc, so on the host the
|
|
|
|
|
// launcher starts it through `flatpak run`: nothing the Flatpak compiled runs
|
|
|
|
|
// on the host itself. The script keeps to what every /bin/sh on Linux shares
|
|
|
|
|
// (test, command -v, grep -q, exec "$@"), and every path in it is quoted.
|
|
|
|
|
export module Crafter.Debug.Build;
|
|
|
|
|
import std;
|
|
|
|
|
import Crafter.Build;
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
namespace Crafter::Debug {
|
|
|
|
|
// The checkout this module was built from. crafter-build compiles module
|
|
|
|
|
// sources from absolute paths, and this file is
|
|
|
|
|
// <checkout>/tooling/interfaces/Crafter.Debug.Build.cppm.
|
|
|
|
|
fs::path Checkout() {
|
|
|
|
|
return fs::path(std::source_location::current().file_name()).parent_path().parent_path().parent_path();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The value of `key` in `section` of an INI-style file such as
|
|
|
|
|
// /.flatpak-info, or "" when either is missing.
|
|
|
|
|
std::string IniValue(std::string_view text, std::string_view section, std::string_view key) {
|
|
|
|
|
bool inSection = false;
|
|
|
|
|
for (const auto part : std::views::split(text, '\n')) {
|
|
|
|
|
const std::string_view line(std::ranges::data(part), std::ranges::size(part));
|
|
|
|
|
if (line.starts_with('[')) {
|
|
|
|
|
inSection = line.size() == section.size() + 2 && line.substr(1, section.size()) == section && line.back() == ']';
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (inSection && line.size() > key.size() && line.starts_with(key) && line[key.size()] == '=') {
|
|
|
|
|
return std::string(line.substr(key.size() + 1));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The Flatpak crafter-build runs in, as its /.flatpak-info describes it:
|
|
|
|
|
// the app id and the branch. Both empty when crafter-build runs natively.
|
|
|
|
|
struct Sandbox {
|
|
|
|
|
std::string app;
|
|
|
|
|
std::string branch;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Sandbox CurrentSandbox() {
|
|
|
|
|
std::ifstream in("/.flatpak-info");
|
|
|
|
|
if (!in) return {};
|
|
|
|
|
const std::string text((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
|
|
|
|
return { IniValue(text, "Application", "name"), IniValue(text, "Instance", "branch") };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A target the debug server can run on: Linux, not the browser.
|
|
|
|
|
bool NativeLinux(std::string_view target) {
|
|
|
|
|
return target.find("-linux-") != std::string_view::npos && !target.starts_with("wasm");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The launcher for `client`. Inside the Flatpak that built the client it
|
|
|
|
|
// runs it directly; on the host it starts it in that Flatpak, on the
|
|
|
|
|
// branch that built it, so the client gets the runtime it was linked
|
|
|
|
|
// against; from another app's sandbox it goes through flatpak-spawn.
|
|
|
|
|
// `--die-with-parent` ends the client when whatever started the launcher
|
|
|
|
|
// is killed, a timed-out `wait` or a Monitor-stopped `log --follow`.
|
feat(cli): pick the app of the project the launcher belongs to
Discovery is per user, so every project's launcher sees every debug app
running, and with two of them up every command refused to guess and
asked for -i, though the agent in one project meant its own app. Two
copies of the example, both named basic, could only be told apart by
id.
The launcher now exports CRAFTER_DEBUG_PROJECT, its project's directory
(flatpak-spawn gets it spelled out), and the client counts an app as
the project's when its executable lies under it, as `-r` runs it from
<project>/bin/<variant>/. With other projects' apps running, commands
pick this project's; two copies of it still need -i. Another project's
app is never picked by default: with this project's app not up yet, or
crashed, the agent would otherwise drive, or quit, someone else's. It
is listed instead, for -i.
`ls` lists this project's apps first, marked, `--json` says which are,
and `--wait` waits for one of them rather than any app; the exit code
says whether one runs. A client started without the launcher has no
project and picks the one app running, as before, and the command the
protocol check prints for an older app's own client now names it with
-i.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:33:59 +01:00
|
|
|
//
|
|
|
|
|
// It tells the client which project it belongs to: with apps of other
|
|
|
|
|
// projects running too, the client picks this project's and leaves the
|
|
|
|
|
// others to -i. flatpak run passes the environment on; flatpak-spawn
|
|
|
|
|
// needs it spelled out.
|
|
|
|
|
std::string LauncherText(const fs::path& client, const Sandbox& sandbox, std::string_view flag, const fs::path& project) {
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
std::string text = std::format(
|
|
|
|
|
"#!/bin/sh\n"
|
|
|
|
|
"# Written by Crafter.Debug (Debug::Use) each time crafter-build loads this\n"
|
|
|
|
|
"# project with {0}: runs the crafter-debug client that build compiled.\n"
|
|
|
|
|
"client={1}\n"
|
feat(cli): pick the app of the project the launcher belongs to
Discovery is per user, so every project's launcher sees every debug app
running, and with two of them up every command refused to guess and
asked for -i, though the agent in one project meant its own app. Two
copies of the example, both named basic, could only be told apart by
id.
The launcher now exports CRAFTER_DEBUG_PROJECT, its project's directory
(flatpak-spawn gets it spelled out), and the client counts an app as
the project's when its executable lies under it, as `-r` runs it from
<project>/bin/<variant>/. With other projects' apps running, commands
pick this project's; two copies of it still need -i. Another project's
app is never picked by default: with this project's app not up yet, or
crashed, the agent would otherwise drive, or quit, someone else's. It
is listed instead, for -i.
`ls` lists this project's apps first, marked, `--json` says which are,
and `--wait` waits for one of them rather than any app; the exit code
says whether one runs. A client started without the launcher has no
project and picks the one app running, as before, and the command the
protocol check prints for an older app's own client now names it with
-i.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:33:59 +01:00
|
|
|
"CRAFTER_DEBUG_PROJECT={2}\n"
|
|
|
|
|
"export CRAFTER_DEBUG_PROJECT\n"
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
"if [ ! -x \"$client\" ]; then\n"
|
fix(tooling): build first, then start the app, then list it
The notes had agents start `crafter-build <flag> -r` in the background
and run `crafter-debug ls --wait 60s` straight after. That raced the
build: the first agent to follow them ran the plain build's stub, which
said "no debug build yet" and exited, since the client `ls --wait` needs
is one of the things the debug build compiles, and it polled by hand
until the build was done.
The race came from the order, so the order changes: build with the flag
in the foreground, which ends once the app and the client are built,
then start the app with -r in the background, then `ls --wait`, which
now only waits for what it is for, the app registering. A launcher that
polls for the build instead was tried and dropped: a shell loop that
reruns itself each second, and waits out its whole --wait when no build
is coming at all.
The consumer notes, the client's guide and the README say so, and the
stub and the not-yet-built launcher name the step that was skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:24:35 +01:00
|
|
|
" echo \"crafter-debug: the client is not built yet; let crafter-build {0} finish first, which builds it with the app\" >&2\n"
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
" exit 2\n"
|
|
|
|
|
"fi\n",
|
feat(cli): pick the app of the project the launcher belongs to
Discovery is per user, so every project's launcher sees every debug app
running, and with two of them up every command refused to guess and
asked for -i, though the agent in one project meant its own app. Two
copies of the example, both named basic, could only be told apart by
id.
The launcher now exports CRAFTER_DEBUG_PROJECT, its project's directory
(flatpak-spawn gets it spelled out), and the client counts an app as
the project's when its executable lies under it, as `-r` runs it from
<project>/bin/<variant>/. With other projects' apps running, commands
pick this project's; two copies of it still need -i. Another project's
app is never picked by default: with this project's app not up yet, or
crashed, the agent would otherwise drive, or quit, someone else's. It
is listed instead, for -i.
`ls` lists this project's apps first, marked, `--json` says which are,
and `--wait` waits for one of them rather than any app; the exit code
says whether one runs. A client started without the launcher has no
project and picks the one app running, as before, and the command the
protocol check prints for an older app's own client now names it with
-i.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:33:59 +01:00
|
|
|
flag, ShellQuote(client.string()), ShellQuote(project.string()));
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
if (sandbox.app.empty()) {
|
|
|
|
|
text += "exec \"$client\" \"$@\"\n";
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
const std::string run = std::format("flatpak run --die-with-parent --branch={} --command=\"$client\" {} \"$@\"",
|
|
|
|
|
ShellQuote(sandbox.branch), ShellQuote(sandbox.app));
|
|
|
|
|
text += std::format(
|
|
|
|
|
"if grep -qsx {0} /.flatpak-info; then exec \"$client\" \"$@\"; fi\n"
|
|
|
|
|
"if command -v flatpak >/dev/null 2>&1; then exec {1}; fi\n"
|
feat(cli): pick the app of the project the launcher belongs to
Discovery is per user, so every project's launcher sees every debug app
running, and with two of them up every command refused to guess and
asked for -i, though the agent in one project meant its own app. Two
copies of the example, both named basic, could only be told apart by
id.
The launcher now exports CRAFTER_DEBUG_PROJECT, its project's directory
(flatpak-spawn gets it spelled out), and the client counts an app as
the project's when its executable lies under it, as `-r` runs it from
<project>/bin/<variant>/. With other projects' apps running, commands
pick this project's; two copies of it still need -i. Another project's
app is never picked by default: with this project's app not up yet, or
crashed, the agent would otherwise drive, or quit, someone else's. It
is listed instead, for -i.
`ls` lists this project's apps first, marked, `--json` says which are,
and `--wait` waits for one of them rather than any app; the exit code
says whether one runs. A client started without the launcher has no
project and picks the one app running, as before, and the command the
protocol check prints for an older app's own client now names it with
-i.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:33:59 +01:00
|
|
|
"if command -v flatpak-spawn >/dev/null 2>&1; then exec flatpak-spawn --host --env=CRAFTER_DEBUG_PROJECT=\"$CRAFTER_DEBUG_PROJECT\" {1}; fi\n"
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
"echo \"crafter-debug: needs flatpak to start $client\" >&2\n"
|
|
|
|
|
"exit 1\n",
|
|
|
|
|
ShellQuote(std::format("name={}", sandbox.app)), run);
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// What build/crafter-debug says before the project has been built with
|
|
|
|
|
// its debug flag.
|
|
|
|
|
std::string StubText(std::string_view flag) {
|
|
|
|
|
return std::format(
|
|
|
|
|
"#!/bin/sh\n"
|
|
|
|
|
"# Written by Crafter.Debug (Debug::Use): this project has no debug build yet.\n"
|
fix(tooling): build first, then start the app, then list it
The notes had agents start `crafter-build <flag> -r` in the background
and run `crafter-debug ls --wait 60s` straight after. That raced the
build: the first agent to follow them ran the plain build's stub, which
said "no debug build yet" and exited, since the client `ls --wait` needs
is one of the things the debug build compiles, and it polled by hand
until the build was done.
The race came from the order, so the order changes: build with the flag
in the foreground, which ends once the app and the client are built,
then start the app with -r in the background, then `ls --wait`, which
now only waits for what it is for, the app registering. A launcher that
polls for the build instead was tried and dropped: a shell loop that
reruns itself each second, and waits out its whole --wait when no build
is coming at all.
The consumer notes, the client's guide and the README say so, and the
stub and the not-yet-built launcher name the step that was skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:24:35 +01:00
|
|
|
"echo \"crafter-debug: no debug build yet; build with: crafter-build {0}, then start the app in the background with: crafter-build {0} -r\" >&2\n"
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
"exit 2\n",
|
|
|
|
|
flag);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Replaces `file` with `text`, executable, unless it already holds it.
|
|
|
|
|
// Through a temporary name and a rename, so a launcher that is running
|
|
|
|
|
// never reads half a file. A failure is a warning: the build itself does
|
|
|
|
|
// not depend on the launcher.
|
|
|
|
|
void WriteIfChanged(const fs::path& file, const std::string& text) {
|
|
|
|
|
{
|
|
|
|
|
std::ifstream in(file, std::ios::binary);
|
|
|
|
|
if (in && std::string((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>()) == text) return;
|
|
|
|
|
}
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
fs::create_directories(file.parent_path(), ec);
|
|
|
|
|
std::random_device random;
|
|
|
|
|
fs::path temp = file;
|
|
|
|
|
temp += std::format(".{:08x}.tmp", random());
|
|
|
|
|
{
|
|
|
|
|
std::ofstream out(temp, std::ios::binary | std::ios::trunc);
|
|
|
|
|
out << text;
|
|
|
|
|
if (!out) {
|
|
|
|
|
std::println(std::cerr, "Crafter.Debug: could not write {}", temp.string());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fs::permissions(temp, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec, ec);
|
|
|
|
|
fs::rename(temp, file, ec);
|
|
|
|
|
if (ec) {
|
|
|
|
|
std::println(std::cerr, "Crafter.Debug: could not write {}: {}", file.string(), ec.message());
|
|
|
|
|
fs::remove(temp, ec);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export namespace Crafter::Debug {
|
|
|
|
|
struct UseOptions {
|
|
|
|
|
// Whether this build includes Crafter.Debug: the consumer's own flag,
|
|
|
|
|
// such as `opts.Has("--debug-api")`. Crafter.Debug never sees the flag.
|
|
|
|
|
bool enabled = false;
|
|
|
|
|
// That flag as the user types it, for the launcher's messages.
|
|
|
|
|
std::string flag = "--debug-api";
|
|
|
|
|
// The define the consumer's own sources test, such as "MYAPP_DEBUG_API".
|
|
|
|
|
std::string define;
|
|
|
|
|
// The args the consumer hands its other dependencies. A dependency
|
|
|
|
|
// two paths share must get the same list on both, or crafter-build
|
|
|
|
|
// refuses the graph ("module ... built twice").
|
|
|
|
|
std::vector<std::string> args;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Adds Crafter.Debug to `cfg` when options.enabled, and writes the
|
2026-11-01 03:21:41 +01:00
|
|
|
// launcher; see the comment at the top. Returns whether it added the
|
|
|
|
|
// library and the define, which is when the consumer adds its debug-only
|
|
|
|
|
// sources: not for every build with the flag, since a target other than
|
|
|
|
|
// native Linux gets neither. `at` defaults to the call site, which is how
|
|
|
|
|
// Use finds the consumer's directory.
|
|
|
|
|
bool Use(Configuration& cfg, const UseOptions& options, std::source_location at = std::source_location::current()) {
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
const fs::path launcher = fs::path(at.file_name()).parent_path() / "build" / "crafter-debug";
|
|
|
|
|
// Only for the project crafter-build was pointed at: a consumer loaded
|
|
|
|
|
// as someone else's dependency has no agent driving it from its own
|
|
|
|
|
// directory. Asked before LocalProject, which loads another project.
|
|
|
|
|
const bool topLevel = IsTopLevelProject();
|
|
|
|
|
if (!options.enabled || !NativeLinux(cfg.target)) {
|
|
|
|
|
if (topLevel && !fs::exists(launcher)) WriteIfChanged(launcher, StubText(options.flag));
|
2026-11-01 03:21:41 +01:00
|
|
|
return false;
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
}
|
|
|
|
|
Configuration* library = LocalProject({ .projectFile = Checkout() / "project.cpp", .args = options.args });
|
|
|
|
|
cfg.dependencies.push_back(library);
|
|
|
|
|
if (!options.define.empty()) cfg.defines.push_back({ options.define, "" });
|
2026-11-01 03:21:41 +01:00
|
|
|
if (!topLevel) return true;
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
const auto client = std::ranges::find(library->dependencies, std::string_view("crafter.debug-cli"),
|
|
|
|
|
[](const Configuration* dependency) { return std::string_view(dependency->name); });
|
|
|
|
|
if (client == library->dependencies.end()) {
|
|
|
|
|
throw std::runtime_error(std::format("Crafter.Debug: {} declares no crafter.debug-cli client", (Checkout() / "project.cpp").string()));
|
|
|
|
|
}
|
feat(cli): pick the app of the project the launcher belongs to
Discovery is per user, so every project's launcher sees every debug app
running, and with two of them up every command refused to guess and
asked for -i, though the agent in one project meant its own app. Two
copies of the example, both named basic, could only be told apart by
id.
The launcher now exports CRAFTER_DEBUG_PROJECT, its project's directory
(flatpak-spawn gets it spelled out), and the client counts an app as
the project's when its executable lies under it, as `-r` runs it from
<project>/bin/<variant>/. With other projects' apps running, commands
pick this project's; two copies of it still need -i. Another project's
app is never picked by default: with this project's app not up yet, or
crashed, the agent would otherwise drive, or quit, someone else's. It
is listed instead, for -i.
`ls` lists this project's apps first, marked, `--json` says which are,
and `--wait` waits for one of them rather than any app; the exit code
says whether one runs. A client started without the launcher has no
project and picks the one app running, as before, and the command the
protocol check prints for an older app's own client now names it with
-i.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:33:59 +01:00
|
|
|
WriteIfChanged(launcher, LauncherText((*client)->BinDir() / (*client)->outputName, CurrentSandbox(), options.flag, launcher.parent_path().parent_path()));
|
2026-11-01 03:21:41 +01:00
|
|
|
return true;
|
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
#pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.
Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.
The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
|
|
|
}
|
|
|
|
|
}
|