Build Script — สคริปท์สร้างโปรแกรม

วันนี้จะพูดเรื่อง build script สักหน่อยครับ โดยปรกติเวลาที่เราเขียนโปรแกรมขึ้นมาสักตัวเนี่ย จากซอร์สโค๊ดไปเป็นไฟล์โปรแกรมจะผ่านขั้นตอนบางอย่าง อย่างถ้าเป็นภาษา C/C++ ก็ต้อง compile แล้วก็ link หรือถ้าเป็นพวก java web application ก็ต้อง compile แล้วเอาผลลัพท์ไปแพ็ครวมกันเป็นแ็คเกจ ขั้นตอนนี้จะเรียกรวม ๆ ว่าการ build ก็คือการเอาไฟล์ต้นฉบับมาสร้างให้เป็นไฟล์แอพพลิเคชั่นที่พร้อมจะถูกเรียกใช้งานนั่นเอง

การ build โปรแกรมนั้นสามารถทำด้วยมือได้ อย่างถ้าคุณเขียนโปรแกรมด้วยภาษา C เนี่ยก็สามารถบิลด์ด้วยคำสั่ง

 gcc test.cpp -o test.exe

หรือถ้าเป็น java ก็จะเป็น

javac Test.java

ซึ่ง พอโปรแกรมเริ่มมีขนาดใหญ่ เริ่มมีจำนวนไฟล์มากขึ้น การที่จะสั่ง build โปรแกรมด้วยมือนั้นคงเป็นเรื่องที่น่าหงุดหงิดมิใช่น้อย โปรแกรม IDE แทบทุกตัวจึงให้ระบบ build โปรแกรมติดมาด้วย อย่างใน Visual Studio ก็แค่กด Ctrl+Shift+B หรือใน Eclipse ก็กด Ctrl+B (ทั้งสองโปรแกรมสามารถสั่ง build ได้จากเมนูครับ แต่จำ shortcut ไว้ก็ดี) ระบบก็จะเริ่ม build โปรแกรมให้คุณ ง่ายไหมล่ะ ?

แต่ระบบ build ของ IDE เองก็มีข้อจำกัด ด้วยความที่ IDE นั้นออกแบบมาเพื่อให้ใช้งานได้จาก GUI ถ้าหากว่าโปรแกรมนั้น ๆ มีขั้นตอนในการ build โปรแกรมที่ซับซ้อนมาก ๆ การเขียน GUI ขึ้นมารองรับทุกกรณีที่เป็นไปได้นั้นเป็นเรื่องยากครับ

ผลิตภัณฑ์ที่ผมทำงานด้วยตัวหนึ่งมีเอกสารอธิบายวิธี build โปรแกรมความยาว 20 หน้ากระดาษ A4 ประกอบอยู่ครับ และขั้นตอนนี้ใช้ IDE ในการ build แล้วนะครับ ซึ่งที่ต้องอธิบายเยอะมากขนาดนั้นเป็นเพราะว่าต้องแก้ไฟล์ configuration หลายไฟล์ และต้องมีการเตรียมไฟล์ resource ซึ่งต้องทำด้วยมือเพราะว่า IDE ไม่รองรับอีกด้วย ที่แย่กว่านั้นคือถ้ามีอะไรที่ทำผิดขั้นตอนไปแม้แต่ขั้นตอนเดียว โปรแกรมนั้นอาจจะไม่ทำงานเลยล่ะครับ

แล้วเราจะทำยังไงให้การ build โปรแกรมนั้นทำได้ง่ายขึ้นดีล่ะ ?

Build File

สำหรับปัญหาการ build โปรแกรม ผมเชื่อว่าหลาย ๆ คนน่าจะคิดถึง build file เป็นอันดับแรก

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

ระบบที่อ่าน build file เพื่อที่จะ build โปรแกรมนั้นก็มีที่นิยมอยู่หลายตัว อย่าง Apache Ant สำหรับ Java หรือ GNU Make สำหรับแพลตฟอร์มบน Unix

build file เองจริง ๆ ก็ไม่ได้ดีกว่าระบบ build บน IDE สักเท่าไหร่หรอกครับ จริงอยู่ว่าเราสามารถระบุการ build ว่าจะต้องทำอะไรยังไงบ้างได้คร่าว ๆ แต่การเพิ่มรายละเอียดในแต่ละขั้นตอนของการ build โปรแกรมนั้นเป็นเรื่องทำได้ยากจนถึงขั้นทำไม่ได้เลยในวิธีนี้ ถ้าคุณเจอซอฟท์แวร์ที่มีขั้นตอนการ build ที่ซับซ้อนมาก ๆ ก็คงเป็นเรื่องลำบากที่จะเขียน build script ให้ทำงานได้อย่างถูกต้องและครบถ้วนอยู่ดี

ที่แย่กว่านั้นคือ build file อ่านยากครับ … มันจะทำอะไรบ้างเราก็ไม่รู้ บางทีต้องปรับแก้โน่นนั่นนี่เพราะว่าไม่รู้ว่าผลลัพท์หน้าตาจะเป็นยังไง

สำหรับผลิตภัณฑ์ที่ผมพูดถึงข้างบน ผมเคยลองเขียน build file แล้ว มันช่วยลดขั้นตอนการบิลด์จาก 20 หน้า เหลือ … ประมาณ 18 หน้าครับ พูดง่าย ๆ คือมันไม่ช่วยอะไรเลย …

Build Script

พูดถึงคำว่า script มันก็เป็นโปรแกรมประเภทหนึ่งที่สามารถรันได้โดยไม่ต้องคอมไพล์ ส่วนเจ้า build script นั้นก็คือโปรแกรมประเภท script ที่เราเขียนขึ้นเพื่อ build โปรแกรมโดยเฉพาะ

ด้วยความที่มันเป็นโปรแกรม (ไม่ใช่แค่การบอก input ให้กับโปรแกรม เหมือนการใช้ build file) เราจึงสามารถที่จะเขียนให้มันทำอะไรก็ได้ตามที่เราต้องการ ซึ่งแน่นอนว่ามันยืดหยุ่นมากกว่าการใช้ build file มาก

ในอดีีต build script นั้นก็ใช้ภาษา script ที่นิยม ๆ กันอย่าง perl, shell script อะไรทำนองนี้นั่นล่ะครับ แต่ปัญหาคือเราต้องเขียนทุกอย่างเองหมดเลยทุกขั้นตอน ซึ่งแน่นอนว่ามันยืดหยุ่น แต่มันก็วุ่นวายด้วย เป็นเรื่องยากที่จะทำให้มันถูกต้อง

สำหรับปัจจุบันมีการพัฒนาภาษา script ที่ใช้สำหรับ build โดยเฉพาะ หรือพัฒนาส่วนของ library ของภาษา script ขึ้นมาเพื่อรองรับการ build ตัวที่ผมใช้แล้วได้ผลดีก็คือ Gradle ส่วนที่ลองค้น ๆ ดูแล้วเจอก็คือ Rake ครับ

Gradle เป็นภาษา Build Script ที่พัฒนาขึ้นจากภาษา Groovy ที่เป็นภาษาที่ทำงานบน Java Virtual Machine (เหมือนกับ XTend ที่ผมพูดถึงบ่อย ๆ ครับ) ซึ่งจริงๆ จะเรียกว่าเป็น Script มันก็ไม่เชิงเพราะว่ามันจะถูกคอมไพล์เป็น Java Class ก่อน แต่ว่าตรงนี้ตัวระบบจะเป็นคนจัดการทั้งหมด แถมคอมไพล์ใหม่ทุกครั้งที่รันโปรแกรม ดังนั้นก็พอถูไถเรียกว่าเป็น Script ได้ล่ะครับ แต่ด้วยความที่มันทำงานบน JVM เราจึงสามารถใช้ Gradle ทำทุกอย่างที่ Java ให้ได้หมดเลย ผมสามารถเขียน build script ให้สร้าง UI เป็น Swing ได้ด้วยนะ (แต่คงไม่ทำ 555)

ผมสร้าง build script ของผลิตภัณฑ์ข้างต้นนี้บนภาษา Gradle (ซึ่งที่จริงก็เป็น Groovy นั่นแหละ) ความสำเร็จของผมก็คือ ผมสามารถย่อคู่มือขนาด 20 หน้าให้เหลือแค่ … บรรทัดเดียว ดังนี้ครับ

gradle jettyRun -Pclient=xxx

script ของผมก็จะทำการคอมไพล์โปรแกรม เตรียมไฟล์ resource แก้ไฟล์ configuration ต่าง ๆ และโหลดเอาโปรแกรมไปรันบน Jetty ซึ่งเป็น Servlet Container ตัวหนึั่ง (เหมือน Tomcat ขั้นตอนต่อไปที่ผมต้องทำคือ … เปิด Web Browser เพื่อเข้าโปรแกรม!

ผมสามารถรันโปรแกรมได้ภายในเวลาไม่ถึงสองนาที จากปรกติที่ต้องเสียเวลาเกือบชม. ผมว่านี่คือความสำเร็จของผมนะ! (เนื่องด้วยความที่ผลิตภัณฑ์เป็น closed source ผมจึงแสดงโค๊ดให้ดูไม่ได้ครับ)

build script สมัยใหม่นั้นได้รวมเอาความเรียบง่ายของ build file มารวมเข้ากับความยืดหยุ่นของของภาษา script ซึ่ง build script ที่เรียบง่ายมาก ๆ นี่มองผ่าน ๆ จะเหมือน build file เลยครับ เช่นไฟล์ข้างล่างนี้

apply plugin: 'java'

sourceCompatibility = 1.5
version = '1.0'
jar {
    manifest {
        attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
    testCompile group: 'junit', name: 'junit', version: '4.+'
}

ดูไม่เหมือนภาษาเขียนโปรแกรมเลยใช่ไหมครับ ?

lambda expression – ประโยคสร้างวัตถุแทนฟังก์ชั่น

หลาย ๆ คนน่าจะเคยเห็นฟังก์ชั่นที่รับพารามิเตอร์เป็นฟังก์ชั่นพอยน์เตอร์ในภาษา C หรือเป็นวัตถุที่มีเมธอดเดียว ใช้สำหรับระบุรายละเอียดวิธีการทำงานของฟังก์ชั่นนั้น ๆ (เรียกว่า functor) อย่างในภาษา C++ จะมีฟังก์ชั่นอย่าง std::sort ซึ่งรับพารามิเตอร์ตัวหนึ่งเป็น functor ที่เปรียบเทียบวัตถุสองตัวแล้วบอกว่าตัวไหนน้อยกว่าตัวไหน

template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );

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

ปัญหาก็คือ … การสร้างเจ้าตัว functor มันวุ่นวายครับ มันจะหน้าตาประมาณนี้ …

struct 
{
    bool operator()(image* a, image* b)
    {   
        return a->area() < b->area();
    }   
} compareArea;

std::vector<image*> vecImage;
std::sort(vecImage.begin(), vecImage.end(), compareArea);

ที่จริงเราเขียนเป็นรูปฟังก์ชั่นก็ได้ แต่ก็จะอ่านแล้วงง ๆ + เราก็จะเสียชื่อฟังก์ชั่นไปอีกหนึ่งชื่อ ทั้ง ๆ ที่ความจริงบางทีเราก็ไม่ต้องการให้คนอื่นใช้ฟังก์ชั่นนี้นอกจากโค๊ดของเรา การเขียนเป็นลักษณะ functor นั้นจะช่วยเรื่องการไม่ให้คนอื่นใช้ฟังก์ชั่นเราได้ครับ

lambda expression เป็นฟีเจอร์ใหม่(?) ของหลาย ๆ ภาษาที่อนุญาตให้ผู้ใช้สามารถสร้าง functor จาก expression ได้เลยโดยไม่ต้องประกาศเป็นตัวแปรหรือวัตถุใหม่ จากข้างบนในภาษา C++ เราจะสามารถเขียนด้วย lambda exp ได้แบบนี้ครับ

std::vector<image*> vecImage;
std::sort(vecImage.begin(), vecImage.end(), [](image* a, image* b) {
    return a->area() < b->area();
});

เทียบกับการสร้างวัตถุ + โอเวอร์โหลด operator() แล้ว น่าจะดูง่ายกว่านะครับ แถมไม่ต้องประกาศตัวแปรไปรับก็ได้ เจ๋งใหม่

แต่เราจะประกาศตัวแปรเก็บไว้ใช้ทีหลังก็ได้นะครับ แบบนี้

auto helloWorld = [](){ std::cout<<"Hello World"<<std::endl; };
helloWorld();

การใช้ lambda expression นี่ทำให้เราทำงานกับฟังก์ชั่นใน ได้ง่ายขึ้นเยอะเลยครับ เพราะว่าเฮดเดอร์ชุดนี้มีฟังก์ชั่นที่รับ functor เป็นพารามิเตอร์มากทีเดียว

ลักษณะพิเศษอย่างหนึ่งของ lambda ก็คือ ฟังก์ชั่นที่เกิดขึ้นในขณะนั้นจะมีสภาวะเป็น closure คือมันจะสามารถเข้าถึงตัวแปรรอบ ๆ ได้ด้วยครับ อย่างโค๊ดข้างล่างนี้

std::vector<image*> vecImage;
std::for_each(vecImage.begin(), vecImage.end(), [this](image* a){
    this->render(a);
});

ผมใช้ [this] แทนที่จะเป็น [] อย่างตัวอย่างข้างบน เพื่อเป็นการระบุว่าในแต่ละ functor ที่เกิดขึ้นนั้นจะเข้าถึง this reference ได้ด้วย

หรืออีกตัวอย่างหนึ่ง

std::vector<int> vecNumber;
int total = 0;
std::for_each(vecNumber.begin(), vecNumber.end(), [&](int a){
    total += a;
});

ถ้าเป็น functor แบบที่ใช้ struct คงเข้าถีึงตัวแปร total ไม่ได้ แต่นี่ไม่ใช่ปัญหาสำหรับ closure ที่เกิดจาก lambda expression ครับ

ตัวอย่างที่ยกมาในครั้งนี้เขียนบนภาษา C++11 ครับ แต่ภาษาอื่น ๆ เขาก็มีฟีเจอร์นี้กันมาชาตินึงได้แล้วล่ะ (C#4.0, Scheme, Clojure, และอื่น ๆ ) ส่วนภาษายอดนิยมอย่าง Java นั้นจะมีฟีเจอร์นี้เข้ามาในเวอร์ชั่น 8 ครับ (ซึ่งออกตัว RC1 มาแล้ว)

Self-Hosting Source Control อย่างง่าย ๆ

ผมเคยพูดถึงเรื่อง Revision Control ไปหลายรอบแล้วเหมือนกัน ครั้งนี้ผมอยากจะแนะนำวิธีที่ผมใช้กับตัวเองอยู่ในปัจจุบัน ก็คือ host file เองใน server ของตัวเองครับ

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

ก่อนอื่นต้องบอกก่อนเลยว่า วิธีที่ง่ายและถูกที่สุดในการสร้าง repository ที่เข้าถึงได้จากทุกที่บนอินเตอร์เนท คือใช้บริการของ BitBucket ครับ ใช้งานได้ 5 คน โค๊ดไม่ต้อง open source ก็ได้ คนอื่นเข้าถึงโค๊ดเราไม่ได้ เรียกได้ว่าสะดวกมาก ๆ

แต่ผมเลือกที่จะ host เองเพราะว่า … ผมมี server ของตัวเองอยู่แล้ว และก็แชร์ไฟล์ผ่านทาง SSH อยู่แล้ว ดังนั้นสิ่งที่ผมทำมีอยู่แค่…ติดตั้ง Git ครับ … เท่านี้ผมก็สามารถเข้าถึง repository ของผมบนอุปกรณ์ของผมเองจากที่ไหนก็ได้ มีผู้ใช้กี่คนก็ได้ และมีขนาดได้สูงสุด 3TB (ตาม HDD ที่ผมใช้ 555) ซึ่งยืดหยุ่นกว่าใช้บริการภายนอกมากทีเดียว

ข้อเสียเองก็มีเหมือนกัน โดยหลัก ๆ แล้วคือ การ commit code ขึ้นไปนั้นจะใช้เวลาค่อนข้างมากถ้าทำจากนอกเนทเวิร์คที่บ้าน เพราะว่า internet ที่ผมใช้มี uplink แค่ 512Kbps แล้วก็จะมีเรื่องของค่าไฟเพิ่มเข้ามา (ผมใช้แชร์ไฟล์อยู่แล้วดังนั้นก็ต้องเสียค่าไฟอยู่แล้ว)

ข้างล่างนี้คือรายละเอียดทั้งหมดที่ผมทำครับ

  1. สร้าง file server ขึ้นมาสักตัว ผมใช้ Raspberry Pi กับ Ext. HDD ของ Seagate (3.5″) ขนาด 1TB เพื่อการนี้ครับ
  2. ติดตั้ง SSH server เพื่อที่จะเข้าถึง repository ได้จากภายนอกเครื่องครับ (ถ้าไม่มีจะสามารถเข้าถึงได้จากตัว server เท่านั้น) เซ็ต user ให้เสามารถเข้าถึง server ผ่าน ssh นี้ด้วยนะครับ
  3. ติดตั้ง git เพื่อเอาไว้สร้าง repository ครับ
  4. สร้าง git repository ในตัวเครื่องด้วยคำสั่ง git init --bare <path-to-repository> ครับ ตัวพารามิเตอร์ --bare นั้นเป็นการระบุว่าให้สร้าง repository โดยไม่ต้องสร้าง working copy ขึ้นมา เพราะเราจะไม่แก้ไฟล์จากในตัว server เลยครับ
  5. สำหรับการเข้าถึงจากเครือข่ายที่อยู่ข้างนอก เราต้องทำการฟอร์เวิร์ดพอร์ทหนึ่งจากตัว router ไปยังพอร์ท 22 ของ server ผมเลือกพอร์ท 2222 เพราะว่าพอร์ท 22 ผมใช้แอคเซสเข้าตัว router ไปแล้วครับ แต่จะใช้พอร์ท 22 ก็ได้ครับ ไม่ผิดแต่อย่างใด
  6. สุดท้ายคือไปสมัครบริการ dynamic dns มาสักตัว ผมใช้บริการของ duckdns.org ครับ ที่ใช้ ddns ช่วยก็เพราะว่าเลขไอพีของเนทเวิร์คเราจะเปลี่ยนไปเรื่อย ๆ ครับ ดังนั้นถ้าให้เราไปจด domain ด้วยวิธีปรกติเนี่ยคงจะไม่ได้ (การเปลี่ยน ip ของตัว static dns ใช้เวลาเป็นวัน ๆ) ส่วนจะให้จำเลขไอพีก็คงไม่เวิร์ค ดังนั้นเราใช้ ddns ช่วยจะง่ายที่สุด เพราะว่าสามารถเปลี่ยนเลขไอพีของ domain name ที่เราใช้อยู่ได้ตลอดเวลา

ทั้งนี้เราจำเป็นต้องมี service หนึ่งตัวที่คอยแจ้งเปลี่ยน IP ให้กับตัว DDNS ทุกครั้งที่เลขไอพีของเนทเวิร์คเราเปลี่ยน ไม่เช่นนั้น domain name ของเราก็คงไร้ประโยชน์ เจ้า service นี้อาจจะรันทืี่ตัว router หรือที่ตัว server ก็ได้ครับ (ผมเลือกที่จะตั้งไว้ที่ router แต่กำลังจะเปลี่ยน 555)

ทีนี้เราก็ได้ repository ที่รันในบ้านเราเอง และเข้าจากที่ไหนก็ได้ผ่านอินเตอร์เนทแล้ว ง่ายไหมครับ 555 ที่จริงบล็อกนี้ไม่มีอะไรเลยนะ

อ้อ ทีนี้แล้วจะใช้ยังไง … เราก็ clone ลงไปที่เครื่องที่ใช้ตามปรกติ หรือจะ push ขึ้นไป หรืออะไรก็แล้วแต่ แต่ที่สำคัญคือ url ที่ใช้นั้นจะเป็น ssh://<username>@<domainname>:port/:<path-to-repository> ครับ อย่างอันที่ผมใช้ก็จะเป็น … เอ๊ะ เดี๋ยว ผมบอกไม่ได้อ่ะครับ 555 ตัวอย่างก็จะเป็นแบบนี้ ssh://noom@example.host.doesnot.exists.com:2222/:home/wutipong/repository/myproj.git ครับ (username นั้นจะไม่ใส่ก็ได้ เดี๋ยวระบบก็จะขึ้นให้ใส่ user กับ password เอง)

หวังว่าจะไม่ยากเกินไปนะครับ