記錄一次在PowerShell下進入VC編譯環境的探索過程

前言

爲了實現 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 的過程,後續有時間我另出一文。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章