From 4fd8fabcb8ec26387888a1a23699411ac11fc30b Mon Sep 17 00:00:00 2001 From: EJClaw Date: Sat, 26 Sep 2026 00:24:31 +0900 Subject: [PATCH] =?UTF-8?q?build:=20=ED=8F=AC=ED=84=B0=EB=B8=94=20exe=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B9=A0=EC=A0=B8=20=EC=9E=88=EB=8D=98=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=A8=EB=B3=84=20=EC=BA=A1?= =?UTF-8?q?=EC=B2=98=20=EB=8F=84=EC=9A=B0=EB=AF=B8=EB=A5=BC=20=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit livesub_capture.exe 가 없으면 포터블 빌드는 그대로 성공하되 프로그램별 캡처가 장치 전체 캡처로 조용히 폴백된다. 핵심 기능이 빠진 exe 가 나오는데 빌드는 PASS 로 보이는 조합이라 그대로 릴리스될 뻔했다. - win-ci VM 에 VS 2022 Build Tools(C++)를 깐다. WASAPI 프로세스 루프백 헤더와 WRL 은 Windows SDK + MSVC 에만 있어서 cmake 만으로는 못 만든다. - 검증 순서에 native_capture 단계를 windows_smoke 앞에 넣는다. 뒤에 두면 smoke 와 spec 이 둘 다 "없음"으로 통과해 버린다. - pyinstaller 단계가 거짓 PASS 를 내던 것을 고친다. 앞 회차의 exe 가 실행 중이면 PermissionError 로 빌드가 죽는데, 남아 있는 옛 exe 때문에 Test-Path 검사는 통과했다. 빌드 전에 프로세스를 죽이고 build/dist 를 지우며 PyInstaller 종료코드도 확인한다. - 통과한 exe 를 호스트로 회수한다. serve-host.py 는 32MB 상한이라 exe 를 413 으로 튕겼다. 상한을 올리고 조각으로 받아 디스크에 바로 쓰며, .part 로 받아 완료 후 옮겨 반쪽 파일이 결과물로 남지 않게 한다. - .gitignore 의 네이티브 산출물 경로가 예전 이름(hearo)이라 빌드한 exe 가 무시되지 않던 것을 livesub 로 고친다. --- .gitignore | 7 ++- scripts/win-ci/serve-host.py | 30 ++++++++++--- scripts/win-ci/serve/stage2.ps1 | 78 ++++++++++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 2eb8840..b8fcce8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,12 @@ venv/ .ruff_cache/ # 네이티브 빌드 산출물 +# +# ⚠️ 패키지 이름은 livesub 다. 예전 이름(hearo)이 그대로 남아 있어서 빌드한 +# livesub_capture.exe 가 무시 대상에서 빠져 있었다. native/process_loopback/build/ -src/hearo/resources/bin/*.exe -src/hearo/resources/bin/*.pdb +src/livesub/resources/bin/*.exe +src/livesub/resources/bin/*.pdb # 모델 캐시 / 학습 산출물 / 사용자 데이터 # diff --git a/scripts/win-ci/serve-host.py b/scripts/win-ci/serve-host.py index 5717ff0..eada6c3 100755 --- a/scripts/win-ci/serve-host.py +++ b/scripts/win-ci/serve-host.py @@ -29,7 +29,10 @@ ROOT = "/home/claude/win-ci/serve" LOG = "/home/claude/win-ci/guest.log" RESULTS = "/home/claude/win-ci/results" PORT = 8099 -MAX_ARTIFACT = 32 * 1024 * 1024 +# 로그 전문뿐 아니라 빌드된 포터블 exe(100MB+)도 이 통로로 회수한다. +# 32MB 로 두면 배포할 exe 가 413 으로 튕긴다. +MAX_ARTIFACT = 1024 * 1024 * 1024 +CHUNK = 1024 * 1024 class Handler(http.server.SimpleHTTPRequestHandler): @@ -82,11 +85,28 @@ class Handler(http.server.SimpleHTTPRequestHandler): if n > MAX_ARTIFACT: self.send_error(413, "artifact too large") return - data = self.rfile.read(n) os.makedirs(RESULTS, exist_ok=True) - with open(os.path.join(RESULTS, name), "wb") as f: - f.write(data) - print(f"artifact {name} {len(data)} bytes", flush=True) + # 통째로 read() 하지 않는다. exe 는 100MB 가 넘어서 메모리에 다 올리면 + # 봇 cgroup 의 메모리 상한을 건드린다. 조각으로 받아 바로 디스크에 쓴다. + # + # 그리고 임시 이름으로 받아 끝난 뒤에 옮긴다. 전송 중에 끊기면 반쪽짜리 + # 파일이 정상 결과물처럼 남아 그대로 릴리스에 올라갈 수 있다. + final = os.path.join(RESULTS, name) + tmp = final + ".part" + left = n + with open(tmp, "wb") as f: + while left > 0: + block = self.rfile.read(min(CHUNK, left)) + if not block: + break + f.write(block) + left -= len(block) + if left: + os.remove(tmp) + self.send_error(400, "artifact truncated") + return + os.replace(tmp, final) + print(f"artifact {name} {n} bytes", flush=True) self.send_response(204) self.end_headers() diff --git a/scripts/win-ci/serve/stage2.ps1 b/scripts/win-ci/serve/stage2.ps1 index a474a5e..9aed73f 100644 --- a/scripts/win-ci/serve/stage2.ps1 +++ b/scripts/win-ci/serve/stage2.ps1 @@ -113,6 +113,41 @@ if (Have 'cmake') { else { Log "WARN cmake 설치 실패 - 워크플로의 native 빌드 단계는 건너뛰어진다" } } +# ------------------------------------------------------ VS 2022 Build Tools (C++) +# livesub_capture.exe (native\process_loopback) 는 WASAPI 프로세스 루프백을 쓴다. +# 그 헤더(audioclientactivationparams.h)와 WRL 은 Windows SDK + MSVC 에만 있다. +# cmake 만 깔면 "generator 는 있는데 컴파일러가 없다"로 configure 에서 죽는다. +# +# 왜 빠뜨리면 안 되나: 이 exe 가 없으면 포터블 빌드가 그대로 성공하되 +# 프로그램별 캡처가 장치 전체 캡처로 조용히 폴백된다. 즉 "게임 소리만 받는다"는 +# 핵심 기능이 빠진 채로 exe 가 나온다. 실제로 그 상태의 exe 가 한 번 나왔다. +$VsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +function Get-MsvcRoot { + if (-not (Test-Path $VsWhere)) { return $null } + $p = & $VsWhere -latest -products * ` + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -property installationPath 2>$null + if ($p) { return ($p | Select-Object -First 1) } + return $null +} +$msvc = Get-MsvcRoot +if ($msvc) { + Log "MSVC 이미 있음: $msvc" +} else { + $f = Get-File 'https://aka.ms/vs/17/release/vs_BuildTools.exe' 'vs_BuildTools.exe' + Log "VS 2022 Build Tools 설치 중 (C++ 워크로드, 수 GB - 20분 이상 걸린다)" + # --wait 없이 Start-Process -Wait 만 쓰면 부트스트래퍼가 먼저 반환해 버려 + # 설치가 끝나기 전에 다음 단계로 넘어간다. 둘 다 있어야 한다. + $p = Start-Process $f -Wait -PassThru -ArgumentList ` + '--quiet', '--wait', '--norestart', '--nocache', ` + '--add', 'Microsoft.VisualStudio.Workload.VCTools', '--includeRecommended' + # 3010 = 설치는 됐고 재부팅만 권고. 실패가 아니다. + Log "Build Tools 설치기 종료코드 $($p.ExitCode)" + $msvc = Get-MsvcRoot + if ($msvc) { Log "MSVC 설치 완료: $msvc" } + else { Log "WARN MSVC 설치 실패 - livesub_capture.exe 를 빌드할 수 없다" } +} + # ------------------------------------------------------------------ act_runner $RunnerDir = 'C:\gitea-runner' $RunnerExe = Join-Path $RunnerDir 'act_runner.exe' @@ -240,7 +275,9 @@ function Send-Artifact($name, $path) { $bytes = [IO.File]::ReadAllBytes($path) # 백틱 줄바꿈을 쓰지 않는다. LF 로 저장된 스크립트에서 토크나이저가 # 어긋나 스크립트 전체가 무효가 된 전례가 있다 (NOTES.md 참고). - $req = @{ Uri = "http://10.0.2.2:8099/artifact/$name"; Method = 'Post'; TimeoutSec = 120; Body = $bytes; ContentType = 'application/octet-stream' } + # 타임아웃은 넉넉히. 로그는 수십 KB 지만 포터블 exe 는 100MB 를 넘고 + # slirp(QEMU user networking) 는 그만큼 빠르지 않다. + $req = @{ Uri = "http://10.0.2.2:8099/artifact/$name"; Method = 'Post'; TimeoutSec = 1800; Body = $bytes; ContentType = 'application/octet-stream' } Invoke-RestMethod @req | Out-Null Log " -> 호스트로 $name ($($bytes.Length) bytes)" } catch { @@ -314,10 +351,47 @@ try { $r = [ordered]@{} $r['pytest'] = Step 'pytest' { & $py -m pytest tests -q -rf --tb=long; if ($LASTEXITCODE -ne 0) { throw "exit $LASTEXITCODE" } } $r['ruff'] = Step 'ruff' { & $py -m ruff check src tests scripts; if ($LASTEXITCODE -ne 0) { throw "exit $LASTEXITCODE" } } + + # 캡처 도우미를 먼저 만든다. windows_smoke 가 이 exe 의 존재를 점검하고, + # livesub.spec 도 있을 때만 포터블 exe 안에 담는다. 순서가 바뀌면 둘 다 + # "없음"으로 통과해서 기능이 빠진 exe 가 나온다. + $r['캡처도우미'] = Step 'native_capture' { + & cmake -S native\process_loopback -B native\process_loopback\build -A x64 + if ($LASTEXITCODE -ne 0) { throw "cmake configure exit $LASTEXITCODE" } + & cmake --build native\process_loopback\build --config Release + if ($LASTEXITCODE -ne 0) { throw "cmake build exit $LASTEXITCODE" } + $cap = 'src\livesub\resources\bin\livesub_capture.exe' + if (-not (Test-Path $cap)) { throw "$cap 없음" } + "livesub_capture.exe $((Get-Item $cap).Length) bytes" + } + $r['windows경로'] = Step 'windows_smoke' { & $py packaging\windows_smoke.py; if ($LASTEXITCODE -ne 0) { throw "exit $LASTEXITCODE" } } - $r['exe빌드'] = Step 'pyinstaller' { & $py -m pip install pyinstaller --quiet; & $py -m PyInstaller packaging\livesub.spec --noconfirm; if (-not (Test-Path 'dist\LiveSub.exe')) { throw 'dist\LiveSub.exe 없음' }; "exe 크기 $([math]::Round((Get-Item 'dist\LiveSub.exe').Length/1MB,1)) MB" } + + $r['exe빌드'] = Step 'pyinstaller' { + & $py -m pip install pyinstaller --quiet + # 이전 산출물을 반드시 지우고 시작한다. 남겨두면 두 가지가 겹쳐 터진다. + # 1) 앞 회차의 exe기동 점검이 띄운 LiveSub 가 파일을 잡고 있어 + # PyInstaller 가 EXE 를 덮어쓰지 못하고 PermissionError 로 죽는다. + # 2) 그런데 옛 exe 는 그대로 남아 있으니 Test-Path 는 통과한다. + # 즉 빌드가 실패했는데 PASS 로 보고된다 - 실제로 그렇게 오보가 났다. + Get-Process LiveSub -ErrorAction SilentlyContinue | Stop-Process -Force + Start-Sleep -Seconds 2 + Remove-Item -Recurse -Force build, dist -ErrorAction SilentlyContinue + & $py -m PyInstaller packaging\livesub.spec --noconfirm + if ($LASTEXITCODE -ne 0) { throw "PyInstaller exit $LASTEXITCODE" } + if (-not (Test-Path 'dist\LiveSub.exe')) { throw 'dist\LiveSub.exe 없음' } + "exe 크기 $([math]::Round((Get-Item 'dist\LiveSub.exe').Length/1MB,1)) MB" + } $r['exe기동'] = Step 'exe_smoke' { & $py packaging\windows_smoke.py --exe dist\LiveSub.exe; if ($LASTEXITCODE -ne 0) { throw "exit $LASTEXITCODE" } } + # 빌드와 기동이 둘 다 통과했을 때만 배포용으로 회수한다. 실패한 exe 를 + # 호스트로 올리면 그게 릴리스에 올라갈 위험이 있다. + if ($r['exe빌드'] -and $r['exe기동']) { + Send-Artifact 'LiveSub.exe' (Join-Path $Repo 'dist\LiveSub.exe') + } else { + Log "exe 회수 건너뜀 (빌드=$($r['exe빌드']) 기동=$($r['exe기동']))" + } + # 판정 요약도 파일로 남긴다. 어떤 리비전을 검증한 결과인지 같이 박아야 # 나중에 "언제 통과한 건지" 를 되짚을 수 있다. $rev = (& git rev-parse --short HEAD 2>&1 | Select-Object -First 1)