build: 포터블 exe 에서 빠져 있던 프로그램별 캡처 도우미를 복구
Some checks failed
Windows / verify (push) Has been cancelled

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 로 고친다.
This commit is contained in:
EJClaw
2026-09-26 00:24:31 +09:00
parent ca096a69c3
commit 4fd8fabcb8
3 changed files with 106 additions and 9 deletions

7
.gitignore vendored
View File

@@ -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
# 모델 캐시 / 학습 산출물 / 사용자 데이터
#

View File

@@ -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()

View File

@@ -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)