หน้านี้จะกล่าวถึงระบบบิลด์ตามงาน วิธีการทำงาน และความซับซ้อนบางอย่างที่อาจเกิดขึ้นกับระบบแบบเฉพาะงาน หลังจากสคริปต์ Shell แล้ว ระบบบิลด์ที่อิงตามงานจะเป็นวิวัฒนาการเชิงตรรกะครั้งต่อไปของการสร้าง
การทำความเข้าใจระบบบิลด์ตามงาน
ในระบบบิลด์ตามงาน หน่วยพื้นฐานของงานคืองาน งานแต่ละงานเป็นสคริปต์ที่สามารถดำเนินการตามตรรกะแบบใดก็ได้ และงานจะระบุงานอื่นๆ เป็นทรัพยากร Dependency ที่ต้องทำงานก่อนหน้า ระบบบิลด์หลักส่วนใหญ่ที่ใช้กันในปัจจุบัน เช่น Ant, Maven, Gradle, Grunt และ Rake ทำงานตามงาน ระบบบิลด์ที่ทันสมัยส่วนใหญ่ต้องใช้วิศวกรเพื่อสร้างไฟล์บิลด์ที่อธิบายวิธีการสร้างบิลด์ แทนที่จะใช้สคริปต์ Shell
ลองดูตัวอย่างนี้จากคู่มือ Ant
<project name="MyProject" default="dist" basedir=".">
<description>
simple example build file
</description>
<!-- set global properties for this build -->
<property name="src" location="src"/>
<property name="build" location="build"/>
<property name="dist" location="dist"/>
<target name="init">
<!-- Create the time stamp -->
<tstamp/>
<!-- Create the build directory structure used by compile -->
<mkdir dir="${build}"/>
</target>
<target name="compile" depends="init"
description="compile the source">
<!-- Compile the Java code from ${src} into ${build} -->
<javac srcdir="${src}" destdir="${build}"/>
</target>
<target name="dist" depends="compile"
description="generate the distribution">
<!-- Create the distribution directory -->
<mkdir dir="${dist}/lib"/>
<!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
<jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
</target>
<target name="clean"
description="clean up">
<!-- Delete the ${build} and ${dist} directory trees -->
<delete dir="${build}"/>
<delete dir="${dist}"/>
</target>
</project>
Buildfile เขียนใน XML และกำหนดข้อมูลเมตาง่ายๆ บางอย่างเกี่ยวกับบิลด์พร้อมกับรายการงาน (แท็ก <target>
ใน XML) (Ant ใช้คำว่า target เพื่อแทน task และใช้คำว่า task เพื่ออ้างถึง commands) แต่ละงานจะเรียกใช้รายการคำสั่งที่เป็นไปได้ที่ Ant กำหนดไว้ ซึ่งรวมถึงการสร้างและลบไดเรกทอรี การเรียกใช้ javac
และการสร้างไฟล์ JAR คำสั่งชุดนี้สามารถขยายได้ด้วยปลั๊กอินที่ผู้ใช้ให้
เพื่อครอบคลุมตรรกะต่างๆ แต่ละงานยังกำหนดงานที่ขึ้นอยู่กับแอตทริบิวต์นั้นได้ด้วย ทรัพยากร Dependency เหล่านี้จะสร้างกราฟแบบวนซ้ำ
ดังที่แสดงในรูปที่ 1
รูปที่ 1 กราฟแบบวนซ้ำแสดงทรัพยากร Dependency
ผู้ใช้จะสร้างบิลด์โดยมอบหมายงานให้กับเครื่องมือบรรทัดคำสั่งของ Ant ตัวอย่างเช่น เมื่อผู้ใช้พิมพ์ ant dist
Ant จะดำเนินการขั้นตอนต่อไปนี้
- โหลดไฟล์ชื่อ
build.xml
ในไดเรกทอรีปัจจุบันและแยกวิเคราะห์เพื่อสร้างโครงสร้างกราฟที่แสดงในรูปที่ 1 - ค้นหางานชื่อ
dist
ที่ระบุไว้ในบรรทัดคำสั่ง และพบว่างานนี้มีการขึ้นต่อกันในงานcompile
- ค้นหางานชื่อ
compile
และพบว่างานดังกล่าวมีการขึ้นต่อกันในงานinit
- ค้นหางานชื่อ
init
และพบว่าไม่มีทรัพยากร Dependency - เรียกใช้คำสั่งที่ระบุไว้ในงาน
init
- เรียกใช้คำสั่งที่ระบุไว้ในงาน
compile
โดยทรัพยากร Dependency ทั้งหมดของงานนั้นได้ทำงานแล้ว - เรียกใช้คำสั่งที่ระบุไว้ในงาน
dist
โดยทรัพยากร Dependency ทั้งหมดของงานนั้นได้ทำงานแล้ว
ในท้ายที่สุด โค้ดที่ Ant จะเรียกใช้เมื่อเรียกใช้งาน dist
เทียบเท่ากับสคริปต์ Shell ต่อไปนี้
./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*
เมื่อตัดไวยากรณ์ออก จริงๆ แล้วบิลด์ไฟล์และสคริปต์บิลด์ไม่ได้แตกต่างกันมากเกินไป แต่เราได้รับประโยชน์มากมายจากการดำเนินการนี้ เราสร้างไฟล์บิลด์ใหม่ในไดเรกทอรีอื่นและลิงก์เข้าด้วยกันได้ เราสามารถเพิ่มงานใหม่ๆ ที่ขึ้นอยู่กับงานที่มีอยู่แล้วได้ง่ายๆ ด้วยวิธีที่ซับซ้อนและที่กำหนดเอง เราต้องส่งเพียงชื่องานเดียวไปยังเครื่องมือบรรทัดคำสั่ง ant
และจะกำหนดทุกอย่างที่จำเป็นต้องทำ
Ant เป็นซอฟต์แวร์เก่าแก่ที่เปิดตัวครั้งแรกในปี 2000 เครื่องมืออื่นๆ เช่น Maven และ Gradle ได้รับการปรับปรุงใน Ant ในช่วงหลายปีที่ผ่านมาและได้เข้ามาแทนที่โดยพื้นฐานแล้วโดยการเพิ่มฟีเจอร์ เช่น การจัดการการอ้างอิงภายนอกโดยอัตโนมัติและไวยากรณ์ที่ดูสะอาดตาโดยไม่ต้องใช้ XML แต่ลักษณะของระบบใหม่เหล่านี้จะยังคงเหมือนเดิม โดยให้วิศวกรเขียนสคริปต์บิลด์ในงานแบบมีหลักการและเป็นโมดูลเป็นงานและให้เครื่องมือสำหรับดำเนินงานเหล่านั้นและจัดการทรัพยากร Dependency ต่างๆ
ด้านมืดของระบบบิลด์ตามงาน
เนื่องจากโดยพื้นฐานแล้ว เครื่องมือเหล่านี้ช่วยให้วิศวกรกำหนดสคริปต์ใดก็ตามให้เป็นงานได้ เครื่องมือจึงมีประสิทธิภาพอย่างยิ่ง จึงช่วยให้คุณทำสิ่งต่างๆ ได้แทบทุกอย่าง แต่ประสิทธิภาพนี้มาพร้อมกับข้อเสีย และระบบการสร้างตามงานอาจทำงานได้ยากเนื่องจากสคริปต์บิลด์ซับซ้อนมากขึ้น ปัญหาของระบบเหล่านี้คือเพราะระบบเหล่านี้ทำให้วิศวกรมีกำลังมากเกินไปและไม่เพียงพอต่อระบบ เนื่องจากระบบไม่รู้ว่าสคริปต์กำลังทำอะไรอยู่ การทำงานจึงแย่ลง เนื่องจากระบบต้องมีความระมัดระวังอย่างมากในการกำหนดเวลาและดำเนินการขั้นตอนการสร้างบิลด์ ระบบไม่มีทางที่จะยืนยันได้ว่าแต่ละสคริปต์กำลังทำในสิ่งที่ทำได้ สคริปต์จึงมีแนวโน้มที่จะเพิ่มความซับซ้อนและกลายเป็นอีกปัญหาหนึ่งที่จำเป็นต้องแก้ไขข้อบกพร่อง
ความยากของขั้นตอนบิลด์พร้อมกัน
เวิร์กสเตชันสำหรับการพัฒนาสมัยใหม่ค่อนข้างมีประสิทธิภาพเนื่องจากมีแกนประมวลผลหลายรายการที่ดำเนินการตามขั้นตอนบิลด์หลายรายการพร้อมกันได้ แต่ระบบที่อิงตามงานมักจะไม่สามารถดำเนินงานพร้อมกัน แม้ในเวลาที่ดูเหมือนว่าจะสามารถทำได้ สมมติว่างาน A ขึ้นอยู่กับงาน B และ C เนื่องจากงาน ข และ ค ไม่ได้พึ่งพากันและกัน จึงปลอดภัยที่จะเรียกใช้พร้อมกันเพื่อให้ระบบสามารถเข้าถึงงาน ก. ได้เร็วขึ้น หรืออาจจะหากพวกเขาไม่ได้พูดถึง แหล่งข้อมูลเดียวกันเลย แต่อาจไม่เป็นเช่นนั้น เพราะทั้ง 2 อย่างจะใช้ไฟล์เดียวกันเพื่อติดตามสถานะและการเรียกใช้สถานะพร้อมกันอาจทำให้เกิดข้อขัดแย้ง โดยทั่วไปแล้วไม่มีวิธีที่ระบบจะรู้ได้ ดังนั้นจึงต้องเสี่ยงกับความขัดแย้งเหล่านี้ (นำไปสู่ปัญหาการสร้างที่พบได้ยากแต่แก้ไขข้อบกพร่องได้ยากมาก) หรือต้องจำกัดบิลด์ทั้งหมดให้ทำงานในเทรดเดียวในกระบวนการเดียว ซึ่งอาจเป็นเรื่องสิ้นเปลืองสิ้นเปลืองของเครื่องสำหรับนักพัฒนาซอฟต์แวร์ที่ทรงพลัง และกำจัดความเป็นไปได้ในการกระจายบิลด์ไปยังหลายๆ เครื่องโดยสิ้นเชิง
ปัญหาในการสร้างบิลด์ที่เพิ่มขึ้น
ระบบบิลด์ที่ดีช่วยให้วิศวกรสร้างบิลด์เพิ่มขึ้นได้อย่างเสถียร เพื่อจะได้ไม่ต้องสร้างฐานของโค้ดใหม่ทั้งหมดตั้งแต่ต้น การดำเนินการนี้มีความสำคัญอย่างยิ่งหากระบบของบิลด์ช้าและไม่สามารถโหลดขั้นตอนบิลด์พร้อมกันด้วยเหตุผลดังกล่าว แต่น่าเสียดายที่ ระบบการสร้างแบบอิงตามงานลำบากเหมือนกัน เนื่องจากงานต่างๆ ทำได้ทุกอย่าง โดยทั่วไปจึงไม่มีวิธีตรวจสอบว่าทำงานเสร็จแล้วหรือไม่ งานจำนวนมากเพียงแค่รับชุดไฟล์ต้นฉบับและเรียกใช้คอมไพเลอร์เพื่อสร้างชุดไบนารี ดังนั้นจึงไม่จำเป็นต้องเรียกใช้งานเหล่านั้นอีกครั้งหากไฟล์ต้นฉบับที่สำคัญไม่มีการเปลี่ยนแปลง แต่หากไม่มีข้อมูลเพิ่มเติม ระบบจะไม่สามารถบอกเรื่องนี้ได้แน่นอน บางทีอาจจะเป็นการดาวน์โหลดไฟล์ที่อาจมีการเปลี่ยนแปลง หรืออาจมีการเขียนการประทับเวลาที่แตกต่างกันในการเรียกใช้แต่ละครั้ง โดยทั่วไปแล้ว ระบบจะต้องทำงานทั้งหมดซ้ำระหว่างการสร้างแต่ละครั้งเพื่อรับประกันความถูกต้อง ระบบบิลด์บางระบบพยายามเปิดใช้งานบิลด์ที่เพิ่มขึ้นโดยให้วิศวกรระบุเงื่อนไขที่จำเป็นต้องเรียกใช้งานอีกครั้ง ในบางครั้ง ปัญหานี้ก็เป็นไปได้ แต่ก็มักเป็นปัญหาที่ซับซ้อนกว่าที่เป็นอยู่ ตัวอย่างเช่น ในภาษาอย่าง C++ ที่อนุญาตให้ไฟล์อื่นๆ รวมไฟล์ได้โดยตรง จะไม่สามารถกำหนดไฟล์ทั้งชุดที่ต้องเฝ้าดูการเปลี่ยนแปลงโดยไม่ต้องแยกวิเคราะห์แหล่งที่มาของอินพุต วิศวกรมักจะใช้ทางลัด ซึ่งทางลัดเหล่านี้อาจทำให้เกิดปัญหาที่พบได้ยากและน่าหงุดหงิดซึ่งผลลัพธ์ของงานถูกนำมาใช้ซ้ำ ทั้งที่ไม่ควรเป็นเช่นนั้น เมื่อเกิดกรณีนี้ขึ้นบ่อยครั้ง วิศวกรจะทำความสะอาดจนเป็นนิสัยก่อนทุกงานสร้างเพื่อให้ได้สถานะใหม่ ซึ่งก็คือการเอาชนะจุดประสงค์ของการมีงานสร้างที่เพิ่มขึ้นตั้งแต่แรก การคิดว่าจะต้องทำงานซ้ำเมื่อใดนั้นเป็นเรื่องที่ซับซ้อนอย่างน่าประหลาดใจ และระบบยังจัดการด้วยแมชชีนได้ดีกว่ามนุษย์
ปัญหาการบำรุงรักษาและแก้ไขข้อบกพร่องของสคริปต์
สุดท้าย สคริปต์ของบิลด์ที่กำหนดโดยระบบบิลด์ตามงานมักจะใช้งานได้ยาก แม้ว่ามักจะมีการตรวจสอบอย่างละเอียดน้อยกว่า แต่สคริปต์บิลด์คือโค้ดเช่นเดียวกับที่ระบบสร้างขึ้น และเป็นที่ซ่อนข้อบกพร่องได้ง่าย ต่อไปนี้คือตัวอย่างบางส่วนของข้อบกพร่องที่พบได้บ่อยมากเมื่อทำงานกับระบบบิลด์ตามงาน
- งาน A ขึ้นอยู่กับงาน B ในการสร้างไฟล์หนึ่งๆ เป็นเอาต์พุต เจ้าของงาน ข ไม่ทราบว่างานอื่นต้องพึ่งพางานดังกล่าว จึงเปลี่ยนไปสร้างเอาต์พุตในตำแหน่งอื่น ซึ่งจะตรวจไม่พบจนกว่าจะมีคนพยายามเรียกใช้งาน A แล้วพบว่าทำงานไม่สำเร็จ
- งาน ก ขึ้นอยู่กับงาน ข ซึ่งขึ้นอยู่กับงาน ค ซึ่งสร้างไฟล์ใดไฟล์หนึ่งเป็นเอาต์พุตที่งาน ก ต้องการ เจ้าของงาน B ตัดสินใจว่าจะไม่ต้องอาศัยงาน C อีกต่อไป ทำให้งาน A ล้มเหลว แม้ว่างาน B จะไม่สนใจงาน C เลยก็ตาม
- นักพัฒนาซอฟต์แวร์งานชิ้นใหม่ตั้งสมมติฐานเกี่ยวกับเครื่องที่กำลังทำงานนี้โดยไม่ได้ตั้งใจ เช่น ตำแหน่งของเครื่องมือหรือค่าของตัวแปรสภาพแวดล้อมบางอย่าง งานนี้จะได้ผลในเครื่องของตน แต่จะล้มเหลวเมื่อนักพัฒนาซอฟต์แวร์รายอื่นพยายามดำเนินการ
- งานมีองค์ประกอบที่ไม่กำหนด เช่น การดาวน์โหลดไฟล์จากอินเทอร์เน็ตหรือการเพิ่มการประทับเวลาไปยังบิลด์ ปัจจุบัน ผู้ใช้ได้รับผลลัพธ์ที่แตกต่างกันทุกครั้งที่เรียกใช้บิลด์ ซึ่งหมายความว่าวิศวกรจะไม่สามารถทำซ้ำและแก้ไขความล้มเหลวหรือความล้มเหลวของกันและกันในระบบบิลด์อัตโนมัติได้เสมอไป
- งานที่มีทรัพยากร Dependency หลายรายการจะสร้างเงื่อนไขการแข่งขันได้ หากงาน A เกี่ยวข้องกับทั้งงาน B และงาน C และทั้ง B และ C ต่างก็แก้ไขไฟล์เดียวกัน งาน A จะได้รับผลลัพธ์ที่ต่างกันขึ้นอยู่กับว่างาน B และ C ใดเสร็จก่อน
ไม่มีวิธีสำหรับวัตถุประสงค์ทั่วไปในการแก้ปัญหาด้านประสิทธิภาพ ความถูกต้อง หรือความสามารถในการบำรุงรักษาเหล่านี้ภายในเฟรมเวิร์กที่อิงตามงานซึ่งได้ระบุไว้ที่นี่ ตราบใดที่วิศวกรสามารถเขียนโค้ดที่กำหนดเองที่ทำงานระหว่างบิลด์ได้ ระบบจะมีข้อมูลไม่เพียงพอที่จะเรียกใช้บิลด์ได้อย่างรวดเร็วและถูกต้องอยู่เสมอ ในการแก้ปัญหา เราต้องนำอำนาจบางส่วนออกจากมือของวิศวกร และนำระบบนี้ไปไว้ในมือของระบบ และปรับแนวคิดใหม่ให้บทบาทของระบบ ไม่ใช่งานที่ต้องทำงาน แต่เป็นการผลิตสิ่งประดิษฐ์
แนวทางนี้นำไปสู่การสร้างระบบการสร้างที่ใช้อาร์ติแฟกต์อย่าง Blaze และ Bazel