Encore ?! Oui... encore. Une nouvelle manière de gérer la construction d'un programme VG5000µ. Après la version Sublime Text et z80asm en 2018, puis la version Visual Studio Code et sjasmplus en 2020, je voulais essayer autre chose.
J'avais laissé de côté Sublime Text et z80asm pour deux raisons : le changement de license de Sublime Text que je n'avais pas apprécié, et le côté très simpliste de z80asm, dont je touchais des limites.
Pour un nouveau projet, je voulais utiliser z88dk, un kit de développement pour machines Z80, avec du support C et ASM, ainsi que des bibliothèques standards. Je voulais aussi approfondir ma connaissance du support de toolchains avec CMake.
Alors oui, cmake
pour un tout petit projet pour des machines des années 80, ça fait un peu surdimensionné... Je le concède. Et ça n'enlève en rien mon envie de fouiller de ce côté.
Un exemple, qui peut servir de base, est disponibles sur GitLab et GitHub. Il y a quelques limitations quand à l'environnement supporté, car je n'ai testé que sur mon ordinateur de développement.
Mais l'essentiel est là, et peut éviter des heures de recherches entre la documentation de CMake et StackOverflow, la première était connue pour son aridité et le second pour, en ce qui concerne CMake, ses 99% de réponses fausses, ou tout du moins obsolètes.
Par la suite, je vais décrire les différents éléments qui forment le projet CMake.
CMakeLists.txt
Tout projet cmake a pour point d'entrée un fichier CMakeLists.txt
.
cmake_minimum_required(VERSION 3.20)
Tout d'abord, on indique la version minimale de cmake
à utiliser. Il est toujours préférable de mettre celle sur laquelle on a validé le fonctionnement, car cmake
évolue souvent et tenter d'en mettre une plus ancienne est acrobatique.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/")
Je vais avoir besoin d'ajouter des scripts de description de machine et de compilateur localement, car non reconnus nativement, du moins aujourd'hui, par cmake
. J'indique donc que certains scripts se trouvent dans le répertoire locale cmake/
, en modifiant la variable CMAKE_MODULE_PATH
.
Je déclare ensuite le nom du projet vg_tests
, ainsi que les langages de programmation supportés.
# If need for cmake debug, the next line can help
# set(CMAKE_VERBOSE_MAKEFILE 1)
Comme indiqué, mettre la variable CMAKE_VERBOSE_MAKEFILE
à 1 permet d'avoir des informations sur les actions qui sont faites lors de la génération du projet. Très pratique pour comprendre quelles sont les lignes de commandes générées, avec leurs options. Essentiel pour mettre au point quand on tâtonne.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ../output)
La variable CMAKE_RUNTIME_OUTPUT_DIRECTORY
indique où sont les artefacts de sortie. Dans notre cas, c'est là que se trouveront les .k7
utilisables pour le vg5000µ (ou les .wav
, si vous le désirez).
set(SOURCE_FILES src/main.c src/auxiliary.asm)
SOURCE_FILES
est une variable interne à ce script. C'est une habitude que de passer par une variable intermédiaire pour spécifier la liste des fichiers sources. Dans certains cas, on peut avoir besoin de la réutiliser.
Ici, je vais compiler et assembler un fichier C et un fichier assembleur.
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
target_compile_options(${PROJECT_NAME} PRIVATE -I$ENV{Z88DK_HOME}/include -Isrc/ -vn -m)
target_link_options(${PROJECT_NAME} PRIVATE -m -create-app -subtype=default)
La déclaration de l'exécutable utilise la variable PROJECT_NAME
, qui prend le nom spécifié dans project()
au tout début, et y associe la liste des fichiers sources.
Puis sont définies les options de compilations et les options pour l'éditeur de liens. La nature des options ne sont pas du domaine de cet article. Elles seront transmises à zcc
, qui est la commande générique pour toutes les opérations de construction dans z88dk
.
# Fixes the k7 format for old z88dk versions.
set(INPUT_FOR_FIX ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${PROJECT_NAME}.k7)
set(ZERO_FILE ${CMAKE_SOURCE_DIR}/zero-file)
set(OUTPUT_FOR_FIX ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${PROJECT_NAME}.fix.k7)
add_custom_command(OUTPUT k7_fix
DEPENDS ${PROJECT_NAME}
COMMAND ${CMAKE_COMMAND} -E cat ${INPUT_FOR_FIX} ${ZERO_FILE} > ${OUTPUT_FOR_FIX}
)
add_custom_target(${PROJECT_NAME}-fix ALL DEPENDS k7_fix)
La version actuelle de z88dk
a un bug au niveau de la génération des données du VG5000µ. Il manque des octets à la fin, qui sont attendues par la ROM pour valider la fin du fichier.
J'ai soumis un fix, qui a été accepté, mais le temps que cela soit déployé partout, j'ajoute cette custom_target
, qui utilise une custom_command
. Le fichier zero-file
est le fichier nécessaire à l'ajustement, et ne contient que des 0
.
Compilation croisée
Lancer cmake
tel quel ne va pas fonctionner, car par défaut, cela utilisera les outils de compilation de la machine hôte. Il faut donc spécifier une environnement de compilation croisée, c'est-à-dire les outils pour compiler pour une machine cible, et non la machine hôte.
Pour cela, cmake
a un mécanisme de déclaration de compilation croisée. Lors de l'initialisation de cmake
, il faut spécifier la variable CMAKE_TOOLCHAIN_FILE
. Ici, cmake -DCMAKE_TOOLCHAIN_FILE=z88dk-vg5000.cmake
.
Ici, on entre un peu dans le tâtonnement. Ce que j'expliquer fonctionne, mais est-ce que c'est carré ? C'est une bonne question.
Voici l'explication du fichier z88dk-vg5000.cmake
.
set(CMAKE_SYSTEM_NAME vg5000)
set(CMAKE_SYSTEM_PROCESSOR Z80)
set(CMAKE_C_COMPILER_ID z88dk)
set(CMAKE_ASM_COMPILER_ID z88dk)
En premier lieu, on fourni des valeurs à des variables internes de cmake
indiquant le nom du système, le processeur, et des identifiants du compilateur et de l'assembleur. Ces variables seront utilisées par cmake
pour déterminer quels fichiers de description il doit chercher.
Il n'est pas toujours très clair de savoir quelle variable influe sur quoi. Il faut se fier aux messages d'erreurs lorsqu'un fichier n'est pas trouvé...
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
Cette variable indique à cmake
si, pour valider le fonctionnement de la chaîne de compilation, un essai se fera sur une bibliothèque ou sur un exécutable. L'exécutable, pour être validé, doit être lancé. Comme je ne définie pas de moyen de lancer l'exécutable, je demande à ne faire l'essai de compilation que sur une bibliothèque, ce qui est en fait le défaut lors d'une compilation croisée.
set(TOOLCHAIN_PREFIX z88dk)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}.zcc)
set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}.zcc)
Ici, j'indique le nom des compilateurs et assembleur. Pour cmake
, tout ce qui prend une source et sort un fichier objet est un compiler
. Pour z88dk
, je passe par le frontend
zcc
plutôt que les exécutable eux-mêmes, ce qui semble être préféré dans la documentation (et dans la façon dont est construit le paquetage).
set(CMAKE_DEPENDS_USE_COMPILER True)
J'indique aussi à cmake
d'utiliser le compilateur pour trouver les dépendances entre les fichiers.
set(CMAKE_C_COMPILE_OBJECT "<CMAKE_C_COMPILER> +vg5k <DEFINES> <INCLUDES> <FLAGS> -o <OBJECT> -c <SOURCE>")
set(CMAKE_C_LINK_EXECUTABLE "<CMAKE_C_COMPILER> +vg5k <FLAGS> <OBJECTS> -o <TARGET> <CMAKE_C_LINK_FLAGS> <LINK_FLAGS> <LINK_LIBRARIES>")
set(CMAKE_ASM_COMPILE_OBJECT "<CMAKE_C_COMPILER> +vg5k <DEFINES> <INCLUDES> <FLAGS> -o <OBJECT> -c <SOURCE>")
On y est presque. Dans un cas classique où le compilateur se comporte de manière classique (disons comme un gcc
, un clang
ou autre), on pourrait se passer de ces lignes. Mais zcc
à besoin comme premier paramètre de la plateforme cible (+vg5k
ici).
J'indique donc à cmake
comment générer la ligne de commande pour les fichiers C
et ASM
, avec une syntaxe de template.
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
Et enfin, je demande à cmake
de ne pas chercher à résoudre les commandes find_library
, que je n'utiliserai pas.
Et ce n'est pas fini !
Le fichier de compilation croisée indique que l'on compile pour VG5000µ. Mais cmake
ne connait pas cette plateforme, et va donc chercher un script qui lui en dirait plus.
Ce que j'ai fait n'est probablement pas entièrement correct, car j'associe la plateforme avec la chaîne de compilation. Et je fais ça dans le fichier cmake/Platform/vg5000.cmake
.
set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE)
z88dk
ne supporte pas un systmème de bibliothèque dynamiques (style DLL
, so
ou dynlib
).
set(CMAKE_C_OUTPUT_EXTENSION .o)
z88dk
ne reconnaît que l'extension .o
comme fichiers objets. On indique donc à cmake
de produire des fichiers objets avec cette extension.
set(CMAKE_SYSTEM_INCLUDE_PATH $ENV{Z88DK_HOME}/include)
set(CMAKE_SYSTEM_LIBRARY_PATH $ENV{Z88DK_HOME}/lib)
set(CMAKE_SYSTEM_PROGRAM_PATH $ENV{Z88DK_HOME}/bin)
Ces variables ont l'air d'être les pendants des CMAKE_FIND_ROOT_PATH_MODE_*
indiqués dans le fichier de compilation croisée. Le fonctionnement n'est pas hyper clair.
Voilà, à présent cmake
sait compiler un fichier C
avec z88dk
pour VG5000µ.
Et l'assembleur ?
Pour une raison que je n'ai pas creusé, mais probablement concernant la séparation des plateformes des compilateurs, le support d'un assembleur nécessite un autre fichier, différent du précédent vg5000.cmake
.
C'est dans cmake/Compiler/z88dk-ASM.cmake
que cmake
va chercher l'outil pour traiter l'ASM
pour z88dk
. Ce qui se tient. Pourquoi est-ce qu'il ne va pas chercher z88dk-C.cmake
au même endroit, cela m'échappe...
Le contenu est strictement identique à celui du fichier vg5000.cmake
, puisque l'on s'adresse au même outil zcc
.
Et la cerise optionnelle
Le fichier z88dk-clion.yaml
est un fichier qui ajoute un support de z88dk
(assez succin) à Clion
. Par défaut, lors de la génération de cmake
, l'IDE va essayer de trouver un certain nombre d'information en interrogeant le compilateur. Les #define
par exemple, ou les répertoires d'inclusion par défaut.
Mais zcc
ne réagissant par bien à la question, Clion affiche un warning. Il est cependant possible de lui indiquer manuellement les informations recherchées, et c'est le but de ce fichier. Cependant, sa description tombe hors du sujet de cet article.
Conclusion
Ça a été une aventure, comme à chaque fois que l'on sort des sentiers battus avec cmake
. J'y ai appris un peu plus de choses, ce qui était probablement l'objectif initial. Et j'ai une façon de générer un programme VG5000µ à partir de tout outil qui utilise cmake
, ce qui couvre aussi Visual Studio Code.