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