#Requires -Version 5.1 # MSIX 빌드 파이프라인. # 1) .NET App publish (framework-dependent) # 2) C++ Shell DLL 빌드 # 3) 패키지 Layout 디렉토리 구성 # 4) makeappx pack # 5) (선택) signtool sign [CmdletBinding()] param( [string]$Configuration = 'Release', [string]$Platform = 'x64', [switch]$Sign, [string]$PfxPath, [securestring]$PfxPassword, [string]$CertThumbprint, [switch]$SelfContained ) $ErrorActionPreference = 'Stop' $repoRoot = Split-Path -Parent $PSScriptRoot $packagingDir = $PSScriptRoot $layoutDir = Join-Path $packagingDir 'Layout' $distDir = Join-Path $packagingDir 'dist' $manifestPath = Join-Path $packagingDir 'Package.appxmanifest' $assetsSrc = Join-Path $packagingDir 'Assets' $appProj = Join-Path $repoRoot 'src\Everything2Everything.App\Everything2Everything.App.csproj' $shellProj = Join-Path $repoRoot 'src\Everything2Everything.Shell\Everything2Everything.Shell.vcxproj' function Import-EnvFile { param([string]$Path) if (Test-Path $Path) { Get-Content $Path | Where-Object { $_ -match '^\s*([^#=\s]+)\s*=\s*(.*)$' } | ForEach-Object { $key = $matches[1].Trim() $val = $matches[2].Trim().Trim('"').Trim("'") if (-not [string]::IsNullOrEmpty($key) -and -not [Environment]::GetEnvironmentVariable($key)) { [Environment]::SetEnvironmentVariable($key, $val, 'Process') } } } } Import-EnvFile (Join-Path $repoRoot '.env') Import-EnvFile (Join-Path $packagingDir '.env') # 환경변수 기반 코드 사이닝 옵션 자동 보정 if (-not $CertThumbprint -and $env:CODE_SIGN_THUMBPRINT) { $CertThumbprint = $env:CODE_SIGN_THUMBPRINT } if (-not $PfxPath -and $env:CODE_SIGN_PFX_PATH) { $PfxPath = if ([System.IO.Path]::IsPathRooted($env:CODE_SIGN_PFX_PATH)) { $env:CODE_SIGN_PFX_PATH } else { Join-Path $repoRoot $env:CODE_SIGN_PFX_PATH } } if (-not $PfxPassword -and $env:CODE_SIGN_PFX_PASSWORD) { $PfxPassword = ConvertTo-SecureString -String $env:CODE_SIGN_PFX_PASSWORD -AsPlainText -Force } # 기본 DevCert 감지 및 자동 서명 활성화 $devCertPfx = Join-Path $packagingDir 'Everything2Everything-DevCert.pfx' if (-not $PfxPath -and -not $CertThumbprint -and (Test-Path $devCertPfx)) { $PfxPath = $devCertPfx if (-not $PfxPassword) { $PfxPassword = ConvertTo-SecureString -String 'Everything2EverythingDev' -AsPlainText -Force } } if (-not $PSBoundParameters.ContainsKey('Sign')) { if ($PfxPath -or $CertThumbprint) { $Sign = $true } } function Find-WindowsSdkTool { param([string]$ToolName) $sdkRoots = @( "${env:ProgramFiles(x86)}\Windows Kits\10\bin", "$env:ProgramFiles\Windows Kits\10\bin" ) | Where-Object { Test-Path $_ } foreach ($root in $sdkRoots) { $versions = Get-ChildItem -Path $root -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | Sort-Object Name -Descending foreach ($v in $versions) { $candidate = Join-Path $v.FullName "x64\$ToolName" if (Test-Path $candidate) { return [string]$candidate } } } return $null } $makeappx = Find-WindowsSdkTool 'makeappx.exe' if (-not $makeappx) { throw 'makeappx.exe를 찾지 못했습니다. Windows 10 SDK가 필요합니다.' } Write-Host "makeappx: $makeappx" if ($Sign) { $signtool = Find-WindowsSdkTool 'signtool.exe' if (-not $signtool) { throw 'signtool.exe를 찾지 못했습니다.' } Write-Host "signtool: $signtool" } $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" if (-not (Test-Path $vswhere)) { throw 'vswhere.exe를 찾지 못했습니다.' } $msbuild = (& $vswhere -latest -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1) if (-not $msbuild) { throw 'MSBuild를 찾지 못했습니다.' } Write-Host "msbuild: $msbuild" # ---- 0) 회귀 게이트: 테스트 실패 시 publish/패키징 중단 ---- # 적대적 리뷰의 '최후 방어선' — 회귀가 MSIX/서명까지 새는 것을 막는다. Write-Host '' Write-Host '[0/5] 테스트 게이트 (dotnet test)' $testProj = Join-Path $repoRoot 'src\Everything2Everything.Tests\Everything2Everything.Tests.csproj' & dotnet test $testProj -c $Configuration | Out-Host if ($LASTEXITCODE -ne 0) { throw '테스트 실패 — publish/패키징을 중단합니다. 회귀를 먼저 수정하세요.' } # ---- 1) .NET App publish ---- Write-Host '' Write-Host '[1/5] .NET App publish' $publishOut = Join-Path $repoRoot ('artifacts\publish\app-' + $Platform.ToLower()) if (Test-Path $publishOut) { Remove-Item $publishOut -Recurse -Force } $rid = if ($Platform -eq 'ARM64') { 'win-arm64' } else { 'win-x64' } $selfFlag = if ($SelfContained) { 'true' } else { 'false' } & dotnet publish $appProj -c $Configuration -r $rid --self-contained $selfFlag -o $publishOut | Out-Host if ($LASTEXITCODE -ne 0) { throw 'dotnet publish 실패' } # ---- 2) C++ Shell DLL ---- Write-Host '' Write-Host '[2/5] C++ Shell DLL 빌드' # nuget.exe 자동 다운로드 (없으면) $nuget = Get-Command nuget.exe -ErrorAction SilentlyContinue if (-not $nuget) { $nugetExe = Join-Path $repoRoot 'tools\nuget.exe' if (-not (Test-Path $nugetExe)) { New-Item -ItemType Directory -Path (Split-Path $nugetExe) -Force | Out-Null Write-Host ' nuget.exe 다운로드 중…' Invoke-WebRequest -Uri 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' -OutFile $nugetExe } $nugetCmd = $nugetExe } else { $nugetCmd = $nuget.Source } $packagesDir = Join-Path $repoRoot 'packages' & $nugetCmd restore (Join-Path $repoRoot 'src\Everything2Everything.Shell\packages.config') -PackagesDirectory $packagesDir | Out-Host if ($LASTEXITCODE -ne 0) { throw 'NuGet restore 실패' } & $msbuild $shellProj /p:Configuration=$Configuration /p:Platform=$Platform /m /v:minimal | Out-Host if ($LASTEXITCODE -ne 0) { throw 'Shell build 실패' } $shellDll = Join-Path $repoRoot ("src\Everything2Everything.Shell\$Platform\$Configuration\Everything2Everything.Shell.dll") if (-not (Test-Path $shellDll)) { throw "Shell DLL 산출물 없음: $shellDll" } # ---- 3) Layout 디렉토리 ---- Write-Host '' Write-Host '[3/5] Layout 디렉토리 구성' if (Test-Path $layoutDir) { try { Remove-Item $layoutDir -Recurse -Force -ErrorAction Stop } catch { Start-Sleep -Milliseconds 600 Remove-Item $layoutDir -Recurse -Force -ErrorAction SilentlyContinue } } New-Item -ItemType Directory -Path $layoutDir | Out-Null Copy-Item -Path (Join-Path $publishOut '*') -Destination $layoutDir -Recurse -Force Copy-Item -Path $shellDll -Destination $layoutDir -Force $layoutAssets = Join-Path $layoutDir 'Assets' New-Item -ItemType Directory -Path $layoutAssets -Force | Out-Null Copy-Item -Path (Join-Path $assetsSrc '*') -Destination $layoutAssets -Force Copy-Item -Path $manifestPath -Destination (Join-Path $layoutDir 'AppxManifest.xml') -Force # ---- 4) makeappx pack ---- Write-Host '' Write-Host '[4/5] makeappx pack' if (-not (Test-Path $distDir)) { New-Item -ItemType Directory -Path $distDir | Out-Null } $msixPath = Join-Path $distDir ("Everything2Everything-$($Platform.ToLower()).msix") if (Test-Path $msixPath) { Remove-Item $msixPath -Force } & $makeappx pack /d $layoutDir /p $msixPath /o | Out-Host if ($LASTEXITCODE -ne 0) { throw 'makeappx pack 실패' } Write-Host "✅ MSIX 산출: $msixPath" # ---- 5) sign ---- if ($Sign) { Write-Host '' Write-Host '[5/5] signtool sign (MSIX 디지털 서명)' $timestampUrl = if ($env:TIMESTAMP_SERVER_URL) { $env:TIMESTAMP_SERVER_URL } else { 'http://timestamp.digicert.com' } $plain = $null if ($CertThumbprint) { & $signtool sign /fd SHA256 /sha1 $CertThumbprint /tr $timestampUrl /td SHA256 $msixPath | Out-Host } elseif ($PfxPath) { if (-not $PfxPassword) { $PfxPassword = Read-Host -AsSecureString -Prompt 'PFX 비밀번호' } $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($PfxPassword) try { $plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) & $signtool sign /fd SHA256 /a /f $PfxPath /p $plain /tr $timestampUrl /td SHA256 $msixPath | Out-Host } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } else { throw '서명을 하려면 -CertThumbprint 또는 -PfxPath 가 필요합니다.' } if ($LASTEXITCODE -ne 0) { throw 'signtool sign 실패' } Write-Host '✅ 서명 완료' # 공개 인증서 (.cer) 내보내기 (신뢰 등록용) $cerPath = Join-Path $distDir "Everything2Everything-DevCert.cer" try { if ($PfxPath -and (Test-Path $PfxPath) -and $plain) { $certObj = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($PfxPath, $plain) [System.IO.File]::WriteAllBytes($cerPath, $certObj.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)) Write-Host "✅ 공개 인증서(.cer) 내보냄: $cerPath" -ForegroundColor Green } elseif ($CertThumbprint) { $certObj = Get-Item "Cert:\CurrentUser\My\$CertThumbprint" -ErrorAction SilentlyContinue if (-not $certObj) { $certObj = Get-Item "Cert:\LocalMachine\My\$CertThumbprint" -ErrorAction SilentlyContinue } if ($certObj) { [System.IO.File]::WriteAllBytes($cerPath, $certObj.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)) Write-Host "✅ 공개 인증서(.cer) 내보냄: $cerPath" -ForegroundColor Green } } } catch { Write-Warning "공개 인증서 내보내기 경고: $_" } # 1-클릭 설치기 스크립트 복사 $installCmdSrc = Join-Path $packagingDir 'Install.cmd' if (Test-Path $installCmdSrc) { Copy-Item $installCmdSrc -Destination (Join-Path $distDir 'Install.cmd') -Force Write-Host "✅ 1-클릭 설치기 복사 완료: dist\Install.cmd" -ForegroundColor Green } } else { Write-Host '' Write-Host '[5/5] 서명 건너뜀 (-Sign 미지정). 사이드로드 시 인증서 필요.' } Write-Host '' Write-Host "최종 산출물: $msixPath"