前言
爲了實現 gitlab CI 自動化構建功能,需要用到 gitlab-runner,在 windows + vc 的場景下需要用到 gitlab-runner for windows。其最終原理是在 powershell 環境下執行 powershell 腳本。
衆所周知,大部分通過命令行編譯VC項目的過程都需要先初始化VC的工作環境(用cmake構建的除外),通常是通過開始菜單中的 “x64 Native Tools Command Prompt for VS 2019” 或 “x86 Native Tools Command Prompt for VS 2019” 快捷方式進入。然而,完全自動化的編譯過程不可能有操作 “開始菜單” 這一說。
gitlab CI + gitlab-runner for windows 的安裝部署不在本文討論範圍。本文只記錄下如何在 powershell 下進入到相應的 VC 編譯環境中(有x86與x64之分)。
探索一
首先,最簡單的方法就是在傳統的命令行下調用微軟已做好的腳本,如:
@call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars32.bat"
或者:
@call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
但是,如果你用的是傳統的.bat腳本,這樣調用是沒問題的,但在 powershell 下沒有 @call 這個指令,你只能採用如下的方式:
cmd /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars32.bat"
這兩種調用方式的差異化在於 @call 方式會保留腳本執行後生成的環境變量,而 cmd /k 的方式則不會。這樣的話,調用了跟沒調用一樣。
所以,這個方法馬上就被否定了。
探索二
爲了解決**“探索一”**中的問題,我們可以將自動化編譯腳本全部寫在一個.bat批處理腳本中,再寫一個 powershell 腳本橋接一下給 gitlab-runner 調用。在.bat批處理腳本中再調用 vcvar32.bat 腳本。如下所示:
rem bat script "mybuild.bat" @call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars32.bat" configure nmake
# powershell script cmd /k "mybuild.bat"
這種方法行得通,但是完全失去了 powershell 腳本的意義。要知道,bat本來就不是設計來 “編程” 的,其醜陋的語法會讓人很難甚。而 powershell 已接近 linux 的 bash 語法了,使用起來很方便,對於需要條件編譯的腳本非常友好。
所以,我們還得再探索更好的方案。
探索三
先整理一下需求。首先,我們需要 powershell 腳本的強大語法功能,又需要執行 vcvars32.bat 來初始化VC編譯環境。
那麼,我們能不能讓調用 vcvars32.bat 時產生的環境變量帶入 powershell 環境呢。答案是肯定的,本方案利用了powershell 的管道能力。
我們先來看看下面一段關鍵代碼:
# 調用批處理(.bat)腳本,並保留生成的環境變量 function Invoke-Environment(){ param( [Parameter(Mandatory=1)][string]$Command # 待執行的腳本文件或命令 ) # 執行批處理腳本,最後調用set指令返回環境變量 foreach($_ in cmd /c " `"$Command`" > null 2>&1 && SET") { if ($_ -match '^([^=]+)=(.*)') { [System.Environment]::SetEnvironmentVariable($matches[1], $matches[2]) } } } Invoke-Environment "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars32.bat"
解釋:上面的代碼就是採用 cmd /c 的方式執行 vcvars32.bat 腳本,其中【> null 2>&1】表示執行過程不讓它輸出任何文本信息,最後再執行 set 指令,打印出所有環境變量,這個打印過程不是打印到顯示器,而是進入了powershell 管理,這樣 powershell 就得到了一串文本。
然後,腳本再採用 foreach 語法枚舉所有的行,對每一行採用正則表達式的方式,解析出所有的環境變量【cmd下的set指令輸出的環境變理是 key=value 的形式】。然後調用 powershell 的 SetEnvironmentVariable 方法設置到 powershell 的環境變量中。
完美!完美嗎???這個方案看似完美,實際上卻隱藏着一個BUG。由於咱們是採用正則表達式解析出各個環境變量的,如果環境變量的其中一個值帶有“=”號,則可能會導致解析混亂。設置錯誤的環境變量值到 powershell 中,可能會有意想不到的BUG。【目前爲止我還沒碰到過,但有所擔心!】
探索四
無意中,發現 VS2019 中竟然自帶 “Developer PowerShell for VS 2019” 快捷方式,啥時候出現的?可能是我之前沒留意!即然有這麼好的東西,那必須拿來使用呀!
通過查看該快捷方式的屬性,知道它最終是執行下面的指令:
C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -noe -c "&{Import-Module """C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"""; Enter-VsDevShell b256ef46}"
很明顯,微軟提供了一個 powershell 組件 “Microsoft.VisualStudio.DevShell.dll” 來實現初始化VC編譯環境,該組件提供了一個方法叫 “Enter-VsDevShell”,從文本的意義來看就是 “進入VS開發環境” 的意思。然而,最後面的一串 “b256ef46” 是什麼鬼?瞎猜可能是產品ID吧,先不管它。接下來我把研究對象放在 “Microsoft.VisualStudio.DevShell.dll” 這個組件上。
爲了方便,我使用 PowerShell ISE進行調試,首先,導入 “Microsoft.VisualStudio.DevShell.dll” 這個模塊。
$vsPath="C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise" Import-Module ("$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll")
再執行 Get-Module,確認模塊已經成功加載:
PS D:\Users\hugo> Get-Module ModuleType Version Name ExportedCommands ---------- ------- ---- ---------------- Script 1.0.0.0 ISE {Get-IseSnippet, Import-IseSnippet, New-IseSnippet} Manifest 3.1.0.0 Microsoft.PowerShell.Management {Add-Computer, Add-Content, Checkpoint-Computer, ... Manifest 3.1.0.0 Microsoft.PowerShell.Utility {Add-Member, Add-Type, Clear-Variable, Compare-Ob... Binary 16.0.0.0 Microsoft.VisualStudio.DevShell {Enter-VsDevShell, Send-VsDevShellTelemetry}
可以看到 “Microsoft.VisualStudio.DevShell.dll” 有兩個方法,分別是 “Enter-VsDevShell” 與 "Send-VsDevShellTelemetry"。
執行 help Enter-VsDevShell,看看有什麼幫助:
PS D:\Users\hugo> help Enter-VsDevShell 名稱 Enter-VsDevShell 語法 Enter-VsDevShell [<CommonParameters>] Enter-VsDevShell [-VsInstanceId] <string> [<CommonParameters>] Enter-VsDevShell [<CommonParameters>] 別名 無 備註 無
幫助信息中有價值的就只有 -VsInstanceId ,估計就是那個 “b256ef46” 了。可我從哪得到這個值呢?不同的產品可能有不同的值吧。總不能把VS的各個版本裝一遍吧?
在 PowerShell ISE中,當加載完 “Microsoft.VisualStudio.DevShell.dll” 模塊後,點擊右側的“命令”輔助工具頁中的“刷新”按鈕。然後在“模塊”下拉列表中可以找到 “Microsoft.VisualStudio.DevShell.dll” 模塊,同時下面列出了兩個方法。點擊“Enter-VsDevShell” 方法,在下面出現了可供調用的參數。其中有一個“VsInstallPath”參數,猜測這個應該是填入VS的安裝目錄,於是填入"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise",然後點擊下方的“運行”按鈕執行,呵呵,果然進入了VC的環境了。對應的命令如下:
PS D:\Users\hugo> Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise" ********************************************************************** ** Visual Studio 2019 Developer PowerShell v16.4.5 ** Copyright (c) 2019 Microsoft Corporation **********************************************************************
執行 ls env: 命令,果然看到了跟執行 vcvars32.bat 後一樣的環境變量。
到這裏,基本有思路了。執行 Enter-VsDevShell 方法時不要用 -VsInstanceId 參數,而是使用 -VsInstallPath,這樣更好理解。
問題來了,以上方法進入的是x86的編譯環境,如果要進入x64位的呢?靠,微軟提供的“開始菜單”中並沒有x64位的 powershell 快捷方式!
從 PowerShell ISE 的命令幫助頁中可以看到有個 DevCmdArguments 參數,猜測應該是通過這個傳入,可是要傳入什麼值呢?
嘗試執行:
Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise" -DevCmdArguments x64
出現如下錯誤信息:
********************************************************************** ** Visual Studio 2019 Developer PowerShell v16.4.5 ** Copyright (c) 2019 Microsoft Corporation ********************************************************************** Enter-VsDevShell : [ERROR:parse_cmd.bat] Invalid command line argument: 'x64'. Argument will be ignored. [ERROR:VsDevCmd.bat] *** VsDevCmd.bat encountered errors. Environment may be incomplete and/or incorrect. *** [ERROR:VsDevCmd.bat] In an uninitialized command prompt, please 'set VSCMD_DEBUG=[value]' and then re-run [ERROR:VsDevCmd.bat] vsdevcmd.bat [args] for additional details. [ERROR:VsDevCmd.bat] Where [value] is: [ERROR:VsDevCmd.bat] 1 : basic debug logging [ERROR:VsDevCmd.bat] 2 : detailed debug logging [ERROR:VsDevCmd.bat] 3 : trace level logging. Redirection of output to a file when using this level is reco mmended. [ERROR:VsDevCmd.bat] Example: set VSCMD_DEBUG=3 [ERROR:VsDevCmd.bat] vsdevcmd.bat > vsdevcmd.trace.txt 2>&1 所在位置 行:1 字符: 1 + Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Vis ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Enter-VsDevShell], Exception + FullyQualifiedErrorId : DevCmdError,Microsoft.VisualStudio.DevShell.Commands.EnterVsDevShellCommand
好吧。搞了半天,原來它最終也是調用 VsDevCmd.bat 這個原始的批處理腳本。我們來理一理。
首先,快捷方式 “x64 Native Tools Command Prompt for VS 2019” 執行下面的指令:
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
打開 vcvars64.bat 這個文件,裏面只有一句:
@call "%~dp0vcvarsall.bat" x64 %*
打開同目錄下的 vcvarsall.bat 文件,看到其中一段代碼:
call "%~dp0..\..\..\Common7\Tools\vsdevcmd.bat" %__VCVARSALL_VSDEVCMD_ARGS%
呵呵,它最終調用的也是 vsdevcmd.bat 這個腳本,而其中的 %__VCVARSALL_VSDEVCMD_ARGS% 就是要傳入的參數。
接下來,我略爲修改一下 vcvarsall.bat 這個腳本,在這一句上面打印出 %__VCVARSALL_VSDEVCMD_ARGS% 變量的值。
echo %__VCVARSALL_VSDEVCMD_ARGS% pause exit call "%~dp0..\..\..\Common7\Tools\vsdevcmd.bat" %__VCVARSALL_VSDEVCMD_ARGS%
保存後,分別執行 “開始菜單” 中的 “x64 Native Tools Command Prompt for VS 2019” 與 “x86 Native Tools Command Prompt for VS 2019”,分別得到以下信息:
-arch=x64 -host_arch=x64
-arch=x86 -host_arch=x86
呵呵,這就是我們想要的參數。【記得把 vcvarsall.bat 這個腳本恢復原樣!】
接下來,我們可以分別通過調用如下指令:
Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise" -DevCmdArguments "-arch=x64 -host_arch=x64" Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise" -DevCmdArguments "-arch=x86 -host_arch=x86"
分別進入x64或x86版本的VC編譯環境了。
最後,發現一個問題,執行完上面的命令後,我們的當前工作目錄改變了。從 PowerShell ISE 的命令幫助頁中可以看到有個 SkipAutomaticLocation 參數,用上它。
Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise" -DevCmdArguments "-arch=x64 -host_arch=x64" -SkipAutomaticLocation
果然不會改變當前工作目錄了。總結起來就是一切靠猜!!!!
探索五
有了 “探索四” 的基礎,我可以規劃 gitlab-runner for windows 的設置了。首先是 config.toml 設置文件。看看我的:
concurrent = 1 check_interval = 0 [session_server] session_timeout = 1800 [[runners]] name = "Visual Studio 2019" output_limit = 40960 url = "https://gitlab.myhost.sz" token = "x2M--9hkxrTXg-GAgWpx" tls-ca-file = "c:\\gitlab-runner\\myhost.sz.crt" executor = "shell" pre_clone_script = "git config --global http.sslVerify false" pre_build_script = "$vsPath=\"c:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\BuildTools\";Import-Module (\"$vsPath\\Common7\\Tools\\Microsoft.VisualStudio.DevShell.dll\")" shell = "powershell" [runners.custom_build_dir] [runners.cache] [runners.cache.s3] [runners.cache.gcs]
其中 pre_build_script 參數就是在執行所有 .gitlab-ci 腳本之前要執行的語句,我們在這裏事先導入 “Microsoft.VisualStudio.DevShell.dll” 模塊。
然後,在 .gitlab-ci 腳本中加入以下語句:
Enter-VsDevShell -VsInstallPath "$vsPath" -SkipAutomaticLocation -DevCmdArguments "-arch=x64 -host_arch=x64"
進入64位版的VC編譯環境。或者加入以下以語:
Enter-VsDevShell -VsInstallPath "$vsPath" -SkipAutomaticLocation -DevCmdArguments "-arch=x86 -host_arch=x86"
進入32位的VC編譯環境。
順便提一下其它設置:由於我搭建的 gitlab 採用了自簽名的證書,啓用了https,所以必需加入參數 “tls-ca-file” 指定自簽名CA證書【注意是CA證書,不是域名證書】。同時還必須加入 pre_clone_script 參數,指示 git 不要驗證域名的合法性。如果不這麼做的話,gitlab-runner 會工作不起來的。
另外,對於無需人爲參與的編譯環境,我採用了 Visual Studio 2019 的 BuildTools 版本而不是Enterprise版本。而操作系統採用了Windows Embedded Standard 7,最小化安裝,作爲虛擬機部署在Exsi上。輕裝上路!關於在Windows Embedded Standard 7上搭建 gilab-runner for windows 的過程,後續有時間我另出一文。