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