PyInstaller
คือเครื่องมือที่ช่วยการแปลงโปรแกรมที่เขียนด้วยไพทอนเป็น execute binary file ที่สามารถนำไปรันได้โดยที่เครื่องคอมพิวเตอร์ปลายทางไม่ต้องติดตั้งไพทอน สำหรับ PyInstaller เป็น cross-platform สามารถใช้งานได้บนวินโดส์ แมค และลีนุกซ์ สนับสนุนไพทอนรุ่น 2.7 และ ไพทอน รุ่น 3.3 ถึง 3.6 จุดมุ่งหมายของ PyInstaller คือต้องการช่วยผู้ใช้ในการแปลงโปรแกรมไพทอน ที่ใช้โมดูลไลบรารีภายนอกเช่น Matplotlib, DJango, wxPython, PyQt เป็นต้น ให้สามารถทำได้ง่ายสะดวก
ติดตั้ง PyInstaller
ติดตั้งง่ายๆด้วยคำสั่ง pip ใน command prompt
pip install pyinstaller
ใช้งาน PyInstaller
การใช้งานสามารถใช้งานผ่าน command line ได้ แต่สำหรับโปรแกรมที่เรียกใช้โมดูลไลบรารีข้างนอกและต้องขนข้อมูล (data) ที่โมดูลไลบรารีนั้นๆต้องการใช้ ผมแนะนำให้ใช้ไฟล์สคริปท์ (Spec file) มาช่วยจะดีกว่า ปรับแต่งได้มากกว่า ตัว spec file จริงๆก็คือไฟล์สคริปท์ของไพทอนนั่นเอง กรณีที่ต้องใช้ Spec file อีกกรณีหนึ่งคือต้องการขนรันไทม์ไลบรารีเช่น .dll หรือ .so ไปแบบแมนวล กรณีที่ผมเจอคือผมใช้ PySide2 ที่รุ่นทางการจริงๆยังไม่ออกมา แต่ hook file ก็มีมาให้แล้วพร้อมกับ PyInstaller รุ่นใหม่ 3.3 แต่ผมใช้งานแล้วยังไม่สำเร็จ ดังนั้นจึงต้องใช้ Spec file นี้เป็นตัวช่วยในการขนรันไทม์ไลบรารีไป ส่วนเรื่อง hook file คืออะไรค่อยว่าอีกที
กรณีศึกษาด้วย Surveyor Pocket Tools บนวินโดส์
โปรแกรม Surveyor Pocket Tools พัฒนาด้วยไพทอน ปัจจุบันใช้ไพทอน รุ่น 3.6 ใช้โมดูลไลบรารีข้างนอกคือ openpyxl, pyproj, geographiclib, gmplot, simplekml, pyshp และที่ขาดไม่ได้คือ PySide2 ซึ่งสำหรับ openpyxl และ pyproj จะมีการขนข้อมูลไปด้วย ส่วน PySide2 ผมจะขนไฟล์ dll ที่ต้องการด้วยมือล้วนๆ
Spec file ของ Surveyor Pocket Tools
มาดูไฟล์สคริปนี้ ผมตั้งชื่อว่า “setup.spec”
# -*- mode: python -*- import sys import PySide2 import os block_cipher = None dirname = os.path.dirname(PySide2.__file__) print("dirname=", dirname) pyside2_resources_path = os.path.join(dirname, 'resources', '') print("resources path=", pyside2_resources_path) pyside2_resources = [(pyside2_resources_path + '*', '.'), (dirname + '/qtwebengineprocess.exe', '.')] added_files = [ ( 'markers/*', 'markers/' ), ( 'geoidgrids/tgm2017.gtx', 'pyproj/data/' ), ( 'geoidgrids/egm96_15.gtx', 'pyproj/data/' ), ( 'geoidgrids/egm08_25.gtx', 'pyproj/data/' ), ( 'database/*', 'database/'), ( 'example data/*', 'example data/'), ( 'qt.conf', '.'), ( 'settings.xml', '.'), ( 'crs.xml', '.')] a = Analysis(['main.py'], pathex=['D:\\sourcecodes\\python\\surveyor pocket tools'], binaries=None, datas=added_files + pyside2_resources, hiddenimports=[], hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, exclude_binaries=True, name='surveyor pocket tools', icon='Land Surveying-64.ico', debug=False, strip=False, upx=False, console=False ) coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, name='setup')
ลองมาดูโค้ดกัน เริ่มจาก import PySide2 เข้ามาเพื่อจะตรวจสอบว่า PySide2 ที่เราใช้งานเป็น 32 บิตหรือ 64 บิต เพื่อจะได้ขนไฟล์ qtwebengineprocess.exe ที่ผมใช้แสดงผล Google Maps ตอนปักหมุด จากนั้นเก็บไดเรคทอรีของ PySide2 ไปเก็บไว้ในตัวแปร pyside2_resources_path ส่วนที่อยู่ของไฟล์เก็บที่ pyside2_resources
# -*- mode: python -*- import sys import PySide2 import os block_cipher = None dirname = os.path.dirname(PySide2.__file__) print("dirname=", dirname) pyside2_resources_path = os.path.join(dirname, 'resources', '') print("resources path=", pyside2_resources_path) pyside2_resources = [(pyside2_resources_path + '*', '.'), (dirname + '/qtwebengineprocess.exe', '.')]
ต่อไปจะขนไฟล์ที่โปรแกรม Surveyor Pocket Tools ต้องการใช้ ให้ใส่ไว้ที่ตัวแปร added_files โครงสร้างเป็น tuple เหมือนกัน และขนไฟล์ชื่อ qt.conf ที่ PySide2 ต้องการไปด้วย
added_files = [ ( 'markers/*', 'markers/' ), ( 'geoidgrids/tgm2017.gtx', 'pyproj/data/' ), ( 'geoidgrids/egm96_15.gtx', 'pyproj/data/' ), ( 'geoidgrids/egm08_25.gtx', 'pyproj/data/' ), ( 'database/*', 'database/'), ( 'example data/*', 'example data/'), ( 'qt.conf', '.'), ( 'settings.xml', '.'), ( 'crs.xml', '.')]
มาดูไดเรคทอรีที่โปรแกรมต้องการดังนี้
ต่อไปมาดูโค้ดส่วนที่สำคัญมาก ‘main.py’ คือไฟล์สคริปท์หลักของโปรแกรม Surveyor Pocket Tools ต่อไปคือ pathex เป็นไดเรคทอรีของไฟล์ไพทอนสคริปท์ และ datas ที่ผมจัดการรวม added_files และ pyside2_resources เข้าด้วยกัน สุดท้าย hookspath คือไดเรคทอรีที่เก็บไฟล์ hook ไว้ สำหรับไฟล์ hook นี้ PyInstaller จะอ่านสคริปท์นี้ทีละไฟล์มาตัดสินใจว่าจะขนข้อมูลไดเรคทอรีไหนไป ผมเลือกใช้ดีฟอลท์ครับคือปล่อยว่าง
a = Analysis(['main.py'], pathex=['D:\\sourcecodes\\python\\surveyor pocket tools'], binaries=None, datas=added_files + pyside2_resources, hiddenimports=[], hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher)
ใช้ PyInstaller คอมไพล์ไฟล์ setup.spec
ผมใช้ Minoconda เมื่อจะคอมไพล์ก็เรียก command prompt มาดังนี้ ใช้คำสั่ง cd เข้ามาที่พาทของสคริปท์ของไพทอน ใช้คำสั่ง dir ดูไฟล์ setup.spec
ต่อไปทำการคอมไพล์ ด้วยคำสั่ง
pyinstaller setup.spec
ผลลัพธ์ของ PyInstaller
เมื่อคอมไพล์เสร็จแล้ว ไม่มี error จะได้ไดเรคทอรีมาสองคือ “build” และ “dist” เมื่อเข้าไปดูใน “dist” จะเห็นไดเรคทอรีย่อยช “setup” ชื่อไดเรคทอรีนี้ PyInstaller จะสร้างตามชื่อหน้าของไฟล์ setup.spec เมื่อเข้าดูที่ไดเรคทอรี “setup” จะเห็นไฟล์ต่างๆที่โปรแกรมต้องการ
ผมลองดับเบิ้ลคลิกไฟล์ “surveyor pocket tools.exe” ก็สามารถเปิดมาและทำงานได้ตามปกติ ลองดูชื่อไดเรคทอรีจะเห็นสองไดเรคทอรี ที่ได้จากไฟล์ hooks คือ openpyxl และ pyproj ลองเข้าไปดูในไดเรคทอรี จะเห็นข้อมูลที่ pyproj ขนไปใช้ หมายเหตุว่าข้อมูลนี้ pyproj จะนำไปเป็นฐานข้อมูลในการแปลงพิกัดตาม datum และ projection
ทำไฟล์ Setup ด้วย Inno Setup
จากนั้นผมจะ copy ไดเรคทอรีที่อยู่ใน “setup” ไปไว้อีกที่หนึ่ง พื้นที่นี้สำหรับใช้ Inno Setup มาทำไฟล์ติดตั้ง ลองดูไดเรคทอรี
ในไดเรคทอรีนี้ผมจะมีไฟล์ “surveyorpockettools64.iss” เป็นไฟล์สคริปท์ของ Inno Setup เพื่อสร้างไฟล์ติดตั้ง setup สำหรับวินโดส์ 64 บิต
#define MyAppName "Surveyor Pocket Tools" #define MyAppEXE "Surveyor Pocket Tools.exe" #define MyShortAppName "SurveyorPocketTools" #define MyMainRoot "Survey Suite" #define Developer "Prajuab Riabroy" #define Version "1.02" #define Build "641" [Setup] AppName={#MyAppName} AppVerName={#MyAppName} V{#Version} DefaultDirName={pf}\{#MyMainRoot}\{#MyAppName} DefaultGroupName={#MyMainRoot}\{#MyAppName} UseSetupLdr=yes UninstallDisplayIcon={app}\{#MyAppEXE} VersionInfoProductName={#MyAppName} VersionInfoCompany=priabroy VersionInfoCopyright=Copyright 2000-2017 by {#Developer} VersionInfoDescription={#MyAppName} VersionInfoProductVersion={#Version} VersionInfoVersion={#Version} OutputDir=Setup OutputBaseFilename={#MyShortAppName}V{#Version}Build{#Build}Setup64 ;OutputDir=TraverseProV250Setup64 ; "ArchitecturesAllowed=x64" specifies that Setup cannot run on ; anything but x64. ArchitecturesAllowed=x64 ; "ArchitecturesInstallIn64BitMode=x64" requests that the install be ; done in "64-bit mode" on x64, meaning it should use the native ; 64-bit Program Files directory and the 64-bit view of the registry. ;ArchitecturesInstallIn64BitMode=x64 ArchitecturesInstallIn64BitMode=x64 AppPublisher={#Developer} AppPublisherURL=https://www.surveyorpockettools.org AppVersion={#Version}.{#Build} LicenseFile = eula.txt ChangesEnvironment=yes SolidCompression=yes Compression=lzma2/ultra64 LZMAUseSeparateProcess=yes LZMADictionarySize=1048576 LZMANumFastBytes=273 [Files] Source: "{#MyAppName}.exe"; DestDir: "{app}" Source: "qtwebengineprocess.exe"; DestDir: "{app}" Source: "base_library.zip"; DestDir: "{app}" ; Source: "qt5_plugins\*"; DestDir: "{app}\qt5_plugins\"; Flags: ignoreversion recursesubdirs Source: "certifi\*"; DestDir: "{app}\certifi\"; Flags: ignoreversion recursesubdirs Source: "PySide2\*"; DestDir: "{app}\PySide2\"; Flags: ignoreversion recursesubdirs Source: "database\*"; DestDir: "{userappdata}\{#MyAppName}\database\"; Source: "markers\*"; DestDir: "{userappdata}\{#MyAppName}\markers\"; Source: "example data\*"; DestDir:"{userappdata}\{#MyAppName}\example data\"; Source: "pyproj\data\*"; DestDir: "{userappdata}\pyproj\data\"; Source: "pyproj\*.pyd"; DestDir: "{app}\pyproj\"; Source: "lxml\*"; DestDir: "{app}\lxml\"; Source: "shiboken2\*"; DestDir: "{app}\shiboken2\"; Source: "*.pak"; DestDir: "{app}"; Source: "*.dll"; DestDir: "{app}" Source: "*.pyd"; DestDir: "{app}" Source: "*.xml"; DestDir: "{userappdata}\{#MyAppName}\"; Source: "qt.conf"; DestDir: "{app}" Source: "icudtl.dat"; DestDir: "{app}" [Icons] ;create icon at start menu group Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExe}" ;create icon at desktop Name: "{commondesktop}\{#MyAppName}"; FileName:"{app}\{#MyAppExe}" [Registry] ; Start "Software\My Company\My Program" keys under HKEY_CURRENT_USER ; and HKEY_LOCAL_MACHINE. The flags tell it to always delete the ; "My Program" keys upon uninstall, and delete the "My Company" keys ; if there is nothing left in them. Root: HKCU; Subkey: "Software\{#MyMainRoot}"; Flags: uninsdeletekeyifempty Root: HKCU; Subkey: "Software\{#MyMainRoot}\{#MyAppName}"; Flags: uninsdeletekey Root: HKLM; Subkey: "Software\{#MyMainRoot}"; Flags: uninsdeletekeyifempty Root: HKLM; Subkey: "Software\{#MyMainRoot}\{#MyAppName}"; Flags: uninsdeletekey Root: HKLM; Subkey: "Software\{#MyMainRoot}\{#MyAppName}\Settings"; ValueType: string; ValueName: "InstalledPath"; ValueData: "{app}" Root: HKLM; Subkey: "Software\{#MyMainRoot}\{#MyAppName}\Settings"; ValueType: string; ValueName: "DevelopedBy"; ValueData: "{#Developer}" Root: HKLM; Subkey: "Software\{#MyMainRoot}\{#MyAppName}\Settings"; ValueType: string; ValueName: "ApplicationName"; ValueData: "{#MyAppName}" ;Root: HKCU; Subkey: "Environment"; ValueType:string; ValueName:"PROJ_LIB"; ValueData:"{userappdata}\{#MyAppName}\geoidgrids\" ; Flags: preservestringtype ;
ถ้าเป็นไฟล์สำหรับ Surveyor Pocket Tools รุ่น 32 บิตเพียงใส่คอมเมนต์หน้า ;ArchitecturesInstallIn64BitMode=x64 ก็พอ สำหรับรายละเอียดสคริปท์ของ Inno Setup ผมจะไม่กล่าวถึงรายละเอียดในที่นี้ผู้อ่านที่สนใจสามารถศึกษาได้ครับ จากนั้นก็ใช้ Inno Setup ทำการ build ก็จะได้ไฟล์ Exe เดี่ยวๆ ที่สามารถ zip ไปให้ผู้ใช้ได้ download ต่อไป พบกันตอนหน้าครับ
ไฟล์ที่รวมในการคมไพล์ถูกเข้ารหัสด้วยหรือเปล่าครับ?
หรือว่าเก็บไว้ใน folder ตามโครงสร้างเดิมและเปิดอ่านได้?
เฉพาะไฟล๋โค้ดของเรา py จะถูกแปลงเป็น pyd ปลอดภัยในระดัยหนึ่ง แต่ในความเป็นจริงสามารถ reverse engineering ได้ครับ