Implement posix app loading (#643)

- Added POSIX desktop support for SDK builds, application packaging, and integration testing.
- Added POSIX filesystem partitions and improved application path handling.
- Added simulator support for loading and running applications dynamically.
- Improved simulator task stack handling and display startup reliability.
- Improved simulator display scaling, resizing, high-DPI support, and pointer accuracy.
- Fixed LVGL timers and input polling on POSIX. (fixes simulator with Linux on some Intel graphics platforms)
- Standardized data paths across platforms.
- Logging now works via separate task: this allows apps to write to log without it affecting their stdout (for apps that output text as relevant date for other apps, like the File Selection app)
- Fixes for app stdio
This commit is contained in:
Ken Van Hoeylandt
2026-08-31 22:09:04 +02:00
committed by GitHub
parent d3656bcd3d
commit 643cbc3806
66 changed files with 2139 additions and 336 deletions
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: Apache-2.0
#include "doctest.h"
#include <app/event.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/scheduler.h>
#include <service/manager.h>
#include <tactility/delay.h>
#include <atomic>
extern ServiceManifest loader_service_manifest; // app-posix-module's own
extern ServiceManifest app_internal_loader_service_manifest; // app-module's real memory loader
namespace {
void ensure_path_loader_registered() {
if (service_manager_find_instance(APP_LOADER_PATH_SERVICE_ID) == nullptr) {
service_manager_add(&loader_service_manifest, /*auto_start=*/true);
}
}
void ensure_memory_loader_registered() {
if (service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID) == nullptr) {
service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
}
}
bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_ms) {
uint32_t waited = 0;
while (waited < timeout_ms) {
if (app_manager_get_state(id) == target) {
return true;
}
delay_millis(10);
waited += 10;
}
return app_manager_get_state(id) == target;
}
std::atomic<int32_t> g_fixture_result { -1 };
std::atomic<bool> g_fixture_result_received { false };
// Starts the dlopen()ed fixture as its own modal child and stashes its returned result, so the
// test can inspect that result from the (in-process, directly readable) parent's own task.
int32_t parent_app_main(int, char*[]) {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
app_event_subscribe(&sub, &event_group);
AppInstanceId self_id = app_scheduler_current_app_id();
AppManifest fixture_manifest {
"test.posix.fixture", "Fixture", APP_CATEGORY_USER,
{ APP_LOCATION_PATH, const_cast<char*>(FIXTURE_APP_PATH) }
};
app_manager_add(&fixture_manifest);
AppInstanceId fixture_id = 0;
app_manager_start_for_result("test.posix.fixture", self_id, 0, nullptr, &fixture_id);
while (true) {
if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
break; // safety net so a bug here can't hang the test suite
}
AppEvent event {};
bool got_result = false;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_RESULT && event.result.launch_id == fixture_id) {
g_fixture_result.store(event.result.result, std::memory_order_release);
got_result = true;
}
}
if (got_result) {
break;
}
}
g_fixture_result_received.store(true, std::memory_order_release);
app_manager_remove("test.posix.fixture");
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}
} // namespace
TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its main(), which resolves a real Tactility symbol against the host") {
ensure_path_loader_registered();
ensure_memory_loader_registered();
g_fixture_result.store(-1, std::memory_order_relaxed);
g_fixture_result_received.store(false, std::memory_order_relaxed);
AppManifest parent_manifest { "test.posix.parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(parent_app_main) } };
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
AppInstanceId parent_id = 0;
REQUIRE_EQ(app_manager_start("test.posix.parent", &parent_id), ERROR_NONE);
REQUIRE(wait_for_state(parent_id, APP_INSTANCE_STATE_STOPPED, 3000));
CHECK(g_fixture_result_received.load(std::memory_order_acquire));
// A positive AppInstanceId proves the fixture's dlopen()ed main() actually resolved and
// called app_scheduler_current_app_id() against the host process, not just "ran and returned
// a hardcoded value" - 0 would mean it thought it wasn't running as an app instance at all.
CHECK_GT(g_fixture_result.load(std::memory_order_acquire), 0);
app_manager_remove("test.posix.parent");
}
@@ -0,0 +1,54 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include <cassert>
#include "FreeRTOS.h"
#include "task.h"
typedef struct {
int argc;
char** argv;
int result;
} TestTaskData;
void test_task(void* parameter) {
auto* data = (TestTaskData*)parameter;
doctest::Context context;
context.applyCommandLine(data->argc, data->argv);
// overrides
context.setOption("no-breaks", true); // don't break in the debugger when assertions fail
data->result = context.run();
vTaskEndScheduler();
vTaskDelete(nullptr);
}
int main(int argc, char** argv) {
TestTaskData data = {
.argc = argc,
.argv = argv,
.result = 0
};
BaseType_t task_result = xTaskCreate(
test_task,
"test_task",
8192,
&data,
1,
nullptr
);
if (task_result != pdPASS) {
return 1;
}
vTaskStartScheduler();
return data.result;
}