RSS

Projecten in Ada, een blik op GPRbuild.

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 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 - 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:lib/gnatcoll-db/sqlite
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 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: