Dependency management is een essentieel onderdeel van softwareontwikkeling. Het verwijst naar het beheren van alle externe libraries, modules, externe broncode … die een project nodig heeft om correct te functioneren. Deze dependencies (= afhankelijkheden) kunnen variëren van kleine hulpprogramma’s tot grote frameworks die een groot deel van de functionaliteit van een applicatie leveren.
Er zijn verschillende uitdagingen verbonden aan dependency management:
Versieconflicten: Wanneer verschillende dependencies verschillende versies van dezelfde bibliotheek vereisen, kunnen er conflicten ontstaan. Dit kan leiden tot bugs of zelfs het failen van de build.
Transitive dependencies: Dit zijn dependencies van dependencies. Het kan moeilijk zijn om bij te houden welke bibliotheken indirect worden geïmporteerd en of ze compatibel zijn met de rest van het project.
Beveiligingsrisico’s: Het gebruik van externe bibliotheken kan beveiligingsrisico’s met zich meebrengen, vooral als deze bibliotheken niet regelmatig worden bijgewerkt.
Compatibiliteit: Het kan een uitdaging zijn om ervoor te zorgen dat alle dependencies compatibel zijn met elkaar en met de gebruikte programmeertaal of runtime.
Tools zoals Gradle helpen bij het automatiseren en vereenvoudigen van dependency management. Hier zijn enkele redenen waarom we deze tools gebruiken:
Automatisering: tools zoals Gradle kunnen het proces van het downloaden en beheren van dependencies automatiseren. Dit betekent dat ontwikkelaars zich kunnen concentreren op het schrijven van code in plaats van het handmatig beheren van bibliotheken.
Versiebeheer: tools zoals Gradle bieden vaak geavanceerde mogelijkheden voor versiebeheer, waardoor het eenvoudiger wordt om versieconflicten op te lossen en ervoor te zorgen dat de juiste versies van bibliotheken worden gebruikt.
Transitive dependency management: vaak worden door deze tools transitieve dependencies automatisch beheerd, wat betekent dat het alle benodigde bibliotheken en hun dependencies downloadt en configureert.
Configuratiebeheer: het stelt ontwikkelaars ook vaak in staat eenvoudig verschillende configuraties in te stellen voor verschillende omgevingen (bijvoorbeeld ontwikkel-, test- en productieomgevingen) en/of verschillende architecturen en operating systems.
Reproduceerbaarheid: door een tool zoals Gradle wordt je software gebouwd met steeds dezelfde versies van bibliotheken, onafhankelijk van wat (en welke versies) op de machines van de ontwikkelaars geïnstalleerd werd. Dat maakt het makkelijker om bestaande software opnieuw te bouwen op een willekeurige machine.
Voorbeeld
Stel je voor dat je een project hebt dat afhankelijk is van een logging-library en een JSON-parser. De logging-library heeft op zijn beurt een specifieke versie van een utility-library nodig.
Met een dependency management tool (bv. Gradle) hoef je alleen maar de logging-library en de JSON-parser te specificeren in je buildscript, en de tool zorgt ervoor dat alle benodigde dependencies, inclusief de transitieve dependency (de utility-library) worden gedownload en correct worden geconfigureerd.
Door zo’n tools te gebruiken, kunnen we de complexiteit van dependency management dus aanzienlijk verminderen en ervoor zorgen dat onze projecten betrouwbaar en veilig blijven.
Tools voor dependency management
Enkele bekende tools voor dependency management zijn:
Gradle, Maven, Ant voor Java
(C/Q)Make (custom config) voor C/C++
npm, yarn, grunt, gulp, (in JS) … voor JS (nodejs)
pip voor Python
nuget (custom config, XML) voor .NET
In de volgende delen gaan we dieper in op deze concepten en er ook praktisch mee aan de slag. We bestuderen meer bepaald Gradle (Java), CMake (C) en pip (Python).
Subsections of 4. Dependency Management
In Java (met Gradle)
Een voorbeeld
Een dependency, of afhankelijkheid, is een externe bibliotheek die wordt gebruikt tijdens de ontwikkeling van een toepassing. Tijdens het vak ‘Software ontwerp in Java’ zijn reeds de volgende externe libraries gebruikt:
Het vertrouwen op zo’n library houdt in dat een extern bestand, zoals een .jar of .war bestand, gedownload en gekoppeld wordt aan de applicatie. In Java koppelen we externe libraries door middel van het CLASSPATH: dat is een lijst van folders en jars die de compiler (bij het compilen) en de Java runtime (bij het uitvoeren) gebruikt om te zoeken naar de implementatie van de gebruikte klassen.
Laten we bijvoorbeeld eens kijken naar het gebruik van de Gson library om Json te genereren vanuit Java.
Serialisatie met behulp van Gson kan op deze manier:
Als we bovenstaande Main.java compileren zonder meer, krijgen we echter de volgende fout:
arne@LT3210121:~/ses/depmanag$ javac Main.java
Main.java:1: error: package com.google.gson does not exist
import com.google.gson.Gson; ^
Main.java:6: error: cannot find symbol
Gson gson= new Gson(); ^
symbol: class Gson
location: class Main
Main.java:6: error: cannot find symbol
Gson gson= new Gson(); ^
symbol: class Gson
location: class Main
3 errors
De klasse Arrays die we importeren is deel van de standaard Java-omgeving. Die zorgt dus niet voor problemen.
Maar de klasse com.google.gson.Gson hebben we niet zelf gemaakt, en willen we importeren uit een library.
We moeten de library daarom eerst downloaden.
(Je kan de jar ook rechtstreeks downloaden met behulp van curl via $ curl https://repo1.maven.org/maven2/com/google/code/gson/gson/2.12.1/gson-2.12.1.jar --output gson-2.12.1.jar. Indien je curl nog niet geïnstalleerd hebt doe dat dan eerst!)
Het programma compileren kan nu met javac -cp gson-2.12.1.jar Main.java. We geven de gedownloade jar mee aan het classpath (via de -cp optie). Dat zorgt ervoor dat de compiler de geïmporteerde klasse Gson nu wel kan terugvinden, en het compileren slaagt.
Om het programma uit te voeren, gebruiken we java -cp "gson-2.12.1.jar:." Main. Merk op dat er nu 2 zaken aan het classpath worden meegegeven: de Google jar, maar ook de huidige directory (.) om Main.class terug te vinden.
Opgelet!
Java classpath separators zijn OS-specifiek! Unix: : in plaats van Windows: ;.
Dit programma kan schematisch worden voorgesteld als volgt:
graph LR;
A["Main klasse"]
C["Gson 2.12.1"]
A -->|depends on| C
De dependency in bovenstaand voorbeeld is gson-2.12.1.jar. Een gemiddelde Java applicatie heeft echter meer dan 10 dependencies! Het beheren van deze bestanden en de verschillende versies (major, minor, revision) geeft vaak conflicten die beter beheerd kunnen worden door tools dan door de typische vergeetachtigheid van mensen. Dit kluwen aan afhankelijkheden, dat erg snel onhandelbaar kan worden, noemt men een Dependency Hell. Er zijn varianten: DLL Hell sinds 16-bit Windows versies, RPM Hell voor Redhat Linux distributies, en JAR Hell voor Java projecten.
De eenvoudigste manier om een library te gebruiken is de volgende procedure te volgen:
Navigeer naar de website van de library en download de jar in een bepaalde map, zoals /lib.
Importeer de juiste klasses met het import statement.
Compileer de code door middel van het -cp lib/dependency1.jar argument.
Voor kleine programma’s met slechts enkele libraries is dit meer dan voldoende. Het kost echter redelijk veel moeite om de juiste versie te downloaden: stap 1 kost meestal meer dan 5 minuten werk.
Note
Merk op dat jar files in een submap steken de syntax van de -cp parameter lichtjes wijzigt: bij compileren wordt het javac -cp "lib/*" Main.java en bij uitvoeren wordt het java -cp "lib/*:." Main. Zonder de toegevoegde punt (.) bij het java commando wordt de main methode in Main zelf niet gevonden. Wildcards zijn toegestaan. Zie ook Understanding the Java Classpath. In de praktijk worden build tools als Gradle gebruikt om projecten automatisch te builden, inclusief het doorgeven van de juiste parameters/dependencies.
Als er een nieuwe versie van de library verschijnt die je wil gebruiken (bv. met een nieuwe feature of een bugfix), moet je dat eerst en vooral zelf nagaan, de jar van de nieuwe versie downloaden, je classpath aanpassen, en alles opnieuw compileren.
Ook bij de uitvoering moet je zorgen dat je de nieuwe versie gebruikt.
Apache Maven
Maven is een build tool van de Apache Foundation om de software te compileren en afhankelijkheden te beheren. Maven is de voorloper van Gradle en bestaat reeds 15 jaar.
Een Maven project heeft een pom.xml bestand (Project Object Model), waarin in XML formaat wordt beschreven hoe de structuur er uit ziet, welke libraries men gebruikt, en zo voort:
Maven is erg populair in de Java wereld, waardoor er verschillende servers zijn die deze pom bestanden samen met hun libraries beheren, zoals de Central Maven Repository en de Google Maven Repository mirrors. De syntax van het configuratiebestand is echter erg onoverzichtelijk, en er zijn ondertussen betere alternatieven beschikbaar, zoals Gradle.
We gaan in deze cursus dus geen gebruik maken van Maven, maar je zal wel verwijzingen naar de Maven repositories tegenkomen bij het gebruik van Gradle.
Gradle
Gradle is, net zoals Maven, een build tool voor de Java wereld (en daarbuiten) die de automatisatie van releasen, builden, testen, configureren, dependencies en libraries managen, … eenvoudiger maakt. Kort gezegd: het maakt het leven van een ontwikkelaar eenvoudiger.
Gradle bouwt verder op de populariteit van Maven door bijvoorbeeld compatibel te zijn met de Repository servers, maar de grootste pijnpunten wegneemt: een moeilijk leesbaar configuratiebestand in XML en complexe command-line scripts.
In een config bestand genaamd build.gradle schrijft men met Groovy, een dynamische taal bovenop de JVM (Java Virtual Machine), op een descriptieve manier hoe Gradle de applicatie moet beheren.
Je build-file is dus een uitvoerbaar (Groovy) script dat gebruik maakt van Gradle-specifieke functies.
Info
Gradle configuratiebestanden kunnen ook in Kotlin geschreven worden (build.gradle.kts) in plaats van Groovy.
Deze optie wint aan populariteit, en is sinds Gradle 8.2 (2023) de default, maar we gebruiken in deze cursus nog de Groovy syntax. De verschillen zijn klein.
Voordelen van Gradle
De grootste voordelen van een build en dependency management tool zoals Gradle zijn onder andere:
Een kleine voetafdruk van de broncode (repository). Het is niet nodig om zelf alle jars van libraries te downloaden en bij te houden in een lib/ folder: Gradle doet dit immers voor jou.
Een project bootstrappen in luttele seconden: download code, voer de Gradle wrapper uit, en alles wordt vanzelf klaargezet (de juiste Gradle versie, de juiste library versies, …)
Een project bootstrappen betekent het opzetten of initialiseren van een project vanaf het begin, vaak met behulp van een tool of framework dat de basisstructuur en configuratie voor je genereert. Dit proces helpt je snel aan de slag te gaan zonder dat je alles handmatig hoeft in te stellen.
Platform- en machine-onafhankelijk projecten bouwen en uitvoeren: een taak uitvoeren via een build tool op mijn PC doet exact hetzelfde als bij jou, dankzij de beschrijving van de stappen in de config file.
Om een library als Gson te kunnen gebruiken, moet je dus niet zelf de jar-bestanden aanleveren; het volstaat simpelweg om twee regels in de gradle-configuratie toe te voegen.
Gradle installeren
Je kan Gradle op je WSL installeren met het commando sudo snap install gradle --classic. (Zie ook Gradle Docs: installing manually) Dit installeert echter niet altijd de nieuwste versie van Gradle, in dit geval v7.2, maar dat is geen probleem (hier komen we zo dadelijk op terug).
Er zijn ook andere manieren om Gradle te installeren, bijvoorbeeld met SDKMAN!. Dit is een tool om allerhande (Java-gebaseerde) Software Development Kits (SDKs) op je systeem te installeren en beheren, en eenvoudig te wisselen tussen versies. Eens SDKMAN! geïnstalleerd is, kan je eenvoudig de laatste versie van gradle installeren met sdk install gradle.
Gradle in VSCode
Je kan ondersteuning toevoegen voor Gradle in VSCode met de juiste extensie, zie Java development environment in VSCode.
In het bijzonder voor Gradle is de Gradle for Java plugin van Microsoft aangewezen.
Deze is deel van het Extension Pack for Java, waarmee je ineens ook andere nuttige extensies voor Java-ontwikkeling installeert.
Deze extensie geeft je extra ondersteuning voor het editeren van je gradle.build bestand (syntax highlighting, code completion).
Bovendien kan je voor je projecten ook de verschillende Gradle-taken bekijken en uitvoeren, alsook nagaan welke dependencies er in welke configuratie (compile, runtime, test) geactiveerd zijn — wat dat precies inhoudt, komt zodadelijk aan bod.
# Gradle vraagt eerst welk soort project je wil aanmaken, we kiezen voor 2. applicationSelect type of project to generate:
1: basic
2: application
3: library
4: Gradle plugin
Enter selection (default: basic)[1..4]2# Nu vraagt Gradle welke programmeertaal we willen gebruiken, we kiezen voor 3. JavaSelect implementation language:
1: C++
2: Groovy
3: Java
4: Kotlin
5: Scala
6: Swift
Enter selection (default: Java)[1..6]3# Nu vraagt Gradle te kiezen tussen een het soort project, we kiezen voor 1. only one application projectSplit functionality across multiple subprojects?:
1: no - only one application project
2: yes - application and library projects
Enter selection (default: no - only one application project)[1..2]1# Nu vraagt Gradle te kiezen tussen programmeertalen voor ons build script (de taal waarmee we onze build.gradle file gaan programmeren), we kiezen voor 1. GroovySelect build script DSL:
1: Groovy
2: Kotlin
Enter selection (default: Groovy)[1..2]1# Nu vraagt Gradle te kiezen tussen een testframework, we kiezen voor 1. JUnit 4Select test framework:
1: JUnit 4 2: TestNG
3: Spock
4: JUnit Jupiter
Enter selection (default: JUnit Jupiter)[1..4]4# Nu vraagt Gradle een projectnaam, default is dit de directorynaam. Vul niets in en druk op enter om de default te gebruiken.Project name (default: gradletest):
# Als laatste vraagt Gradle je de naam van de source package te kiezen. Gelijkaardig als in INF 1 kiezen we voor be.ses.<app_name>Source package (default: gradletest): be.ses.my_application
Gradle vraagt ons tijdens de init een aantal opties te kiezen. Alhoewel we in deze lessen 90% van de tijd de opties kiezen zoals hierboven getoond in het voorbeeld, kan je hieronder toch een overzicht terugvinden met enkele andere opties:
Select type of build to generate: opties - Application - Library (gaan we niet verder op in) - Gradle Plugin (gaan we niet verder op in) - Basic
Application: Dit type is geconfigureerd om een volwaardige uitvoerbare applicatie te bouwen. Het bevat extra configuraties en plugins, zoals de application plugin, die helpt bij het definiëren van de hoofdmethode en het maken van uitvoerbare JAR-bestanden. Geschikt voor het ontwikkelen van volledige applicaties die je kunt uitvoeren en distribueren. (99% van de tijd gaan we dit type gebruiken)
Basic: Dit is een eenvoudig project zonder specifieke plugins of configuraties. Het bevat alleen de minimale structuur en bestanden die nodig zijn om een Gradle-project te starten. Je kan dan enkel nog de projectnaam kiezen en de programmeertaal voor Gradle.
Select implementation language: Je ziet dat Gradle dus ook voor andere programmeertalen gebruikt kan worden. Wij kiezen hier echter voor Java.
Select application structure: opties - 1. only one application project - application and library projects
Only one application project: Dit type project is gericht op het bouwen van één enkele applicatie. De projectstructuur is eenvoudig en bevat meestal alleen de bronbestanden en configuratiebestanden die nodig zijn om de applicatie te bouwen en uit te voeren. Geschikt voor kleinere projecten of wanneer er geen behoefte is aan herbruikbare componenten.
Application and library project: Dit type project is opgesplitst in meerdere modules, waaronder een applicatiemodule en een of meer bibliotheekmodules. De bibliotheekmodules bevatten herbruikbare code die door de applicatiemodule kan worden gebruikt. Deze structuur bevordert modulariteit en hergebruik van code, wat vooral nuttig is voor grotere projecten of wanneer je van plan bent om delen van je code in andere projecten te gebruiken.
Bij het aanmaken van je project maakt Gradle een aantal folders en bestanden aan. We overlopen hieronder de functionaliteit van de belangrijkste onderdelen.
Ontleding van een Gradle project mappenstructuur
Als we kijken naar de bestanden- en mappenstructuur van een voorbeeld Gradle project, vinden we dit terug:
Broncode (.java bestanden) van het app subproject in src/main/java en src/test/java, met productie- en testcode gescheiden.
Eventueel resources (bv. afbeeldingen, html en css voor webapplicaties …)
Gecompileerde code (.class bestanden) in de build/ (of ook wel out) folder.
Een gradle map, met daarin de Gradle-wrapper en bijhorende configuratie (hier komen we zodadelijk op terug). Deze folder hoor je toe te voegen aan je versiebeheersysteem (git).
Twee executables (gradlew.bat voor Windows en een gradlew-shell script voor Linux/Unix). Ook deze twee executables voeg je toe aan git.
Twee Gradle configuratie-bestanden:
settings.gradle voor het project als geheel. Hierin kan je de projectnaam aanpassen: rootProject.name = 'new_name'. Je lijst hier ook de verschillende subprojecten op: include('app').
build.gradle: eentje per subproject (hier enkel app). Hierin specifieer je de dependencies en andere instellingen om dat specifieke subproject te bouwen.
Later kan er ook een verborgen .gradle map verschijnen. De inhoud hiervan hoort niet thuis in je versiebeheersysteem!
Gradle wrapper (gradlew)
Wanneer je een Gradle project aanmaakt, creëert Gradle vanzelf ook een wrapper. Dat is een soort lokale executable in de vorm van een ./gradlew-executable (of gradlew.bat voor op Windows).
Dit wrapper-script zal, wanneer het uitgevoerd wordt, een specifieke versie van Gradle downloaden in de gradle-map.
Dit heeft enkele voordelen:
Consistentie: Het zorgt ervoor dat iedereen die aan het project werkt dezelfde versie van Gradle gebruikt, ongeacht de versie die lokaal is geïnstalleerd. (Door steeds de gradlew in de projectdirectory te gebruiken)
Gemak: Gebruikers hoeven Gradle niet apart te installeren, omdat de wrapper automatisch de juiste versie downloadt en gebruikt. Enkel om initieel een project aan te maken heb je een lokale versie van Gradle nodig.
Automatisering: Het maakt het eenvoudiger om build-scripts en CI/CD-pijplijnen te configureren (zie een later hoofdstuk), omdat de wrapper de benodigde Gradle-versie beheert.
De specifieke versie van Gradle die je gebruikt hangt (onder andere) af van de Java-versie die je gebruikt in je project.
Hier vind je een overzicht van de minimum-versies van Gradle die compatibel zijn met welke Java-versies (JVM provided by JRE or JDK).
Eens je weet welke versie van Gradle je wil gebruiken, kan je de lokale gradlew updaten met het volgende commando: ./gradlew wrapper --gradle-version x.x (bijvoorbeeld 8.13 voor Java versies 23 of ouder).
Kijk na of je de gewenste versie gebruikt met ./gradlew --version.
Tip
Soms is je Java-versie te nieuw om een oude Gradle-wrapper uit te voeren en de Gradle-versie te updaten (bijvoorbeeld JDK 17 met Gradle 6.7 in plaats van 7.0 of nieuwer). Je krijgt dan een foutmelding zoals General error during conversion: Unsupported class file major version 65 of Could not initialize class `org.codehaus.groovy.reflection.ReflectionCache` .
Om dit op te lossen, kan je de te gebruiken versie van Gradle ook rechtstreeks specifiëren in gradle/wrapper/gradle-wrapper.properties, door het versienummer in de distributionUrl aan te passen.
Gradle laat je soms zelf ook wel weten naar welke versie je moet updaten aan de hand van je gebruikte Java code.
Bij het uitvoeren van gradlew gebeurt hetvolgende:
De wrapper downloadt de juiste versie van Gradle zelf (dus installatie van Gradle via snap/SDKMAN!/… is niet nodig voor een bestaand Gradle-project), afhankelijk van de specificaties in de properties file.
Vervolgens downloadt Gradle de juiste libraries om het project te kunnen compilen en uitvoeren.
Aan deze wrapper kan je commando’s meegeven (gradle tasks, zie later). Bijvoorbeeld, ./gradlew run om je programma (te compileren en) uit te voeren:
Dit is exact hetzelfde als in een IDE zoals IntelliJ het project runnen met de knop ‘Run’ (play-knop):
Ontleding van build.gradle
De belangrijkste file voor Gradle is de build.gradle file die zich in de ./app directory bevindt. Die ziet er als volgt:
/* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java application project to get you started.
* For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle
* User Manual available at https://docs.gradle.org/7.2/userguide/building_java_projects.html
*/plugins{// Apply the application plugin to add support for building a CLI application in Java.
id'application'}repositories{// Use Maven Central for resolving dependencies.
mavenCentral()}dependencies{// Use JUnit test framework.
testImplementation'junit:junit:4.13.2'// This dependency is used by the application.
implementation'com.google.guava:guava:30.1.1-jre'}application{// Define the main class for the application.
mainClass='be.ses.my_application.App'}
Met de Groovy syntax definiëren we verschillende configuratie-blokken als bloknaam { ... }. We onderscheiden volgende blokken:
plugins: hier kan je plugins voor Gradle toevoegen, je voegt ze toe op basis van id
id 'application': de plugin voor Java (stand-alone) applicaties. Dit voegt taken toe zoals build en test voor je applicatie.
repositories: hiermee specificeer je waar de dependencies in de dependencies functie gezocht en gedownload moeten worden. Meestal gebruik je hier de default, namelijk de standaard maven central repository (ingebouwde URL).
dependencies: hiermee specificeer je de dependencies voor je project. Dependencies worden toegevoegd met een zogenaamde dependency configuration, die aangeeft wanneer ze nodig zijn. De meest voorkomende zijn:
implementation (productie dependencies): deze dependencies zijn beschikbaar bij het compileren en uitvoeren van alle code.
testImplementation (test dependencies): deze dependencies zijn enkel beschikbaar bij het compileren en uitvoeren van test-code.
runtimeOnly: deze dependencies worden enkel gebruikt bij het uitvoeren van de applicatie.
Als argument moet je de dependency opgeven in de vorm groep:naam:versie. Je vindt deze meestal makkelijk terug op de gebruikte repository.
application: met mainClass = 'be.ses.my_application.App' geef je aan welke main-methode van welke klasse moet gerund worden wanneer je je applicatie wil runnen. (Dit is dus het entrypoint van je applicatie)
Dependencies toevoegen
In het kort volg je volgende procedure als je Gradle je dependencies laat beheren:
Bij het uitvoeren van gradle downloadt Gradle automatisch de juiste opgegeven versie van de Gson library en gebruikt die om je applicatie te compileren en uit te voeren.
De download komt niet terecht in je project maar in een gedeelde cache-folder, zodat elke versie van een library slechts éénmaal gedownload wordt op je systeem.
Die cache-folder staan in een submap van je home folder: ~/.gradle. Dit kan je controleren door ls ~/.gradle/caches/modules-2/files-2.1/, waar je nu dus ook de com.google.code.gson-directory terug vindt. Met tree ~/.gradle/caches/modules-2/files-2.1/com.google.code.gson kan je eens inspecteren hoe die directory er juist uitziet. (Indien je tree nog niet geïnstalleerd hebt doe dat dan eerst!)
Deze cache-folder kan groeien naarmate je meer met Gradle werkt en meer versies van libraries downloadt. Je mag deze cache-folder gerust verwijderen van je systeem; de volgende keer zullen de nodige dependencies gewoon opnieuw gedownload worden.
De voordelen van het gebruik van Gradle voor dependencies zijn dus:
Het zoeken van libraries beperkt zich tot één centrale (Maven Repository) website, waar alle verschillende versies duidelijk worden vermeld.
Het downloaden van libraries beperkt zich tot één centrale locatie op je harde schijf (~/.gradle/caches/modules-2/files-2.1/): 10 verschillende Java projecten die gebruik maken van Gson vereisen linken naar dezelfde gradle bestanden. Je hebt dus geen 10 kopieën nodig van de Gson.jar.
Het beheren van dependencies en versies beperkt zich tot één centraal configuratiebestand: build.gradle. Dit is (terecht) een integraal deel van het project!
Er bestaan twee types van dependencies: directe (soms ook harde dependency genoemd) en transitieve (zachte dependency).
Een directe dependency is een afhankelijkheid die het project nodig heeft, zoals het gebruik van Gson, waarbij dit in de dependencies {} config zit.
Een transitieve of indirecte dependency is een dependency van een dependency. In een van de oefeningen hieronder maken we een project (1) aan, dat een project (2) gebruikt, dat Gson gebruikt. In project 1 is project 2 een directe dependency, en Gson een transitieve. In Project 2 is Gson een directe dependency (en komt project 1 niet voor):
graph LR;
A[Project 1]
B[Project 2]
C[Gson]
A --> B
B --> C
A -.-> C
Deze transitieve dependencies worden afgehandeld door Groovy. Het is niet nodig om ze toe te voegen aan je dependencies {} configuratie.
Meer zelfs, als je fouten krijgt gerelateerd aan dependencies, is het geen goed idee om de zachte (transitieve) dependency (stippellijn) te veranderen in een harde, door die als directe dependency toe te voegen in de configuratie. Gradle biedt hier betere alternatieven voor.
Gradle tasks
./gradlew tasks --all voorziet een overzicht van alle beschikbare taken voor een bepaald Gradle project, opgesplitst per fase (build tasks, build setup tasks, documentation tasks, help tasks, verification tasks). Gradle plugins voorzien vaak extra tasks, zoals bijvoorbeeld de maven plugin om te publiceren naar repositories.
Belangrijke taken zijn onder andere:
test: voer alle unit testen uit. Een rapport hiervan is beschikbaar op build/reports/tests/test/index.html. (Hier komen we zeker nog op terug in het hoofdstuk rond test-driven development)
clean: verwijder alle binaries en metadata.
build: compile en test het project.
publish: (maven plugin) publiceer de gebouwde versie naar een repository.
jar: compile en package in een jar bestand
javadoc: (plugin) genereert HTML javadoc. Een rapport hiervan is beschikbaar op build/docs/javadoc/index.html.
Onderstaande screenshot is een voorbeeld van een Unit Test HTML rapport dat gegenereerd wordt elke keer de test task uitgevoerd wordt:
Compile to JAR
Zoals we in het hoofdstuk rond build systems in Java gezien hebben kan je alle bestanden groeperen in een .jar bestand om er een ’executable’ van te maken die door iemand anders uitgevoerd kan worden op een JVM. Om zo’n jar te maken gebruik je het commando ./gradlew jar. Er wordt dan een main_classname.jar aangemaakt in je /app/build/libs directory.
Zonder extra opties in je build.gradle wordt er echter geen 'Main-Class'-attribuut toegevoegd aan de MANIFEST file van je jar. Daarom kan je volgende optie instellen in je build.gradle waarbij application.mainClass een referentie is naar de value die je daar hebt ingesteld:
jar{manifest{attributes('Main-Class':application.mainClass// dit maakt een verwijzing naar de mainClass eigenschap die je in het application {} blok hebt ingesteld
)}archiveBaseName='myJarName'// enkel als je de naam van de gegenereerde jar wil wijzigen
}
Weetje
We kunnen ook zelf een kleine task coderen in Groovy die simpelweg de ‘group’ van ons project uitprint:
taskexample{printlnproject.group}
We gebruiken deze dan via ./gradlew example.
Fat jars (shadow jars)
Merk op dat een typisch project dat gebouwd of uitgevoerd wordt via Gradle geen jars van de dependencies bevat. Die worden immers automatisch door Gradle gedownload en in de juiste map (de Gradle cache) geplaatst.
Bij het maken van een jar van je applicatie zal Gradle standaard de dependencies dus niet mee in jouw jar file steken. Dat is logisch: Gradle geeft de voorkeur aan het scheiden van de applicatiecode en de dependencies. Hierdoor blijft de jar file kleiner en wordt het eenvoudiger om dependencies te beheren en bij te werken.
Wil je nu toch dat een (eind)gebruiker bijvoorbeeld niet zelf aan dependency management via Gradle moet doen, dan is het mogelijk om ook toch de applicatie en alle dependencies te bundelen in één grote jar file. Dit wordt een uber jar, fat jar, of shadow jar genoemd. De gebruiker heeft dan alles om je code uit te voeren zolang hij/zij over de juiste versie van de JVM beschikt.
Je kan Gradle zo een shadowJar laten aanmaken op deze manier:
Voeg de Shadow plugin toe aan je build.gradle bestand:
Bouw je project met de shadow jar taak: ./gradlew shadowJar
Hieronder zie je het verschil tussen compileren tot een shadowJar of een gewone jar:
arne@LT3210121:~/ses/depgradle/app/build/libs$ ls
app-1.0-SNAPSHOT-shadow.jar app-1.0-SNAPSHOT.jar
# Trying to run normal jar via cli manuallyarne@LT3210121:~/ses/depgradle/app/build/libs$ java -jar app-1.0-SNAPSHOT.jar
Exception in thread "main" java.lang.NoClassDefFoundError: com/google/gson/Gson
at be.ses.depgradle.App.main(App.java:10)Caused by: java.lang.ClassNotFoundException: com.google.gson.Gson
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526) ... 1 more
# Trying to run shadowJar via cli manuallyarne@LT3210121:~/ses/depgradle/app/build/libs$ java -jar app-1.0-SNAPSHOT-shadow.jar
1
Aangezien we toch meestal gebruik gaan maken van build tools zoals Gradle die steeds zelf de nodige dependecies download en toevoegt is het gebruik van de shadowJar in deze cursus echter beperkt
Meer output
De standaard output geeft enkel weer of er iets gelukt is of niet:
Je kan meer informatie krijgen met de volgende parameters:
--info, output LogLevel INFO. Veel irrelevante info wordt ook getoond.
--warning-mode all, toont detail informatie van warning messages
--stacktrace, toont de detail stacktrace bij exceptions
Repositories
Zoals eerder vermeld gebruikt Gradle Maven-repositories om de bestanden (denk: de .jar) van alle opgelijste dependencies op te halen.
We bekijken hier hoe je extra repositories kan toevoegen, wat in zo’n repository zit, en hoe je zelf kan publiceren naar een repository.
Extra repositories toevoegen
Veelgebruikte libraries zijn eenvoudig te vinden via de Central Maven Repository. Wanneer echter een eigen library werd gecompileerd, die dan in andere projecten worden gebruikt, schiet deze methode tekort: interne libraries zijn uiteraard niet op een publieke server gepubliceerd.
mavenCentral(), jcenter(), en google() zijn ingebouwde repositories. Eigen Maven folders en URLs toevoegen kan ook.
Tenslotte kan je ook een lokale folder toevoegen:
repositories{flatDir{dirs'lib'}}
Door nu de nodige .jar files toe te voegen aan de folder ./app/lib kunnen de juiste dependencies ook gevonden worden. Indien de ./app/lib directory nog niet bestaat, ga je die eerst moeten toevoegen.
Deze laatste manier is vooral handig wanneer je een library (.jar) wil gebruiken die niet via een repository beschikbaar zijn. Zo kan je deze dependencies toch nog via Gradle beheren.
Een jar uit zo’n flatDir repository als dependency toevoegen doe je als volgt:
Klik op ‘View All’ bij de Gson module op de MVN Central Repo om te inspecteren welke bestanden typisch worden aangeleverd in een Maven repository:
De library zelf, in een bepaalde versie (gson-2.12.1.jar).
Eventueel de javadoc en/of sources als aparte jars (gson-2.12.1-javadoc.jar, gson-2.12.1-sources.jar).
Een .pom XML bestand (gson-2.12.1.pom).
metadata over het build-proces(gson-2.12.1.buildinfo)
checksums (md5 en sha1) en digitale handtekeningen (asc) op alle vorige bestanden
Het .pom XML bestand beschrijft welke afhankelijkheden deze module op zich heeft. Zo kan een hele dependency tree worden opgebouwd! Het beheren van alle afhankelijkheden is complexer dan op het eerste zicht lijkt, en laat je dus beter over aan deze gespecialiseerde tools. Google heeft voor Gson enkel Junit als test dependency aangeduid:
Grote projecten kunnen makkelijk afhankelijk zijn van tientallen libraries, die op hun beurt weer afhankelijk zijn van meerdere andere libraries.
Hieronder een voorbeeld van een dependency tree voor een typische grote webapplicatie geschreven in Java:
Je kan deze opvragen via Gradle met de de task dependencies: ./gradlew dependencies. Detailinformatie voor specifieke dependencies kunnen worden opgevraagd met de dependencyInsight task. Zie ook: Viewing and debugging dependencies in de Gradle docs.
Publiceren naar een Maven Repository
Gradle voorziet een plugin genaamd ‘maven-publish’ die bovenvermelde bestanden automatisch aanmaakt, zodat je jouw project kan uploaden naar een online repository, zoals Maven central, of een lokale maven repository. Op die manier kunnen andere projecten jouw project dan weer als dependency gaan gebruiken. Activeer de plugin en voeg een publishing block toe met de volgende eigenschappen:
plugins{id'java'id'maven-publish'// toevoegen!
}group='be.ses'version='1.0-SNAPSHOT'publishing{publications{maven(MavenPublication){groupId=project.group.toString()version=versionartifactId='projectnaam'fromcomponents.java}}repositories{maven{url="/home/arne/local-maven-repo"// gebruik je eigen home-folder!
}}}
Indien die directory nog niet bestaat wordt deze aangemaakt!
Windows-gebruikers dienen in de url value te werken met dubbele backslashes (\\) in plaats van forward slashes (/) om naar het juiste pad te navigeren.
Deze uitbreiding voegt de target publish toe aan Gradle. Dus: ./gradlew publish publiceert de nodige bestanden in de aangegeven folder.
De aangemaakte lokale Maven repository ziet er nu zo uit:
Een Gradle project dat nu gebruik wilt maken van de libraries in die lokale Maven repository dient enkel een tweede Maven repository plaats te definiëren:
repositories{mavenCentral()maven{url="/home/arne/local-maven-repo"// gebruik je eigen home-folder!
}}
In de praktijk ga je natuurlijk eerder een gedeelde (al dan niet interne) locatie gebruiken als repository, en geen map op je eigen harde schijf.
Dat laatste doet immers veel van de voordelen van het gebruik van Gradle teniet.
Oefeningen
Oefening 1: Hoger lager
Maak een nieuw Gradle project aan met de naam: higher_lower en gebruik de packagenaam be.ses.higher_lower.
Hint
Om een packagenaam in te stellen in recentere versies van gradle, moet je het --package argument meegeven:
gradle init --package be.ses.higher_lower.
Delete de test in app/src/test/java/be/ses/higher_lower/App.java. Anders zal je je project niet kunnen runnen.
Aangezien we input aan de gebruiker gaan vragen moeten we een extra optie instellen in de app/build.gradle:
run{standardInput=System.in}
Kopieer onderstaande code voor de klasse app/src/main/java/be/ses/higher_lower/App.java:
App.java (klik om te tonen)
packagebe.ses.higher_lower;importjava.util.Random;importjava.util.Scanner;publicclassApp{publicstaticvoidmain(String[]args){Scannerscanner=newScanner(System.in);Randomrandom=newRandom();System.out.println("Welcome to the Higher or Lower game!");System.out.println("Guess if the next number will be higher or lower.");intcurrentNumber=random.nextInt(100)+1;booleanplaying=true;intscore=0;while(playing){System.out.println("Current number: "+currentNumber);System.out.print("Will the next number be higher or lower? (h/l): ");Stringguess=scanner.next().toLowerCase();intnextNumber=random.nextInt(100)+1;if((guess.equals("h")&&nextNumber>currentNumber)||(guess.equals("l")&&nextNumber<currentNumber)){System.out.println("Correct! The next number was: "+nextNumber);score++;}else{System.out.println("Wrong! The next number was: "+nextNumber);playing=false;}currentNumber=nextNumber;}System.out.println("Game over! Your final score: "+score);scanner.close();}}
Test je programma: Aangezien we input vragen via de scanner moeten we ook op een speciale manier ons project runnen: gebruik ./gradlew --console plain run (of ./gradlew -q --console plain run om helemaal geen output van gradle tasks ertussen te zien.)
Maak een jar via Gradle.
Copy de app/build/libs/app.jar-file naar een andere directory en hernoem naar higherLower.jar.
Run de jar via de terminal: java -jar higherLower.jar. Je krijgt een foutmelding gerelateerd aan het niet vinden van een main-klasse. Los die op (zie de uitleg hierboven) en probeer opnieuw.
Oefening 2: Scorebord-library
Ontwerp een eenvoudige library genaamd ‘scorebord’ die scores kan bijhouden voor bordspelletjes. Deze library kan dan gebruikt worden door toekomstige digitale Java bordspellen (en zal gebruikt worden door onze hoger-lager app).
In een Scorebord kan je spelers toevoegen door middel van een naam en een score. Er is een mogelijkheid om de huidige score van een speler op te vragen, en de winnende speler. Deze gegevens worden met behulp van Gson in een json bestand bewaard, zodat bij het heropstarten van een spel de scores behouden blijven.
packagebe.ses.scorebord;importjava.io.IOException;importjava.nio.file.Files;importjava.nio.file.Path;importjava.util.ArrayList;importjava.util.Comparator;importjava.util.List;importjava.util.NoSuchElementException;importcom.google.gson.Gson;publicclassScorebord{privateList<SpelerScore>spelerScores=newArrayList<>();publicvoidsetScore(Stringname,intcurrentScore){spelerScores.add(newSpelerScore(name,currentScore));spelerScores.sort(Comparator.comparing(ss->ss.getScore()));}publicintgetScoreOf(Stringname){for(vars:spelerScores){if(s.getName().equals(name)){returns.getScore();}}thrownewNoSuchElementException("No player with that name");}publicStringgetWinner(){SpelerScorewinner=null;for(varss:spelerScores){if(winner==null||ss.getScore()>winner.getScore()){winner=ss;}}returnwinner.getName();}@OverridepublicStringtoString(){Stringresult="";for(varss:spelerScores){result+=ss.getName()+": "+ss.getScore()+"\n";}returnresult;}publicvoidsaveToJson(Stringfilename)throwsIOException{vargson=newGson();varjson=gson.toJson(this);Files.write(Path.of("scorebord.json"),json.getBytes());}publicstaticScorebordgetScorebordFromJson(Stringfilename)throwsIOException{if(!Files.exists(Path.of(filename))){returnnewScorebord();}vargson=newGson();varjson=Files.readString(Path.of("scorebord.json"));varscorebord=gson.fromJson(json,Scorebord.class);returnscorebord;}}
De klasse SpelerScore is een intern hulpmiddel om te serialiseren van/naar JSON. Deze klasse wordt gebruikt in de implementatie van Scorebord.
Maak via de CLI een nieuw Gradle - Java project. Groupid: be.ses. Geef je project de naam: scorebord.
Configureer Gradle zodat het commando gradlew jar een bestand scorebord-1.0.jar creëert in de build/libs folder.
Tip: je update best je gradle wrappen naar versie 8.13 of hoger.
Oefening 3: Hoger lager met scorebord
Voeg dat bovenstaand scorebord project als een library toe aan higher_lower-applicatie uit de eerste oefening. Kopieer de scorebord-1.0.jar-jarfile lokaal in een ./app/lib folder in je higher_lower project en instrueer Gradle zo dat dit als flatDir repository wordt opgenomen (zie boven). Update nu ook de App klasse in de higher_lower-applicatie:
Nieuwe App.java
packagebe.ses.higher_lower;importjava.io.IOException;importjava.util.Random;importjava.util.Scanner;importbe.ses.scorebord.Scorebord;publicclassApp{publicstaticvoidmain(String[]args)throwsIOException{Scannerscanner=newScanner(System.in);Randomrandom=newRandom();Scorebordscorebord=Scorebord.getScorebordFromJson("highscores.json");System.out.println("Welcome to the Higher or Lower game!");System.out.println("This is the current leaderboard: \n"+scorebord+"\n\n");System.out.println("Guess if the next number will be higher or lower.");intcurrentNumber=random.nextInt(100)+1;booleanplaying=true;intscore=0;while(playing){System.out.println("Current number: "+currentNumber);System.out.print("Will the next number be higher or lower? (h/l): ");Stringguess=scanner.next().toLowerCase();intnextNumber=random.nextInt(100)+1;if((guess.equals("h")&&nextNumber>currentNumber)||(guess.equals("l")&&nextNumber<currentNumber)){System.out.println("Correct! The next number was: "+nextNumber);score++;}else{System.out.println("Wrong! The next number was: "+nextNumber);playing=false;}currentNumber=nextNumber;}System.out.println("Game over! Your final score: "+score);System.out.println("What is your name?");Stringname=scanner.next();scorebord.setScore(name,score);System.out.println("This is the new leaderboard: \n"+scorebord+"\n\n");scorebord.saveToJson("highscores.json");scanner.close();}}
Als de dependencies goed liggen, kan je een nieuw Scorebord aanmaken, en herkent VSCode dit met CTRL+Space. Hieronder een voorbeeld van Gson:
Gradle extention for VSCode Ctrl+space
Als we het project uitvoeren, werkt dit echter niet: we krijgen een foutmelding bij het opslaan.
Dat komt omdat we een library gebruiken (scorebord), die op zijn beurt een library gebruikt (Gson).
Maar de Gson dependency is niet in onze huidige Gradle file gedefinieerd.
Om dit op te lossen, hebben we 3 opties:
zelf een dependency naar Gson toevoegen. Dat is echter ten zeerste af te raden, omdat we zo het automatische dependencybeheer via Gradle omzeilen.
een shadow jar/fat jar maken van scorebord. Dat is echter ook niet de meest aangewezen manier, opnieuw omdat we ingaan tegen de bedoeling van Gradle.
overschakelen naar een lokale Maven repository waarnaar we onze scorebord-library publiceren (zie eerder). Dan worden ook transitieve dependencies automatisch ingeladen. Verwijder de flatDir en voeg een lokale maven URL toe. Publiceer het scorebord project naar diezelfde URL volgens de instructies van de maven-publish plugin.
Denkvragen
Hoe zou je transitieve dependencies handmatig kunnen beheren? Wat zijn de voor- en nadelen?
Wat gebeurt er als project1-1.0 afhankelijk is van lib1-1.0 en lib1-2.0, en lib1-1.0 van lib2-1.0 - een oudere versie dus?
Als ik publiceer naar een lokale folder, welke bestanden zijn dan absoluut noodzakelijk voor iemand om mijn library te kunnen gebruiken?
In C (met CMake)
CMake
CMake is een open-source tool dat wordt gebruikt voor het beheren van de build processen van softwareprojecten in C en C++. Het biedt een platformonafhankelijke manier om build configuraties te genereren, wat betekent dat het kan worden gebruikt op verschillende besturingssystemen zoals Windows, macOS en Linux. CMake maakt gebruik van eenvoudige configuratiebestanden, genaamd CMakeLists.txt (soms meerdere van deze files in je project, je kan namelijk in meerdere directories zo een CMakeLists.txt file aanmaken), waarin de projectstructuur en de dependencies worden gedefinieerd. Deze configuratiebestanden worden vervolgens gebruikt om platformspecifieke buildsystemen te genereren, zoals Makefiles op Unix-achtige systemen (of Visual Studio-projectbestanden op Windows).
Een van de belangrijkste voordelen van CMake ten opzichte van het gebruik van alleen Makefiles is de verbeterde ondersteuning voor dependency management. Met Makefiles moet je handmatig de afhankelijkheden tussen bestanden en bibliotheken beheren, wat foutgevoelig en tijdrovend kan zijn. CMake automatiseert dit proces door dependencies automatisch te detecteren en te beheren. Dit zorgt ervoor dat alleen de noodzakelijke onderdelen opnieuw worden opgebouwd wanneer er wijzigingen worden aangebracht, wat de build tijd aanzienlijk kan verkorten.
Daarnaast biedt CMake een aantal geavanceerde functies die het beheren van dependencies verder vereenvoudigen. Zo ondersteunt CMake het gebruik van externe packages en libraries via de find_package-functie, waarmee je eenvoudig externe afhankelijkheden kunt integreren in je project. Ook biedt CMake ondersteuning voor moderne C++-functionaliteiten zoals target-gebaseerde builds, waarbij je specifieke build opties en dependencies kunt toewijzen aan individuele targets in plaats van aan het hele project. Hierdoor wordt het beheer van complexe projecten met meerdere componenten een stuk eenvoudiger en overzichtelijker. Deze functionaliteiten vallen echter buiten de scope van deze cursus.
Kortom, CMake biedt een krachtig en flexibel alternatief voor traditionele Makefiles, met name op het gebied van dependency management. Het automatiseert en vereenvoudigt het beheer van dependencies.
Je kan CMake simpel installeren op je WSL met behulp van volgend commando: sudo apt install cmake -y.
‘CMakeLists.txt’ en project structure
CMake werkt op basis van een CMakeLists.txt. Daarin specificeer je bepaalde instellingen over de structuur van je project en hoe je project juist gebuild en gelinkt moet worden. Verder kan je net als in Makefiles ook gebruik maken van variabelen om het proces nog meer te automatiseren. Je maakt volgens conventie ook een ./build directory aan waarin dan alle build-files terecht zullen komen. Een van die build-files in een door CMake gegenereerde make file waarmee je dan simpelweg via het commando make je project effectief kan builden naar een binary die je kan uitvoeren. Hieronder bekijken we hoe zo een ‘CMakeLists.txt’ eruit ziet in de root van je project en welke functionaliteiten je hebt.
We starten met een zeer simpel project met een main.c-file, een helloworld.c-file en een helloworld.h-file:
Klik hier om de code te zien/verbergen voor het kleine voorbeeld🔽
Een van de eenvoudigste CMakeLists.txt ziet er dan als volgt uit:
# Specify the minimum required version of CMake you need to use with this project. (Simply use your own version of CMake)cmake_minimum_required(VERSION 3.28)# Specify the project nameproject(myproject)# Specify: the name of the binary to build, the source files needed to build the binaryadd_executable(myproject.bin main.c helloworld.c)
Nu voeren we volgens conventie het cmake commando uit in de build-directory zodat alle nodige build files, waaronder de Makefile aangemaakt worden in die directory: cd ./build && cmake ..
Dat resulteert in volgende files en folders in de build-directory waarvan we eigenlijk enkel geinteresserd zijn in de Makefile:
Klik hier om de code te zien/verbergen voor de contents in de Makefile. Het fijne is nu dat je niet perfect moet weten wat er gebeurt omdat we die verantwoordelijkheid nu bij CMake gelegd hebben🔽
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 3.28
# Default target executed when no arguments are given to make.
default_target:all.PHONY :default_target# Allow only one "make -f Makefile2" at a time, but pass parallelism.
.NOTPARALLEL:#=============================================================================
# Special targets provided by cmake.
# Disable implicit rules so canonical targets will work.
.SUFFIXES:# Disable VCS-based implicit rules.
% : %,v# Disable VCS-based implicit rules.
% :RCS/%
# Disable VCS-based implicit rules.
% :RCS/%,v# Disable VCS-based implicit rules.
% :SCCS/s.%
# Disable VCS-based implicit rules.
% :s.%
.SUFFIXES: .hpux_make_needs_suffix_list# Command-line flag to silence nested $(MAKE).
$(VERBOSE)MAKESILENT= -s
#Suppress display of executed commands.
$(VERBOSE).SILENT:# A target that is always out of date.
cmake_force:.PHONY :cmake_force#=============================================================================
# Set environment variables for the build.
# The shell in which to execute make rules.
SHELL= /bin/sh
# The CMake executable.
CMAKE_COMMAND= /usr/bin/cmake
# The command to remove a file.
RM= /usr/bin/cmake -E rm -f
# Escaping for special characters.
EQUALS==# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR= /home/arne/ses/cmake_example
# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR= /home/arne/ses/cmake_example/build
#=============================================================================
# Targets provided globally by CMake.
# Special rule for the target edit_cache
edit_cache: @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "No interactive CMake dialog available..." /usr/bin/cmake -E echo No\ interactive\ CMake\ dialog\ available.
.PHONY :edit_cache# Special rule for the target edit_cache
edit_cache/fast:edit_cache.PHONY :edit_cache/fast# Special rule for the target rebuild_cache
rebuild_cache: @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..." /usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR).PHONY :rebuild_cache# Special rule for the target rebuild_cache
rebuild_cache/fast:rebuild_cache.PHONY :rebuild_cache/fast# The main all target
all:cmake_check_build_system$(CMAKE_COMMAND) -E cmake_progress_start /home/arne/ses/cmake_example/build/CMakeFiles /home/arne/ses/cmake_example/build//CMakeFiles/progress.marks
$(MAKE)$(MAKESILENT) -f CMakeFiles/Makefile2 all
$(CMAKE_COMMAND) -E cmake_progress_start /home/arne/ses/cmake_example/build/CMakeFiles 0.PHONY :all# The main clean target
clean:$(MAKE)$(MAKESILENT) -f CMakeFiles/Makefile2 clean
.PHONY :clean# The main clean target
clean/fast:clean.PHONY :clean/fast# Prepare targets for installation.
preinstall:all$(MAKE)$(MAKESILENT) -f CMakeFiles/Makefile2 preinstall
.PHONY :preinstall# Prepare targets for installation.
preinstall/fast:$(MAKE)$(MAKESILENT) -f CMakeFiles/Makefile2 preinstall
.PHONY :preinstall/fast# clear depends
depend:$(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1.PHONY :depend#=============================================================================
# Target rules for targets named myproject.bin
# Build rule for target.
myproject.bin:cmake_check_build_system$(MAKE)$(MAKESILENT) -f CMakeFiles/Makefile2 myproject.bin
.PHONY :myproject.bin# fast build rule for target.
myproject.bin/fast:$(MAKE)$(MAKESILENT) -f CMakeFiles/myproject.bin.dir/build.make CMakeFiles/myproject.bin.dir/build
.PHONY :myproject.bin/fasthelloworld.o:helloworld.c.o.PHONY :helloworld.o# target to build an object file
helloworld.c.o:$(MAKE)$(MAKESILENT) -f CMakeFiles/myproject.bin.dir/build.make CMakeFiles/myproject.bin.dir/helloworld.c.o
.PHONY :helloworld.c.ohelloworld.i:helloworld.c.i.PHONY :helloworld.i# target to preprocess a source file
helloworld.c.i:$(MAKE)$(MAKESILENT) -f CMakeFiles/myproject.bin.dir/build.make CMakeFiles/myproject.bin.dir/helloworld.c.i
.PHONY :helloworld.c.ihelloworld.s:helloworld.c.s.PHONY :helloworld.s# target to generate assembly for a file
helloworld.c.s:$(MAKE)$(MAKESILENT) -f CMakeFiles/myproject.bin.dir/build.make CMakeFiles/myproject.bin.dir/helloworld.c.s
.PHONY :helloworld.c.smain.o:main.c.o.PHONY :main.o# target to build an object file
main.c.o:$(MAKE)$(MAKESILENT) -f CMakeFiles/myproject.bin.dir/build.make CMakeFiles/myproject.bin.dir/main.c.o
.PHONY :main.c.omain.i:main.c.i.PHONY :main.i# target to preprocess a source file
main.c.i:$(MAKE)$(MAKESILENT) -f CMakeFiles/myproject.bin.dir/build.make CMakeFiles/myproject.bin.dir/main.c.i
.PHONY :main.c.imain.s:main.c.s.PHONY :main.s# target to generate assembly for a file
main.c.s:$(MAKE)$(MAKESILENT) -f CMakeFiles/myproject.bin.dir/build.make CMakeFiles/myproject.bin.dir/main.c.s
.PHONY :main.c.s# Help Target
help: @echo "The following are some of the valid targets for this Makefile:" @echo "... all (the default if no target is provided)" @echo "... clean" @echo "... depend" @echo "... edit_cache" @echo "... rebuild_cache" @echo "... myproject.bin" @echo "... helloworld.o" @echo "... helloworld.i" @echo "... helloworld.s" @echo "... main.o" @echo "... main.i" @echo "... main.s".PHONY :help#=============================================================================
# Special targets to cleanup operation of make.
# Special rule to run CMake to check the build system integrity.
# No rule that depends on this can have commands that come from listfiles
# because they might be regenerated.
cmake_check_build_system:$(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0.PHONY :cmake_check_build_system
Nu gaan we van die Makefile gebruik maken om met het make-commando de binary effectief te builden: make. (zorg er natuurlijk wel voor dat je in de ./build-directory zit!)
Er is nu een binary verschenen in de ./build-directory met de naam zoals gespecificeerd in de CMakeLists.txt-file, die je onmiddellijk kan uitvoeren.
Dependency management met CMake en variables
We kunnen nu de CMake file aanpassen zodat de binary automatisch de naam van het project gebruikt door standaard variabelen te gebruiken:
Je kan CMake ook wat zelf wat extra info text laten printen met
message("Hallo vanuit CMake je CMAKE_PROJECT_NAME is '${CMAKE_PROJECT_NAME}'")
Verder kunnen we de helloworld.c en helloworld.h files beschouwen als een kleine internal dependency en kunnen we deze dus best in een soort ./libs/helloworld-directory steken. Dan moeten we ook wel onze CMakeLists.txt-file aanpassen en BELANGRIJK in die ./libs/helloworld-directory moeten we ook een eigen CMakeLists.txt-file aanmaken waarin je definieert hoe die dependency juist gebuild en gelinkt moet worden. Onze project folder ziet er nu dan als volgt uit:
Klik hier om de code te zien/verbergen voor ./CMakeLists.txt🔽
# Specify the minimum required version of CMake you need to use with this project. (Simply use your own version of CMake)cmake_minimum_required(VERSION 3.28)# Specify the project nameproject(myproject)# Add the helloworld directory to the projectadd_subdirectory(libs/helloworld)# Specify: the name of the binary to build, the source files needed to build the binary# You don't need to specify the helloworld.c file here anymore, see belowadd_executable(${CMAKE_PROJECT_NAME}.bin main.c)message("Hallo vanuit CMake je CMAKE_PROJECT_NAME is '${CMAKE_PROJECT_NAME}'")# Link internal library 'helloworld' to the main binarytarget_link_libraries(${CMAKE_PROJECT_NAME}.bin PRIVATE helloworld)
Belangrijk: Merk op dat we de directory naar de library moeten toevoegen via add_subdirectory(), we niet langer de helloworld.c moeten vermelden bij de add_executable()-functie omdat we dit nu aangeven met de functie target_link_libraries():
target_link_libraries: Dit is een CMake-commando dat wordt gebruikt om libraries te koppelen aan een specifieke target. Een target kan een executable of een library zijn die je wilt builden.
${CMAKE_PROJECT_NAME}.bin: Dit is de naam van de target waaraan je de library wilt koppelen. ${CMAKE_PROJECT_NAME} is een variabele die de naam van het project bevat, en .bin is een extension die aangeeft dat het om een executable/binary bestand gaat.
PRIVATE: Dit is een eigenschap die aangeeft hoe de gekoppelde library zichtbaar is voor andere targets. In dit geval betekent PRIVATE dat de library alleen zichtbaar is voor de target ${CMAKE_PROJECT_NAME}.bin en niet voor andere targets die afhankelijk zijn van deze target. Dit helpt om de dependencies te beperken en de build configuratie overzichtelijk te houden. Andere opties buiten ‘PRIVATE’ zijn: ‘PUBLIC’ en ‘INTERFACE’.
PUBLIC: Wanneer je een library met de PUBLIC-eigenschap koppelt, betekent dit dat de library zichtbaar is voor zowel de target zelf als voor alle targets die afhankelijk zijn van deze target. Dit is handig wanneer je wilt dat de dependency doorgegeven wordt aan andere targets die deze target gebruiken.
INTERFACE: Deze eigenschap geeft aan dat de library alleen zichtbaar is voor targets die afhankelijk zijn van de target, maar niet voor de target zelf. Dit is nuttig voor het definiëren van interface-afhankelijkheden die alleen relevant zijn voor afhankelijke targets.
PRIVATE: Alleen zichtbaar voor de target zelf.
helloworld: Dit is de naam van de library die je wilt koppelen aan de target. In dit geval is helloworld waarschijnlijk een library die je hebt gedefinieerd of geïmporteerd in je project.
./libs/helloworld/CMakeLists.txt
Klik hier om de code te zien/verbergen voor ./libs/helloworld/CMakeLists.txt🔽
# Specify the minimum required version of CMake you need to use with this project. (Simply use your own version of CMake)cmake_minimum_required(VERSION 3.28)# Create a sort of temp project that compiles so it can be linked to the main project AND specify it is a C projectproject(Helloworld C)# Now add the compiled project binary as a libraryadd_library(helloworld helloworld.c)# Include this directory's binary as a binary that can be used by the main project to link withtarget_include_directories(helloworld PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
Belangrijk: Deze CMakeLists.txt file bevat essentiële instructies die samen zorgen dat de interne dependency correct wordt opgebouwd en bruikbaar is voor het main project. Allereerst definieert de project(Helloworld C) regel een nieuw project met de naam “Helloworld” en specificeert dat het in de programmeertaal C is geschreven, waardoor CMake de juiste compiler en instellingen selecteert. Met add_library(helloworld helloworld.c) wordt een statische of dynamische bibliotheek aangemaakt op basis van de broncode in “helloworld.c”, zodat deze gecompileerde code later in het hoofdproject gelinkt kan worden. Tenslotte zorgt target_include_directories(helloworld PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) ervoor dat de directory waarin deze CMakeLists.txt en de headers staan, toegevoegd wordt aan de include-paden van alle targets die de “helloworld” library gebruiken. Hierdoor weet het main project waar de benodigde headers te vinden zijn, wat essentieel is voor een correcte compilatie en koppeling tussen de modules.
Voorbeeld met externe library
Voor dit hoofdstuk is er geen oefening voorzien, maar wel een voorbeeld analoog aan de oefening rond dependency management in Java. In dit geval hebben we een “Higher Lower C” project waarbij we de volgende structuur hebben. De entrypoint van onze applicatie is de main.c-file, de scorebord.c en scorebord.h kunnen gezien worden als een interne library en cJSON is een externe library/dependency. Hieronder vind je dan een mogelijke structuur voor ons project terug. Je kan dit project en bijhorende files ook downloaden met volgende github link.
Wil je de content van de source files bekijken, kan je dat hieronder. Het is in dit hoofdstuk vooral belangrijk dat je begrijpt wat de regels in de verschillende CMakeLists.txt betekenen en wat het voordeel is om het op deze manier te doen.
source code: files in root
Klik hier om de code te zien/verbergen voor CMakeLists.txt🔽
cmake_minimum_required(VERSION 3.28)project(HogerLagerC)# Gebruik de C99-standaardset(CMAKE_C_STANDARD 99)set(CMAKE_C_STANDARD_REQUIRED ON)add_subdirectory(external/cJSON)# Voeg de scorebord library toeadd_subdirectory(internal/scorebord)# Maak de executable van het speladd_executable(hoger_lager main.c)# Link de externe JSON-librarytarget_link_libraries(hoger_lager PRIVATE scorebord cjson)
Klik hier om de code te zien/verbergen voor main.c🔽
/* main.c - Hoger Lager spel */#include<stdio.h>#include<stdlib.h>#include<time.h>#include"scorebord.h"intmain(void){srand((unsigned)time(NULL));Scorebord*scorebord=load_scorebord("highscores.json");printf("Welkom bij het Hoger Lager spel!\n");printf("Dit is de huidige ranglijst:\n");print_scorebord(scorebord);printf("\nRaad of het volgende getal hoger of lager zal zijn.\n");intcurrentNumber=rand()%100+1;intscore=0;charchoice;while(1){printf("Huidig getal: %d\n",currentNumber);printf("Zal het volgende hoger (h) of lager (l) zijn? ");scanf(" %c",&choice);intnextNumber=rand()%100+1;if((choice=='h'&&nextNumber>currentNumber)||(choice=='l'&&nextNumber<currentNumber)){printf("Correct! Het volgende getal was: %d\n",nextNumber);score++;}else{printf("Fout! Het volgende getal was: %d\n",nextNumber);break;}currentNumber=nextNumber;}printf("Game over! Jouw eindscore: %d\n",score);printf("Wat is je naam? ");charname[50];scanf("%49s",name);add_score(scorebord,name,score);save_scorebord("highscores.json",scorebord);free_scorebord(scorebord);return0;}
source code: files in ./internal/scorebord
Klik hier om de code te zien/verbergen voor ./internal/scorebord/CMakeLists.txt🔽
cmake_minimum_required(VERSION 3.28)project(Scorebord C)include(FetchContent)FetchContent_Declare( cjson
GIT_REPOSITORY https://github.com/DaveGamble/cJSON.git
GIT_TAG v1.7.14 # Specify the version you want to use)FetchContent_MakeAvailable(cjson)add_library(scorebord scorebord.c)# Ensure cJSON include directories are correctly settarget_include_directories(scorebord PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}${cjson_SOURCE_DIR}${cjson_BINARY_DIR})# Link cJSON library to scorebordtarget_link_libraries(scorebord PRIVATE cjson)
Klik hier om de code te zien/verbergen voor ./internal/scorebord/scorebord.h🔽
#ifndef SCOREBORD_H
#define SCOREBORD_H
#include"player.h"typedefstruct{Player*players;intcount;intcapacity;}Scorebord;// Laadt de scorebordgegevens vanuit een JSON-bestand.
Scorebord*load_scorebord(constchar*filename);// Slaat de scorebordgegevens op in een JSON-bestand.
voidsave_scorebord(constchar*filename,Scorebord*sb);// Voegt een nieuwe score toe aan het scorebord.
voidadd_score(Scorebord*sb,constchar*name,intscore);// Print het scorebord naar de standaard output.
voidprint_scorebord(Scorebord*sb);// Geeft het geheugen van het scorebord vrij.
voidfree_scorebord(Scorebord*sb);#endif // SCOREBORD_H
Klik hier om de code te zien/verbergen voor ./internal/scorebord/scorebord.c🔽
#include<stdio.h>#include<stdlib.h>#include<string.h>#include"scorebord.h"#include"cJSON.h" // Externe JSON-library: cJSON// Hulpfunctie: maakt een nieuw Scorebord aan
staticScorebord*create_scorebord(){Scorebord*sb=malloc(sizeof(Scorebord));if(sb){sb->count=0;sb->capacity=10;// begin met ruimte voor 10 spelers
sb->players=malloc(sb->capacity*sizeof(Player));}returnsb;}Scorebord*load_scorebord(constchar*filename){FILE*fp=fopen(filename,"r");if(!fp){// Bestaat niet? Geef een nieuw scorebord terug.
returncreate_scorebord();}// Lees het hele bestand in een buffer
fseek(fp,0,SEEK_END);longfsize=ftell(fp);fseek(fp,0,SEEK_SET);char*data=malloc(fsize+1);fread(data,1,fsize,fp);fclose(fp);data[fsize]='\0';cJSON*json=cJSON_Parse(data);free(data);Scorebord*sb=create_scorebord();if(json){intarraySize=cJSON_GetArraySize(json);for(inti=0;i<arraySize;i++){cJSON*item=cJSON_GetArrayItem(json,i);cJSON*name=cJSON_GetObjectItem(item,"name");cJSON*score=cJSON_GetObjectItem(item,"score");if(cJSON_IsString(name)&&cJSON_IsNumber(score)){add_score(sb,name->valuestring,score->valueint);}}cJSON_Delete(json);}returnsb;}voidsave_scorebord(constchar*filename,Scorebord*sb){cJSON*json=cJSON_CreateArray();for(inti=0;i<sb->count;i++){cJSON*item=cJSON_CreateObject();cJSON_AddStringToObject(item,"name",sb->players[i].name);cJSON_AddNumberToObject(item,"score",sb->players[i].score);cJSON_AddItemToArray(json,item);}char*rendered=cJSON_Print(json);FILE*fp=fopen(filename,"w");if(fp){fputs(rendered,fp);fclose(fp);}free(rendered);cJSON_Delete(json);}voidadd_score(Scorebord*sb,constchar*name,intscore){if(sb->count>=sb->capacity){sb->capacity*=2;sb->players=realloc(sb->players,sb->capacity*sizeof(Player));}// Kopieer de naam (maximaal 49 karakters) en voeg de score toe
strncpy(sb->players[sb->count].name,name,sizeof(sb->players[sb->count].name)-1);sb->players[sb->count].name[sizeof(sb->players[sb->count].name)-1]='\0';sb->players[sb->count].score=score;sb->count++;}voidprint_scorebord(Scorebord*sb){// Eenvoudige weergave; je kunt hier sorteren op score toevoegen indien gewenst.
for(inti=0;i<sb->count;i++){printf("%s: %d\n",sb->players[i].name,sb->players[i].score);}}voidfree_scorebord(Scorebord*sb){if(sb){free(sb->players);free(sb);}}
Klik hier om de code te zien/verbergen voor ./internal/scorebord/player.h🔽
Dependency management in Python kan efficiënt worden afgehandeld met pip en venv. Deze tools helpen ervoor te zorgen dat je projecten de benodigde libraries hebben en dat verschillende projecten elkaar niet verstoren door conflicting dependencies.
pip is de package manager voor Python. Hiermee kun je extra libraries en dependencies installeren en beheren die niet zijn opgenomen in de standaardbibliotheek. Om een pakket te installeren met pip, kun je de volgende command prompt gebruiken:
pip install <package_name>
Je kunt ook de versie van het pakket specificeren die je wilt installeren:
pip install package_name==1.2.3
Om de dependencies van je project bij te houden, kun je een requirements.txt-bestand maken waarin alle packages en hun versies worden vermeld. Dit bestand kan worden gegenereerd met:
pip freeze > requirements.txt
Om de dependencies in requirements.txt dan ergens te installeren, kun je het volgende commando gebruiken:
pip install -r requirements.txt
venv is een module die ondersteuning biedt voor het maken van lichte, geïsoleerde Python-omgevingen. Je installeer de module opt je WSL via sudo apt install python3.12-venv -y. Venv is vooral handig wanneer je meerdere projecten hebt met verschillende dependencies en zeker bij meerdere projecten met verschillende versies van dezelfde dependencies. Om een virtuele omgeving (virtual environment = venv) te maken, kun je de volgende opdracht gebruiken (dit hoeft niet specifiek in de directory van je prject te zijn):
python3 -m venv <myenv_name>
Hiermee wordt een directory genaamd myenv_name gemaakt die een kopie van de Python-interpreter en een pip-executable bestand bevat. Om de virtuele omgeving te activeren, gebruik je:
source <myenv_name>/bin/activate
Info
Je kan venv ook op Windows gebruiken maar dan gebruik je het commando: <myenv_name>\Scripts\activate om de virtual environment te activeren.
Zodra de virtuele omgeving is geactiveerd, worden alle packages die met pip worden geïnstalleerd beperkt tot deze omgeving, zodat de dependencies van je project geïsoleerd blijven van andere projecten. Om de virtuele omgeving te deactiveren, voer je eenvoudigweg volgend commando uit:
deactivate
Je zal dan zien dat je CLI je dan ook toont in welke ‘venv’ je je bevindt:
(<myenv_name>) arne@LT3210121:~/myproject$
Door pip en venv te combineren, kun je effectief dependencies beheren, meerdere projectomgevingen simpel onderhouden en conflicten tussen verschillende projecten vermijden. Deze aanpak zorgt ervoor dat je projecten reproduceerbaar blijven en eenvoudig op verschillende systemen kunnen worden opgezet.
Voorbeeld ‘hoger lager’
Hieronder staat een voorbeeld van hoe je het ‘hoger lager’-project kunt inrichten als een Python-project met een virtual environment, een requirements.txt-file voor de externe JSON-library (via simplejson) en met de scorebord.py als interne dependency in een libs-directory. De projectstructuur ziet er dan als volgt uit:
Requirements.txt bevat nu enkel volgende regel: simplejson==3.20.1 en kan je installeren in de ‘venv’ met pip install -r requirements.txt.
Je main.py ziet er dan als volgt uit, waarbij de if __name__ == '__main__': een indicatie geeft dat deze file de entrypoint tot onze applicatie is:
Klik hier om de code te zien/verbergen voor main.py🔽
importrandomfromlibs.scorebordimportScoreboarddefmain():# Laad het scorebord vanuit het JSON-bestand (als het bestaat)sb=Scoreboard.load_from_json('highscores.json')print("Welcome to the Higher or Lower game!")print("This is the current leaderboard:\n",sb,"\n")print("Guess if the next number will be higher or lower.")current_number=random.randint(1,100)score=0playing=Truewhileplaying:print("Current number:",current_number)guess=input("Will the next number be higher or lower? (h/l): ").lower().strip()next_number=random.randint(1,100)if(guess=='h'andnext_number>current_number)or(guess=='l'andnext_number<current_number):print("Correct! The next number was:",next_number)score+=1else:print("Wrong! The next number was:",next_number)playing=Falsecurrent_number=next_numberprint("Game over! Your final score:",score)name=input("What is your name? ")sb.add(name,score)print("This is the new leaderboard:\n",sb,"\n")Scoreboard.save_to_json('highscores.json',sb)if__name__=='__main__':main()
Bestand: libs/init.py
Dit bestand kan leeg zijn, maar het zorgt ervoor dat Python de map libs als package herkent.
Info
Wil je meer info over de conventies over het werken met interne libraries in python projecten, bekijk dan deze video.
Interne dependency: scorebord.py
Dit is de interne dependency die het scorebord beheert. Hierin gebruiken we de externe JSON-library (simplejson) om de gegevens op te slaan en te laden.
Klik hier om de code te zien/verbergen voor ./libs/scorebord.py🔽
importsimplejsonasjsonclassScoreboard:def__init__(self):self.scores=[]defadd(self,name,score):self.scores.append({'name':name,'score':score})deftotal_score(self,name):returnsum(player['score']forplayerinself.scoresifplayer['name']==name)defget_winner(self):ifnotself.scores:return"No players yet"returnmax(self.scores,key=lambdaplayer:player['score'])['name']def__str__(self):# Sorteer de scores aflopend en formatteer de outputsorted_scores=sorted(self.scores,key=lambdaplayer:player['score'],reverse=True)return"\n".join(f"{player['name']}: {player['score']}"forplayerinsorted_scores)@staticmethoddefsave_to_json(filename,scoreboard):withopen(filename,'w')asf:json.dump(scoreboard.scores,f,indent=4)@staticmethoddefload_from_json(filename):sb=Scoreboard()try:withopen(filename,'r')asf:scores=json.load(f)ifscoresisnotNone:sb.scores=scoresexcept(FileNotFoundError,json.JSONDecodeError):# Bestand bestaat niet of is ongeldig, geef een nieuw scorebord terug.passreturnsb
Info
Meer info over het gebruik van classes in Python vind je hier.
Builden/runnen
Je zou dit project nu kunnen laten compileren tot een binary met bijvoorbeeld pyinstaller zoals we gezien hebben in build tools voor python, maar we kunnen het project ook simpelweg uitvoeren met python via:
python3 ./app.py
De bronbestanden voor dit voorbeeld kan je ook in deze github repo terugvinden. Merk op dat in dit project de ‘venv’ niet is opgenomen in het versiebeheer via de higher_lower_venv/ in de ./.gitignore-file wat de conventie is.