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

หน้านี้จะพูดถึงระบบบิลด์ที่อิงตามงาน วิธีการทำงาน และข้อมูลแทรกที่อาจเกิดขึ้นกับระบบที่อิงตามงาน หลังจากสคริปต์ 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>

ไฟล์บิลด์จะเขียนไว้ใน XML และกำหนดข้อมูลเมตาทั่วไปบางส่วนเกี่ยวกับบิลด์พร้อมด้วยรายการงาน (แท็ก <target> ใน XML) (มดใช้คำว่า target เพื่อแสดงถึง task และใช้คำว่า task เพื่ออ้างถึงคำสั่ง) แต่ละงานจะเรียกใช้รายการคำสั่งที่เป็นไปได้ที่กำหนดโดย Ant ซึ่งรวมถึงการสร้างและลบไดเรกทอรี การเรียกใช้ javac และการสร้างไฟล์ JAR คำสั่งชุดนี้สามารถขยายได้ด้วยปลั๊กอินที่ได้จากผู้ใช้เพื่อให้ครอบคลุมตรรกะทุกประเภท แต่ละงานยังสามารถกำหนดงานที่ต้องใช้ผ่านแอตทริบิวต์ทรัพยากรได้ด้วย ทรัพยากร Dependency เหล่านี้จะสร้างกราฟแบบวงกลม ดังที่แสดงในรูปที่ 1

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

รูปที่ 1 กราฟแบบอะซิงโครนัสที่แสดงทรัพยากร Dependency

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

  1. โหลดไฟล์ชื่อ build.xml ในไดเรกทอรีปัจจุบันและแยกวิเคราะห์เพื่อสร้างโครงสร้างกราฟที่แสดงในรูปที่ 1
  2. ค้นหางานชื่อ dist ที่ระบุในบรรทัดคำสั่งและพบว่างานดังกล่าวขึ้นอยู่กับงานชื่อ compile
  3. ค้นหางานชื่อ compile และพบว่างานดังกล่าวขึ้นอยู่กับงานชื่อ init
  4. ค้นหางานชื่อ init และพบว่าไม่มีทรัพยากร Dependency
  5. ดำเนินการตามคำสั่งที่ระบุไว้ในงาน init
  6. เรียกใช้คำสั่งที่กำหนดไว้ในงาน compile เนื่องจากมีการเรียกใช้ทรัพยากร Dependency ทั้งหมดของงานดังกล่าว
  7. เรียกใช้คำสั่งที่กำหนดไว้ในงาน 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 เนื่องจากงาน B และ C ไม่มีการพึ่งพากัน คุณจะเรียกใช้พร้อมกันได้อย่างปลอดภัยไหม เพื่อให้ระบบทำงาน ก ได้เร็วขึ้น บางที พวกเขาอาจไม่ได้ เปลี่ยนแปลงอะไรในแหล่งข้อมูลเดียวกัน อาจจะไม่เหมือนกัน บางทีทั้งคู่อาจใช้ไฟล์เดียวกันในการติดตามสถานะ และการเรียกใช้สถานะพร้อมกันก็อาจทำให้เกิดความขัดแย้งได้ โดยทั่วไปแล้วไม่มีวิธีใดที่จะทำให้ระบบทราบได้ จึงต้องเสี่ยงที่จะขัดแย้งกัน (ซึ่งนำไปสู่ปัญหาบิลด์ที่พบได้ยากแต่แก้ไขยาก) หรือจำเป็นต้องจำกัดบิลด์ทั้งหมดให้ทำงานในเทรดเดียวในกระบวนการเดียว นี่อาจเป็นการสิ้นเปลืองคอมพิวเตอร์สำหรับนักพัฒนาซอฟต์แวร์ที่มีประสิทธิภาพ และจะลบล้างความเป็นไปได้ของการเผยแพร่บิลด์ในเครื่องหลายเครื่องอย่างสิ้นเชิง

ความยากในการใช้บิลด์ที่เพิ่มขึ้น

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

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

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

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

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

แนวทางนี้นำไปสู่การสร้างระบบบิลด์ที่อิงตามอาร์ติแฟกต์ เช่น Blaze และ Bazel