# 선행 설치 오프라인 설치기 2026-11-12. EXE 버전의 **선행 설치** 탭에 **오프라인용 설치기 생성**을 추가했다. 선행 설치 목록의 설치 프로그램을 모두 담은 실행 파일 하나를 만들고, 그 파일을 대상 PC에서 실행하면 목록을 위에서부터 감지·설치하며 진행 과정을 HTML 화면으로 보여 준다. 제품 파일을 포함한 EXE 빌드, 서명, 대상 PC 설치 시험은 범위 밖이다. ## 사용 1. 설치 구성에서 버전을 열고 **선행 설치** 탭에서 **오프라인용 설치기 생성**을 누른다. 버전을 읽기만 하므로 잠긴 버전에서도 된다. 2. 저장 위치를 고른다. 기본 이름은 `<제품>-<버전>-Prerequisites.exe`이고 같은 이름의 파일이 있으면 만들지 않는다. 3. 팝업 창이 열려 전체 진행(설치 프로그램 확인 → 설치기에 넣기 → 다시 읽어 확인)과 항목마다의 단계를 보여 준다. 실행 중에는 **취소 요청**만 누를 수 있다. 4. 끝나면 같은 팝업에 경로, 크기, SHA-256, 항목별 출처(지정한 파일·제작기 기본 제공 파일·이번에 다운로드)와 `설치 시험: notRun`, 또는 실패·취소 이유가 표시되고 **닫기**로 닫는다(2026-11-13, 처음의 탭 아래 패널을 사용자 요청으로 팝업으로 바꿈). 5. 대상 PC에서 실행하면 관리자 권한을 요청하고, 항목별 상태(설치 필요·이미 설치됨·해당 없음)를 보여 준 뒤 **설치**로 차례로 설치한다. CLI: `cargo run -p package-builder -- offline-setup --project <.isproj> --version <버전> --output <새 .exe>`. ## 결정 - 사용자 결정(2026-11-12): 설치기 UI는 Rust + Tauri(WebView2)의 HTML 화면으로 만들고 WebView2 Runtime은 대상 PC에 있다고 가정한다. 없으면 설치기가 `MissingWebView2` 안내를 띄우고 종료하며 WebView2를 대신 설치하지 않는다. - 결과물은 단일 파일 설치 결정(4GB 미만)을 따른다. 설치기 원본 `PrerequisiteSetup.exe` 뒤에 설치 프로그램을 붙이는 형식이며, 폴더나 여러 파일로 나누지 않는다. - 오프라인용이므로 `download` 방식 항목도 설치기 안에 넣는다. 설치 프로그램은 지정한 파일 → 제작기의 `SetupPrerequisites` → 다운로드 주소 순으로 찾는다. `bundled` 항목의 파일이 없을 때 다운로드로 대신하지 않는다. - 실행 중인 설치 프로그램은 중단하지 않는다(절반 설치 방지). 중단 요청은 다음 항목 전에 적용하고, 실패한 항목 뒤의 항목은 설치하지 않는다. 재부팅은 안내만 한다. - 대상 PC의 감지는 레지스트리 값만 읽는다. 파일 버전 감지(`fileVersion`)는 설치기를 만들 때 `UnsupportedPrerequisiteDetection`으로 거부한다. - 설치기의 종료 코드는 Windows Installer 관례를 따른다: 0 성공(설치할 항목 없음 포함), 3010 재시작 필요, 1603 실패·시작 불가, 1602 사용자 취소·중단. ## 모델과 실행 - 형식(`package-domain::setup_bundle`): `[설치기 원본][설치 프로그램 1..n][manifest JSON][0 채움][꼬리 32바이트]`. 꼬리는 원본 끝(설치 프로그램 시작), manifest 위치, manifest 길이(각 `u64` LE)와 `ISPRQ001`이다. 파일은 8바이트 경계로 끝나며, 서명된 파일은 PE 보안 디렉터리의 인증서 표 앞까지 읽는다. manifest는 `{format: 1, product: {name, version, manufacturer}, items: [{id, name, architecture, fileName, arguments, detect, rebootCodes, sha256, bytes, offset}]}`이고 알 수 없는 필드를 거부한다. 항목은 원본 바로 뒤부터 빈틈없이 이어져 manifest 앞에서 끝나야 한다(`DamagedSetup`). - 만들기(`package-infra::build_offline_setup`): 입력 검사(`NoPrerequisites`, `InvalidPrerequisites`, `UnsupportedPrerequisiteDetection`) → 출력 검사(`InvalidSetupOutput`, `OutputExists`, `MissingOutputFolder`) → 원본 검사(`MissingSetupStub`, `InvalidSetupStub`: MZ가 아님·서명됨·이미 목록 있음) → 4GB 검사(`SetupTooLarge`) → 설치 프로그램 확보와 SHA-256·크기 확인(`MissingPrerequisiteFile`, `PrerequisiteChanged`, `DownloadFailed`, `DownloadMismatch`) → 출력 폴더의 임시 파일에 원본·설치 프로그램을 스트림 해시하며 쓰기(`InputChanged`) → manifest·꼬리 → 같은 리더로 다시 읽어 manifest와 원본·항목 해시 확인(`ArtifactMismatch`) → `persist_noclobber`. 다운로드는 시스템 임시 폴더에 받고 끝나면 지운다. 진행은 `OfflineSetupProgress {stage, index, name, done, total}`(바이트 수는 10진 문자열)로 알린다. - 다운로드(`download_installer`): `check_download_link`와 같은 WinHTTP 요청을 함수로 나눠 본문까지 받는다. HTTPS 이외로 이동하면 거부하고, `Content-Length`가 기록과 다르거나 받은 크기가 넘치면 즉시 멈추며, 실패·취소 시 받던 파일을 지운다. 기존 파일은 덮어쓰지 않는다. - 설치기(`apps/prerequisite-setup`, 실행 파일 `PrerequisiteSetup.exe`): 매니페스트 `requireAdministrator`, `withGlobalTauri`, 명령 `setup_overview`·`start_install`·`cancel_install`·`finish_setup`, 이벤트 `setup-item`·`setup-finished`·`setup-close-blocked`. 시작할 때 WebView2(`HKLM` 32비트 보기 또는 `HKCU`의 `EdgeUpdate\Clients\{F3017226-…}` `pv`) → 자기 파일의 목록 → Windows 아키텍처(`IsWow64Process2`)를 확인하고 실패하면 메시지 상자 후 1603으로 끝낸다. 작업 폴더(`%TEMP%\PrerequisiteSetup-*`)에 항목마다 `<번호>\<파일 이름>`으로 꺼내고, 공유 읽기만 허용한 핸들로 SHA-256을 다시 확인해 실행이 끝날 때까지 쓰기·삭제를 막는다. `.exe`는 직접, `.msi`는 `System32\msiexec.exe /i`, `.msu`는 `System32\wusa.exe`로 실행하며 실행 인수는 셸 없이 그대로 넘긴다. WebView2 데이터도 작업 폴더에 두고 종료할 때 지운다. 로그는 `%TEMP%\PrerequisiteSetup-.log`. - 데스크톱: Tauri 명령 `build_offline_setup(version)`(저장 대화상자 → `session.busy` → 진행 이벤트 `offline-setup-progress`, 단계·항목·퍼센트가 바뀔 때만 보냄), 취소는 기존 `cancel_build`. 화면 모듈 `offline-setup.ts`(팝업 `renderOfflineSetupDialog`, 전체 진행 `offlineOverall`). - 빌드: `apps/prerequisite-setup`은 컴파일할 때 `ui/dist`를 넣으므로 `npm.cmd run build`(새 `build:setup` 포함)가 cargo보다 먼저다. `Check.ps1`과 `check` 스킬의 순서를 fmt → 프런트엔드 → test → clippy → 실행 파일 빌드로 바꿨다. 관리자 권한 프로그램이라 테스트 빌드를 만들지 않는다(`test = false`). `Publish-Desktop.ps1`은 `PrerequisiteSetup.exe`를 `InstallerStudio.exe` 옆에 복사한다. 제작기는 원본을 실행 파일 옆에서만 찾는다(다른 폴더로 대신 찾지 않음). 개발 빌드는 제작기와 같은 `--target-dir`에 있어야 하므로 VS Code 빌드 작업(`target/vscode`)도 `prerequisite-setup`을 함께 빌드한다(2026-11-13, VS Code에서 띄운 제작기가 `MissingSetupStub`을 낸 문제 수정). ## 확인 결과 - `scripts/Check.ps1` 전체 통과(fmt → npm ci·test·build → cargo test → clippy → 두 실행 파일 빌드). Rust 테스트 122개 통과, 6개 무시. 새 테스트는 도메인 8개(꼬리 왕복·손상 거부, 8바이트 채움, 인증서 표 위치, 항목 배치 검사, 파일 이름, 아키텍처 적용, 버전 비교, 종료 코드 판정), 인프라 단위 8개(추출·잠금·변조·손상·취소, 설치 프로그램 실행 방식, 감지 입력 거부, 다운로드 주소 거부와 기존 파일 보존, 기본 파일 이름), 통합 6개(지정 파일+기본 제공 파일로 만들고 다시 읽어 추출, 기존 출력 보존, 변경·누락 파일, 거부되는 목록, 원본 검사, 취소 시 잔여물 없음). - 무시 테스트 직접 실행: 레지스트리 읽기(이 PC의 `CurrentBuild`·DWORD·없는 키/값, 쓰기 없음), 기본 목록에서 가장 작은 설치 프로그램 실제 다운로드와 해시 일치·틀린 해시 거부, 다운로드 항목을 받아 설치기에 넣기. 모두 통과. - 프런트엔드 테스트 154개 통과(설치기 화면 8개, 오프라인 설치기 팝업 8개 포함). 팝업은 브라우저에서 앱 스타일로 진행·완료·실패 상태를 렌더링해 확인했다(실제 Tauri 창에서 버튼으로 연 시험은 아님). 데스크톱과 설치기 화면의 `tsc`·Vite 빌드 통과. - 실제 프로젝트로 생성: `D:\egbim-setup\egbimExe.isproj` 1.0.0(6개 항목, 모두 기본 제공 파일)을 CLI로 임시 폴더에 만들었다. 243,683,008바이트, SHA-256 `22a9a63e5c8a3d82efb24af843785c98d45bec1683763df9e69971c61e8535a6`, 약 23초. 프로젝트와 버전 파일의 해시는 생성 전후로 같았다. 시험 파일은 지웠다. - 관리자 권한 요구: 원본과 만든 설치기 모두 일반 권한 셸에서 시작하면 "requires elevation"으로 거부되어 매니페스트와 PE가 유효함을 확인했다. 서명 상태는 `NotSigned`. - 설치기 화면: 브라우저에서 Tauri 연결을 흉내 내 대기·진행·재시작 필요·실패 상태를 렌더링해 확인했고, 목록이 창 안에서 스크롤되도록 고쳤다. 실제 WebView2 창은 띄우지 않았다. - 실행하지 않음: 만든 설치기를 관리자 권한으로 실행(실제 설치), 대상 PC·VM 설치 시험, WebView2가 없는 PC, Arm64·x86 Windows, 데스크톱 앱에서 버튼을 눌러 보는 GUI 시험, 배포본(`Publish-Desktop.ps1`)으로 만든 설치기, 서명한 설치기 읽기. 증빙: [시험 결과 JSON](../../src/tests/verification-2026-11-12-prerequisite-offline-setup.json) ## 근거 - [Microsoft Learn: Distribute your app and the WebView2 Runtime](https://learn.microsoft.com/microsoft-edge/webview2/concepts/distribution) - [Microsoft Learn: Standard installer command-line options (msiexec)](https://learn.microsoft.com/windows/win32/msi/standard-installer-command-line-options) - [Microsoft Learn: MsiExec.exe and InstMsi.exe error messages (1602, 1603, 3010)](https://learn.microsoft.com/windows/win32/msi/error-codes) - [Microsoft Learn: PE format — the attribute certificate table](https://learn.microsoft.com/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only) - [Microsoft Learn: IsWow64Process2](https://learn.microsoft.com/windows/win32/api/wow64apiset/nf-wow64apiset-iswow64process2)