Przejdź do zawartości

Borland C++ Compiler/MAKE

Z Wikibooks, biblioteki wolnych podręczników.

Wprowadzenie

[edytuj]

MAKE pozwala zautomatyzować proces budowania projektu programistycznego poprzez stawianie odpowiednich zasad dla plików źródłowych i wywoływaniu komend systemowych. Poza tym program ten znacznie przyspiesza proces budowy programu, ponieważ wykonuje operacje tylko na plikach, które zostały zmodyfikowane od czasu ostatniej kompilacji, ma to szczególnie duże znaczenie w przypadku dużych projektów.

Plik Makefile

[edytuj]

Najważniejszy plik dla MAKE. W nim znajdują się wszelkie zasady operacji na źródłach, wywołania programów, zmienne i instrukcje odpowiedzialne za zarządzanie budową projektu. Utworzyć go można w najprostszym edytorze tekstowym (np. w systemowym Notatniku). Należy pamiętać jednak, aby plik zapisać jako makefile.mak lub makefile (bez rozszerzenia). To, którą opcję wybierzesz jest już obojętne.

Wywołanie

[edytuj]

Wywołanie MAKE jest nieco inne niż pozostałych narzędzi Borlanda. W tym przypadku kluczową rolę odgrywa lokalizacja na dysku, z której wywołujesz program. W lokalizacji tej musi się znajdować plik makefile wraz ze skopiowanym (z katalogu \bin naszych FCLT) make.exe. To ważne! Inaczej programy wywoływane z makefile'a nie będą działały poprawnie. Będziesz wywoływać właśnie tę kopię make'a, więc najpierw trzeba wejść w jej lokalizację:

CD <lokalizacja>
MAKE [<opcje>] [<wynik>]
<opcje>
MAKE ma mało opcji, a ponadto w większości z nich można zastąpić odpowiednimi komendami w makefile'u; więcej na ten temat w rozdziale: Plik projektu i konfiguracja MAKE
<wynik>
czyli plik(i) wynikowy/e Twojego projektu, pamiętaj że nie musisz ich tutaj wymieniać

w praktyce wywołanie MAKE może wyglądać tak:

CD c:\Hello
MAKE -a -s

Składnia makefile'a

[edytuj]

Składnia makefile to już swego rodzaju język programowania do budowy projektów programistycznych. Możemy rozróżnić jej kilka podstawowych elementów:

komentarze
po prostu komentarze, tak jak w innych językach programowania
zasady
zasady wg których MAKE dobiera pliki źródłowe i wynikowe po wypełnieniu których wywołuje...

komendy systemowe "siłą" MAKE jest możliwość wywoływania nie tylko narzędzi Borland-a, ale także komend DOS'owych i innych programów takich jak np. kompilator assemblera.

zmienne
tak jak w każdym języku programowania "kontenery" do których możemy przypisać łańcuchy znaków.
dyrektywy
chociaż będę je opisywał na końcu nie powinny być lekceważone, to jedne z najważniejszych elementów w każdym języku programowania, tak samo jest i tutaj.

A tak wygląda przykładowy makefile, z którego korzystasz na co dzień prawdopodobnie nawet o tym nie wiedząc, czyli builtins.mak z BIN'a:

#
# Inprise C++Builder - (C) Copyright 1999 by Borland International
#
CC       = bcc32
RC       = brcc32
AS       = tasm32

!if $d(__NMAKE__)
CXX      = bcc32 -P
CPP      = bcc32 -P
!endif

.asm.obj:
      $(AS) $(AFLAGS) $&.asm

.c.exe:
      $(CC) $(CFLAGS) $&.c

.c.obj:
      $(CC) $(CFLAGS) /c $&.c

.cpp.exe:
      $(CC) $(CFLAGS) $&.cpp

.cpp.obj:
      $(CC) $(CPPFLAGS) /c $&.cpp

.rc.res:
      $(RC) $(RFLAGS) /r $&
  
.SUFFIXES: .exe .obj .asm .c .res .rc

!if !$d(BCEXAMPLEDIR)
BCEXAMPLEDIR = $(MAKEDIR)\..\EXAMPLES
!endif

Czarna magia? W następnych podrozdziałach będzie mowa o najważniejsze elementach składni pliku makefile.

Aby to co pisze nie było tylko czystą teorią wymyśliłem projekt HelloProject, którego makefile będziemy rozwijać w miarę postępów w nauce. Jak zorganizować sobie miejsce pracy nad tym projektem? Należy utworzyć nowy katalog z nazwą projektu: C:\Hello\ Następnie trzeba tam skopiować wszystkie jego pliki źródłowe:

  • Hello.cpp
  • klasa.cpp
  • klasa.h
  • zasoby.rc
  • DEF.def
  • makefile.mak

Wynikiem tego projektu będzie windowsowy EXE'k: Hello.exe. Nieważne, co będzie robił ten program, to tylko ćwiczenie, resztę pozostawiam Tobie, Twojej wyobraźni i inwencji. MAKE będziemy wywoływać tam, gdzie jest makefile, czyli z katalogu c:\Hello\. Tam też powinna znaleźć się kopia programu MAKE.

Komentarze

[edytuj]

W makefile'u tak jak w C++, również możesz używać komentarzy jedno liniowych. Wszystko to co znajdzie się w jednej linii po symbolu hash'a: # MAKE ignoruje.

# to jest komentarz

Zasady ogólne i szczegółowe

[edytuj]

Zasady regulują wywoływanie narzędzi w makefile'u. Ogólnie rzecz biorąc zasady określają jakie pliki ma przetwarzać MAKE, ewentualnie gdzie one się znajdują, gdzie ma się znaleźć wynik operacji. Ważną kwestią w zrozumieniu zasad jest fakt, że zasada to tylko prawo, za wypełnienie którego odpowiedzialny jesteś Ty. Pamiętaj też że zasada przekazuje tylko informacje dla MAKE, natomiast komendy, które za jej pomocą wywołujesz rządzą się już swoimi prawami. Zarówno przy komendach, jak i przy samych zasadach można używać symbolu \ jeśli zabraknie Ci miejsca w linii:

<txt>\
<dok. txt'u>

Ogólny szablon zasady wygląda tak:

<zasada>
 <komenda>
 [<komenda>]
 [...]

Pamiętaj tylko o przynajmniej jednej spacji przed komendą, aby nie znajdowała się ona tuż przy początku linii - to błąd! Błędem jest też nie zapisanie samej zasady przy początku linii. Jednym słowem musi to wyglądać tak, jak na powyższym szablonie. Zasada zacznie "wchodzić w życie" (wywoływać swoje komendy), jeśli jej pliki zależne (źródłowe) będą różniły się datą i czasem ostatniej modyfikacji z datą plików wynikowych lub jeśli pliki wynikowe w ogóle nie będą istnieć. Np. jeśli chcemy skompilować plik Hello.cpp za pomocą zasad to kompilator zostanie wywołany kiedy Hello.cpp i Hello.obj będą miały inne daty modyfikacji lub jeśli plik Hello.obj nie będzie istniał. Jak widać w tytule tego rozdziału zasady możemy podzielić na dwa typy: ogólne i szczegółowe (implicit & explicit rules).

Zasady ogólne (implicit rules)

[edytuj]

Zasady te tyczą się wszystkich plików z określonym rozszerzeniem (ewentualnie możesz określić folder, w którym MAKE ma zezwalać na operacje na nich). Zasady te zakładają tylko zmianę rozszerzenia, a nazwy plików pozostawiają. Implicit rule wygląda tak:

[{<lokalizacja1>}].<roz1>[{<loklalizacja2>}].<roz2>:
 <komenda/y>

gdzie:

<lokalizacja1>
lokalizacja/e plików zależnych (źródłowych) (w przypadku gdy jest ich więcej jako separatora używamy średnika:';')
<roz1>
rozszerzenie plików źródłowych
<lokalizacja2>
lokalizacja/e na dysku plików wynikowych (docelowych) operacji (gdzie mają się one znaleźć) oddzielone jedna od drugiej średnikiem:';'
<roz2>
rozszerzenie plików wynikowych

Na przykład:

{C:\src}.cpp{C:\obj}.obj: 
 bcc32 -c c:\src\hello.cpp
 copy hello.obj c:\obj

Przejdźmy teraz do analizy tych trzech linijek kodu. Pozwoli Ci to lepiej zrozumieć działanie zasad make'a. W tym przypadku zasada zezwala na operacje na plikach *.cpp tylko z katalogu c:\src, a ich wynik w magiczny sposób musi znaleźć się w c:\obj, oczywiście nie da się tego dokonać bez pomocy systemowego słowa kluczowego COPY albo użycia w parametrach wywołania kompilatora opcji -n. Jeśli nie określisz w zasadzie tych lokalizacji, to dozwolone będą operacje na wszystkich plikach na całym Twoim twardzielu. Określając w zasadzie lokalizacje plików źródłowych nie łudź się że zostanie to przekazane komendom które wywołujesz, dlatego podczas kompilacji zapisałem plik hello.cpp wraz jego z lokalizacją, oczywiście nie musiałbym tego robić, gdybyśmy odpalili MAKE z lokalizacji c:\src.

Zasady szczegółowe (explicit rules)

[edytuj]

Zasady szczegółowe określają z jakich plików źródłowych powstaje plik wynikowy. W tym przypadku musimy podać dokładne nazwy plików a nie jak to było w przypadku zasad ogólnych samego rozszerzenia plików. Spójrzmy na składnię:

<wynik(i)>:[:] [{<lokalizacja>}]<src>
 <komenda/y>
<wynik(i)>
nazwa pliku/ów wynikowego/ych, który ma być wygenerowany
<lokalizacja>
lokalizacja/e plików źródłowych oddzielone od siebie średnikami:';'
<src>
plik(i) źródłowy/e

Przykład powinien rozwiać wszelkie wątpliwości:

Hello.exe: {C:\obj}Hello.obj klasa.obj
 ilink32 /aa /Tpe c0w32.obj C:\obj\Hello.obj C:\obj\klasa.obj,\
Hello.exe,,cw32.lib import32.lib,,

Zasada zakłada że Hello.obj i klasa.obj znajdują się tylko i wyłącznie w katalogu c:\obj. Tak jak w zasadach ogólnych tak i tutaj ta lokalizacja nie ma nic wspólnego z komendami - i w tym przypadku musisz podać lokalizacje każdego ze swoich plików źródłowych (oczywiście jeśli znajdują się one tam gdzie makefile nie musisz tego robić). Zmieni się to dopiero po wprowadzeniu do kodu inteligentnych zmiennych które będą potrafiły czerpać informacje z zasad, ale o tym później.

Wszystkie pliki docelowe (wynikowe) z zasad ogólnych powinny być automatycznie plikami źródłowymi w zasadzie szczegółowej. Inaczej MAKE danej zasady ogólnej nie weźmie pod uwagę. Pokaże to na przykładzie:

.cpp.obj:
 bcc32 -c Hello.cpp
 
.rc.res:              # MAKE nie zwraca na tę zasadę uwagi, bo nie dodałeś
 brcc32 zasoby.rc     # pliku zasoby.res do src zasady szczegółowej

Hello.exe: Hello.obj
 ilink32 /aa /Tpe c0w32.obj Hello.obj klasa.obj,Hello.exe,,\
cw32.lib import32.lib,,zasoby.res

W tym przypadku konsolidator zgłosi błąd, że nie może znaleźć pliku zasoby.res, nic dziwnego - BRCC32 wcale nie został wywołany.
MAKE zakłada, iż zawsze dysponujemy tylko jednym plikiem wynikowym, ale i temu można zaradzić tworząc symboliczne pliki wynikowe o tak:

ALL: <PlikWynikowy1> <PlikWynikowy2> [<Plikwynikowy3> [...]]

Teraz nic nie stoi na przeszkodzie, aby wcześniejszy niefortunny kod zamienić na poniższy:

ALL: zasoby.res Hello.exe

.cpp.obj:
 bcc32 -c Hello.cpp

zasoby.res: zasoby.rc
 brcc32 zasoby.rc

Hello.exe: Hello.obj
 ilink32 /aa /Tpe c0w32.obj Hello.obj klasa.obj,Hello.exe,,\
cw32.lib import32.lib,,zasoby.res

Oczywiście to już od Ciebie zależy jakich zasad wolisz używać. Moim zadaniem jednak warto użyć wielu zasad ogólnych, a tylko jedną szczegółową, chyba że musisz w jednym projekcie mieć dwa pliki EXE (lub DLL). Jest też możliwość przypisania wielu zasad do jednego pliku wynikowego używamy wtedy podwójnego dwukropka:

mylib.lib:: obj1.obj obj2.obj
  tlib mylib +obj1.obj +obj2.obj

mylib.lib:: obj3.obj obj4.obj
  implib mylib +lib3.obj +lib4.obj

Najpierw do biblioteki statycznej mylib.lib dodawane są obj1.obj i obj2.obj, następnie w drugiej zasadzie do tej samej biblioteki oddajemy: obj3.obj oraz obj4.obj.

Zasada "bez zasad" :-)

[edytuj]

Jest jeszcze jeden typ zasad. Właściwie to nie wiem, czy można by nazywać to zasadą, bo nie określa żadnych praw dostępu do plików. Jej składnia jest miej więcej taka:

<nazwa>:
 <komenda/y>

Przydaje się ona do wywołania narzędzi typu debugger lub TOUCH które nie czynią żadnych zmian w rozszerzeniu pliku zatem nie można ich normalnie wywołać z jakichkolwiek wcześniejszych zasad. Poza tym zasada ta zawsze "wejdzie w życie", gdyż nie mamy tu żadnych plików zależnych ani wynikowych. Pewnym jej ograniczeniem jest to że po zakończeniu jej działań MAKE także kończy. Cóż, nie można mieć wszystkiego ;). Dla przykładu podaje wywołanie programu TOUCH:

TOUCH:
 cd c:\Hello
 touch *.cpp *.rc

Komendy systemowe

[edytuj]

Na co nam zasady skoro i tak nigdy nie zostaną poparte działaniami? - na nic. Dlatego, aby coś zaczęło działać trzeba to...wywołać :). Jak już pisałem oprócz zwykłych wywołań narzędzi z FCLT można tu też umieszczać komendy systemowe, czy włączać inne program takie jak np. kompilator assemblera, czy swój własny program już po skompilowaniu. Pamiętaj jednak, że komendy nie mogą występować w makefile'u od tak sobie, muszą one zawierać się w jakiejś zasadzie tylko w ten sposób można wywoływać narzędzia przez MAKE, inaczej program (MAKE) zgłosi błąd. Poza tym przy wywołaniu 'czegoś' można użyć następujących przedrostków:

@ wywołanie danego narzędzia nie ma być wyświetlane w linii poleceń, MAKE zawsze przed zastosowaniem jakiejś komendy najpierw wyświetla na ekranie jej wywołanie, ten przedrostek temu zapobiega
- kontynuuj działnia nawet jeśli narzędzie zgłosi błędy np. zapis:
-bcc32 -c hello.cpp
sprawi, że nawet jeśli w kodzie źródłowym hello.cpp będzie błąd MAKE nie zakończy procesu budowy projektu.
-<liczba> w miejsce <liczba> wpisz liczbę, po zwróceniu której make ma przestać dalej działać (wywoływać); np.:
-0bcc32 -c hello.cpp
da nam zakończenie działania programu gdy BCC32 zakończy działanie kodem wyjścia równym 0

Właściwie to już możesz spróbować napisać swój własny, prosty makefile, który zautomatyzuje wywołanie kompilatora, kompilatora zasobów i konsolidatora.

#----------------------------
# (c) 2003 by Karol Ossowski
#----------------------------
# >>   Hello Project     << #
#----------------------------

.cpp.obj:
 BCC32 -tW -c -q -w-par -w-rvl Hello.cpp klasa.cpp

.rc.res:
 BRCC32 zasoby.rc

Hello.exe: Hello.obj klasa.obj zasoby.res
 ILINK32 /aa /Tpe /Gk /t /q -w-dup c0w32.obj Hello.obj\
klasa.obj,Hello.exe,,cw32.lib import32.lib,DEF.def,\
zasoby.res

Ten makefile jest dość prymitywny, jednak automatyzuje on już całą budowę projektu HelloProject. Spróbuj go odpalić samemu.

Zmienne

[edytuj]

W makefile'u możemy deklarować zmienne gdzie umieścimy pewne łańcuchy znaków, później te "stringi" (z ang. string znaczy łańcuch) będziemy mogli wykorzystać wpisując nazwę zmiennej. Przypisywanie wartości do zmiennej wygląda tak:

<nazwa> = [<łańcuch_znaków>]

Zwroć uwagę, że przypisywany łańcuch nie jest ujęty w "" tak jak to jest w C/C++, wszystko to co znajdzie się za znakiem równości: = w jednej linii zostanie przypisane do zmiennej <nazwa>. np.:

SRC = Hello.cpp klasa.cpp

Wykorzystanie tej zmiennej w makefile'u będzie za to przypominać języki skryptowe takie jak Perl czy Bash:

.cpp.obj:
 bcc32 -c $(SRC)

I tak każda zmienna przez Ciebie zadeklarowane będzie poprzedzane '$' i ujęte w nawiasy '()', ale analizując te dwa przykłady już możesz odkryć pożyteczność tego wynalazku: deklarując na początku zmienną SRC nie musisz pamiętać już wszystkich plików źródłowych, które wykorzystasz w swoim programie, nawet jeśli projekt się rozrośnie nie będziesz musiał dopisywać w każdym wywołaniu korzystającym ze źródeł jeszcze jednego pliku, ale wystarczy, że na samym początku dopiszesz jeszcze jeden plik *.cpp, a zmienna będzie wtedy wskazywała już na dwa pliki źródłowe.
Jak już pisałem zmienne to po prostu łańcuchy znaków, więc możemy przypisać im również opcje, jakie chcemy wykorzystać przy wywoływaniu narzędzi:

C_FLAGS = -tW -c -v -w-par -w-rvl

Teraz wywołanie BCC32 będzie w makefile'u wyglądać tak:

.cpp.obj:
 bcc32 $(C_FLAGS) $(SRC)

Zmiana wartości zmiennej

[edytuj]

W każdym zapamiętanym w zmiennej "stringu" można dokonać zamiany "podstringów". Robimy to korzystając z następującego szablonu:

$(<nazwa_zm>:<stary_str>=<nowy_str>)

Na przykład można to wykorzystać do zmiany rozszerzenia plików z jakich korzystamy:

SRC = Hello.cpp klasa.cpp
$(SRC:.cpp=.obj)

Teraz nie musisz pisać już dodatkowej zmiennej z wynikami konslolidacji - zmienna $(SRC) możemy wykorzystać do wywołania kompilatora, a do wywołania konsolidatora - $(SRC:.cpp=.obj).

Specjalne zmienne

[edytuj]

Istnieją jeszcze zmienne specjalne, które są już stworzone przez MAKE i możesz je używać w komendach tuż po zasadach. Są one bardzo wygodne:

zmienna przekazywane dane w zasadach ogólnych przekazywane dane w zasadach szczegółowych
$* plik źródłowy (bez rozszerzenia) z lokalizacją plik wynikowy (bez rozszerzenia) z lokalizacją
$< plik źródłowy+rozszerzenie z lokalizacją plik wynikowy+rozszerzenie z lokalizacją
$: lokalizacja plików źródłowych lokalizacja pliku wynikowego
$. plik źródłowy+rozszerzenie plik wynikowy+rozszerzenie
$& plik źródłowy plik wynikowy
$@ plik wynikowy+rozszerzenie z lokalizacją to samo co $<
$** to samo co $< wszystkie pliki źródłowe+rozszerzenie
$? to samo co $< stare pliki źródłowe

Przyznajcie, że zmieniają one wręcz filozofie zasad, które stają się nie tylko prawami dostępu do plików, ale i źródłem informacji dla komend np. o lokalizacji plików. Przykład użycia specjalnych makr:

{c:\cpp}.cpp.obj:
 bcc32 -q -c $<

ten kod spowoduje kompilacje wszystkich plików *.cpp w katalogu c:\cpp, a pliki *.obj, które wygeneruje umieści w lokalizacji w której znajduje makefile. To nie wszystko - zmiennym specjalnym można też zmieniać wartości stosując specjalne znaczniki. Znaczniki te są najczęściej używane przy zmiennych: $@ i $<. Dodanie takiego znacznika modyfikującego wartość danej zmiennej powinno wyglądać tak:

$(<znak specjalnego makra>:[<znacznik>])

Oto typy znaczników do standardowych makr MAKE'a:

znacznik przekazywane dane o pliku przykład użycia przykładowy rezultat dla pliku C:\Proj\kod.cpp
D lokalizacja $(<D) C:\Proj\
F nazwa i rozszerzenie $(@F) kod.cpp
B nazwa $(<B) kod
R lokalizacja,nazwa $(@R) C:\Proj\kod

i jeszcze przykład:

{c:\cpp}.cpp{c:\obj}.obj:
 bcc32 -q -c -n$(@D) $<

ten fragment skryptu spowodowałby kompilację plików *.cpp z katalogu c:\cpp i zapisywałby pliki *.obj do katalogu c:\obj.

Twoje zmienne powinny być w szczególności używane przy zasadach jakie będziesz stawiał, a zmienne specjalne w komendach np.:

$(BIN): $(SRC:.cpp=.obj) 
 ILINK32 $(L_FLAGS) c0w32.obj $**,$.,,cw32.lib import32.lib $(LIB),$(DEF),$(RES)

Używając zmiennych $** lub $? w zasadzie szczegółowej (explicit rule) można do komendy dodać przedrostek &, który będzie oznaczał, że komenda odnosi się tylko do jednego pliku z pola <src> (normalnie makra $** i $? w zasadach szczegółowych oznaczają wszystkie pliki źródłowe, co uniemożliwi pracę niektórych komend) np.:

hello.exe: hello.obj klasa.obj
 copy $** c:\obj

Po takiej operacji komenda systemowa COPY zgłosi błąd, ponieważ $** wskazuje na pliki hello.obj i klasa.obj, a COPY może obsługiwać tylko jeden plik. Ten błąd da się naprawić:

hello.exe: hello.obj klasa.obj
 &copy  $** c:\obj

Teraz system wykona dwie operacje zamiast jednej:

copy hello.obj c:\obj
copy klasa.obj c:\obj

..aha przed przystąpieniem do kolejnego rozdziału rozwiniemy nasz HelloProject o obsługe zmiennych. Aby zachować pewien porządek w katalogu c:\Hello najpierw stworzymy następujące podkatalogi,a w nich zawrzemy pliki do projektu HelloProject:

.\src\ <-- Hello.cpp klasa.cpp
.\src\hdr\ <-- klasa.h
.\src\res\ <-- zasoby.res
.\obj\ <-- makefile.mak DEF.def make.exe *.obj *.li?
.\bin\

Teraz proszę zerknąć na ten kod:

#----------------------------
# (c) 2003 by Karol Ossowski
#----------------------------
# >>   Hello Project     << #
#----------------------------

DIR = c:\Hello
 
C_FLAGS = -tW -c -q -w-par -w-rvl
L_FLAGS = /aa /Tpe /Gk /t /q -w-dup
 
SRC = Hello.cpp klasa.cpp
HDR = klasa.h            # linijka odana dla porządku ;)
RES = zasoby.res
DEF = DEF.def
LIB =
BIN = Hello.exe
 
{$(DIR)\src}.cpp{$(DIR)\obj}.obj:
 BCC32 $(C_FLAGS) -I$(DIR)\src\hdr $<

{$(DIR)\src\res}.rc{$(DIR)\obj}.res:
 BRCC32 $<
 @COPY $:$(@F) $(DIR)\obj
 @DEL  $:$(@F)

$(BIN): {$(DIR)\obj}$(SRC:.cpp=.obj) $(RES)
 ILINK32 $(L_FLAGS) c0w32.obj $(SRC:.cpp=.obj),$.,,cw32.lib\
import32.lib $(LIB),$(DEF),$(RES)
 COPY $. $(DIR)\bin
 DEL $.

Jak widać makefile bardzo zyskał na funkcjonalności i co najważniejsze na uniwersalności: dzięki zmiennym cały czas możesz zmieniać skład plików projektu zasadniczo nie ingerując w zasady. Nawet jeśli w projekcie nie przewidujesz zasobów to nie będzie tragedii: operacja kompilacji zasobów nie będzie miała miejsca a plik nie zostanie dodany przez konsolidator. Poza tym zmienna DIR nie została stworzone bez powodu. Dzieki niej możliwa jest także budowa nowego, całkowicie innego projektu, którego wynikiem będzie $(BIN), plikami źródłowymi $(SRC) itd. Oczywiście przy tej operacji trzeba zachować tę samą strukturę podkatalogów projektu i rozmieszczenia w nich typów plików.
Teraz małe sprostowanie tak dziwnego zapisu kompilacji zasobów: BRCC32 nie zachowuje się jak większość programów z FCLT ponieważ zostawia plik wynikowy w lokalizacji ze źródłami.
Jest pewien mankament tego makefile'a: jeśli nie masz plików zależnych zasady szczegółowej MAKE się o nie upomni, choć mają one dopiero powstać w trakcie trwania makefile'a. Jest na to rada: przed pierwszym (później nie ma już takiej potrzeby) uruchomieniem makefile'a "dotknij" programem TOUCH wszystkie pliki źródłowe. Np. w tym projekcie będzie to wyglądać tak:

cd c:\Hello\obj
touch zasoby.res klasa.obj Hello.obj
cd c:\Hello\src
touch *.cpp
cd .\res
touch *.rc

Można rozwiązać to w ten sposób... ale jest znacznie bardziej funkcjonalna dyrektywa...

Dyrektywy

[edytuj]

Dyrektyw/słów kluczowych w borlandowskim makefile'u jest mniej niż np. w C++ ale wystarczająco dużo, aby efektywnie zarządzać projektem. W tym przypadku spisałem niemal wszystkie wyrażenia (chyba że sam któregoś nie rozumiałem). Nie musisz się ich uczyć na pamięć, wystarczy, że będziesz miał te 5 tabelek zawsze pod ręką. Warto jednak na początek je przeczytać, żeby przekonać się jakie są możliwości słów kluczowych w MAKE. Ważną zasadą przy wykorzystywaniu dyrektyw jest to aby MAKE nie pomylił danej dyrektywy z wywołaniem programu. Jeśli więc chcemy dyrektywa umieścić w polu komend jakiejś zasady to trzeba ją postawić na początku wiersza w innym przypadku zostanie ona potraktowana jako komenda systemowa, a to może oznaczać tylko kłopoty...

Pliki projektu i konfiguracja MAKE

[edytuj]
nazwa wiersz poleceń opis
.autodepend -a sprawdzaj pliki nagłówkowe przed kompilacją i jeśli zostaną zmodyfikowane kompiluj jeszcze raz pliki które z nich korzystają
.noautodepend -a- nie sprawdzaj plików nagłówkowych
.cacheautodepend -c przechowuj w pamięci podręcznej pliki wchodzące w skład projektu i jeśli nie zostaną w poczynione żadne zmiany nie wykonuj na nich operacji ponownie
.nocacheautodepend -c- nie przechowuj w pamięci podręcznej plików wchodzących w skład projektu
.keep -K zachowuj pliki tymczasowe tworzone podczas działania programu MAKE
.nokeep -K- nie przechowuj plików tymczasowych które są tworzone podczas działania MAKE'a
.ignore -i ignoruj wartość jaką zwróci komenda
.noIgnore -i- nie ignoruj wartości jaką zwróci komenda
.silent -s nie pokazuj na ekranie wywołania narzedzia
.nosilent -s- pokazuj na ekranie wywołanie narzędzia lub komendę jaką wykonuje system
.swap -S wyczyść swoją pamięć zanim zaczniesz wywoływać narzędzia (ta instrukcja jest dobra podczas operacji na dużych plikach)
.noswap -S- nie czyść pamięci przed wywoływaniem narzędzi
-r ignoruje zasady jakie podaje nam standardowy plik makefile BUILTINS.mak znajdujący się w katalogu BIN kompilatora
szablon opis
.precious: <PlikWynikowy> jeśli któryś z programów "padnie" MAKE wyrzuca swój plik wynikowy. ta komenda temu zapobiega
.suffixes: .<roz1> [.<roz2> [.<roz3>]..] twórz pliki najpierw z rozszerzeniem: <roz1> później z <roz2>, a na końcu z <roz3> (itd.); ta dyrektywa jest analizowana przez zasady ogólne i określa ona porządek tworzenia PlikówDocelowych
.path.<roz> = <lokalizacja> szukaj pliku z rozszerzeniem <roz> w podanej <lokalizacji> (ta dyrektywa niweluje problem z wcześniejszym makefile'em)
!include [<lokalizacja>]<Nazwapliku> dodaj tekst do obecnego makefile'a z pliku <NazwaPliku> (działa jak makro-instrukcja #include w C/C++)
!undef <nazwa_zmiennej> "wyrzuć" zmienną <nazwa_zmiennej>

Instrukcje warunkowe

[edytuj]
szablon opis
!ifdef <nazwa_zmiennej> <operacje> jeśli zmienna <nazwa_zmiennej> jest zadeklarowana wykonaj <operacje>
!ifndef <nazwa_zmiennej> <operacje> jeśli zmienna <nazwa_zmiennej> nie jest zadeklarowane wykonaj <operacje>
!if <warunek> <operacje> jeśli <warunek> zostanie spełniony wykonaj <operacje>
!else <operacje> w przeciwnym wypadku wykonaj <operacje> (musi występować z !if lub !ifdef lub !ifndef)
!elif <warunek> <operacje> w przeciwnym wypadku, jeśli <warunek> jest spełniony wykonaj <operacje> (musi występować z !if lub !ifdef, !ifndef, !else)
!endif kończy instrukcję warunkową

Instrukcję warunkową warto bardziej wnikliwie zanalizować, ponieważ jak sądzę będziesz ją często używał(a).

  • Każda instrukcja warunkowa musi się kończyć dyrektywą !endif
  • Przy określaniu warunków instrukcji warunkowej możemy użyć między innymi następujących operatorów:
operator opis operator opis
- negacja || logiczne OR
+ dodawanie ! logiczne NOT
- odejmowanie&& logiczne AND
* mnożenie >= większe lub równe
/ dzielenie <= mniejsze lub równe
== równe > większe
!= nierówne < mniejsze
  • Aby sprawdzić czy zmienna jest zadeklarowana (tj. czy je wcześniej stworzyliśmy) można użyć specjalnego znacznika d. Zastosowanie go jest równoważne z użyciem dyrektywy !ifdef:
    • !ifdef <zmienna> to, to samo co !if $d(<zmienna>)
    • !ifndef <zmienna> to, to samo co !if !$d(<zmienna>)

Ekran

[edytuj]
szablon opis rezultat
!error <komunikat> polecenie, po natrafieniu na które MAKE kończy działanie i wyświetla na ekranie rezultat... Fatal makefile <numer_linii>: Error directive: <komunikat>
!message <komunikat> jeśli MAKE natrafi na to polecenie, wyświetla na ekranie rezultat... <komunikat>

W obu dyrektywach "ekranowych" można używać zmiennych do reprezentacji komunikatów.

MAKE w praktyce

[edytuj]

Teraz pora na wzbogacenie naszego makefile'a o dyrektywy i tym samym doprowadzenie skryptu do ostatecznej wersji. Przeanalizuj ten kod, a na pewno rozjaśni Ci się w głowie, o czym była mowa przez ostatnie 25kB :). Do tego projektu potrzebujesz jeszcze jeden plik: HelloProject.txt który jako jedyny powinien się znaleźć w katalogu głównym projektu. Nasz ostateczny makefile będzie się bowiem składał z dwóch plików pierwszy to przed chwilą utworzony HelloProject.txt, a drugi to wszystkim znany makefile.mak (połączone są one dyrektywą !include). Dodatkowo w katalogu c:\Hello\src powinieneś utworzyć jeszcze jeden folder i zamieścić tam pliki *.asm:

.\asm <-- ewentualne wstawki asmowe (*.asm)

Teraz możesz przejść do analizy.

c:\Hello\HelloProject.txt:

#----------------------------
# (c) 2004 by Karol Ossowski
#----------------------------
# >>   Hello Project     << #
#----------------------------

#Tryb budowy projektu:
# 1 - Release
# 2 - Debug
# 3 - Rebuilt
TRYB = 1

SRC = Hello.cpp klasa.cpp      #nie wstawiaj przecinków
HDR = klasa.h
ASM =
RES = zasoby.res
LIB =
BIN = Hello.exe
DEF = DEF.def

C_FLAGS = -tW -c -q -w-par -w-rvl
L_FLAGS = /aa /Tpe /Gk /t /q -w-dup


c:\Hello\obj\makefile.mak:

DIR = c:\Hello                  #nie wstawiaj na końcu '\'

!include $(DIR)\HelloProject.txt

.silent 
.autodepend
.cacheautodepend
.suffixes: .cpp .asm .rc .res .obj .exe

.path.obj = $(DIR)\obj
.path.res = $(DIR)\obj
.path.def = $(DIR)\obj

!if $(TRYB)== 2
 DEBUG=-v 
!message_________.:Tryb DEBUG:._________
!elif $(TRYB)== 1
 !message_________.:Tryb RELEASE:._________
!elif $(TRYB)== 3
 !message odbudowywanie....
REBUILT:
 TOUCH $(DIR)\src\*.cpp
 TOUCH $(DIR)\src\res\*.rc
 TOUCH $(DIR)\src\asm\*.asm
 DEL $(DIR)\obj\*.il*
!else 
!error NIE wybrano trybu budowy projektu
!endif

#Zasoby#
{$(DIR)\src\res}.rc{$(DIR)\obj}.RES:
  BRCC32 $<
  COPY $:$(@F) $(DIR)\obj#kopiowanie plików wynikowych do \obj
  DEL  $:$(@F)           #likwidowanie powyższych plików w \src

#Asembler#
{$(DIR)\src\asm}.asm{$(DIR)\obj}.obj:
  TASM32 $<

#Kompilacja#
{$(DIR)\src}.cpp{$(DIR)\obj}.obj:
  BCC32 $(C_FLAGS) $(DEBUG) -I$(DIR)\src\hdr $<

#Konsolidacja#
$(BIN): $(SRC:.cpp=.obj) $(ASM:.asm=.obj) $(RES)
  ILINK32 $(L_FLAGS) $(DEBUG) c0w32.obj $(SRC:.cpp=.obj)\
$(ASM:.asm=.obj),$.,,cw32.lib import32.lib $(LIB),$(DEF),$(RES)
!if $(TRYB)==2
  TD32 $(BIN) 
!else
  COPY $. $(DIR)\bin
  $(DIR)\bin\$(BIN)
!endif

Przyznam, że ten makefile jest dość zaawansowany jak na materiał dla świeżo upieczonych użytkowników MAKE'a ale chciałem pokazać jak najwięcej możliwości tego narzędzia. Kod ten składa się z dwóch części:

  1. interfejs (HelloProject.txt) - miejsce, które będziesz stale używał(a) w czasie pracy nad projektem. Możesz deklarować tam nowe pliki źródłowe, odznaczać opcje linkera i kompilatora, i określać tryb budowy projektu;
  2. implementacja (makefile.mak) - część, w której zawarte są same komendy budowy projektu, ten segment jest tworzony tylko raz podczas pisania makefile'a i od tego czasu raczej nie powinieneś go ruszać;

Tak naprawdę skrypt ten jest na tyle uniwersalny, że możesz używać go w wielu projektach (byle była zachowana odpowiednia struktura katalogów). Do makefile'a dodałem zasadę kompilującą w wstawki assemblerowe. Program TASM32, który jest kompilatorem assemblera niestety nie należy do FCLT, to wciąż komercyjny produkt Borlanda. Opcję tą dodałem tylko po to, aby makefile był już w 100% kompletny. W kodzie użyłem jeszcze jednego programu spoza FCLT. Jest to Turbo Debugger (TD32).TD32 to świetny darmowy debugger który powinieneś mieć w swojej kolekcji, jest do ściągnięcia z ze internetowej Borlanda.