Projecten in Ada, een blik op GPRbuild
======================================
2020-09-17
Nu ik serieuze grotemensenprojecten in Ada maak, moet ik
beter kijken naar mijn bestandsindeling en naar de manier
waarop ik omga met externe afhankelijkheden. Mijn software
moet betrouwbaar compileren en daarbij moet ik verder kijken
dan mijn eigen ontwikkelcomputer lang is. Het is dus tijd om
een fatsoenlijk bouwplan te maken, een zinnige ordening van
de broncodebestanden te bekokstoven, en een logische plek
voor externe bibliotheken te verzinnen.
Oké, laat ik eerlijk zijn: ik ben geen fan van het
compileren en de bijbehorende bureaucratie met allerhande
bouwgereedschappen. Ik houd voornamelijk van het nadenken
over software-architectuur en van het coden zelf. Ik kan
slechts met tegenzin de voor- en nadelen van GNU Make versus
CMake bespreken. Ik word niet bijzonder gelukkig van
geautomatiseerde buildservers en het nieuwe IT-vakgebied van
devops lijkt me verminderd jolijtig.
Het liefst benader ik de praktijk van het compileren en
linken van software zoals Casey Muratori dat in Handmade
Hero [1] doet. In deze serie van video’s bouwt hij een
videogame vanaf nul. In een van de eerste afleveringen
beschrijft hij zijn werkwijze. Hij gebruikt weliswaar Visual
Studio, maar typt voornamelijk in Emacs. Daarnaast gebeurt
het daadwerkelijke compileren en linken via het uitvoeren
van een batchbestand dat hij heeft klaargezet bovenin de map
met broncode. Hij schuwt ingewikkelde bouwsystemen en dat
vind ik verfrissend. Je hebt vaak de neiging om dingen
verfrissend te vinden die je zelf ook doet, maar waar je
enige gêne over voelt en ik vermoed dat dit zo’n situatie
is. Ik gebruik namelijk al jaren een soortgelijke aanpak als
Casey doet in Hand Made Hero, maar dacht altijd dat dit meer
een teken van luiheid dan vernuft was :-).
De meeste van mijn projecten hebben een eenvoudig
shellscript dat ik met een druk op F5 in Vim kan aanroepen.
Daarna voert hij de noodzakelijke stappen uit om tot een
uitvoerbaar bestand te komen en klaar is kees. Ik probeer in
mijn shellscript alleen het broodnodige te doen. Als ik kan
voorkomen dat ik een ingewikkelde Makefile moet maken dan
doe ik dat en roep ik de compiler het liefst rechtstreeks
aan. Als ik vervelende dependency managers het nakijken kan
geven en mijn afhankelijkheden handmatig kan beheren dan
prefereer ik dat.
Deze shellscriptwerkwijze heeft jammer genoeg zijn grenzen.
Juist bij het onderwerp afhankelijkheden moet ik vaak de
muismat in de ring gooien en er toch voor kiezen om de
bouwgereedschappen te gebruiken die in zwang zijn bij de
taal in kwestie. Hoe graag ik ook zoals Casey de Geweldenaar
zou werken, blijk ik in de praktijk toch een schaamteloze
capitulator: iemand die met gebogen hoofd uitvoert wat het
Grootsysteem van hem verwacht. Bij Haskell betekent dit dat
ik Cabal of Stack gebruik. Bij Python gebruik ik Pipenv of
Pip in combinatie met Virtualenv. Bij PHP-projecten heb ik
Composer, ik rammel met Cocoapods bij iOS en dat alles staat
nog los van de onuitsprekelijke rituelen die ik moet
uitvoeren om JavaScriptprojecten tot een goed einde te
brengen.
De afgelopen week was ik aan het kijken hoe ik het best mijn
Ada-projecten kan bouwen. In hobbyland is het relatief
eenvoudig: je voert gewoon *gnatmake* uit en hakuna matata!
Afhankelijkheden zoals Gnatcoll komen door de ether
aanzweven met een simpele verwijzing naar de plek waar deze
op mijn machine is geïnstalleerd en alles wordt netjes tot
een geheel verknoopt. Ik was eigenlijk best tevreden met
gnatmake. Het combineert het compileren, binden en linken
van je project en doet dit ook nog vrij vlot.
Desalniettemin begon ik milde fronsrimpels te krijgen bij
het kale toepassen van gnatmake in mijn werkprojecten. Wat
moet ik met de afhankelijkheden waar ik zo lustig naar
verwijs in mijn code? Was hij na het compileren mijn code
niet met objectbestanden aan het linken die zich puur in de
schaduwen van mijn ontwikkelcomputer ophielden? Hoe kan ik
ervoor zorgen dat de uitvoerbare bestanden die ik bouw
reproduceerbaar zijn als ik niet precies weet waar alle
ingrediënten vandaan komen?
Het compileren van een compleet softwareproject is als met
een kruidenmix. Het is leuk en aardig als je de perfecte
melange van kerriekruiden heb gevonden bij de plaatselijke
toko, maar als je de perfecte kerriesmaak niet zelf kunt
reproduceren zal je de toko altijd bij de hand moeten hebben
als je nieuwe mix nodig hebt. Als de toko is gesloten op een
gure winteravond en de bodem van je kerriezakje is bereikt,
of als een niet aflatende stroom visite met vrolijk
gekleurde benzinepompboeketjes je weerhoudt het huis te
verlaten, of als de apocalyps eindelijk daar is en je niets
liever wil dan rijst met kerrie eten tussen de brokstukken
van onze eens zo voortreffelijke samenleving dan heb je wel
een bijzonder naargeestige kerriegerelateerde kwelling bij
de hand. Het compileren van een softwareproject met
gereedschappen die je boven de pet zijn gegroeid is exact
als die vermaledijde kerriemix.
Ik vind complexiteit meestal een aantrekkelijke eigenschap
in problemen, behalve in de liefde, bij belastingwetten en
in buildsystemen. Toch ben ik schoorvoetend naar het
populairste buildsysteem voor Ada
- GPRbuild [8] - gaan kijken en wonder boven wonder beviel
het wat ik zag! Deze kerriekruiden zijn hier, kenbaar en
talrijk!
GPRbuild is de aangewezen manier om grotere Ada-projecten in
elkaar te klussen zonder dat je het overzicht verliest in
allerhande magische prevelingen die voor jou worden
uitgesproken. GPR staat voor Gnat Project Rnog iets. Ik
weet eigenlijk niet waar de R voor staat. Record? Reference?
Rabbits? Rollercoaster? Of misschien is het een recursief
omgekeerd acroniem waarbij de R staat voor RPG. Sommige
dingen zijn te leuk om je af te vragen en verpest je liever
niet door ze op te zoeken.
Maar goed. Je kunt .gpr-bestanden gebruiken om het
bouwproces van je project te omschrijven. Gewoonlijk is dat
het moment dat ik voorzichtig een stapje achteruit doe en
check waar de nooduitgangen zijn. Ik heb namelijk een
vervelend zenuwtrekje dat opspeelt zodra ik met
buildbestanden moet werken, en niet uitsluitend als ze
eindigen in .gradle. Gelukkig zijn GPR-bestanden vrij
zinnig. Feitelijk is het een miniscriptingtaaltje dat
enigszins op Ada lijkt. Je beschrijft wat je bouwt, welke
talen en compilers je daarbij gebruikt, in welke mappen hij
alles kan vinden en je kunt verwijzigingen naar
afhankelijkheden regelen. Met name dat laatste was het mij
om te doen, want ik wil de bibliotheken waar mijn project
van afhankelijk is het liefst bij de hand hebben op dezelfde
plek als waar ik mijn eigen code opberg.
Als je een project met een gpr-bestand wil compileren en
linken dan kun je simpelweg de commandline-aanroep
“gprbuild” gebruiken, of je kunt hem compilen in de IDE Gnat
Studio. Zowel GPRbuild als Gnat Studio maken gebruik van
hetzelfde gpr-formaat. Alhoewel ik een verstokte
Vim-gebruiker ben, vind ik dit wel erg fraai. Ik vind het
vrij onhandig dat IDE’s vaak met hun eigen bestanden met
projectadministratie komen en de wisselwerking tussen
compileren op de commandline en met buildbots en IDE’s is
meestal moeizaam. Het is dus heel prettig dat als je met
Gnat werkt het niet echt uitmaakt of je op de commandline
compileert, of dat je dit klikkertieklik in de IDE doet.
Hier zie je de inhoud van het GPR-bestand van een vrij
minimaal testprojectje waarin ik de Sqlite-bibliotheek van
Gnatcoll gebruik:
with "lib/gnatcoll-db/sqlite/gnatcoll_sqlite.gpr";
project Jelle is
for Create_Missing_Dirs use "True";
for Source_Dirs use ("src");
for Object_Dir use "obj";
for Exec_Dir use "build";
for Main use ("main.adb");
package Compiler is
for Switches ("ada") use ("-gnat12");
end Compiler;
end Jelle;
In de eerste regel gebruik ik “with”. Hiermee kun je een
ander projectbestand importeren en zo je boom van
afhankelijkheden opbouwen. In dit voorbeeld verwijs ik naar
een relatieve locatie. In de bestandsstructuur van mijn
project heb ik op het hoogste niveau een mapje genaamd
“lib”. Daaronder staan dan weer mapjes met alle externe
bibliotheken die ik gebruik. Er is een standaard
zoekvolgorde die GPRbuild gebruikt als hij te importeren
projecten wil localiseren:
- Eerst kijkt hij of hij het projectbestand in de map van
het huidige projectbestand kan vinden
- Daarna gaat hij kijken naar de mappen die gespecificeerd
zijn in de omgevingsvariabelen GPR_PROJECT_PATH,
GPR_PROJECT_PATH_FILE en ADA_PROJECT_PATH. Mits deze
bestaan natuurlijk. GPR_PROJECT_PATH_FILE bevat – als hij
gedefinieerd is – de naam van een tekstbestand dat
regelgescheiden padnamen van projecten bevat,.
GPR_PROJECT_PATH en ADA_PROJECT_PATH kunnen
dubbelepuntgescheiden paden bevatten naar projecten.
- Als hij nog steeds de import niet kan vinden dan kijkt hij
of de code in kwestie misschien algemeen is geïnstalleerd.
Als je bijvoorbeeld Gnat Community hebt geïnstalleerd dan
heb je al een hoop bibliotheken cadeau gekregen.
- <compiler_prefix>/<target>/<runtime>/share/gpr
- <compiler_prefix>/<target>/<runtime>/lib/gnat
- <compiler_prefix>/<target>/share/gpr
- <compiler_prefix>/<target>/lib/gnat
- <compiler_prefix>/share/gpr/
- <compiler_prefix>/lib/gnat/
Ik heb graag de afhankelijkheden van mijn project bij de
hand, netjes in dezelfde mappenstructuur als mijn eigen
code. Als ik de boel dan moet compileren op een andere
machine, dan weet ik zeker dat ik dezelfde versies van alle
afhankelijkheden gebruik. Om mijn project te bouwen heb ik
een piepklein bash-scriptje gemaakt dat de paden van de
afhankelijkheden in GPR_PROJECT_PATH zet:
#!/bin/bash
GPR_PROJECT_PATH=lib/gnatcoll-core:lib/gnatcoll-db/sql
export GPR_PROJECT_PATH
Dit shellscriptje stelt simpelweg de GPR_PROJECT_PATH
omgevingsvariabele in. Om het scriptje en daadwerkelijk
zijn omgevingsvariabelige magie te laten doen met je hem
“sourcen”, in bash doe je dat zo:
$ source setenv.sh
of nog korter:
$ . setenv.sh
Je ziet dat ik niet alleen de Sqlite library van Gnatcoll
heb toegevoegd, maar ook Gnatcoll-core en de Sql library.
Dit zijn weer afhankelijkheden van de Sqlite library zelf.
Als ik deze niet had toegevoegd aan het projectpad dan zou
hij ze misschien van een andere plek hebben geplukt. De
afhankelijkheidsbomen van de meeste Ada-projecten zijn
aanmerkelijk minder diep dan bij het gemiddelde
Node.js-project, dus deze zijn nog wel handmatig te volgen.
Wellicht dat ooit een systeem als Alire [11] onontbeerlijk
wordt voor het cultiveren van de afhankelijkheidsjungle,
maar persoonlijk hoop ik van niet. Het zou fijn zijn als
deze complexiteit zich beperkt tot behapbare strookjes
gemeentegroen.
Als je nu in dezelfde Bash-sessie gprbuild uitvoert dan zal
hij voor de imports eerst in de mappen kijken die je in je
omgevingsvariabelen hebt gedefinieerd. Mijn
Gnat-installatie heeft ook al een gnatcoll aan boord, maar
deze zal hij negeren en die van mij pakken. Je kunt in je
gpr-bestand nog een hoop andere dingen instellen en alles
overgieten met een hartige saus van logica en zelfgekozen
variabelen. Je kunt bijvoorbeeld verschillende flags
configureren die weer invloed hebben op de gekozen
compileropties. Zo kun je een aparte flag hebben voor het
compileren in debugmodus met allerhande gemene pedante
waarschuwingen over je codestijl, of een aparte flag die
elke mogelijke Ada-optimalisatie loslaat op je broncode. Ook
heeft GPRbuild uitgebreide ondersteuning voor het compileren
van andere talen, want veel projecten bestaan uiteindelijk
uit een meertalige mengsel.
Al met al ben ik vrij tevreden over GPRbuild en het
bijbehorende bestandsformaat. Ik hoef geen XML te typen en
de mogelijkheden lijken vrij zinnig en krachtig. Het is ook
aardig dat Gnat Studio hetzelfde bestand gebruikt voor zijn
instellingen. Zo kan ik mijn gebruikelijke programmeerwerk
in Vim doen, maar als ik even wil baden in het zonlicht van
een grafische debugomgeving in plaats van de rauwe
GDB-op-de-terminal-ervaring dan kan ik de IDE openen en
zodoende blijmoedig door mijn breakpoints heenklikken.
Hyperlinks:
[1]:
https://handmadehero.org/
[2]:
https://www.adacore.com/gems/gem-65
[3]:
https://www.adacore.com/gems/gem-104-gprbuild-and-configuration-files-part-1
[3]:
https://www.adacore.com/gems/gem-108-gprbuild-and-configuration-files-part-2
[4]:
https://www.adacore.com/gems/gem-152-defining-a-new-language-in-a-project-file
[5]:
https://www.adacore.com/gems/gem-157-gprbuild-and-code-generation
[6]:
https://docs.adacore.com/gprbuild-docs/html/gprbuild_ug/gnat_project_manager.html
[7]:
https://docs.adacore.com/gprbuild-docs/html/gprbuild_ug.html
[8]:
https://github.com/AdaCore/gprbuild
[9]:
https://gcc.gnu.org/onlinedocs/gcc-10.2.0/gnat_ugn/The-GNAT-Compilation-Model.html#The-GNAT-Compilation-Model
[10]:
https://people.cs.kuleuven.be/~dirk.craeynest/ada-belgium/events/09/090207-fosdem/02a-gnat-project-facility.pdf
[11]:
https://alire.ada.dev/
-----------------------------------------------------------
Tags: ada, nederlands