戰(zhàn):安裝配置與Makefile避坑指南)
前兩天有個(gè)朋友跑過(guò)來(lái)問(wèn)我你在Windows下寫(xiě)代碼怎么老開(kāi)Cmd敲make這玩意兒不是Linux才有的嗎我說(shuō)不是這句話我太熟了因?yàn)槲易约侯^一回在Windows Cmd里敲make也收到過(guò)一句冷冰冰的“make不是內(nèi)部或外部命令”。Make本身不挑操作系統(tǒng)它只是默認(rèn)出現(xiàn)在絕大多數(shù)Unix環(huán)境里Windows沒(méi)給它預(yù)留位置而已。這篇我就想把這件小事講透怎么讓W(xué)indows Cmd認(rèn)識(shí)make怎么在里面把Makefile寫(xiě)對(duì)、跑順以及我實(shí)際踩過(guò)的那些坑。無(wú)論你是剛接觸Make的新手還是從Linux切回Windows的老手這篇文章應(yīng)該都能幫你少走幾步彎路。1. 為什么我在Windows上折騰make一個(gè)被反復(fù)提起的需求1.1 跨平臺(tái)工程管理的最小代價(jià)先說(shuō)個(gè)很常見(jiàn)的場(chǎng)景項(xiàng)目里要管的不只是編譯還有清理中間文件、拷貝靜態(tài)資源、跑測(cè)試、打壓縮包。在Linux下大家習(xí)慣了用make一條命令把所有這些串起來(lái)到了Windows突然發(fā)現(xiàn)不會(huì)玩了。有人會(huì)說(shuō)用IDE不就行了實(shí)話說(shuō)輕量項(xiàng)目開(kāi)個(gè)IDE確實(shí)舒服但如果項(xiàng)目里有大量命令行工具要串聯(lián)、有復(fù)雜的參數(shù)組合IDE的構(gòu)建配置往往比寫(xiě)Makefile還繞。我接觸過(guò)的不少副業(yè)項(xiàng)目、實(shí)驗(yàn)性小工具更愿意保持一個(gè)純命令行的構(gòu)建入口方便隨時(shí)在任意機(jī)器上復(fù)現(xiàn)。Makefile就是這樣一個(gè)入口不管底下是什么工具鏈只要裝好依賴(lài)一條make命令就能完成同一件事。從Linux/Mac切換過(guò)來(lái)的人最開(kāi)始往往會(huì)瘋狂找“make的Windows版安裝包”裝上之后就以為萬(wàn)事大吉結(jié)果第一次跑就發(fā)現(xiàn)各種詭異問(wèn)題。這正是我寫(xiě)這篇文章的另一個(gè)原因工具裝好只是第一步在Cmd里用好make和它在Unix下的習(xí)慣差異不小尤其是路徑、轉(zhuǎn)義、編碼這幾個(gè)地方坑又深又隱蔽。1.2 Cmd不原生支持make問(wèn)題到底出在哪Windows本身不是沒(méi)有構(gòu)建工具系統(tǒng)自帶的那套編譯環(huán)境、以及各種IDE內(nèi)置的構(gòu)建系統(tǒng)都有自己的規(guī)則。問(wèn)題在于make這套“目標(biāo)—依賴(lài)—配方”的思維和Windows原生命令體系沒(méi)有天然結(jié)合Cmd默認(rèn)不認(rèn)make就像它默認(rèn)不認(rèn)很多Unix工具一樣。關(guān)鍵點(diǎn)在于Cmd只是一個(gè)命令行解釋器它不認(rèn)識(shí)make是“缺了一個(gè)可執(zhí)行文件”而Make真正跑起來(lái)時(shí)它會(huì)把Makefile里的每一條配方命令交給shell去執(zhí)行。在Windows上這個(gè)shell默認(rèn)就是Cmd。所以這里其實(shí)有兩層一層是make這個(gè)程序本身要能被cmd找到另一層是make調(diào)用cmd執(zhí)行命令時(shí)命令要符合cmd的語(yǔ)法。理解這個(gè)分層后面踩坑就能少吃一半。很多人只把注意力放在“怎么裝make”卻忽略了自己在Makefile里寫(xiě)的每一行配方最后都是要交給cmd去跑的。你以為寫(xiě)的是通用構(gòu)建腳本實(shí)際上一半是Make語(yǔ)法一半是Cmd語(yǔ)法兩邊必須協(xié)調(diào)好。2. 讓Cmd認(rèn)識(shí)make兩條安裝路徑與我的選擇2.1 安裝方式一下載可執(zhí)行文件并加入PATH最直接的方式就是拿到make的可執(zhí)行文件放進(jìn)一個(gè)固定目錄然后把這個(gè)目錄加到PATH里。我當(dāng)時(shí)操作的大致過(guò)程是這樣從make的Windows移植版發(fā)布頁(yè)下載對(duì)應(yīng)的壓縮包解壓后能看到一個(gè)make.exe有些版本也叫g(shù)numake.exe。把它改名成make.exe放到一個(gè)專(zhuān)門(mén)放命令行工具的目錄下。我習(xí)慣用C:\tools\make\這種結(jié)構(gòu)方便以后換版本。打開(kāi)系統(tǒng)環(huán)境變量設(shè)置在PATH里新增一行C:\tools\make。重新開(kāi)一個(gè)Cmd窗口輸入make --version驗(yàn)證。這一步里有幾個(gè)容易忽略的地方。首先修改完環(huán)境變量之后已經(jīng)打開(kāi)的那些Cmd窗口不會(huì)自動(dòng)刷新必須開(kāi)新窗口。其次如果機(jī)器上裝過(guò)其他軟件自帶的make變體PATH里的優(yōu)先級(jí)會(huì)決定你調(diào)用的到底是哪一個(gè)。我建議驗(yàn)證的時(shí)候不要只看版本號(hào)再執(zhí)行一下where make能看到完整路徑確認(rèn)不是你意料之外的那個(gè)文件。2.2 安裝方式二用開(kāi)源包管理器一條命令裝完如果不想手動(dòng)下載、解壓、配PATH用包管理器是更省事的辦法。很多Windows用戶會(huì)裝一個(gè)開(kāi)源包管理工具這類(lèi)工具一般都有現(xiàn)成的make包名稱(chēng)可能叫make或gnu-make之類(lèi)的。我兩年前換新機(jī)器時(shí)試過(guò)這種方式一條安裝命令跑完P(guān)ATH也自動(dòng)配好了比手動(dòng)下載省事得多。不過(guò)有兩點(diǎn)要注意第一部分包管理工具安裝時(shí)需要管理員權(quán)限安裝過(guò)程中如果出現(xiàn)權(quán)限相關(guān)的報(bào)錯(cuò)改用管理員身份重開(kāi)一個(gè)終端再裝第二裝完同樣要開(kāi)新窗口確認(rèn)make --version能正常輸出。包管理器的方式對(duì)新手更友好因?yàn)樗岩蕾?lài)關(guān)系也一并處理了。make本身沒(méi)有復(fù)雜依賴(lài)但不同構(gòu)建場(chǎng)景可能還需要其他命令行工具用包管理器統(tǒng)一管起來(lái)后面再補(bǔ)工具時(shí)會(huì)順手很多。2.3 驗(yàn)證是否安裝成功不管用哪種方式裝完之后的驗(yàn)證都是同一個(gè)套路make --version正常情況下會(huì)輸出GNU Make的版本信息。如果系統(tǒng)提示“不是內(nèi)部或外部命令”大概率是PATH沒(méi)配好或者新窗口沒(méi)開(kāi)。如果提示能找到文件但“無(wú)法啟動(dòng)”或者直接閃退可能是下載的版本架構(gòu)不對(duì)換一個(gè)匹配系統(tǒng)的版本再試。驗(yàn)證這一步別跳過(guò)也別只敲一次就滿足。我建議順手測(cè)試一下make能否真正執(zhí)行跨目錄的配方比如新建一個(gè)空目錄里面放一個(gè)只有target:和一行echo ok的Makefile然后make target能打印ok就說(shuō)明基本鏈路是通的。這一步能把“make程序能運(yùn)行”和“make能調(diào)用cmd執(zhí)行命令”兩層都驗(yàn)證到。3. Makefile的基本邏輯先理解構(gòu)建規(guī)則再動(dòng)手寫(xiě)3.1 一條規(guī)則的三要素目標(biāo)、依賴(lài)、配方Makefile的核心是一條條“規(guī)則”每條規(guī)則的三要素可以理解成做菜的流程目標(biāo)是“端出一盤(pán)番茄炒蛋”依賴(lài)是“番茄、雞蛋、油鹽”配方是“洗、切、下鍋翻炒”。目標(biāo): 依賴(lài)1 依賴(lài)2 配方命令當(dāng)你要生成某個(gè)目標(biāo)時(shí)make會(huì)先看依賴(lài)是否存在、是否有更新。如果依賴(lài)不存在就去找能生成依賴(lài)的規(guī)則如果依賴(lài)已經(jīng)存在但比目標(biāo)新就要重新執(zhí)行配方只有目標(biāo)已經(jīng)存在、且所有依賴(lài)都比目標(biāo)舊它才會(huì)告訴你“無(wú)事可做”。這套增量邏輯是make存在的根本價(jià)值。你的代碼改了一行重新執(zhí)行make它不會(huì)把整個(gè)項(xiàng)目推倒重來(lái)而是只重編受影響的部分。在Windows Cmd下這套邏輯同樣成立只是命令語(yǔ)法要稍微注意。3.2 看一個(gè)最簡(jiǎn)單的Makefile先寫(xiě)一個(gè)純粹用來(lái)驗(yàn)證的示例文件名叫Makefile放在某個(gè)空目錄里say-hello: echo hello make保存之后在該目錄下開(kāi)Cmd執(zhí)行make say-hello正常情況下會(huì)輸出hello make。注意配方那一行前面必須是一個(gè)真實(shí)的Tab字符不能是四個(gè)空格這是新手最容易卡住的地方之一。如果Makefile沒(méi)有指定目標(biāo)直接執(zhí)行make它會(huì)默認(rèn)選擇文件里第一條規(guī)則的目標(biāo)。所以很多項(xiàng)目會(huì)把第一條規(guī)則寫(xiě)成all或者default作為整個(gè)項(xiàng)目構(gòu)建的入口。3.3 變量、默認(rèn)目標(biāo)與偽目標(biāo)真實(shí)項(xiàng)目的Makefile很少只用裸規(guī)則一般還會(huì)用到變量。變量的寫(xiě)法是變量名 值引用時(shí)用$(變量名)CC gcc CFLAGS -Wall -O2 all: main.exe main.exe: main.c $(CC) $(CFLAGS) -o main.exe main.c這樣做的好處是將來(lái)?yè)Q編譯器或者調(diào)整參數(shù)只改變量定義就行不用滿文件搜索替換。再就是偽目標(biāo)。像clean這種目標(biāo)它并不生成一個(gè)叫clean的文件只是為了執(zhí)行清理動(dòng)作。如果不聲明成偽目標(biāo)一旦目錄里碰巧出現(xiàn)一個(gè)叫clean的文件make就會(huì)認(rèn)為目標(biāo)已經(jīng)存在且無(wú)需更新后面的清理命令永遠(yuǎn)不執(zhí)行。所以要用.PHONY聲明.PHONY: all clean在Windows下因?yàn)槲募到y(tǒng)和路徑的處理習(xí)慣不同于Unix偽目標(biāo)的使用更要養(yǎng)成習(xí)慣。我見(jiàn)過(guò)有人就是因?yàn)閏lean同名文件問(wèn)題誤以為“make的清理功能在Windows上失效了”其實(shí)只是少了這一行聲明。4. Cmd環(huán)境下make實(shí)戰(zhàn)用一個(gè)模擬項(xiàng)目走一遍完整流程4.1 項(xiàng)目結(jié)構(gòu)和要解決的問(wèn)題以我最近在整的一個(gè)模擬項(xiàng)目X為例一個(gè)小型C語(yǔ)言工程目錄結(jié)構(gòu)很簡(jiǎn)單demo-project/ ├─ src/ │ ├─ main.c │ └─ utils.c ├─ include/ │ └─ utils.h └─ Makefile我想讓make替我完成四件事清理舊產(chǎn)物、創(chuàng)建輸出目錄、編譯程序、清理臨時(shí)文件。這其實(shí)已經(jīng)覆蓋了多數(shù)小項(xiàng)目的日常操作。4.2 從零寫(xiě)Makefile一步步加糖第一版Makefile先把規(guī)則鋪開(kāi)寫(xiě)CC gcc CFLAGS -Iinclude -Wall main.exe: src/main.c src/utils.c include/utils.h $(CC) $(CFLAGS) -o main.exe src/main.c src/utils.c這個(gè)版本能工作但顯得啰嗦。每次新增源文件規(guī)則和命令里的源文件列表要改兩處。所以通常會(huì)用變量和自動(dòng)變量重構(gòu)CC gcc CFLAGS -Iinclude -Wall SRCS src/main.c src/utils.c main.exe: $(SRCS) include/utils.h $(CC) $(CFLAGS) -o $ $(SRCS)這里$代表目標(biāo)文件名也就是main.exe。自動(dòng)變量能少寫(xiě)很多重復(fù)內(nèi)容我后面排查別人的Makefile時(shí)看到一大串硬編碼文件名就知道這多半是從Unix風(fēng)格腳本直接抄過(guò)來(lái)的在Windows下會(huì)埋下路徑隱患。4.3 在Cmd里真正跑起來(lái)然后補(bǔ)上清理和初始化輸出目錄的規(guī)則重點(diǎn)是這一版要能在Cmd下順暢運(yùn)行CC gcc CFLAGS -Iinclude -Wall SRCS src/main.c src/utils.c .PHONY: all clean init all: init main.exe main.exe: $(SRCS) include/utils.h $(CC) $(CFLAGS) -o $ $(SRCS) init: if not exist out mkdir out clean: if exist main.exe del main.exe在Cmd窗口切到項(xiàng)目目錄執(zhí)行make正常情況下會(huì)先創(chuàng)建out目錄然后編譯出main.exe。這里我用if not exist而不是Linux風(fēng)格的mkdir -p因?yàn)閙ake在Windows上會(huì)把配方交給cmd執(zhí)行cmd對(duì)mkdir -p的理解和Unix完全不同甚至?xí)苯訄?bào)語(yǔ)法錯(cuò)誤。再執(zhí)行make clean看看清理是否正常。整個(gè)過(guò)程跑通之后你就可以在Makefile里持續(xù)加規(guī)則了比如make test跑測(cè)試、make package打壓縮包原理完全一樣。5. Cmd下真正容易踩的坑路徑、轉(zhuǎn)義與編碼5.1 反斜杠與正斜杠的拉鋸戰(zhàn)Windows的路徑傳統(tǒng)寫(xiě)法是反斜杠\這也是坑的最大來(lái)源。問(wèn)題在于Makefile語(yǔ)法里行尾的反斜杠是“續(xù)行符”。如果一條配方命令里寫(xiě)了反斜杠結(jié)尾make會(huì)認(rèn)為下一行是這條命令的延續(xù)于是命令被攔腰截?cái)鄨?bào)錯(cuò)方式還特別隱性。我現(xiàn)在的習(xí)慣是Makefile里的路徑盡量統(tǒng)一用正斜杠。Windows Cmd本身對(duì)正斜杠的兼容性足夠好傳給大多數(shù)命令行工具也沒(méi)問(wèn)題。反過(guò)來(lái)如果非要用反斜杠必須寫(xiě)成雙反斜杠\\或者配合作者自己封閉好的寫(xiě)法否則遲早出問(wèn)題。5.2 變量展開(kāi)的兩套語(yǔ)法會(huì)打架這里是最容易讓人精神分裂的地方。Makefile里引用變量用$(變量名)而Cmd里引用環(huán)境變量用%變量名%。一行配方既要被make解析又要被cmd執(zhí)行兩邊都有“變量”這個(gè)概念但語(yǔ)法完全不同。舉個(gè)例子我想在配方里拿到當(dāng)前用戶目錄show: echo $(USERPROFILE)執(zhí)行后你會(huì)發(fā)現(xiàn)輸出的是空或者原樣字母因?yàn)閙ake試圖把它自己的變量USERPROFILE展開(kāi)而make并不認(rèn)識(shí)這個(gè)變量。正確的寫(xiě)法是用$(...)包住cmd的環(huán)境變量或者用$$轉(zhuǎn)義讓make把內(nèi)容保留給cmd去處理show: echo %USERPROFILE%注意這里%USERPROFILE%在make解析時(shí)沒(méi)有特殊含義會(huì)原樣傳給cmdcmd看到之后再展開(kāi)。這是我在Windows下寫(xiě)Makefile時(shí)反復(fù)踩過(guò)的坑沒(méi)有幾次血淚教訓(xùn)真的記不住。5.3 換行續(xù)行的經(jīng)典誤傷Unix風(fēng)格的Makefile里續(xù)行很常見(jiàn)一條命令太長(zhǎng)就在行尾加反斜杠拆成多行。但Windows Cmd的續(xù)行符是^不是反斜杠。如果在配方里寫(xiě)all: echo hello ^ worldmake會(huì)把這兩行當(dāng)兩條獨(dú)立命令傳給cmdcmd看到第一行末尾的^會(huì)等待下一行輸入結(jié)果第二行又被make當(dāng)作新配方行單獨(dú)傳過(guò)去輸出的東西可能完全不是你預(yù)期的。我的規(guī)避辦法很簡(jiǎn)單需要續(xù)行的命令要么合并到同一行用連接要么把邏輯寫(xiě)進(jìn)一個(gè).bat腳本再由make調(diào)用。不要在Makefile里用cmd的^續(xù)行跨shell的語(yǔ)法混用最容易翻車(chē)。5.4 編碼Makefile不要帶BOM中文注釋別亂用Windows下新建文件有時(shí)默認(rèn)帶BOM尤其用某些圖形編輯器保存時(shí)。GNU Make對(duì)帶BOM的Makefile兼容性并不好表現(xiàn)是莫名其妙找不到規(guī)則或者第一個(gè)目標(biāo)名變成類(lèi)似六個(gè)點(diǎn)字符的東西。我建議所有Makefile都保存為不帶BOM的UTF-8編碼。中文注釋也要留意。Cmd的默認(rèn)代碼頁(yè)在中文Windows下通常是GBK如果Makefile是UTF-8編碼且里面寫(xiě)了中文注釋make讀取時(shí)可能把注釋內(nèi)容解析壞進(jìn)而影響后面的規(guī)則識(shí)別。穩(wěn)妥的做法是要么Makefile里只寫(xiě)英文要么確保保存編碼和cmd代碼頁(yè)一致。我自己的習(xí)慣是項(xiàng)目?jī)?nèi)約定文件統(tǒng)一UTF-8但Makefile里的注釋全部用英文省得在不同機(jī)器上跑出亂碼。6. 排查鏈路再現(xiàn)當(dāng)make在Cmd下拉響警報(bào)6.1 三個(gè)高頻報(bào)錯(cuò)的基本分診我遇到的報(bào)錯(cuò)大致可以分三類(lèi)。第一類(lèi)是“不是內(nèi)部或外部命令”說(shuō)明make這個(gè)程序沒(méi)在PATH里或者輸入的命令名本身打錯(cuò)。這屬于環(huán)境問(wèn)題重新配PATH就好。第二類(lèi)是make: Nothing to be done for all這通常不是環(huán)境問(wèn)題而是規(guī)則問(wèn)題要么all沒(méi)有依賴(lài)要么依賴(lài)沒(méi)變化。不一定是寫(xiě)錯(cuò)也可能是增量判斷符合預(yù)期。第三類(lèi)是make: *** No rule to make target clean意思是make找不到對(duì)應(yīng)規(guī)則。可能是文件名寫(xiě)錯(cuò)、目標(biāo)沒(méi)寫(xiě)對(duì)也可能是編碼問(wèn)題導(dǎo)致make解析不到規(guī)則。6.2 一次典型Bug的完整排查過(guò)程我調(diào)試模擬項(xiàng)目X的時(shí)候遇到過(guò)這么個(gè)問(wèn)題第一次執(zhí)行make就報(bào)“系統(tǒng)找不到指定的路徑”但配方里用到的命令單獨(dú)放到Cmd里執(zhí)行是好的。我先確認(rèn)make能跑make --version正常。接著懷疑是配方命令的問(wèn)題于是手動(dòng)執(zhí)行Makefile里的編譯命令發(fā)現(xiàn)完全正常。再仔細(xì)看Makefile發(fā)現(xiàn)問(wèn)題出在路徑寫(xiě)法上源文件路徑用了類(lèi)似src\utils.c的寫(xiě)法而Makefile解析時(shí)行尾的反斜杠被當(dāng)成了續(xù)行符導(dǎo)致命令被拆開(kāi)實(shí)際傳到cmd的路徑是殘缺的。我把路徑統(tǒng)一改成src/utils.c之后問(wèn)題立刻消失。這次排查給我留下一個(gè)習(xí)慣任何奇怪的路徑類(lèi)報(bào)錯(cuò)先去看Makefile里所有反斜杠的上下文尤其是路徑最后一級(jí)是不是帶了反斜杠。大部分Windows下make路徑問(wèn)題根源都在這里。6.3 一張排查順序表照著做能省一晚上以下是我沉淀出來(lái)的一套排查順序遇到make在Cmd下報(bào)錯(cuò)我會(huì)按這個(gè)順序過(guò)一遍癥狀優(yōu)先檢查點(diǎn)常見(jiàn)修復(fù)提示不是內(nèi)部或外部命令PATH、新窗口、make版本架構(gòu)重配PATH開(kāi)新Cmd窗口找不到規(guī)則 / 目標(biāo)不存在目標(biāo)名拼寫(xiě)、Makefile文件名、BOM編碼修正文件名或目標(biāo)另存為無(wú)BOM編碼配方命令報(bào)語(yǔ)法錯(cuò)誤是否用了sh語(yǔ)法、是否存在路徑反斜杠改用cmd語(yǔ)法路徑用正斜杠變量輸出為空或原樣變量是被make還是cmd解析環(huán)境變量用%VAR%make變量用$(VAR)中文亂碼導(dǎo)致規(guī)則錯(cuò)亂Makefile編碼和cmd代碼頁(yè)注釋改英文統(tǒng)一無(wú)BOM UTF-8莫名其妙跳過(guò)某些步驟偽目標(biāo)是否聲明為.PHONY檢查同名文件并聲明.PHONY我自己調(diào)試時(shí)還會(huì)加一個(gè)幾乎從不失手的手段先執(zhí)行make -n讓make只打印將要執(zhí)行的命令而不真正執(zhí)行。然后把打印出來(lái)的每一條命令復(fù)制到Cmd窗口手動(dòng)跑一遍能通過(guò)就說(shuō)明是make解析層面有問(wèn)題不能通過(guò)就說(shuō)明是命令本身的語(yǔ)法問(wèn)題。這個(gè)二分法能讓人快速縮小出錯(cuò)范圍比盯著報(bào)錯(cuò)信息瞎猜高效得多。用到現(xiàn)在我的體感是Windows Cmd不是非得繞開(kāi)make它只是需要你比在Linux下多留幾個(gè)心眼。工具的差異不在工具本身而在你對(duì)底層命令體系的感知。最后分享一個(gè)小習(xí)慣新寫(xiě)或者改動(dòng)Makefile之后先跑一次make -n把即將執(zhí)行的命令逐條過(guò)目再讓make真正去跑。這一步花不了三十秒但它能幫你把一大類(lèi)低級(jí)錯(cuò)誤擋在發(fā)生之前。