Metaprogramming
“metaprogramming” ဟု ပြောရာတွင် မည်သည့်အရာကို ဆိုလိုသနည်း။ Code ကို တိုက်ရိုက် ရေးသားခြင်း သို့မဟုတ် ပိုမို ထိရောက်စွာ အလုပ်လုပ်ခြင်းထက် လုပ်ငန်းစဉ် (process) နှင့် ပိုမို သက်ဆိုင်သော အရာများ၏ အစုအဝေးအတွက် ကျွန်ုပ်တို့ ပေးနိုင်သည့် အကောင်းဆုံး အမည်ဖြစ်ပါသည်။ ဤသင်ခန်းစာတွင် Code များကို Build လုပ်ခြင်း၊ Testing ပြုလုပ်ခြင်း နှင့် Dependency များကို စီမံခန့်ခွဲခြင်း စနစ်များကို လေ့လာသွားပါမည်။ ကျောင်းသားတစ်ယောက်၏ နေ့စဉ် ဘဝတွင် ဤသည်တို့မှာ အရေးပါမှု နည်းပါးသည်ဟု ထင်ရသော်လည်း အလုပ်သင် အဖြစ် သို့မဟုတ် အပြင် ကမ္ဘာစစ်စစ်တွင် ကြီးမားသော Code base များနှင့် စတင် ထိတွေ့သည့်အခါ ဤအရာများကို နေရာတိုင်း၌ တွေ့မြင်ရမည် ဖြစ်ပါသည်။ “metaprogramming” ဟူသော ဝေါဟာရသည် “ပရိုဂရမ်များကို စီမံဆောင်ရွက်သော ပရိုဂရမ်များ” ဟုလည်း အဓိပ္ပာယ် ရနိုင်သည်ကို သတိပြုပါ၊ သို့သော် ဤသင်ခန်းစာ၏ ရည်ရွယ်ချက်မှာ ထို အဓိပ္ပာယ်မျိုး မဟုတ်ပါ။
Build systems
LaTeX ဖြင့် စာတမ်းတစ်စောင် ရေးသားပါက၊ စာတမ်း ထွက်ပေါ်လာရန် မည်သည့် Command များကို runရမည်နည်း။ Benchmark များကို runရန်၊ ပုံဆွဲရန်၊ ထိုပုံကို စာတမ်းထဲ ထည့်သွင်းရန် Command များရော မည်သို့နည်း။ သို့မဟုတ် အတန်းတွင် ပေးထားသော Code များကို Compile လုပ်ပြီး Test များကို runရန် မည်သို့ လုပ်ရမည်နည်း။
Code ပါသည်ဖြစ်စေ မပါသည်ဖြစ်စေ ပရောဂျက် အများစုတွင် “build process” (ဆောက်လုပ်မှု လုပ်ငန်းစဉ်) တစ်ခု ရှိကြပါသည်။ Input မျက်နှာပြင်မှ Output သို့ ရောက်ရှိရန် လုပ်ဆောင်ရမည့် လုပ်ငန်းစဉ် အစဉ်လိုက် ဖြစ်ပါသည်။ မကြာခဏဆိုသလို ထို လုပ်ငန်းစဉ်တွင် အဆင့်များစွာ၊ လမ်းခွဲများစွာ ပါဝင်နိုင်ပါသည်။ ပုံဆွဲရန် ဤသည်ကို runပါ၊ ရလဒ်များ ထုတ်ရန် ထိုသည်ကို runပါ၊ စာတမ်းအချောသတ် ထုတ်ရန် အခြားတစ်ခု runပါ ဟူ၍ ဖြစ်ပါသည်။ ဤအတန်းတွင် တွေ့မြင်ခဲ့ရသော အရာများစွာကဲ့သို့ပင် ဤ စိတ်ပျက်ဖွယ်ရာများနှင့် ကြုံတွေ့ရသူမှာ သင် တစ်ယောက်တည်း မဟုတ်ပါ၊ ကံကောင်းစွာပင် သင့်ကို ကူညီနိုင်မည့် Tool များစွာ ရှိနေပါပြီ။
ယင်းတို့ကို အများအားဖြင့် “build systems” ဟု ခေါ်ဆိုကြပြီး အမျိုးအစား များစွာ ရှိကြပါသည်။ မည်သည့် စနစ်ကို သုံးမည်ဆိုသည်မှာ လက်ရှိ လုပ်ဆောင်ရမည့် အလုပ်၊ နှစ်သက်သော ဘာသာစကား နှင့် ပရောဂျက် ဆိုဒ် ပေါ် မူတည်ပါသည်။ သို့သော် ယင်းတို့၏ ပင်မ အနှစ်သာရမှာ အလွန် ဆင်တူကြပါသည်။ အချို့သော dependencies များ၊ targets များ နှင့် တစ်ခုမှ တစ်ခုသို့ ကူးပြောင်းရန် rules များကို သတ်မှတ်ပေးရသည်။ Build system ကို သီးခြား Target တစ်ခု လိုချင်ကြောင်း ပြောလိုက်ပါက၊ ယင်း Target ၏ မှီခိုမှုများ အားလုံးကို ရှာဖွေပြီး နောက်ဆုံး Target မထွက်မချင်း ရလဒ်အလယ်အလတ်များကို ထုတ်လုပ်ရန် Rule များကို ကျင့်သုံးပေးခြင်းမှာ ယင်း၏ အလုပ် ဖြစ်ပါသည်။ ထို့အပြင် Build system သည် Dependency မပြောင်းလဲဘေ ယခင် Build မှ ရလဒ် ရရှိနိုင်သော Target များအတွက် မလိုအပ်ဘဲ Rule များကို runမနေဘဲ ထိရောက်စွာ ဆောင်ရွက်ပေးပါသည်။
make သည် အသုံးအများဆုံး Build system များထဲမှ တစ်ခု ဖြစ်ပြီး UNIX အခြေပြု ကွန်ပျူတာ အများစုတွင် တပ်ဆင်ထားသည်ကို တွေ့ရမည် ဖြစ်သည်။ အားနည်းချက် အချို့ ရှိသော်လည်း ရိုးရှင်းမှ အသင့်အတင့် ပရောဂျက်များအတွက် အလွန် အဆင်ပြေပါသည်။ make ကို runလိုက်သောအခါ လက်ရှိ Directory ရှိ Makefile ဟုခေါ်သော ဖိုင်ကို ဖတ်ရှုပါသည်။ Target များ၊ Dependencies များနှင့် Rules များ အားလုံးကို ထိုဖိုင်ထဲတွင် သတ်မှတ်ထားသည်။ အောက်ပါ နမူနာကို ကြည့်ပါ -
paper.pdf: paper.tex plot-data.png
pdflatex paper.tex
plot-%.png: %.dat plot.py
./plot.py -i $*.dat -o $@
ဤဖိုင်ရှိ ညွှန်ကြားချက် တစ်ခုစီသည် ညာဘက်ခြမ်းကို အသုံးပြု၍ ဘယ်ဘက်ခြမ်းကို မည်သို့ ထုတ်လုပ်မည်ဟူသော Rule ဖြစ်ပါသည်။ သို့မဟုတ် အခြားတစ်မျိုး ပြောရလျှင် ညာဘက်ခြမ်းရှိ အရာများသည် Dependencies များ ဖြစ်ကြပြီး ဘယ်ဘက်ခြမ်းသည် Target ဖြစ်ပါသည်။ ခြားထားသော အောက်ပါ Block သည် ထို Dependency များမှ Target ကို ထုတ်လုပ်ပေးမည့် ပရိုဂရမ် အစဉ်လိုက် ဖြစ်သည်။ make တွင် ပထမဆုံး ညွှန်ကြားချက်သည် Default goal လည်း ဖြစ်ပါသည်။ Argument မပါဘဲ make runပါက ဤ Target ကို Build လုပ်မည် ဖြစ်သည်။ သို့မဟုတ် make plot-data.png ဟု runပါက ထို Target ကို သီးသန့် Build လုပ်မည် ဖြစ်သည်။
Rule ရှိ % သည် “pattern” ဖြစ်ပြီး ဘယ်ဘက်နှင့် ညာဘက်ရှိ တူညီသော String ကို ကိုက်ညီစေပါသည်။ ဥပမာ Target plot-foo.png ကို တောင်းဆိုပါက make သည် Dependency များ ဖြစ်သော foo.dat နှင့် plot.py တို့ကို ရှာဖွေမည် ဖြစ်သည်။ ယခု အလွတ် ဖြစ်နေသော Directory တွင် make runပါက မည်သို့ ဖြစ်မည်ကို ကြည့်ပါ -
$ make
make: *** No rule to make target 'paper.tex', needed by 'paper.pdf'. Stop.
make က paper.pdf ကို Build ရန် paper.tex လိုအပ်ကြောင်းနှင့် ထိုဖိုင် ဖန်တီးရန် Rule မရှိကြောင်း သတိပေးလိုက်ခြင်း ဖြစ်သည်။ ဖိုင်ကို ဖန်တီးကြည့်ကြပါစို့ -
$ touch paper.tex
$ make
make: *** No rule to make target 'plot-data.png', needed by 'paper.pdf'. Stop.
စိတ်ဝင်စားစရာမှာ plot-data.png ကို ဖန်တီးရန် Rule ရှိသော်လည်း ယင်းသည် Pattern rule ဖြစ်နေခြင်း ဖြစ်သည်။ Source file (data.dat) မရှိသေးသောကြောင့် make က ထိုဖိုင်ကို မထုတ်လုပ်နိုင်ကြောင်း ပြောခြင်း ဖြစ်သည်။ ဖိုင်များ အားလုံး ဖန်တီးကြည့်ကြပါစို့ -
$ cat paper.tex
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\includegraphics[scale=0.65]{plot-data.png}
\end{document}
$ cat plot.py
#!/usr/bin/env python
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-i', type=argparse.FileType('r'))
parser.add_argument('-o')
args = parser.parse_args()
data = np.loadtxt(args.i)
plt.plot(data[:, 0], data[:, 1])
plt.savefig(args.o)
$ cat data.dat
1 1
2 2
3 3
4 4
5 8
ယခု make runလိုက်ပါက မည်သို့ ဖြစ်မည်နည်း -
$ make
./plot.py -i data.dat -o plot-data.png
pdflatex paper.tex
... lots of output ...
PDF ဖိုင် ထွက်ရှိလာသည်ကို တွေ့ရမည် ဖြစ်သည်!
make ကို ထပ်မံ runကြည့်ပါက မည်သို့ ဖြစ်မည်နည်း -
$ make
make: 'paper.pdf' is up to date.
ဘာမှ ထပ်မလုပ်တော့ပါ! အဘယ်ကြောင့်နည်း။ အကြောင်းမှာ ထပ်မလုပ်ရန် မလိုအပ်သောကြောင့် ဖြစ်သည်။ ယခင်က Build လုပ်ထားသော Target များသည် မူလ Dependency များနှင့် ယှဉ်လျှင် အပ်ဒိတ် ဖြစ်နေဆဲ ဖြစ်ကြောင်း စစ်ဆေးလိုက်ခြင်း ဖြစ်သည်။ paper.tex ကို ပြင်ဆင်ပြီး make ပြန်runကြည့်ပါက -
$ vim paper.tex
$ make
pdflatex paper.tex
...
make သည် plot.py ကို ပြန်လည် မrunခဲ့သည်ကို သတိပြုပါ၊ အကြောင်းမှာ မလိုအပ်သောကြောင့် ဖြစ်သည်; plot-data.png ၏ Dependency မည်သည့်အရာမျှ မပြောင်းလဲခဲ့ပါ!
Dependency management
ပိုမို ကျယ်ပြန့်သော အဆင့်တွင် သင်၏ Software ပရောဂျက်များသည် အခြား ပရောဂျက်များကို Dependency အဖြစ် မှီခိုနေလေ့ ရှိပါသည်။ တပ်ဆင်ထားသော ပရိုဂရမ်များ (ဥပမာ python)၊ System package များ (ဥပမာ openssl) သို့မဟုတ် သင်၏ ဘာသာစကားရှိ Library များ (ဥပမာ matplotlib) ကို မှီခိုနိုင်ပါသည်။ ယနေ့ခေတ်တွင် Dependency အများစုကို တစ်နေရာတည်း၌ ထားရှိပြီး လွယ်ကူစွာ တပ်ဆင်နိုင်သော repository များမှ ရရှိနိုင်ပါသည်။ Ubuntu system package များအတွက် apt tool မှတဆင့် ဝင်ရောက်သော Ubuntu package repositories၊ Ruby library များအတွက် RubyGems၊ Python library များအတွက် PyPI သို့မဟုတ် Arch Linux အတွက် Arch User Repository တို့ ဖြစ်ကြပါသည်။
ဤ Repository များနှင့် ချိတ်ဆက် ဆောင်ရွက်သည့် နည်းလမ်းများသည် Repository တစ်ခုနှင့်တစ်ခု Tool တစ်ခုနှင့်တစ်ခု ကွဲပြားသောကြောင့် သီးခြား တစ်ခုချင်းစီ၏ အသေးစိတ်ကို သွားမည် မဟုတ်ပါ။ အစား ယင်းတို့ အားလုံး သုံးစွဲကြသော အသုံးများသော ဝေါဟာရများကို သင်ကြားပေးပါမည်။ ပထမဆုံးမှာ versioning (မူကွဲ သတ်မှတ်ခြင်း) ဖြစ်ပါသည်။ အခြား ပရောဂျက်များ မှီခိုသော ပရောဂျက် အများစုသည် Release တိုင်းတွင် version number တစ်ခု ထုတ်ပြန်ကြသည်။ ဥပမာ 8.1.3 သို့မဟုတ် 64.1.20192004။ ကိန်းဂဏန်းများ ဖြစ်လေ့ရှိကြသည်။ Version number များသည် ရည်ရွယ်ချက် အများအပြား ဆောင်ရွက်ပြီး အရေးကြီးဆုံးမှာ Software ဆက်လက် အလုပ်လုပ်စေရန် ဖြစ်သည်။ ဥပမာ ကျွန်ုပ်၏ Library တွင် Function တစ်ခု၏ အမည်ကို ပြောင်းလဲလိုက်သော Release အသစ်တစ်ခု ထုတ်လိုက်သည် ဆိုပါစို့။ အကယ်၍ အခြားသူက ကျွန်ုပ်၏ Library ကို မှီခို၍ Build လုပ်ပါက Function မရှိတော့သဖြင့် Build ပျက်စီးသွားနိုင်ပါသည်။ Versioning သည် သီးခြား Version သို့မဟုတ် Version အပိုင်းအခြားကို သတ်မှတ်ခွင့် ပြုခြင်းဖြင့် ဤပြဿနာကို ဖြေရှင်းပေးပါသည်။
သို့သော် ဤသည်မှာလည်း အပြည့်အဝ မဟုတ်သေးပါ! အကယ်၍ Public API ကို မပြောင်းလဲဘဲ လုံခြုံရေး အပ်ဒိတ်တစ်ခု ထုတ်လိုက်ပြီး မူလ Version အသုံးပြုသူ အားလုံး ချက်ချင်း သုံးသင့်သည် ဆိုပါစို့။ ဤနေရာတွင် Version နံပါတ် အုပ်စုများ ရောက်ရှိလာပါသည်။ နံပါတ်တစ်ခုစီ၏ အဓိပ္ပာယ်သည် ပရောဂျက်အလိုက် ကွဲပြားသော်လည်း အသုံးများသော စံနှုန်းတစ်ခုမှာ semantic versioning ဖြစ်ပါသည်။ Semantic versioning တွင် Version နံပါတ်တိုင်းသည် major.minor.patch ပုံစံ ဖြစ်သည် -
- API ပြောင်းလဲမှု မရှိပါက patch version ကို တိုးပါ။
- နောက်ပြန် ညီညွတ်မှု ရှိသော (backwards-compatible) API အသစ် ထည့်ပါက minor version ကို တိုးပါ။
- နောက်ပြန် ညီညွတ်မှု မရှိသော API ပြောင်းလဲမှု ပြုလုပ်ပါက major version ကို တိုးပါ။
ဤသည်မှာ အကျိုးကျေးဇူးများစွာ ပေးစွမ်းပါသည်။ ယခုအခါ ကျွန်ုပ်၏ ပရောဂျက်သည် သင်၏ ပရောဂျက်ကို မှီခိုပါက၊ Major version တူညီနေသရွှေ့ နောက်ဆုံး Release ကို အသုံးပြုခြင်းသည် ဘေးကင်းသင့်ပါသည်။ ဥပမာ Version 1.3.7 ကို မှီခိုပါက 1.3.8, 1.6.1 သို့မဟုတ် 1.3.0 တို့ဖြင့် Build လုပ်ခြင်းသည် အဆင်ပြေသင့်ပါသည်။ Version 2.2.4 မူ Major version တိုးသွားသဖြင့် အဆင်မပြေနိုင်ပါ။ Python ၏ Version နံပါတ်များတွင်လည်း ဤသည်ကို တွေ့နိုင်ပါသည်။ Python 2 နှင့် Python 3 code များ ရောနှော၍ မရခြင်းမှာ major version bump ဖြစ်ခဲ့သောကြောင့် ဖြစ်သည်။
Dependency စီမံခန့်ခွဲမှု စနစ်များနှင့် အလုပ်လုပ်သည့်အခါ lock files အယူအဆကိုလည်း တွေ့ရနိုင်ပါသည်။ Lock file ဆိုသည်မှာ လက်ရှိ မှီခိုနေသော Dependency တစ်ခုစီ၏ တိကျသော Version ကို စာရင်းပြုလုပ်ထားသည့် ဖိုင်ဖြစ်ပါသည်။ သာမန်အားဖြင့် အပ်ဒိတ် ပရိုဂရမ်ကို runမှသာ Version အသစ်များသို့ အဆင့်မြှင့်မည် ဖြစ်သည်။ မလိုအပ်ဘဲ ပြန်လည် Compile လုပ်ခြင်းကို ရှောင်ရှားရန်၊ ပြန်လည် ထုတ်လုပ်နိုင်သော Build များ ရရှိရန် စသည့် အကြောင်းပြချက်များ ရှိပါသည်။ ဤ Lock ပြုလုပ်ခြင်း၏ ပြင်းထန်သော မူကွဲမှာ vendoring ဖြစ်ပြီး ယင်းသည် Dependency များ၏ Code အားလုံးကို မိမိ ပရောဂျက်ထဲသို့ ကူးယူလိုက်ခြင်း ဖြစ်သည်။ ၎င်းသည် အပြောင်းအလဲများကို အပြည့်အဝ ထိန်းချုပ်နိုင်စေသော်လည်း Maintainer များထံမှ အပ်ဒိတ်များကို ကိုယ်တိုင် ယူဆောင်ရမည် ဖြစ်သည်။
Continuous integration systems
ကြီးမားသော ပရောဂျက်များတွင် အလုပ်လုပ်လာသည်နှင့်အမျှ အပြောင်းအလဲ ပြုလုပ်တိုင်း ထပ်မံ လုပ်ဆောင်ရမည့် အလုပ်များ ရှိလာသည်ကို တွေ့ရမည်။ Documentation မူကွဲအသစ် တင်ခြင်း၊ Compiled version တင်ခြင်း၊ Code များကို PyPI သို့ လွှင့်ခြင်း၊ Test စာအုပ်များ runခြင်း စသည်တို့ ဖြစ်သည်။ GitHub တွင် Pull Request တင်တိုင်း စတိုင် စစ်ဆေးစေချင်သလား။ ဤကဲ့သို့သော လိုအပ်ချက်များအတွက် Continuous Integration ကို လေ့လာရမည် ဖြစ်သည်။
Continuous integration (CI) ဆိုသည်မှာ “Code ပြောင်းလဲတိုင်း runမည့် အရာများ” အတွက် ခြုံငုံ ခေါ်ဆိုသော ဝေါဟာရ ဖြစ်ပြီး Open-source ပရောဂျက်များအတွက် အခမဲ့ ပေးသော CI ကုမ္ပဏီများစွာ ရှိပါသည်။ Travis CI, Azure Pipelines နှင့် GitHub Actions တို့ ဖြစ်ကြသည်။ ယင်းတို့ အားလုံးသည် ပရောဂျက်တွင် ဖိုင်တစ်ခု ထည့်သွင်း၍ ဖြစ်ရပ်များ ဖြစ်ပေါ်သည့်အခါ မည်သို့ ပြုလုပ်ရမည်ကို သတ်မှတ်ပေးရသည်။ အသုံးအများဆုံး Rule မှာ “Code ပို့လိုက်ပါက Test များ runပါ” ဟူ၍ ဖြစ်သည်။ ဖြစ်ရပ် ဖြစ်ပေါ်ပါက Virtual machine ကို စတင်၍ ရလဒ်များကို မှတ်တမ်းတင်ပေးပါသည်။ Test ပျက်စီးပါက အကြောင်းကြားစာ ရရှိရန် ပြုလုပ်ထားနိုင်ပါသည်။
CI စနစ်၏ ဥပမာတစ်ခုအဖြစ် ဤအတန်း၏ ဝဘ်ဆိုက်ကို GitHub Pages ဖြင့် ပြင်ဆင်ထားပါသည်။ Pages သည် master သို့ Push လုပ်တိုင်း Jekyll ကို runပေးသော CI Action ဖြစ်ပါသည်။ ဤသည်မှာ ဝဘ်ဆိုက်ကို အပ်ဒိတ်လုပ်ရန် အလွန် လွယ်ကူစေပါသည်! Local တွင် ပြင်ဆင်သည်၊ Git ဖြင့် Commit လုပ်သည်၊ Push လုပ်သည်။ CI က ကျန်သည်များကို လုပ်ဆောင်ပေးပါသည်။
A brief aside on testing
ကြီးမားသော Software ပရောဂျက် အများစုတွင် “test suite” ပါရှိကြပါသည်။ Testing ၏ အယူအဆအချို့ကို အောက်ပါအတိုင်း တွေ့ရနိုင်ပါသည် -
- Test suite: Test အားလုံး၏ စုစည်းမှု
- Unit test: သီးခြား အင်္ဂါရပ်တစ်ခုကို သီးသန့် စမ်းသပ်သော “micro-test”
- Integration test: အစိတ်အပိုင်း အသီးသီး အတူတကွ အလုပ်လုပ်ပုံကို စမ်းသပ်သော “macro-test”
- Regression test: ယခင်က ဖြစ်ပွားခဲ့သော Bug ထပ်မံ မဖြစ်စေရန် စမ်းသပ်သော Test
- Mocking: သီးခြား အင်္ဂါရပ်များကို သီးသန့် စမ်းသပ်နိုင်ရန် Function, Module များကို အတု ပြုလုပ်ခြင်း (ဥပမာ “mock the network” သို့မဟုတ် “mock the disk”)
Exercises
-
Makefile အများစုတွင်
cleanဟုခေါ်သော Target ပါရှိသည်။ ယင်းသည်cleanဖိုင် ထုတ်ရန် မဟုတ်ဘဲ ပြန်လည် Build လုပ်နိုင်သော ဖိုင်များကို ရှင်းလင်းရန် ဖြစ်သည်။paper.pdf၏Makefileတွင်cleantarget ထည့်သွင်းပါ။ Target ကို phony ပြုလုပ်ရန် လိုပါမည်။git ls-filesကို သုံးနိုင်ပါသည်။ -
Rust ၏ build system ရှိ Dependency Version သတ်မှတ်ချက် နည်းလမ်းများကို လေ့လာပါ။ နည်းလမ်း တစ်ခုစီအတွက် သင့်တော်သော အခြေအနေများကို စဉ်းစားပါ။
-
Git ကိုယ်တိုင်သည် ရိုးရှင်းသော CI စနစ်အဖြစ် အလုပ်လုပ်နိုင်ပါသည်။ Git repo တိုင်းရှိ
.git/hooksတွင် သီးခြား အရေးယူမှုများ ဖြစ်ပေါ်ပါက runမည့် Script ဖိုင်များ ရှိပါသည်။make paper.pdfrunမည့်pre-commithook တစ်ခု ရေးပါ၊makeမအောင်မြင်ပါက Commit ကို ငြင်းပယ်ပါစေ။ -
GitHub Pages ဖြင့် အလိုအလျောက် ထုတ်ဝေသော စာမျက်နှာတစ်ခု ပြင်ဆင်ပါ။ Repo ရှိ Shell ဖိုင်များတွင်
shellcheckrunရန် GitHub Action ထည့်သွင်းပါ။ -
Repo ရှိ
.mdဖိုင်များတွင်proselintသို့မဟုတ်write-goodrunမည့် မိမိပိုင် GitHub action ကို ဖန်တီးပါ။ စာလုံးပေါင်း မှားသော PR ဖြင့် စမ်းသပ်ပါ။
Licensed under CC BY-NC-SA.