《Makefile 光學教程》之面向 Makefile 編程· 多線程下載[Msys2 Packages]

此教程將計劃以兩部分內容呈現,目標是從零基礎到 GNU make 最本原的原理的掌握,這是第二部分內容,分按不同的工程類型分成多個示范項目來展示。零基本可以先看第一部分:Basic Concepts:
??? Basic Concepts
??? Demo Projects
第二部分計劃提供以下工程示范:
??? Scheme R6RS 語言規(guī)范文檔處理 [LaTeX]
??? Multi threaded Download [Msys2 Packages]
??? C/C++ Project Templates
??? Erlang Project Templates
??? Unit Test
完整《Makefile 光學教程》以及 GNU M4 教程參考開源文檔:https://github.com/Jeangowhy/opendocs/blob/main/Makefile.md
GNU Make 不像 CMake 等現代的自動化構建工具,內部提供了基本的網絡功能。但是,Make 可以通過 shell 與各種工具進行配合作戰(zhàn),一點不影響它發(fā)揮 Makefile 腳本的功能性。另外,Make 插件接口可以很方便接入 C/C++ 編寫的程序,但是通常不需要這樣做。直接通過 shell 配合 Node 或者 Deno 等開發(fā)平臺,或者直接使用的命令行工具,如 curl 和 wget 等等就可以很好地完成網絡訪問功能。
關于 curl 和 wget,它們都是網絡訪問工具,前者依賴 libcurl,后者獨立,都支持文件上傳下載,分別使用 -F 和 --post-file 參數上傳文件。另外,curl 默認輸出到 stdout,wget 則是輸出到文件,可以通過 -o stdout 重定向到標準輸出文件。
curl 通用性較好,并且支持常見的協(xié)議:FTP, FTPS, GOPHER, HTTP, HTTPS, SCP, SFTP, TFTP, TELNET, DICT, LDAP, LDAPS, FILE, POP3, IMAP, SMTP, RTMP and RTSP。wget 支持 HTTP, HTTPS and FTP。https://eternallybored.org/misc/wget/
當然,這些工具限制性較多,適用于簡單的靜態(tài)頁面處理,這些下載工具非常專職,沒有多線程模式,make 提供的多進程構建功能就可以很好地實現多線程下載。另外使用 Node 或者 Deno 平臺,或者是 Python 等等,使用異步 I/O 就可以很方便實現類型多線程下載的功能。但是別忘了,這里是《面向 Makefile 編程》,并且 wget 不會檢查是否已經下載過文件。
另外,wget 實現了遞歸下載功能,很像曾經的 webzip 網站打包軟件,可以下載頁面上匹配條件的鏈接文件。需要使用 -l 和 -np 參數來避免下載整個站點,除非確實是這樣的目的:
使用 make 多進程下載,首先就必需“搞”到文件鏈接地址列表。但是 make 雖然天生就是處理字符串的宏編程工具,但是它是專職于構建系統(tǒng)的,提供的字符串處理函數也是基于文件名的處理。即使是其內置的 patsust 字符串替換函數,也只是按“空格”、“Tab”或“換行”作為分隔的列表進行字符串的替換操作,本身不提供向字符串插入功能字符的功能,如插入換行符這種操作是不能夠的。
因此,在處理 JSON 這樣的數據時需要使用 jq 這樣的外部工具來打配合,或者更自由的方案是編寫 Node 或者 Deno 等等平臺的 JavaScript/TypeScript 腳本擴展。JSON 作為一個通用數據格式規(guī)范,應該領域非常廣泛,個人認為它的價值超過 XML 格式,至少比 XML 節(jié)能多了。https://jqlang.github.io/jq/
jq 是命令行工具,它可以格式化 json 數據,也可以指定 filter 過濾器來查詢 json 中對應的數據。最基本的就是 . 這個過濾器,它表示等值,輸入什么就輸出什么。然后就是各種獲取指定數據的過濾器,這里介紹幾種最基礎最常用的:
1. Object Identifier-Index: .string
2. Object Index: [string]
3. Array Index: [number]
4. Array/String Slice: .[<number>:<number>]?
示范使用 curl 和 jq 處理 Msys2 軟件包 API 接口數據,接口返回 JSON 數據會包含軟件包在 Msys2 數據庫中的精確匹配、模糊匹配到的名字,:?
如果 json 文件已經下載到本地還可以直接使用 more or less 命令配合管道操作符將文件內容傳遞給 jq 命令進行解析,以下命令提供參考,最終輸出結果是 "mingw-w64-pkg-config":
這里給 Msys2 作個簡要介紹,并說明如何從 Cygwin 發(fā)展到 MinGW,再到 Msys2 交叉編譯環(huán)境。
1995 年 Cygnus 工程師 Steve Chamberlain 發(fā)現 Windows 系統(tǒng)使用的 COFF 目標文件,即可執(zhí)行文件格式,與此同時 GNU 的工具鏈已經支持 x86 和 COFF 的目標文件,并提供 C 語言庫 newlib,這是嵌入式系統(tǒng)上的 C 標準庫的實現。他認為既然 GNU 的工具鏈已經能夠編譯生成 x86 指令集的機器碼,并可鏈接生成 COFF 格式的目標文件,而且還提供可移植到任意平臺的 C 標準庫 newlib, 那么理論上只要將 GCC 根據對應目標平臺重新編譯,重定向作為一個交叉編譯器。那么這個 GCC 編譯器可以生成 Windows 平臺下的可執(zhí)行文件。Steve Chamberlain 開發(fā)出原型,將他這個項目命名為 Cygwin。
Cygwin 的編譯和調用方式需要依賴一層 POSIX 到 Windows API 的中間層,比起日漸龐大的 Cygwin, 或許一個最小化且不需要中間層 GNU 工具鏈更能滿足一些開發(fā)的需求, 于是 Colin Peters 在 1998 年創(chuàng)建了一個開源項目并撰寫了最初的版本,將其命名為 mingw32 (Minimalist GNU for W32)。其意思就是 Windows 上的最小化 GNU 工具鏈,Windows 簡稱為 W32。后來為了避免暗示它僅限于生成 32 位二進制文件,就移除名稱中的 32 變成 MinGW。
Msys 2.0 也是為 Windows 系統(tǒng)提供 Unix 類系統(tǒng)編譯環(huán)境的基礎平臺軟件,它是基于現代 Cygwin 和 MinGW,對 MSys 的獨立重寫版本。MSYS2 vs Cygwin,MSYS2 中的 Unix 類工具直接基于 Cygwin,因此兩者存在一些功能重疊。Cygwin 專注于在 Windows 上按原樣構建 Unix 軟件,MSYS2 則專注于構建基于 Windows API 的本地軟件。也就是說,Cygwin 移植更徹底,這就是為何 Cygwin POSIX 到 Windows 的中間層特別巨大。
有了 Msys2 就可以在 Windows 開發(fā) Unix 應用程序,并構建出可以運行在 Windows 系統(tǒng)環(huán)境中的應用程序。Msys2 本身基于 Cygwin 構建,結合了 Arch Linux 的 pacman 依賴管理工具,使用它可以很方便地安裝需要的組件,比如 ARM 嵌入式開發(fā)需要使用 GCC 交叉編譯。
MSYS2 提供一個 Unix 類系統(tǒng)環(huán)境外,還有 shell 命令行界面和軟件庫,使得在 Windows 上安裝、使用、構建和移植軟件更加容易。這意味著 Bash, Autotools, Make, Git, GCC, GDB 等等 GNU 軟件都可以通過 Pacman 軟件包管理工具進行安裝。
比如,安裝 pkg-config 應用就可以執(zhí)行以下命令安裝,這是一個開發(fā)環(huán)境的依賴處理工具,可以用它來檢測依賴庫文件的位置信息,并生成 GCC 或 MSVC 編譯器命令行參數:
Msys2 基礎軟件倉庫有三個:
1. msys2: MSYS2-dependent software
2. mingw64: 64-bit Windows 原生應用程序,使用 mingw-w64 x86_64 編譯工具鏈編譯;
3. mingw32: 32-bit Windows 原生應用程序,使用 mingw-w64 i686 編譯工具鏈編譯;
目前,已經發(fā)展出包括 LLVM 編譯工具鏈的共 7 大軟件倉庫,它們的軟件包命名規(guī)則如下:
為了避免使用長前綴名,可以使用 bash pacboy 腳本替代 pacman 執(zhí)行軟件包安裝,在軟件包名指定一個簡寫后綴即可:
Pacboy 腳本可能需要通過 pacman 安裝,如果不默認沒有提供;
秉承生命就是折騰的原則,這里不使用 pacman 這么好用的軟件包管理工具,因為它確實太好用了,我就想用 Makefile 錘它。
Msys2 雖然提供了一套 API,但是提供的功能太簡單了,只負責查軟件包的名字,至于其依賴還得通過返回的 JSON 數據去對應的 Web 頁面上找。因為,其本身提供的 Pacman 就提供了自動依賴處理功能。
既然決定要用 Makefile 這把錘,那么就用嘗試用 Node.js 給它裝上舒服一點的錘把手:編寫一個模塊腳本處理 Web 頁面的文件鏈接地址列表。
這里使用 Node 進行 JavaScrip/TypesScript 腳本編程需要了解決的一些基本概念:
1. 每個 .js 腳本文件就是一個 Node 模塊;
2. 每個腳本模塊在 Node 加載運行時,會通過模塊加載器傳入以下參數:
3. process 引用當前 Node 進程,可以通過它獲取當前運行環(huán)境信息,包括命令行參數;
4. module 當前模塊的引用,它包含 exports 變量,用于導出模塊中需要導出的符號;
命令行參數保存在 `process.argv` 變量,是字符串列表,首個元素即 0 號索引對應的是 Node 進程文件路徑,其次是當前腳本模塊路徑,后面是其它命令行參數。使用 `length` 屬性可以獲取命令行參數數量,甚至還可以使用 `Object.keys(process.argv).length`。
Node 模塊沒有默認入口函數,將模塊腳本傳遞給 node 命令就執(zhí)行它,如果執(zhí)行取決于模塊代碼邏輯。但是有一個默認導出符號 exports.default,默認導出符號和 exports 其它所有導出符號構成整個模塊的可以供外部調用的接口。使用 require() 方法就可以引用其它腳本模塊,或者在最新版本中,使用 import 引用 ESM 規(guī)范模塊。
Node 模塊實現代碼放到面,現在來實現 Makefile 腳本:
1. 定義了 Trace 調試宏函數,設置 TRACE 變量就可以激活它打印函數調用信息;
2. 定義了 counter 計數器,此函數借用了 shell 環(huán)境中的 $((a+b)) 算術語法;
3. 定義了一個 PACKGE 指定記錄等下載的文件列表,列表使用 file 讀?。?/p>
4. 每個待下載文件將使用靜態(tài)匹配規(guī)則映射為使用 foeach 生成的?pkg1 pkg2 pkg3 ... 等等;
5. 獲取文件列表使用 %.init 規(guī)則,調用 shell 命令執(zhí)行 Node 的 JavaScript 腳本獲得;
counter 計數器將用來映射 PACKAGE 文件列表,每一個行使用前綴名 pkg 加序號表示,映射后的名稱就可以作為規(guī)則中的 Target 命令使用,因為所以文件沒有依賴關系,都是獨立的構建目標。通過 -jN 指定 Makefile 運行的進程數據,即可以實現多進程下載。但有一個問題:如果手動更新列表文件,那么 Makefile 腳本執(zhí)行時就會執(zhí)行初始目標的構建,去調用外部腳本獲取新的列表:?

以下為 Node 腳本模擬擴展,供 Make 調用以獲取 Msys2 軟件倉庫中軟件包以及依賴包下載地址,暫時命名為 msys2pac.js,和 Makefile 腳本中調用一致即可。此腳本將近 200 行,對于《面向 Makefile 編程》來說,有點“奪目”了。這里就作一個簡單的說明:
1. 腳本中設置了一個 help() 函數,在輸入參數不正確時提示使用方法;
2. 腳本中使用了 Fetch API,這是 Node 試用特性,為了消隱警告信息重置了 warning 事件;
3. Prefix ApiInfo PackageInfo 等等都用于說明 Msys2 API 接口返回的 JSON 數據結構引入的類型定義,目標是為啟用?TypeScript LSP 服務智能提示參考;

以上這些輔助性功能就占據腳本將近一半,接下來主要是三個功能函數,用于查詢軟件包歸屬的分類,并分類頁面提供的地址去提供出 Web 頁面的下載地址。因為依賴關系是多層的,腳本中設置了 3 層頁面跳轉。腳本并不一定處理好所有依賴包,目前只處理了常規(guī)的依賴包頁面,還有 Virtual Package,至于會不會有其它特殊的頁面還不清楚,這可能會導致腳本運行報錯,就需要根據具體問題進行處理。