Een programma bouwen met GNU Make. Bestanden maken

Ik heb mij altijd aangetrokken gevoeld tot minimalisme. Het idee dat één ding één ding moet doen, maar dan zo goed mogelijk, resulteerde in de creatie van UNIX. En hoewel UNIX niet langer een eenvoudig systeem kan worden genoemd, en het minimalisme daarin niet zo gemakkelijk te zien is, kan het worden beschouwd als een duidelijk voorbeeld van de kwantitatieve en kwalitatieve transformatie van veel eenvoudige en begrijpelijke dingen in één zeer complex en ondoorzichtig systeem. Bij de ontwikkeling heeft make ongeveer hetzelfde pad gevolgd: eenvoud en duidelijkheid, met groeiende schaal, veranderd in een verschrikkelijk monster (denk aan je gevoelens toen je voor het eerst een makefile opende).

Dat ik make lange tijd voortdurend negeerde, was te wijten aan het gemak van de gebruikte IDE’s en de onwil om dit ‘overblijfsel uit het verleden’ (in wezen luiheid) te begrijpen. Al deze vervelende knoppen, menu's, enz. de attributen van allerlei ateliers dwongen mij op zoek te gaan naar een alternatief voor de werkwijze die ik tot nu toe beoefende. Nee, ik ben geen make-goeroe geworden, maar de kennis die ik heb opgedaan is voldoende voor mijn kleine projecten. Dit artikel is bedoeld voor degenen die, zoals ik onlangs, uit de gezellige raamslavernij willen breken en de ascetische maar vrije wereld van de schelp willen betreden.

Merk- basisinformatie

make is een hulpprogramma dat is ontworpen om de conversie van bestanden van de ene vorm naar de andere te automatiseren. De conversieregels worden gespecificeerd in een script genaamd Makefile, dat zich in de hoofdmap van de werkmap van het project moet bevinden. Het script zelf bestaat uit een reeks regels, die op hun beurt worden beschreven:

1) doelen (wat deze regel doet);
2) details (wat nodig is om aan de regel te voldoen en doelen te bereiken);
3) opdrachten (die deze transformaties uitvoeren).

In het algemeen syntaxis van makefile kan als volgt worden weergegeven:

# Identificatie vindt uitsluitend plaats met behulp van tabbladtekens, # elk commando moet worden voorafgegaan door een inspringing<цели>: <реквизиты> <команда #1> ... <команда #n>

Dat wil zeggen, de make-regel is het antwoord op drie vragen:

(Waar maken we het van? (details)) ---> [Hoe maken we het? (opdrachten)] ---> (Wat doen we? (doelen))
Het is gemakkelijk in te zien dat de vertaal- en compilatieprocessen heel mooi in dit diagram passen:

(bronbestanden) ---> [uitzending] ---> (objectbestanden)
(objectbestanden) ---> [link] ---> (uitvoerbare bestanden)

De eenvoudigste Makefile

Laten we zeggen dat we een programma hebben dat uit slechts één bestand bestaat:

/* * hoofd.c */ #include int main() ( printf("Hallo wereld!\n"); return 0; )
Om het te compileren is een heel eenvoudig makefile voldoende:

Hallo: main.c gcc -o hallo main.c
Deze Makefile bestaat uit één regel, die op zijn beurt bestaat uit een doel - "hello", een prop - "main.c", en een commando - "gcc -o hello main.c". Om nu te compileren, hoeft u alleen maar het make-commando in de werkmap uit te voeren. Standaard voert make de allereerste regel uit als het uitvoeringsdoel niet expliciet werd opgegeven toen het werd aangeroepen:

$ maken<цель>

Compilatie uit meerdere bronnen

Laten we aannemen dat we een programma hebben dat uit 2 bestanden bestaat:
hoofd.c
/* * main.c */ int main() ( hallo(); return 0; )
en hallo.c
/* * hallo.c */ #include void hallo() ( printf("Hallo wereld!\n"); )
De Makefile die dit programma compileert, zou er als volgt uit kunnen zien:

Hallo: main.c hallo.c gcc -o hallo main.c hallo.c
Het is behoorlijk functioneel, maar heeft één belangrijk nadeel: we zullen verder onthullen welke.

Incrementele compilatie

Laten we ons voorstellen dat ons programma uit een tiental of twee bronbestanden bestaat. We brengen wijzigingen aan in een van hen en willen deze opnieuw opbouwen. Als u de in het vorige voorbeeld beschreven aanpak gebruikt, worden alle bronbestanden opnieuw gecompileerd, wat een negatieve invloed heeft op de hercompilatietijd. De oplossing is om de compilatie in twee fasen te verdelen: de vertaalfase en de koppelingsfase.

Nu, na het wijzigen van een van de bronbestanden, volstaat het om het te vertalen en alle objectbestanden te koppelen. Tegelijkertijd slaan we de fase over van het vertalen van details die niet door wijzigingen worden beïnvloed, waardoor de compilatietijd in het algemeen wordt verkort. Deze aanpak wordt incrementele compilatie genoemd. Om dit te ondersteunen, vergelijkt u de wijzigingstijden van doelen en hun details (met behulp van data bestandssysteem), waardoor het onafhankelijk beslist welke regels moeten worden gevolgd en welke eenvoudigweg kunnen worden genegeerd:

Hoofd.o: hoofd.c gcc -c -o hoofd.o hoofd.c hallo.o: hallo.c gcc -c -o hallo.o hallo.c hallo: hoofd.o hallo.o gcc -o hallo hoofd. o hallo.o
Probeer dit project te bouwen. Om het te bouwen, moet u het doel expliciet specificeren, d.w.z. geef het commando hallo.
Wijzig daarna een van de bronbestanden en bouw deze opnieuw. Houd er rekening mee dat tijdens de tweede compilatie alleen het gewijzigde bestand wordt vertaald.

Eenmaal uitgevoerd, zal make meteen proberen het hallo-doel te pakken te krijgen, maar om het te maken heb je de main.o- en hello.o-bestanden nodig, die nog niet bestaan. Daarom zal de uitvoering van de regel worden uitgesteld en zal er worden gezocht naar regels die beschrijven hoe de ontbrekende details kunnen worden verkregen. Zodra alle details zijn ontvangen, zal make terugkeren naar de uitvoering van het uitgestelde doel. Dit betekent dat make de regels recursief uitvoert.

Fictieve doelen

In feite kunnen niet alleen echte bestanden als doelwit fungeren. Iedereen die programma's uit de broncode heeft moeten bouwen, zou bekend moeten zijn met twee standaardopdrachten in de UNIX-wereld:

$ make $ make install
Met het make-commando wordt het programma gecompileerd en met het make install-commando wordt het geïnstalleerd. Deze aanpak is erg handig, omdat alles wat nodig is om de applicatie te bouwen en te implementeren, aanwezig is doel systeem opgenomen in één bestand (laten we het configuratiescript even vergeten). Houd er rekening mee dat we in het eerste geval het doel niet specificeren, en in het tweede geval is het doel niet om te creëren installatiebestand en het proces van het installeren van een applicatie op het systeem. Zogenaamde fictieve (nep)doelen stellen ons in staat dergelijke trucs uit te voeren. Hier korte lijst standaard doelen:

  • alles is het standaarddoel. U hoeft dit niet expliciet op te geven wanneer u make aanroept.
  • clean - maak de map leeg van alle bestanden die zijn verkregen als resultaat van de compilatie.
  • installeren - installatie uitvoeren
  • verwijderen - en respectievelijk verwijderen.
Om te voorkomen dat make naar bestanden met dergelijke namen zoekt, moeten deze in de Makefile worden gedefinieerd met behulp van de .PHONY-richtlijn. Het volgende is een voorbeeld van Makefile met als doel alles, opschonen, installeren en verwijderen:

PHONY: alles schoon installeren alles verwijderen: hallo schoon: rm -rf hallo *.o main.o: main.c gcc -c -o main.o main.c hello.o: hello.c gcc -c -o hallo. o hallo.c hallo: main.o hello.o gcc -o hallo main.o hello.o installeren: install ./hello /usr/local/bin verwijdering: rm -rf /usr/local/bin/hello
Nu kunnen we ons programma bouwen, installeren/verwijderen en ook de werkmap opschonen met behulp van standaard make-doelen.

Merk op dat het all-doel geen opdrachten specificeert; het enige dat ze nodig heeft, zijn de hallo-rekwisieten. Als je de recursieve aard van make kent, is het niet moeilijk je voor te stellen hoe dit script zal werken. Het moet ook worden opgemerkt Speciale aandacht op het feit dat als het hello-bestand al bestaat (overgelaten na de vorige compilatie) en de details ervan niet zijn gewijzigd, de opdracht make er wordt niets meer in elkaar gezet. Dit is een klassieke hark. Als u bijvoorbeeld een headerbestand wijzigt dat per ongeluk niet in de lijst met details is opgenomen, kunt u dit verkrijgen lange uren hoofdpijn. Om een ​​volledige herbouw van het project te garanderen, moet u daarom eerst de werkmap leegmaken:

$ maak schoon $ maak
U moet sudo gebruiken om de installatie-/verwijderingsdoelen te voltooien.

Variabelen

Iedereen die bekend is met de DRY-regel (Don't repeat own) heeft waarschijnlijk al gemerkt dat er iets mis is, namelijk dat onze Makefile een groot aantal herhaalde fragmenten bevat, wat tot verwarring kan leiden bij daaropvolgende pogingen om het uit te breiden of te wijzigen. imperatieve talen voor deze doeleinden hebben we variabelen en constanten; make beschikt ook over soortgelijke faciliteiten. Variabelen in make heten strings en worden heel eenvoudig gedefinieerd:

=
Er is een onuitgesproken regel dat variabelen in hoofdletters moeten worden genoemd, bijvoorbeeld:

SRC = main.c hallo.c
Dit is hoe we de lijst met bronbestanden hebben gedefinieerd. Om de waarde van een variabele te gebruiken, moet er een dereferentie van worden gemaakt met behulp van de $(-constructie ); bijvoorbeeld zo:

Gcc -o hallo $(SRC)
Hieronder staat een makefile die twee variabelen gebruikt: TARGET - om de naam van het doelprogramma te bepalen en PREFIX - om het pad te bepalen om het programma op het systeem te installeren.

TARGET = hallo PREFIX = /usr/local/bin .PHONY: alles schoon installeren alles verwijderen: $(TARGET) schoon: rm -rf $(TARGET) *.o main.o: main.c gcc -c -o main. o main.c hallo.o: hallo.c gcc -c -o hello.o hello.c $(TARGET): main.o hello.o gcc -o $(TARGET) main.o hello.o install: install $ (TARGET) $(PREFIX) verwijderen: rm -rf $(PREFIX)/$(TARGET)
Dit is al mooier. Ik denk dat het bovenstaande voorbeeld geen speciaal commentaar voor je nodig heeft.

Automatische variabelen

Automatische variabelen zijn bedoeld om makefiles te vereenvoudigen, maar hebben naar mijn mening een negatieve invloed op de leesbaarheid ervan. Hoe het ook zij, ik zal hier een paar van de meest gebruikte variabelen opsommen, en wat je ermee moet doen (of of je het überhaupt moet doen) is aan jou:
  • $@ Doelnaam van de regel die wordt verwerkt
  • $< Имя первой зависимости обрабатываемого правила
  • $^ Lijst met alle afhankelijkheden van de verwerkte regel
Als iemand zijn scripts volledig wil verdoezelen, kun je hier inspiratie opdoen:

Oorsprong

Vóór de creatie van make, bestonden build-systemen (compilaties) van Unix-software doorgaans uit bijbehorende shell-build-scripts bron programma's.

make is gemaakt door Stuart Feldman ( Stuart Veldman) in 1977 bij Bell Labs.

Er zijn tegenwoordig veel hulpprogramma's voor het bijhouden van afhankelijkheid beschikbaar, maar make is een van de meest gebruikte, voornamelijk vanwege het feit dat het sinds de PWB/UNIX-versie in Unix is ​​opgenomen ( voor Programmeurswerkbank), dat tools voor softwareontwikkeling bevatte.

Moderne versies

Er zijn verschillende versies van make, gebaseerd op het originele merk of helemaal opnieuw geschreven, met dezelfde bestandsformaten en Basisprincipes en algoritmen, en bevat ook enkele verbeteringen en uitbreidingen. Bijvoorbeeld:

  • BSD-merk, gebaseerd op het werk van Adam de Boer ( Adam de Boor) via de make-versie, met de mogelijkheid om parallel te bouwen; is in een of andere vorm gemigreerd naar FreeBSD, NetBSD en OpenBSD.
  • GNU make wordt meegeleverd met de meeste GNU/Linux-distributies en wordt vaak gebruikt in combinatie met het GNU-buildsysteem.
$ vrijen, geen oorlog. $ uname -r 7.1-RELEASE-p3

Bestand maken

Het make-programma voert opdrachten uit volgens de regels in speciaal bestand. Dit bestand wordt een makefile genoemd. Normaal gesproken beschrijft een makefile hoe een programma moet worden gecompileerd en gekoppeld.

Een makefile bestaat uit regels en variabelen. De regels hebben de volgende syntaxis:

Doel1 doel2 ...: prop1 prop2 ... team1 team2 ...

Regel is een reeks opdrachten waarvan de uitvoering zal leiden tot het samenstellen van bestanden - doelen uit bestanden- rekwisieten.

De regel vertelt make dat de bestanden geproduceerd door de commando's ( doelen) zijn afhankelijk van de overeenkomstige prop-bestanden. make controleert of gebruikt de inhoud van prop-bestanden op geen enkele manier. Het opgeven van een lijst met prop-bestanden is echter alleen vereist zodat make de aanwezigheid van deze bestanden kan verifiëren voordat opdrachten worden uitgevoerd en om afhankelijkheden tussen bestanden bij te houden.

Meestal is het doel de naam van het bestand dat wordt gegenereerd als gevolg van de opgegeven opdrachten. Een doel kan ook de naam zijn van een actie die wordt uitgevoerd als gevolg van het uitvoeren van opdrachten (het schone doel in makefiles voor het compileren van programma's verwijdert bijvoorbeeld doorgaans alle bestanden die tijdens het compilatieproces zijn gemaakt).

Lijnen bevatten ploegen, moet beginnen met een tabteken.

Laten we eens kijken naar een eenvoudig C-programma. Laat het programmaprogramma bestaan ​​uit een paar codebestanden - main.c en lib.c, evenals één headerbestand - definitions.h, dat in beide codebestanden is opgenomen. Om een ​​programma te maken, moet u daarom de objectbestanden main.o en lib.o maken van paren (main.c definieert.h) en (lib.c definieert.h), en deze vervolgens in het programma koppelen. Bij handmatige montage moet u de volgende opdrachten geven:

Cc -c main.c definieert.h cc -c lib.c definieert.h cc -o programma main.o lib.o

Als er wijzigingen worden aangebracht in het bestand definitions.h tijdens de ontwikkeling van het programma, moeten beide bestanden opnieuw worden gecompileerd en gekoppeld, en als lib.c wordt gewijzigd, hoeft main.o mogelijk niet opnieuw te worden gecompileerd.

Voor elk bestand dat we tijdens het compilatieproces moeten verkrijgen, moeten we dus aangeven op basis van welke bestanden en met welk commando het is gemaakt. Het make-programma doet op basis van deze gegevens het volgende:

  • verzamelt uit deze informatie de juiste volgorde van opdrachten om de vereiste resultaatbestanden te verkrijgen;
  • en initieert de creatie van het vereiste bestand alleen als een dergelijk bestand niet bestaat, of ouder is dan de bestanden waarvan het afhankelijk is.

Als u niet expliciet een doel opgeeft wanneer u make uitvoert, wordt het eerste doel in het make-bestand dat niet begint met een "." verwerkt.

Voor het programmaprogramma is het voldoende om het volgende makefile te schrijven:

Programma: main.o lib.o cc -o programma main.o lib.o main.o lib.o: definieert.h

Een aantal kenmerken zijn het vermelden waard. De naam van het tweede doel specificeert twee bestanden en specificeert geen compilatieopdracht voor hetzelfde doel. Bovendien wordt de afhankelijkheid van objectbestanden van “*.c”-bestanden nergens expliciet vermeld. Feit is dat het make-programma vooraf gedefinieerde regels heeft voor het verkrijgen van bestanden met bepaalde extensies. Dus voor een doelobjectbestand (extensie “.o”), wanneer een corresponderend bestand met de extensie “.c” wordt gedetecteerd, wordt de “cc -c”-compiler aangeroepen, die dit “.c”-bestand en alle afhankelijkheidsbestanden aangeeft. in de parameters.

De syntaxis voor het definiëren van variabelen is:

Variabele = waarde

Betekenis kan een willekeurige reeks tekens zijn, inclusief spaties en toegang tot de waarden van andere variabelen. Dat gezegd hebbende, kunnen we ons makefile als volgt aanpassen:

OBJ = main.o lib.o programma: $(OBJ) cc -o programma $(OBJ) $(OBJ): definieert.h

Opgemerkt moet worden dat de berekening van de waarde van variabelen alleen plaatsvindt op het moment van gebruik (er wordt gebruik gemaakt van de zogenaamde luie berekening). Als u bijvoorbeeld het hele doel opbouwt uit het volgende make-bestand, wordt de tekenreeks 'Huh?' op het scherm afgedrukt.

Foo = $(bar) bar = $(ugh) ugh = Huh? alles: echo $(foo)

Lib.o: lib.h

Zo kan één doelbestand voor meerdere doeleinden worden gespecificeerd. Waarin volle lijst afhankelijkheden voor een bestand worden samengesteld uit de afhankelijkheidslijsten van alle doelen waaraan het deelneemt; het bestand wordt slechts één keer gemaakt.

zie ook

Koppelingen

  • GNU make-handleiding op gnu.org
  • GNU make manual (versie 3.79) (Russisch)
  • FreeBSD handleiding maken
  • Problemen oplossen met de opdrachten ./config, make en make install
  • GNU-merk gebruiken (Russisch)
  • Effectief gebruik van GNU Make (Russisch)
  • Hulp bij het maken van een Makefile (Russisch)

Wikimedia Stichting. 2010.

  • Haller, Lev Michajlovitsj
  • Camargue (paard)

Kijk wat "Maken" is in andere woordenboeken:

    maken- maken, v. T. D); P. pr. &vb. N. (maken).] 1. Om te veroorzaken dat… Het Collaborative International Dictionary of English

    maken- maak 1vt. gemaakt, tot stand brengen 1. tot stand brengen; specif., a) te vormen door vorm te geven of... ... Engels Wereldwoordenboek

    Maken- (engl. machen, erstellen) is een computerprogramma, het Shellscript bevat commando's in de Abhängigkeit von Bedingungen ausführt. Er is een hoger niveau bereikt door de software-entwicklung. Genutzt wird es beispielsweise, um in einem Projekt, das … Deutsch Wikipedia

    Maken- Dit artikel wordt gemaakt door de logica van de instelling. Voor een definitie van "make", zie het artikel make du Wiktionnaire. maak een logicaële traditie van UNIX. C est un “productiemoteur”: il sert à appeler … Wikipédia in het Frans

    maken- (engl. machen, erstellen) is een computerprogramma, das Kommandos in Abhängigkeit von Bedingungen ausführt. Er is veel aandacht voor de software-entwicklung als programmeerwerk. Genutzt wird es beispielsweise, eh in Projekten, die … Deutsch Wikipedia

In dit boek beschrijf ik mijn ervaring met het hulpprogramma GNU-merk en in het bijzonder mijn techniek voor het voorbereiden van makefiles. Ik vind mijn techniek best handig, omdat het het volgende inhoudt: Automatische opbouw van een lijst met bestanden met bronteksten, Automatische generatie afhankelijkheden van opgenomen bestanden (met behulp van de compiler GCC) en "Parallelle" assemblage van debug- en werkversies van het programma.

  • Automatische creatie van een lijst met bestanden met bronteksten
  • Automatisch genereren van afhankelijkheden van opgenomen bestanden (met behulp van een compiler GCC)
  • "Parallelle" assemblage van debug- en werkversies van het programma

Mijn boek is op een enigszins ongebruikelijke manier opgebouwd. In de regel worden boeken gebouwd volgens het principe ‘van eenvoudig naar complex’. Dit is handig voor beginners, maar kan een uitdaging zijn voor professionals. Een ervaren programmeur zal gedwongen worden om door het boek te ‘waden’ en hoofdstukken over te slaan met informatie die hem bekend is. Ik besloot het boek op een ander principe te bouwen. De hele ‘kwintessens’ van het boek, het ‘hoofdidee’ ervan, is vervat in het eerste hoofdstuk. De overige hoofdstukken zijn min of meer aanvullend.

Aan het begin van elk hoofdstuk beschrijf ik kort wat er wordt behandeld en welke kennis je nodig hebt om de stof die in het hoofdstuk wordt gepresenteerd met succes te begrijpen. Voor degenen die het gevoel hebben dat ze niet goed op de hoogte zijn van het onderwerp, wijs ik op aanvullende hoofdstukken die eerst gelezen moeten worden.

Voor werk gebruikte ik GNU-merk versie 3.79.1. Enkele oude versies GNU-merk(bijvoorbeeld versie 3.76.1 uit de distributie Slackware-versie 3.5) werkt mogelijk niet correct met een voorbeeld van een "traditionele" makefile-structuur (blijkbaar "accepteren" ze de oude vorm van het schrijven van sjabloonregels niet).

1. Mijn techniek voor het gebruik van GNU Make

In dit hoofdstuk beschrijf ik mijn methode voor het maken van makefiles voor bouwprojecten met behulp van het programma GNU-merk en compiler GCC (GNU Compiler-collectie). Er wordt aangenomen dat u bekend bent met het hulpprogramma GNU-merk. Als dit niet het geval is, lees dan eerst.

1.1. Voorbeeldproject

Als voorbeeld zal ik een "hypothetisch" project gebruiken: een teksteditor. Het bestaat uit verschillende bestanden met brontekst in de taal C++ (hoofd.cpp, Editor.cpp, TextLine.cpp) en verschillende bevatten bestanden ( hoofd.h,Redacteur.h, TextLine.h). Als u toegang heeft tot internet, kunt u de “elektronische” versie van de voorbeelden in het boek verkrijgen op mijn startpagina op www.geocities.com/SiliconValley/Office/6533. Als internet niet voor u beschikbaar is, worden de lijsten met de bestanden weergegeven die in de voorbeelden worden gebruikt.

1.2. De "traditionele" manier om makefiles te bouwen

In het eerste voorbeeld wordt de makefile op de "traditionele" manier gebouwd. Alle bronbestanden van het samengestelde programma bevinden zich in één map:

  • voorbeeld_1-traditioneel/
    • hoofd.cpp
    • hoofd.h
    • Editor.cpp
    • Redacteur.h
    • TextLine.cpp
    • TextLine.h
    • Maakbestand

Er wordt aangenomen dat er een compiler wordt gebruikt om het programma te compileren GCC en objectbestanden hebben de extensie " .O". Bestand Maakbestand ziet eruit als:

# # example_1-traditional/Makefile # # Voorbeeld van een “traditionele” makefile-structuur # iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ .cpp.o: gcc -c $< main.o: main.h Editor.h TextLine.h Editor.o: Editor.h TextLine.h TextLine.o: TextLine.h

De eerste regelkrachten maken koppel het programma opnieuw wanneer een van de objectbestanden verandert. De tweede regel stelt dat objectbestanden afhankelijk zijn van overeenkomstige bronbestanden. Elke wijziging aan het bronbestand zorgt ervoor dat het opnieuw wordt gecompileerd. De volgende paar regels geven aan van welke headerbestanden elk objectbestand afhankelijk is. Deze manier om een ​​makefile te bouwen lijkt mij lastig omdat:

  • Het is vereist om "expliciet" alle objectbestanden op te sommen waaraan het programma is gekoppeld
  • Het is vereist om “expliciet” te vermelden van welke headerbestanden een bepaald objectbestand afhankelijk is
  • Het uitvoerbare bestand van het programma wordt in de "huidige" map geplaatst. Als ik verschillende versies van het programma nodig heb (bijvoorbeeld debuggen en werken), dan is elke keer dat ik van de ene versie naar de andere ga, een volledige hercompilatie van het programma vereist om ongewenst "vermengen" te voorkomen. verschillende versies objectbestanden.

Het is duidelijk dat de traditionele manier om makefiles te maken verre van ideaal is. De enige manier waarop deze methode handig kan zijn, is de “compatibiliteit”. Blijkbaar zullen zelfs de meest “oude” of “exotische” versies prima werken met zo’n makefile maken(Bijvoorbeeld, maak bedrijven Microsoft). Als een dergelijke “compatibiliteit” niet nodig is, kunt u uw leven veel gemakkelijker maken door het te gebruiken ruime mogelijkheden nutsvoorzieningen GNU-merk. Laten we proberen de tekortkomingen van de "traditionele" aanpak weg te nemen.

1.3. Automatische creatie van een lijst met objectbestanden

Het "handmatig" opsommen van alle objectbestanden die in het programma zijn opgenomen, is behoorlijk vervelend werk, dat gelukkig kan worden geautomatiseerd. Natuurlijk, een “eenvoudig trucje” zoals:

IEdit: *.o gcc$< -o $@

zal niet werken, omdat alleen bestaande V dit moment objectbestanden. Ik gebruik een iets complexere methode, die is gebaseerd op de veronderstelling dat Alle De bronbestanden moeten worden gecompileerd en gekoppeld aan het samengestelde programma. Mijn techniek bestaat uit twee stappen:

  • Krijg een lijst met alle bestanden met de broncode van het programma (alle bestanden met de extensie " .cpp"). Om dit te doen, kunt u de functie gebruiken wildcard.
  • Converteer de lijst met bronbestanden naar een lijst met objectbestanden (vervang de extensie " .cpp"uitbreiden" .O"). Om dit te doen, kunt u de functie gebruiken patsubst.

Het volgende voorbeeld bevat aangepaste versie makefile:

  • voorbeeld_2-auto_obj /
    • hoofd.cpp
    • hoofd.h
    • Editor.cpp
    • Redacteur.h
    • TextLine.cpp
    • TextLine.h
    • Maakbestand

Bestand Maakbestand ziet er nu zo uit:

# # example_2-auto_obj/Makefile # # Voorbeeld van het automatisch opbouwen van een lijst met objectbestanden # iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp)) gcc $^ -o $@ %.o : %. cpp gcc -c $< main.o: main.h Editor.h TextLine.h Editor.o: Editor.h TextLine.h TextLine.o: TextLine.h

De lijst met programmaobjectbestanden wordt automatisch samengesteld. Eerst de functie gebruiken wildcard produceert een lijst met alle bestanden met de extensie " .cpp", gelegen in de projectmap. Gebruik vervolgens de functie patsubst, wordt de resulterende lijst met bronbestanden geconverteerd naar een lijst met objectbestanden. Het makefile is nu universeler geworden - met kleine wijzigingen kan het worden gebruikt om verschillende programma's te bouwen.

1.4. Automatische constructie van afhankelijkheden op headerbestanden

Het "handmatig" opsommen van de afhankelijkheden van objectbestanden op headerbestanden is zelfs nog vervelender en onaangenamer dan het "handmatig" opsommen van objectbestanden. Het is absoluut noodzakelijk om dergelijke afhankelijkheden aan te geven - tijdens de ontwikkeling van programma's kunnen headerbestanden vrij vaak veranderen (klassebeschrijvingen worden bijvoorbeeld traditioneel in headerbestanden geplaatst). Als u de afhankelijkheden van objectbestanden op de overeenkomstige headerbestanden niet specificeert, kan er een situatie ontstaan ​​waarin verschillende programmaobjectbestanden worden gecompileerd met verschillende versies van hetzelfde headerbestand. En dit kan op zijn beurt leiden tot een gedeeltelijk of volledig verlies van functionaliteit van het samengestelde programma.

Het handmatig opsommen van afhankelijkheden vergt behoorlijk wat nauwgezet werk. Het is niet voldoende om eenvoudigweg het bronbestand te openen en de namen op te sommen van alle headerbestanden die worden gebruikt #erbij betrekken. Feit is dat sommige headerbestanden op hun beurt andere headerbestanden kunnen bevatten, dus je moet de hele ‘keten’ van afhankelijkheden volgen.

Nutsvoorziening GNU-merk zal niet in staat zijn om zelfstandig een lijst met afhankelijkheden op te stellen, omdat het hiervoor in de brontekstbestanden zal moeten "kijken" - en dit valt natuurlijk buiten zijn "competentie". Gelukkig kan het arbeidsintensieve proces van het bouwen van afhankelijkheden worden geautomatiseerd met behulp van een compiler GCC. Voor samenwerking Met maken compiler GCC heeft verschillende opties:

Compilatie sleutel Doel
-M Voor elk brontekstbestand zal de preprocessor een uitvoer uitvoeren standaard uitvoer lijst met afhankelijkheden in de vorm van een regel voor het programma maken. De lijst met afhankelijkheden omvat het bronbestand zelf, evenals alle bestanden die zijn opgenomen met behulp van richtlijnen #erbij betrekken<имя_файла> En #include "bestandsnaam". Na het starten van de preprocessor stopt de compiler met werken en worden er geen objectbestanden gegenereerd.
-MM Vergelijkbaar met een sleutel -M #include "bestandsnaam"
-MD Vergelijkbaar met een sleutel -M, maar de lijst met afhankelijkheden wordt niet naar de standaarduitvoer uitgevoerd, maar ernaar geschreven apart bestand afhankelijkheden. De naam van dit bestand wordt gevormd uit de naam van het bronbestand door de extensie ervan te vervangen door " .D". Bijvoorbeeld het afhankelijkheidsbestand voor het bestand hoofd.cpp zal gebeld worden hoofd.d. In tegenstelling tot de sleutel -M, gaat de compilatie gewoon door en wordt niet onderbroken na de opstartfase van de preprocessor.
-MMD Vergelijkbaar met een sleutel -MD, maar alleen het bronbestand zelf en de bestanden die zijn opgenomen met behulp van de richtlijn zijn opgenomen in de lijst met afhankelijkheden #include "bestandsnaam"

Zoals uit de tabel blijkt, kan de compiler op twee manieren werken: in één geval produceert de compiler alleen een lijst met afhankelijkheden en eindigt hij met werken (opties -M En -MM). In het andere geval vindt de compilatie zoals gebruikelijk plaats, alleen wordt naast het objectbestand ook een afhankelijkheidsbestand gegenereerd (opties -MD En -MMD). Ik geef er de voorkeur aan om de tweede optie te gebruiken - het lijkt mij handiger en zuiniger omdat:

  • Als een van de bronbestanden verandert, wordt slechts één corresponderend afhankelijkheidsbestand opnieuw opgebouwd
  • De constructie van afhankelijkheidsbestanden vindt “parallel” plaats met het hoofdwerk van de compiler en heeft praktisch geen invloed op de compilatietijd

Van de twee mogelijke opties -MD En -MMD, Ik geef de voorkeur aan de eerste omdat:

  • Een richtlijn gebruiken #erbij betrekken<имя_файла> Ik neem vaak niet alleen de "standaard" bestanden op, maar ook mijn eigen headerbestanden, die soms kunnen veranderen (bijvoorbeeld de headerbestanden van mijn applicatiebibliotheek LIB).
  • Soms is het handig om naar te kijken vol een lijst met headerbestanden die in de module zijn opgenomen, inclusief “standaard” bestanden.

Nadat de afhankelijkheidsbestanden zijn gegenereerd, moet u ze beschikbaar maken voor het hulpprogramma maken. Dit kan worden bereikt met behulp van de richtlijn erbij betrekken.

Voeg $(wildcard *.d) toe

Let op het gebruik van de functie wildcard. Ontwerp

Inclusief *.d

werkt alleen correct als de map minstens één bestand bevat met de extensie " .D". Als er geen dergelijke bestanden zijn, dan maken zal crashen omdat het zal mislukken bij het “bouwen” van deze bestanden (hiervoor zijn geen instructies beschikbaar!). Als u de functie gebruikt wildcard, en als er geen bestanden zijn om naar te zoeken, retourneert deze functie eenvoudigweg een lege string. Vervolgens de richtlijn erbij betrekken met een leeg tekenreeksargument wordt genegeerd zonder een fout te veroorzaken. Nu kun je componeren nieuwe optie makefile voor mijn "hypothetische" project:

  • voorbeeld_3-auto_depend /
    • hoofd.cpp
    • hoofd.h
    • Editor.cpp
    • Redacteur.h
    • TextLine.cpp
    • TextLine.h
    • Maakbestand

Dit is hoe het eruit ziet Maakbestand uit dit voorbeeld:

# # example_3-auto_depend/Makefile # # Voorbeeld van het automatisch opbouwen van afhankelijkheden uit headerbestanden # iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp)) gcc $^ -o $@ %.o: % .cpp gcc -c -MD $< include $(wildcard *.d)

Na voltooiing van de werkzaamheden maken De projectmap zal er als volgt uitzien:

  • voorbeeld_3-auto_depend /
    • iBewerken
    • hoofd.cpp
    • hoofd.h
    • hoofd.o
    • hoofd.d
    • Editor.cpp
    • Redacteur.o
    • Redacteur.d
    • Redacteur.h
    • TextLine.cpp
    • TextLine.o
    • TextLine.d
    • TextLine.h
    • Maakbestand

Bestanden met de extensie " .D" worden door een compiler gegenereerd GCC afhankelijkheidsbestanden. Zo ziet het bestand er bijvoorbeeld uit Redacteur.d, waarin de afhankelijkheden voor het bestand worden vermeld Editor.cpp

Editor.o: Editor.cpp Editor.h TextLine.h

Wanneer u nu een van de bestanden wijzigt - Editor.cpp, Redacteur.h of TextLine.h, bestand Editor.cpp wordt opnieuw gecompileerd om een ​​nieuwe versie van het bestand te produceren Redacteur.o.

Heeft de beschreven techniek nadelen? Ja, helaas is er één nadeel. Gelukkig is het naar mijn mening niet al te belangrijk. Het punt is dat het nut maken verwerkt het makefile "in twee stappen". De richtlijn wordt eerst verwerkt erbij betrekken en de afhankelijkheidsbestanden zullen worden opgenomen in de makefile, en dan, bij de “tweede doorgang”, zullen ze al worden uitgevoerd noodzakelijke acties om het project te bouwen.

Het blijkt dat de “huidige” build afhankelijkheidsbestanden gebruikt die zijn gegenereerd tijdens de “vorige” build. Meestal levert dit geen problemen op. Er zullen alleen problemen optreden als een van de headerbestanden om de een of andere reden niet meer bestaat. Laten we naar een eenvoudig voorbeeld kijken. Laten we zeggen dat ik bestanden heb hoofd.cpp En hoofd.h:

Bestand hoofd.cpp:

#include "main.h" void main() ( )

Bestand hoofd.h:

// hoofd.h

In dit geval het afhankelijkheidsbestand dat door de compiler is gegenereerd hoofd.d zal er als volgt uitzien:

Main.o: main.cpp main.h

Als ik nu het bestand hernoem hoofd.h V hoofd_2.h en wijzig het bestand dienovereenkomstig hoofd.cpp

Bestand hoofd.cpp:

#include "main_2.h" void main() ( )

Dat volgende montage project zal mislukken omdat het afhankelijkheidsbestand hoofd.d verwijst naar een headerbestand dat niet meer bestaat hoofd.h.

De oplossing voor deze situatie is het verwijderen van het afhankelijkheidsbestand hoofd.d. Vervolgens wordt het project normaal gebouwd en gemaakt een nieuwe versie dit bestand, waarbij al wordt verwezen naar het headerbestand hoofd_2.h:

Main.o: main.cpp main_2.h

Als u een “populair” headerbestand hernoemt of verwijdert, kunt u het project eenvoudigweg opnieuw opbouwen, waarbij u eerst alle objectbestanden en afhankelijkheidsbestanden verwijdert.

1.5. "Distributie" van bestanden met bronteksten in mappen

Het makefile uit de vorige paragraaf is behoorlijk functioneel en kan met succes worden gebruikt om kleine programma's te bouwen. Naarmate de omvang van het programma echter toeneemt, wordt het niet erg handig om alle bronbestanden in één map op te slaan. In dit geval geef ik er de voorkeur aan om ze te ‘splitsen’ in verschillende mappen die de logische structuur van het project weerspiegelen. Om dit te doen moet je de makefile enigszins aanpassen. Om de impliciete regel te maken

%.o: %.cpp gcc -c $<

bleef werken, ik gebruik een variabele VPATH, waarin alle mappen worden vermeld waarin de bronteksten zich kunnen bevinden. In het volgende voorbeeld heb ik de bestanden geplaatst Editor.cpp En Redacteur.h naar de catalogus Editor en de bestanden TextLine.cpp En TextLine.h naar de catalogus Tekstlijn:

  • voorbeeld_4-multidir/
    • hoofd.cpp
    • hoofd.h
    • Editor/
      • Editor.cpp
      • Redacteur.h
    • Tekstregel/
      • TextLine.cpp
      • TextLine.h
    • Maakbestand

Dit is hoe het eruit ziet Maakbestand voor dit voorbeeld:

# # example_4-multidir/Makefile # # Een voorbeeld van het “verdelen” van bronteksten in verschillende mappen # source_dirs:= . Editor TextLine search_wildcards:= $(addsuffix /*.cpp,$(source_dirs)) iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcards)))) gcc $^ -o $ @VPATH:= $(bron_dirs) %.o: %.cpp gcc -c -MD $(addprefix -I,$(bron_dirs)) $< include $(wildcard *.d)

Vergeleken met vorige versie makefile heeft de volgende wijzigingen ondergaan:

  • Om de lijst met mappen met broncodes op te slaan, heb ik een aparte variabele gemaakt bron_mappen, aangezien deze lijst op verschillende plaatsen moet worden gespecificeerd.
  • Zoekpatroon voor een functie wildcard(variabel zoek_wildcards) is "dynamisch" gebouwd op basis van de lijst met mappen bron_mappen
  • Variabele wordt gebruikt VPATH zodat de sjabloonregel kan zoeken naar bronbestanden in de opgegeven lijst met mappen
  • De compiler mag in alle bronmappen naar headerbestanden zoeken. Gebruik hiervoor de functie voorvoegsel toevoegen en selectievakje -I compiler GCC.
  • Bij het genereren van een lijst met objectbestanden wordt de naam van de map waarin ze zich bevinden “verwijderd” uit de namen van de bronbestanden (met behulp van de functie nietdir)

1.6. Een programma bouwen met verschillende compilatieopties

Vaak is het nodig om meerdere versies van een programma te verkrijgen die verschillend zijn samengesteld. Typisch voorbeeld- debug- en werkversies van het programma. In dergelijke gevallen gebruik ik een eenvoudige techniek:

  • Alle versies van het programma zijn gebouwd met hetzelfde makefile.
  • De noodzakelijke compilerinstellingen "komen" in het makefile via parameters die aan het programma worden doorgegeven maken V opdrachtregel.

Voor elke programmaconfiguratie maak ik een klein batchbestand dat aanroept maken met de nodige parameters:

  • voorbeeld_5-multiconfig/
    • hoofd.cpp
    • hoofd.h
    • Editor/
      • Editor.cpp
      • Redacteur.h
    • Tekstregel/
      • TextLine.cpp
      • TextLine.h
    • Maakbestand
    • make_debug
    • make_release

Bestanden make_debug En make_release- dit zijn batchbestanden die worden gebruikt om respectievelijk de debug- en werkversies van het programma te bouwen. Hier ziet u bijvoorbeeld hoe het batchbestand eruit ziet make_release

Maak compile_flags = "-O3 -funroll-loops -fomit-frame-pointer"

Houd er rekening mee dat de regel met de variabele waarde compile_flags staat tussen aanhalingstekens omdat het spaties bevat. Batch bestand make_debug lijkt op elkaar:

Maak compile_flags = "-O0 -g"

Dit is hoe het eruit ziet Maakbestand voor dit voorbeeld:

# # example_5-multiconfig/Makefile # # Een voorbeeld van het verkrijgen van meerdere versies van een programma met behulp van één makefile # source_dirs:= . Editor TextLine search_wildcards:= $(addsuffix /*.cpp,$(source_dirs)) overschrijft compile_flags += -pipe iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcards)))) gcc $^ -o $@ VPATH:= $(bron_dirs) %.o: %.cpp gcc -c -MD $(addprefix -I,$(bron_dirs)) $(compile_flags) $< include $(wildcard *.d)

Variabel compile_flags krijgt zijn waarde van de opdrachtregel en wordt vervolgens gebruikt bij het compileren van de broncode. Om de compiler te versnellen, wordt een vlag toegevoegd aan de compilatieparameters -pijp. Houd er rekening mee dat u de richtlijn moet gebruiken overschrijven om een ​​variabele te veranderen compile_flags in het make-bestand.

1.7. "Distributie" van verschillende versies van het programma in afzonderlijke mappen

In het geval dat ik meerdere versies van hetzelfde programma compileer (bijvoorbeeld een debug- en een productieversie), wordt het lastig om de compilatieresultaten in dezelfde map te plaatsen. Wanneer u van de ene versie naar de andere gaat, moet u het programma volledig opnieuw compileren om ongewenste “vermenging” van objectbestanden van verschillende versies te voorkomen.

Om dit probleem op te lossen plaats ik de compilatieresultaten van elke versie van het programma in een eigen map. Zo wordt bijvoorbeeld de debug-versie van het programma (inclusief alle objectbestanden) in de map geplaatst debuggen en de werkende versie van het programma wordt in de map geplaatst uitgave:

    • debuggen/
    • uitgave/
    • hoofd.cpp
    • hoofd.h
    • Editor/
      • Editor.cpp
      • Redacteur.h
    • Tekstregel/
      • TextLine.cpp
      • TextLine.h
    • Maakbestand
    • make_debug
    • make_release

De grootste moeilijkheid was om het programma te krijgen maken plaats de resultaten van het werk in verschillende mappen. Na geprobeerd te hebben verschillende varianten, kwam ik tot de conclusie dat de eenvoudigste manier het gebruik van een selectievakje is --map bij het bellen maken. Deze vlag zorgt ervoor dat het hulpprogramma de map die op de opdrachtregel is opgegeven, "current" maakt voordat het makefile wordt verwerkt.

Hier ziet u bijvoorbeeld hoe het batchbestand eruit ziet make_release, dat een werkende versie van het programma compileert (de resultaten van de compilatie worden in de directory uitgave):

Mkdir release make compile_flags="-O3 -funroll-loops -fomit-frame-pointer" \ --directory=release \ --makefile=../Makefile

Team mkdir gemakshalve ingevoerd - als u de map verwijdert uitgave, waarna het tijdens de volgende build opnieuw wordt gemaakt. In het geval van een "samengestelde" mapnaam (bijvoorbeeld bak/vrijgave) kunt u bovendien het selectievakje gebruiken -P. Selectievakje --map krachten maken Voordat u met het werk begint, maakt u de opgegeven map uitgave huidig. Selectievakje --makebestand zal het programma aangeven maken, waar het makefile van het project zich bevindt. Ten opzichte van de "huidige" map uitgave, zal deze zich in de "bovenliggende" map bevinden.

Batchbestand voor het samenstellen van een debug-versie van het programma ( make_debug) lijkt op elkaar. Het enige verschil zit in de naam van de map waar de compilatieresultaten worden geplaatst ( debuggen) en nog een set compilatievlaggen:

Mkdir debug make compile_flags="-O0 -g" \ --directory=debug \ --makefile=../Makefile

Hier is het laatste makefile om het "hypothetische" project te bouwen teksteditor:

# # example_6-multiconfig-multidir/Makefile # # Een voorbeeld van het “verspreiden” van verschillende versies van een programma in afzonderlijke mappen # program_name:= iEdit source_dirs:= . Editor TextLine source_dirs:= $(addprefix ../,$(source_dirs)) search_wildcards:= $(addsuffix /*.cpp,$(source_dirs)) $(programma_naam):$(notdir$(patsubst %.cpp,%. o,$(wildcard $(search_wildcards)))) gcc $^ -o $@ VPATH:= $(source_dirs) %.o: %.cpp gcc -c -MD $(compile_flags) $(addprefix -I,$( bron_dirs)) $< include $(wildcard *.d)

In deze definitieve versie heb ik de naam van het uitvoerbare bestand van het programma "verplaatst" naar een afzonderlijke variabele programma naam. Om dit makefile aan te passen om een ​​ander programma te bouwen, hoeft u nu alleen de eerste paar regels te wijzigen.

Na de lancering batchbestanden make_debug En make_release De map met het laatste voorbeeld ziet er als volgt uit:

  • voorbeeld_6-multiconfig-multidir/
    • debuggen/
      • iBewerken
      • hoofd.o
      • hoofd.d
      • Redacteur.o
      • Redacteur.d
      • TextLine.o
      • TextLine.d
    • uitgave/
      • iBewerken
      • hoofd.o
      • hoofd.d
      • Redacteur.o
      • Redacteur.d
      • TextLine.o
      • TextLine.d
    • hoofd.cpp
    • hoofd.h
    • Editor/
      • Editor.cpp
      • Redacteur.h
    • Tekstregel/
      • TextLine.cpp
      • TextLine.h
    • maakbestand
    • make_debug
    • make_release

Het is te zien dat de objectbestanden voor de werk- en debug-configuratie van het programma in verschillende mappen zijn geplaatst. Kant-en-klare uitvoerbare bestanden en afhankelijkheidsbestanden gaan daar ook naartoe.

In dit hoofdstuk heb ik mijn methodologie voor het werken met makefiles uiteengezet. De overige hoofdstukken zijn min of meer "aanvullend" van aard.

2. GNU-merk

In dit hoofdstuk zal ik enkele functies van het programma kort beschrijven. GNU-merk, die ik gebruik bij het schrijven van mijn makefiles, en zal ook wijzen op de verschillen met de “traditionele” versies maken. Er wordt aangenomen dat u bekend bent met het werkingsprincipe soortgelijke programma's. Lees anders eerst even

GNU-merk- dit is de programmaversie maken gedistribueerd Stichting Vrije Software (Vrij Software Stichting - FSF) Binnen de projectgrenzen GNU ( www.gnu.org). U kunt de nieuwste versie van het programma en de documentatie verkrijgen op de startpagina van het programma www.gnu.org/software/make of op de pagina Paul D.Smith- een van de auteurs GNU-merk ( www.paulandlesley.org/gmake).

Programma GNU-merk heeft zeer gedetailleerde en goed geschreven documentatie die ik ten zeerste aanbeveel om te bekijken. Als u geen toegang tot internet heeft, gebruik dan de documentatie in het formaat Info, die in uw distributie moet worden opgenomen Linux. Wees voorzichtig met manpaginadocumentatie ( mens maken) - het bevat in de regel slechts fragmentarische en zeer verouderde informatie.

2.1. Twee soorten variabelen

GNU-merk ondersteunt twee manieren om variabelen in te stellen, die qua betekenis enigszins verschillen. De eerste manier is traditioneel, met behulp van de operator " = ":

Compile_flags = -O3 -funroll-loops -fomit-frame-pointer

Deze methode wordt ondersteund door alle versies van het hulpprogramma. maken. Het kan bijvoorbeeld worden vergeleken met het definiëren van een macro in de taal Si

#define compile_flags "-O3 -funroll-loops -fomit-frame-pointer"

De waarde van een variabele die is opgegeven met de operator " = ", wordt berekend op het moment dat het wordt gebruikt. Bijvoorbeeld bij het verwerken van een makefile:

Var1 = één var2 = $(var1) twee var1 = drie allemaal: @echo $(var2)

De regel "drie twee" wordt op het scherm weergegeven. Variabele waarde var2 wordt onmiddellijk berekend op het moment dat de opdracht wordt uitgevoerd echo en zal vertegenwoordigen huidig variabele waarde var1, waaraan de regel wordt toegevoegd "twee". Als gevolg hiervan kan dezelfde variabele niet tegelijkertijd aan de linker- en rechterkant van de uitdrukking verschijnen, omdat dit tot oneindige recursie kan leiden. GNU-merk herkent dergelijke situaties en onderbreekt de verwerking van het makefile. Het volgende voorbeeld levert een fout op:

Compile_flags = -pipe $(compile_flags)

GNU-merk ondersteunt ook de tweede, nieuwe manier om een ​​variabele in te stellen - met behulp van de operator " := ":

Compile_flags:= -O3 -funroll-loops -fomit-frame-pointer

In dit geval werkt de variabele als "gewone" tekstvariabelen in elke programmeertaal. Hier is een geschatte analoog van deze uitdrukking in de taal C++

String compile_flags = "-O3 -funroll-loops -fomit-frame-pointer";

De waarde van de variabele wordt berekend wanneer de toewijzingsoperator wordt verwerkt. Als we bijvoorbeeld schrijven

Var1:= één var2:= $(var1) twee var1:= drie allemaal: @echo $(var2)

Wanneer een dergelijk makefile wordt verwerkt, wordt de regel “one two” op het scherm weergegeven.

Een variabele kan zijn gedrag "veranderen", afhankelijk van welke toewijzingsoperator er het laatst op is toegepast. Dezelfde variabele kan zich gedurende zijn hele leven zowel als een “macro” als als een “tekstvariabele” gedragen.

Ik schrijf al mijn makefiles met de operator " := ". Deze methode lijkt mij handiger en betrouwbaarder. Bovendien is het efficiënter, omdat de waarde van de variabele niet elke keer opnieuw wordt berekend als deze wordt gebruikt. Meer informatie over de twee manieren om variabelen in te stellen kunt u vinden in de documentatie op GNU-merk In hoofdstuk " De twee smaken van variabelen" .

2.2. Functies voor tekstmanipulatie

Nutsvoorziening GNU-merk bevat een groot aantal handige functies die tekstreeksen en bestandsnamen manipuleren. In het bijzonder gebruik ik in mijn makefiles de functies voorvoegsel toevoegen, voegt achtervoegsel toe, wildcard, nietdir En patsubst. De syntaxis die wordt gebruikt om functies aan te roepen is

$(functie_naam parameter1, parameter2 ...)

Functie voorvoegsel toevoegen behandelt de tweede parameter als een lijst met woorden gescheiden door spaties. Aan het begin van elk woord wordt de string toegevoegd die als eerste parameter wordt doorgegeven. Als resultaat van het uitvoeren van de makefile:

Src_dirs:= Editor Tekstregel src_dirs:= $(addprefix ../../, $(src_dirs)) alles: @echo $(src_dirs)

wordt op het scherm weergegeven

../../Editor ../../TextLine

Je kunt zien dat elke mapnaam het voorvoegsel ' ../../ ". Functie voorvoegsel toevoegen wordt besproken in het gedeelte "Functies voor bestandsnamen" van de handleiding. GNU-merk.

Functie voegt achtervoegsel toe werkt vergelijkbaar met de functie voorvoegsel toevoegen, voegt alleen de opgegeven tekenreeks toe aan het einde van elk woord. Als resultaat van het uitvoeren van de makefile:

Source_dirs:= Editor TextLine search_wildcards:= $(addsuffix /*.cpp, $(source_dirs)) alles: @echo $(search_wildcards)

wordt op het scherm weergegeven

Editor/*.cpp TextLine/*.cpp

Je kunt zien dat elke mapnaam het achtervoegsel " /*.cpp". Functie voegt achtervoegsel toe wordt besproken in het gedeelte "Functies voor bestandsnamen" van de handleiding. GNU-merk.

Functie wildcard"breidt" een sjabloon of meerdere sjablonen die eraan zijn doorgegeven uit in een lijst met bestanden die overeenkomen met deze sjablonen. Laat het in de directory staan Editor er is een bestand Editor.cpp en in de map Tekstlijn- bestand TextLine.cpp:

  • wildcard_voorbeeld/
    • Editor/
      • Editor.cpp
    • Tekstregel/
      • TextLine.cpp
    • maakbestand

Als resultaat van het uitvoeren van dit makefile:

Search_wildcards:= Editor/*.cpp TextLine/*.cpp source_files:= $(wildcard $(search_wildcards)) alles: @echo $(source_files)

wordt op het scherm weergegeven

Editor/Editor.cpp TextLine/TextLine.cpp

U kunt zien dat de sjablonen zijn omgezet in lijsten met bestanden. Functie wildcard in detail besproken in de sectie "De functie" wildcard"gidsen voor GNU-merk.

Functie nietdir Hiermee kunt u de naam van de map waarin deze zich bevindt, uit de bestandsnaam “verwijderen”. Als resultaat van het uitvoeren van de makefile:

Source_files:= Editor/Editor.cpp TextLine/TextLine.cpp source_files:= $(notdir $(source_files)) alles: @echo $(source_files)

wordt op het scherm weergegeven

Editor.cpp Tekstlijn.cpp

Het is te zien dat de “paden” naar deze bestanden uit de bestandsnamen zijn verwijderd. Functie nietdir wordt besproken in het gedeelte "Functies voor bestandsnamen" van de handleiding. GNU-merk.

Functie patsubst zorgt ervoor dat je kunt veranderen op de aangegeven manier woorden die bij het patroon passen. Er zijn drie parameters nodig: een patroon, een nieuwe variant van het woord en de originele string. De bronreeks wordt behandeld als een lijst met woorden, gescheiden door een spatie. Elk woord dat overeenkomt met het opgegeven patroon wordt vervangen door een nieuwe versie van het woord. De sjabloon kan het speciale teken "%" gebruiken, wat "een willekeurig aantal willekeurige tekens" betekent. Als het teken "%" voorkomt in de nieuwe woordvariant (tweede parameter), wordt dit vervangen door de tekst die overeenkomt met het teken "%" in de sjabloon. Als resultaat van het uitvoeren van de makefile:

Source_files:= Editor.cpp TextLine.cpp object_files:= $(patsubst %.cpp, %.o, $(source_files)) alles: @echo $(object_files)

wordt op het scherm weergegeven

Editor.o TextLine.o

Het is duidelijk dat alle woorden een einde hebben " .cpp"vervangen door" .O". Functie patsubst heeft een tweede, kortere opnameoptie voor die gevallen waarin u het achtervoegsel van een woord moet wijzigen (bijvoorbeeld de extensie in de bestandsnaam vervangen). Een kortere versie ziet er als volgt uit:

$(variabele_naam:.oude_achtervoegsel=.nieuwe_achtervoegsel)

Met behulp van de “korte” notatie kan het vorige voorbeeld als volgt worden geschreven:

Source_files:= Editor.cpp TextLine.cpp object_files:= $(source_files:.cpp=.o) alles: @echo $(object_files)

Functie patsubst besproken in het gedeelte "Functies voor stringvervanging en analyse" van de handleiding GNU-merk.

2.3. Nieuwe manier om sjabloonregels in te stellen

In "traditionele" versies maken een sjabloonregel wordt gespecificeerd met behulp van constructies als:

Cpp.o: gcc $^ -o $@

Dat wil zeggen, bestanden met bepaalde extensies (" .cpp" En " .O" in dit geval).

GNU-merk ondersteunt een meer universele aanpak: het gebruik van bestandsnaamsjablonen. Het symbool wordt gebruikt om het patroon aan te geven "%" , wat betekent "een reeks tekens van willekeurige lengte". Symbool "%" aan de rechterkant van de regel wordt vervangen door de tekst die overeenkomt met het symbool "%" aan de linkerkant. Profiteren nieuw formulier records, kan het bovenstaande voorbeeld als volgt worden geschreven:

%.o: %.cpp gcc $^ -o $@

In mijn makefiles gebruik ik de nieuwe vorm van het schrijven van sjabloonregels omdat ik het handiger vind (sjabloon- en niet-sjabloonregels hebben nu een vergelijkbare syntaxis) en universeel (je kunt niet alleen bestanden opgeven die verschillen in hun extensies).

2.4. Variabel VPATH

Een variabele gebruiken VPATH u kunt een lijst met mappen opgeven waarin sjabloonregels naar afhankelijkheden zoeken. In het volgende voorbeeld:

VPATH:= Editor Tekstregel %.o: %.cpp gcc -c $<

maken zal zoeken naar bestanden met de extensie " .cpp"eerst binnen huidige map en vervolgens, indien nodig, in submappen Editor En Tekstlijn. Ik gebruik deze functie vaak omdat ik er de voorkeur aan geef de broncode in een hiërarchie van mappen te plaatsen die de logische structuur van het programma weerspiegelen.

Variabel VPATH wordt beschreven in het hoofdstuk "VPATH: zoekpad voor alle afhankelijkheden" van de handleiding GNU-merk. Op de pagina Paul D.Smith er is een artikel met de titel "Hoe u VPAT niet gebruikt

Je hebt waarschijnlijk een vraag: is het mogelijk om deze bestanden niet afzonderlijk te compileren, maar om het hele programma in één keer samen te stellen met één commando? Kan.

gcc berekenen.c main.c -o kalkul -lm

Zou je zeggen dat dit handig is? Handig voor ons programma omdat het uit slechts twee c-bestanden bestaat. Een professioneel programma kan echter uit enkele tientallen van dergelijke bestanden bestaan. Het zou uiterst vervelend zijn om ze allemaal op één regel te typen. Maar er is een mogelijkheid om dit probleem op te lossen. De namen van alle bronbestanden en alle opdrachten voor het bouwen van het programma kunnen in een apart tekstbestand worden geplaatst. En lees ze vanaf daar met één kort commando.

Laten we zo'n tekstbestand maken en gebruiken. Verwijder in de projectmap kalkul2 alle bestanden behalve berekenen.h, berekenen.c, main.c. Maak vervolgens in dezelfde map een bestand aan nieuw bestand, noem het Makefile (geen extensies). Plaats daar de volgende tekst.

Kalkul: bereken.o hoofd.o gcc bereken.o hoofd.o -o kalkul -lm bereken.o: bereken.c bereken.h gcc -c bereken.c hoofd.o: hoofd.c bereken.h gcc -c hoofd .c schoon: rm -f kalkul berekenen.o main.o installeren: cp kalkul /usr/local/bin/kalkul verwijderen: rm -f /usr/local/bin/kalkul

Let op de ingevoerde regels, ingesprongen vanaf de linkerrand. Deze inspringing wordt verkregen met behulp van de Tab-toets. Dat is de enige manier waarop het moet gebeuren! Als u de spatiebalk gebruikt, worden de opdrachten niet uitgevoerd.

Vervolgens geven we een commando dat uit slechts één woord bestaat:

En onmiddellijk verschijnen zowel objectbestanden als de startbare in ons project. Het make-programma is ontworpen om opdrachten in een bestand met de standaardnaam Makefile te interpreteren. Laten we de structuur ervan bekijken.

De Makefile is een lijst met regels. Elke regel begint met een aanwijzer die een "Doel" wordt genoemd. Het wordt gevolgd door een dubbele punt en vervolgens worden afhankelijkheden aangegeven, gescheiden door een spatie. In ons geval is het duidelijk dat het uiteindelijke kalkul-bestand afhankelijk is van de objectbestanden berekenen.o en main.o. Daarom moeten ze worden verzameld voordat kalkul wordt geassembleerd. Na de afhankelijkheden worden opdrachten geschreven. Elke opdracht moet op een aparte regel staan, gescheiden van het begin van de regel door de Tab-toets. De structuur van een Makefile-regel kan zeer complex zijn. Er kunnen variabelen, vertakkende constructies en lussen zijn. Deze kwestie vereist een afzonderlijke gedetailleerde studie.

Als we naar de eerste drie regels kijken, worden ze goed begrepen. Er zijn dezelfde commando's die we al hebben gebruikt. Wat betekenen de regels voor opschonen, installeren en verwijderen?

De opschoningsregel bevat de opdracht rm, waarmee uitvoerbare bestanden en objectbestanden worden verwijderd. De vlag -f betekent dat als het te verwijderen bestand ontbreekt, het programma dit moet negeren zonder berichten weer te geven. De opschoningsregel is dus bedoeld om het project ‘op te schonen’ en het terug te brengen naar de staat waarin het zich bevond vóór het make-commando.

Loop

Er verschenen objectbestanden en een uitvoerbaar bestand. Nu

Voorwerp en uitvoerbaar bestand s verdwenen. Het enige dat overblijft zijn de c-bestanden, het h-bestand en de Makefile zelf. Dat wil zeggen, het project is "schoongemaakt" van de resultaten van de make-opdracht.

De installatieregel plaatst het uitvoerbare bestand in de map /usr/local/bin - de standaardlocatiemap gebruikersprogramma's. Dit betekent dat er vanaf elke locatie gebeld kan worden eenvoudig bellen haar naam. Maar u kunt alleen iets in deze map plaatsen door als “superuser” in het systeem in te loggen. Om dit te doen, moet u de opdracht su geven en het "superuser" -wachtwoord invoeren. Anders geeft het systeem aan dat u de toegang wordt geweigerd. Het verlaten van de “superuser” gebeurt met het exit-commando. Dus,

Nu kunt u dit programma eenvoudig starten door de programmanaam in te voeren, zonder het pad op te geven.

U kunt de map /usr/local/bin openen. Er zou een bestand moeten zijn met de naam kalkul.

Laten we nu ‘onszelf opruimen’ en het systeem niet verstoppen.

Kijk naar de map /usr/local/bin. Het kalkul-bestand is verdwenen. De verwijderingsregel verwijdert dus een programma uit de systeemmap.

Dmitry Panteleichev (dimanix2006 bij rambler dot ru) - Make-files

Ik heb mij altijd aangetrokken gevoeld tot minimalisme. Het idee dat één ding één ding moet doen, maar dan zo goed mogelijk, resulteerde in de creatie van UNIX. En hoewel UNIX niet langer een eenvoudig systeem kan worden genoemd, en het minimalisme daarin niet zo gemakkelijk te zien is, kan het worden beschouwd als een duidelijk voorbeeld van de kwantitatieve en kwalitatieve transformatie van veel eenvoudige en begrijpelijke dingen in één zeer complex en ondoorzichtig systeem. Bij de ontwikkeling heeft make ongeveer hetzelfde pad gevolgd: eenvoud en duidelijkheid, met groeiende schaal, veranderd in een verschrikkelijk monster (denk aan je gevoelens toen je voor het eerst een makefile opende).

Dat ik make lange tijd voortdurend negeerde, was te wijten aan het gemak van de gebruikte IDE’s en de onwil om dit ‘overblijfsel uit het verleden’ (in wezen luiheid) te begrijpen. Al deze vervelende knoppen, menu's, enz. de attributen van allerlei ateliers dwongen mij op zoek te gaan naar een alternatief voor de werkwijze die ik tot nu toe beoefende. Nee, ik ben geen make-goeroe geworden, maar de kennis die ik heb opgedaan is voldoende voor mijn kleine projecten. Dit artikel is bedoeld voor degenen die, zoals ik onlangs, uit de gezellige raamslavernij willen breken en de ascetische maar vrije wereld van de schelp willen betreden.

Merk- basisinformatie

make is een hulpprogramma dat is ontworpen om de conversie van bestanden van de ene vorm naar de andere te automatiseren. De conversieregels worden gespecificeerd in een script genaamd Makefile, dat zich in de hoofdmap van de werkmap van het project moet bevinden. Het script zelf bestaat uit een reeks regels, die op hun beurt worden beschreven:

1) doelen (wat deze regel doet);
2) details (wat nodig is om aan de regel te voldoen en doelen te bereiken);
3) opdrachten (die deze transformaties uitvoeren).

Over het algemeen kan de makefile-syntaxis als volgt worden weergegeven:

# Het inspringen gebeurt uitsluitend met behulp van tabtekens, # elk commando moet worden voorafgegaan door een inspringing<цели>: <реквизиты> <команда #1> ... <команда #n>

Dat wil zeggen, de make-regel is het antwoord op drie vragen:

(Waar maken we het van? (details)) ---> [Hoe maken we het? (opdrachten)] ---> (Wat doen we? (doelen))
Het is gemakkelijk in te zien dat de vertaal- en compilatieprocessen heel mooi in dit diagram passen:

(bronbestanden) ---> [uitzending] ---> (objectbestanden)
(objectbestanden) ---> [link] ---> (uitvoerbare bestanden)

De eenvoudigste Makefile

Laten we zeggen dat we een programma hebben dat uit slechts één bestand bestaat:

/* * hoofd.c */ #include int main() ( printf("Hallo wereld!\n"); return 0; )
Om het te compileren is een heel eenvoudig makefile voldoende:

Hallo: main.c gcc -o hallo main.c
Deze Makefile bestaat uit één regel, die op zijn beurt bestaat uit een doel - "hello", een prop - "main.c", en een commando - "gcc -o hello main.c". Om nu te compileren, hoeft u alleen maar het make-commando in de werkmap uit te voeren. Standaard voert make de allereerste regel uit als het uitvoeringsdoel niet expliciet werd opgegeven toen het werd aangeroepen:

$ maken<цель>

Compilatie uit meerdere bronnen

Laten we aannemen dat we een programma hebben dat uit 2 bestanden bestaat:
hoofd.c
/* * main.c */ int main() ( hallo(); return 0; )
en hallo.c
/* * hallo.c */ #include void hallo() ( printf("Hallo wereld!\n"); )
De Makefile die dit programma compileert, zou er als volgt uit kunnen zien:

Hallo: main.c hallo.c gcc -o hallo main.c hallo.c
Het is behoorlijk functioneel, maar heeft één belangrijk nadeel: we zullen verder onthullen welke.

Incrementele compilatie

Laten we ons voorstellen dat ons programma uit een tiental of twee bronbestanden bestaat. We brengen wijzigingen aan in een van hen en willen deze opnieuw opbouwen. Als u de in het vorige voorbeeld beschreven aanpak gebruikt, worden alle bronbestanden opnieuw gecompileerd, wat een negatieve invloed heeft op de hercompilatietijd. De oplossing is om de compilatie in twee fasen te verdelen: de vertaalfase en de koppelingsfase.

Nu, na het wijzigen van een van de bronbestanden, volstaat het om het te vertalen en alle objectbestanden te koppelen. Tegelijkertijd slaan we de fase over van het vertalen van details die niet door wijzigingen worden beïnvloed, waardoor de compilatietijd in het algemeen wordt verkort. Deze aanpak wordt incrementele compilatie genoemd. Om dit te ondersteunen, vergelijkt het de wijzigingstijd van doelen en hun details (met behulp van bestandssysteemgegevens), waardoor het onafhankelijk beslist welke regels moeten worden uitgevoerd en welke eenvoudigweg kunnen worden genegeerd:

Hoofd.o: hoofd.c gcc -c -o hoofd.o hoofd.c hallo.o: hallo.c gcc -c -o hallo.o hallo.c hallo: hoofd.o hallo.o gcc -o hallo hoofd. o hallo.o
Probeer dit project te bouwen. Om het te bouwen, moet u het doel expliciet specificeren, d.w.z. geef het commando hallo.
Wijzig daarna een van de bronbestanden en bouw deze opnieuw. Houd er rekening mee dat tijdens de tweede compilatie alleen het gewijzigde bestand wordt vertaald.

Eenmaal uitgevoerd, zal make meteen proberen het hallo-doel te pakken te krijgen, maar om het te maken heb je de main.o- en hello.o-bestanden nodig, die nog niet bestaan. Daarom zal de uitvoering van de regel worden uitgesteld en zal er worden gezocht naar regels die beschrijven hoe de ontbrekende details kunnen worden verkregen. Zodra alle details zijn ontvangen, zal make terugkeren naar de uitvoering van het uitgestelde doel. Dit betekent dat make de regels recursief uitvoert.

Fictieve doelen

In feite kunnen niet alleen echte bestanden als doelwit fungeren. Iedereen die programma's uit de broncode heeft moeten bouwen, zou bekend moeten zijn met twee standaardopdrachten in de UNIX-wereld:

$ make $ make install
Met het make-commando wordt het programma gecompileerd en met het make install-commando wordt het geïnstalleerd. Deze aanpak is erg handig omdat alles wat nodig is om de applicatie op het doelsysteem te bouwen en te implementeren in één bestand is opgenomen (laten we het configuratiescript even vergeten). Houd er rekening mee dat we in het eerste geval het doel niet specificeren, en in het tweede geval is het doel helemaal niet het maken van het installatiebestand, maar het proces van het installeren van de applicatie op het systeem. Zogenaamde fictieve (nep)doelen stellen ons in staat dergelijke trucs uit te voeren. Hier is een korte lijst met standaarddoelen:

  • alles is het standaarddoel. U hoeft dit niet expliciet op te geven wanneer u make aanroept.
  • clean - maak de map leeg van alle bestanden die zijn verkregen als resultaat van de compilatie.
  • installeren - installatie uitvoeren
  • verwijderen - en respectievelijk verwijderen.
Om te voorkomen dat make naar bestanden met dergelijke namen zoekt, moeten deze in de Makefile worden gedefinieerd met behulp van de .PHONY-richtlijn. Het volgende is een voorbeeld van Makefile met als doel alles, opschonen, installeren en verwijderen:

PHONY: alles schoon installeren alles verwijderen: hallo schoon: rm -rf hallo *.o main.o: main.c gcc -c -o main.o main.c hello.o: hello.c gcc -c -o hallo. o hallo.c hallo: main.o hello.o gcc -o hallo main.o hello.o installeren: install ./hello /usr/local/bin verwijdering: rm -rf /usr/local/bin/hello
Nu kunnen we ons programma bouwen, installeren/verwijderen en ook de werkmap opschonen met behulp van standaard make-doelen.

Merk op dat het all-doel geen opdrachten specificeert; het enige dat ze nodig heeft, zijn de hallo-rekwisieten. Als je de recursieve aard van make kent, is het niet moeilijk je voor te stellen hoe dit script zal werken. Je moet ook speciale aandacht besteden aan het feit dat als het hello-bestand al bestaat (overgelaten na de vorige compilatie) en de details ervan niet zijn gewijzigd, de opdracht make er wordt niets meer in elkaar gezet. Dit is een klassieke hark. Het wijzigen van een headerbestand dat per ongeluk niet in de lijst met details staat, kan bijvoorbeeld vele uren kopzorgen opleveren. Om een ​​volledige herbouw van het project te garanderen, moet u daarom eerst de werkmap leegmaken:

$ maak schoon $ maak
U moet sudo gebruiken om de installatie-/verwijderingsdoelen te voltooien.

Variabelen

Iedereen die bekend is met de DRY-regel (Don't repeat own) heeft waarschijnlijk al gemerkt dat er iets mis is, namelijk dat onze Makefile een groot aantal herhaalde fragmenten bevat, wat tot verwarring kan leiden bij volgende pogingen om het uit te breiden of te wijzigen. In imperatieve talen hiervoor hebben we variabelen en constanten; variabelen in make worden strings genoemd en zijn heel eenvoudig gedefinieerd:

=
Er is een onuitgesproken regel dat variabelen in hoofdletters moeten worden genoemd, bijvoorbeeld:

SRC = main.c hallo.c
Dit is hoe we de lijst met bronbestanden hebben gedefinieerd. Om de waarde van een variabele te gebruiken, moet er een dereferentie van worden gemaakt met behulp van de $(-constructie ); bijvoorbeeld zo:

Gcc -o hallo $(SRC)
Hieronder staat een makefile die twee variabelen gebruikt: TARGET - om de naam van het doelprogramma te bepalen en PREFIX - om het pad te bepalen om het programma op het systeem te installeren.

TARGET = hallo PREFIX = /usr/local/bin .PHONY: alles schoon installeren alles verwijderen: $(TARGET) schoon: rm -rf $(TARGET) *.o main.o: main.c gcc -c -o main. o main.c hallo.o: hallo.c gcc -c -o hello.o hello.c $(TARGET): main.o hello.o gcc -o $(TARGET) main.o hello.o install: install $ (TARGET) $(PREFIX) verwijderen: rm -rf $(PREFIX)/$(TARGET)
Dit is al mooier. Ik denk dat het bovenstaande voorbeeld geen speciaal commentaar voor je nodig heeft.

Automatische variabelen

Automatische variabelen zijn bedoeld om makefiles te vereenvoudigen, maar hebben naar mijn mening een negatieve invloed op de leesbaarheid ervan. Hoe het ook zij, ik zal hier een paar van de meest gebruikte variabelen opsommen, en wat je ermee moet doen (of of je het überhaupt moet doen) is aan jou:
  • $@ Doelnaam van de regel die wordt verwerkt
  • $< Имя первой зависимости обрабатываемого правила
  • $^ Lijst met alle afhankelijkheden van de verwerkte regel
Als iemand zijn scripts volledig wil verdoezelen, kun je hier inspiratie opdoen: