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
8 changes: 8 additions & 0 deletions platform/linux/Integration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ add_executable(RivetUpdateHandoffTest
target_compile_features(RivetUpdateHandoffTest PRIVATE cxx_std_17)
target_link_libraries(RivetUpdateHandoffTest PRIVATE rivet::platform)
target_compile_options(RivetUpdateHandoffTest PRIVATE -Wall -Wextra -Wpedantic)

add_executable(RivetLayoutTest
layout_test.cpp
)
target_compile_features(RivetLayoutTest PRIVATE cxx_std_17)
target_link_libraries(RivetLayoutTest PRIVATE rivet::platform)
target_compile_options(RivetLayoutTest PRIVATE -Wall -Wextra -Wpedantic)
target_compile_features(RivetGlobalShortcutsTest PRIVATE cxx_std_17)
target_include_directories(RivetGlobalShortcutsTest PRIVATE ../system)
target_compile_options(RivetGlobalShortcutsTest PRIVATE -Wall -Wextra -Wpedantic)
Expand Down Expand Up @@ -86,3 +93,4 @@ endif()

enable_testing()
add_test(NAME RivetUpdateHandoffTest COMMAND RivetUpdateHandoffTest)
add_test(NAME RivetLayoutTest COMMAND RivetLayoutTest)
70 changes: 70 additions & 0 deletions platform/linux/Integration/layout_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#include "layout.hpp"

#include <cassert>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
#include <unistd.h>

namespace fs = std::filesystem;

namespace {

void stage_layout(fs::path const& root, bool with_core) {
fs::create_directories(root / "runtime");
fs::create_directories(root / "res");
for (char const* boot :
{"petite.boot", "scheme.boot", "racket.boot"}) {
std::ofstream out(root / "runtime" / boot);
out << boot;
}
if (with_core) {
std::ofstream out(root / "res" / "core.zo");
out << "core";
}
}

} // namespace

int main() {
auto scratch = fs::temp_directory_path() /
("rivet-layout-test-" + std::to_string(::getpid()));
fs::create_directories(scratch);

// A complete layout is discovered with every path anchored at the root.
auto complete = scratch / "complete";
stage_layout(complete, true);
auto layout = rivet::linux_runtime::discover_layout(complete);
assert(layout.has_value());
assert(layout->stage_root == complete);
assert(layout->resources == complete / "res");
assert(layout->petite_boot == complete / "runtime" / "petite.boot");
assert(layout->scheme_boot == complete / "runtime" / "scheme.boot");
assert(layout->racket_boot == complete / "runtime" / "racket.boot");
assert(layout->core == complete / "res" / "core.zo");

// A missing backend bundle is not a layout, with or without the boots.
auto truncated = scratch / "truncated";
stage_layout(truncated, false);
assert(!rivet::linux_runtime::discover_layout(truncated).has_value());
auto empty = scratch / "empty";
fs::create_directories(empty);
assert(!rivet::linux_runtime::discover_layout(empty).has_value());

// The remediation hint stays displayable and names the expected files.
auto const& hint = rivet::linux_runtime::layout_error_hint();
assert(hint.find("runtime/*.boot") != std::string::npos);
assert(hint.find("res/core.zo") != std::string::npos);

// The executable's own path resolves on Linux (/proc/self/exe); other
// hosts throw, and this test still exercises the root-based discovery.
try {
assert(!rivet::linux_runtime::executable_path().empty());
} catch (...) {
}

std::error_code cleanup;
fs::remove_all(scratch, cleanup);
return 0;
}
44 changes: 43 additions & 1 deletion platform/linux/host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ platform/linux/
└── host/
├── GeneratedBackend.hpp # scaffold schema; raco rivet build replaces it
├── CMakeLists.txt
└── src/main.cpp # GTK4 counter demo over the generated client
└── src/
├── main.cpp # GTK4 counter demo over the generated client
└── rpc_bridge.hpp # completion → main-loop marshaling (below)
```

## Building by hand
Expand All @@ -61,6 +63,46 @@ and `res/core.zo` (produced by `raco ctool --mods`, the same artifacts every
platform host consumes) next to `RivetHost`. `raco rivet build`, `dev`, and
`package` create this layout automatically.

### Runtime layout discovery

Do not re-derive the layout in application code. The platform library owns
the contract (`rivet::linux_runtime`, `layout.hpp`):

```cpp
auto layout = rivet::linux_runtime::discover_layout();
if (!layout.has_value()) {
show(rivet::linux_runtime::layout_error_hint()); // names the missing files
}
// layout->petite_boot / scheme_boot / racket_boot / core / stage_root
```

`discover_layout()` looks beside the running executable; an explicit-root
overload exists for tests and unusual staging. `raco rivet build`, `dev`,
and `package` produce exactly the tree it looks for.

### Completions belong on the main loop

Backend completions and events arrive on the runtime's reader thread, and
nothing in that context may touch GTK objects. Use the shipped bridge
(`src/rpc_bridge.hpp`) instead of hand-rolling `g_idle_add` plumbing:

```cpp
#include "rpc_bridge.hpp"

void deliver_count(rivet_app::Result<std::int64_t> result) {
rivet_app::post_to_main(
[flat = rivet_app::unpack(std::move(result))]() mutable {
if (flat.ok) { set_count(flat.value); }
else { set_status("State error: " + flat.error); }
});
}
```

`unpack` flattens `Result<T>` into `Unpacked<T>` (value or error, decided on
the reader thread — no exception flow crosses the hop); `post_to_main` runs
the closure on the GTK main loop and frees it there. `main.cpp` shows the
pattern end to end.

## Display policy

Wayland is the primary Linux target. CI starts a headless Weston compositor,
Expand Down
88 changes: 20 additions & 68 deletions platform/linux/host/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>

#include "GeneratedBackend.hpp"
#include "layout.hpp"
#include "rpc_bridge.hpp"
#include "system_services.hpp"
#include "theme.hpp"

Expand Down Expand Up @@ -51,67 +50,19 @@ AppState g_state;

void perform_orderly_shutdown();

std::filesystem::path executable_path() {
return std::filesystem::read_symlink("/proc/self/exe");
}

// Rivet keeps runtime/res beside the executable in development and packages.
struct RuntimeLayout {
std::filesystem::path petite_boot;
std::filesystem::path scheme_boot;
std::filesystem::path racket_boot;
std::filesystem::path core;
};

std::optional<RuntimeLayout> discover_runtime_layout() {
std::filesystem::path const exe = executable_path();
std::filesystem::path const roots[] = {exe.parent_path()};
for (auto const& root : roots) {
RuntimeLayout layout{
root / "runtime" / "petite.boot",
root / "runtime" / "scheme.boot",
root / "runtime" / "racket.boot",
root / "res" / "core.zo",
};
if (std::filesystem::exists(layout.petite_boot) &&
std::filesystem::exists(layout.scheme_boot) &&
std::filesystem::exists(layout.racket_boot) &&
std::filesystem::exists(layout.core)) {
return layout;
}
}
return std::nullopt;
}

struct IntResult {
bool ok{false};
std::int64_t value{0};
std::string error;
};

int on_count_delivered(gpointer user_data) {
std::unique_ptr<IntResult> result(static_cast<IntResult*>(user_data));
if (result->ok) {
g_state.set_count(result->value);
gtk_widget_set_sensitive(GTK_WIDGET(g_state.increment), TRUE);
} else {
g_state.set_status("State error: " + result->error);
gtk_widget_set_sensitive(GTK_WIDGET(g_state.increment), TRUE);
}
return G_SOURCE_REMOVE;
}

// One completion, flattened once, dispatched once: unpack() runs on the
// backend reader thread, post_to_main moves the flattened result to the
// GTK main loop, and the closure frees itself there (issue #247).
void deliver_count_result(rivet_app::Result<std::int64_t> result) {
auto* delivered = new IntResult;
try {
delivered->value = result.get();
delivered->ok = true;
} catch (std::exception const& error) {
delivered->error = error.what();
} catch (...) {
delivered->error = "unknown backend failure";
}
g_idle_add(on_count_delivered, delivered);
rivet_app::post_to_main(
[flat = rivet_app::unpack(std::move(result))]() mutable {
if (flat.ok) {
g_state.set_count(flat.value);
} else {
g_state.set_status("State error: " + flat.error);
}
gtk_widget_set_sensitive(GTK_WIDGET(g_state.increment), TRUE);
});
}

int on_backend_finished(gpointer) {
Expand Down Expand Up @@ -165,16 +116,17 @@ int on_backend_finished(gpointer) {
}

void start_backend() {
auto layout = discover_runtime_layout();
// The runtime library owns the layout contract (issue #246); hosts locate
// stage roots through it instead of re-deriving the paths.
auto layout = rivet::linux_runtime::discover_layout();
if (!layout.has_value()) {
g_state.set_status(
"Missing Rivet runtime layout (runtime/*.boot, res/core.zo) next to "
"the executable. Build with raco rivet build/dev.");
g_state.set_status(rivet::linux_runtime::layout_error_hint());
return;
}

rivet::linux_runtime::RacketRuntimeConfig config;
config.executable_path = executable_path().string();
config.executable_path =
rivet::linux_runtime::executable_path().string();
config.petite_boot = layout->petite_boot.string();
config.scheme_boot = layout->scheme_boot.string();
config.racket_boot = layout->racket_boot.string();
Expand Down
71 changes: 71 additions & 0 deletions platform/linux/host/src/rpc_bridge.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#pragma once

// Main-thread bridge for GTK hosts (issue #247): backend completions and
// events arrive on the embedded runtime's reader thread, and nothing in
// that context may touch GTK objects. Hosts flatten one completion into an
// Unpacked<T> (value or error, no exception flow across threads) and hand a
// closure to post_to_main, which runs it on the GTK main loop and frees it
// there. The template main.cpp shows the pattern end to end.

#include <glib.h>

#include <exception>
#include <functional>
#include <memory>
#include <string>
#include <type_traits>
#include <utility>

#include "GeneratedBackend.hpp"

namespace rivet_app {

// One completion, flattened for widget code: exactly one of value/error is
// meaningful, decided on the reader thread before the hop.
template <typename T>
struct Unpacked {
bool ok{false};
T value{};
std::string error;
};

template <>
struct Unpacked<void> {
bool ok{false};
std::string error;
};

template <typename T>
Unpacked<T> unpack(Result<T> result) {
Unpacked<T> flat;
try {
if constexpr (!std::is_void_v<T>) {
flat.value = result.get();
} else {
result.get();
}
flat.ok = true;
} catch (std::exception const& error) {
flat.error = error.what();
} catch (...) {
flat.error = "unknown backend failure";
}
return flat;
}

// Runs work on the GTK main loop. The closure is heap-allocated on the
// calling (reader) thread and freed on the main thread no matter which
// GSource callback result the invocation produces.
inline void post_to_main(std::function<void()> work) {
auto* pending = new std::function<void()>(std::move(work));
g_idle_add(
+[](gpointer user_data) -> gint {
std::unique_ptr<std::function<void()>> work(
static_cast<std::function<void()>*>(user_data));
(*work)();
return G_SOURCE_REMOVE;
},
pending);
}

} // namespace rivet_app
1 change: 1 addition & 0 deletions platform/linux/rivet_linux_sources.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ include_guard(GLOBAL)
set(RIVET_LINUX_PLATFORM_SOURCES
"${RIVET_ROOT}/platform/linux/chart/chart_area.cpp"
"${RIVET_ROOT}/platform/linux/runtime/backend.cpp"
"${RIVET_ROOT}/platform/linux/runtime/layout.cpp"
"${RIVET_ROOT}/platform/linux/system/autostart.cpp"
"${RIVET_ROOT}/platform/linux/system/clipboard.cpp"
"${RIVET_ROOT}/platform/linux/system/crash_shutdown.cpp"
Expand Down
39 changes: 39 additions & 0 deletions platform/linux/runtime/layout.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#include "layout.hpp"

namespace rivet::linux_runtime {

std::filesystem::path executable_path() {
return std::filesystem::read_symlink("/proc/self/exe");
}

std::optional<RuntimeLayout> discover_layout(
std::filesystem::path const& stage_root) {
RuntimeLayout layout;
layout.stage_root = stage_root;
layout.resources = stage_root / "res";
layout.petite_boot = stage_root / "runtime" / "petite.boot";
layout.scheme_boot = stage_root / "runtime" / "scheme.boot";
layout.racket_boot = stage_root / "runtime" / "racket.boot";
layout.core = stage_root / "res" / "core.zo";
std::error_code probe;
if (std::filesystem::exists(layout.petite_boot, probe) &&
std::filesystem::exists(layout.scheme_boot, probe) &&
std::filesystem::exists(layout.racket_boot, probe) &&
std::filesystem::exists(layout.core, probe)) {
return layout;
}
return std::nullopt;
}

std::optional<RuntimeLayout> discover_layout() {
return discover_layout(executable_path().parent_path());
}

std::string const& layout_error_hint() {
static std::string const hint =
"Missing Rivet runtime layout (runtime/*.boot, res/core.zo) next to "
"the executable. Build with raco rivet build/dev.";
return hint;
}

} // namespace rivet::linux_runtime
Loading
Loading