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

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

กราฟอะคริลิกที่แสดงการอ้างอิง

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

ปัญหาในการสร้างบิลด์ที่เพิ่มขึ้น

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

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

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

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

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

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