build: arborescence bin/<plateforme>/<type> et build unifié (ADR 0010)

- platform/layout : racine sequa-root.json, chemins des composants, arguments et chemins en UTF-8.
- Règles install() : bin/windows-x64/{debug,release,asan}/ (core, render, studio + ui, engines).
- Le Core trouve sequa-render et les adaptateurs depuis la racine (--sequa-root, passé par
  Studio host à sa copie fantôme) : plus de render-unavailable depuis le panneau Rendu.
- L'adaptateur Godot cherche Godot et son projet à côté de lui (surcharge par --adapter-options) ;
  template_release pour le type release. Godot hébergé dans les presets debug et release.
- scripts/build-all.ps1 : Godot, C++, interface, arrêt des processus du dossier visé, install ;
  check.ps1 l'appelle (modification de l'ADR 0004).
- docs/installation/ : compiler, lancer, installer et désinstaller par plateforme.
This commit is contained in:
Ange-Marie MAURIN - A2M Systèmes committed 2026-11-07 15:50:37 +01:00
1 parent 28b1b44294
commit da0d31385b
51 files changed
+1468 -197

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# Build
build/
/bin/
/installers/
out/
cmake-build-*/
CMakeUserPresets.json
+5
View File
@@ -28,12 +28,14 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
include(SequaCompilerOptions)
include(SequaDependencies)
include(SequaInstall) # bin/<plateforme>/<type>/ (ADR 0010)
if(SEQUA_BUILD_TESTS)
enable_testing()
endif()
add_subdirectory(platform/process) # lancement de processus enfants (Studio host, Core)
add_subdirectory(platform/layout) # arborescence des binaires, racine sequa-root.json (ADR 0010)
add_subdirectory(core)
add_subdirectory(3d)
add_subdirectory(engines/godot/adapter) # Godot lui-même n'est pas compilé ici (SCons, scripts/build-godot.ps1)
@@ -43,6 +45,9 @@ if(SEQUA_BUILD_SERVER)
add_subdirectory(studio/host) # dépend d'IXWebSocket et nlohmann/json
endif()
# Interface construite par npm run build (studio/ui/dist), absente tant qu'elle n'est pas construite.
install(DIRECTORY "${PROJECT_SOURCE_DIR}/studio/ui/dist/" DESTINATION studio/ui OPTIONAL)
if(SEQUA_BUILD_PROTOTYPES AND EXISTS "${PROJECT_SOURCE_DIR}/prototypes/CMakeLists.txt")
add_subdirectory(prototypes)
endif()
+6 -4
View File
@@ -23,25 +23,26 @@
"name": "windows-msvc-debug",
"displayName": "Windows MSVC Debug",
"inherits": "windows-base",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "SEQUA_BIN_TYPE": "debug", "SEQUA_BUILD_GODOT_RUNTIME": "ON" }
},
{
"name": "windows-msvc-release",
"displayName": "Windows MSVC Release",
"inherits": "windows-base",
"cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" }
"cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", "SEQUA_BIN_TYPE": "release", "SEQUA_BUILD_GODOT_RUNTIME": "ON" }
},
{
"name": "windows-msvc-asan",
"displayName": "Windows MSVC AddressSanitizer",
"inherits": "windows-base",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "SEQUA_SANITIZERS": "address" }
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "SEQUA_BIN_TYPE": "asan", "SEQUA_SANITIZERS": "address" }
},
{
"name": "windows-msvc-prototypes",
"displayName": "Windows MSVC Release + prototypes",
"inherits": "windows-msvc-release",
"cacheVariables": { "SEQUA_BUILD_PROTOTYPES": "ON", "SEQUA_BUILD_GODOT_RUNTIME": "ON" }
"installDir": "${sourceDir}/build/${presetName}/install",
"cacheVariables": { "SEQUA_BUILD_PROTOTYPES": "ON" }
},
{
"name": "linux-base",
@@ -62,6 +63,7 @@
"cacheVariables": {
"CMAKE_CXX_COMPILER": "clang++",
"CMAKE_BUILD_TYPE": "Debug",
"SEQUA_BIN_TYPE": "asan",
"SEQUA_SANITIZERS": "address;undefined"
}
},
+5 -4
View File
@@ -17,7 +17,7 @@ Documentation de référence : [docs/SEQUA_Engine_resume_complet_v2.md](docs/SEQ
| [3d/](3d) | SEQUA 3D : interface vers les moteurs 3D (adaptateurs, capacités, synchronisation) | C++20 |
| [render/](render) | `sequa-render` : processus de rendu lancé par le Core, charge une bibliothèque d'adaptateur ([ADR 0009](docs/adr/0009-processus-de-rendu-lance-par-le-core.md)) | C++20 |
| [engines/](engines) | Adaptateurs des moteurs 3D : `godot/adapter` + `godot/vendor/godot-src` (fork de Godot 4.7.2-stable) et `godot/vendor/godot-cpp` (10.0.0-stable), sous-modules ; `debug/adapter` (rendu 2D sans GPU, en images ou vidéo) | C++20 |
| [platform/](platform) | Code propre à chaque système, partagé (lancement de processus enfants liés au parent) | C++20 |
| [platform/](platform) | Code propre à chaque système, partagé : lancement de processus enfants liés au parent (`process/`), arborescence des binaires et racine `sequa-root.json` (`layout/`) | C++20 |
| [schema/](schema) | Contrat de données et protocole Studio↔Core | Protobuf / FlatBuffers |
| [docs/](docs) | Documentation, décisions d'architecture (`docs/adr/`), spécifications des protocoles (`docs/specs/`), tutoriels (`docs/tutoriels/`) | Markdown |
| [artwork/](artwork) | Identité graphique et sources visuelles | PNG |
@@ -38,14 +38,15 @@ Pas encore de rendu neuronal ni de carte de confiance. Seul Windows a été cons
## Construire et vérifier
Il n'y a pas d'intégration continue sur serveur : les vérifications se lancent localement.
Il n'y a pas d'intégration continue sur serveur : les vérifications se lancent localement. Voir aussi [docs/installation/](docs/installation/) pour les pages par plateforme (prérequis, compiler, lancer, installer, désinstaller).
| Plateforme | Commande | Prérequis |
|---|---|---|
| Windows | `.\scripts\build.ps1 [-Preset <nom>] [-Clean] [-NoTests]` | Visual Studio 2022 avec outils C++ (MSVC, CMake et Ninja fournis) |
| Windows (build complet) | `.\scripts\build-all.ps1 [-Type debug\|release\|asan\|all] [-NoTests] [-Godot] [-SkipGodot] [-SkipUi] [-NoStop] [-Clean]` | Visual Studio 2022 avec outils C++ (MSVC, CMake, Ninja fournis), Python 3, Node.js 24, sous-modules Godot, GPU Vulkan |
| Windows (un preset CMake seul) | `.\scripts\build.ps1 [-Preset <nom>] [-Clean] [-NoTests]` | idem |
| Windows, avant commit | `.\scripts\check.ps1` | idem + Node.js pour Studio |
| Linux / macOS | `./scripts/build.sh [<preset>]` | CMake ≥ 3.25, Ninja, GCC ou Clang |
| Lancer SEQUA Studio (Core + interface) | `.\build\windows-msvc-debug\studio\host\app\sequa-studio.exe [--dev] [--watch]` (voir [studio/host](studio/host/README.md)) | build + `npm run build` dans `studio/ui` |
| Lancer SEQUA Studio (Core + interface) | `.\bin\windows-x64\debug\studio\sequa-studio.exe [--dev] [--watch]` | build complet |
| Interface seule (développement) | `.\scripts\studio.ps1 [-Core ws://hôte:port] [-MockCore]` ou `./scripts/studio.sh` | Node.js 24 |
| Windows, cuDNN pour l'inférence GPU | `.\scripts\install-cudnn.ps1 [-Version <x.y.z>] [-CudaMajor 12\|13] [-DryRun]` | CUDA Toolkit installé + GPU NVIDIA ; télécharge cuDNN (~1,3 Go), demande d'accepter la licence NVIDIA et définit `CUDNN_PATH` (utilisateur, sans droits admin) |
+67
View File
@@ -0,0 +1,67 @@
# SPDX-License-Identifier: MPL-2.0
#
# Arborescence installée (ADR 0010) : bin/<plateforme>/<type>/ en développement, même
# contenu dans le dossier d'installation. `cmake --install <build>` la remplit.
# Plateforme : système et processeur visés (windows-x64, linux-arm64…).
if(WIN32)
set(_sequa_os "windows")
elseif(APPLE)
set(_sequa_os "macos")
else()
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _sequa_os)
endif()
if(MSVC)
set(_sequa_cpu "${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}")
else()
set(_sequa_cpu "${CMAKE_SYSTEM_PROCESSOR}")
endif()
string(TOLOWER "${_sequa_cpu}" _sequa_cpu)
if(_sequa_cpu MATCHES "^(amd64|x86_64|x64)$")
set(_sequa_cpu "x64")
elseif(_sequa_cpu MATCHES "^(arm64|aarch64)$")
set(_sequa_cpu "arm64")
endif()
set(SEQUA_PLATFORM "${_sequa_os}-${_sequa_cpu}" CACHE STRING "Plateforme de bin/<plateforme>/<type>")
# Type : fixé par le preset (debug, release, asan) ; à défaut, d'après CMAKE_BUILD_TYPE.
if(CMAKE_BUILD_TYPE MATCHES "^(Release|RelWithDebInfo|MinSizeRel)$")
set(_sequa_default_type "release")
else()
set(_sequa_default_type "debug")
endif()
set(SEQUA_BIN_TYPE "${_sequa_default_type}" CACHE STRING "Type de bin/<plateforme>/<type>")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${PROJECT_SOURCE_DIR}/bin/${SEQUA_PLATFORM}/${SEQUA_BIN_TYPE}"
CACHE PATH "Racine de l'arborescence installée" FORCE)
endif()
message(STATUS "SEQUA : installation dans ${CMAKE_INSTALL_PREFIX}")
# Repère de la racine (sequa::platform::find_root).
configure_file("${CMAKE_CURRENT_LIST_DIR}/sequa-root.json.in" "${PROJECT_BINARY_DIR}/sequa-root.json" @ONLY)
install(FILES "${PROJECT_BINARY_DIR}/sequa-root.json" DESTINATION .)
# Bibliothèques d'exécution qu'un exécutable lancé hors de l'invite de Visual Studio ne
# trouverait pas : ASan de MSVC, à côté de chaque exécutable.
set(SEQUA_RUNTIME_FILES "")
if(MSVC AND SEQUA_SANITIZERS MATCHES "address")
get_filename_component(_sequa_cl_dir "${CMAKE_CXX_COMPILER}" DIRECTORY)
file(GLOB SEQUA_RUNTIME_FILES "${_sequa_cl_dir}/clang_rt.asan_dynamic-*.dll")
if(NOT SEQUA_RUNTIME_FILES)
message(WARNING "Bibliothèque ASan introuvable dans ${_sequa_cl_dir} : bin/ ne se lancera que depuis l'invite de Visual Studio")
endif()
endif()
# Installe un exécutable dans <destination>/ avec les bibliothèques d'exécution.
function(sequa_install_executable target destination)
install(TARGETS ${target} RUNTIME DESTINATION ${destination})
if(SEQUA_RUNTIME_FILES)
install(FILES ${SEQUA_RUNTIME_FILES} DESTINATION ${destination})
endif()
endfunction()
# Installe une bibliothèque d'adaptateur (MODULE) dans engines/<moteur>/.
function(sequa_install_adapter target engine)
install(TARGETS ${target} LIBRARY DESTINATION engines/${engine} RUNTIME DESTINATION engines/${engine})
endfunction()
+6
View File
@@ -0,0 +1,6 @@
{
"layout": 1,
"sequaVersion": "@PROJECT_VERSION@",
"platform": "@SEQUA_PLATFORM@",
"type": "@SEQUA_BIN_TYPE@"
}
+3 -2
View File
@@ -38,7 +38,7 @@ if(SEQUA_BUILD_SERVER)
add_library(sequa::core_server ALIAS sequa_core_server)
target_link_libraries(sequa_core_server
PUBLIC sequa::core sequa::3d
PRIVATE sequa::core_render sequa::3d_wire sequa::process nlohmann_json::nlohmann_json ixwebsocket::ixwebsocket)
PRIVATE sequa::core_render sequa::3d_wire sequa::process sequa::layout nlohmann_json::nlohmann_json ixwebsocket::ixwebsocket)
if(WIN32)
target_link_libraries(sequa_core_server PRIVATE ws2_32) # vérification exclusive du port
endif()
@@ -48,8 +48,9 @@ if(SEQUA_BUILD_SERVER)
set_target_properties(sequa_core_app PROPERTIES OUTPUT_NAME sequa-core)
# L'application utilise le superviseur du rendu, interne à sequa_core_server.
target_include_directories(sequa_core_app PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/server")
target_link_libraries(sequa_core_app PRIVATE sequa::core_server sequa::core_render sequa::process)
target_link_libraries(sequa_core_app PRIVATE sequa::core_server sequa::core_render sequa::process sequa::layout)
sequa_target_defaults(sequa_core_app)
sequa_install_executable(sequa_core_app core)
endif()
# --- Tests ------------------------------------------------------------------------------
+3 -2
View File
@@ -44,10 +44,11 @@ Protocole **0.2**, JSON provisoire, spécifié dans [docs/specs/protocole-studio
- `CoreRuntime` (implémente `CoreServices`) : bail réversible (`--linked`, attach/detach), demande d'arrêt, `instanceId`, compteurs.
- `LiveSession` : session en direct à 60 Hz (fil dédié), entrée maintenue, export (enregistrement + empreintes par tick), import par rejeu puis poursuite dans le même enregistrement.
- `CoreServer` : transport IXWebSocket, jeton (`?token=`) et contrôle de l'`Origin` à l'ouverture, écoute locale par défaut, refus explicite si le port est déjà pris.
- Rendu piloté par Studio (protocole 0.3, §9 ; ADR 0010) : `--render <godot|debug>` lance `sequa-render` depuis l'arborescence `bin/<plateforme>/<type>/` (repérée via `sequa-root.json`) ; option `--sequa-root <dossier>` pour forcer la racine.
```powershell
.\build\windows-msvc-debug\core\sequa-core.exe --help
.\build\windows-msvc-debug\core\sequa-core.exe --port 9100 --token <hex> --linked
.\bin\windows-x64\debug\core\sequa-core.exe --help
.\bin\windows-x64\debug\core\sequa-core.exe --port 9100 --token <hex> --linked
```
En usage normal, c'est Studio host (`sequa-studio`) qui lance et supervise le Core.
+30 -29
View File
@@ -8,6 +8,8 @@
#include "sequa/core/server/server.hpp"
#include "sequa/core/version.hpp"
#include "sequa/platform/layout.hpp"
#include <charconv>
#include <chrono>
#include <csignal>
@@ -21,6 +23,7 @@
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
@@ -59,6 +62,7 @@ void print_usage()
" --render-fps <n> cadence imposée au rendu (debug : 10 conseillé)\n"
" --render-options <json> réglages propres à l'adaptateur\n"
" --render-log <fichier> journal du rendu (défaut : sortie du Core)\n"
" --sequa-root <dossier> racine de l'arborescence (défaut : trouvée depuis l'exécutable)\n"
" --help\n"
"Codes de sortie : 0 arrêt normal, 1 démarrage impossible, 2 arguments invalides, 3 bail expiré.\n");
}
@@ -74,19 +78,6 @@ std::optional<T> parse_number(std::string_view text, int base = 10)
return value;
}
std::filesystem::path current_executable()
{
#ifdef _WIN32
std::wstring buffer(32768, L'\0');
const DWORD size = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
buffer.resize(size);
return std::filesystem::path(buffer);
#else
std::error_code error;
return std::filesystem::read_symlink("/proc/self/exe", error);
#endif
}
} // namespace
int main(int argc, char** argv)
@@ -97,17 +88,20 @@ int main(int argc, char** argv)
SetConsoleOutputCP(CP_UTF8); // journaux en UTF-8 (accents, JSON reçu)
#endif
// Arguments en UTF-8 (Windows : CommandLineToArgvW ; POSIX : argv tel quel).
const std::vector<std::string> args = sequa::platform::utf8_arguments(argc, argv);
server::ServerOptions options;
server::CoreRuntimeOptions runtime_options;
std::string render_adapter; // vide : pas de rendu
server::RenderStartRequest render_request; // paramètres de render/start
runtime_options.core_version = std::string(version_string());
for (int i = 1; i < argc; ++i) {
const std::string_view arg = argv[i];
const bool has_value = i + 1 < argc;
for (std::size_t i = 0; i < args.size(); ++i) {
const std::string_view arg = args[i];
const bool has_value = i + 1 < args.size();
const auto invalid = [&](const char* what) {
std::fprintf(stderr, "%s invalide : %s\n", what, argv[i]);
std::fprintf(stderr, "%s invalide : %s\n", what, arg.data());
return exit_bad_arguments;
};
if (arg == "--help" || arg == "-h") {
@@ -117,52 +111,54 @@ int main(int argc, char** argv)
if (arg == "--linked") {
runtime_options.linked = true;
} else if (arg == "--host" && has_value) {
options.host = argv[++i];
options.host = args[++i];
} else if (arg == "--port" && has_value) {
const auto port = parse_number<int>(argv[++i]);
const auto port = parse_number<int>(args[++i]);
if (!port || *port <= 0 || *port > 65535) {
return invalid("Port");
}
options.port = *port;
} else if (arg == "--token" && has_value) {
options.token = argv[++i];
options.token = args[++i];
if (options.token.empty()) {
return invalid("Jeton");
}
} else if (arg == "--allowed-origin" && has_value) {
options.allowed_origins.emplace_back(argv[++i]);
options.allowed_origins.emplace_back(args[++i]);
} else if (arg == "--lease-grace-ms" && has_value) {
const auto grace = parse_number<long long>(argv[++i]);
const auto grace = parse_number<long long>(args[++i]);
if (!grace || *grace < 0) {
return invalid("Délai de grâce");
}
runtime_options.lease_grace = std::chrono::milliseconds(*grace);
} else if (arg == "--seed" && has_value) {
const std::string_view text = argv[++i];
const std::string_view text = args[++i];
const auto seed = text.size() <= 16 ? parse_number<std::uint64_t>(text, 16) : std::nullopt;
if (!seed) {
return invalid("Graine");
}
runtime_options.seed = *seed;
} else if (arg == "--render" && has_value) {
render_adapter = argv[++i];
render_adapter = args[++i];
if (render_adapter != "godot" && render_adapter != "debug") {
return invalid("Adaptateur de rendu");
}
} else if (arg == "--render-headless") {
render_request.headless = true;
} else if (arg == "--render-fps" && has_value) {
const auto fps = parse_number<std::uint32_t>(argv[++i]);
const auto fps = parse_number<std::uint32_t>(args[++i]);
if (!fps || *fps > 240) {
return invalid("Cadence de rendu");
}
render_request.fps = *fps;
} else if (arg == "--render-options" && has_value) {
render_request.adapter_options = argv[++i];
render_request.adapter_options = args[++i];
} else if (arg == "--render-log" && has_value) {
runtime_options.render_log = argv[++i];
runtime_options.render_log = sequa::platform::path_from_utf8(args[++i]);
} else if (arg == "--sequa-root" && has_value) {
runtime_options.sequa_root = sequa::platform::path_from_utf8(args[++i]);
} else {
std::fprintf(stderr, "Argument inconnu : %s\n", argv[i]);
std::fprintf(stderr, "Argument inconnu : %s\n", arg.data());
print_usage();
return exit_bad_arguments;
}
@@ -176,7 +172,12 @@ int main(int argc, char** argv)
};
// Remplir les options du runtime AVANT de construire le CoreRuntime.
runtime_options.core_executable = current_executable();
if (runtime_options.sequa_root.empty()) {
const auto exe = sequa::platform::current_executable();
if (!exe.empty()) {
runtime_options.sequa_root = sequa::platform::find_root(exe.parent_path()).value_or(std::filesystem::path{});
}
}
const std::string host = options.host == "0.0.0.0" ? std::string("127.0.0.1") : options.host;
std::string url = "ws://" + host + ":" + std::to_string(options.port) + "/";
if (!options.token.empty()) {
+2 -2
View File
@@ -27,8 +27,8 @@ struct CoreRuntimeOptions {
bool real_time = true; // faux : session sans fil (tests), voir LiveSession
std::chrono::milliseconds shutdown_delay{200}; // laisse partir la réponse à core/shutdown
// --- Options pour le rendu piloté par Studio (protocole 0.3, §9 ; ADR 0009) ---
std::filesystem::path core_executable; // pour find_render_executable / find_adapter_library
// --- Options pour le rendu piloté par Studio (protocole 0.3, §9 ; ADR 0010) ---
std::filesystem::path sequa_root; // racine de l'arborescence (sequa-root.json) : sequa-render et adaptateurs (ADR 0010)
std::string render_core_url; // URL WebSocket du Core avec jeton
std::filesystem::path render_log; // journal du rendu (vide = sortie héritée)
std::function<void(std::string_view)> log; // fonction de journalisation
+30 -37
View File
@@ -3,9 +3,10 @@
#include "render_hub.hpp"
#include "sequa/platform/layout.hpp"
#include <system_error>
#include <utility>
#include <vector>
namespace sequa::core::server {
namespace {
@@ -17,50 +18,42 @@ constexpr auto restart_delay = std::chrono::seconds(1);
constexpr auto restart_window = std::chrono::minutes(1);
constexpr std::size_t max_restarts_per_window = 3;
#ifdef _WIN32
constexpr const char* executable_suffix = ".exe";
constexpr const char* library_suffix = ".dll";
constexpr const char* library_prefix = "";
#elif defined(__APPLE__)
constexpr const char* executable_suffix = "";
constexpr const char* library_suffix = ".dylib";
constexpr const char* library_prefix = "lib";
#else
constexpr const char* executable_suffix = "";
constexpr const char* library_suffix = ".so";
constexpr const char* library_prefix = "lib";
#endif
} // namespace
std::optional<std::filesystem::path> first_existing(const std::vector<std::filesystem::path>& candidates)
std::optional<std::filesystem::path> find_render_executable(const std::filesystem::path& sequa_root)
{
for (const auto& candidate : candidates) {
std::error_code error;
if (std::filesystem::is_regular_file(candidate, error)) {
return candidate;
}
if (sequa_root.empty()) {
return std::nullopt;
}
sequa::platform::Layout layout(sequa_root);
const auto path = layout.render_executable();
std::error_code error;
if (std::filesystem::is_regular_file(path, error)) {
return path;
}
return std::nullopt;
}
} // namespace
std::optional<std::filesystem::path> find_render_executable(const std::filesystem::path& core_exe)
{
const std::filesystem::path dir = core_exe.parent_path();
const std::string name = std::string("sequa-render") + executable_suffix;
return first_existing({dir / name, dir.parent_path() / "render" / name});
}
std::optional<std::filesystem::path> find_adapter_library(const std::filesystem::path& core_exe,
std::optional<std::filesystem::path> find_adapter_library(const std::filesystem::path& sequa_root,
std::string_view adapter)
{
if (adapter != "godot" && adapter != "debug") {
return std::nullopt;
}
const std::filesystem::path dir = core_exe.parent_path();
const std::string name =
std::string(library_prefix) + "sequa_" + std::string(adapter) + "_adapter" + library_suffix;
return first_existing({dir / name, dir.parent_path() / "engines" / std::string(adapter) / "adapter" / name});
if (sequa_root.empty()) {
return std::nullopt;
}
sequa::platform::Layout layout(sequa_root);
const auto opt = layout.adapter_library(adapter);
if (!opt) {
return std::nullopt;
}
const auto path = *opt;
std::error_code error;
if (std::filesystem::is_regular_file(path, error)) {
return path;
}
return std::nullopt;
}
RenderSupervisor::RenderSupervisor(RenderLaunch launch, std::string core_url, std::function<void(std::string_view)> log)
@@ -78,7 +71,7 @@ RenderSupervisor::~RenderSupervisor()
bool RenderSupervisor::launch()
{
std::vector<std::string> args = {"--core", core_url_,
"--adapter", launch_.adapter.string(),
"--adapter", sequa::platform::path_to_utf8(launch_.adapter),
"--adapter-options", launch_.adapter_options,
"--width", std::to_string(launch_.width),
"--height", std::to_string(launch_.height)};
@@ -94,11 +87,11 @@ bool RenderSupervisor::launch()
options.kill_with_parent = true; // jamais de rendu orphelin (VRAM)
process_ = platform::ChildProcess::launch(launch_.executable, args, options);
if (!process_) {
log_("Rendu : lancement impossible de " + launch_.executable.string());
log_("Rendu : lancement impossible de " + sequa::platform::path_to_utf8(launch_.executable));
state_ = State::stopped;
return false;
}
log_("Rendu lancé (pid " + std::to_string(process_->pid()) + ", " + launch_.adapter.filename().string() + ")");
log_("Rendu lancé (pid " + std::to_string(process_->pid()) + ", " + sequa::platform::path_to_utf8(launch_.adapter.filename()) + ")");
state_ = State::running;
return true;
}
+4 -4
View File
@@ -70,10 +70,10 @@ private:
std::chrono::steady_clock::time_point restart_at_;
};
// Bibliothèque d'adaptateur « godot » ou « debug » et sequa-render, cherchés à côté de
// l'exécutable du Core puis dans l'arborescence de build (build/<preset>/…).
[[nodiscard]] std::optional<std::filesystem::path> find_render_executable(const std::filesystem::path& core_exe);
[[nodiscard]] std::optional<std::filesystem::path> find_adapter_library(const std::filesystem::path& core_exe,
// Bibliothèque d'adaptateur « godot » ou « debug » et sequa-render, cherchés dans
// l'arborescence SEQUA (ADR 0010) à partir de la racine (sequa-root.json).
[[nodiscard]] std::optional<std::filesystem::path> find_render_executable(const std::filesystem::path& sequa_root);
[[nodiscard]] std::optional<std::filesystem::path> find_adapter_library(const std::filesystem::path& sequa_root,
std::string_view adapter);
} // namespace sequa::core::server
+7 -6
View File
@@ -252,7 +252,7 @@ std::optional<SessionImportData> CoreRuntime::session_import(std::span<const std
return session_.import_session(recording, resume);
}
// --- Rendu piloté par Studio (protocole 0.3, §9) ---
// --- Rendu piloté par Studio (protocole 0.3, §9 ; ADR 0010) ---
RenderStartResult CoreRuntime::render_start(const RenderStartRequest& request)
{
@@ -270,13 +270,14 @@ RenderStartResult CoreRuntime::render_start(const RenderStartRequest& request)
return RenderStartResult{std::nullopt, "render-unavailable", "Adaptateur inconnu : " + request.adapter};
}
// Trouver l'exécutable et la bibliothèque d'adaptateur.
if (options_.core_executable.empty()) {
return RenderStartResult{std::nullopt, "render-unavailable", "Chemin de l'exécutable Core non fourni."};
// Trouver l'exécutable et la bibliothèque d'adaptateur à partir de la racine SEQUA.
if (options_.sequa_root.empty()) {
return RenderStartResult{std::nullopt, "render-unavailable",
"Racine SEQUA introuvable (sequa-root.json) : lancer depuis bin/<plateforme>/<type>/ ou passer --sequa-root."};
}
const auto executable = find_render_executable(options_.core_executable);
const auto adapter_lib = find_adapter_library(options_.core_executable, request.adapter);
const auto executable = find_render_executable(options_.sequa_root);
const auto adapter_lib = find_adapter_library(options_.sequa_root, request.adapter);
if (!executable || !adapter_lib) {
std::string msg = "Rendu introuvable : ";
+4 -4
View File
@@ -22,8 +22,8 @@ CoreRuntimeOptions fast(bool linked)
options.attach_deadline = 100ms;
options.shutdown_delay = 20ms;
options.core_version = "test";
// Options de rendu pour les tests : core_executable dans un dossier temporaire vide.
options.core_executable = std::filesystem::temp_directory_path() / "sequa_test_empty" / "sequa-core";
// Options de rendu pour les tests : racine dans un dossier temporaire vide (pas de sequa-render).
options.sequa_root = std::filesystem::temp_directory_path() / "sequa_test_empty";
options.render_core_url = "ws://127.0.0.1:9100/?token=test";
options.render_log = "";
options.log = [](std::string_view) {};
@@ -129,10 +129,10 @@ TEST_CASE("Runtime : comptage des clients")
// --- Tests du rendu (protocole 0.3) ---
TEST_CASE("Runtime : render_start avec core_executable vide -> render-unavailable")
TEST_CASE("Runtime : render_start avec sequa_root vide -> render-unavailable")
{
CoreRuntimeOptions opts = fast(false);
opts.core_executable = ""; // invalide
opts.sequa_root = ""; // racine inconnue
CoreRuntime runtime(opts);
RenderStartRequest req;
@@ -81,7 +81,7 @@ Une installation pour toute la machine (`%ProgramFiles%`, `/opt/sequa`) garde la
- Une bibliothèque commune (`platform/layout`) connaît la disposition relative : à partir de la racine, `core/`, `render/`, `studio/ui/`, `engines/<nom>/`.
- Racine : `--sequa-root <dossier>` si fourni, sinon premier dossier parent de l'exécutable contenant `sequa-root.json`.
- Studio host passe `--sequa-root` au Core lancé depuis sa copie fantôme ; le Core en déduit `sequa-render` et l'adaptateur.
- L'adaptateur Godot reçoit le chemin de la bibliothèque Godot et du projet par `--adapter-options` (JSON), rempli par le Core depuis la disposition : `SEQUA_GODOT_DEFAULT_LIBRARY` et `SEQUA_GODOT_DEFAULT_PROJECT` disparaissent.
- L'adaptateur Godot cherche la bibliothèque Godot et le projet à côté de lui-même (`engines/godot/`), ce qui garde le Core ignorant du moteur (§7.0.1) ; `--adapter-options` (JSON : `godotLibrary`, `project`) peut les imposer. `SEQUA_GODOT_DEFAULT_LIBRARY` et `SEQUA_GODOT_DEFAULT_PROJECT` disparaissent.
- Le repli actuel sur `build/<preset>/…` et `studio/ui/dist` est retiré : on lance depuis `bin/`.
### Build unifié
+36
View File
@@ -0,0 +1,36 @@
# Installation de SEQUA
Ce dossier décrit comment compiler, lancer, installer et désinstaller SEQUA sur chaque plateforme.
## Arborescence des binaires (ADR 0010)
En développement comme une fois installé, les exécutables et leurs ressources sont organisés ainsi :
```text
bin/
└── <plateforme>/ # windows-x64, linux-x64, macos-arm64, ...
└── <type>/ # debug, release, asan
├── sequa-root.json # repère de la racine (version disposition, plateforme, type, version SEQUA)
├── core/ # sequa-core
├── render/ # sequa-render
├── studio/ # sequa-studio
│ └── ui/ # interface construite (copie de studio/ui/dist)
└── engines/
├── debug/ # sequa_debug_adapter
└── godot/ # sequa_godot_adapter, bibliothèque Godot (LibGodot), project/
```
- `<plateforme>` = système et processeur (ex. `windows-x64`).
- `<type>` = preset de construction : `debug` (template_debug Godot), `release` (template_release Godot), `asan` (sans Godot, adaptateur debug seulement).
- Le contenu de `bin/<plateforme>/<type>/` est **l'image d'installation** : il se déplace tel quel.
- La racine est trouvée automatiquement par `sequa-root.json` ; on peut la forcer avec `--sequa-root <dossier>`.
## Pages par plateforme
| Plateforme | Page | État |
|---|---|---|
| Windows x64 | [windows-x64.md](windows-x64.md) | Construit et testé |
| Linux x64 | [linux-x64.md](linux-x64.md) | **Non vérifié** (ADR 0004) |
| macOS arm64 | [macos-arm64.md](macos-arm64.md) | **Non vérifié** (ADR 0004) |
Chaque page contient : Prérequis, Compiler (mode développement), Lancer, Installer, Désinstaller.
+67
View File
@@ -0,0 +1,67 @@
# Installation sur Linux x64
> **NON VÉRIFIÉ** : cette page décrit la procédure prévue. Les presets Linux n'ont pas encore tourné (ADR 0004). L'hébergement de Godot est Windows-only pour l'instant (adaptateur de débogage seulement).
## Prérequis
- **CMake ≥ 3.25**, **Ninja**, **GCC** ou **Clang**.
- **Python 3** dans le `PATH` (pour SCons si Godot devient disponible).
- **Node.js 24** dans le `PATH`.
- **Sous-modules Git** pour l'adaptateur Godot (quand il sera pris en charge) :
```bash
git submodule update --init engines/godot/vendor/godot-src engines/godot/vendor/godot-cpp
```
- GPU Vulkan pour Godot (futur).
## Compiler (mode développement)
Pas encore de `build-all.sh`. Étapes manuelles :
```bash
# Interface (une fois)
(cd studio/ui && npm ci && npm run build)
# Preset CMake (linux-gcc-debug par défaut)
./scripts/build.sh linux-gcc-debug
# Installation dans l'arborescence bin/
cmake --install build/linux-gcc-debug --prefix bin/linux-x64/debug
```
Variables d'environnement utiles :
- `CLEAN=1` : supprime `build/<preset>` avant de configurer.
- `NO_TESTS=1` : compile sans lancer les tests.
Le script `build.sh` détecte la plateforme et choisit le preset par défaut (`linux-gcc-debug` sur Linux, `macos-clang-debug` sur macOS).
## Lancer
Depuis l'image de développement dans `bin/` :
```bash
# Studio complet (Core + interface)
bin/linux-x64/debug/studio/sequa-studio [--dev] [--watch]
# Core seul (trouve sa racine via sequa-root.json ; --sequa-root <dossier> pour forcer)
bin/linux-x64/debug/core/sequa-core --render debug --render-headless --render-fps 10
```
L'adaptateur Godot n'est pas disponible ; seul l'adaptateur de débogage (`engines/debug/`) fonctionne.
## Installer (par utilisateur, sans droits administrateur)
Emplacement prévu :
- Programme : `~/.local/opt/sequa/` (copie de `bin/linux-x64/release/`)
- Lien : `~/.local/bin/sequa-studio` → `~/.local/opt/sequa/studio/sequa-studio`
- Données d'exécution : `$XDG_RUNTIME_DIR/sequa` (fichiers d'exécution, logs, copies fantômes du Core)
- État persistant : `~/.local/state/sequa/`
## Désinstaller
Supprimer les dossiers créés à l'installation :
```bash
rm -rf ~/.local/opt/sequa
rm -f ~/.local/bin/sequa-studio
rm -rf "$XDG_RUNTIME_DIR/sequa"
rm -rf ~/.local/state/sequa
```
+63
View File
@@ -0,0 +1,63 @@
# Installation sur macOS arm64
> **NON VÉRIFIÉ** : cette page décrit la procédure prévue. Les presets macOS n'ont pas encore tourné (ADR 0004). L'hébergement de Godot est Windows-only pour l'instant (adaptateur de débogage seulement). Le paquet `.app` est reporté (ADR 0007 : Godot Metal reporté).
## Prérequis
- **CMake ≥ 3.25**, **Ninja**, **Clang** (Xcode Command Line Tools).
- **Python 3** dans le `PATH` (pour SCons si Godot devient disponible).
- **Node.js 24** dans le `PATH`.
- **Sous-modules Git** pour l'adaptateur Godot (quand il sera pris en charge) :
```bash
git submodule update --init engines/godot/vendor/godot-src engines/godot/vendor/godot-cpp
```
- GPU Metal pour Godot (futur).
## Compiler (mode développement)
Pas encore de `build-all.sh`. Étapes manuelles :
```bash
# Interface (une fois)
(cd studio/ui && npm ci && npm run build)
# Preset CMake (macos-clang-debug par défaut)
./scripts/build.sh macos-clang-debug
# Installation dans l'arborescence bin/
cmake --install build/macos-clang-debug --prefix bin/macos-arm64/debug
```
Variables d'environnement utiles :
- `CLEAN=1` : supprime `build/<preset>` avant de configurer.
- `NO_TESTS=1` : compile sans lancer les tests.
Le script `build.sh` détecte la plateforme et choisit le preset par défaut (`linux-gcc-debug` sur Linux, `macos-clang-debug` sur macOS).
## Lancer
Depuis l'image de développement dans `bin/` :
```bash
# Studio complet (Core + interface)
bin/macos-arm64/debug/studio/sequa-studio [--dev] [--watch]
# Core seul (trouve sa racine via sequa-root.json ; --sequa-root <dossier> pour forcer)
bin/macos-arm64/debug/core/sequa-core --render debug --render-headless --render-fps 10
```
L'adaptateur Godot n'est pas disponible ; seul l'adaptateur de débogage (`engines/debug/`) fonctionne.
## Installer (par utilisateur, sans droits administrateur)
Emplacement prévu (paquet `.app` reporté) :
- Programme : copie de `bin/macos-arm64/release/` (emplacement à définir, `.app` plus tard)
- Données : `~/Library/Application Support/SEQUA/` (fichiers d'exécution, logs, copies fantômes du Core, état persistant)
## Désinstaller
Supprimer les dossiers créés à l'installation :
```bash
rm -rf ~/Library/Application\ Support/SEQUA
# Programme : supprimer le dossier ou le paquet .app une fois défini
```
+75
View File
@@ -0,0 +1,75 @@
# Installation sur Windows x64
## Prérequis
- **Visual Studio 2022** avec la charge de travail « Développement Desktop en C++ » (MSVC, CMake, Ninja fournis).
- **Python 3** dans le `PATH` (SCons est installé dans `engines/godot/.venv` par `build-godot.ps1`).
- **Node.js 24** dans le `PATH`.
- **Sous-modules Git** pour l'adaptateur Godot :
```powershell
git submodule update --init engines/godot/vendor/godot-src engines/godot/vendor/godot-cpp
```
- **GPU Vulkan** pour l'exécution de Godot.
Optionnel (pour l'inférence GPU, prototype `gpu_interop`) :
- **CUDA Toolkit** : `.\scripts\install-cuda.ps1` (télécharge ~2,7 Go, installeur interactif, nécessite UAC).
- **cuDNN** : `.\scripts\install-cudnn.ps1` (télécharge ~1,3 Go, accepte la licence NVIDIA, définit la variable utilisateur `CUDNN_PATH`, installe dans `%LOCALAPPDATA%\SEQUA\sdk\cudnn\`).
## Compiler (mode développement)
Une seule commande construit tout (Godot, C++, interface, installation dans `bin\`) :
```powershell
.\scripts\build-all.ps1 [-Type debug|release|asan|all] [-NoTests] [-Godot] [-SkipGodot] [-SkipUi] [-NoStop] [-Clean]
```
- `-Type` : `debug` (défaut), `release`, `asan`, ou `all` (les trois).
- `-NoTests` : compile sans lancer les tests.
- `-Godot` : force la reconstruction de Godot (après modification du fork).
- `-SkipGodot` : ne construit jamais Godot (échoue à la configuration s'il manque).
- `-SkipUi` : ne construit pas l'interface (installe `studio\ui\dist` s'il existe).
- `-NoStop` : échoue au lieu d'arrêter les processus SEQUA qui tournent depuis le dossier visé.
- `-Clean` : supprime `build\<preset>` et `bin\windows-x64\<type>` avant de construire.
Le script :
1. Construit Godot (LibGodot) via `build-godot.ps1` (`template_debug` pour `debug`, `template_release` pour `release`, rien pour `asan`) — 10 à 20 min la première fois par variante.
2. Lance le preset CMake via `build.ps1` (compilation et tests).
3. Construit l'interface (`npm ci` si `node_modules` manque, puis `npm run build`).
4. **Arrête les processus SEQUA** lancés depuis `bin\windows-x64\<type>\` (Studio host, Core, rendu) — Windows refuse de remplacer un fichier en cours d'utilisation. Le Core, qui tourne depuis une copie fantôme, est reconnu à son option `--sequa-root`.
5. Copie les produits vers `bin\windows-x64\<type>\` via `cmake --install` (adaptateurs, bibliothèque et projet Godot, interface, `sequa-root.json`).
Exemples :
```powershell
.\scripts\build-all.ps1
.\scripts\build-all.ps1 -Type all -NoTests
```
## Lancer
Depuis l'image de développement dans `bin\` :
```powershell
# Studio complet (Core + interface)
.\bin\windows-x64\debug\studio\sequa-studio.exe [--dev] [--watch]
# Core seul (trouve sa racine via sequa-root.json ; --sequa-root <dossier> pour forcer)
.\bin\windows-x64\debug\core\sequa-core.exe --render debug --render-headless --render-fps 10
```
## Installer (par utilisateur, sans droits administrateur)
Aucun installeur n'existe encore. Copier le contenu de `bin\windows-x64\release\` vers `%LOCALAPPDATA%\Programs\SEQUA\` et lancer `studio\sequa-studio.exe` depuis là.
Sur une autre machine sans Visual Studio : le **Microsoft Visual C++ Redistributable 2015-2022 x64** est requis. Les types `debug` et `asan` ne tournent que sur une machine de développement.
## Désinstaller
1. Fermer Studio.
2. Supprimer le programme installé : `%LOCALAPPDATA%\Programs\SEQUA\` (copie installée) ou `bin\windows-x64\<type>\` (développement).
3. Données utilisateur (peuvent être supprimées) : `%LOCALAPPDATA%\SEQUA\` contenant `run\` (fichiers d'exécution, logs, copies fantômes du Core), `downloads\`, `sdk\`.
4. Variable d'environnement utilisateur `CUDNN_PATH` si cuDNN a été installé par le script :
```powershell
[Environment]::SetEnvironmentVariable("CUDNN_PATH", $null, "User")
```
5. Restes de développement seulement : `build\`, `engines\godot\.venv\`, `studio\ui\node_modules\`, binaires Godot dans `engines\godot\vendor\godot-src\bin\`.
6. CUDA Toolkit : désinstaller via « Applications installées » de Windows.
+2 -2
View File
@@ -87,10 +87,10 @@ Le Core n'envoie aucun message spontané en 0.2.
| Option | Effet |
|---|---|
| `--core-exe <chemin>` | Exécutable source du Core (défaut : `sequa-core[.exe]` à côté de `sequa-studio`, sinon dans `build/<preset>/core/`) |
| `--core-exe <chemin>` | Exécutable source du Core (défaut : `core/sequa-core[.exe]` depuis la racine `sequa-root.json`) |
| `--core-port <n>` | 9100 |
| `--ui-port <n>` | 9180 |
| `--ui-dir <chemin>` | Dossier de l'interface construite (défaut : `studio/ui/dist` trouvé en remontant depuis l'exécutable) |
| `--ui-dir <chemin>` | Dossier de l'interface construite (défaut : `studio/ui` depuis la racine `sequa-root.json`) |
| `--dev [url]` | Ne sert pas l'interface ; ouvre le navigateur sur le serveur Vite (défaut `http://localhost:5173`) |
| `--detach-core` | Lance le Core puis lève le bail (`core/detach`) : il survivra au host |
| `--no-browser` | N'ouvre pas le navigateur |
@@ -215,12 +215,11 @@ Le faux Core répond à `render/*` avec un rendu **SIMULÉ** : tout le panneau s
### Avec le vrai Core et Godot
```powershell
# Une fois : Godot en bibliothèque, puis le preset qui contient Godot, le rendu et les adaptateurs
.\scripts\build-godot.ps1
.\scripts\build.ps1 -Preset windows-msvc-prototypes -BuildDir build\windows-msvc-prototypes
# Une fois : build complet (Godot, C++, interface, installation dans bin\)
.\scripts\build-all.ps1
# Terminal 1 : le Core seul (sans jeton : poste local uniquement)
.\build\windows-msvc-prototypes\core\sequa-core.exe
# Terminal 1 : le Core seul (trouve sa racine via sequa-root.json)
.\bin\windows-x64\debug\core\sequa-core.exe
# Terminal 2 : Studio
cd studio/ui; npm run dev # http://localhost:5173/?core=ws://127.0.0.1:9100
```
@@ -252,7 +251,7 @@ Limites connues, utiles pour lire le panneau :
```powershell
.\scripts\build.ps1 -Preset windows-msvc-debug # tout, avec les tests
.\scripts\build.ps1 -Preset windows-msvc-debug -TestRegex "Canal de rendu|render"
.\scripts\build.ps1 -Preset windows-msvc-debug -BuildDir build\windows-msvc-debug -TestRegex "Canal de rendu|render"
```
## 8. Ta liste de contrôle pour un nouveau panneau
+1
View File
@@ -21,6 +21,7 @@ if(TARGET nlohmann_json::nlohmann_json)
set_target_properties(sequa_debug_adapter_module PROPERTIES OUTPUT_NAME sequa_debug_adapter PREFIX "")
target_link_libraries(sequa_debug_adapter_module PRIVATE sequa::debug_adapter nlohmann_json::nlohmann_json)
sequa_target_defaults(sequa_debug_adapter_module)
sequa_install_adapter(sequa_debug_adapter_module debug)
endif()
if(SEQUA_BUILD_TESTS)
+27 -10
View File
@@ -15,8 +15,15 @@ sequa_target_defaults(sequa_godot_adapter)
if(SEQUA_BUILD_GODOT_RUNTIME)
set(_godot_src "${PROJECT_SOURCE_DIR}/engines/godot/vendor/godot-src")
set(SEQUA_GODOT_LIBRARY "${_godot_src}/bin/godot.windows.template_debug.x86_64.dll"
# Variante de Godot (ADR 0010) : template_debug pour Debug, template_release sinon.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(_godot_target "template_debug")
else()
set(_godot_target "template_release")
endif()
set(SEQUA_GODOT_LIBRARY "${_godot_src}/bin/godot.windows.${_godot_target}.x86_64.dll"
CACHE FILEPATH "Godot construit en bibliothèque (LibGodot)")
get_filename_component(_godot_library_name "${SEQUA_GODOT_LIBRARY}" NAME)
if(NOT WIN32)
message(FATAL_ERROR "SEQUA_BUILD_GODOT_RUNTIME : Windows seulement pour l'instant")
endif()
@@ -37,7 +44,7 @@ if(SEQUA_BUILD_GODOT_RUNTIME)
set(GODOTCPP_API_VERSION "4.7" CACHE STRING "" FORCE)
message(STATUS "godot-cpp : API officielle 4.7 (extraire celle du fork : scripts/build-godot.ps1 -Target editor)")
endif()
set(GODOTCPP_TARGET "template_debug" CACHE STRING "" FORCE)
set(GODOTCPP_TARGET "${_godot_target}" CACHE STRING "" FORCE)
# Même bibliothèque d'exécution que le reste de SEQUA (/MD, /MDd en Debug) : godot-cpp
# choisirait sinon /MT pour tout le projet.
set(GODOTCPP_USE_STATIC_CPP OFF CACHE BOOL "" FORCE)
@@ -49,22 +56,32 @@ if(SEQUA_BUILD_GODOT_RUNTIME)
set_target_properties(godot-cpp PROPERTIES PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/godot-cpp/bin")
target_sources(sequa_godot_adapter PRIVATE src/godot_host.cpp src/godot_scene.cpp)
target_link_libraries(sequa_godot_adapter PRIVATE godot::cpp)
# PROVISOIRE : chemins fixés à la construction, le processus de rendu les fournira (ADR 0008).
target_compile_definitions(sequa_godot_adapter PRIVATE
SEQUA_GODOT_RUNTIME=1
SEQUA_GODOT_DEFAULT_LIBRARY="${SEQUA_GODOT_LIBRARY}"
SEQUA_GODOT_DEFAULT_PROJECT="${CMAKE_CURRENT_SOURCE_DIR}/project")
target_link_libraries(sequa_godot_adapter PRIVATE godot::cpp sequa::layout)
target_compile_definitions(sequa_godot_adapter PRIVATE SEQUA_GODOT_RUNTIME=1)
# Bibliothèque chargée par sequa-render (ADR 0009) : point d'entrée C versionné.
add_library(sequa_godot_adapter_module MODULE src/entry_point.cpp)
set_target_properties(sequa_godot_adapter_module PROPERTIES OUTPUT_NAME sequa_godot_adapter PREFIX "")
target_link_libraries(sequa_godot_adapter_module PRIVATE sequa::godot_adapter)
target_link_libraries(sequa_godot_adapter_module PRIVATE sequa::godot_adapter sequa::layout
nlohmann_json::nlohmann_json)
# Nom seul : la bibliothèque Godot est cherchée à côté de l'adaptateur (ADR 0010).
target_compile_definitions(sequa_godot_adapter_module PRIVATE
SEQUA_GODOT_LIBRARY_NAME="${_godot_library_name}")
sequa_target_defaults(sequa_godot_adapter_module)
# Démonstration de l'étape 3 : scène SIMULÉE poussée depuis SEQUA vers Godot.
# engines/godot/ : adaptateur, bibliothèque Godot et projet chargé par Godot.
sequa_install_adapter(sequa_godot_adapter_module godot)
install(FILES "${SEQUA_GODOT_LIBRARY}" DESTINATION engines/godot)
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/project/" DESTINATION engines/godot/project
PATTERN ".godot" EXCLUDE PATTERN ".gitignore" EXCLUDE)
# Démonstration de l'étape 3 : scène SIMULÉE poussée depuis SEQUA vers Godot. Outil de
# développement, non installé : il désigne Godot et le projet dans le dépôt.
add_executable(sequa_godot_demo demo/main.cpp)
target_link_libraries(sequa_godot_demo PRIVATE sequa::godot_adapter)
target_compile_definitions(sequa_godot_demo PRIVATE
SEQUA_GODOT_DEMO_LIBRARY="${SEQUA_GODOT_LIBRARY}"
SEQUA_GODOT_DEMO_PROJECT="${CMAKE_CURRENT_SOURCE_DIR}/project")
sequa_target_defaults(sequa_godot_demo)
if(SEQUA_BUILD_TESTS)
add_test(NAME sequa_godot_demo COMMAND sequa_godot_demo --frames 300)
+25
View File
@@ -19,6 +19,31 @@ Implémentation de `sequa::three_d::I3DEngineAdapter` (voir [3d/](../../../3d))
| `src/godot_scene.*` | Nœuds Godot qui reflètent la scène SEQUA (deltas déjà validés par `SceneState`) |
| `include/sequa/godot/conversions.hpp` | Conversions pures (rotation, couleur sRGB, formes), testées sans moteur |
### Recherche de la bibliothèque et du projet Godot (ADR 0010)
L'adaptateur est installé dans `engines/godot/` à côté de la bibliothèque Godot (LibGodot) et du dossier `project/` chargé par Godot.
- **Par défaut** : l'adaptateur cherche la bibliothèque (`SEQUA_GODOT_LIBRARY_NAME`, ex. `godot.windows.template_release.x86_64.dll`) et le projet `project/` dans le même dossier que lui (`engines/godot/`).
- **Surcharge via `--adapter-options`** (JSON, chemins en UTF-8) :
```json
{ "godotLibrary": "C:/chemin/vers/godot.dll", "project": "C:/chemin/vers/projet" }
```
Les deux clés sont facultatives ; si absentes, les valeurs par défaut (côté de l'adaptateur) sont utilisées.
### Variantes de construction Godot (template_debug / template_release)
`scripts/build-godot.ps1` construit Godot en bibliothèque partagée (LibGodot) avec deux cibles :
- **template_debug** : utilisée automatiquement en build Debug (`CMAKE_BUILD_TYPE=Debug`). Contient les assertions et symboles de débogage.
- **template_release** : utilisée en build Release/RelWithDebInfo/MinSizeRel. Optimisée, sans assertions.
Le script est invoqué ainsi :
```powershell
.\scripts\build-godot.ps1 # template_debug (par défaut)
.\scripts\build-godot.ps1 -Target template_release
```
CMake sélectionne la variante selon `CMAKE_BUILD_TYPE` (voir `CMakeLists.txt` lignes 19-23). Le nom de fichier de la bibliothèque (`SEQUA_GODOT_LIBRARY_NAME`) est passé en définition de compilation au module `sequa_godot_adapter_module` uniquement.
### Décisions de l'étape 3 (2026-11-07)
- **Frontière binaire reportée** (question ouverte de l'ADR 0008) : l'adaptateur reste une bibliothèque liée statiquement au programme qui l'utilise. Le passage en bibliothèque chargée, avec point d'entrée C versionné, se fera avec le processus de rendu (étape 4), en même temps que le choix du transport Core ↔ rendu.
+4 -1
View File
@@ -10,6 +10,7 @@
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <string>
#include <variant>
#include <vector>
@@ -73,7 +74,9 @@ int main(int argc, char** argv)
}
}
godot::GodotEngineAdapter adapter;
godot::GodotEngineAdapter adapter(godot::GodotPaths{
std::filesystem::path(SEQUA_GODOT_DEMO_LIBRARY),
std::filesystem::path(SEQUA_GODOT_DEMO_PROJECT)});
three_d::EngineConfig config;
config.window_title = "SEQUA - démonstration adaptateur Godot (scène SIMULÉE)";
if (auto error = adapter.start(config)) {
@@ -3,10 +3,17 @@
#include "sequa/3d/adapter.hpp"
#include <filesystem>
#include <memory>
namespace sequa::godot {
// Chemins de la bibliothèque Godot (LibGodot) et du projet Godot chargé par le moteur.
struct GodotPaths {
std::filesystem::path library; // chemin vers la DLL/so de LibGodot
std::filesystem::path project; // dossier du projet Godot (contenant project.godot)
};
// Version de Godot visée (sous-module engines/godot/vendor/godot-src).
inline constexpr const char* engine_version = "4.7.2-stable";
@@ -28,7 +35,10 @@ inline constexpr const char* engine_version = "4.7.2-stable";
// PROVISOIRE : render_frame() ne lit encore aucun tampon (étape 5).
class GodotEngineAdapter final : public three_d::I3DEngineAdapter {
public:
// Constructeur par défaut : chemins vides (utilisé par les tests).
GodotEngineAdapter();
// Constructeur avec chemins explicites de la bibliothèque et du projet Godot.
explicit GodotEngineAdapter(GodotPaths paths);
~GodotEngineAdapter() override;
[[nodiscard]] three_d::EngineCapabilities capabilities() const override { return godot_capabilities(); }
@@ -41,8 +51,9 @@ public:
[[nodiscard]] three_d::FrameOutcome render_frame(const three_d::FrameRequest& request) override;
private:
struct Runtime; // moteur hébergé et nœuds de la scène (vide sans SEQUA_BUILD_GODOT_RUNTIME)
struct Runtime; // moteur hébergé et nœuds de la scène (vide sans SEQUA_GODOT_RUNTIME)
GodotPaths paths_{};
three_d::SceneState scene_;
std::unique_ptr<Runtime> runtime_;
bool running_ = false;
+48 -6
View File
@@ -1,27 +1,69 @@
// SPDX-License-Identifier: MPL-2.0
// Point d'entrée de la bibliothèque d'adaptateur Godot (ADR 0009).
//
// Réglages (--adapter-options de sequa-render, tous facultatifs) :
// { "godotLibrary": "<chemin vers LibGodot>", "project": "<dossier du projet Godot>" }
// Les chemins sont en UTF-8. Par défaut : bibliothèque et projet à côté de l'adaptateur
// (engines/godot/ d'après ADR 0010).
#include "sequa/3d/adapter_library.hpp"
#include "sequa/godot/godot_adapter.hpp"
#include "sequa/platform/layout.hpp"
#include <nlohmann/json.hpp>
#include <cstring>
#include <filesystem>
#include <new>
#include <string>
SEQUA_ADAPTER_EXPORT sequa::three_d::I3DEngineAdapter* sequa_create_engine_adapter(std::uint32_t interface_version,
const char* options_json,
char* error, std::size_t error_size)
const char* options_json,
char* error, std::size_t error_size)
{
using sequa::three_d::write_adapter_error;
if (interface_version != sequa::three_d::adapter_interface_version) {
write_adapter_error(error, error_size, "adaptateur Godot : version d'interface différente");
return nullptr;
}
// Aucun réglage propre à Godot pour l'instant : seuls l'absence d'options et « {} » sont acceptés.
if (options_json != nullptr && options_json[0] != '\0' && std::strcmp(options_json, "{}") != 0) {
write_adapter_error(error, error_size, "adaptateur Godot : aucun réglage accepté");
// Valeurs par défaut : bibliothèque et projet à côté de l'adaptateur.
const std::filesystem::path adapter_dir =
sequa::platform::module_path(reinterpret_cast<const void*>(&sequa_create_engine_adapter)).parent_path();
std::filesystem::path library_path = adapter_dir / SEQUA_GODOT_LIBRARY_NAME;
std::filesystem::path project_path = adapter_dir / "project";
// Analyse des options JSON si fournies.
const std::string text = options_json != nullptr && options_json[0] != '\0' ? options_json : "{}";
const nlohmann::json options = nlohmann::json::parse(text, nullptr, false);
if (options.is_discarded() || !options.is_object()) {
write_adapter_error(error, error_size, "adaptateur Godot : réglages JSON invalides");
return nullptr;
}
return new (std::nothrow) sequa::godot::GodotEngineAdapter();
// Clés autorisées : "godotLibrary" et "project" (chaînes UTF-8).
for (const auto& [key, value] : options.items()) {
if (key == "godotLibrary") {
if (!value.is_string()) {
write_adapter_error(error, error_size, "adaptateur Godot : \"godotLibrary\" doit être une chaîne");
return nullptr;
}
library_path = sequa::platform::path_from_utf8(value.get<std::string>());
} else if (key == "project") {
if (!value.is_string()) {
write_adapter_error(error, error_size, "adaptateur Godot : \"project\" doit être une chaîne");
return nullptr;
}
project_path = sequa::platform::path_from_utf8(value.get<std::string>());
} else {
write_adapter_error(error, error_size,
("adaptateur Godot : clé inconnue \"" + key + "\" dans les réglages").c_str());
return nullptr;
}
}
sequa::godot::GodotPaths paths{std::move(library_path), std::move(project_path)};
return new (std::nothrow) sequa::godot::GodotEngineAdapter(std::move(paths));
}
SEQUA_ADAPTER_EXPORT void sequa_destroy_engine_adapter(sequa::three_d::I3DEngineAdapter* adapter)
+3 -1
View File
@@ -44,6 +44,8 @@ bool runtime_available() noexcept
GodotEngineAdapter::GodotEngineAdapter() = default;
GodotEngineAdapter::GodotEngineAdapter(GodotPaths paths) : paths_(std::move(paths)) {}
GodotEngineAdapter::~GodotEngineAdapter()
{
stop();
@@ -55,7 +57,7 @@ std::optional<Error> GodotEngineAdapter::start(const three_d::EngineConfig& conf
return Error{Error::Code::already_started, "adaptateur Godot déjà démarré"};
}
#if SEQUA_GODOT_RUNTIME
GodotHost::StartOutcome outcome = GodotHost::start(config);
GodotHost::StartOutcome outcome = GodotHost::start(config, paths_);
if (auto* error = std::get_if<Error>(&outcome)) {
return std::move(*error);
}
+17 -8
View File
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MPL-2.0
#include "godot_host.hpp"
#include "sequa/platform/layout.hpp"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/godot_instance.hpp>
#include <godot_cpp/classes/main_loop.hpp>
@@ -50,12 +52,14 @@ GDExtensionBool host_init(GDExtensionInterfaceGetProcAddress get_proc_address, G
return init.init();
}
void* load_library(const char* path)
void* load_library(const std::filesystem::path& path)
{
#ifdef _WIN32
return LoadLibraryA(path);
// Chemin large pour prendre en charge les chemins Unicode.
return LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
#else
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
// POSIX : le chemin est déjà en UTF-8 natif.
return dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
#endif
}
@@ -114,7 +118,7 @@ LibraryKeeper g_library_keeper __attribute__((init_priority(101)));
} // namespace
GodotHost::StartOutcome GodotHost::start(const three_d::EngineConfig& config)
GodotHost::StartOutcome GodotHost::start(const three_d::EngineConfig& config, const GodotPaths& paths)
{
if (config.headless) {
// --headless de Godot coupe le rendu ; le vrai headless viendra du fork (ADR 0007).
@@ -127,12 +131,15 @@ GodotHost::StartOutcome GodotHost::start(const three_d::EngineConfig& config)
return Error{Error::Code::engine_failure,
"Godot ne redémarre pas dans le même processus (LibGodot) : relancer le processus de rendu"};
}
if (paths.library.empty() || paths.project.empty()) {
return Error{Error::Code::engine_failure,
"Godot : chemins de la bibliothèque et du projet non fournis"};
}
// PROVISOIRE : chemins fixés à la construction ; le processus de rendu les fournira (ADR 0008).
void* library = load_library(SEQUA_GODOT_DEFAULT_LIBRARY);
void* library = load_library(paths.library);
if (library == nullptr) {
return Error{Error::Code::engine_failure,
std::string("Godot : impossible de charger ") + SEQUA_GODOT_DEFAULT_LIBRARY};
"Godot : impossible de charger " + sequa::platform::path_to_utf8(paths.library)};
}
auto* create = reinterpret_cast<CreateInstanceFn>(load_symbol(library, "libgodot_create_godot_instance"));
if (create == nullptr || load_symbol(library, "libgodot_destroy_godot_instance") == nullptr) {
@@ -140,9 +147,11 @@ GodotHost::StartOutcome GodotHost::start(const three_d::EngineConfig& config)
return Error{Error::Code::engine_failure, "Godot : bibliothèque construite sans library_type=shared_library"};
}
// Argument --path encodé en UTF-8 (Godot attend de l'UTF-8).
std::string project_utf8 = sequa::platform::path_to_utf8(paths.project);
std::vector<std::string> args = {"sequa",
"--path",
SEQUA_GODOT_DEFAULT_PROJECT,
project_utf8,
"--rendering-driver",
"vulkan",
"--resolution",
+2 -1
View File
@@ -2,6 +2,7 @@
#pragma once
#include "sequa/3d/adapter.hpp"
#include "sequa/godot/godot_adapter.hpp"
#include <memory>
#include <variant>
@@ -20,7 +21,7 @@ namespace sequa::godot {
class GodotHost {
public:
using StartOutcome = std::variant<std::unique_ptr<GodotHost>, three_d::Error>;
[[nodiscard]] static StartOutcome start(const three_d::EngineConfig& config);
[[nodiscard]] static StartOutcome start(const three_d::EngineConfig& config, const GodotPaths& paths);
GodotHost(const GodotHost&) = delete;
GodotHost& operator=(const GodotHost&) = delete;
@@ -42,3 +42,20 @@ TEST_CASE("GodotEngineAdapter : rendu sans fenetre refuse")
error->code == three_d::Error::Code::not_implemented));
CHECK_FALSE(adapter.running());
}
TEST_CASE("GodotEngineAdapter : constructeur par defaut avec chemins vides echoue au start si runtime disponible")
{
if (!godot::runtime_available()) {
return; // sans runtime, start() renvoie not_implemented
}
godot::GodotEngineAdapter adapter; // chemins vides
three_d::EngineConfig config;
config.width = 800;
config.height = 600;
config.window_title = "test";
const auto error = adapter.start(config);
REQUIRE(error.has_value());
CHECK(error->code == three_d::Error::Code::engine_failure);
CHECK(error->message.find("non fournis") != std::string::npos);
CHECK_FALSE(adapter.running());
}
+33
View File
@@ -0,0 +1,33 @@
# SPDX-License-Identifier: MPL-2.0
#
# Bibliothèque de disposition de l'arborescence SEQUA (ADR 0010).
# Fournit : recherche de la racine (sequa-root.json), chemins relatifs
# (core/, render/, studio/, engines/<moteur>/), noms d'exécutables et
# de modules selon la plateforme. Utilisée par le Core, le Studio host
# et les adaptateurs.
add_library(sequa_layout
src/layout.cpp)
add_library(sequa::layout ALIAS sequa_layout)
target_include_directories(sequa_layout
PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_compile_features(sequa_layout PUBLIC cxx_std_20)
if(WIN32)
target_link_libraries(sequa_layout PRIVATE shell32) # CommandLineToArgvW
elseif(NOT APPLE)
# dladdr pour module_path.
target_link_libraries(sequa_layout PUBLIC ${CMAKE_DL_LIBS})
endif()
sequa_target_defaults(sequa_layout)
if(SEQUA_BUILD_TESTS)
add_executable(sequa_layout_tests
tests/main.cpp
tests/test_layout.cpp)
target_link_libraries(sequa_layout_tests PRIVATE sequa::layout doctest::doctest)
sequa_target_defaults(sequa_layout_tests)
doctest_discover_tests(sequa_layout_tests)
endif()
+44
View File
@@ -0,0 +1,44 @@
# Plateforme : disposition de l'arborescence
Brique commune qui connaît la structure de l'installation SEQUA (ADR 0010).
Elle permet à chaque composant (Studio host, Core, rendu, adaptateurs) de
retrouver les autres binaires et ressources à partir d'un unique repère :
le fichier `sequa-root.json` à la racine.
## Arborescence attendue
```text
<root>/sequa-root.json
<root>/core/sequa-core(.exe)
<root>/render/sequa-render(.exe)
<root>/studio/sequa-studio(.exe)
<root>/studio/ui/ (interface web, index.html)
<root>/engines/<engine>/sequa_<engine>_adapter(.dll|.so)
```
## API (`sequa::platform`)
| Pièce | Rôle |
|---|---|
| `root_marker` | Nom du fichier repère (`"sequa-root.json"`). |
| `current_executable()` | Chemin absolu de l'exécutable en cours (Windows / Linux / macOS). |
| `module_path(address)` | Chemin du module (exe ou bibliothèque partagée) contenant `address` (dladdr / GetModuleHandleEx). |
| `find_root(start)` | Remonte depuis `start` jusqu'à la racine du FS ; premier dossier contenant `sequa-root.json`. |
| `executable_name(base)` | Ajoute `.exe` sous Windows, sinon inchangé. |
| `module_name(base)` | Ajoute `.dll` sous Windows, `.so` ailleurs (macOS compris, suffixe CMake MODULE). |
| `is_valid_engine_name(engine)` | Non vide, seulement `[a-z0-9_]`. |
| `utf8_arguments(argc, argv)` | Arguments en UTF-8 sans le nom du programme (Windows : relus par `CommandLineToArgvW`, argv étant dans la page de code locale). |
| `path_from_utf8(text)`, `path_to_utf8(path)` | Conversions chemin ↔ UTF-8 ; à utiliser pour tout chemin passé en argument à un processus enfant ou reçu de lui. |
| `Layout(root)` | Façade orientée objet : `core_executable()`, `render_executable()`, `studio_executable()`, `ui_dir()`, `engine_dir(engine)`, `adapter_library(engine)` (nullopt si nom invalide). |
## Utilisation
- **Studio host** : trouve la racine via `find_root(current_executable())`, construit un `Layout`, passe `--sequa-root <root>` au Core lancé (copie fantôme, ADR 0006).
- **Core** : reçoit `--sequa-root`, en déduit `sequa-render` et l'adaptateur moteur via `Layout`.
- **Adaptateurs** : `module_path` sur un symbole interne → dossier de l'adaptateur → `engines/<moteur>/` pour la bibliothèque Godot et le projet (ADR 0010 §7.0.1).
Aucune dépendance externe hors STL. Tests avec doctest (MIT).
## Référence
[ADR 0010 : Arborescence des binaires (développement et installation), build unifié](../../docs/adr/0010-arborescence-des-binaires-et-build-unifie.md)
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MPL-2.0
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace sequa::platform {
// Fichier repère à la racine de l'arborescence SEQUA (ADR 0010).
inline constexpr const char* root_marker = "sequa-root.json";
// Chemin absolu de l'exécutable courant.
// Windows : GetModuleFileNameW avec tampon grandissant.
// Linux : readlink("/proc/self/exe").
// macOS : _NSGetExecutablePath.
// Chemin vide en cas d'échec.
[[nodiscard]] std::filesystem::path current_executable();
// Chemin du module (exe ou bibliothèque partagée) qui contient `address`.
// Windows : GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT) + GetModuleFileNameW.
// POSIX : dladdr.
// Chemin vide en cas d'échec.
[[nodiscard]] std::filesystem::path module_path(const void* address);
// Premier dossier qui contient root_marker (fichier régulier), en partant de
// `start` (inclus) et en remontant jusqu'à la racine du système de fichiers.
[[nodiscard]] std::optional<std::filesystem::path> find_root(const std::filesystem::path& start);
// "sequa-core" -> "sequa-core.exe" sous Windows, inchangé ailleurs.
[[nodiscard]] std::string executable_name(std::string_view base);
// Bibliothèque chargée (CMake MODULE avec PREFIX "") :
// "x" -> "x.dll" sous Windows, "x.so" ailleurs (macOS compris : suffixe des modules CMake).
[[nodiscard]] std::string module_name(std::string_view base);
// Vrai si `engine` est non vide et ne contient que [a-z0-9_].
[[nodiscard]] bool is_valid_engine_name(std::string_view engine) noexcept;
// Arguments de la ligne de commande en UTF-8, sans le nom du programme. Sous Windows,
// argv est dans la page de code locale : ils sont relus par CommandLineToArgvW. Ailleurs,
// argv est repris tel quel (UTF-8 par convention).
[[nodiscard]] std::vector<std::string> utf8_arguments(int argc, char** argv);
// Conversions entre chemin et UTF-8 (les arguments des processus enfants sont en UTF-8,
// voir sequa::platform::ChildProcess). std::filesystem::path(std::string) supposerait la
// page de code locale sous Windows.
[[nodiscard]] std::filesystem::path path_from_utf8(std::string_view text);
[[nodiscard]] std::string path_to_utf8(const std::filesystem::path& path);
class Layout {
public:
explicit Layout(std::filesystem::path root);
[[nodiscard]] const std::filesystem::path& root() const noexcept;
[[nodiscard]] std::filesystem::path core_executable() const; // root/core/sequa-core[.exe]
[[nodiscard]] std::filesystem::path render_executable() const; // root/render/sequa-render[.exe]
[[nodiscard]] std::filesystem::path studio_executable() const; // root/studio/sequa-studio[.exe]
[[nodiscard]] std::filesystem::path ui_dir() const; // root/studio/ui
[[nodiscard]] std::filesystem::path engine_dir(std::string_view engine) const; // root/engines/<engine>
// root/engines/<engine>/sequa_<engine>_adapter + suffixe module ;
// nullopt si nom invalide.
[[nodiscard]] std::optional<std::filesystem::path> adapter_library(std::string_view engine) const;
private:
std::filesystem::path root_;
};
} // namespace sequa::platform
+238
View File
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: MPL-2.0
#include "sequa/platform/layout.hpp"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <system_error>
#include <utility>
#if defined(_WIN32)
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
# include <shellapi.h>
#else
# include <dlfcn.h>
# include <unistd.h>
# if defined(__APPLE__)
# include <mach-o/dyld.h>
# endif
#endif
namespace sequa::platform {
namespace {
// Vérifie si un chemin est un fichier régulier (sans lever d'exception).
[[nodiscard]] bool is_regular_file_noexcept(const std::filesystem::path& p) noexcept {
std::error_code ec;
const auto status = std::filesystem::status(p, ec);
return !ec && std::filesystem::is_regular_file(status);
}
} // namespace
std::filesystem::path current_executable() {
#if defined(_WIN32)
std::wstring buffer;
buffer.resize(256);
while (true) {
const DWORD size = static_cast<DWORD>(buffer.size());
const DWORD copied = ::GetModuleFileNameW(nullptr, buffer.data(), size);
if (copied == 0) {
return {};
}
if (copied < size) {
buffer.resize(copied);
break;
}
buffer.resize(buffer.size() * 2);
}
return std::filesystem::path(buffer).make_preferred();
#elif defined(__APPLE__)
std::uint32_t size = 1024;
std::string buffer(size, '\0');
while (true) {
const int result = _NSGetExecutablePath(buffer.data(), &size);
if (result == 0) {
buffer.resize(std::strlen(buffer.c_str()));
break;
}
buffer.resize(size);
}
// Résoudre les liens symboliques éventuels
std::error_code ec;
const auto resolved = std::filesystem::canonical(buffer, ec);
return ec ? std::filesystem::path(buffer).make_preferred() : resolved.make_preferred();
#else
// Linux : /proc/self/exe
std::error_code ec;
const auto path = std::filesystem::read_symlink("/proc/self/exe", ec);
return ec ? std::filesystem::path{} : path.make_preferred();
#endif
}
std::filesystem::path module_path(const void* address) {
#if defined(_WIN32)
HMODULE module = nullptr;
const BOOL ok = ::GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(address),
&module);
if (!ok || module == nullptr) {
return {};
}
std::wstring buffer;
buffer.resize(256);
while (true) {
const DWORD size = static_cast<DWORD>(buffer.size());
const DWORD copied = ::GetModuleFileNameW(module, buffer.data(), size);
if (copied == 0) {
return {};
}
if (copied < size) {
buffer.resize(copied);
break;
}
buffer.resize(buffer.size() * 2);
}
return std::filesystem::path(buffer).make_preferred();
#else
Dl_info info{};
if (dladdr(address, &info) == 0 || info.dli_fname == nullptr) {
return {};
}
std::error_code ec;
const auto path = std::filesystem::canonical(info.dli_fname, ec);
return ec ? std::filesystem::path(info.dli_fname).make_preferred() : path.make_preferred();
#endif
}
std::optional<std::filesystem::path> find_root(const std::filesystem::path& start) {
std::error_code ec;
auto current = std::filesystem::absolute(start, ec);
if (ec) {
return std::nullopt;
}
while (true) {
const auto marker_path = current / root_marker;
if (is_regular_file_noexcept(marker_path)) {
return current;
}
const auto parent = current.parent_path();
if (parent == current) {
// Atteint la racine du système de fichiers
break;
}
current = parent;
}
return std::nullopt;
}
std::string executable_name(std::string_view base) {
#if defined(_WIN32)
return std::string(base) + ".exe";
#else
return std::string(base);
#endif
}
std::string module_name(std::string_view base) {
#if defined(_WIN32)
return std::string(base) + ".dll";
#else
return std::string(base) + ".so";
#endif
}
bool is_valid_engine_name(std::string_view engine) noexcept {
if (engine.empty()) {
return false;
}
return std::ranges::all_of(engine, [](char c) {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
});
}
std::vector<std::string> utf8_arguments(int argc, char** argv)
{
#if defined(_WIN32)
(void)argc;
(void)argv;
std::vector<std::string> result;
int count = 0;
LPWSTR* wide = ::CommandLineToArgvW(::GetCommandLineW(), &count);
if (wide == nullptr) {
return result;
}
for (int i = 1; i < count; ++i) {
result.push_back(path_to_utf8(std::filesystem::path(wide[i])));
}
::LocalFree(static_cast<HLOCAL>(wide));
return result;
#else
std::vector<std::string> result;
for (int i = 1; i < argc; ++i) {
result.emplace_back(argv[i]);
}
return result;
#endif
}
std::filesystem::path path_from_utf8(std::string_view text)
{
const std::u8string utf8(text.begin(), text.end());
return std::filesystem::path(utf8);
}
std::string path_to_utf8(const std::filesystem::path& path)
{
const std::u8string utf8 = path.u8string();
return std::string(utf8.begin(), utf8.end());
}
Layout::Layout(std::filesystem::path root) : root_(std::move(root)) {}
const std::filesystem::path& Layout::root() const noexcept {
return root_;
}
std::filesystem::path Layout::core_executable() const {
return root_ / "core" / executable_name("sequa-core");
}
std::filesystem::path Layout::render_executable() const {
return root_ / "render" / executable_name("sequa-render");
}
std::filesystem::path Layout::studio_executable() const {
return root_ / "studio" / executable_name("sequa-studio");
}
std::filesystem::path Layout::ui_dir() const {
return root_ / "studio" / "ui";
}
std::filesystem::path Layout::engine_dir(std::string_view engine) const {
return root_ / "engines" / engine;
}
std::optional<std::filesystem::path> Layout::adapter_library(std::string_view engine) const {
if (!is_valid_engine_name(engine)) {
return std::nullopt;
}
const std::string adapter_base = "sequa_" + std::string(engine) + "_adapter";
return engine_dir(engine) / module_name(adapter_base);
}
} // namespace sequa::platform
+3
View File
@@ -0,0 +1,3 @@
// SPDX-License-Identifier: MPL-2.0
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
+228
View File
@@ -0,0 +1,228 @@
// SPDX-License-Identifier: MPL-2.0
#include "sequa/platform/layout.hpp"
#include <doctest/doctest.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
namespace sequa::platform::testing {
// Dossier temporaire propre à un test, supprimé à la fin.
class TempDir {
public:
TempDir()
: path_(std::filesystem::temp_directory_path()
/ ("sequa-layout-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + "-"
+ std::to_string(++counter_)))
{
std::filesystem::create_directories(path_);
}
~TempDir()
{
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
TempDir(const TempDir&) = delete;
TempDir& operator=(const TempDir&) = delete;
[[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; }
private:
inline static int counter_ = 0;
std::filesystem::path path_;
};
// Écrit un fichier repère vide.
inline void touch(const std::filesystem::path& file)
{
std::ofstream out(file);
REQUIRE(out.good());
}
// Fonction dont l'adresse sert à module_path.
inline void function_in_test_executable() {}
} // namespace sequa::platform::testing
TEST_SUITE("sequa::platform::layout") {
TEST_CASE("find_root finds marker in start directory itself") {
using namespace sequa::platform;
using namespace sequa::platform::testing;
TempDir tmp;
const auto root = tmp.path();
std::error_code ec;
std::filesystem::create_directories(root / "core", ec);
REQUIRE(!ec);
std::filesystem::create_directories(root / "render", ec);
REQUIRE(!ec);
// Créer le fichier repère
const auto marker = root / root_marker;
std::filesystem::create_directories(marker.parent_path(), ec);
REQUIRE(!ec);
touch(marker);
const auto found = find_root(root);
REQUIRE(found.has_value());
CHECK(std::filesystem::equivalent(found.value(), root));
}
TEST_CASE("find_root finds marker two levels up from nested directory") {
using namespace sequa::platform;
using namespace sequa::platform::testing;
TempDir tmp;
const auto root = tmp.path();
const auto nested = root / "a" / "b" / "c";
std::error_code ec;
std::filesystem::create_directories(nested, ec);
REQUIRE(!ec);
// Créer le fichier repère à la racine
const auto marker = root / root_marker;
touch(marker);
const auto found = find_root(nested);
REQUIRE(found.has_value());
CHECK(std::filesystem::equivalent(found.value(), root));
}
TEST_CASE("find_root returns nullopt when marker absent") {
using namespace sequa::platform;
using namespace sequa::platform::testing;
TempDir tmp;
const auto root = tmp.path();
const auto nested = root / "a" / "b";
std::error_code ec;
std::filesystem::create_directories(nested, ec);
REQUIRE(!ec);
const auto found = find_root(nested);
CHECK_FALSE(found.has_value());
}
TEST_CASE("Layout paths match expected structure") {
using namespace sequa::platform;
using namespace sequa::platform::testing;
TempDir tmp;
const auto root = tmp.path();
Layout layout(root);
CHECK(layout.root() == root);
CHECK(layout.core_executable() == root / "core" / executable_name("sequa-core"));
CHECK(layout.render_executable() == root / "render" / executable_name("sequa-render"));
CHECK(layout.studio_executable() == root / "studio" / executable_name("sequa-studio"));
CHECK(layout.ui_dir() == root / "studio" / "ui");
CHECK(layout.engine_dir("godot") == root / "engines" / "godot");
}
TEST_CASE("adapter_library returns correct filename and rejects invalid names") {
using namespace sequa::platform;
using namespace sequa::platform::testing;
TempDir tmp;
const auto root = tmp.path();
Layout layout(root);
// Nom valide
const auto godot_adapter = layout.adapter_library("godot");
REQUIRE(godot_adapter.has_value());
CHECK(godot_adapter.value() == root / "engines" / "godot" / module_name("sequa_godot_adapter"));
// Nom invalide : contient des caractères non autorisés
CHECK_FALSE(layout.adapter_library("../x").has_value());
CHECK_FALSE(layout.adapter_library("").has_value());
CHECK_FALSE(layout.adapter_library("Godot").has_value()); // majuscule
CHECK_FALSE(layout.adapter_library("godot-1").has_value()); // tiret
CHECK_FALSE(layout.adapter_library("godot ").has_value()); // espace
}
TEST_CASE("executable_name and module_name per platform") {
using namespace sequa::platform;
#if defined(_WIN32)
CHECK(executable_name("sequa-core") == "sequa-core.exe");
CHECK(module_name("sequa_godot_adapter") == "sequa_godot_adapter.dll");
#else
CHECK(executable_name("sequa-core") == "sequa-core");
CHECK(module_name("sequa_godot_adapter") == "sequa_godot_adapter.so");
#endif
}
TEST_CASE("current_executable returns non-empty existing path") {
using namespace sequa::platform;
const auto exe = current_executable();
CHECK_FALSE(exe.empty());
std::error_code ec;
CHECK(std::filesystem::exists(exe, ec));
CHECK(!ec);
}
TEST_CASE("module_path of a function in this test equals current_executable") {
using namespace sequa::platform;
const auto exe = current_executable();
REQUIRE_FALSE(exe.empty());
// Adresse d'une fonction locale pour tester module_path
const auto mod = module_path(reinterpret_cast<const void*>(&testing::function_in_test_executable));
REQUIRE_FALSE(mod.empty());
std::error_code ec;
CHECK(std::filesystem::equivalent(exe, mod, ec));
CHECK(!ec);
}
TEST_CASE("is_valid_engine_name accepts valid names and rejects invalid ones") {
using namespace sequa::platform;
CHECK(is_valid_engine_name("godot"));
CHECK(is_valid_engine_name("debug"));
CHECK(is_valid_engine_name("engine_123"));
CHECK(is_valid_engine_name("a"));
CHECK_FALSE(is_valid_engine_name(""));
CHECK_FALSE(is_valid_engine_name("Godot"));
CHECK_FALSE(is_valid_engine_name("godot-1"));
CHECK_FALSE(is_valid_engine_name("godot "));
CHECK_FALSE(is_valid_engine_name("godot.engine"));
CHECK_FALSE(is_valid_engine_name("../godot"));
CHECK_FALSE(is_valid_engine_name("godot/evil"));
}
TEST_CASE("path_from_utf8 and path_to_utf8 keep non-ASCII characters") {
using namespace sequa::platform;
const std::string utf8 = "C:/Users/Am\xC3\xA9lie/SEQUA"; // « é » en UTF-8
const auto path = path_from_utf8(utf8);
CHECK(path.filename() == path_from_utf8("SEQUA"));
CHECK(path_to_utf8(path) == utf8);
CHECK(path.u8string() == u8"C:/Users/Amélie/SEQUA");
}
TEST_CASE("utf8_arguments drops the program name") {
using namespace sequa::platform;
char program[] = "sequa-layout-tests";
char* argv[] = {program, nullptr};
const auto args = utf8_arguments(1, argv);
#if !defined(_WIN32)
CHECK(args.empty());
#else
// Sous Windows, la vraie ligne de commande du test (arguments de doctest éventuels).
CHECK(args.size() < 64);
#endif
}
} // TEST_SUITE
+2 -1
View File
@@ -8,8 +8,9 @@ add_executable(sequa_render
src/adapter_library.cpp
src/core_link.cpp)
set_target_properties(sequa_render PROPERTIES OUTPUT_NAME sequa-render)
target_link_libraries(sequa_render PRIVATE sequa::3d sequa::3d_wire ixwebsocket::ixwebsocket)
target_link_libraries(sequa_render PRIVATE sequa::3d sequa::3d_wire sequa::layout ixwebsocket::ixwebsocket)
if(UNIX)
target_link_libraries(sequa_render PRIVATE ${CMAKE_DL_LIBS})
endif()
sequa_target_defaults(sequa_render)
sequa_install_executable(sequa_render render)
+3 -3
View File
@@ -13,10 +13,10 @@ Spécification : [docs/specs/protocole-core-rendu-0.1.md](../docs/specs/protocol
## Lancer (par le Core)
```powershell
# Godot, fenêtre de jeu (preset avec SEQUA_BUILD_GODOT_RUNTIME)
.\build\windows-msvc-prototypes\core\sequa-core.exe --render godot
# Godot, fenêtre de jeu (type debug ou release avec Godot hébergé)
.\bin\windows-x64\debug\core\sequa-core.exe --render godot
# Sans fenêtre : vue de côté en vidéo à 10 images/s (ffmpeg facultatif, sinon images PPM)
.\build\windows-msvc-debug\core\sequa-core.exe --render debug --render-headless --render-fps 10 `
.\bin\windows-x64\debug\core\sequa-core.exe --render debug --render-headless --render-fps 10 `
--render-options '{\"output\": \"build/render-debug.mp4\"}'
```
+3 -2
View File
@@ -2,6 +2,7 @@
#include "adapter_library.hpp"
#include "sequa/3d/adapter_library.hpp"
#include "sequa/platform/layout.hpp"
#include <array>
@@ -53,13 +54,13 @@ AdapterLibrary::LoadOutcome AdapterLibrary::load(const std::filesystem::path& pa
{
void* library = open_library(path);
if (library == nullptr) {
return "impossible de charger " + path.string();
return "impossible de charger " + sequa::platform::path_to_utf8(path);
}
auto* create = reinterpret_cast<three_d::CreateAdapterFn>(find_symbol(library, three_d::create_adapter_symbol));
auto* destroy = reinterpret_cast<three_d::DestroyAdapterFn>(find_symbol(library, three_d::destroy_adapter_symbol));
if (create == nullptr || destroy == nullptr) {
close_library(library);
return path.string() + " n'est pas une bibliothèque d'adaptateur SEQUA";
return sequa::platform::path_to_utf8(path) + " n'est pas une bibliothèque d'adaptateur SEQUA";
}
std::array<char, 512> error{};
+11 -9
View File
@@ -8,6 +8,7 @@
#include "core_link.hpp"
#include "sequa/3d/wire.hpp"
#include "sequa/platform/layout.hpp"
#include <algorithm>
#include <chrono>
@@ -47,12 +48,12 @@ std::optional<std::uint32_t> parse_uint(const char* text)
return static_cast<std::uint32_t>(value);
}
std::optional<Options> parse(int argc, char** argv)
std::optional<Options> parse(const std::vector<std::string>& args)
{
Options o;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
const char* value = i + 1 < argc ? argv[i + 1] : nullptr;
for (std::size_t i = 0; i < args.size(); ++i) {
const std::string_view arg = args[i];
const char* value = i + 1 < args.size() ? args[i + 1].c_str() : nullptr;
auto take_uint = [&](std::uint32_t& out) {
const auto parsed = value != nullptr ? parse_uint(value) : std::nullopt;
if (parsed) {
@@ -62,11 +63,11 @@ std::optional<Options> parse(int argc, char** argv)
return parsed.has_value();
};
if (arg == "--core" && value != nullptr) {
o.core_url = argv[++i];
o.core_url = args[++i];
} else if (arg == "--adapter" && value != nullptr) {
o.adapter = argv[++i];
o.adapter = args[++i];
} else if (arg == "--adapter-options" && value != nullptr) {
o.adapter_options = argv[++i];
o.adapter_options = args[++i];
} else if (arg == "--width") {
if (!take_uint(o.width)) return std::nullopt;
} else if (arg == "--height") {
@@ -136,7 +137,7 @@ int run(const Options& options)
return exit_startup;
}
render::AdapterLibrary::LoadOutcome loaded = render::AdapterLibrary::load(options.adapter, options.adapter_options);
render::AdapterLibrary::LoadOutcome loaded = render::AdapterLibrary::load(sequa::platform::path_from_utf8(options.adapter), options.adapter_options);
if (auto* error = std::get_if<std::string>(&loaded)) {
report_failure(link, "adapter-load", *error);
return exit_startup;
@@ -245,7 +246,8 @@ int run(const Options& options)
int main(int argc, char** argv)
{
const std::optional<Options> options = parse(argc, argv);
const std::vector<std::string> args = sequa::platform::utf8_arguments(argc, argv);
const std::optional<Options> options = parse(args);
if (!options) {
std::fprintf(stderr,
"usage : sequa-render --core <url> --adapter <bibliothèque> [--adapter-options <json>]\n"
+158
View File
@@ -0,0 +1,158 @@
# SPDX-License-Identifier: MPL-2.0
# Messages en ASCII : compatible Windows PowerShell 5.1 sans BOM.
<#
.SYNOPSIS
Build unifie (ADR 0010) : Godot, C++, interface, puis bin\<plateforme>\<type>\.
.DESCRIPTION
Pour chaque type demande :
1. Godot (LibGodot) via build-godot.ps1, seulement si sa bibliotheque manque
(template_debug pour debug, template_release pour release, rien pour asan) ;
2. le preset CMake via build.ps1 (compilation et tests) ;
3. arret des processus SEQUA lances depuis bin\<plateforme>\<type>\ (Studio, Core,
rendu) : Windows refuse de remplacer un fichier en cours d'utilisation ;
4. cmake --install vers bin\<plateforme>\<type>\.
L'interface (studio\ui) est construite une fois, avant le premier type.
Lancer ensuite : bin\windows-x64\<type>\studio\sequa-studio.exe
.PARAMETER Type
debug (defaut), release, asan, ou all (les trois).
.PARAMETER NoTests
Compile sans lancer les tests.
.PARAMETER Godot
Reconstruit Godot meme si sa bibliotheque existe (apres une modification du fork).
.PARAMETER SkipGodot
Ne construit jamais Godot (echoue a la configuration s'il manque).
.PARAMETER SkipUi
Ne construit pas l'interface (installe studio\ui\dist s'il existe).
.PARAMETER NoStop
Echoue au lieu d'arreter les processus SEQUA qui tournent depuis le dossier vise.
.PARAMETER Clean
Supprime build\<preset> et bin\<plateforme>\<type> avant de construire.
.PARAMETER Help
Affiche cette aide.
.EXAMPLE
.\scripts\build-all.ps1
.EXAMPLE
.\scripts\build-all.ps1 -Type all -NoTests
#>
[CmdletBinding()]
param(
[ValidateSet("debug", "release", "asan", "all")]
[string]$Type = "debug",
[switch]$NoTests,
[switch]$Godot,
[switch]$SkipGodot,
[switch]$SkipUi,
[switch]$NoStop,
[switch]$Clean,
[Alias("h", "?")]
[switch]$Help
)
if ($Help) {
Get-Help $PSCommandPath -Detailed
exit 0
}
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$platform = "windows-x64"
$types = if ($Type -eq "all") { @("debug", "release", "asan") } else { @($Type) }
$presets = @{ debug = "windows-msvc-debug"; release = "windows-msvc-release"; asan = "windows-msvc-asan" }
$godotTargets = @{ debug = "template_debug"; release = "template_release"; asan = $null }
function Invoke-Step([string]$label, [scriptblock]$cmd) {
Write-Host "==> $label" -ForegroundColor Cyan
& $cmd
if ($LASTEXITCODE -ne 0) { throw "Echec : $label (code $LASTEXITCODE)" }
}
function Build-Ui {
$ui = Join-Path $root "studio\ui"
Push-Location $ui
try {
if (-not (Test-Path (Join-Path $ui "node_modules"))) {
Invoke-Step "studio\ui : npm ci" { npm ci }
}
Invoke-Step "studio\ui : npm run build" { npm run build }
}
finally {
Pop-Location
}
}
function Build-Godot([string]$target) {
$library = Join-Path $root "engines\godot\vendor\godot-src\bin\godot.windows.$target.x86_64.dll"
if ((Test-Path $library) -and -not $Godot) { return }
if ($SkipGodot) { return }
Write-Host "==> Godot $target (10 a 20 min la premiere fois)" -ForegroundColor Cyan
& (Join-Path $PSScriptRoot "build-godot.ps1") -Target $target
}
# Processus SEQUA lances depuis $dir : executable dans $dir, ou Core (copie fantome)
# dont la ligne de commande designe $dir par --sequa-root.
function Get-SequaProcesses([string]$dir) {
$prefix = [IO.Path]::GetFullPath($dir).TrimEnd('\') + '\'
Get-CimInstance Win32_Process -Filter "Name LIKE 'sequa-%'" | Where-Object {
($_.ExecutablePath -and $_.ExecutablePath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) -or
($_.CommandLine -and $_.CommandLine.IndexOf($prefix.TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase) -ge 0)
}
}
function Stop-SequaProcesses([string]$dir) {
$running = @(Get-SequaProcesses $dir)
if ($running.Count -eq 0) { return }
$names = ($running | ForEach-Object { "$($_.Name) (pid $($_.ProcessId))" }) -join ", "
if ($NoStop) { throw "Processus SEQUA en cours depuis $dir : $names (-NoStop)" }
Write-Host "==> arret : $names" -ForegroundColor Yellow
foreach ($process in $running) {
Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue
}
foreach ($process in $running) {
Wait-Process -Id $process.ProcessId -Timeout 10 -ErrorAction SilentlyContinue
}
}
$start = Get-Date
if (-not $SkipUi) { Build-Ui }
foreach ($t in $types) {
$preset = $presets[$t]
$bin = Join-Path $root "bin\$platform\$t"
$buildDir = Join-Path $root "build\$preset"
if ($godotTargets[$t]) { Build-Godot $godotTargets[$t] }
$buildArgs = @{ Preset = $preset }
if ($NoTests) { $buildArgs.NoTests = $true }
if ($Clean) { $buildArgs.Clean = $true }
& (Join-Path $PSScriptRoot "build.ps1") @buildArgs
Stop-SequaProcesses $bin
if ($Clean -and (Test-Path $bin)) { Remove-Item -Recurse -Force $bin }
# L'interface change de noms de fichiers a chaque build : pas de restes.
$uiBin = Join-Path $bin "studio\ui"
if (Test-Path $uiBin) { Remove-Item -Recurse -Force $uiBin }
# cmake est dans le PATH depuis build.ps1 (CMake de Visual Studio).
Invoke-Step "install $preset -> bin\$platform\$t" { cmake --install $buildDir --prefix $bin }
}
$elapsed = (Get-Date) - $start
Write-Host ("==> termine en {0:N1} min" -f $elapsed.TotalMinutes) -ForegroundColor Green
foreach ($t in $types) {
Write-Host " bin\$platform\$t\studio\sequa-studio.exe"
}
+9 -7
View File
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: MPL-2.0
#
# Verification locale complete avant commit (remplace la CI) :
# tous les presets Windows, puis Studio s'il existe.
# Studio, puis build unifie de tous les types Windows.
# Messages en ASCII : compatible Windows PowerShell 5.1 sans BOM.
#
# .\scripts\check.ps1
@@ -12,8 +12,9 @@
Verification locale complete avant commit (remplace la CI).
.DESCRIPTION
Construit et teste les presets windows-msvc-debug, -release et -asan, puis lance
npm ci et npm run check dans studio/.
Lance npm ci et npm run check dans studio\ui, puis build-all.ps1 -Type all :
Godot, presets windows-msvc-debug, -release et -asan (compilation et tests),
remplissage de bin\windows-x64\<type>\.
.PARAMETER SkipStudio
Ne verifie pas Studio.
@@ -37,10 +38,7 @@ if ($Help) {
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
foreach ($preset in "windows-msvc-debug", "windows-msvc-release", "windows-msvc-asan") {
& (Join-Path $PSScriptRoot "build.ps1") -Preset $preset
}
# Interface d'abord : npm run check construit aussi studio\ui\dist, que build-all installe.
$studio = Join-Path $root "studio\ui"
if (-not $SkipStudio -and (Test-Path (Join-Path $studio "package.json"))) {
Push-Location $studio
@@ -57,4 +55,8 @@ if (-not $SkipStudio -and (Test-Path (Join-Path $studio "package.json"))) {
}
}
# Build unifie (ADR 0010, modification de l'ADR 0004) : debug, release et asan, Godot
# compris, puis bin\windows-x64\<type>\.
& (Join-Path $PSScriptRoot "build-all.ps1") -Type all -SkipUi
Write-Host "Verification locale OK" -ForegroundColor Green
+4 -5
View File
@@ -12,11 +12,10 @@ Face C++ de SEQUA Studio ([README de Studio](../README.md)) : l'exécutable que
## Utilisation
```powershell
.\scripts\build.ps1 # construit Core, host et tests
cd studio\ui; npm run build; cd ..\.. # construit l'interface servie
.\build\windows-msvc-debug\studio\host\app\sequa-studio.exe # Core + interface + navigateur
.\build\windows-msvc-debug\studio\host\app\sequa-studio.exe --dev # avec Vite (scripts\studio.ps1 dans un autre terminal)
.\build\windows-msvc-debug\studio\host\app\sequa-studio.exe --watch # recharge le Core à chaque recompilation
.\scripts\build-all.ps1 # construit tout : Core, host, interface, arborescence unifiée
.\bin\windows-x64\debug\studio\sequa-studio.exe # Core + interface + navigateur
.\bin\windows-x64\debug\studio\sequa-studio.exe --dev # avec Vite (scripts\studio.ps1 dans un autre terminal)
.\bin\windows-x64\debug\studio\sequa-studio.exe --watch # recharge le Core à chaque recompilation
```
Commandes dans la console : `status`, `reload` (nouvel exécutable du Core, session rejouée et comparée tick par tick), `restart`, `detach` / `attach` (le Core survit ou non à Studio, réversible), `log`, `quit`.
+2 -1
View File
@@ -6,13 +6,14 @@ add_library(sequa_host_app STATIC
src/options.cpp
src/studio_host.cpp)
target_include_directories(sequa_host_app PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(sequa_host_app PUBLIC sequa::host_supervisor sequa::host_client sequa::host_web)
target_link_libraries(sequa_host_app PUBLIC sequa::host_supervisor sequa::host_client sequa::host_web sequa::layout)
sequa_target_defaults(sequa_host_app)
add_executable(sequa_studio_app src/main.cpp)
set_target_properties(sequa_studio_app PROPERTIES OUTPUT_NAME sequa-studio)
target_link_libraries(sequa_studio_app PRIVATE sequa_host_app)
sequa_target_defaults(sequa_studio_app)
sequa_install_executable(sequa_studio_app studio)
if(SEQUA_BUILD_TESTS)
add_executable(sequa_host_app_tests
+9 -2
View File
@@ -12,6 +12,8 @@
#include "sequa/studio/host/ui_server.hpp"
#include "sequa/studio/host/urls.hpp"
#include "sequa/platform/layout.hpp"
#include <atomic>
#include <chrono>
#include <csignal>
@@ -127,7 +129,8 @@ int main(int argc, char** argv)
#if defined(_WIN32)
SetConsoleOutputCP(CP_UTF8);
#endif
std::vector<std::string_view> args(argv + 1, argv + argc);
const auto utf8_args = sequa::platform::utf8_arguments(argc, argv);
std::vector<std::string_view> args(utf8_args.begin(), utf8_args.end());
const ParsedOptions parsed = parse_options(args);
if (!parsed.options) {
std::fprintf(stderr, "%s\n%s", parsed.error.c_str(), usage().c_str());
@@ -145,6 +148,7 @@ int main(int argc, char** argv)
std::fprintf(stderr, "Exécutable du Core introuvable : préciser --core-exe.\n");
return 2;
}
const auto sequa_root = find_sequa_root(exe_dir);
const auto run_dir = default_run_dir();
if (!run_dir) {
std::fprintf(stderr, "Dossier d'exécution introuvable (variable d'environnement manquante).\n");
@@ -165,6 +169,9 @@ int main(int argc, char** argv)
config.allowed_origins.push_back(*origin);
}
}
if (sequa_root) {
config.sequa_root = *sequa_root;
}
StudioHost host(config);
if (const std::string error = host.start(); !error.empty()) {
@@ -190,7 +197,7 @@ int main(int argc, char** argv)
} else {
const auto ui_dir = options.ui_dir.empty() ? find_ui_dir(exe_dir) : std::optional(options.ui_dir);
if (!ui_dir) {
say("Interface introuvable (studio/ui/dist) : lancer « npm run build » dans studio/ui, ou utiliser --dev.");
say("Interface introuvable (studio/ui de l'arborescence) : construire avec scripts/build-all.ps1, ou utiliser --dev.");
} else {
UiServerOptions ui_options;
ui_options.port = options.ui_port;
+26 -18
View File
@@ -5,6 +5,8 @@
#include <system_error>
#include <vector>
#include "sequa/platform/layout.hpp"
#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
@@ -47,10 +49,10 @@ bool is_file(const std::filesystem::path& path)
std::string usage()
{
return "Usage : sequa-studio [options]\n"
" --core-exe <chemin> exécutable du Core (défaut : trouvé à côté ou dans build/<preset>/core)\n"
" --core-exe <chemin> exécutable du Core (défaut : core/ de l'arborescence (sequa-root.json))\n"
" --core-port <n> port du Core (défaut 9100)\n"
" --ui-port <n> port de l'interface servie (défaut 9180)\n"
" --ui-dir <chemin> interface construite (défaut : studio/ui/dist)\n"
" --ui-dir <chemin> interface construite (défaut : studio/ui de l'arborescence)\n"
" --dev [url] n'ouvre que le serveur Vite (défaut http://localhost:5173)\n"
" --detach-core le Core survivra à Studio (bail levé)\n"
" --no-browser n'ouvre pas le navigateur\n"
@@ -85,10 +87,10 @@ ParsedOptions parse_options(std::span<const std::string_view> args)
options.dev_url = "http://localhost:5173";
}
} else if (arg == "--core-exe" && has_value) {
options.core_exe = std::filesystem::path(std::u8string(args[i + 1].begin(), args[i + 1].end()));
options.core_exe = sequa::platform::path_from_utf8(args[i + 1]);
++i;
} else if (arg == "--ui-dir" && has_value) {
options.ui_dir = std::filesystem::path(std::u8string(args[i + 1].begin(), args[i + 1].end()));
options.ui_dir = sequa::platform::path_from_utf8(args[i + 1]);
++i;
} else if ((arg == "--core-port" || arg == "--ui-port") && has_value) {
const auto port = parse_port(args[++i]);
@@ -106,29 +108,35 @@ ParsedOptions parse_options(std::span<const std::string_view> args)
return {.options=options, .error={}};
}
std::optional<std::filesystem::path> find_sequa_root(const std::filesystem::path& exe_dir)
{
return sequa::platform::find_root(exe_dir);
}
std::optional<std::filesystem::path> find_core_exe(const std::filesystem::path& exe_dir)
{
if (is_file(exe_dir / core_name)) {
return exe_dir / core_name;
const auto root = find_sequa_root(exe_dir);
if (!root) {
return std::nullopt;
}
for (std::filesystem::path dir = exe_dir; dir.has_parent_path() && dir != dir.parent_path();
dir = dir.parent_path()) {
if (is_file(dir / "core" / core_name)) {
return dir / "core" / core_name;
}
const auto layout = sequa::platform::Layout(*root);
const auto exe = layout.core_executable();
if (is_file(exe)) {
return exe;
}
return std::nullopt;
}
std::optional<std::filesystem::path> find_ui_dir(const std::filesystem::path& exe_dir)
{
for (std::filesystem::path dir = exe_dir; dir.has_parent_path() && dir != dir.parent_path();
dir = dir.parent_path()) {
// const auto candidate = dir / "studio" / "ui" / "dist";
auto candidate = dir / "studio" / "ui" / "dist";
if (is_file(candidate / "index.html")) {
return candidate;
}
const auto root = find_sequa_root(exe_dir);
if (!root) {
return std::nullopt;
}
const auto layout = sequa::platform::Layout(*root);
const auto ui = layout.ui_dir();
if (is_file(ui / "index.html")) {
return ui;
}
return std::nullopt;
}
+5 -3
View File
@@ -32,13 +32,15 @@ struct ParsedOptions {
[[nodiscard]] ParsedOptions parse_options(std::span<const std::string_view> args);
[[nodiscard]] std::string usage();
// Exécutable du Core : à côté de sequa-studio, sinon <ancêtre>/core/sequa-core[.exe]
// en remontant depuis le dossier de sequa-studio (arborescence de build CMake).
// Exécutable du Core : root/core/sequa-core[.exe] où root contient sequa-root.json (ADR 0010).
[[nodiscard]] std::optional<std::filesystem::path> find_core_exe(const std::filesystem::path& exe_dir);
// Interface construite : <ancêtre>/studio/ui/dist contenant index.html.
// Interface construite : root/studio/ui contenant index.html où root contient sequa-root.json (ADR 0010).
[[nodiscard]] std::optional<std::filesystem::path> find_ui_dir(const std::filesystem::path& exe_dir);
// Racine de l'arborescence SEQUA : premier dossier contenant sequa-root.json en remontant depuis exe_dir.
[[nodiscard]] std::optional<std::filesystem::path> find_sequa_root(const std::filesystem::path& exe_dir);
// Origine (schéma://hôte:port) d'une URL http(s), pour --allowed-origin du Core.
[[nodiscard]] std::optional<std::string> origin_of(std::string_view url);
+5
View File
@@ -9,6 +9,8 @@
#include "sequa/studio/host/urls.hpp"
#include "sequa/studio/host/wire.hpp"
#include "sequa/platform/layout.hpp"
#include <thread>
#include <utility>
@@ -134,6 +136,9 @@ std::string StudioHost::launch_core()
std::vector<std::string> args = {"--host", config_.host, "--port", std::to_string(config_.core_port),
"--token", run_.token, "--linked"};
if (!config_.sequa_root.empty()) {
args.insert(args.end(), {"--sequa-root", sequa::platform::path_to_utf8(config_.sequa_root)});
}
for (const std::string& origin : config_.allowed_origins) {
args.insert(args.end(), {"--allowed-origin", origin});
}
+1
View File
@@ -29,6 +29,7 @@ using sequa::platform::LaunchOptions;
struct HostConfig {
std::filesystem::path core_exe; // exécutable SOURCE du Core (jamais lancé directement)
std::filesystem::path run_dir; // dossier d'exécution (§7.4)
std::filesystem::path sequa_root; // racine de l'arborescence (sequa-root.json), passée au Core (--sequa-root), vide : non passée
std::string host = "127.0.0.1";
std::uint16_t core_port = 9100;
std::vector<std::string> allowed_origins; // transmis au Core (--allowed-origin)
+31 -15
View File
@@ -3,6 +3,8 @@
#include <doctest/doctest.h>
#include "sequa/platform/layout.hpp"
#include <filesystem>
#include <fstream>
#include <string_view>
@@ -74,26 +76,40 @@ TEST_CASE("Options : origine d'une URL")
CHECK_FALSE(origin_of("http://").has_value());
}
TEST_CASE("Options : recherche du Core et de l'interface dans une arborescence de build")
TEST_CASE("Options : recherche du Core et de l'interface dans une arborescence avec sequa-root.json")
{
const auto root = std::filesystem::temp_directory_path() / "sequa-app-options-test";
std::filesystem::remove_all(root);
#if defined(_WIN32)
const char* core_name = "sequa-core.exe";
#else
const char* core_name = "sequa-core";
#endif
const auto exe_dir = root / "build" / "preset" / "studio" / "host" / "app";
const auto core_name = sequa::platform::executable_name("sequa-core");
const auto exe_dir = root / "studio" / "host" / "app";
std::filesystem::create_directories(exe_dir);
std::filesystem::create_directories(root / "build" / "preset" / "core");
std::filesystem::create_directories(root / "studio" / "ui" / "dist");
std::ofstream(root / "build" / "preset" / "core" / core_name) << "x";
std::ofstream(root / "studio" / "ui" / "dist" / "index.html") << "<html>";
std::filesystem::create_directories(root / "core");
std::filesystem::create_directories(root / "studio" / "ui");
std::ofstream(root / "sequa-root.json") << "{}";
std::ofstream(root / "core" / core_name) << "x";
std::ofstream(root / "studio" / "ui" / "index.html") << "<html>";
CHECK(find_core_exe(exe_dir) == root / "build" / "preset" / "core" / core_name);
CHECK(find_ui_dir(exe_dir) == root / "studio" / "ui" / "dist");
CHECK(find_sequa_root(exe_dir) == root);
CHECK(find_core_exe(exe_dir) == root / "core" / core_name);
CHECK(find_ui_dir(exe_dir) == root / "studio" / "ui");
std::filesystem::remove_all(root);
}
TEST_CASE("Options : sans sequa-root.json retourne nullopt")
{
const auto root = std::filesystem::temp_directory_path() / "sequa-app-options-test-no-root";
std::filesystem::remove_all(root);
const auto exe_dir = root / "studio" / "host" / "app";
std::filesystem::create_directories(exe_dir);
std::filesystem::create_directories(root / "core");
std::filesystem::create_directories(root / "studio" / "ui");
std::ofstream(root / "core" / sequa::platform::executable_name("sequa-core")) << "x";
std::ofstream(root / "studio" / "ui" / "index.html") << "<html>";
CHECK_FALSE(find_sequa_root(exe_dir).has_value());
CHECK_FALSE(find_core_exe(exe_dir).has_value());
CHECK_FALSE(find_ui_dir(exe_dir).has_value());
std::ofstream(exe_dir / core_name) << "x"; // à côté : prioritaire (distribution)
CHECK(find_core_exe(exe_dir) == exe_dir / core_name);
std::filesystem::remove_all(root);
}