ระบบบิลด์ตามงาน

หน้านี้ครอบคลุมระบบบิลด์แบบอิงตามงาน วิธีการทำงาน และความซับซ้อนบางอย่างที่อาจเกิดขึ้นกับระบบแบบอิงตามงาน ระบบบิลด์แบบอิงตามงานเป็นการพัฒนาที่สมเหตุสมผลถัดจากการใช้สคริปต์เชลล์

ทำความเข้าใจระบบบิลด์แบบอิงตามงาน

ในระบบบิลด์แบบอิงตามงาน หน่วยงานพื้นฐานคือ "งาน" งานแต่ละรายการคือสคริปต์ที่สามารถดำเนินการตามตรรกะทุกประเภท และงานจะระบุงานอื่นๆ เป็นทรัพยากร 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

กราฟอะคริลิกแสดงทรัพยากร Dependency

รูปที่ 1 กราฟแบบไม่มีวงจรที่แสดงทรัพยากร Dependency

ผู้ใช้ดำเนินการบิลด์โดยระบุงานให้กับเครื่องมือบรรทัดคำสั่งของ Ant ตัวอย่างเช่น เมื่อผู้ใช้พิมพ์ ant dist Ant จะทำตามขั้นตอนต่อไปนี้

  1. โหลดไฟล์ชื่อ build.xml ในไดเรกทอรีปัจจุบันและแยกวิเคราะห์ไฟล์เพื่อสร้างโครงสร้างกราฟที่แสดงในรูปที่ 1
  2. ค้นหางานชื่อ dist ที่ระบุไว้ในบรรทัดคำสั่งและพบว่างานนี้มีทรัพยากร Dependency เป็นงานชื่อ compile
  3. ค้นหางานชื่อ compile และพบว่างานนี้มีทรัพยากร Dependency เป็นงานชื่อ init
  4. ค้นหางานชื่อ init และพบว่างานนี้ไม่มีทรัพยากร Dependency
  5. ดำเนินการตามคำสั่งที่กำหนดไว้ในงาน init
  6. ดำเนินการตามคำสั่งที่กำหนดไว้ในงาน compile เมื่อทรัพยากร Dependency ทั้งหมดของงานนั้นทำงานแล้ว
  7. ดำเนินการตามคำสั่งที่กำหนดไว้ในงาน dist เมื่อทรัพยากร Dependency ทั้งหมดของงานนั้นทำงานแล้ว

ท้ายที่สุด โค้ดที่ Ant ดำเนินการเมื่อเรียกใช้งาน dist จะเทียบเท่ากับสคริปต์เชลล์ต่อไปนี้

./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 ในช่วงหลายปีที่ผ่านมาและแทนที่ 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