หน้านี้ครอบคลุมระบบบิลด์ที่อิงตามงาน วิธีการทำงาน และความซับซ้อนบางอย่างที่อาจเกิดขึ้นกับระบบที่อิงตามงาน ระบบบิลด์ที่อิงตามงานเป็นการพัฒนาที่สมเหตุสมผลถัดจากการใช้สคริปต์เชลล์
ทำความเข้าใจระบบบิลด์ที่อิงตามงาน
ในระบบบิลด์ที่อิงตามงาน หน่วยงานพื้นฐานคือ "งาน" งานแต่ละรายการคือสคริปต์ที่สามารถดำเนินการลอจิกทุกประเภท และงานจะระบุงานอื่นๆ เป็นทรัพยากร Dependency ที่ต้องทำงานก่อน ระบบบิลด์หลักๆ ที่ใช้กันในปัจจุบัน เช่น Ant, Maven, Gradle, Grunt และ Rake เป็นระบบที่อิงตามงาน ระบบบิลด์ที่ทันสมัยส่วนใหญ่กำหนดให้นักพัฒนาซอฟต์แวร์สร้างไฟล์บิลด์ที่อธิบายวิธีดำเนินการบิลด์แทนการใช้สคริปต์เชลล์
ลองดูตัวอย่างนี้จากคู่มือ 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>
ไฟล์บิลด์เขียนด้วย XML และกำหนดข้อมูลเมตาอย่างง่ายเกี่ยวกับบิลด์
พร้อมกับรายการงาน (แท็ก <target> ใน XML) (Ant ใช้คำว่า
target เพื่อแสดงถึง task และใช้คำว่า task เพื่ออ้างถึง
commands) งานแต่ละรายการจะดำเนินการตามรายการคำสั่งที่เป็นไปได้ซึ่งกำหนดโดย Ant ซึ่งในที่นี้รวมถึงการสร้างและลบไดเรกทอรี การเรียกใช้ javac และการสร้างไฟล์ JAR ชุดคำสั่งนี้สามารถขยายได้โดยปลั๊กอินที่ผู้ใช้ให้มาเพื่อครอบคลุมลอจิกทุกประเภท นอกจากนี้ งานแต่ละรายการยังกำหนดงานที่งานนั้นต้องพึ่งพาผ่านแอตทริบิวต์ depends ได้ด้วย ทรัพยากร Dependency เหล่านี้จะสร้างกราฟแบบไม่มีวงจร ดังที่แสดงในรูปที่ 1
รูปที่ 1 กราฟแบบไม่มีวงจรที่แสดงทรัพยากร Dependency
ผู้ใช้ดำเนินการบิลด์โดยระบุงานให้กับเครื่องมือบรรทัดคำสั่งของ Ant ตัวอย่างเช่น เมื่อผู้ใช้พิมพ์ ant dist Ant จะทำตามขั้นตอนต่อไปนี้
- โหลดไฟล์ชื่อ
build.xmlในไดเรกทอรีปัจจุบันและแยกวิเคราะห์ไฟล์เพื่อสร้างโครงสร้างกราฟที่แสดงในรูปที่ 1 - ค้นหางานชื่อ
distที่ระบุไว้ในบรรทัดคำสั่งและพบว่างานนี้มีทรัพยากร Dependency เป็นงานชื่อcompile - ค้นหางานชื่อ
compileและพบว่างานนี้มีทรัพยากร Dependency เป็นงานชื่อinit - ค้นหางานชื่อ
initและพบว่างานนี้ไม่มีทรัพยากร Dependency - ดำเนินการคำสั่งที่กำหนดไว้ในงาน
init - ดำเนินการคำสั่งที่กำหนดไว้ในงาน
compileเมื่อทรัพยากร Dependency ทั้งหมดของงานนั้นทำงานแล้ว - ดำเนินการคำสั่งที่กำหนดไว้ในงาน
distเมื่อทรัพยากร Dependency ทั้งหมดของงานนั้นทำงานแล้ว
ท้ายที่สุด โค้ดที่ Ant ดำเนินการเมื่อเรียกใช้งาน dist จะเทียบเท่ากับสคริปต์ของ Shell ต่อไปนี้
./createTimestamp.shmkdir 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 ในช่วงหลายปีที่ผ่านมาและแทนที่ Ant โดยการเพิ่มฟีเจอร์ต่างๆ เช่น การจัดการทรัพยากร Dependency ภายนอกโดยอัตโนมัติและไวยากรณ์ที่ชัดเจนขึ้นโดยไม่มี XML แต่ลักษณะของระบบใหม่เหล่านี้ยังคงเหมือนเดิม นั่นคือช่วยให้นักพัฒนาซอฟต์แวร์เขียนสคริปต์บิลด์ในลักษณะที่เป็นระบบและเป็นโมดูลในรูปแบบงาน และมีเครื่องมือสำหรับดำเนินการงานเหล่านั้นและจัดการทรัพยากร Dependency ระหว่างงาน
ข้อเสียของระบบบิลด์ที่อิงตามงาน
เนื่องจากเครื่องมือเหล่านี้ช่วยให้นักพัฒนาซอฟต์แวร์กำหนดสคริปต์ใดก็ได้เป็นงาน จึงมีประสิทธิภาพมากและช่วยให้คุณทำสิ่งต่างๆ ได้เกือบทุกอย่างที่นึกออก แต่ประสิทธิภาพดังกล่าวก็มาพร้อมกับข้อเสีย และระบบบิลด์ที่อิงตามงานอาจใช้งานได้ยากขึ้นเมื่อสคริปต์บิลด์มีความซับซ้อนมากขึ้น ปัญหาของระบบดังกล่าวคือระบบให้ อำนาจแก่นักพัฒนาซอฟต์แวร์มากเกินไปและให้อำนาจแก่ระบบไม่เพียงพอ เนื่องจากระบบไม่ทราบว่าสคริปต์กำลังทำอะไรอยู่ ประสิทธิภาพจึงลดลง เนื่องจากระบบต้องระมัดระวังอย่างมากในการกำหนดเวลาและดำเนินการตามขั้นตอนบิลด์ และระบบไม่มีวิธีที่จะยืนยันว่าสคริปต์แต่ละรายการกำลังทำสิ่งที่ควรทำ สคริปต์จึงมีแนวโน้มที่จะซับซ้อนมากขึ้นและกลายเป็นอีกสิ่งหนึ่งที่ต้องแก้ไขข้อบกพร่อง
ความยากในการขนานขั้นตอนบิลด์
เวิร์กสเตชันการพัฒนาที่ทันสมัยมีประสิทธิภาพสูงมาก โดยมีหลายคอร์ที่สามารถดำเนินการตามขั้นตอนบิลด์หลายขั้นตอนพร้อมกันได้ แต่ระบบที่อิงตามงานมักไม่สามารถขนานการดำเนินการงานได้ แม้ว่าดูเหมือนว่าระบบจะทำได้ สมมติว่างาน A ต้องพึ่งพางาน B และ C เนื่องจากงาน B และ C ไม่มีทรัพยากร Dependency ต่อกัน จึงสามารถเรียกใช้งานพร้อมกันได้ไหมเพื่อให้ระบบทำงานกับงาน A ได้เร็วขึ้น อาจทำได้หากงานทั้ง 2 ไม่ได้ใช้ทรัพยากรเดียวกัน แต่ก็อาจทำไม่ได้เช่นกัน เนื่องจากทั้ง 2 งานอาจใช้ไฟล์เดียวกันเพื่อติดตามสถานะของตนเอง และการเรียกใช้งานพร้อมกันอาจทำให้เกิดความขัดแย้ง โดยทั่วไปแล้วระบบไม่มีทางทราบได้ ดังนั้นระบบจึงต้องเสี่ยงกับความขัดแย้งเหล่านี้ (ซึ่งนำไปสู่ปัญหาบิลด์ที่เกิดขึ้นไม่บ่อยนักแต่แก้ไขข้อบกพร่องได้ยากมาก) หรือต้องจำกัดบิลด์ทั้งหมดให้ทำงานในเธรดเดียวในกระบวนการเดียว ซึ่งอาจเป็นการสิ้นเปลืองทรัพยากรของเครื่องมือพัฒนาที่มีประสิทธิภาพสูง และตัดความเป็นไปได้ในการกระจายบิลด์ไปยังเครื่องหลายเครื่องออกไปโดยสิ้นเชิง
ความยากในการดำเนินการบิลด์แบบเพิ่มทีละส่วน
ระบบบิลด์ที่ดีช่วยให้นักพัฒนาซอฟต์แวร์ดำเนินการบิลด์แบบเพิ่มทีละส่วนได้อย่างน่าเชื่อถือ เพื่อให้การเปลี่ยนแปลงเล็กน้อยไม่จำเป็นต้องสร้างโค้ดเบสทั้งหมดใหม่ตั้งแต่ต้น ซึ่งสำคัญอย่างยิ่งหากระบบบิลด์ทำงานช้าและไม่สามารถขนานขั้นตอนบิลด์ได้ด้วยเหตุผลที่กล่าวไว้ข้างต้น แต่โชคร้ายที่ระบบบิลด์ที่อิงตามงานก็ประสบปัญหาในส่วนนี้เช่นกัน เนื่องจากงานสามารถทำอะไรก็ได้ จึงไม่มีวิธีตรวจสอบโดยทั่วไปว่างานนั้นเสร็จสิ้นแล้วหรือไม่ งานจำนวนมากเพียงแค่รับชุดไฟล์ต้นทางและเรียกใช้คอมไพเลอร์เพื่อสร้างชุดไบนารี ดังนั้นจึงไม่จำเป็นต้องเรียกใช้อีกครั้งหากไฟล์ต้นทางพื้นฐานไม่เปลี่ยนแปลง แต่หากไม่มีข้อมูลเพิ่มเติม ระบบก็ไม่สามารถพูดได้อย่างแน่นอนว่างานนั้นเสร็จสิ้นแล้วหรือไม่ เนื่องจากงานอาจดาวน์โหลดไฟล์ที่อาจมีการเปลี่ยนแปลง หรืออาจเขียนการประทับเวลาที่อาจแตกต่างกันในแต่ละครั้งที่เรียกใช้ โดยทั่วไปแล้วระบบจะต้องเรียกใช้งานทุกงานอีกครั้งในระหว่างการบิลด์แต่ละครั้งเพื่อให้มั่นใจในความถูกต้อง ระบบบิลด์บางระบบพยายามเปิดใช้บิลด์แบบเพิ่มทีละส่วนโดยอนุญาตให้นักพัฒนาซอฟต์แวร์ระบุเงื่อนไขที่ต้องเรียกใช้งานอีกครั้ง บางครั้งก็ทำได้ แต่หลายครั้งก็เป็นปัญหาที่ซับซ้อนกว่าที่เห็น ตัวอย่างเช่น ในภาษาอย่าง C++ ที่อนุญาตให้ไฟล์อื่นๆ รวมไฟล์ได้โดยตรง เป็นไปไม่ได้ที่จะกำหนดชุดไฟล์ทั้งหมดที่ต้องตรวจสอบการเปลี่ยนแปลงโดยไม่แยกวิเคราะห์แหล่งที่มาของอินพุต นักพัฒนาซอฟต์แวร์มักจะใช้ทางลัด และทางลัดเหล่านี้อาจนำไปสู่ปัญหาที่เกิดขึ้นไม่บ่อยนักและน่าหงุดหงิด ซึ่งผลลัพธ์ของงานจะถูกนำกลับมาใช้ซ้ำแม้ว่าจะไม่ควรทำก็ตาม เมื่อเกิดเหตุการณ์นี้บ่อยครั้ง นักพัฒนาซอฟต์แวร์จะติดนิสัยในการเรียกใช้คำสั่ง clean ก่อนการบิลด์ทุกครั้งเพื่อให้ได้สถานะใหม่ ซึ่งเป็นการทำลายวัตถุประสงค์ของการมีบิลด์แบบเพิ่มทีละส่วนไปโดยสิ้นเชิง การพิจารณาว่าเมื่อใดที่ต้องเรียกใช้งานอีกครั้งเป็นเรื่องที่ซับซ้อนอย่างน่าประหลาดใจ และเป็นงานที่เครื่องจักรจัดการได้ดีกว่ามนุษย์
ความยากในการดูแลรักษาและแก้ไขข้อบกพร่องของสคริปต์
สุดท้ายนี้ สคริปต์บิลด์ที่กำหนดโดยระบบบิลด์ที่อิงตามงานมักใช้งานได้ยาก แม้ว่าสคริปต์บิลด์มักจะได้รับการตรวจสอบน้อยกว่า แต่สคริปต์บิลด์ก็เป็นโค้ดเช่นเดียวกับระบบที่กำลังสร้าง และเป็นที่ซ่อนข้อบกพร่องได้ง่าย ต่อไปนี้เป็นตัวอย่างข้อบกพร่องที่พบบ่อยมากเมื่อทำงานกับระบบบิลด์ที่อิงตามงาน
- งาน A ต้องพึ่งพางาน B เพื่อสร้างไฟล์หนึ่งๆ เป็นเอาต์พุต เจ้าของงาน B ไม่ทราบว่างานอื่นๆ ต้องพึ่งพางานนี้ จึงเปลี่ยนงาน B ให้สร้างเอาต์พุตในตำแหน่งอื่น ซึ่งจะตรวจพบไม่ได้จนกว่าจะมีคนพยายามเรียกใช้งาน A แล้วพบว่างานนั้นล้มเหลว
- งาน A ต้องพึ่งพางาน B ซึ่งต้องพึ่งพางาน C ซึ่งสร้างไฟล์หนึ่งๆ เป็นเอาต์พุตที่งาน A ต้องการ เจ้าของงาน B ตัดสินใจว่างาน B ไม่จำเป็นต้องพึ่งพางาน C อีกต่อไป ซึ่งทำให้งาน A ล้มเหลวแม้ว่างาน B จะไม่สนใจงาน C เลยก็ตาม
- นักพัฒนาซอฟต์แวร์ของงานใหม่สันนิษฐานเกี่ยวกับเครื่องที่เรียกใช้งานโดยไม่ตั้งใจ เช่น ตำแหน่งของเครื่องมือหรือค่าของตัวแปรสภาพแวดล้อมหนึ่งๆ งานนี้ทำงานในเครื่องของนักพัฒนาซอฟต์แวร์ แต่ล้มเหลวทุกครั้งที่นักพัฒนาซอฟต์แวร์คนอื่นลองใช้
- งานหนึ่งๆ มีคอมโพเนนต์แบบไม่กำหนดผลลัพธ์ เช่น การดาวน์โหลดไฟล์จากอินเทอร์เน็ตหรือการเพิ่มการประทับเวลาลงในบิลด์ ตอนนี้ผู้ใช้จะได้รับผลลัพธ์ที่อาจแตกต่างกันในแต่ละครั้งที่เรียกใช้งาน ซึ่งหมายความว่านักพัฒนาซอฟต์แวร์จะไม่สามารถทำซ้ำและแก้ไขความล้มเหลวของผู้อื่นหรือความล้มเหลวที่เกิดขึ้นในระบบบิลด์อัตโนมัติได้เสมอไป
- งานที่มีทรัพยากร Dependency หลายรายการอาจทำให้เกิดสภาวะการแข่งขัน หากงาน A ต้องพึ่งพาทั้งงาน B และงาน C และทั้งงาน B และ C แก้ไขไฟล์เดียวกัน งาน A จะได้รับผลลัพธ์ที่แตกต่างกันขึ้นอยู่กับว่างาน B หรือ C งานใดเสร็จก่อน
ไม่มีวิธีอเนกประสงค์ในการแก้ปัญหาด้านประสิทธิภาพ ความถูกต้อง หรือความสามารถในการดูแลรักษาเหล่านี้ภายในเฟรมเวิร์กที่อิงตามงานที่ระบุไว้ที่นี่ ตราบใดที่นักพัฒนาซอฟต์แวร์สามารถเขียนโค้ดที่กำหนดเองซึ่งทำงานระหว่างการบิลด์ได้ ระบบก็จะมีข้อมูลไม่เพียงพอที่จะเรียกใช้งานได้อย่างรวดเร็วและถูกต้องเสมอไป หากต้องการแก้ปัญหา เราต้องลดอำนาจของนักพัฒนาซอฟต์แวร์และมอบอำนาจนั้นคืนให้กับระบบ และเปลี่ยนแนวคิดเกี่ยวกับบทบาทของระบบใหม่ โดยให้ระบบไม่ใช่ผู้เรียกใช้งาน แต่เป็นผู้สร้างอาร์ติแฟกต์
แนวทางนี้นำไปสู่การสร้างระบบบิลด์ที่อิงตามอาร์ติแฟกต์ เช่น Blaze และ Bazel
