使用Docker中的Windows Container構(gòu)建.NET4.x項(xiàng)目

0. 背景

鑒于目前公司的舊項(xiàng)目是.NET4.5開(kāi)發(fā)的,為方便部署,打算使用docker的Windows Container做一個(gè)打包鏡像

目前基于Windows Container的例子太少,所以也確實(shí)踩了不少坑,這里記錄一下

1. 操作系統(tǒng)版本

Windows家庭版是可以安裝Docker,啟動(dòng)并成功運(yùn)行Linux Container。
但如果要切換到Windows Container將會(huì)卡在啟動(dòng)階段,且不會(huì)有任何報(bào)錯(cuò)!

卡在啟動(dòng)階段

由于剛換了筆記本,平時(shí)也沒(méi)注意系統(tǒng)版本,這個(gè)問(wèn)題卡了1天,中間嘗試了各種命令,配置服務(wù),注冊(cè)表都無(wú)效;
最后切換到“Windows專(zhuān)業(yè)版”問(wèn)題解決。

如果遇到卡在啟動(dòng)階段一直無(wú)法成功啟動(dòng)的,不妨換系統(tǒng)版本試一試。

2. Git版本

因?yàn)槟壳暗膚indows sdk鏡像中不包含wingetchoco所以如果需要額外的軟件支持是需要自己打包鏡像的時(shí)候安裝進(jìn)去的。
當(dāng)時(shí)下載了git官網(wǎng)最新的2.38.0,在本地跑的好好的一段腳本,在docker直接卡死無(wú)響應(yīng)。

每當(dāng)執(zhí)行到`git pull`時(shí)就卡死

在網(wǎng)上搜了一圈,偶爾發(fā)現(xiàn)一篇文章,說(shuō)是git版本太低導(dǎo)致命令卡死,但是,我下的是最新版本?。?/strong>,抱著試一試想法,把git版本退回本地版本2.34.1.windows.1,問(wèn)題解決。

另外使用git還需要對(duì)git進(jìn)行一些安全方面的配置,這個(gè)網(wǎng)上可以隨便找到

@rem build.bat
# 設(shè)置git安全策略
RUN git config --global --add safe.directory "*"
RUN git config --system http.sslverify false

3. 設(shè)置nuget緩存

做過(guò)CI的同學(xué)肯定都知道nuget必須是要緩存的,否則每次構(gòu)建都需要完整的下載全部nuget依賴(lài),會(huì)很慢。
.net core可以通過(guò)dotnet restore --packages "..."的方式指定緩存文件夾,但dotnet restore命令在framework的項(xiàng)目中無(wú)法使用,只能通過(guò)nuget.exe的來(lái)還原,他們的設(shè)置方式不一樣。
需要通過(guò)命令設(shè)置全局緩存文件夾:nuget config -set globalPackagesFolder=c:/nuget repositoryPath=c:/nuget,這一步可以放在Dockerfile中執(zhí)行

4. 安裝NodeJs

Web項(xiàng)目構(gòu)建可能需要依賴(lài)NodeJs,但微軟的SDK鏡像中并不包含,需要自己安裝。

缺少NodeJs

安裝方式:
node-v16.17.1-x64.msi

###Dockerfile
COPY ./node-v16.17.1-x64.msi c:/bat/node-v16.17.1-x64.msi
# 安裝nodejs
RUN msiexec /a "C:\bat\node-v16.17.1-x64.msi" /qb TARGETDIR="C:\nodejs"
# 設(shè)置環(huán)境變量
RUN setx path '%path%;C:/nodejs/nodejs'

5. NuGet私倉(cāng)鑒權(quán)

這個(gè)問(wèn)題我還不知道發(fā)生的原因,現(xiàn)象就是:

將nuget賬號(hào)密碼配置在Dockerfile階段會(huì)失效,在編譯時(shí)依然顯示需要賬號(hào)密碼鑒權(quán)
目前解決方案是將配置操作放在編譯階段

### build.bat
nuget source remove -name mynuget
nuget source add -source http://mynuget/nuget-hosted/ -Name mynuget -Username mynuget -Password mynuget

6. NuGet還原失敗1

這是一個(gè)已知的bug,它會(huì)阻止nuget restore的操作,顯示全部項(xiàng)目已還原,但實(shí)際上并沒(méi)有 具體看這里
解決方法是在 *.csproj 文件中刪除類(lèi)似下面這樣的代碼

 <!-- web.config -->
 <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
    <PropertyGroup>
      <ErrorText>這臺(tái)計(jì)算機(jī)上缺少此項(xiàng)目引用的 NuGet 程序包。使用“NuGet 程序包還原”可下載這些程序包。有關(guān)更多信息,請(qǐng)參見(jiàn) http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。</ErrorText>
    </PropertyGroup>
    <Error Condition="!Exists('..\packages\Microsoft.TypeScript.MSBuild.4.6.4\build\Microsoft.TypeScript.MSBuild.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.TypeScript.MSBuild.4.6.4\build\Microsoft.TypeScript.MSBuild.props'))" />
    <Error Condition="!Exists('..\packages\Microsoft.TypeScript.MSBuild.4.6.4\build\Microsoft.TypeScript.MSBuild.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.TypeScript.MSBuild.4.6.4\build\Microsoft.TypeScript.MSBuild.targets'))" />
    <Error Condition="!Exists('..\packages\EntityFramework.6.4.4\build\EntityFramework.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\EntityFramework.6.4.4\build\EntityFramework.props'))" />
    <Error Condition="!Exists('..\packages\EntityFramework.6.4.4\build\EntityFramework.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\EntityFramework.6.4.4\build\EntityFramework.targets'))" />
  </Target>

7. NuGet還原失敗2

如果項(xiàng)目引用關(guān)系比較復(fù)雜,msbuild通過(guò)單個(gè)csproj文件分析項(xiàng)目關(guān)系還原依賴(lài)會(huì)出現(xiàn)問(wèn)題;
nuget restore "%csproj%" -PackagesDirectory "packages" -NonInteractive -Recursive
之后可能會(huì)出現(xiàn)編譯失敗的情況
解決方案:通過(guò)sln項(xiàng)目文件還原依賴(lài)沒(méi)事
nuget restore "%sln%" -PackagesDirectory "packages" -NonInteractive

8. Access is denied.

這個(gè)問(wèn)題我也沒(méi)搞懂是為什么,目前觀(guān)察到的現(xiàn)象就是:

編譯項(xiàng)目A,A引用了B,使用msbuild -r "A項(xiàng)目目錄" -property:OutDir="輸出目錄";編譯A項(xiàng)目,同時(shí)會(huì)將B項(xiàng)目也輸出到輸出目錄,此時(shí)在docker中無(wú)法操作B項(xiàng)目輸出的文件,刪除,移動(dòng),重命名等操作均返回Access is denied 。

Access is denied

重新運(yùn)行一個(gè)容器可以正常操作;
起初以為是有進(jìn)程鎖文件,但使用tasklist沒(méi)有觀(guān)察到異常進(jìn)程;
再次懷疑是權(quán)限不足,使用takeown提升權(quán)限,顯示成功,但依然無(wú)法操作。

這種情況在每次編譯一個(gè)項(xiàng)目時(shí)并不會(huì)有什么太大的問(wèn)題,但如果一次編譯多個(gè)項(xiàng)目,且項(xiàng)目之間有引用關(guān)系時(shí),編譯第二個(gè)項(xiàng)目就會(huì)出現(xiàn)因?yàn)槲募o(wú)權(quán)限操作而編譯失敗的情況。

目前的解決方案是,編譯項(xiàng)目到各自項(xiàng)目的bin目錄,最后再xcopy到發(fā)布目錄

@rem build.bat
msbuild -r "%~1" -verbosity:n -property:WarningLevel=0;OutDir=.\bin\;Configuration=%MODE%
if exist "%~p1bin\_PublishedWebsites\%~n1" (
    call :Exec "xcopy /seyfrhq "%~p1bin\_PublishedWebsites\%~n1" C:\publish\%~n1"
) else (
    if exist "%~p1bin\_PublishedWebsites" (
        call :Exec "xcopy /seyfrhq "%~p1bin\_PublishedWebsites" C:\publish\%~n1"
    ) else (
        call :Exec "xcopy /seyfrhq "%~p1bin" C:\publish\%~n1"
    )
)

9. 多個(gè)分支并發(fā)構(gòu)建

由于實(shí)際場(chǎng)景中項(xiàng)目較大,考慮到每次都從git拉取整個(gè)項(xiàng)目時(shí)間較長(zhǎng),所以選擇掛載項(xiàng)目目錄到docker容器中;
但這就出現(xiàn)了多個(gè)Runner同時(shí)構(gòu)建不同分支時(shí),必然會(huì)互相影響

解決方案是:將.git文件夾復(fù)制到docker容器內(nèi)部,然后執(zhí)行還原+切換分支,再進(jìn)行構(gòu)建,這樣即使多進(jìn)程同時(shí)構(gòu)建也不會(huì)互相影響。

@rem build.bat
cd "c:\src"
mkdir C:\source\.git & xcopy /seyfrhqk "c:\src\.git" "c:\source\.git"
mkdir C:\source\packages & xcopy /seyfrhqk "c:\src\packages" "c:\source\packages
cd "c:\source"
git clean -dfx -q -e web.config -e packages & git reset --hard & git reset HEAD . & git checkout .
git checkout %BRANCH% && git pull origin %BRANCH% -q

10. build.bat

echo off

if exist "c:\bat\DEBUG" if "%1"=="" (
    echo "DEBUG MODE"
    cmd
    exit /b
)

call :Exec "rd /s /q c:\publish"
cd "c:\src"

if "%MODE%"=="" set MODE=DEBUG
if "%BRANCH%"=="" set BRANCH=master


echo "=================================================================="
echo "==== bat version 2022-10-07 ======================================"
echo "====  2022-10-07 14:54:48  ======================================"
echo "============ dir ================================================="
echo "BRANCH     = %BRANCH%"
echo "MODE       = %MODE%"
echo "SLN        = %SLN%"
echo "CSPROJ     = %CSPROJ%"
echo "=================================================================="

call :Exec "git fetch --all" 

call :Exec "mkdir C:\source\.git & xcopy /seyfrhqk "c:\src\.git" "c:\source\.git""
@REM call :Exec "mkdir C:\source\packages & xcopy /seyfrhqk "c:\src\packages" "c:\source\packages""
call :Exec "cd "c:\source""
call :Exec "git clean -dfx -q -e web.config -e packages & git reset --hard & git reset HEAD . & git checkout ."
call :Exec "git checkout %BRANCH% && git pull origin %BRANCH% -q" 

for /r c:\src %%i in (web.config) do (
    if exist %%i (
        call :CopyToSource %%i
    )
)

@REM 如果有私倉(cāng),這里進(jìn)行配置
@REM call :Exec "nuget source list |findstr mynuget && nuget source remove -name mynuget"
@REM call :Exec "nuget source add -source http://mynuget/ -Name mynuget -Username mynuget -Password mynuget"
if "%SLN%"=="" (
    call :Exec "nuget restore "c:\source" -PackagesDirectory "packages" -NonInteractive"
) else (
    call :Exec "nuget restore "c:\source\%SLN%" -PackagesDirectory "packages" -NonInteractive"
)

call :ResolvePath %CSPROJ% CSPROJ
set entry=%CSPROJ%

:loop
for /f "tokens=1* delims=;" %%a in ("%entry%") do (
    echo %%a
    call :ResolvePath %%a entry
    call :Build %entry%
    set entry=%%b
)
if defined entry goto :loop

rem === Functions ===
exit /b

:ResolvePath
    set %2=%~dpfn1
    exit /b

:Exec
    echo "******************************************************************"
    echo "> %~1"
    call %~1
    exit /b

:Build
    mkdir "C:\publish\%~n1"
    @REM call :Exec "nuget restore "%~1" -PackagesDirectory "packages" -NonInteractive -Recursive"
    call :Exec "msbuild -r "%~1" -verbosity:n -property:WarningLevel=0;OutDir=.\bin\;Configuration=%MODE%"
    call :Exec "taskkill /f /im VBCSCompiler.exe /t"
    
    if exist "%~p1bin\_PublishedWebsites\%~n1" (
        call :Exec "xcopy /seyfrhq "%~p1bin\_PublishedWebsites\%~n1" C:\publish\%~n1"
    ) else (
        if exist "%~p1bin\_PublishedWebsites" (
            call :Exec "xcopy /seyfrhq "%~p1bin\_PublishedWebsites" C:\publish\%~n1"
        ) else (
            call :Exec "xcopy /seyfrhq "%~p1bin" C:\publish\%~n1"
        )
    )
    
:CopyToSource
    set src=%~1
    set dest=%src:c:\src=c:\source%
    echo F|xcopy /seyfrhqk "%src%" "%dest%"
    exit /b

11. Dockerfile

FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2019

WORKDIR "C:/"
COPY ./git "C:/git"
COPY ./build.bat c:/bat/build.bat
COPY ./node-v16.17.1-x64.msi c:/bat/node-v16.17.1-x64.msi
# COPY ["./NETFramework", "C:/Program Files (x86)/Reference Assemblies/Microsoft/Framework/.NETFramework"]
RUN Write-Output 2022-10-07 >/imgver

# 創(chuàng)建基礎(chǔ)文件夾
RUN "md c:/source/;md c:/publish/;md c:/nuget/"
# 安裝nodejs
RUN msiexec /a "C:\bat\node-v16.17.1-x64.msi" /qb TARGETDIR="C:\nodejs"
# 設(shè)置環(huán)境變量
RUN setx path '%path%;C:/git/bin;C:/nodejs/nodejs'
# 設(shè)置nuget緩存文件夾
RUN nuget config -set globalPackagesFolder=c:/nuget repositoryPath=c:/nuget
ENV NUGET_HTTP_CACHE_PATH="c:/nuget"

# 設(shè)置git安全策略
RUN git config --global --add safe.directory "*"
RUN git config --system http.sslverify false

# 聲明鏡像變量
ENV MODE=DEBUG CSPROJ= BRANCH=main SLN=
# 啟動(dòng)腳本
ENTRYPOINT ["/bat/build.bat"]
ONBUILD RUN echo 'base image build time : 2022-10-07 14:53:22'

編譯鏡像
cls && docker rmi -f build && docker build . -t build

構(gòu)建.NET項(xiàng)目

docker run -it --rm ^
    -v "D:/codes/dotnet/CSM":"c:/src" ^
    -v "D:/publish":"c:/publish" ^
    -v "D:/apps/.nuget":"c:/nuget" ^
    -e CSPROJ="WebUI/WebUI.csproj;Api/Api.csproj" ^
    -e MODE="DEBUG" ^
    -e BRANCH="release/v1.0" ^
    build
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容