בניית Java עם Makefile במקום Ant
כל פרויקט Java שאני פוגש מגיע עם build.xml, וכל build.xml הוא כמה מאות שורות XML שעושות את מה
ש-Makefile של חמש-עשרה שורות עושה לכל שפה אחרת במחשב. אני בונה את ה-Java שלי עם make. הנה איך,
והנה החשבון הכן של מה מרוויחים ומה מוותרים עליו.
ה-Makefile
המבנה הוא המקובל: מקורות תחת src/, מחלקות מקומפלות תחת build/, וקובץ jar אחד כתוצר.
JAVAC := javac
JAR := jar
JAVAC_FLAGS := -Xlint:all -encoding UTF-8
SRC_DIR := src
BUILD_DIR := build
JAR_FILE := myapp.jar
MAIN_CLASS := com.example.Main
SOURCES := $(shell find $(SRC_DIR) -name '*.java')
CLASSES := $(patsubst $(SRC_DIR)/%.java,$(BUILD_DIR)/%.class,$(SOURCES))
.PHONY: all clean run
all: $(JAR_FILE)
$(BUILD_DIR)/%.class: $(SRC_DIR)/%.java
@mkdir -p $(BUILD_DIR)
$(JAVAC) $(JAVAC_FLAGS) -d $(BUILD_DIR) -cp $(BUILD_DIR):$(SRC_DIR) $<
$(JAR_FILE): $(CLASSES)
$(JAR) cfe $@ $(MAIN_CLASS) -C $(BUILD_DIR) .
run: $(JAR_FILE)
java -jar $(JAR_FILE)
clean:
rm -rf $(BUILD_DIR) $(JAR_FILE)
כלל התבנית ממפה כל קובץ מקור לקובץ המחלקה שלו, ולכן make מקמפל רק את המקורות שהשתנו. הארגומנט
-cp כולל גם את ספריית הבנייה (למחלקות שכבר קומפלו) וגם את ספריית המקור (כדי ש-javac ימצא את
המקורות של מחלקות שעדיין לא קומפלו). כלל ה-jar משתמש ב--e כדי לקבוע את נקודת הכניסה במניפסט, כך
שהתוצאה רצה עם java -jar.
אם יש לכם קובצי jar של צד שלישי, שימו אותם ב-lib/ והוסיפו אותם לנתיב המחלקות:
LIBS := $(wildcard lib/*.jar)
CLASSPATH := $(BUILD_DIR):$(SRC_DIR):$(subst $(eval) ,:,$(LIBS))
והשתמשו ב--cp $(CLASSPATH) בכלל הקומפילציה.
מה מרוויחים
כלי אחד להכול. פרויקט הוא לעיתים רחוקות רק Java. יש ספריית C עם עטיפת JNI, סקריפט פייתון שמייצר
קובץ מקור, מדריך ב-LaTeX, סקריפט מעטפת שאורז את הגרסה. Ant יודע על Java. make יודע על קבצים ופקודות,
וזה מה שכל אלה הם. אותו make all בונה את כל העץ, והתלויות בין החלקים של Java והחלקים שאינם Java
מבוטאות באותה שפה שבה מבוטאות התלויות בתוך כל אחד מהם.
תלויות אמיתיות. Makefile הוא גרף תלויות ולא שום דבר אחר. המטרות של Ant הן רשימת צעדים להרצה;
מושג העדכניות שלו הוא כל מה שכל משימה מיישמת במקרה, והתשובה של משימת javac היא להשוות חותמות זמן
של מקור ומחלקה לקבצים שהיא מקבלת. make עושה את אותה השוואה, אבל עושה אותה לכל כלל, כולל הכללים
שכתבתם למקורות המיוצרים ולאריזה, באופן אחיד, והוא מאפשר לכם לומר שה-jar תלוי בתבנית המניפסט ותבנית
המניפסט תלויה בקובץ הגרסה.
בלי XML. זו אינה טרוניה אסתטית. תיאור בנייה הוא קוד, ו-XML היא שפה גרועה לקוד: מילולית מדי, קשה להשוואת גרסאות, ובלי דרך לחשב דבר בלי לצלול לתוסף. ל-Makefile יש משתנים, פונקציות, כללי תבנית ומעטפת.
הוא כבר שם. בכל מחשב יוניקס יש make. איש אינו צריך להתקין כלי בנייה כדי לבנות את הפרויקט.
מה מוותרים עליו
קומפילציה תוספתית אינה מה שהיא נראית. כלל התבנית מקמפל מחדש מקור שהשתנה, אבל מחלקות Java תלויות
במחלקות אחרות בדרכים שמערכת הקבצים אינה מראה. שנו קבוע ב-A.java ו-B.class, שהטמיע את הקבוע הזה,
שגוי עכשיו, ול-make אין מושג מפני ש-B.java לא השתנה. ל-Ant יש בדיוק אותה בעיה והוא מסתיר אותה
בדיוק באותה מידה. התיקון הכן בשני המקרים הוא make clean לפני בניית גרסה, ו-javac על כל מערך המקור
לכל שינוי שנוגע בממשקים. לפרויקט בגודל רגיל הקומפילציה המלאה לוקחת שניות, ולכן הכלל התוספתי הוא נוחות
בזמן הפיתוח ולא משהו לבטוח בו.
טיפול בנתיב המחלקות הוא ידני. האלמנטים <path> ו-<fileset> של Ant, ומאוחר יותר Ivy ו-Maven,
מנהלים בשבילכם אוספי jar. עם make אתם מונים אותם, כמו למעלה, ופותרים את התלויות שלהם בעצמכם.
לפרויקט עם שלושה קובצי jar ב-lib/ זה טריוויאלי. לפרויקט עם ארבעים תלויות טרנזיטיביות זה מחיר ממשי,
וזהו הטיעון החזק ביותר בעד הכלים הייעודיים ל-Java.
אנשי Java אינם מצפים לזה. תורם שפותח את הפרויקט ואינו מוצא build.xml או pom.xml יהיה מבולבל
לדקה. זו דקה, וה-Makefile קצר דיו לקריאה בתוכה, אבל זה חיכוך ממשי ואיני מעמיד פנים אחרת.
היכן אני נוחת
לפרויקט שהוא בעיקר Java ויש לו עץ תלויות גדול, השתמשו בכלי שעץ התלויות רוצה. לכל דבר אחר, ובמיוחד לכל דבר שמערבב Java עם שפות אחרות, ה-Makefile פשוט יותר, כן יותר לגבי מה תלוי במה, ואינו מוסיף מערכת בנייה לרשימת הדברים שתורם חדש חייב ללמוד. חמש-עשרה השורות שלמעלה בנו כל תוכנית Java שכתבתי במשך שנים, ומעולם לא התגעגעתי ל-XML.