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

รายงานปัญหา ดูแหล่งที่มา

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

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

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

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

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

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

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

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

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