feat: add prerequisites, offline prerequisite setup and version tabs
- EXE versions: prerequisite list with the built-in catalog, custom installers, download link checks and install file analysis; entries are added by the user (no automatic adding), one "추가" dialog with 기본 목록 / 직접 추가 tabs - offline prerequisite setup: single exe made from PrerequisiteSetup.exe (Windows task dialog, no prerequisites) plus every installer, hash checked before, while and after appending; optional WebView2 Runtime chosen in the save dialog; replaces an existing file only after the user agrees (CLI --replace) - desktop: progress popup with 생성위치 열기, save dialog fix for verbatim paths, EXE 서명 and 설치파일 생성 as version tabs, start screen 도구 › 서명하기 placeholder - CLAUDE.md: results are overwritten only with the user's consent - test kit and scripts for Windows Sandbox and Hyper-V checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
366c8fe173
commit
4964f2ced4
110 files changed
+13960
-295
No files matched your search
@@ -46,3 +46,11 @@
|
||||
- 루트 행에 전체 설치 경로를 표시하고, 하위 폴더·파일 행은 마우스를 올리면 전체 경로를 보여 준다(프런트엔드 테스트 79개 통과).
|
||||
- 기본 설치 폴더(EXE)도 삭제할 수 있다. 그 안을 대상으로 하는 규칙과 대기 폴더를 지우고 트리에서 숨긴다(다른 루트는 영향 없음). 설치 위치 추가로 다시 넣거나 그 안을 대상으로 하는 항목이 생기면 다시 보인다. 숨김은 화면 상태라 버전을 다시 열면 원래대로 보인다.
|
||||
- 설치 위치 추가 팝업은 기본 설치 폴더와 허용된 Windows 폴더를 모두 나열하고, 이미 트리에 있는 위치는 "추가됨"으로 비활성화한다. **직접 입력…**으로 `[속성]하위\경로`(속성 없으면 기본 설치 폴더 기준)를 받는다. 허용되지 않은 속성은 `UnsupportedTargetRoot`, 이미 있는 위치·폴더는 `DuplicateFolder`로 팝업 안에 표시한다. 프런트엔드 테스트 81개 통과.
|
||||
|
||||
## 후속 변경 (같은 날): 로컬 폴더 불러오기와 끌어다 놓기
|
||||
|
||||
- 폴더 추가 팝업에 **로컬 폴더 불러오기…**를 추가했다. 새 명령 `choose_folders`가 폴더 선택 창을 열고, 고른 폴더마다 같은 이름의 하위 폴더와 `sourceFolder` = 그 폴더, `include` = `**/*`인 규칙을 만든다. 같은 위치의 같은 폴더는 다시 추가하지 않고, 대상에 둘 수 없는 이름(MSIX 루트의 VFS 등)은 `InvalidFolderName`으로 전체를 거부한다.
|
||||
- 탐색기에서 설치파일 트리로 파일·폴더를 끌어다 놓으면 놓은 행의 폴더(파일 행이면 그 파일의 폴더, 행 밖이면 기본 설치 폴더)에 추가한다. 창의 OS 드롭 이벤트에서 Rust가 경로를 세션에 기록하고 새 명령 `take_dropped`(인자 없음)가 한 번 소비한다. 화면은 포인터 위치만 쓴다. 설치파일 트리가 열려 있지 않거나 팝업이 떠 있으면 무시한다.
|
||||
- 소스 규칙은 파일 추가와 같다: 연결된 소스 폴더 밖이면 `OutsideSource`, 연결된 소스가 없으면 항목들의 공통 상위 폴더(드라이브 루트 바로 아래의 폴더 하나면 그 폴더 자체)를 첫 소스로 연결한다.
|
||||
- 확인: Rust 테스트 통과(`locate_items`의 파일·폴더 분리, 파일 대화상자의 폴더 거부, `link_folder`), 프런트엔드 테스트 84개 통과, 데스크톱 빌드 통과. 실제 끌어다 놓기와 폴더 선택 창 GUI 시험은 수행하지 않았다.
|
||||
- 끌어다 놓기와 로컬 폴더 불러오기는 바로 적용하지 않는다. 후보 규칙을 `preview_files`로 읽어 **추가할 파일 검토** 팝업에 트리로 보여 주고, 확장자별·검색어별(파일 이름 기준, 대소문자 무시) 일괄 제외와 파일별 제외를 거쳐 **적용**해야 버전에 넣는다. 제외는 폴더 규칙의 제외 패턴(`**/*.ext`, `**/*검색어*`, 개별 경로를 문자 그대로)과 이름 규칙에서의 이름 삭제로 옮기며, 남는 파일이 없는 규칙은 넣지 않는다. 같은 폴더·같은 이름은 다시 넣지 않는다. 취소하면 아무것도 바뀌지 않는다(처음 연결한 소스 폴더만 남는다). 프런트엔드 테스트 88개 통과; GUI 시험은 수행하지 않았다.
|
||||
@@ -0,0 +1,52 @@
|
||||
# EXE 버전의 선행 설치 프로그램
|
||||
|
||||
2026-11-12. EXE 버전에 제품보다 먼저 설치할 프로그램 목록(`prerequisites`)을 추가하고, 설치 구성에 **선행 설치** 탭을 만들었다. 제작기가 제공하는 기본 목록에서 고르거나 이 PC의 설치 프로그램 파일을 직접 등록한다. 화면·모델·입력 검사까지이며, 대상 PC에서의 감지·다운로드·설치와 EXE 빌드는 범위 밖이다.
|
||||
|
||||
## 사용
|
||||
|
||||
1. 설치 구성에서 버전을 열고 **선행 설치** 탭을 연다.
|
||||
2. **기본 목록에서 추가**를 눌러 필요한 런타임을 고른다. 처음에는 설치 중 다운로드 방식으로 추가된다.
|
||||
3. 인터넷이 없는 PC에도 설치해야 하면 방식을 **설치 파일에 포함**으로 바꾸고 **파일 지정**으로 같은 설치 프로그램 파일을 고른다.
|
||||
4. 기본 목록 항목은 대화상자 아래 **추가 방식**에서 **설치 파일에 포함**을 고르면 제작기의 `SetupPrerequisites` 폴더에 있는 설치 프로그램을 그대로 쓴다. 이 폴더에 파일이 있는 항목에는 **오프라인** 표시가 붙는다.
|
||||
5. 기본 목록에 없는 프로그램은 **개별 설치 추가**로 `.exe`·`.msi`·`.msu`를 골라 등록하고, 화면 없이 설치하는 실행 인수를 적는다.
|
||||
|
||||
## 결정
|
||||
|
||||
- 지원 대상은 Windows 10 이상이다. InstallShield 2016의 선행 설치 정의 297개 중 Windows 10에서 의미 있는 항목만 추려 기본 목록 16개로 시작한다.
|
||||
- 기본은 설치 중 다운로드이고, 제작자가 설치 프로그램을 설치 파일에 미리 넣을 수 있다.
|
||||
- 오프라인 설치를 위해 기본 목록의 설치 프로그램을 리소스 폴더 `SetupPrerequisites`에 보관하고, 제작기 배포본에도 실행 파일 옆 `SetupPrerequisites` 폴더로 함께 배포한다. 파일은 Git에 넣지 않고 `scripts/Get-Prerequisites.ps1`로 목록의 주소에서 받아 SHA-256을 확인한다.
|
||||
- 버튼은 **기본 목록에서 추가**, **개별 설치 추가** 순서다.
|
||||
|
||||
## 모델과 실행
|
||||
|
||||
- 버전 파일: `prerequisites = [{id, name, architecture, delivery, url, file, sha256, bytes, arguments, detect, rebootCodes}]`. 필드가 없는 기존 `.isver`는 빈 목록으로 연다. `schemaVersion`은 그대로다.
|
||||
- `architecture = x64 | x86 | any`, `delivery = download | bundled`. `bytes`는 10진 문자열, `sha256`은 소문자 64자.
|
||||
- `file = {source, path}`는 파일 규칙처럼 소스 alias와 그 안의 `/` 경로다. 절대 경로는 로컬 설정의 소스 연결에만 남는다.
|
||||
- `detect = {kind, view, path, name, minimum}`. `registryValue`는 `view`(32/64)의 `HKLM\` 또는 `HKCU\` 키에 값이 있고 `minimum`이 있으면 그 이상일 때, `fileVersion`은 파일 버전이 `minimum` 이상일 때 설치된 것으로 본다. 버전은 숫자 단위로 비교하고 앞의 `v`는 무시한다. `detect`가 없으면 항상 실행한다.
|
||||
- 기본 목록: `package-domain`의 `prerequisites.json`(Visual C++ 2010 SP1·2012 Update 4·2013·2015-2022, .NET 8·9·10 Desktop Runtime, .NET Framework 4.8·4.8.1). 고른 항목은 버전 파일에 복사되므로 이후 목록이 바뀌어도 저장된 버전은 변하지 않는다. 주소는 버전이 고정된 주소를 쓴다.
|
||||
- 입력 검사(도메인): `DuplicatePrerequisiteId`, `InvalidPrerequisiteName`, `UnsupportedPrerequisiteArchitecture`, `UnsupportedPrerequisiteDelivery`, `InvalidPrerequisiteUrl`(HTTPS, `.exe`·`.msi`·`.msu`), `MissingPrerequisiteFile`, `InvalidPrerequisiteFile`, `MissingPrerequisiteHash`, `InvalidPrerequisiteArguments`, `InvalidPrerequisiteDetection`, `InvalidPrerequisiteRebootCodes`.
|
||||
- 입력 검사(인프라): 포함 방식의 파일이 없으면 `MissingPrerequisiteFile`, 기록된 SHA-256·크기와 다르면 `PrerequisiteChanged`. `file`이 없는 포함 항목은 `SetupPrerequisites/<id>/<주소의 파일 이름>`을 쓴다. 다운로드 방식은 이 PC에서 아무것도 확인하지 않는다.
|
||||
- 리소스 폴더: 제작기는 실행 파일 폴더부터 위로 세 단계까지 `SetupPrerequisites`를 찾는다. 개발 빌드(`src/target/debug`)는 `src/SetupPrerequisites`, 배포본은 실행 파일 옆 폴더를 쓴다. `prerequisite_catalog` 명령은 기본 목록과 함께 이 폴더에 파일이 있는 ID(`builtin`)를 돌려준다. 내용 해시는 입력 검사 때 확인한다.
|
||||
- 스크립트: `Get-Prerequisites.ps1`(받기, 이미 있는 일치 파일은 건너뜀, 다른 내용의 파일이 있으면 멈춤, 알 수 없는 ID 거부), `Publish-Desktop.ps1 -Output <새 폴더>`(release 빌드, 설치 프로그램 16개의 해시를 확인한 뒤 실행 파일과 함께 복사, 기존 폴더 거부).
|
||||
- 화면: 다운로드 주소가 있는 항목에 SHA-256이 다른 파일을 지정하면 `PrerequisiteHashMismatch`, 기본 목록에 없는 ID는 `UnknownPrerequisite`, 리소스 폴더에 없는 항목을 포함 방식으로 추가하면 `MissingBuiltinPrerequisite`.
|
||||
- Tauri 명령: `prerequisite_catalog`(기본 목록), `choose_prerequisite_file`(파일 대화상자로 고른 파일의 소스 내 경로·SHA-256·크기). CLI 명령은 추가하지 않았다.
|
||||
- 범위 밖: 대상 PC에서의 감지·다운로드·실행·재부팅 처리, 다운로드 주소의 생존 확인, 항목 순서 변경, 감지 조건 편집 화면, 패치 버전에서의 선행 설치 제약, VBA 7.1처럼 공개 주소가 없는 프로그램의 기본 목록 제공.
|
||||
|
||||
## 확인 결과
|
||||
|
||||
- Rust 테스트 87개 통과(무시 1개: SDK 통합). 기본 목록 16개가 검증을 통과하는지, 주소·해시·파일·감지 조건 거부, 필드 없는 기존 버전 파일 열기와 왕복 저장, 알 수 없는 필드 거부, 포함 파일의 변경·누락 탐지, 리소스 폴더의 파일 위치·해시 확인과 이름이 아닌 ID·주소 거부를 검사한다.
|
||||
- 프런트엔드 테스트 118개 통과; TypeScript/Vite 빌드와 `cargo build -p msix-studio --features custom-protocol` 통과. 목록 복사, 파일 등록, 해시 불일치 거부, 리소스 폴더에 없는 항목의 포함 추가 거부와 오프라인 표시, 버튼 순서, 그리드·대화상자 렌더링과 이스케이프, 잠금·MSIX 상태를 검사한다.
|
||||
- 기본 목록의 설치 프로그램 18개(목록 16개와 VS 2026 계열 Visual C++ 2개)를 2026-11-12에 Microsoft 주소에서 내려받아 디지털 서명이 유효함을 확인하고 SHA-256과 크기를 기록했다. .NET 6개는 Microsoft 공개 SHA-512와도 일치했다.
|
||||
- 감지 조건의 레지스트리 키는 개발 PC(Windows 11)의 32비트 보기에 존재함을 확인했다. 설치되지 않은 항목(Visual C++ 2012 x86 등)의 키는 같은 규칙으로 추정했다.
|
||||
- `Get-Prerequisites.ps1`: 리소스 폴더 16개 모두 건너뜀, 빈 폴더에 `vc2012-x86`을 실제로 받아 해시 확인, 변조한 파일과 알 수 없는 ID 거부를 확인했다.
|
||||
- `Publish-Desktop.ps1`: release 빌드 후 실행 파일과 설치 프로그램 16개를 복사했고, 같은 출력 폴더로 다시 실행하면 거부됨을 확인했다. 시험 출력은 지웠다.
|
||||
- 배포본 실행 파일이 옆의 `SetupPrerequisites`를 찾는지는 앱을 띄워 확인하지 않았다.
|
||||
- 실제 앱에서 탭을 조작하는 GUI 시험, 설치 프로그램 실행, 대상 PC 설치 시험은 수행하지 않았다.
|
||||
|
||||
증빙: [시험 결과 JSON](../../src/tests/verification-2026-11-12-exe-prerequisites.json)
|
||||
|
||||
## 근거
|
||||
|
||||
- [Microsoft Learn: Latest supported Visual C++ Redistributable downloads](https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist)
|
||||
- [.NET release metadata](https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json)
|
||||
- [Microsoft Learn: Determine which .NET Framework versions are installed](https://learn.microsoft.com/dotnet/framework/install/how-to-determine-which-versions-are-installed)
|
||||
@@ -0,0 +1,51 @@
|
||||
# 선행 설치 오프라인 설치기
|
||||
|
||||
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-<UNIX 초>.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)
|
||||
@@ -0,0 +1,27 @@
|
||||
# EXE 서명·설치파일 생성 탭과 서명 도구 자리
|
||||
|
||||
2026-11-13. EXE 프로젝트의 사이드바에서 **서명**과 **검증 · 빌드**를 빼고, 버전을 연 **설치 구성**의 탭 끝에 **서명**과 **설치파일 생성**을 더했다. EXE 서명과 EXE 빌드는 여전히 구현하지 않았고, MSIX 프로젝트의 화면은 바뀌지 않는다.
|
||||
|
||||
## 결정
|
||||
|
||||
- 사용자 요청: "해당 화면에서 서명, 설치파일 생성 탭을 추가하고 사이드 바에서는 메뉴 빼"(EXE 버전의 설치 구성 화면).
|
||||
- 탭 순서: 일반 · 설치파일 · 선행 설치 · 레지스트리 · 바로가기 · 설치화면 · 서명 · 설치파일 생성.
|
||||
- **서명** 탭은 기존 EXE 서명 안내(미서명, 아직 지원하지 않음)를 보여 준다. 서명 설정은 버전이 아니라 프로젝트에 속하므로 모든 버전에 같이 적용된다고 탭 설명에 적었다.
|
||||
- **설치파일 생성** 탭은 기존 검증·빌드 화면(입력 검사, 비활성 빌드 버튼)을 선택한 버전 기준으로 보여 준다. 입력 검사 오류 수는 사이드바의 점 대신 탭의 숫자로 보인다.
|
||||
- 입력 검사를 마치면 **설치파일 생성** 탭으로, `signing`으로 시작하는 검사 오류를 누르면 **서명** 탭으로 간다.
|
||||
- "서명 기능만 따로 사용가능하도록 시작 영역 하단에 도구 서명하기 메뉴를 추가해줘": 시작 화면 **시작** 아래에 **도구** 제목과 **서명하기** 버튼을 더했다. 대상은 "서명이 지원하는 확장명 전부"(SignTool이 서명할 수 있는 모든 형식)로 정했고, 인증서 출처·결과 위치·타임스탬프는 사용자가 "추후 구현"으로 답해 버튼을 **구현 예정**으로 막아 두었다(누를 수 없고 동작 없음).
|
||||
|
||||
## 모델과 실행
|
||||
|
||||
- 프로젝트·버전 파일: 바뀌지 않는다.
|
||||
- 화면: `install-tabs.ts`에 `signing`·`build` 구역을 더하고 EXE에서만 보인다(`sectionsFor`). `sectionForField`는 `signing…` 경로를 `signing`으로 보낸다. `main.ts`의 `navTabs`는 EXE에서 설치 앱 정보·설치 구성만 돌려준다.
|
||||
|
||||
## 확인 결과
|
||||
|
||||
- 프런트엔드 테스트 150개 통과(탭 순서·EXE 전용 구역·배지·경로 판정, 시작 화면의 도구 › 서명하기 위치와 비활성 검사 갱신). TypeScript 검사와 Vite 빌드 통과.
|
||||
- 시작 화면은 브라우저 미리보기(Vite)에서 도구 › 서명하기 배치를 보았다.
|
||||
- 실제 앱에서 탭을 눌러 보는 시험은 수행하지 않았다.
|
||||
|
||||
## 근거
|
||||
|
||||
- 내부 결정이며 외부 문서 근거는 없다.
|
||||
@@ -0,0 +1,41 @@
|
||||
# 오프라인 설치기 덮어쓰기
|
||||
|
||||
2026-11-13. **오프라인용 설치기 생성**이 같은 이름의 기존 파일을 사용자가 동의했을 때 새 설치기로 바꾸게 했다. 기존에는 저장 창에서 기존 파일을 고르면 `OutputExists`로 멈췄다. 같은 날 "결과는 덮어쓰지 않는다" 원칙을 "결과는 사용자가 판단해 동의할 때만 덮어쓴다"로 바꿨다. 이 변경은 MSIX 빌드 결과와 내보내기에 동의 절차를 만들지 않았고, 프로젝트·소스 파일의 덮어쓰기를 허용하지 않는다.
|
||||
|
||||
## 결정
|
||||
|
||||
- 사용자 요청: "덮어 쓰기 가능하게 해줘"(`OutputExists` 오류 화면과 함께).
|
||||
- 원칙 변경: "결과는 사용자 판단에 의해 덮어쓰기 가능하게 해"(사용자 결정). `CLAUDE.md`의 "결과는 덮어쓰지 않는다"를 "결과는 사용자가 판단해 동의할 때만 덮어쓴다"로 바꿨다. 동의가 없으면 기존 결과를 거부하고, 동의해도 새 결과가 완성된 뒤에 한 번에 바꾼다. 지금 동의 절차가 있는 것은 오프라인 설치기뿐이다.
|
||||
- 동의는 사용자가 직접 한다. GUI는 Windows 저장 창이 기존 파일을 바꿀지 묻고(`FOS_OVERWRITEPROMPT`를 명시적으로 켬), 동의해야 그 경로를 돌려준다. CLI는 `--replace`를 줄 때만 바꾸고, 없으면 지금처럼 `OutputExists`로 거부한다.
|
||||
- 교체는 새 설치기가 완성된 뒤 한 번에 한다. 같은 폴더의 임시 파일에 쓰고 다시 읽어 SHA-256을 확인한 다음, 한 번의 이름 바꾸기(`MoveFileEx` 교체)로 기존 파일 자리에 놓는다. 입력 검사 거부, 설치 프로그램 변경·누락, 취소, 다시 읽기 실패에서는 기존 파일이 그대로 남는다.
|
||||
- 동의해도 바꾸지 않는 것: 같은 이름의 폴더(`InvalidSetupOutput`), 이번 설치기를 만드는 데 쓰는 파일인 설치기 원본 `PrerequisiteSetup.exe`와 담을 설치 프로그램 파일(`OutputIsInput`). 바꿀 파일이 실행 중이거나 읽기 전용이면 `OutputNotWritten`으로 멈춘다.
|
||||
|
||||
## 고친 오류
|
||||
|
||||
- 같은 날 사용자 화면에서 **오프라인용 설치기 생성**이 "매개 변수가 틀립니다. (0x80070057)"로 멈췄다. 열린 프로젝트 경로는 `canonicalize` 때문에 `\\?\D:\...` 형식인데, 저장 창의 시작 폴더를 만드는 `SHCreateItemFromParsingName`은 이 형식을 `E_INVALIDARG`로 거부한다(이 PC에서 `D:\networkSetup`은 `0x00000000`, `\\?\D:\networkSetup`은 `0x80070057`로 확인). `save_dialog.rs`의 `shell_path`가 `\\?\D:\…`를 `D:\…`로, `\\?\UNC\server\share\…`를 `\\server\share\…`로 바꿔 넘기고, 실패하면 `SaveDialogFailed`와 폴더를 알린다. 고친 뒤 실제 앱에서 저장 창을 여는 시험은 수행하지 않았다.
|
||||
|
||||
## 모델과 실행
|
||||
|
||||
- 프로젝트·로컬·버전 파일: 바뀌지 않는다.
|
||||
- 엔진: `package_infra::build_offline_setup(doc, version, inputs, output, replace, cancel, progress)`. `replace`가 새 인수다. `check_output`은 기존 파일을 `replace`일 때만 받고 폴더는 거부한다. 설치 프로그램 위치를 정한 뒤 `check_not_input`이 출력과 원본·설치 프로그램의 정규 경로를 비교한다. 마지막 저장은 `replace`면 `persist`, 아니면 `persist_noclobber`이다.
|
||||
- GUI: `build_offline_setup` Tauri 명령은 저장 창이 물은 뒤 받은 경로이므로 `replace = true`로 호출한다. 명령 인자와 권한은 바뀌지 않는다.
|
||||
- CLI: `package-builder offline-setup ... --replace`.
|
||||
- 새 오류 코드: `OutputIsInput`. `OutputExists`와 `OutputNotWritten`은 조치 문구를 바꿨다.
|
||||
- 범위 밖: 백업 파일(`.bak`)을 남기지 않는다. 바뀐 기존 파일은 되돌릴 수 없다.
|
||||
|
||||
## 확인 결과
|
||||
|
||||
- `cargo fmt --all --check`, `cargo clippy --locked --workspace --all-targets -- -D warnings` 통과.
|
||||
- Rust 테스트 133개 통과, 7개 무시(`#[ignore]`). 오프라인 설치기 통합 테스트 9개 통과, 1개 무시(네트워크 다운로드). 새 테스트:
|
||||
- `an_existing_output_is_refused_unless_the_user_agreed_to_replace_it`
|
||||
- `an_agreed_replacement_happens_only_once_the_new_setup_is_complete`
|
||||
- `a_folder_the_setup_program_or_a_carried_installer_is_never_replaced`
|
||||
- 프런트엔드 테스트 149개 통과. TypeScript 검사와 Vite 빌드 통과.
|
||||
- GUI에서 저장 창의 덮어쓰기 확인을 실제로 누르는 시험, CLI `--replace` 실제 실행, 실행 중인 파일을 바꾸는 시험은 수행하지 않았다. 만든 설치기는 설치 시험을 하지 않았다(`installationTest: notRun`).
|
||||
|
||||
증빙: [시험 결과 JSON](../../src/tests/verification-2026-11-13-offline-setup-replace.json)
|
||||
|
||||
## 근거
|
||||
|
||||
- [FILEOPENDIALOGOPTIONS (`FOS_OVERWRITEPROMPT`)](https://learn.microsoft.com/windows/win32/api/shobjidl_core/ne-shobjidl_core-_fileopendialogoptions)
|
||||
- [MoveFileExW (`MOVEFILE_REPLACE_EXISTING`)](https://learn.microsoft.com/windows/win32/api/winbase/nf-winbase-movefileexw)
|
||||
@@ -0,0 +1,41 @@
|
||||
# 선행 설치 추가 창과 자동 추가 중단
|
||||
|
||||
2026-11-13. **선행 설치** 탭의 두 추가 버튼(기본 목록에서 추가, 개별 설치 추가)을 **추가** 하나와 탭 창(**기본 목록 | 직접 추가**)으로 합쳤다. 설치파일이 바뀔 때 기본 목록 항목을 목록에 자동으로 넣던 동작을 없애고, 사용자가 버튼으로 고르게 했다. 프로젝트·버전 파일 형식과 설치파일 분석(`detect_prerequisites`) 결과는 바뀌지 않는다.
|
||||
|
||||
## 결정
|
||||
|
||||
- "기본목록에서 추가 와 개별 설치 추가는 하나로 "추가" 버튼으로 통일할거야. 팝업창에서 탭으로 기본목록 | 직접추가 로 분리해서 구현해": 도구 모음은 **추가**와 **오프라인용 설치기 생성** 두 개다. 설치파일 분석 안내와 "기본 목록에 없는 선행 설치가 필요합니다" 창의 버튼은 **직접 추가**이며 추가 창을 **직접 추가** 탭으로 연다.
|
||||
- "온라인 오프라인은 추가 뒤 목록에서 조정하게 할거야": **기본 목록** 탭은 항목만 고른다. 고른 항목은 설치 중 다운로드(`download`)로 추가되고, 방식은 그리드의 방식 칸에서 바꾼다. 창 폭은 760px(창이 좁으면 맞춰 줄임), 목록 글씨는 13px·11px로 줄였다.
|
||||
- "기본목록에서는 설치파일 기반 자동 추가 버튼을 만들고, 해당 버튼을 누르면 현재 설치파일 기준으로 필요한 항목들 자동 선택되게 해줘": **설치파일 기반 자동 선택**을 누르면 현재 설치파일 분석에서 버전에 없는 기본 목록 항목을 선택하고 "설치파일에 필요"로 표시한다. 창을 열 때는 아무것도 미리 선택하지 않는다. 버튼은 고르기만 하고, 넣는 것은 **추가**다.
|
||||
- "선행 설치에서 설치파일 기반이더라도 목록에서 뺄 수 있게 해", 선택지 "자동 추가를 없앰": 설치파일이 바뀌어도 목록에 자동으로 넣지 않는다. 2026-11-12에 "자동 교정 없음" 원칙의 예외로 정했던 자동 추가를 거둔다. 설치파일 분석으로 넣은 항목도 그리드에서 지우면 사용자가 다시 넣기 전까지 돌아오지 않는다. 기본 목록에 없는 런타임을 알리는 창은 그대로 띄운다(추가는 하지 않음).
|
||||
- "오류는 제일 하단에 뜨게해": 편집 영역의 오류 상자를 내용 맨 아래로 옮기고, 내용이 길면 창 아래쪽에 붙어 보이게 했다(`position: sticky`).
|
||||
- "안내 메시지는 최하단에 띄워": 설치파일 분석 안내(필요한데 목록에 없는 항목, **필요 항목 추가**·**직접 추가** 버튼, 분석 중 표시)를 그리드 위에서 탭 맨 아래, 그리드 설명 다음으로 옮겼다.
|
||||
- "이 메시지는 지워. 알려줄 필요 없어"("설치파일에서 감지한 선행 설치가 모두 목록에 있습니다"): 할 일이 없을 때의 제목(모두 목록에 있음, 감지한 항목 없음)을 없앴다. 안내는 목록에 없는 항목, 기본 목록에 없는 런타임, 읽지 못한 파일이 있을 때만 제목을 달고, 감지되지 않는 등록 항목·경고만 있으면 목록만, 아무것도 없으면 상자를 그리지 않는다.
|
||||
- "오류는 최하단에 보여줘", "팝업 오류는 팝업에서만 보이게 해": 오프라인용 설치기 팝업의 실패 이유를 항목 목록 아래, **닫기** 위로 옮겼다. 그 실패 이유는 페이지 아래 오류 상자에 다시 띄우지 않는다(팝업을 열기 전의 저장 실패 같은 오류는 그대로 페이지에 보인다).
|
||||
- 그리드 아래 설명은 방식, 실행 인수, 오프라인용 설치기 생성 세 줄만 남겼다(사용자 요청으로 나머지 삭제).
|
||||
- "다시 읽어 확인 => 검증 으로 변경해줘": 오프라인용 설치기 팝업의 마지막 단계 이름을 **검증**으로, 끝난 항목을 **검증 완료**로 바꿨다(전체 진행 "3/3 검증").
|
||||
- "닫기 버튼 왼쪽에 생성위치 열기 버튼을 만들고, 클릭시 해당 폴더 띄워줘": 만든 뒤 팝업의 **닫기** 왼쪽에 **생성위치 열기**를 두었다. 새 Tauri 명령 `open_offline_setup_location`은 인자를 받지 않고, `build_offline_setup`이 세션에 기록한 마지막 설치기를 탐색기에서 선택해 연다(`reveal_source`, `explorer.exe /select,`). 오류 `NoOfflineSetup`, `MissingOfflineSetup`은 팝업에만 보인다.
|
||||
- "한줄로 표시하고 글자 크기를 다른 글자들과 동일하게 해"(저장 창의 WebView2 체크 상자): Windows 저장 창은 추가 컨트롤이 하나뿐이면 저장 버튼 옆으로 옮겨 좁게 그리고(`MakeProminent` 기본 동작), 체크 상자 글자는 약 130픽셀에서 줄바꿈·생략한다. 체크 상자를 시각 그룹 **선행 설치:**(파일 형식(T):과 같은 열·글꼴)에 넣고 글자를 **WebView2 포함**으로 줄였으며, 빈 텍스트 컨트롤을 하나 더 두어 파일 형식 아래 줄에 놓이게 했다. x64·약 205MB 설명은 그리드 설명과 README에 둔다. 이어서 사용자가 "저장 버튼 좌측 공간으로는 불가능해?"라고 물어, 다른 세션이 빈 텍스트 컨트롤을 빼고 그룹에 `MakeProminent`를 걸어 **선행 설치: ☑ WebView2 포함**을 저장 버튼 왼쪽 한 줄에 두었다(그 세션의 시험 프로그램 캡처로 확인, [WebView2 선택 기록](2026-11-13-webview2-prerequisite-option.md)). 다시 "선행설치 : 는 지우고 체크박스와 글자사이의 간격을 좀 더 띄워. 그리고 가로줄 정렬을 해줘"에 따라 그룹을 없애고, 앞에 공백 두 칸을 둔 체크 상자 **WebView2 포함** 하나에 `MakeProminent`를 걸었다. 저장 버튼과의 약 2px 세로 어긋남과 파일 형식 아래 빈 띠는 API로 바꿀 수 없어 남았다(같은 기록 참고). 마지막으로 "WebView2 설치 포함 으로 문구 수정해"에 따라 글자를 **WebView2 설치 포함**으로 바꿨다(시험 프로그램 캡처에서 저장 버튼 왼쪽 한 줄에 들어감을 확인).
|
||||
|
||||
## 모델과 실행
|
||||
|
||||
- 프로젝트·로컬·버전 파일: 바뀌지 않는다.
|
||||
- 화면(`prerequisites-editor.ts`): `renderAddPrerequisiteDialog`·`bindAddPrerequisiteDialog`·`readAddPrerequisiteDialog`가 창을 만든다. 탭 본문은 `renderCatalogPanel`, `renderCustomPrerequisitePanel`이다. 자동 선택은 순수 함수 `detectionSelection(scan, catalogIds, listed)`로 고를 항목과 결과 문장을 정한다.
|
||||
- 조정(`main.ts`): 동작 `prerequisite-add`(기본 목록 탭), `prerequisite-add:custom`(직접 추가 탭). 자동 선택은 탭의 분석이 현재 설치파일 기준이면 그 결과를, 아니면 `detect_prerequisites`를 새로 호출한다. `autoAddDetected`를 `announceChangedNeeds`로 바꿔 기본 목록에 없는 런타임 안내만 한다.
|
||||
- 새 오류 코드: `NothingSelected`(기본 목록 탭에서 아무것도 고르지 않고 **추가**).
|
||||
- 문구: 설치파일 분석의 기본 목록에 없는 런타임 안내 끝을 "직접 추가하세요."로 바꿨다(`package-domain` `prerequisite_detection.rs`).
|
||||
|
||||
## 확인 결과
|
||||
|
||||
- 프런트엔드 테스트 150개 통과. TypeScript 검사와 Vite 빌드 통과.
|
||||
- `package-domain` 설치파일 분석 테스트 6개 통과(문구 변경 뒤).
|
||||
- 추가 창은 브라우저에서 정적 미리보기로 두 탭의 크기와 배치를 보았고, 가짜 분석 결과를 주는 시험 페이지에서 **설치파일 기반 자동 선택**을 눌러 선택·표시·결과 문장·카테고리 전환을 보았다.
|
||||
- 저장 창 체크 상자 배치는 같은 COM 호출을 하는 임시 시험 프로그램(Per-Monitor V2, 이 PC의 Windows 11)으로 단일 컨트롤, 시각 그룹, 빈 텍스트 추가, 긴 한글·영문 문구, 짧은 문구를 띄워 화면을 캡처해 비교했고, 시험 프로그램은 지웠다.
|
||||
- Rust 테스트 134개 통과(7개 무시), clippy·fmt 통과.
|
||||
- 실제 데스크톱 앱에서 실제 설치파일로 누르는 시험, 오류 상자 위치 확인, 자동 추가가 일어나지 않는지 확인하는 시험, **생성위치 열기**를 눌러 탐색기가 열리는지 보는 시험, 앱의 저장 창 모양 확인은 수행하지 않았다.
|
||||
|
||||
증빙: [시험 결과 JSON](../../src/tests/verification-2026-11-13-prerequisite-add-dialog.json)
|
||||
|
||||
## 근거
|
||||
|
||||
- [WAI-ARIA Authoring Practices: Tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
|
||||
@@ -0,0 +1,45 @@
|
||||
# 오프라인 설치기의 WebView2 Runtime 선택
|
||||
|
||||
2026-11-13. 모든 오프라인용 설치기에 맨 앞에 넣던 Microsoft Edge WebView2 Runtime(x64)을 설치기를 만들 때 고르게 했다. **오프라인용 설치기 저장** 창의 **저장** 버튼 왼쪽에 **WebView2 설치 포함** 체크 상자를 더했고, 기본으로 체크되어 있다. 선행 설치기 화면이 Windows 작업 대화상자로 바뀌어 설치기 자신은 WebView2가 필요 없으므로, 필요 없는 설치기에서는 약 205MB를 뺄 수 있게 했다. 제품 설치기(setup.exe)에 WebView2 Fixed Version 런타임을 넣는 일과 대상 PC 설치 시험은 범위 밖이다.
|
||||
|
||||
## 사용
|
||||
|
||||
1. 설치 구성 › **선행 설치** 탭에서 **오프라인용 설치기 생성**을 누른다.
|
||||
2. 저장 창의 **저장** 버튼 왼쪽에 **WebView2 설치 포함** 체크 상자가 체크된 채 열린다. 그대로 저장하면 WebView2 Runtime(x64, 약 205MB)을 맨 앞에 넣고, 해제하고 저장하면 넣지 않는다.
|
||||
3. 이 선택은 프로젝트와 버전 파일에 저장하지 않는다. 설치기를 만들 때마다 다시 고른다.
|
||||
4. CLI `offline-setup`은 기본으로 넣고, `--no-webview2`를 주면 뺀다.
|
||||
|
||||
## 결정
|
||||
|
||||
- 사용자 결정(2026-11-13): 오프라인 설치기를 만들 때 WebView2를 선택해서 넣게 한다. 처음에는 선행 설치 기본 목록 항목으로 구현했으나, 사용자가 저장 창의 체크 상자를 요청해 **체크 상자만** 쓰기로 바꿨다. 기본 목록에서는 다시 뺐다. 같은 날 사용자 요청으로 기본값을 **체크**로 정했다.
|
||||
- 같은 날 오전에 구현한 "모든 오프라인 설치기에 자동 포함"을 대체한다. 체크한 채 만든 설치기는 오전 동작과 같다: 맨 앞에 넣고, 버전 목록에 같은 ID(`webview2-x64`)가 있으면 목록의 항목은 뺀다.
|
||||
- 항목 내용: ID `webview2-x64`, x64 Evergreen Standalone 설치 프로그램 215,195,856바이트(SHA-256 `1d1c838e5971343c060415da7c630c3c1ca7ad33fbb61cca660f3c1122f4cca8`), 실행 인수 `/silent /install`, 재부팅 종료 코드 없음, 감지 `webView2`. 대상 PC에서는 레지스트리가 아니라 WebView2 로더(`GetAvailableCoreWebView2BrowserVersionString`)로 설치 여부를 판단한다.
|
||||
- 저장 창은 rfd로는 체크 상자를 더할 수 없어 이 창만 Windows 공통 파일 대화상자(`IFileSaveDialog` + `IFileDialogCustomize::AddCheckButton`)로 직접 띄운다. 다른 파일 대화상자는 그대로 rfd를 쓴다.
|
||||
- 체크 상자는 시각 그룹(`StartVisualGroup` "선행 설치:" → `AddCheckButton` "WebView2 포함" → `EndVisualGroup`) 안에 두고 그룹을 `MakeProminent`로 **저장** 버튼 왼쪽에 놓는다. 처음 문구 "WebView2 Runtime (x64) 포함 (약 205MB)"는 이 자리에서 약 130px 폭으로 두 줄이 되어, 사용자가 "한줄로 표시하고 글자 크기를 다른 글자들과 동일하게 해"라고 요청했다. 그래서 문구를 짧게 하고, 한때 빈 `AddText`를 더해 파일 형식 아래 줄로 옮겼다. 이후 사용자가 "저장 버튼 좌측 공간으로는 불가능해?"라고 물어 짧은 문구 그대로 다시 저장 버튼 왼쪽으로 옮겼다. 시험 프로그램 화면에서 한 줄로 들어가고 글자 크기가 창의 다른 글자와 같은 것을 확인했다.
|
||||
- 이어서 사용자가 "선행설치 : 는 지우고 체크박스와 글자사이의 간격을 좀 더 띄워. 그리고 가로줄 정렬을 해줘"라고 요청했다. 시각 그룹을 없애고 체크 상자 하나를 `MakeProminent`로 저장 버튼 왼쪽에 둔다. `IFileDialogCustomize`에는 상자와 글자 사이 간격 설정이 없어 글자 앞에 공백 두 칸을 넣는다(`LABEL_INDENT`).
|
||||
- Windows가 정해 바꾸지 못한 것:
|
||||
- 줄 맞춤: 시험 프로그램 화면(960×540)을 픽셀로 재면 세로 중심이 체크 상자 글자 499.5px, 저장 버튼 501.5px, 폴더 숨기기 504px이다. 체크 상자의 위치·크기를 정하는 API가 없다.
|
||||
- 빈 공간: 파일 형식 줄과 아래 버튼 줄 사이의 약 55px 빈 공간도 Windows 배치다. 처음 rfd 저장 창에도 같은 간격이 있었다. `IFileSaveDialog::SetCollectedProperties`로 수집할 파일 속성을 빈 목록·`NULL`로 지정해도 바뀌지 않았다.
|
||||
|
||||
## 모델과 실행
|
||||
|
||||
- 프로젝트 형식: 바뀐 필드가 없다.
|
||||
- `package-domain`: `setup_webview2()`(`setup-webview2.json`)가 WebView2 항목을 준다. 기본 목록(`prerequisites.json`)은 16개 그대로다.
|
||||
- `package-infra`: `OfflineSetupInputs.first`에 만들 때 고른 항목을 넘긴다. 체크하면 `[setup_webview2()]`, 해제하면 빈 목록이다. `build_offline_setup`은 `first` → 버전 목록(같은 ID 제외) 순서로 검사하고 담는다.
|
||||
- 데스크톱: 새 모듈 `src-tauri/src/save_dialog.rs`가 저장 창을 자기 스레드의 STA에서 앱 창을 소유자로 띄우고, 고른 경로와 체크 상태를 돌려준다. 취소(`ERROR_CANCELLED`)는 `None`이며 실패는 `SaveDialogFailed`이고, Windows가 아니면 `UnsupportedPlatform`으로 거부한다. Tauri 명령 `build_offline_setup(version)`은 호출한 창(`WebviewWindow`)을 받는다. 프런트엔드 호출 인자와 권한은 바뀌지 않았다. 의존성: `windows` 0.62(`Win32_Foundation`, `Win32_System_Com`, `Win32_UI_Shell`, `Win32_UI_Shell_Common`, Windows 전용).
|
||||
- 화면: 선행 설치 탭 안내 문구가 체크 상자를 설명한다. 진행 팝업은 빌더가 알리는 이름으로 행을 만들어, WebView2를 넣으면 맨 앞에 행이 하나 늘어난다.
|
||||
- 스크립트: `Get-Prerequisites.ps1`·`Publish-Desktop.ps1`은 기본 목록과 `setup-webview2.json`을 함께 읽는다. `New-OfflineSetupTestKit.ps1`은 `catalog.exe`를 기본값(WebView2 포함)으로, 가짜 설치기들은 `--no-webview2`로 만든다. `Invoke-OfflineSetupTests.ps1`은 `catalog.exe`에 WebView2가 있을 때만 설치 또는 건너뜀을 요구하고, 없으면 `notCarried`로 적는다.
|
||||
|
||||
## 확인 결과
|
||||
|
||||
- `cargo fmt --all --check` 통과. `cargo test --locked --workspace` 131개 통과, 7개 무시. `cargo clippy --locked --workspace --all-targets -- -D warnings` 통과.
|
||||
- 프런트엔드 `npm.cmd test` 148개 통과, `npm.cmd run build`(`tsc`·Vite) 통과.
|
||||
- 바뀐 테스트: 도메인 `the_setup_webview2_is_a_valid_pinned_download_judged_by_its_loader_outside_the_catalog`, 통합 `entries_chosen_for_the_setup_come_first_replace_a_listed_copy_and_are_not_added_otherwise`(고르지 않으면 목록만, 고르면 맨 앞·목록의 같은 ID 제외·`webView2` 감지·인수 유지·기본 제공 파일 사용), 프런트엔드 진행 행 2개(WebView2 포함·미포함).
|
||||
- 저장 창: 사용자가 앱에서 띄워 두 문제를 알렸고 둘 다 고쳤다. 시작 폴더 `\\?\` 경로 오류(0x80070057)는 [덮어쓰기 기록](2026-11-13-offline-setup-replace.md)에 있다. 다른 하나는 체크 상자 배치다. 지금 배치(그룹 + `MakeProminent`)는 앱과 같은 매니페스트(Common Controls 6, PerMonitorV2)로 같은 COM 호출을 하는 시험 프로그램을 띄워 화면을 찍어 확인했다(Windows 11, 960×540 창). 앱 안에서 띄운 화면은 아니다.
|
||||
- 실행하지 않음: 데스크톱 앱에서 체크한 채와 해제한 채로 각각 설치기를 만들어 결과를 비교하는 GUI 시험, `scripts/Check.ps1` 전체(`npm.cmd ci`와 실행 파일 빌드 포함), 실제 프로젝트로 CLI 생성(`--no-webview2` 포함), 대상 PC·VM 설치 시험, WebView2가 없는 PC, 로더 무시 테스트(`--ignored`).
|
||||
|
||||
## 근거
|
||||
|
||||
- [Microsoft Learn: Distribute your app and the WebView2 Runtime](https://learn.microsoft.com/microsoft-edge/webview2/concepts/distribution)
|
||||
- [Microsoft Learn: IFileDialogCustomize::AddCheckButton](https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialogcustomize-addcheckbutton)
|
||||
- [Microsoft Learn: Common Item Dialog](https://learn.microsoft.com/windows/win32/shell/common-file-dialog)
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
| 파일 | 내용 |
|
||||
| --- | --- |
|
||||
| `<이름>.isproj` (`formats.exe`) | `architecture`(x64·x86), `upgradeCode`(생성 시 대문자 `{GUID}`, 모든 버전 공유), `installScope`(`perMachine`만), `installRoot`(`[ProgramFiles64Folder]` 또는 x86의 `[ProgramFilesFolder]` + 안전한 상대 경로), `properties`(사용자 속성 `{name, value}`; 기본 속성은 `package_domain::BUILTIN_PROPERTIES`, 해석·치환은 `parse_formatted`·`expand_formatted`). 최상위 `files`·`registry`는 비어 있어야 한다(`VersionedContent`). 템플릿: [`ExeInstallerProject.isproj`](../../src/fixtures/ExeInstallerProject.isproj) |
|
||||
| `<이름>.versions/<버전>.isver` (`ExeVersion`) | `version`(Major.Minor.Patch, 파일 이름과 같아야 함), `description`(제작자 메모, 1000자 이하, 없으면 빈 문자열, 설치 화면에 표시 안 함, `DescriptionTooLong`), `productCode`(대문자 `{GUID}`, UpgradeCode와 달라야 함. 메이저 버전마다 새 값, 패치 버전은 라인 메이저 버전과 같은 값), `updateType`(`major` 기본 / `patch`, [15 EXE 버전과 누적 패치](15-exe-versions-and-patches.md)), `locked`(사용자가 잠금·해제), `files`(파일 규칙), `registry`, `shortcuts`(`{id, location, folder, name, target, arguments}`; `target`은 그 버전의 설치 파일 중 하나, `arguments`는 서식 문자열). 스키마: `schemas/exe-version.schema.json`, 예제: [`exe.1.0.0.isver`](examples/exe.1.0.0.isver) |
|
||||
| `<이름>.isproj` (`formats.exe`) | `architecture`(x64·x86), `upgradeCode`(생성 시 대문자 `{GUID}`, 모든 버전 공유), `installScope`(`perMachine`만), `installRoot`(`[ProgramFiles64Folder]` 또는 x86의 `[ProgramFilesFolder]` + 안전한 상대 경로. 상대 경로에는 `[Manufacturer]`·`[ProductName]`만 쓸 수 있고 실제 값으로 바꾼 뒤 검사한다. 템플릿 기본값은 `[ProgramFiles64Folder][Manufacturer]\[ProductName]`), `properties`(사용자 속성 `{name, value}`; 기본 속성은 `package_domain::BUILTIN_PROPERTIES`, 해석·치환은 `parse_formatted`·`expand_formatted`). 최상위 `files`·`registry`는 비어 있어야 한다(`VersionedContent`). 템플릿: [`ExeInstallerProject.isproj`](../../src/fixtures/ExeInstallerProject.isproj) |
|
||||
| `<이름>.versions/<버전>.isver` (`ExeVersion`) | `version`(Major.Minor.Patch, 파일 이름과 같아야 함), `description`(제작자 메모, 1000자 이하, 없으면 빈 문자열, 설치 화면에 표시 안 함, `DescriptionTooLong`), `productCode`(대문자 `{GUID}`, UpgradeCode와 달라야 함. 메이저 버전마다 새 값, 패치 버전은 라인 메이저 버전과 같은 값), `updateType`(`major` 기본 / `patch`, [15 EXE 버전과 누적 패치](15-exe-versions-and-patches.md)), `locked`(사용자가 잠금·해제), `files`(파일 규칙), `registry`, `shortcuts`(`{id, location, folder, name, target, arguments}`; `target`은 그 버전의 설치 파일 중 하나, `arguments`는 서식 문자열), `prerequisites`(제품보다 먼저 설치할 프로그램. `{id, name, architecture, delivery, url, file, sha256, bytes, arguments, detect, rebootCodes}`; `delivery`는 `download`(설치 중 다운로드) / `bundled`(설치 파일에 포함), `file`은 소스 alias와 그 안의 경로, 없는 파일은 빈 목록으로 연다. [변경 기록](../changes/2026-11-12-exe-prerequisites.md)). 스키마: `schemas/exe-version.schema.json`, 예제: [`exe.1.0.0.isver`](examples/exe.1.0.0.isver) |
|
||||
|
||||
새 EXE 프로젝트는 버전 없이 시작하며, 첫 버전은 사용자가 번호를 지정해 빈 내용으로 만든다. 이후 버전은 선택한 버전을 복사하며, 메이저 버전은 새 ProductCode를 받고 패치 버전은 아래쪽 메이저 버전의 ProductCode를 이어받는다. 검증은 다른 버전과의 관계(`validate_version_line`: ProductCode 재사용, 패치 기준 잠금, 이전 버전 내용 제거·이동 금지)도 검사한다. 저장은 버전 파일마다 읽은 시점의 해시를 비교하고(`RevisionConflict`), 잠긴 버전의 내용 변경(`VersionLocked`)과 패치가 ProductCode를 이어받는 메이저 버전의 삭제(`VersionInUse`)를 거부한다. 화면은 잠긴 버전의 삭제 버튼을 숨기며, 잠금 해제와 삭제를 한 번에 저장하는 것은 허용한다. 바꾼 파일만 `.isver.bak`을 남기고 다시 쓰며, 삭제한 버전 파일은 `.isver.bak`으로 옮긴다. 검증은 공유 설정(`validate`)과 선택한 버전(`validate_version`, 필드 경로 `version.*`)을 함께 검사한다. 이전 단일 버전 EXE 형식(`msiVersion`·`productCode`·`shortcuts`가 `formats.exe`에 있는 파일)은 자동 변환하지 않고 열기를 거부한다. 버전 파일의 `installerUi`(설치 단계·화면 문구·모양·스크립트)는 [14 EXE 설치 UI](14-exe-installer-ui.md#제작기-설치화면-편집-2026-11-09)를 따르며, 없으면 표준 마법사로 연다. 아래 범용 예제의 나머지 EXE 필드(prerequisites, patchPolicy, installationFlow)는 아직 모델에 없으며 알 수 없는 필드로 거부한다.
|
||||
|
||||
|
||||
@@ -16,5 +16,28 @@
|
||||
}
|
||||
],
|
||||
"registry": [],
|
||||
"shortcuts": []
|
||||
"shortcuts": [],
|
||||
"prerequisites": [
|
||||
{
|
||||
"id": "vc14-x64",
|
||||
"name": "Microsoft Visual C++ 2015-2022 Redistributable 14.44.35211 (x64)",
|
||||
"architecture": "x64",
|
||||
"delivery": "download",
|
||||
"url": "https://download.visualstudio.microsoft.com/download/pr/e9604e21-4b79-4096-a4b3-1b63d4b55b4d/CC0FF0EB1DC3F5188AE6300FAEF32BF5BEEBA4BDD6E8E445A9184072096B713B/VC_redist.x64.exe",
|
||||
"sha256": "cc0ff0eb1dc3f5188ae6300faef32bf5beeba4bdd6e8e445a9184072096b713b",
|
||||
"bytes": "25635768",
|
||||
"arguments": "/install /quiet /norestart",
|
||||
"detect": {
|
||||
"kind": "registryValue",
|
||||
"view": "32",
|
||||
"path": "HKLM\\SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\x64",
|
||||
"name": "Version",
|
||||
"minimum": "14.44.35211"
|
||||
},
|
||||
"rebootCodes": [
|
||||
1641,
|
||||
3010
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
"architecture": "x64",
|
||||
"upgradeCode": null,
|
||||
"installScope": "perMachine",
|
||||
"installRoot": "[ProgramFiles64Folder]Example Publisher\\Sample Desktop Tool",
|
||||
"installRoot": "[ProgramFiles64Folder][Manufacturer]\\[ProductName]",
|
||||
"properties": []
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user