diff --git a/.gitignore b/.gitignore index facb425..dbeebe5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Build build/ +/bin/ +/installers/ out/ cmake-build-*/ CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e87356..b7c229e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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/// (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() diff --git a/CMakePresets.json b/CMakePresets.json index 6d57998..1e8f83a 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -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" } }, diff --git a/README.md b/README.md index 69c0a10..81a78ee 100644 --- a/README.md +++ b/README.md @@ -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 ] [-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 ] [-Clean] [-NoTests]` | idem | | Windows, avant commit | `.\scripts\check.ps1` | idem + Node.js pour Studio | | Linux / macOS | `./scripts/build.sh []` | 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 ] [-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) | diff --git a/cmake/SequaInstall.cmake b/cmake/SequaInstall.cmake new file mode 100644 index 0000000..8c9645f --- /dev/null +++ b/cmake/SequaInstall.cmake @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# Arborescence installée (ADR 0010) : bin/// en développement, même +# contenu dans le dossier d'installation. `cmake --install ` 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//") + +# 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//") + +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 / 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//. +function(sequa_install_adapter target engine) + install(TARGETS ${target} LIBRARY DESTINATION engines/${engine} RUNTIME DESTINATION engines/${engine}) +endfunction() diff --git a/cmake/sequa-root.json.in b/cmake/sequa-root.json.in new file mode 100644 index 0000000..a4a6292 --- /dev/null +++ b/cmake/sequa-root.json.in @@ -0,0 +1,6 @@ +{ + "layout": 1, + "sequaVersion": "@PROJECT_VERSION@", + "platform": "@SEQUA_PLATFORM@", + "type": "@SEQUA_BIN_TYPE@" +} diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index aad418f..b25de70 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -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 ------------------------------------------------------------------------------ diff --git a/core/README.md b/core/README.md index 64d3f86..ec9cf6b 100644 --- a/core/README.md +++ b/core/README.md @@ -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 ` lance `sequa-render` depuis l'arborescence `bin///` (repérée via `sequa-root.json`) ; option `--sequa-root ` 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 --linked +.\bin\windows-x64\debug\core\sequa-core.exe --help +.\bin\windows-x64\debug\core\sequa-core.exe --port 9100 --token --linked ``` En usage normal, c'est Studio host (`sequa-studio`) qui lance et supervise le Core. diff --git a/core/app/main.cpp b/core/app/main.cpp index d3cf4ed..d9fcc95 100644 --- a/core/app/main.cpp +++ b/core/app/main.cpp @@ -8,6 +8,8 @@ #include "sequa/core/server/server.hpp" #include "sequa/core/version.hpp" +#include "sequa/platform/layout.hpp" + #include #include #include @@ -21,6 +23,7 @@ #include #include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -59,6 +62,7 @@ void print_usage() " --render-fps cadence imposée au rendu (debug : 10 conseillé)\n" " --render-options réglages propres à l'adaptateur\n" " --render-log journal du rendu (défaut : sortie du Core)\n" + " --sequa-root 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 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(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 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(argv[++i]); + const auto port = parse_number(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(argv[++i]); + const auto grace = parse_number(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(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(argv[++i]); + const auto fps = parse_number(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()) { diff --git a/core/include/sequa/core/server/runtime.hpp b/core/include/sequa/core/server/runtime.hpp index 540b16f..d8a9cbd 100644 --- a/core/include/sequa/core/server/runtime.hpp +++ b/core/include/sequa/core/server/runtime.hpp @@ -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 log; // fonction de journalisation diff --git a/core/src/server/render_supervisor.cpp b/core/src/server/render_supervisor.cpp index 18795eb..e1f7fc7 100644 --- a/core/src/server/render_supervisor.cpp +++ b/core/src/server/render_supervisor.cpp @@ -3,9 +3,10 @@ #include "render_hub.hpp" +#include "sequa/platform/layout.hpp" + #include #include -#include 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 first_existing(const std::vector& candidates) +std::optional 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 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 find_adapter_library(const std::filesystem::path& core_exe, +std::optional 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 log) @@ -78,7 +71,7 @@ RenderSupervisor::~RenderSupervisor() bool RenderSupervisor::launch() { std::vector 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; } diff --git a/core/src/server/render_supervisor.hpp b/core/src/server/render_supervisor.hpp index 5681951..e4ed930 100644 --- a/core/src/server/render_supervisor.hpp +++ b/core/src/server/render_supervisor.hpp @@ -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//…). -[[nodiscard]] std::optional find_render_executable(const std::filesystem::path& core_exe); -[[nodiscard]] std::optional 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 find_render_executable(const std::filesystem::path& sequa_root); +[[nodiscard]] std::optional find_adapter_library(const std::filesystem::path& sequa_root, std::string_view adapter); } // namespace sequa::core::server diff --git a/core/src/server/runtime.cpp b/core/src/server/runtime.cpp index ae1d35a..987ac93 100644 --- a/core/src/server/runtime.cpp +++ b/core/src/server/runtime.cpp @@ -252,7 +252,7 @@ std::optional CoreRuntime::session_import(std::span// 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 : "; diff --git a/core/tests/server/test_runtime.cpp b/core/tests/server/test_runtime.cpp index d2f9903..b4fbd3a 100644 --- a/core/tests/server/test_runtime.cpp +++ b/core/tests/server/test_runtime.cpp @@ -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; diff --git a/docs/adr/0010-arborescence-des-binaires-et-build-unifie.md b/docs/adr/0010-arborescence-des-binaires-et-build-unifie.md index 1b40377..0335b2c 100644 --- a/docs/adr/0010-arborescence-des-binaires-et-build-unifie.md +++ b/docs/adr/0010-arborescence-des-binaires-et-build-unifie.md @@ -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//`. - Racine : `--sequa-root ` 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//…` et `studio/ui/dist` est retiré : on lance depuis `bin/`. ### Build unifié diff --git a/docs/installation/README.md b/docs/installation/README.md new file mode 100644 index 0000000..8e89105 --- /dev/null +++ b/docs/installation/README.md @@ -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/ +└── / # windows-x64, linux-x64, macos-arm64, ... + └── / # 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/ +``` + +- `` = système et processeur (ex. `windows-x64`). +- `` = preset de construction : `debug` (template_debug Godot), `release` (template_release Godot), `asan` (sans Godot, adaptateur debug seulement). +- Le contenu de `bin///` 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 `. + +## 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. \ No newline at end of file diff --git a/docs/installation/linux-x64.md b/docs/installation/linux-x64.md new file mode 100644 index 0000000..99b2412 --- /dev/null +++ b/docs/installation/linux-x64.md @@ -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/` 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 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 +``` \ No newline at end of file diff --git a/docs/installation/macos-arm64.md b/docs/installation/macos-arm64.md new file mode 100644 index 0000000..75aacab --- /dev/null +++ b/docs/installation/macos-arm64.md @@ -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/` 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 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 +``` \ No newline at end of file diff --git a/docs/installation/windows-x64.md b/docs/installation/windows-x64.md new file mode 100644 index 0000000..4d66cd8 --- /dev/null +++ b/docs/installation/windows-x64.md @@ -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\` et `bin\windows-x64\` 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\\` (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\\` 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 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\\` (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. \ No newline at end of file diff --git a/docs/specs/protocole-studio-core-0.2.md b/docs/specs/protocole-studio-core-0.2.md index 3e805cd..778f9ca 100644 --- a/docs/specs/protocole-studio-core-0.2.md +++ b/docs/specs/protocole-studio-core-0.2.md @@ -87,10 +87,10 @@ Le Core n'envoie aucun message spontané en 0.2. | Option | Effet | |---|---| -| `--core-exe ` | Exécutable source du Core (défaut : `sequa-core[.exe]` à côté de `sequa-studio`, sinon dans `build//core/`) | +| `--core-exe ` | Exécutable source du Core (défaut : `core/sequa-core[.exe]` depuis la racine `sequa-root.json`) | | `--core-port ` | 9100 | | `--ui-port ` | 9180 | -| `--ui-dir ` | Dossier de l'interface construite (défaut : `studio/ui/dist` trouvé en remontant depuis l'exécutable) | +| `--ui-dir ` | 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 | diff --git a/docs/tutoriels/01-panneau-studio-de-bout-en-bout.md b/docs/tutoriels/01-panneau-studio-de-bout-en-bout.md index dd77a3f..82abbeb 100644 --- a/docs/tutoriels/01-panneau-studio-de-bout-en-bout.md +++ b/docs/tutoriels/01-panneau-studio-de-bout-en-bout.md @@ -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 diff --git a/engines/debug/adapter/CMakeLists.txt b/engines/debug/adapter/CMakeLists.txt index 4693cb5..02dbe3a 100644 --- a/engines/debug/adapter/CMakeLists.txt +++ b/engines/debug/adapter/CMakeLists.txt @@ -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) diff --git a/engines/godot/adapter/CMakeLists.txt b/engines/godot/adapter/CMakeLists.txt index 59a7207..6e0c274 100644 --- a/engines/godot/adapter/CMakeLists.txt +++ b/engines/godot/adapter/CMakeLists.txt @@ -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) diff --git a/engines/godot/adapter/README.md b/engines/godot/adapter/README.md index 33ba9a9..2fa76c8 100644 --- a/engines/godot/adapter/README.md +++ b/engines/godot/adapter/README.md @@ -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. diff --git a/engines/godot/adapter/demo/main.cpp b/engines/godot/adapter/demo/main.cpp index aac4b54..947549e 100644 --- a/engines/godot/adapter/demo/main.cpp +++ b/engines/godot/adapter/demo/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -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)) { diff --git a/engines/godot/adapter/include/sequa/godot/godot_adapter.hpp b/engines/godot/adapter/include/sequa/godot/godot_adapter.hpp index 6fec9ef..2b180e8 100644 --- a/engines/godot/adapter/include/sequa/godot/godot_adapter.hpp +++ b/engines/godot/adapter/include/sequa/godot/godot_adapter.hpp @@ -3,10 +3,17 @@ #include "sequa/3d/adapter.hpp" +#include #include 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_; bool running_ = false; diff --git a/engines/godot/adapter/src/entry_point.cpp b/engines/godot/adapter/src/entry_point.cpp index 5d7d9dc..a00492d 100644 --- a/engines/godot/adapter/src/entry_point.cpp +++ b/engines/godot/adapter/src/entry_point.cpp @@ -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": "", "project": "" } +// 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 #include +#include #include +#include 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(&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()); + } 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()); + } 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) diff --git a/engines/godot/adapter/src/godot_adapter.cpp b/engines/godot/adapter/src/godot_adapter.cpp index 9175794..d19838f 100644 --- a/engines/godot/adapter/src/godot_adapter.cpp +++ b/engines/godot/adapter/src/godot_adapter.cpp @@ -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 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(&outcome)) { return std::move(*error); } diff --git a/engines/godot/adapter/src/godot_host.cpp b/engines/godot/adapter/src/godot_host.cpp index cf5197b..104668c 100644 --- a/engines/godot/adapter/src/godot_host.cpp +++ b/engines/godot/adapter/src/godot_host.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 #include "godot_host.hpp" +#include "sequa/platform/layout.hpp" + #include #include #include @@ -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(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 args = {"sequa", "--path", - SEQUA_GODOT_DEFAULT_PROJECT, + project_utf8, "--rendering-driver", "vulkan", "--resolution", diff --git a/engines/godot/adapter/src/godot_host.hpp b/engines/godot/adapter/src/godot_host.hpp index e724dee..8688931 100644 --- a/engines/godot/adapter/src/godot_host.hpp +++ b/engines/godot/adapter/src/godot_host.hpp @@ -2,6 +2,7 @@ #pragma once #include "sequa/3d/adapter.hpp" +#include "sequa/godot/godot_adapter.hpp" #include #include @@ -20,7 +21,7 @@ namespace sequa::godot { class GodotHost { public: using StartOutcome = std::variant, 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; diff --git a/engines/godot/adapter/tests/test_godot_adapter.cpp b/engines/godot/adapter/tests/test_godot_adapter.cpp index 428f6ab..a9feccc 100644 --- a/engines/godot/adapter/tests/test_godot_adapter.cpp +++ b/engines/godot/adapter/tests/test_godot_adapter.cpp @@ -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()); +} diff --git a/platform/layout/CMakeLists.txt b/platform/layout/CMakeLists.txt new file mode 100644 index 0000000..fa36236 --- /dev/null +++ b/platform/layout/CMakeLists.txt @@ -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//), 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() \ No newline at end of file diff --git a/platform/layout/README.md b/platform/layout/README.md new file mode 100644 index 0000000..0ee02f3 --- /dev/null +++ b/platform/layout/README.md @@ -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 +/sequa-root.json +/core/sequa-core(.exe) +/render/sequa-render(.exe) +/studio/sequa-studio(.exe) +/studio/ui/ (interface web, index.html) +/engines//sequa__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 ` 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//` 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) \ No newline at end of file diff --git a/platform/layout/include/sequa/platform/layout.hpp b/platform/layout/include/sequa/platform/layout.hpp new file mode 100644 index 0000000..dc3ada7 --- /dev/null +++ b/platform/layout/include/sequa/platform/layout.hpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MPL-2.0 +#pragma once + +#include +#include +#include +#include +#include + +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 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 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/ + + // root/engines//sequa__adapter + suffixe module ; + // nullopt si nom invalide. + [[nodiscard]] std::optional adapter_library(std::string_view engine) const; + +private: + std::filesystem::path root_; +}; + +} // namespace sequa::platform \ No newline at end of file diff --git a/platform/layout/src/layout.cpp b/platform/layout/src/layout.cpp new file mode 100644 index 0000000..8c27a86 --- /dev/null +++ b/platform/layout/src/layout.cpp @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: MPL-2.0 + +#include "sequa/platform/layout.hpp" + +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# include +#else +# include +# include +# if defined(__APPLE__) +# include +# 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(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(address), + &module); + if (!ok || module == nullptr) { + return {}; + } + + std::wstring buffer; + buffer.resize(256); + while (true) { + const DWORD size = static_cast(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 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 utf8_arguments(int argc, char** argv) +{ +#if defined(_WIN32) + (void)argc; + (void)argv; + std::vector 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(wide)); + return result; +#else + std::vector 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 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 \ No newline at end of file diff --git a/platform/layout/tests/main.cpp b/platform/layout/tests/main.cpp new file mode 100644 index 0000000..fd6452a --- /dev/null +++ b/platform/layout/tests/main.cpp @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: MPL-2.0 +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include \ No newline at end of file diff --git a/platform/layout/tests/test_layout.cpp b/platform/layout/tests/test_layout.cpp new file mode 100644 index 0000000..5605fb8 --- /dev/null +++ b/platform/layout/tests/test_layout.cpp @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MPL-2.0 + +#include "sequa/platform/layout.hpp" + +#include + +#include +#include +#include +#include +#include + +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(&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 \ No newline at end of file diff --git a/render/CMakeLists.txt b/render/CMakeLists.txt index 4bc3943..5db0cf1 100644 --- a/render/CMakeLists.txt +++ b/render/CMakeLists.txt @@ -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) diff --git a/render/README.md b/render/README.md index 22eb1af..a615c21 100644 --- a/render/README.md +++ b/render/README.md @@ -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\"}' ``` diff --git a/render/src/adapter_library.cpp b/render/src/adapter_library.cpp index e6e39d0..d8eeae8 100644 --- a/render/src/adapter_library.cpp +++ b/render/src/adapter_library.cpp @@ -2,6 +2,7 @@ #include "adapter_library.hpp" #include "sequa/3d/adapter_library.hpp" +#include "sequa/platform/layout.hpp" #include @@ -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(find_symbol(library, three_d::create_adapter_symbol)); auto* destroy = reinterpret_cast(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 error{}; diff --git a/render/src/main.cpp b/render/src/main.cpp index 2c8f46d..6d179ee 100644 --- a/render/src/main.cpp +++ b/render/src/main.cpp @@ -8,6 +8,7 @@ #include "core_link.hpp" #include "sequa/3d/wire.hpp" +#include "sequa/platform/layout.hpp" #include #include @@ -47,12 +48,12 @@ std::optional parse_uint(const char* text) return static_cast(value); } -std::optional parse(int argc, char** argv) +std::optional parse(const std::vector& 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 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(&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 = parse(argc, argv); + const std::vector args = sequa::platform::utf8_arguments(argc, argv); + const std::optional options = parse(args); if (!options) { std::fprintf(stderr, "usage : sequa-render --core --adapter [--adapter-options ]\n" diff --git a/scripts/build-all.ps1 b/scripts/build-all.ps1 new file mode 100644 index 0000000..9e57c4d --- /dev/null +++ b/scripts/build-all.ps1 @@ -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\\\. + +.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\\\ (Studio, Core, + rendu) : Windows refuse de remplacer un fichier en cours d'utilisation ; + 4. cmake --install vers bin\\\. + L'interface (studio\ui) est construite une fois, avant le premier type. + + Lancer ensuite : bin\windows-x64\\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\ et bin\\ 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" +} diff --git a/scripts/check.ps1 b/scripts/check.ps1 index 6860e6b..b449ada 100644 --- a/scripts/check.ps1 +++ b/scripts/check.ps1 @@ -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\\. .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\\. +& (Join-Path $PSScriptRoot "build-all.ps1") -Type all -SkipUi + Write-Host "Verification locale OK" -ForegroundColor Green diff --git a/studio/host/README.md b/studio/host/README.md index 425dec7..5fc3fac 100644 --- a/studio/host/README.md +++ b/studio/host/README.md @@ -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`. diff --git a/studio/host/app/CMakeLists.txt b/studio/host/app/CMakeLists.txt index f3f2dac..4a097ae 100644 --- a/studio/host/app/CMakeLists.txt +++ b/studio/host/app/CMakeLists.txt @@ -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 diff --git a/studio/host/app/src/main.cpp b/studio/host/app/src/main.cpp index 01b3c8b..423d13b 100644 --- a/studio/host/app/src/main.cpp +++ b/studio/host/app/src/main.cpp @@ -12,6 +12,8 @@ #include "sequa/studio/host/ui_server.hpp" #include "sequa/studio/host/urls.hpp" +#include "sequa/platform/layout.hpp" + #include #include #include @@ -127,7 +129,8 @@ int main(int argc, char** argv) #if defined(_WIN32) SetConsoleOutputCP(CP_UTF8); #endif - std::vector args(argv + 1, argv + argc); + const auto utf8_args = sequa::platform::utf8_arguments(argc, argv); + std::vector 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; diff --git a/studio/host/app/src/options.cpp b/studio/host/app/src/options.cpp index b59ff4d..183700e 100644 --- a/studio/host/app/src/options.cpp +++ b/studio/host/app/src/options.cpp @@ -5,6 +5,8 @@ #include #include +#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 exécutable du Core (défaut : trouvé à côté ou dans build//core)\n" + " --core-exe exécutable du Core (défaut : core/ de l'arborescence (sequa-root.json))\n" " --core-port port du Core (défaut 9100)\n" " --ui-port port de l'interface servie (défaut 9180)\n" - " --ui-dir interface construite (défaut : studio/ui/dist)\n" + " --ui-dir 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 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 args) return {.options=options, .error={}}; } +std::optional find_sequa_root(const std::filesystem::path& exe_dir) +{ + return sequa::platform::find_root(exe_dir); +} + std::optional 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 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; } diff --git a/studio/host/app/src/options.hpp b/studio/host/app/src/options.hpp index b8c5d97..de2e506 100644 --- a/studio/host/app/src/options.hpp +++ b/studio/host/app/src/options.hpp @@ -32,13 +32,15 @@ struct ParsedOptions { [[nodiscard]] ParsedOptions parse_options(std::span args); [[nodiscard]] std::string usage(); -// Exécutable du Core : à côté de sequa-studio, sinon /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 find_core_exe(const std::filesystem::path& exe_dir); -// Interface construite : /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 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 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 origin_of(std::string_view url); diff --git a/studio/host/app/src/studio_host.cpp b/studio/host/app/src/studio_host.cpp index fe664ab..c5f463e 100644 --- a/studio/host/app/src/studio_host.cpp +++ b/studio/host/app/src/studio_host.cpp @@ -9,6 +9,8 @@ #include "sequa/studio/host/urls.hpp" #include "sequa/studio/host/wire.hpp" +#include "sequa/platform/layout.hpp" + #include #include @@ -134,6 +136,9 @@ std::string StudioHost::launch_core() std::vector 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}); } diff --git a/studio/host/app/src/studio_host.hpp b/studio/host/app/src/studio_host.hpp index 02b9f33..65e14ca 100644 --- a/studio/host/app/src/studio_host.hpp +++ b/studio/host/app/src/studio_host.hpp @@ -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 allowed_origins; // transmis au Core (--allowed-origin) diff --git a/studio/host/app/tests/test_options.cpp b/studio/host/app/tests/test_options.cpp index f9c5326..f98ab16 100644 --- a/studio/host/app/tests/test_options.cpp +++ b/studio/host/app/tests/test_options.cpp @@ -3,6 +3,8 @@ #include +#include "sequa/platform/layout.hpp" + #include #include #include @@ -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") << ""; + 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") << ""; - 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") << ""; + + 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); }