Meson build sistem

Radite na obimnim projektima sa izuzetno velikim brojem fajlova, direktorijuma i spoljašnjih zavisnosti? Pitate se kako najlakše sve to povezati i uspešno iskompajlirati na tri različite platforme? Rešenje za ove probleme je Meson, build sistem koji omogućava da se kroz samo par linija koda vaš projekat, na potpuno identičan način, iskompajlira na svim Linux, Windows ili MacOS operativnim sistemima.

Build sistemi

Build sistemi su softverski alati dizajnirani da automatizuju proces izgradnje programa. Najopštije rečeno, posao build sistema jeste da mapira set izvornih fajlova u jedan ili više ciljnih binarnih fajlova – build targets.

Važna osobina build sistema je ta da oni memorišu akcije koje su već preduzete, tako da izvršavaju build akcije samo nad resursima koji su promenjeni od prethodnog build-ovanja; ovaj proces se naziva partial rebuild. Kako bi partial rebuild bio moguć, build sistem mora da ima potpuno razumevanje svih zavisnosti u programu, koje mogu biti veoma kompleksne. Postoje dva metoda za specifikaciju zavisnosti:

  • Eksplicitni metod podrazumeva da korisnik u build definiciji navede zavisnosti koristeći ugrađene funkcije koje pruža build sistem.
  • Implicitni metod podrazumeva da build sistem pokušava da pronađe i poveže zavisnosti samostalno, bez pomoći programera. Ovo se obavlja na više načina, među kojima je i jednostavno prepoznavanje ekstenzija. Na primer, ako postoji hello.c fajl, build sistem će zaključiti zavisnost od hello.c do hello.o (objektni fajl na Linuksu). Dalje, dodatne zavisnosti se mogu saznati i iz samog sadržaja fajlova. Na primer, build sistem može da skenira .c fajl u potrazi za #include direktivama koje opisuju novu zavisnost između header fajla i biblioteke, itd.

Meson

Meson je build sistem otvorenog koda osmišljen da bude izuzetno brz, i, što je još važnije, što lakši za upotrebu. Glavni cilj Meson-a jeste da programer provede što manje vremena u pisanju i debagovanju build definicija, kao i u čekanju da taj isti sistem odradi svoj posao.

Odlike Meson alata su:

  1. Multiplatformska podrška za: Linux, Windows, MacOS;
  2. Podržani su C, C++, D, Fortran, Java, Rust;
  3. Build definicije su čitljive i intuitivne.;
  4. Optimizovan za ekstremno brze potpune i inkrementalne build-ove, bez žrtvovanja tačnosti.

Postoji veliki broj drugih build sistema: CMake, Premake, Autotools… Po čemu se Meson izdvaja?

  • Ukoliko tražite intuitivnost, bićete prezadovoljni onime što Meson pruža. Već na primerima build definicija koje možete naći u ovom članku primetićete da vam za mnoge funkcije objašnjenje neće biti potrebno. To je samo po sebi indikator jasne sintakse i načina razmišljanja, koji ne gubi smisao sa rastom kompleksnosti projekta na koji se primenjuje.

  • Kompletno kodiranje koje se od korisnika Meson-a očekuje obavlja se u programskom jeziku Python. To samo po sebi pruža veću fleksibilnost u smislu toga da ovaj sistem mogu koristiti i osobe koje nemaju preterano duboko poznavanje programiranja, kao i svega onoga što se odvija iza samog koda, takoreći „ispod haube“.

  • Sistem visokih performansi – to je drugi naziv za Meson. Kao dokaz imamo praktični eksperiment na sledećem linku: https://mesonbuild.com/Simple-comparison.html. Napravljen je zahtevan projekat koji je zatim build-ovan na različitim build sistemima, uključujući i Meson. Merena su vremena za različite kategorije: generisanje konfiguracionih build fajlova, obavljanje samog build-a i tzv. empty build – kada se utvrđuje da je sve već build-ovano. Može se videti da je Meson jasan pobednik ovog eksperimenta, sa izvanrednim rezultatima u svakoj od kategorija.

  • Održavanje inkrementalnih build-ova što kraćim jeste jedan od ključnih faktora u povećanju produktivnosti programera. To znači da je cilj build sistema da omogući programeru da se u potpunosti preda razvoju samog softvera, a da što manje vremena provede u build-ovanju koje ga samo može izbaciti iz kreativne zone.

  • Iako relativno „mlad“ softver – prošlo je šest godina od njegovog prvog javnog objavljivanja (za poređenje se može uzeti CMake koji postoji na tržištu već skoro dve decenije), Meson je prepoznat od strane zajednice kao nezamenljiv alat u svojoj sferi. Tome može posvedočiti veliki broj projekata na kojima je primenjen baš ovaj build sistem. Listu tih projekata možete pogledati na sledećem linku: https://mesonbuild.com/Users.html.

Instalacija

Meson ima dve značajne zavisnosti na koje se oslanja, to su Python i Ninja. Na svakom operativnom sistemu obe zavisnosti moraju biti prisutne pored samog Meson-a, kako bi sistem bio upotrebljiv.

Meson je implementiran u programskom jeziku Python i u njegovim build definicijama može se izvršavati standardni Python kod. Verzija Python-a mora biti 3.5 ili novija.

Ninja je sam po sebi još jedan build sistem, koji Meson koristi kako bi za njega obavio određene zadatke. Meson se najvećim delom bavi generisanjem optimizovanih build konfiguracionih fajlova, dok Ninja koristi te fajlove kako bi, na najbrži mogući način, obavio sam build projekta i proizveo željene binarne fajlove.

Linux instalacija

Na Linux distribucijama moguća je potpuna instalacija svih neophodnih zavisnosti za Meson pokretanjem sledeće komande iz terminala:

$ sudo apt install python3 ninja-build

Nakon toga, Meson je najbolje instalirati upotrebom PIP3 package manager-a:

$ sudo apt install python3-pip
$ sudo pip3 install meson

Druga komanda zahteva root privilegije i instaliraće Meson na celom sistemu, za sve korisnike. Alternativa tome jeste komanda: pip3 install --user meson, koja ne zahteva nikakve posebne privilegije i instaliraće Meson samo za korisnika koji je poziva.

Umesto instalacije Meson-a moguće je preuzeti i gotov projekat (.tar.gz fajl) sa sledećeg linka: https://github.com/mesonbuild/meson/releases. Odatle se Meson može pokrenuti iz root direktorijuma komandom ./meson.py.

Napomena: Ne preporučuje se instalacija Meson-a putem nekih drugih package manager-a osim PIP-a, jer nisu podržane od strane zvanične Meson zajednice.

Windows i MacOS instalacija

Odgovarajuću verziju Python-a možete preuzeti sa linka: https://www.python.org/downloads/.

Sa zvanične GitHub stranice može se preuzeti jednostavan .msi instaler za određenu arhitekturu, koji će na sistemu instalirati i Meson i Ninja alate: https://github.com/mesonbuild/meson/releases.

Važna napomena: U nastavku ovog članka svi primeri biće izvršavani na Ubuntu 18.04 operativnom sistemu. Kako je Meson cross-platform alat, nakon njegove uspešne instalacije, svi primeri će moći na identičan način da se izvršavaju i na Windows, odnosno MacOS operativnim sistemima.

Izvorni kod primera

Kompletan kod za sve primere koji se pojavljuju u ovom članku dostupan je za preuzimanje i pregled na sledećem GitHub repozitorijumu. Na Linux operativnim sistemima, primeri se mogu preuzeti i pokretanjem sledećih komandi iz terminala (kloniranje repozitorijuma):

$ sudo apt install git
$ git clone https://github.com/Sthrone/Meson.git

Jednostavan primer

Kako bismo se uverili da Meson stvarno radi, napravimo prvo jedan jednostavan program Pozdrav.c na sledeći način:

#include <stdio.h>

int main(int argc, char **argv)
{
    printf("Pozdrav sa IMI-ja!\n");
    return 0;
}

Zatim ćemo kreirati fajl koji će sadržati Meson build definiciju pod nazivom meson.build u istom direktorijumu:

project('PozdravProjekat', 'c')
executable('imi_pozdrav', 'Pozdrav.c')

To je sve, sada možemo da build-ujemo našu aplikaciju. Prvo je neophodno da inicijalizujemo build pokretanjem sledeće komande u radnom direktorijumu:

$ meson builddir

The Meson build system
Version: 0.50.0
Source dir: /home/ubuntu/Desktop/Meson/Primer01
Build dir: /home/ubuntu/Desktop/Meson/Primer01/builddir
Build type: native build
Project name: PozdravProjekat
Project version: undefined
Native C compiler: cc (gcc 7.3.0 "cc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0")
Build machine cpu family: x86_64
Build machine cpu: x86_64
Build targets in project: 1
Found ninja-1.8.2 at /usr/bin/ninja

Na taj način se kreira zadati builddir direktorijum, u kojem je Meson sačuvao build konfiguracione fajlove. Najbitniji među njima je build.ninja fajl, u kojem se, opšte rečeno, nalazi niz instrukcija koje je Meson zadao Ninja-i na izvršenje, a koje za cilj imaju izgradnju konačnih binarnih fajlova. Važno je napomenuti da korisnik nikako ne treba da vrši manuelne izmene nad fajlovima u ovom direktorijumu. Kada je to završeno, možemo pokrenuti build našeg koda pokretanjem komandi:

$ cd builddir 
$ ninja

[2/2] Linking target imi_pozdrav.

Konačno, u istom direktorijumu Ninja je kreirao traženi exe fajl, koji izvršavamo:

$ ./imi_pozdrav

Pozdrav sa IMI-ja!

Kompletan projekat može se preuzeti ovde.

Build definicije

Sve Meson definicije počinju project() funkcijom. Ona određuje ime projekta i naziv programskog jezika koji se u njemu koristi.

Funkcija executable() prima dva argumenta. Prvi argument je build target – naziv fajla koji će se dobiti kompajliranjem projekta. Drugi argument jeste izvorni kod. Ukoliko se naš izvorni kod sastoji od više fajlova, njihove nazive možemo staviti u niz i sačuvati u vidu neke promenljive, koju ćemo zatim proslediti kao drugi argument funkcije executable().

Funkcija executable() kao povrtanu vrednost ima jedan executable objekat. Taj objekat se može upotrebiti za jedinično testiranje korišćenjem test() funkcije.

project('Primer', 'c')
src = ['izvorni_kod_1.c', 'izvorni_kod_2.c', 'izvorni_kod_3.c'] 
exe = executable('izvrsni_fajl', src) 
test('Naziv testa', exe)

Ukoliko se za ovakav kod pokrene komanda ninja test, pokrenuće se i svi test primeri opisani test() funkcijama, a korisniku će se u terminalu prikazati koji testovi su uspešno izvršeni, a koji nisu.

Zavisnosti

Mali broj aplikacija je potpuno samostalan, već koriste neke eksterne biblioteke i framework-e da odrade neki deo posla. Meson omogućava veoma lako pronalaženje i upotrebu spoljašnjih zavisnosti.

zlib_zavisnost = dependency('zlib', version : '>=1.2.8') 
exe = executable('zlib_program', 'program.c', dependencies : zlib_zavisnost)

U prvoj liniji Meson-u se govori da pronađe spoljašnju biblioteku zlib određene verzije u sistemu – ovo je tipičan primer eksplicitnog metoda o kojem je bilo reči u uvodu. Ukoliko Meson ne pronađe traženu biblioteku, ispisaće grešku do koje je došlo. Zatim se u drugoj liniji vrši izgradnja executable objekta koristeći pronađene zavisnosti. Ukoliko zavisnosti ima više, moguće je proslediti parametar kao niz:

executable('izvrsni_fajl', 'izvorni_kod.c', dependencies : [zavisnost_1, zavisnost_2, zavisnost_3, zavisnost_4])

Ako je neka zavisnost opciona, možemo reći Meson-u da ne podiže grešku ukoliko je ne pronađe u sistemu, na sledeći način:

opcionalna_zavisnost = dependency('zlib', required : false)

Primer upotrebe zavisnosti

Za primer zavisnosti koristićemo GTK+ biblioteku za kreiranje grafičkih interfejsa. Možemo je instalirati pokretanjem komande:

$ sudo apt install libgtk-3-dev

Možemo upotrebiti isti projekat sa prethodnog primera, samo ćemo izmeniti izvorni fajl i build definiciju. Fajl Pozdrav.c sada će imati sledeći izgled:

#include <gtk/gtk.h>

int main(int argc, char **argv)
{
    GtkWidget *win;
    gtk_init(&argc, &argv);

    win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(win), "Pozdrav sa IMI-ja!");
    g_signal_connect(win, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    gtk_widget_show(win);
    gtk_main();
}

Cilj ovog koda jeste da napravi jedan prazan prozor koristeći funkcije biblioteke GTK+. U fajlu meson.build opisujemo novu zavisnost koju koristimo, na sledeći način:

project('PozdravProjekat', 'c')
gtk_zavisnost = dependency('gtk+-3.0')
exe = executable('imi_pozdrav', 'Pozdrav.c', dependencies : gtk_zavisnost)
test('Prvi test', exe)

Sada nije potrebno izvršiti ponovni build od nule, već ćemo samo „regenerisati“ postojeće konfiguracione fajlove komandom iz postojećeg direktorijuma builddir:

$ ninja reconfigure

[0/1] Regenerating build files.
The Meson build system
Version: 0.50.0
Source dir: /home/ubuntu/Desktop/Meson/Primer02
Build dir: /home/ubuntu/Desktop/Meson/Primer02/builddir
Build type: native build
Project name: PozdravProjekat
Project version: undefined
Native C compiler: cc (gcc 7.3.0 "cc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0")
Build machine cpu family: x86_64
Build machine cpu: x86_64
Found pkg-config: /usr/bin/pkg-config (0.29.1)
Dependency gtk+-3.0 found: YES 3.22.30
Build targets in project: 1
Found ninja-1.8.2 at /usr/bin/ninja

$ ninja

[2/2] Linking target imi_pozdrav.

Meson je automatski detektovao promene u build definiciji i pobrinuo se za sve nove zavisnosti. Pokrenimo sada naš program komandom ./imi_pozdrav.

Novi prozor je kreiran kao što je i očekivano.

Kompletan projekat za ovaj primer može se preuzeti ovde.

Build targets

Build target je zapravo binarni fajl koji predstavlja rezultat samog build-ovanja. Meson obezbeđuje tri vrste build target-a:

  • executable
  • static_library
  • shared_library

Do sada smo davali samo primere u kojima se build-uju izvršni fajlovi (executable). Meson omogućava automatizovano build-ovanje kako statičkih, tako i dinamičkih biblioteka. Ispod je dat primer definicije za build-ovanje verzionisane dinamičke biblioteke i linkovanja executable objekta sa tom bibliotekom:

project('PrimerBiblioteke', 'c')
lib = shared_library('naziv_biblioteke', 'izvorni_kod_biblioteke.c', version : '1.2.3', soversion : '0') 
executable('izvrsni_fajl', 'program.c', link_with : lib)

Sve ovo je moguće u samo par linija koda, bez pisanja shell skripti i podešavanja environment promenljivih.

Primer build-a biblioteke

Sada ćemo pokazati kako je pomoću Meson alata moguće izgraditi dinamičku biblioteku od nekog izvornog koda, zatim je povezati sa programom koji koristi njenu funkciju, i najzad istestirati dobijeni izvršni fajl.

Prvo ćemo u fajlu funkcija.c dati jednu funkciju koja će činiti našu jednostavnu dinamičku biblioteku:

#include <stdio.h>

void Pozdrav()
{
    printf("Pozdrav sa IMI-ja!\n");
}

Zatim imamo naš C program – program.c, koji samo pravi poziv ka funkciji koja je definisana u biblioteci koju ćemo izgraditi. U prvoj liniji je data deklaracija te funkcije, a ključnom rečju extern se govori kompajleru da definicija ove funkcije nije data u tom fajlu. Navođenje ove deklaracije nije obavezno, ali se svakako preporučuje kako bi se izbegla upozorenja prilikom kompajliranja.

extern void Pozdrav();

int main()
{
    Pozdrav();
    return 0;
}

Najzad, imamo i našu build definiciju – meson.build. Nakon standardne project() deklaracije, imamo funkciju shared_library(), koja zapravo kreira dinamičku (deljenu) biblioteku. Prvi argument te funkcije jeste naziv biblioteke (Meson automatski dodaje prefiks lib na ime, tako da će se u našem slučaju fajl ove biblioteke zvati libpozdrav.so), dok je drugi argument naziv izvornog fajla. Povratna vrednost ove funkcije je objekat biblioteke, koji ćemo već u sledećoj liniji iskoristiti kako bismo preko link_with direktive povezali naš program.c program sa novokreiranom bibliotekom.

project('DinamickaBiblioteka', 'c')
pozdrav_lib = shared_library('pozdrav', 'funkcija.c')
exe = executable('program', 'program.c', link_with : pozdrav_lib)
test('Prvi test', exe)

Sa ova tri fajla u istom direktorijumu možemo započeti build:

$ meson builddir

The Meson build system
Version: 0.50.0
Source dir: /home/ubuntu/Desktop/Meson/Primer03
Build dir: /home/ubuntu/Desktop/Meson/Primer03/builddir
Build type: native build
Project name: DinamickaBiblioteka
Project version: undefined
Native C compiler: cc (gcc 7.3.0 "cc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0")
Build machine cpu family: x86_64
Build machine cpu: x86_64
Build targets in project: 2
Found ninja-1.8.2 at /usr/bin/ninja

Obratimo pažnju da u pretposlednjem redu izlaza iz komande piše da postoje dva build target-a u ovom projektu. Dakle, Meson je tumačeći build definiciju razumeo da će ovaj build za izlaz imati dve binarne datoteke: jednu dinamičku biblioteku (.so) i jedan izvršni (.exe) fajl.

$ cd builddir
$ ninja

[5/5] Linking target program.

$ ninja test

[0/1] Running all tests.
1/1 Prvi test                               OK       0.00 s 

Ok:                    1
Expected Fail:         0
Fail:                  0
Unexpected Pass:       0
Skipped:               0
Timeout:               0

Full log written to /home/ubuntu/Desktop/Meson/Primer03/builddir/meson-logs/testlog.txt

Linkovanje i testiranje je uspešno prošlo, sada možemo pokrenuti naš program:

$ ./program

Pozdrav sa IMI-ja!

Kompletan projekat za ovaj primer može se preuzeti ovde.

Dodatne funkcionalnosti

Pomoću Meson-a se pored samog build-ovanja, mogu vršiti i mnoge provere koje se tiču postupka kompajliranja. Izvlačenje compiler objekta iz glavne meson varijable obavlja se na sledeći način:

compiler = meson.get_compiler('c')

Dobili smo objekat default-nog C kompajlera na sistemu, sada možemo upotrebiti ovaj objekat za više provera:

kod = '''
# include <stdio.h>

void Pozdrav()
{
    printf("Pozdrav sa IMI-ja!\n");
}
'''

1.) Da li se kod uspešno kompajlira?

uspesnost_kompajliranja = compiler.compiles(kod, name : 'Provera kompajliranja')

Promenljiva uspesnost_kompajliranja sadržaće vrednost true ili false, u zavisnosti od uspešnosti kompajliranja zadatog koda. Argument name je opcion, ako se navede Meson će zapisati rezultate provere u svoj log.

2.) Da li se kod uspešno kompajlira i linkuje?

uspesnost_linkovanja = compiler.links(kod, args : '-lfoo', name : 'Provera linkovanja')

Kako je -lfoo neispravan argument, rezultat ove provere biće vrednost false, dakle, linkovanje nije uspešno izvršeno.

3.) Kompajliranje i testiranje aplikacije

kod = '''

# include <stdio.h>

int main(int argc, char **argv)
{
    printf("%s\n", "Izlaz!");
    fprintf(stderr, "%s\n", "Greska!");
    return 0;
}

'''

rezultat_testiranja = compiler.run(kod, name: 'Testiranje')

U ovom primeru promenljiva rezultat_testiranja je zapravo objekat sa četiri pristupne get() metode:

  • compiled() – vraća bool vrednost u zavisnosti od uspešnosti procesa kompajliranja. Na našem primeru je to vrednost true.
  • returncode() – vraća ceo broj koji predstavlja izlazni kod aplikacije. Na našem primeru je to 0 (nema greške).
  • stdout() – vraća tekstualnu vrednost koja predstavlja ispis na standardnom izlazu aplikacije. Na našem primeru je to vrednost „Izlaz!“
  • stderr() – vraća tekstualnu vrednost koja predstavlja ispis na standardnom izlazu za greške. Na našem primeru je to vrednost „Greska!“

Ove metode mogu biti izuzetno korisne kada je potrebno na osnovu izlaza iz neke aplikacije odrediti dalji tok build-ovanja. Umesto pisanja posebnih shell skripti, sada možemo samo postaviti jednostavne uslove poput:

if rezultat_testiranja.returncode() == 0
    uspesnost_linkovanja_2 = compiler.links(kod, name : 'Provera linkovanja 2')
endif

Za kraj, možemo spojiti sve prethodne primere u jednu build definiciju, kako bismo videli izlaz koji će nam Meson dati.

project('DodatneFunkcionalnosti', 'c')
compiler = meson.get_compiler('c')

kod = '''
    
#include 

void Pozdrav()
{
    printf("Pozdrav sa IMI-ja!\n");
}

'''
stderr()
uspesnost_kompajliranja = compiler.compiles(kod, name : 'Provera kompajliranja')
uspesnost_linkovanja = compiler.links(kod, args : '-lfoo', name : 'Provera linkovanja')

kod = '''

#include 

int main(int argc, char **argv)
{
    printf("%s\n", "Izlaz!");
    fprintf(stderr, "%s\n", "Greska!");
    return 0;
}

'''

rezultat_testiranja = compiler.run(kod, name: 'Testiranje')

if rezultat_testiranja.returncode() == 0
    uspesnost_linkovanja_2 = compiler.links(kod, name : 'Provera linkovanja 2')
endif

Ako samo za ovaj fajl započnemo build, dobijamo očekivani izlaz:

$ meson builddir

The Meson build system
Version: 0.50.0
Source dir: /home/ubuntu/Desktop/Meson/Primer04
Build dir: /home/ubuntu/Desktop/Meson/Primer04/builddir
Build type: native build
Project name: DodatneFunkcionalnosti
Project version: undefined
Native C compiler: cc (gcc 7.3.0 "cc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0")
Build machine cpu family: x86_64
Build machine cpu: x86_64
Checking if "Provera kompajliranja" compiles: YES
Checking if "Provera linkovanja" links: NO
Checking if "Testiranje" runs: YES
Checking if "Provera linkovanja 2" links: YES
Build targets in project: 0
Found ninja-1.8.2 at /usr/bin/ninja

Kompletan projekat za ovaj primer može se preuzeti ovde.

Meson pruža i mnoge druge funkcionalnosti, poput: rada sa nitima, priključivanja dodatnih modula, integracije sa razvojnim okruženjima itd. Potpunu listu funkcionalnosti možete pronaći na zvaničnoj stranici Meson-a: https://mesonbuild.com/Manual.html.

Korisni linkovi

Autor: Stefan Nestorović

Student četvrte godine Informatike na Institutu za matematiku i informatiku, Prirodno-matematičkog fakulteta u Kragujevcu.

Stefan Nestorović

Student četvrte godine Informatike na Institutu za matematiku i informatiku, Prirodno-matematičkog fakulteta u Kragujevcu.