콘텐츠로 건너뛰기
Codex

Windows 머신 없이 Windows 빌드 설정 검증하기

TECH NOTE · AUTOMATION OPS

Windows 빌드 실패의 절반은 설정 실수라 macOS에서도 잡을 수 있습니다. 파일 시그니처와 창 옵션을 미리 검증하는 스크립트를 정리했습니다.

빌드는 저쪽에서 하는데 실수는 여기서 난다

데스크톱 앱을 macOS에서 개발하고 Windows 빌드는 다른 기계에서 했다. 그 기계는 늘 켜져 있지 않아서, 빌드를 돌리려면 옮겨 가야 했다.

실패의 절반이 허무했다. 아이콘 파일이 없거나, 번들 타겟을 하나만 적었거나, 창 옵션 하나가 빠져 있었다. 전부 macOS에서 알 수 있는 것들이었다.

빌드 실패에는 두 종류가 있다.

  • 설정이 틀림 — 파일이 없다, 값이 안 맞는다. 어느 OS에서든 확인 가능
  • 컴파일이 안 됨 — 플랫폼 전용 API, 링커 문제. 그 OS에서만 확인 가능

앞쪽만 걸러도 왕복이 확 준다. 그래서 사전검증 스크립트를 하나 만들었다.

npm run windows:preflight

실패를 즉시 멈추게 만든다

스크립트의 뼈대는 단순하다. 조건이 안 맞으면 그 자리에서 죽는다.

function fail(message) {
  console.error(`windows preflight failed: ${message}`);
  process.exit(1);
}

function assert(condition, message) {
  if (!condition) fail(message);
}

function assertFile(path, label) {
  assert(existsSync(path), `missing ${label}: ${path}`);
}

메시지에 경로를 넣는 게 중요하다. “아이콘이 없습니다”보다 “missing windows icon: src-tauri/icons/icon.ico”가 낫다. 사전검증은 몇 주에 한 번 돌리는 물건이라, 실패했을 때 무슨 뜻인지 다시 알아내야 하면 값이 반감된다.

파일이 있는 것과 쓸 수 있는 것은 다르다

처음에는 existsSync만 했다. 그걸로 한 번 뚫렸다.

Git 설정이 꼬여 PNG가 LFS 포인터 텍스트로 체크아웃돼 있었다. 파일은 있고 크기도 0이 아니라 통과했는데, 빌드는 당연히 실패했다.

그래서 내용을 본다.

function assertPngSignature(path, label) {
  const signature = readFileSync(path).subarray(0, 8).toString("hex");
  assert(signature === "89504e470d0a1a0a", `${label} must be a checked-out PNG asset`);
}

PNG는 첫 8바이트가 정해져 있다. 이 여덟 바이트만 보면 진짜 PNG인지 판별된다.

전체를 파싱할 필요가 없다. “있다”가 아니라 “그럴듯하다”까지만 확인해도 실수의 대부분이 걸린다.

창 옵션은 조용히 무시된다

이쪽이 더 고약하다. Tauri 설정에서 창 옵션을 빠뜨리면 오류가 안 난다. 기본값으로 뜬다.

투명 배경으로 띄워야 하는 창인데 transparent가 빠지면, 빌드는 성공하고 앱은 실행되고, 흰 네모가 화면에 떠 있다. 실행해 보기 전에는 모른다.

그래서 필요한 값을 전부 확인한다.

const window = tauriConf.app?.windows?.[0];
assert(window, "tauri.conf.json must define app.windows[0]");
assert(window.transparent === true, "Windows pet window should stay transparent");
assert(window.decorations === false, "Windows pet window should stay frameless");
assert(window.alwaysOnTop === true, "Windows pet window should stay always-on-top");
assert(window.skipTaskbar === true, "Windows pet window should skip taskbar");
assert(window.resizable === false, "Windows pet window should stay fixed-size");
assert(window.width === 320 && window.height === 320, "Windows pet window should be 320x320");

번들 타겟도 같은 이유로 문자열째 비교한다.

assert(
  packageJson.scripts?.["tauri:build:windows"] === "tauri build --bundles nsis,msi",
  "package.json script tauri:build:windows must build both nsis and msi bundles",
);

nsis만 적혀 있어도 빌드는 성공한다. MSI가 안 나올 뿐이다. 배포 직전에 알게 되면 다시 빌드해야 한다.

소스 문자열까지 본다

여기서부터는 취향이 갈린다. 이 스크립트는 소스 파일을 읽어 특정 패턴이 있는지도 확인한다.

const terrainSource = readFileSync(terrainSourcePath, "utf8");
const mainSource = readFileSync(mainSourcePath, "utf8");

보안 정책(CSP) 문자열도 통째로 비교한다.

const requiredCsp = "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost ...";

소스를 문자열로 검사하는 건 보통 나쁜 신호다. 리팩터링 한 번에 깨지고, 실제 동작과 무관할 수 있다.

여기서 쓴 이유는 하나다. 이 값들은 실수로 지워지면 조용히 위험해지는 것들이다. CSP를 느슨하게 바꿔도 테스트는 전부 통과하고 앱도 잘 돈다. 보안이 약해질 뿐이다.

동작으로 확인할 수 없는 계약이면 문자열 검사가 차선이다. 다만 왜 그런지 메시지에 남겨야 한다. 안 그러면 다음 사람이 “이 이상한 검사”를 지운다.

이게 보증하지 않는 것

분명히 해 둘 게 있다. 사전검증이 통과해도 빌드가 성공한다는 뜻이 아니다.

  • Windows 전용 API 컴파일 오류
  • 링커 문제
  • 설치 프로그램 생성 실패
  • 실행 시 동작

전부 못 잡는다. 이 스크립트가 하는 일은 “Windows 기계로 가기 전에 확실히 실패할 것들을 미리 거른다” 하나다.

그래서 문서에 이렇게 적어 뒀다.

Windows artifact 검증은 별도 Windows 머신에서 수행한다.

사전검증은 왕복 횟수를 줄이는 물건이지 대체재가 아니다. 이걸 헷갈리면 “사전검증 통과했으니 배포해도 되겠지”가 된다.

어디에 쓸 만한가

크로스 플랫폼 빌드가 아니어도 같은 구조가 통한다.

상황 미리 검증할 수 있는 것
다른 OS에서 빌드 설정 파일, 에셋 존재와 형식
CI에서만 도는 배포 환경변수 이름, 시크릿 키 목록
프로덕션에만 있는 설정 스키마, 필수 키
앱스토어 제출 아이콘 크기, 권한 설명 문자열

공통점은 확인 지점이 실행 지점보다 훨씬 뒤에 있다는 것이다. 그 사이에 있는 실수를 앞으로 당기는 게 사전검증이다.

Node로 짜면 의존성 없이 node scripts/preflight.mjs 한 줄로 돈다. 파일을 읽고 JSON을 파싱하고 비교하는 게 전부라 도구가 필요 없다.

정리

  • 빌드 실패는 설정 실수플랫폼 컴파일 문제로 갈린다. 앞은 어디서든 잡을 수 있다
  • 파일 존재 확인만으로는 부족하다. 매직 넘버 몇 바이트로 내용까지 본다
  • 설정 누락은 오류가 아니라 기본값으로 나타난다. 필요한 값을 전부 명시적으로 확인한다
  • 동작으로 확인 불가능한 계약(보안 정책 등)은 문자열 검사가 차선이다. 이유를 메시지에 남긴다
  • 실패 메시지에 경로와 기대값을 넣는다. 가끔 돌리는 스크립트다
  • 사전검증 통과 ≠ 빌드 성공. 왕복을 줄이는 물건이지 대체재가 아니다

Windows 기계까지 가서 “아이콘이 없다”는 오류를 보는 게 세 번쯤 반복되면 만들게 된다.

이 블로그 더 보기

이 블로그는 실제 프로젝트를 AI와 함께 굴리면서 남긴 기록입니다.

  • 새 글은 RSS로 받아볼 수 있습니다.
  • 시리즈 전체는 여기에 정리해 두었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다