หน้านี้ครอบคลุมระบบบิลด์แบบอิงตามงาน วิธีการทำงาน และความซับซ้อนบางอย่างที่อาจเกิดขึ้นกับระบบแบบอิงตามงาน ระบบบิลด์แบบอิงตามงานเป็นการพัฒนาที่สมเหตุสมผลถัดจากการใช้สคริปต์เชลล์
ทำความเข้าใจระบบบิลด์แบบอิงตามงาน
ในระบบบิลด์แบบอิงตามงาน หน่วยงานพื้นฐานคือ "งาน" งานแต่ละรายการคือสคริปต์ที่สามารถดำเนินการลอจิกทุกประเภท และงานจะระบุงานอื่นๆ เป็นทรัพยากร 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 ไม่ต้องพึ่งพากัน จึงเรียกใช้งานทั้ง 2 งานพร้อมกันได้อย่างปลอดภัยเพื่อให้ระบบสามารถไปยังงาน A ได้เร็วขึ้นหรือไม่ อาจทำได้หากทั้ง 2 งานไม่แตะต้องทรัพยากรเดียวกัน แต่ก็อาจทำไม่ได้เช่นกัน เนื่องจากทั้ง 2 งานอาจใช้ไฟล์เดียวกันเพื่อติดตามสถานะของตนเอง และการเรียกใช้งานทั้ง 2 งานพร้อมกันอาจทำให้เกิดความขัดแย้ง โดยทั่วไปแล้วระบบไม่มีทางทราบได้ ดังนั้นระบบจึงต้องเสี่ยงกับความขัดแย้งเหล่านี้ (ซึ่งนำไปสู่ปัญหาบิลด์ที่เกิดขึ้นไม่บ่อยนักแต่แก้ไขข้อบกพร่องได้ยากมาก) หรือต้องจำกัดบิลด์ทั้งหมดให้ทำงานในเธรดเดียวในกระบวนการเดียว ซึ่งอาจเป็นการสิ้นเปลืองทรัพยากรของเครื่องมือพัฒนาที่มีประสิทธิภาพสูง และตัดความเป็นไปได้ในการกระจายบิลด์ไปยังเครื่องหลายเครื่องออกไปโดยสิ้นเชิง
ความยากในการดำเนินการบิลด์แบบเพิ่มทีละส่วน
ระบบบิลด์ที่ดีช่วยให้วิศวกรดำเนินการบิลด์แบบเพิ่มทีละส่วนได้อย่างน่าเชื่อถือ เพื่อให้การเปลี่ยนแปลงเล็กน้อยไม่จำเป็นต้องสร้างฐานของโค้ดทั้งหมดใหม่ตั้งแต่ต้น ซึ่งมีความสำคัญอย่างยิ่งหากระบบบิลด์ทำงานช้าและไม่สามารถขนานขั้นตอนบิลด์ได้ด้วยเหตุผลที่กล่าวไว้ข้างต้น แต่โชคไม่ดีที่ระบบบิลด์แบบอิงตามงานก็ประสบปัญหาในส่วนนี้เช่นกัน เนื่องจากงานสามารถทำอะไรก็ได้ ระบบจึงไม่มีวิธีตรวจสอบโดยทั่วไปว่างานนั้นเสร็จสิ้นแล้วหรือไม่ งานจำนวนมากเพียงแค่รับชุดไฟล์ต้นทางและเรียกใช้คอมไพเลอร์เพื่อสร้างชุดไบนารี ดังนั้นจึงไม่จำเป็นต้องเรียกใช้อีกครั้งหากไฟล์ต้นทางพื้นฐานไม่เปลี่ยนแปลง แต่หากไม่มีข้อมูลเพิ่มเติม ระบบก็ไม่สามารถพูดได้อย่างแน่นอนว่างานนั้นเสร็จสิ้นแล้ว เนื่องจากงานอาจดาวน์โหลดไฟล์ที่อาจมีการเปลี่ยนแปลง หรืออาจเขียนการประทับเวลาที่อาจแตกต่างกันในแต่ละครั้งที่เรียกใช้งาน ระบบจึงต้องเรียกใช้งานทุกงานอีกครั้งในระหว่างการบิลด์แต่ละครั้งเพื่อให้มั่นใจในความถูกต้อง ระบบบิลด์บางระบบพยายามเปิดใช้บิลด์แบบเพิ่มทีละส่วนโดยอนุญาตให้นักพัฒนาซอฟต์แวร์ระบุเงื่อนไขที่ต้องเรียกใช้งานอีกครั้ง บางครั้งก็ทำได้ แต่หลายครั้งก็เป็นปัญหาที่ซับซ้อนกว่าที่เห็น ตัวอย่างเช่น ในภาษาอย่าง C++ ที่อนุญาตให้ไฟล์อื่นๆ รวมไฟล์ได้โดยตรง ระบบจะกำหนดชุดไฟล์ทั้งหมดที่ต้องตรวจสอบการเปลี่ยนแปลงไม่ได้หากไม่แยกวิเคราะห์แหล่งที่มาของอินพุต นักพัฒนาซอฟต์แวร์มักจะใช้ทางลัด และทางลัดเหล่านี้อาจนำไปสู่ปัญหาที่เกิดขึ้นไม่บ่อยนักและน่าหงุดหงิด ซึ่งผลลัพธ์ของงานจะถูกนำกลับมาใช้ซ้ำแม้ว่าจะไม่ควรก็ตาม เมื่อเกิดเหตุการณ์นี้บ่อยครั้ง นักพัฒนาซอฟต์แวร์จะติดนิสัยในการเรียกใช้คำสั่ง clean ก่อนการบิลด์ทุกครั้งเพื่อให้ได้สถานะใหม่ ซึ่งเป็นการทำลายวัตถุประสงค์ของการมีบิลด์แบบเพิ่มทีละส่วนไปโดยสิ้นเชิง การพิจารณาว่าเมื่อใดที่ต้องเรียกใช้งานอีกครั้งเป็นเรื่องที่ซับซ้อนอย่างน่าประหลาดใจ และเป็นงานที่เครื่องจักรจัดการได้ดีกว่ามนุษย์
ความยากในการดูแลรักษาและแก้ไขข้อบกพร่องของสคริปต์
สุดท้ายนี้ สคริปต์บิลด์ที่กำหนดโดยระบบบิลด์แบบอิงตามงานมักใช้งานได้ยาก แม้ว่าสคริปต์บิลด์มักจะได้รับการตรวจสอบน้อยกว่า แต่สคริปต์บิลด์ก็เป็นโค้ดเช่นเดียวกับระบบที่กำลังสร้าง และเป็นที่ซ่อนข้อบกพร่องได้ง่าย ต่อไปนี้คือตัวอย่างข้อบกพร่องที่พบบ่อยมากเมื่อใช้ระบบบิลด์แบบอิงตามงาน
- งาน A ต้องพึ่งพางาน B ในการสร้างไฟล์หนึ่งๆ เป็นเอาต์พุต เจ้าของงาน 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
