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

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

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

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

ความยากในการดำเนินการบิลด์แบบเพิ่มทีละส่วน

ระบบบิลด์ที่ดีช่วยให้นักพัฒนาซอฟต์แวร์ดำเนินการบิลด์แบบเพิ่มทีละส่วนได้อย่างน่าเชื่อถือ เพื่อให้การเปลี่ยนแปลงเล็กน้อยไม่จำเป็นต้องสร้างโค้ดเบสทั้งหมดใหม่ตั้งแต่ต้น ซึ่งมีความสำคัญอย่างยิ่งหากระบบบิลด์ทำงานช้าและไม่สามารถ ขนานขั้นตอนการบิลด์ได้ด้วยเหตุผลที่กล่าวไว้ข้างต้น แต่โชคไม่ดีที่ ระบบบิลด์ตามงานก็ประสบปัญหาในส่วนนี้เช่นกัน เนื่องจากงานสามารถทำอะไรก็ได้ โดยทั่วไปแล้วจึงไม่มีวิธีตรวจสอบว่างานเสร็จสมบูรณ์แล้วหรือยัง งานจำนวนมาก เพียงแค่รับชุดไฟล์ต้นฉบับและเรียกใช้คอมไพเลอร์เพื่อสร้างชุด ไบนารี ดังนั้นจึงไม่จำเป็นต้องเรียกใช้อีกครั้งหากไฟล์ต้นฉบับพื้นฐาน ไม่เปลี่ยนแปลง แต่หากไม่มีข้อมูลเพิ่มเติม ระบบก็ไม่สามารถพูดได้อย่างแน่นอนว่างานเสร็จสมบูรณ์แล้วหรือยัง เนื่องจากงานอาจดาวน์โหลดไฟล์ที่อาจมีการเปลี่ยนแปลง หรืออาจ เขียนการประทับเวลาที่อาจแตกต่างกันในแต่ละครั้งที่เรียกใช้งาน โดยทั่วไปแล้วระบบจะต้องเรียกใช้งานทุกงานอีกครั้งระหว่างการบิลด์แต่ละครั้งเพื่อให้มั่นใจในความถูกต้อง ระบบบิลด์บางระบบพยายามเปิดใช้การบิลด์แบบเพิ่มทีละส่วนโดยอนุญาตให้นักพัฒนาซอฟต์แวร์ระบุเงื่อนไขที่ต้องเรียกใช้งานอีกครั้ง บางครั้งก็ทำได้ แต่ ส่วนใหญ่มักเป็นปัญหาที่ซับซ้อนกว่าที่เห็น ตัวอย่างเช่น ในภาษา อย่าง C++ ที่อนุญาตให้ไฟล์อื่นๆ รวมไฟล์ได้โดยตรง เป็นไปไม่ได้ที่จะกำหนดชุดไฟล์ทั้งหมดที่ต้องตรวจสอบการเปลี่ยนแปลง โดยไม่แยกวิเคราะห์แหล่งที่มาของอินพุต นักพัฒนาซอฟต์แวร์มักจะใช้ทางลัด และ ทางลัดเหล่านี้อาจนำไปสู่ปัญหาที่เกิดขึ้นไม่บ่อยนักและน่าหงุดหงิด ซึ่งผลลัพธ์ของงานจะถูกนำกลับมาใช้ซ้ำแม้ว่าจะไม่ควรทำก็ตาม เมื่อเกิดเหตุการณ์นี้บ่อยครั้ง นักพัฒนาซอฟต์แวร์จะติดนิสัยในการเรียกใช้คำสั่งล้างข้อมูลก่อนการบิลด์ทุกครั้งเพื่อให้ได้สถานะใหม่ ซึ่งเป็นการทำลายวัตถุประสงค์ของการบิลด์แบบเพิ่มทีละส่วนตั้งแต่แรก การพิจารณาว่าเมื่อใดที่ต้องเรียกใช้งานอีกครั้งเป็นเรื่องที่ซับซ้อนอย่างน่าประหลาดใจ และเป็น งานที่เครื่องจักรจัดการได้ดีกว่ามนุษย์

ความยากในการดูแลรักษาและแก้ไขข้อบกพร่องของสคริปต์

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

  • งาน 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