# Module Versioning System ## Vue d'Ensemble Le système de versioning des modules utilise un format **MAJOR.MINOR.PATCH.BUILD** automatiquement généré par CMake lors de la compilation. Ce système permet de : - **Tracer les versions** : Identifier précisément quelle version du code a créé une save - **Détecter incompatibilités** : Alerter si une save utilise une version différente - **Debugging** : Reproduire bugs avec la version exacte du module - **Développement** : Versions incrémentales automatiques sans maintenance manuelle ## Format de Version ### Structure : MAJOR.MINOR.PATCH.BUILD ``` 0.1.15.3847 │ │ │ │ │ │ │ └─── BUILD : Auto-increment à chaque build (hash sources) │ │ └────── PATCH : Auto-increment à chaque commit │ └──────── MINOR : Manuel (fonctionnalités nouvelles) └────────── MAJOR : Manuel (breaking changes) ``` ### Composantes #### MAJOR (Manuel) - **Incrémenté** : Breaking changes, incompatibilités majeures - **Exemples** : Refonte complète architecture, changement format save incompatible - **Gestion** : Modifié manuellement dans fichier `VERSION` - **Valeur initiale** : `0` (pré-release), `1` (première version stable) #### MINOR (Manuel) - **Incrémenté** : Nouvelles fonctionnalités, ajouts non-breaking - **Exemples** : Nouveau type de tank, nouveau système économique - **Gestion** : Modifié manuellement dans fichier `VERSION` - **Valeur initiale** : `1` #### PATCH (Automatique) - **Incrémenté** : À chaque commit Git - **Calcul** : Nombre de commits depuis le début du projet - **Exemples** : Bugfixes, optimisations, refactoring mineur - **Gestion** : Calculé automatiquement par CMake via `git rev-list --count HEAD` #### BUILD (Automatique) - **Incrémenté** : À chaque rebuild (si sources modifiées) - **Calcul** : Hash MD5 des fichiers sources (`.cpp`, `.h`) - **But** : Distinguer builds de dev avec code non-commité - **Gestion** : Calculé automatiquement par CMake ### Exemples de Versions ``` 0.1.0.0 → Première version dev (MAJOR.MINOR manuel, PATCH/BUILD = 0) 0.1.5.1234 → Après 5 commits, build 1234 0.1.5.1235 → Même commit, sources modifiées → nouveau build 0.1.6.1235 → Nouveau commit, sources identiques → même build (théorique) 0.2.10.3847 → MINOR bump (nouvelle feature) + 10 commits 1.0.0.0 → Release stable, reset PATCH/BUILD ``` ## Implémentation CMake ### Fichier VERSION Chaque module contient un fichier `VERSION` à la racine : ``` modules/tank/VERSION ``` Contenu du fichier : ``` 0.1 ``` Ce fichier contient uniquement `MAJOR.MINOR`. Les composantes `PATCH.BUILD` sont calculées automatiquement. ### CMakeLists.txt du Module ```cmake cmake_minimum_required(VERSION 3.20) project(TankModule) # ============================================================================ # MODULE VERSIONING # ============================================================================ # 1. Lire MAJOR.MINOR depuis fichier VERSION if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION") file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" BASE_VERSION) string(STRIP "${BASE_VERSION}" BASE_VERSION) else() set(BASE_VERSION "0.1") message(WARNING "No VERSION file found, using default: ${BASE_VERSION}") endif() # 2. Calculer PATCH : nombre de commits Git execute_process( COMMAND git rev-list --count HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE PATCH_COUNT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET RESULT_VARIABLE GIT_RESULT ) if(NOT GIT_RESULT EQUAL 0) set(PATCH_COUNT "0") message(WARNING "Git not available, PATCH set to 0") endif() # 3. Calculer BUILD : hash des fichiers sources file(GLOB_RECURSE MODULE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h" ) # Concaténer contenu de tous les fichiers set(SOURCES_CONTENT "") foreach(SOURCE_FILE ${MODULE_SOURCES}) file(READ ${SOURCE_FILE} FILE_CONTENT) string(APPEND SOURCES_CONTENT ${FILE_CONTENT}) endforeach() # Hash MD5 du contenu total string(MD5 SOURCES_HASH "${SOURCES_CONTENT}") # Convertir hash hex en nombre décimal (4 premiers caractères) string(SUBSTRING ${SOURCES_HASH} 0 4 BUILD_HEX) # Calculer valeur numérique (modulo 10000 pour limiter à 4 chiffres) math(EXPR BUILD_NUM "0x${BUILD_HEX} % 10000") # 4. Assembler version complète set(MODULE_VERSION "${BASE_VERSION}.${PATCH_COUNT}.${BUILD_NUM}") message(STATUS "Module version: ${MODULE_VERSION}") message(STATUS " BASE: ${BASE_VERSION} (from VERSION file)") message(STATUS " PATCH: ${PATCH_COUNT} (git commits)") message(STATUS " BUILD: ${BUILD_NUM} (sources hash)") # 5. Définir macro C++ pour accès au runtime add_definitions(-DMODULE_VERSION="${MODULE_VERSION}") # ============================================================================ # MODULE BUILD # ============================================================================ # Inclure sources file(GLOB_RECURSE TANK_SOURCES src/*.cpp) # Créer bibliothèque partagée (.so) add_library(tank-module SHARED ${TANK_SOURCES}) # Configuration compilation target_include_directories(tank-module PRIVATE include) target_compile_features(tank-module PRIVATE cxx_std_20) # Sortie set_target_properties(tank-module PROPERTIES OUTPUT_NAME "tank" PREFIX "" SUFFIX ".so" ) message(STATUS "Building TankModule v${MODULE_VERSION}") ``` ### Utilisation dans le Code C++ ```cpp // src/TankModule.h #ifndef MODULE_VERSION #define MODULE_VERSION "unknown" #endif class TankModule : public IModule, public IModuleSave { public: std::string getVersion() const override { return MODULE_VERSION; // Défini par CMake } nlohmann::json serialize() override { json data; data["module_name"] = "tank"; data["module_version"] = getVersion(); // "0.1.15.3847" // ... reste de la sérialisation return data; } }; ``` ### Affichage au Runtime ```cpp void TankModule::initialize() { LOG_INFO("TankModule v{} initializing...", getVersion()); } ``` Sortie console : ``` [INFO] TankModule v0.1.15.3847 initializing... ``` ## Workflow de Développement ### 1. Création Nouveau Module ```bash # Créer structure module mkdir -p modules/new_module/src cd modules/new_module # Créer fichier VERSION echo "0.1" > VERSION # Créer CMakeLists.txt avec système versioning # (copier template ci-dessus) ``` **Première compilation** : ```bash cmake . && make ``` Sortie : ``` -- Module version: 0.1.0.0 -- BASE: 0.1 (from VERSION file) -- PATCH: 0 (git commits) -- BUILD: 0 (sources hash) -- Building NewModule v0.1.0.0 ``` ### 2. Développement Itératif **Scénario 1 : Modifier code sans commit** ```bash # Éditer src/NewModule.cpp nano src/NewModule.cpp # Rebuild make ``` Sortie : ``` -- Module version: 0.1.0.4582 -- PATCH: 0 (aucun commit) -- BUILD: 4582 (hash sources a changé) ``` **Scénario 2 : Commit modifications** ```bash git add . git commit -m "Add feature X" # Rebuild make ``` Sortie : ``` -- Module version: 0.1.1.4582 -- PATCH: 1 (nouveau commit) -- BUILD: 4582 (sources identiques au commit) ``` **Scénario 3 : Rebuild sans modification** ```bash # Rebuild immédiat make ``` Sortie : ``` -- Module version: 0.1.1.4582 (version identique, aucun changement) ``` ### 3. Release Mineure (Nouvelle Feature) ```bash # Bump MINOR version echo "0.2" > VERSION # Commit git add VERSION git commit -m "Bump version to 0.2 (new feature: advanced tactics)" # Rebuild cmake . && make ``` Sortie : ``` -- Module version: 0.2.2.5123 -- BASE: 0.2 (nouvelle version) -- PATCH: 2 (commits depuis début projet) -- BUILD: 5123 (hash actuel) ``` ### 4. Release Majeure (Breaking Change) ```bash # Bump MAJOR version echo "1.0" > VERSION git add VERSION git commit -m "Release 1.0.0 - Stable API" cmake . && make ``` Sortie : ``` -- Module version: 1.0.3.5123 -- BASE: 1.0 (version stable) -- PATCH: 3 (total commits) -- BUILD: 5123 ``` ## Comparaison de Versions ### Dans le Système de Save Lors du chargement d'une save, comparaison des versions : ```cpp bool SaveSystem::loadModule(const std::string& moduleName) { auto module = moduleSystem->getModule(moduleName); json moduleData = readJsonFile(modulePath); std::string savedVersion = moduleData["module_version"]; std::string currentVersion = module->getVersion(); if (savedVersion != currentVersion) { LOG_WARN("Module {} version mismatch:", moduleName); LOG_WARN(" Save version: {}", savedVersion); LOG_WARN(" Current version: {}", currentVersion); // Analyser différences VersionInfo saved = parseVersion(savedVersion); VersionInfo current = parseVersion(currentVersion); if (saved.major != current.major) { LOG_ERROR(" MAJOR version mismatch - likely incompatible!"); return false; // Incompatibilité critique } else if (saved.minor != current.minor) { LOG_WARN(" MINOR version mismatch - some features may differ"); // Continuer avec warning } else if (saved.patch != current.patch) { LOG_INFO(" PATCH version mismatch - minor changes only"); // Sûr, juste bugfixes } else { LOG_DEBUG(" BUILD version mismatch - dev builds differ"); // Totalement sûr } } return module->deserialize(moduleData); } ``` ### Structure VersionInfo ```cpp struct VersionInfo { int major; int minor; int patch; int build; static VersionInfo parse(const std::string& versionStr) { VersionInfo info; std::sscanf(versionStr.c_str(), "%d.%d.%d.%d", &info.major, &info.minor, &info.patch, &info.build); return info; } bool isCompatibleWith(const VersionInfo& other) const { // Compatible si MAJOR identique return major == other.major; } bool hasBreakingChanges(const VersionInfo& other) const { return major != other.major; } }; ``` ## Gestion Multi-Modules ### Script CMake Global Pour centraliser la logique de versioning, créer un fichier CMake réutilisable : ```cmake # cmake/ModuleVersioning.cmake function(setup_module_versioning) # Lire VERSION if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION") file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" BASE_VERSION) string(STRIP "${BASE_VERSION}" BASE_VERSION) else() set(BASE_VERSION "0.1") endif() # Calculer PATCH execute_process( COMMAND git rev-list --count HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE PATCH_COUNT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET RESULT_VARIABLE GIT_RESULT ) if(NOT GIT_RESULT EQUAL 0) set(PATCH_COUNT "0") endif() # Calculer BUILD file(GLOB_RECURSE MODULE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h" ) set(SOURCES_CONTENT "") foreach(SOURCE_FILE ${MODULE_SOURCES}) file(READ ${SOURCE_FILE} FILE_CONTENT) string(APPEND SOURCES_CONTENT ${FILE_CONTENT}) endforeach() string(MD5 SOURCES_HASH "${SOURCES_CONTENT}") string(SUBSTRING ${SOURCES_HASH} 0 4 BUILD_HEX) math(EXPR BUILD_NUM "0x${BUILD_HEX} % 10000") # Assembler version set(MODULE_VERSION "${BASE_VERSION}.${PATCH_COUNT}.${BUILD_NUM}" PARENT_SCOPE) # Log message(STATUS "Module version: ${BASE_VERSION}.${PATCH_COUNT}.${BUILD_NUM}") endfunction() ``` ### Utilisation dans Module ```cmake # modules/tank/CMakeLists.txt cmake_minimum_required(VERSION 3.20) project(TankModule) # Importer fonction versioning include(${CMAKE_SOURCE_DIR}/cmake/ModuleVersioning.cmake) # Setup version automatique setup_module_versioning() # Définir macro add_definitions(-DMODULE_VERSION="${MODULE_VERSION}") # ... reste du CMakeLists ``` ## Best Practices ### 1. Quand Bumper MAJOR ? **Situations justifiant MAJOR bump** : - Changement format save incompatible - Refonte API du module (signatures fonctions changent) - Suppression de fonctionnalités existantes - Changement architecture fondamentale **Exemple** : ```bash # Avant : TankModule v0.8.x utilise grille 10x10 # Après : TankModule v1.0.0 utilise grille 16x16 (incompatible) echo "1.0" > VERSION ``` ### 2. Quand Bumper MINOR ? **Situations justifiant MINOR bump** : - Ajout nouveau type de tank/composant - Nouvelle fonctionnalité (mode formation, tactiques AI) - Amélioration significative sans breaking change **Exemple** : ```bash # Ajout système de camouflage → 0.1 → 0.2 echo "0.2" > VERSION ``` ### 3. PATCH et BUILD sont Automatiques **Ne jamais modifier manuellement** : - PATCH suit automatiquement les commits - BUILD suit automatiquement les modifications code ### 4. Versioning pour Tests En environnement de test, fixer une version stable : ```cmake if(DEFINED ENV{CI_BUILD}) # Build CI : version fixe pour reproductibilité set(MODULE_VERSION "0.0.0.0") else() # Build normal : versioning automatique setup_module_versioning() endif() ``` ### 5. Affichage Version au Démarrage ```cpp void logModuleVersions() { LOG_INFO("=== Module Versions ==="); for (auto& module : moduleSystem->getAllModules()) { LOG_INFO(" {}: v{}", module->getName(), module->getVersion()); } LOG_INFO("======================="); } ``` Sortie : ``` [INFO] === Module Versions === [INFO] tank: v0.1.15.3847 [INFO] economy: v0.2.8.1203 [INFO] factory: v0.1.20.4512 [INFO] transport: v0.1.5.982 [INFO] ======================= ``` ## Debugging avec Versions ### Reproduire un Bug Si un bug est reporté avec une save : 1. **Lire version du module dans la save** : ```bash cat saves/abc123/server_1/tank.json | grep module_version # "module_version": "0.1.15.3847" ``` 2. **Retrouver commit correspondant** : ```bash # PATCH = 15 commits git log --oneline | head -n 15 | tail -n 1 ``` 3. **Vérifier hash sources** : ```bash # BUILD = 3847 correspond à un certain hash # Comparer avec hashes de commits proches ``` 4. **Checkout version exacte pour debugging** : ```bash git checkout cmake . && make # Version devrait matcher ou être proche ``` ### Logs de Version ```cpp void TankModule::initialize() { LOG_INFO("TankModule v{} initializing", getVersion()); LOG_DEBUG(" Compiled: {} {}", __DATE__, __TIME__); LOG_DEBUG(" Compiler: {}", __VERSION__); } ``` ## Alternatives et Limitations ### Alternative 1 : Semantic Versioning Manuel **Avantage** : Contrôle total sur versions **Inconvénient** : Maintenance manuelle, oublis fréquents ### Alternative 2 : Git Tags Only ```cmake execute_process( COMMAND git describe --tags --always OUTPUT_VARIABLE MODULE_VERSION ) ``` **Avantage** : Simple, suit git **Inconvénient** : Nécessite tags, pas de granularité build ### Limitation Actuelle : Hash Non-Déterministe Le hash des sources peut varier selon : - Ordre de lecture des fichiers (filesystem dépendant) - Encodage des fichiers - Whitespace differences **Solution future** : Hash déterministe avec tri des fichiers et normalisation. ## Migration depuis Système Existant Si des modules existants utilisent un autre système : ```cmake # Détecter ancien système if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/version.txt") file(READ "${CMAKE_CURRENT_SOURCE_DIR}/version.txt" OLD_VERSION) message(WARNING "Found old version.txt, migrating to VERSION file") # Extraire MAJOR.MINOR string(REGEX MATCH "^[0-9]+\\.[0-9]+" BASE_VERSION ${OLD_VERSION}) file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" "${BASE_VERSION}") message(STATUS "Migration complete: ${OLD_VERSION} -> ${BASE_VERSION}.x.x") endif() ``` ## Références Croisées - `systeme-sauvegarde.md` : Utilisation des versions dans le système de save - `architecture-modulaire.md` : Interface IModule, contraintes modules - `claude-code-integration.md` : Optimisation builds pour développement AI ## Exemples Complets ### Module Complet avec Versioning Voir structure complète dans : ``` modules/tank/ ├── VERSION # "0.1" ├── CMakeLists.txt # Setup versioning + build ├── src/ │ ├── TankModule.cpp # Utilise MODULE_VERSION │ └── Tank.cpp └── include/ └── TankModule.h # Déclare getVersion() ``` ### Test Versioning ```cpp #ifdef TESTING #include void testVersioning() { TankModule module; std::string version = module.getVersion(); assert(!version.empty()); assert(version != "unknown"); // Parse version VersionInfo info = VersionInfo::parse(version); assert(info.major >= 0); assert(info.minor >= 0); assert(info.patch >= 0); assert(info.build >= 0); std::cout << "Versioning test passed: " << version << std::endl; } #endif ```