rubrapack 매뉴얼←↑→

20 원본 파일과 rubrapack 명령

이 장의 차례
20.1시작하기첫 패키지약관 페이지와 선택 구성요소나머지를 설명하는 곳20.2예20.3원본 형식20.4TOML 부분집합20.5표MSIX 패키지설치한 프로그램으로 등록하기: [action.ID]프로그램이나 DLL 실행: 동작 하나 [action.ID]레지스트리 값: [registry.ID]바로가기: [shortcut.ID]파일 형식과 링크: [assoc.ID], [protocol.ID]탐색기의 우클릭 메뉴: [menu.ID]COM 클래스: [com.ID]탐색기 처리기: [handler.ID]MSIX 전용: [msix-extension.ID]파일 옮기기, ODBC, 게시 구성 요소어셈블리: assembly지우기와 복사: [remove.ID], [copy.ID]제거할 때 싹 지우기: recursive, on = "uninstall"모든 사용자의 것: users = "all"환경 변수: [env.ID]INI 파일: [ini.ID]설치 프로그램 하나에 여러 패키지: [chain]병합 모듈: [merge.ID]병합 모듈 만들기: [module]검색과 요구: [search.ID], [require.ID]서비스, 글꼴, 권한사용자별 패키지와 겸용 패키지: scope캐비닛설치된 앱 항목과 속성명령줄로 설치하기창 없이 설치하기대화창: ui여러 언어언어팩아이콘, 완료 페이지, 설치 범위 페이지나만의 대화창 페이지: [dialog.ID], [dialog-control.ID]빌보드: [billboard.ID]아키텍처와 업그레이드 계열먼저 지워야 하는 판진행하기 전에: 사전 점검다른 사본: [other-copy.ID]나중에 정리하기: 정리 작업제품과 함께 지워지는 추가 기능: parent, remove-addons여러 인스턴스 나란히: instances다른 제품을 대신하기: replaces와일드카드: [files.ID]빈 폴더: [folder.ID]기능(feature)조건: when제거할 때 파일 남기기: keep변수Windows 이름프로그램 파일설치 폴더 지키기경로20.6명령줄20.7진단 코드

원본 파일 하나가 패키지 하나를 기술한다. 형식은 TOML 1.0의 엄격한 부분집합이고 이름은 *.toml 이라서, 편집기와 GitHub, AI 도우미가 TOML 로 알아본다. 모든 원본은 올바른 TOML 이지만, 부분집합 밖의 TOML 기능은 무시하지 않고 오류로 거부한다.

20.1 시작하기#

rubrapack 은 프로그램 파일 하나다. 릴리스 페이지에서 rubrapack-<판>-windows-x64.exe(원하면 rubrapack.exe 로 이름을 바꾼다)나 rubrapack-<판>-linux-x86_64(chmod +x 한다)를 내려받아 그 자리에서 실행하거나 PATH 에 둔다. 따로 설치할 것은 없다: 런타임도, SDK 도, 라이브러리도 필요 없다. 같은 프로그램이 Windows 에서도 Linux 에서도 같은 패키지를 만든다.

이 절은 설치 파일을 아는 독자를 위한 짧은 판이다. 제1부 튜토리얼은 같은 것을 처음부터 한 걸음씩 가르치고, 모든 표와 옵션까지 나아간다: 시작하기 전에부터 읽는다.

첫 패키지#

배포할 것을 dist/ 폴더에 둔다: 프로그램, 그리고 그것이 필요로 하는 파일은 dist/files/ 에(하위 폴더도 그대로 간다). 그다음 rubrapack 에게 시작할 원본을 쓰게 한다:

rubrapack new app -i        # 묻고 나서 app.toml 을 쓰고 검사한다

제품 이름, 판, 파일이 든 폴더, 주 프로그램(아키텍처는 파일에서 읽는다), Program Files 아래 폴더, 누구를 위해 설치하는지, 어떤 하위 폴더가 선택 구성요소인지, 대화창, 약관(옆에 LICENSE.txt, .md, .rtf 가 있으면 권한다), 한국어 대화창, 바로가기를 묻는다. Enter 는 대괄호 안의 값을 쓴다. 끝에는 같은 답을 한 줄 명령(rubrapack new app --dist dist --ui features ...)으로 보여 주는데, 스크립트나 AI 도우미는 묻지 않고 이것을 돌리면 된다. 이름 없이 rubrapack new 만 주면 파일 이름까지 묻고, rubrapack new app(옵션 없이)은 대신 고정된 틀을 쓴다.

어느 쪽이든 app.toml 은 읽고 고칠 수 있는 평문이다 - 아무 편집기로나, 또는 rubrapack edit app.toml 으로. edit 는 같은 질문을 지금 값을 기본으로 보여 주는 메뉴다(판, 설치 폴더, 대화창과 약관, 선택 구성요소, 바로가기, 그리고 프로그램 폴더가 바뀐 뒤 파일 목록 맞추기). 그 값만 바꾸고 다른 줄, 주석, 표는 그대로 둔다. 스크립트에서는: rubrapack edit app.toml --set define.VERSION=1.1.0 --sync. 아래 원본은 프로그램과 파일을 Program Files\My App 에 설치하고, 시작 메뉴에 넣고, 사용자가 폴더를 바꿀 수 있는 대화창을 보인다:

format = 1

[package]
name = "My App"
manufacturer = "My Company"            # 설정 > 설치된 앱에 보인다
version = "$(VERSION)"
arch = "x64"                           # x64, arm64, x86
upgrade-code = "{E8C1815C-CCD7-4F3F-B914-92A4E3F3A317}"   # `new` 가 준 것; 영원히 간직한다
ui = "installdir"

[define]
VERSION = "1.0.0"

[dir.INSTALLDIR]
path = "$(ProgramFiles)/My App"

[file.App]
dir = "INSTALLDIR"
source = "dist/app.exe"

[files.Rest]
dir = "INSTALLDIR"
glob = "dist/files/**"

[shortcut.StartMenu]
dir = "Programs"
name = "My App"
target = "file:App"
rubrapack build app.toml -o app-1.0.0.msi
rubrapack build app.toml -o app-1.0.1.msi -D VERSION=1.0.1    # 다음 판

이것으로 완전한 설치 파일이다. "설치된 앱"에 나타나고, 스스로 복구하고, 설치한 것을 모두 지운다. 더 높은 판은 설치된 판을 대체하고, 같거나 낮은 판은 안내문과 함께 거부된다. 컴포넌트 GUID, 파일 키, 캐비닛과 표는 원본에서 끌어내므로, 판에서 판으로 기억해야 할 것은 업그레이드 코드뿐이다.

약관 페이지와 선택 구성요소#

설치 파일이라면 흔히 바라는 두 가지: 설치 전에 사용자가 약관에 동의하는 것, 그리고 일부 구성요소를 고를 수 있는 것. license 는 약관 페이지를 더하고("동의합니다"에 표시하기 전에는 다음 단추가 꺼져 있다), ui = "features" 는 사용자가 설치할 기능을 고르는 트리를 더한다. 아래 원본은 프로그램을 늘 설치하고, 예제는 보여 주되 고르지 않은 상태로 둔다:

format = 1

[package]
name = "My App"
manufacturer = "My Company"
version = "$(VERSION)"
arch = "x64"
upgrade-code = "{E8C1815C-CCD7-4F3F-B914-92A4E3F3A317}"
ui = "features"                        # welcome, license, folder, feature tree, ready
license = "LICENSE.txt"                # Next stays off until "I accept" is ticked

[define]
VERSION = "1.0.0"

[feature.Main]
title = "My App"
description = "The program itself."
required = true                        # always installed: the tree does not offer to leave it out

[feature.Samples]
title = "Samples"
description = "Example documents to try the program with."
level = 2                              # offered in the tree, not selected by default

[dir.INSTALLDIR]
path = "$(ProgramFiles)/My App"
feature = "Main"

[dir.SamplesDir]
path = "$(INSTALLDIR)/samples"
feature = "Samples"

[file.App]
dir = "INSTALLDIR"
source = "dist/app.exe"

[files.Rest]
dir = "INSTALLDIR"
glob = "dist/files/**"

[files.Samples]
dir = "SamplesDir"
glob = "dist/samples/**"

[shortcut.StartMenu]
dir = "Programs"
name = "My App"
target = "file:App"

약관 글은 LICENSE.txt(.txt, .md, .rtf)에, 예제 파일은 dist/samples/ 에 둔다. level = 2 인 기능은 사용자가 표시하지 않으면 설치되지 않고, required 는 트리가 그 기능을 빼자고 제안하지 못하게 한다. 나중에 사용자는 "설치된 앱"의 "변경"에서 고른 것을 바꿀 수 있고, 명령줄에서도 대화창 없이 같은 일을 한다: msiexec /i app.msi /qn ADDLOCAL=Samples 는 예제를 더하고, REMOVE=Samples 는 뺀다(required 는 트리에만 적용되고 명령줄은 묶지 않는다). 나머지는 기능(feature)과 조건: when에 있다.

나머지를 설명하는 곳#

하려는 일읽을 곳
msiexec 로 설치, 업그레이드, 제거, 기록 남기기첫 설치 파일, 판과 업그레이드
오류를 이해하고 패키지 안 보기(lint, inspect, extract, 종료 코드)검사하고 들여다보기, 진단 코드
패키지에 서명하기서명과 타임스탬프
스크립트나 CI 작업, 또는 AI 도우미로 빌드하기빨리 시작하기와 자동화

20.2 예#

format = 1

[define]
VERSION = "1.4.0"

[package]
name = "Example App"
manufacturer = "Example"
version = "$(VERSION)"
arch = "x64"                                        # x64, arm64, x86 가운데 하나 - 기본값 없음
upgrade-code = "{0B9A6C1E-3D2F-4A5B-8C7D-6E5F4A3B2C1D}" # 한 번 만들어 영원히 쓴다
language = "en-US"                                  # 또는 "ko-KR"

[dir.INSTALLDIR]
path = "$(ProgramFiles)/Example App"

[dir.Docs]
path = "$(INSTALLDIR)/docs"

[file.MainExe]
dir = "INSTALLDIR"
source = "dist/app.txt"

[file.Guide]
dir = "Docs"
source = "dist/guide.txt"
name = "User guide.txt"
rubrapack build example.toml -o example.msi -D VERSION=1.4.1
rubrapack inspect example.msi File

20.3 원본 형식#

원본의 첫 키는 어떤 표보다도 앞에 오는 format = 1 이다: 원본 형식의 판을 정수 하나로 적는다. 원본을 다르게 써야 할 때만 올라가고, 표나 키가 새로 생겨도 그대로다. 이것이 없는 원본은 rubrapack 0.18 이하의 것으로 보고 경고(RP1108)를 낸다. 그 뒤에 바뀐 것은 쓰인 자리에서 거부한다 - 형식 1 은 경로를 $(...) 로 시작한다 (경로). 더 큰 수가 적힌 원본에는 더 새 rubrapack 이 필요하다.

20.4 TOML 부분집합#

받는 것: [kind]·[kind.ID] 표, 맨 키(A-Z a-z 0-9 _ -), TOML 이스케이프가 되는 기본 문자열 "...", 리터럴 문자열 '...'(이스케이프 없음 - 백슬래시와 따옴표를 쓸 때 편하다: 'SOFTWARE\Example'), 십진수와 0x 정수, true/false, 한 가지 형만 담은 배열, # 주석. BOM 이 있거나 없는 UTF-8, 또는 BOM 이 있는 UTF-16LE. 줄 끝은 LF 나 CRLF.

오류로 거부하는 것: 여러 줄 문자열, 인라인 표, 표 배열, 점으로 이은 키와 따옴표 키, 세 부분 이상의 표 이름, 부동소수, 날짜, 숫자 속 _, 8진·2진 수, 빈 배열이나 섞인 배열, 첫 표보다 앞의 키(format 은 빼고). 표와 키의 순서는 아무 의미가 없다.

20.5 표#

표키(굵은 것은 반드시)
[package]name, manufacturer, version(a.b.c 또는 a.b.c.d), arch, upgrade-code, upgrade-code-x64 / -arm64 / -x86, product-code, summary-name(ASCII), language, scope(machine, user, dual), ui(none, basic, minimal, installdir, features), license(.txt, .md, .rtf), reboot(suppress/allow), cleanup(false: 정리 작업 없음), preflight(false: 진행 전 점검 없음), close-programs(ask, always, never), extra-files(ask, keep, delete), parent(추가 기능: 본체 제품의 upgrade-code), remove-addons(true: 이 제품을 지우면 추가 기능도 지운다), replaces(이 패키지가 대신하는 제품들의 업그레이드 코드), downgrade-message, compress(none, mszip, mszip:0..mszip:9, lzx, lzx:15..lzx:21; 기본 mszip:6), cab(embed 또는 external), cab-max-size(MiB), refuse-upgrade-below, refuse-upgrade-message, instances(N >= 2: N-1개의 이름 붙은 인스턴스, 컴퓨터 단위 전용)
[define]변수: NAME = "value"
[feature.ID]title, description, level(1-32767), hidden, parent, required, follow-parent, when, default-when, advertise(allow 기본, disallow)
[dir.ID]path = 기준/상대/경로, feature, guard(true: 설치 폴더 지키기 참고)
[file.ID]dir, source, name, vital(기본 true), any-arch, feature, component-guid, keep, when, isolate-for(.exe 의 file:ID: 그 프로그램 옆에 이 DLL 의 전용 사본, MSI 전용), assembly(dotnet, win32), assembly-for(.exe 의 file:ID: 개인 어셈블리), manifest(Win32 어셈블리 매니페스트의 file:ID) - 어셈블리 참조
[files.ID]dir, glob, vital, any-arch, feature, keep, when
[folder.ID]dir, name, keep, feature
[arp]no-modify, no-repair, no-remove, help(URL), about(URL), icon(.ico), logo(.png: 8비트 RGB 나 RGBA, 한 변 1024픽셀 이하; 메뉴 패키지의 로고) - "설치된 앱"에 제품이 어떻게 보이는가
[property.ID]value, secure, hidden - 대문자 이름의 공개 속성
[action.ID]do/undo 한 쌍: run(.exe 의 file:ID), do, undo, check(do 가 만든 것이 있으면 0; 되돌리기 실패 보고서용). 또는 동작 하나: run 과 args, 또는 dll(설치된 .dll 의 file:ID)이나 binary(패키지 안 .dll)와 entry; phase(immediate, deferred 기본, commit, rollback), as(system, user), when, data, before / after(표준 동작) 또는 sequence, ignore-exit. 프로그램이나 DLL 실행 참조
[registry.ID]root(HKLM, HKCU, HKCR, HKMU), key, name, value, type, remove, on(remove 와 함께: install, 또는 uninstall - 제거할 때 키 전체), users(all: 모든 사용자의 것, 모든 사용자의 것 참고), keep, view, with, feature, when
[remove.ID]dir, name(* 와 ?; 없으면 폴더 자체), on(install, uninstall, both), upgrade(false: 업그레이드가 이 판을 지울 때는 하지 않음), recursive(true: 제거할 때 폴더와 그 안 전부), users(all: 모든 사용자의 것, 모든 사용자의 것 참고), feature
[ini.ID]dir, file, section, key, value, mode(set, add, remove), feature, when
[require.ID]condition, message
[search.ID]property(또는 dir ID), kind(registry: root, key, name, view; file: path, file, min-version; dir: path; component: component-guid)
[service.ID]file(.exe 를 가리키는 file:ID), name, display-name, description, start(auto, demand, disabled), account(LocalSystem, LocalService, NetworkService), args, start-on-install
[assoc.ID]extension(.ext, 소문자), prog-id, target(.exe 를 가리키는 file:ID), description, icon(file:ID), logo(정사각형 PNG: MSIX 에서 그 형식의 아이콘), args(기본 "%1"), content-type, perceived-type, default(false: "연결 프로그램"에만)
[menu.ID]on(파일 형식, "*", "folder", "background", "drive", "assoc:ID". 하위 메뉴의 항목에는 없다), text 또는 text-xx, target(.exe 를 가리키는 file:ID. 없으면 하위 메뉴), args(기본 "%1"), icon(file:ID), parent(하위 메뉴의 ID), multi(each, one, single), extended, windows11(기본 true), list(multi = "one" 과 함께: 경로가 명령줄에 다 들어가지 않을 때 %* 대신 쓰는 것. %L 은 경로를 담은 파일), hide-when("HKCU\\key\\value" 또는 "HKLM\\...": 그 값이 있는 동안 Windows 11 항목을 숨김) - 탐색기 우클릭 메뉴의 항목: 탐색기의 우클릭 메뉴 참조
[other-copy.ID]msix(앱 패키지의 식별 이름)와 publisher, 또는 upgrade-code(다른 MSI 의 것), action(warn 기본, refuse), message 또는 message-xx([1] 이 제품, [2] 찾은 사본) - 다른 방식으로 설치된 같은 프로그램을 처음 설치하기 전에 찾는다: 다른 사본 참조. MSI 전용
[protocol.ID]name(스킴, 소문자), target(.exe 를 가리키는 file:ID), description, args(기본 "%1")
[com.ID]file(.exe 나 .dll 의 file:ID), class({GUID}), description, threading(sta 기본, mta, both, neutral; DLL), args(프로그램), prog-id, app-id({GUID}), surrogate(dllhost 에서 도는 DLL), typelib({LIBID}), typelib-version(기본 "1.0"), typelib-file(기본: 서버), msi-only - COM 클래스, COM 클래스 참고
[handler.ID]kind(thumbnail, preview, property), class([com.*] 의 DLL 클래스), types([".ext", ...]), description(미리 보기 처리기의 이름), msi-only - 탐색기 처리기, 탐색기 처리기 참고
[font.ID]file(path = "$(Fonts)" 인 dir 에 드는 파일의 file:ID), title
[permission.ID]target(dir:ID, file:ID, registry:ID), sddl
[env.ID]name, value, mode(set, append, prepend), keep, feature, when
[copy.ID]source(file:ID), dir, name(기본: 원본 파일 이름)
[move.ID]from(dir ID, 또는 property:NAME), name(와일드카드), to(dir ID), new-name, copy(true: 원본은 남음), with(file:ID) 또는 feature - 설치할 때 옮기거나 복사하는 파일. MSI 전용
[odbc-driver.ID]file(드라이버 .dll 의 file:ID), description, setup(설정 .dll 의 file:ID), attributes(["Key=Value", ...]) - ODBC 드라이버. MSI 전용
[odbc-source.ID]driver([odbc-driver] ID) 또는 driver-name(with 과 함께), name, registration(machine, user), attributes - ODBC 데이터 원본. MSI 전용
[publish.ID]file(file:ID: 그 구성 요소를 게시), category({GUID}), qualifier, data - 다른 프로그램이 MsiProvideQualifiedComponent 로 찾는 구성 요소. MSI 전용
[merge.ID]source(.msm 병합 모듈), dir(모듈의 뿌리 폴더가 갈 곳), feature, config(설정할 수 있는 모듈에 ["이름=값", ...]). MSIX 에서는 파일과 레지스트리 값만
[module]name(모듈의 ID: 영문자·숫자·_, 35자 이내), manufacturer, version, arch, id(모듈의 GUID, 모든 판에서 그대로), language(기본 neutral, en-US, ko-KR), compress(none, mszip, mszip:0..mszip:9) - [package] 대신: 병합 모듈, 병합 모듈 만들기 참고
[ui]install-dir(dir ID; 기본 INSTALLDIR), banner(.bmp), launch(file:ID), launch-args, launch-checked, save-log(기본 true), details(기본 true: 진행 페이지가 하는 일을 보인다), embedded(.dll: 작성자 자신의 UI 처리기, MSI 전용), languages(영어에 덧붙일 언어, 예 ["ko"]), license-xx, name-xx, font-xx, langid-xx - 여러 언어 참고
[ui-text.ID]text 또는 text-xx - 내장 대화창 문구 하나를 바꾼다
[dialog.ID]after(내장 페이지 또는 다른 [dialog.*]), title, description, title-xx, description-xx
[dialog-control.ID]dialog, type(text, checkbox, edit, radio, combo, listbox, bitmap), x, y, width, height, text, property, values, labels, image(.bmp, bitmap 전용), text-xx, labels-xx
[billboard.ID]image(.bmp), feature, order (MSI 전용; 그 feature 가 설치되는 동안 진행 화면에 보임)
[shortcut.ID]dir(dir ID, 또는 Programs, Desktop, StartMenu, Startup), name, target(file:ID), args, description, working-dir(dir ID), icon(.ico), when, advertise(true: 바로가기 참고, MSI 전용)
[msix]identity-name, publisher, display-name, display-name-xx, publisher-display-name, publisher-display-name-xx, min-version, capabilities(이름들), file-system-virtualization, registry-virtualization(기본 true), main-package, main-publisher, modification, framework, external-location, symbols(.pdb 파일), language-packs(기본 true), appinstaller-uri, package-uri, update-hours(0-255, 기본 24), update-prompt, update-blocks, update-background - MSIX 패키지 참고
[msix-app.ID]executable([file.*] ID), display-name, display-name-xx, description, description-xx, logo-150, logo-44, store-logo(저마다 .scale-NNN 변형을 둘 수 있다), background-color(기본 transparent, #RRGGBB), hidden(시작 메뉴 항목 없음)
[msix-dependency.ID]name, publisher, min-version - MSIX 가 필요로 하는 프레임워크 패키지 - MSIX 전용
[winget]identifier(Publisher.Package), installer-url(패키지를 내려받을 주소), license, short-description, short-description-xx, publisher-url, package-url, license-url, moniker, tags(16개까지) - build --winget DIR 가 읽는다
[msix-psf]files(Package Support Framework 의 파일이 든 폴더), config(그 config.json), launcher(기본 PsfLauncher64.exe, x86 은 PsfLauncher32.exe) - MSIX 전용
[msix-extension.ID]kind(alias: alias; startup-task: task-id, display-name, enabled; firewall: direction(in, out), protocol(tcp, udp), ports(8080 또는 8000-8100), profile(all, domain, private, public), file(기본은 앱의 프로그램); com-server: file(.exe 나 .dll), class({GUID}), display-name, args(.exe), threading(sta 기본, mta, both, neutral; .dll); toast: class, file(기본은 앱의 프로그램), args(기본 -ToastActivated); context-menu: file(.dll), class, types([".txt", "*"]), verb(기본은 표 ID; 영문자와 숫자만), threading; app-extension: name(제공하는 범주), extension-id(기본은 표 ID), public-folder, display-name, description; app-extension-host: names(불러오는 범주들); app-service: name, entry-point(서비스를 맡는 활성화 가능 클래스), multiple-instances; share-target: types([".txt", "*"]), formats(["Text", "Uri"]; 둘 중 하나는 있어야 함), description; autoplay: action, content-events, device-events(하나는 있어야 함), verb(기본은 표 ID), class({GUID}; device-events 에 필요), args(내용 이벤트에서는 class 가 없을 때만), display-name), app([msix-app.*] ID; 기본은 첫째) - MSIX 전용
[chain]name, manufacturer, version, arch(설치 프로그램 자신의 것: x64, x86, arm64), elevate(기본 true), languages(설치 프로그램 자신의 문구에 쓸, 언어팩이 있는 언어 코드) - 체인 원본: 설치 프로그램 하나에 여러 패키지 참고
[chain-package.ID]source(.msi, .msp, .msix, .msixbundle), properties(msiexec 속성; MSIX 에는 없음), vital(기본 true)

dir 경로의 $(기준)은 다른 dir ID 이거나 Windows 폴더다: $(ProgramFiles)(x64/arm64 는 64비트, x86 은 32비트), $(ProgramFiles(x86)), $(CommonProgramFiles), $(APPDATA), $(LOCALAPPDATA), $(ProgramData), $(TEMP), $(SystemRoot), 그리고 환경 변수가 없는 폴더 $(StartMenu), $(Programs), $(Desktop), $(Startup), $(System), $(Fonts) - Windows 이름 참고. 폴더 하나만 쓴 경로(path = "$(Fonts)")는 그 폴더 바로 안에 드는 파일에 쓴다.

ID 는 [A-Za-z_][A-Za-z0-9_]* 꼴이고(최대 72자, feature 는 38자) dir·파일·feature 를 통틀어 서로 달라야 한다.

MSIX 패키지#

출력 이름이 .msix 로 끝나면 같은 원본이 MSIX 패키지가 된다: 완전 신뢰로 도는 데스크톱 앱 하나, 아키텍처 하나. MSIX 에만 필요한 것은 표 두 개가 더 말한다:

[msix]
identity-name = "Example.App"               # 3~50자: A-Z a-z 0-9 . -
publisher = "C=KR, O=Example, CN=Example"   # 서명 인증서의 주체, 마지막 부분부터
publisher-display-name = "Example"          # 기본: [package] manufacturer
min-version = "10.0.17763.0"                # 설치되는 가장 오래된 Windows(이것이 기본값)

[msix-app.Main]
executable = "MainExe"                      # 앱을 시작하는 [file.*]
display-name = "Example App"                # 기본: [package] name
description = "An example"                  # 기본: 표시 이름
logo-150 = "assets/Square150x150.png"       # PNG, 150x150
logo-44 = "assets/Square44x44.png"          # PNG, 44x44
store-logo = "assets/StoreLogo.png"         # PNG, 50x50

설치한 프로그램으로 등록하기: [action.ID]#

[action.Tip]
run = "file:MainExe"      # 이 패키지가 설치하는 .exe
do = "--register"         # 파일을 설치한 뒤, 그리고 복구할 때 다시 실행
undo = "--unregister"     # 제거할 때, 파일을 지우기 전에 실행

rubrapack 은 이 한 쌍을 관리자 권한의 지연(deferred) 동작과 그 되돌림(rollback) 짝으로 바꾼다. 그래서 제거와 업그레이드는 전부 되거나 전혀 되지 않는다: 뒤에서 무엇이 실패하면(또는 do/undo 자체가 0 아닌 값으로 끝나면) 파일이 되돌아오고 다른 명령이 등록을 되살린다. 두 명령은 두 번 실행해도 안전해야 하고, 아무것도 묻지 않고 끝나야 한다 - 창 없이 실행되고 사용자를 기다리는 것은 없다. 인자는 쓴 그대로 넘어간다(서식 문자열이 아니다).

뒤에서 무엇이 실패하면 Windows Installer 가 설치를 되돌리고, 위 명령들이 등록을 되살린다. 그 되돌리기마저 잘못되면 사용자에게 보고서가 남는다: 바탕 화면(없으면 다운로드)의 <제품> setup report <날짜와 시각>.txt 로, 사용자의 언어로 무엇을 하던 중이었는지, 무엇이 원래와 다른지, 이제 무엇을 하면 되는지, 설치 로그가 어디 있는지를 적는다. 보는 것은 업그레이드가 바꾸던 옛 판(다시 설치돼 있는가)과 각 check 다:

[action.Tip]
run = "file:MainExe"
do = "--register"
undo = "--unregister"
check = "--status"        # --register 가 만든 것이 있으면 0 으로 끝난다

되돌린 뒤에 check 는 제품이 원래 있었으면(업그레이드, 복구, 제거) 0 으로, 처음 설치가 실패한 것이면 0 이 아닌 값으로 끝나야 한다. 모두 원래대로면 아무것도 쓰지 않는다. check 는 서식 문자열이다([INSTALLDIR] 은 설치 폴더).

프로그램이나 DLL 실행: 동작 하나 [action.ID]#

위의 do/undo 쌍 말고, [action.*] 하나로 한 가지를 한 번 실행한다:

[action.Warm]
run = "file:MainExe"       # 이 패키지가 설치하는 .exe
args = "--warm-cache"      # 서식 문자열([INSTALLDIR] 등이 채워진다)
phase = "deferred"         # immediate | deferred(기본) | commit | rollback
when = "NOT Installed"     # 조건(선택)
before = "InstallFinalize" # 자리: 표준 동작 앞/뒤, 또는 sequence = <n>

[action.Ping]
dll = "file:HelperDll"     # 이 패키지가 설치하는 .dll
entry = "PingServer"       # 부를 함수
phase = "deferred"

[action.Clean]
binary = "clean.dll"       # 설치하지 않고 패키지에 담는 .dll
entry = "CleanUp"
phase = "commit"
as = "system"              # system | user(컴퓨터 단위 패키지의 지연 동작은 기본 SYSTEM)
data = "[INSTALLDIR]"      # 지연 DLL 이 CustomActionData 로 읽는 값(서식 문자열)

레지스트리 값: [registry.ID]#

[registry.InstallDir]
root = "HKLM"
key = 'SOFTWARE\Example'          # 리터럴 문자열은 백슬래시를 그대로 둔다
name = "InstallDir"               # 없으면 키의 기본값
value = "[INSTALLDIR]"            # MSI 서식 문자열: [PROPERTY], [#FileID], "[" 는 [\[]

type 은 string(기본), expand, dword(0~0xFFFFFFFF 정수), qword(정수, 또는 "0x" 와 16자 이하의 16진수), binary(16진수), multi(문자열 배열)이다. Windows Installer 는 REG_QWORD 를 스스로 쓰지 못한다. 그래서 rubrapack 이 작은 도우미 DLL 을 패키지에 넣어 쓰게 하고, 설치가 실패하면 그 DLL 이 이전 값을 되살린다. 값 하나가 구성 요소(component) 하나이고 제거할 때 지워진다(keep = true 면 남는다). with = "file:ID" 는 대신 그 파일의 구성 요소에 넣는다. 64비트 패키지에서 값은 64비트 레지스트리 보기로 가고, view = "32" 는 32비트 보기에 쓴다. remove = true(value 없이)는 설치 중에 이름 붙은 값을 - name 이 없으면 키 전체를 - 지운다. on = "uninstall" 을 더하면(name 없이) 키와 그 아래 전부를 제거할 때 지운다: 제거할 때 싹 지우기.

바로가기: [shortcut.ID]#

[dir.Menu]
path = "$(Programs)/Example"         # 시작 메뉴 > Example

[shortcut.Settings]
dir = "Menu"
name = "Example settings"         # ".lnk" 는 붙여 준다
target = "file:MainExe"
args = "--settings \"[INSTALLDIR]\""

바로가기는 대상 파일(과 그 feature)에 딸린다. 바로가기 때문에 만든 폴더는 제거할 때 지운다. 컴퓨터 전체 패키지에서 Programs 와 Desktop 은 모든 사용자의 시작 메뉴와 공용 바탕화면이다.

advertise = true 는 광고(필요할 때 설치) 바로가기를 만든다. 대상은 대상 파일의 feature 이고, Windows 는 바로가기를 처음 쓸 때 그 feature 를 설치하며 빠진 것이 있으면 고친다. 광고 바로가기에는 icon 이 필요하고(설치 전에 Windows 가 보여 준다) when 과 함께 쓰지 않는다. feature 에 advertise = "disallow" 를 주면 그 feature 는 광고되지 않는다(그런 feature 를 가리키는 광고 바로가기는 거절된다). 광고 바로가기는 MSI 를 위한 것이다. MSIX 는 패키지 전체를 설치하므로 거기서는 advertise 가 거절된다.

MSIX 에서 바로가기는 앱을 시작한다(대상은 [msix-app.*] 의 실행 파일이어야 한다): Programs 나 StartMenu 의 것은 그 앱 자신의 시작 메뉴 항목이고(args 없음), Desktop 의 것은 매니페스트에 적히며 min-version = "10.0.19645.0" 이상이 필요하다(RP1614). 다른 폴더, working-dir, args 속 [...] 는 거기서 오류다(RP1613). Startup 은 시작 작업으로 쓴다.

파일 형식과 링크: [assoc.ID], [protocol.ID]#

[assoc.Doc]
extension = ".exdoc"
prog-id = "Example.Document"      # 같은 prog-id 의 표들은 한 종류의 문서를 뜻한다
description = "Example document"
target = "file:MainExe"
args = "--open \"%1\""            # 기본값은 "%1"

[protocol.Link]
name = "example"                  # example:... 가 프로그램을 연다
target = "file:MainExe"

MSI 에서는 프로그램의 구성 요소에 든 HKEY_CLASSES_ROOT 아래 레지스트리 값이다: 확장자의 기본값은 prog-id, prog-id 에는 설명과 DefaultIcon(icon, 없으면 프로그램의 첫 아이콘), shell\open\command 가 있고, 스킴에는 URL Protocol 이 붙는다. HKEY_CLASSES_ROOT 는 설치 방식을 따른다: 컴퓨터 전체면 HKLM\Software\Classes, 사용자별이면 HKCU\Software\Classes 에 들고, 제거하면 없어진다. 이 프로그램만 맡겠다는 형식은 곧바로 이 프로그램으로 열리고, 사용자가 다른 프로그램을 골라 두었으면 Windows 는 그 선택을 지킨다.

MSIX 에서는 대상 실행 파일을 가진 앱의 매니페스트에 들어가고(파일 형식 연결, 프로토콜) args 는 평문이어야 한다. icon 은 쓰지 않는다. logo 는 파일 형식에 자기 그림을 준다: 크기는 자유인 정사각형 PNG 이고(탐색기가 크기를 맞춘다), prog-id 마다 한 번 패키지에 들어간다. logo 가 없으면 앱의 로고가 파일 형식을 나타낸다. 로고 이름의 확장자 앞에 .scale-100, .scale-125, .scale-150, .scale-200, .scale-400 을 붙인 파일들은 앱의 로고처럼 그 배율의 그림으로 쓰고, .targetsize-16, -24, -32, -48, -256 을 붙인 파일(N x N 픽셀)은 그 크기의 그림으로 쓴다. 탐색기의 작은 아이콘이 이것을 쓴다. .targetsize-16_altform-unplated 처럼 붙인 파일은 같은 그림을 타일 바탕 없이 보일 때 쓴다(RP1608). 이런 파일이 있으면 로고 파일 자체는 없어도 되고, 있으면 배율 100 그림이 된다. prog-id 를 같이 쓰는 표들은 같은 logo 를 주거나 모두 주지 않는다(RP1316). MSI 는 logo 를 쓰지 않으므로(MSI 의 아이콘은 icon) 한 소스로 둘 다 빌드한다.

파일 형식에는 무엇이 들었는지, 그리고 프로그램이 그 형식을 가져갈지도 적을 수 있다:

[assoc.Picture]
extension = ".png"
prog-id = "Example.Picture"
target = "file:MainExe"
content-type = "image/png"        # 그 형식의 미디어 형식
perceived-type = "image"          # image, text, audio, video, compressed, document, system, application
default = false                   # "연결 프로그램"에만 올리고 형식을 가져가지 않는다

모든 [assoc.*] 는 자기 prog-id 를 확장자의 OpenWithProgids 에 올려서 "연결 프로그램"에 나오게 한다. default = false 면 그것만 한다(확장자의 기본값은 건드리지 않는다). MSIX 에서는 content-type 이 매니페스트에 들어가고, perceived-type 과 default 는 MSI 의 것이다.

탐색기의 우클릭 메뉴: [menu.ID]#

[menu.Convert]
on = [".png", ".jpg", "folder"]   # 어디에 나오는가
text = "Convert with Example"
text-ko = "Example 로 변환"        # 언어별 문구
target = "file:MainExe"
args = "--convert \"%1\""         # "%1" 은 고른 것의 경로. 기본값은 "%1" 하나
icon = "file:MainExe"             # 기본: 프로그램 자신의 아이콘

[menu.Tools]                      # target 이 없으면 하위 메뉴
on = "*"
text = "Example tools"

[menu.Checksum]
parent = "Tools"                  # 그 하위 메뉴의 항목. 하위 메뉴가 나오는 곳에 나온다
text = "Checksum"
target = "file:MainExe"
args = "--sum \"%1\""
multi = "single"                  # 하나만 골랐을 때만

항목은 우클릭한 것을 넘겨 패키지의 프로그램을 시작한다. on 은 자리 하나 또는 목록이다: 파일 형식 (".png"), "*"(모든 파일), "folder", "background"(열린 폴더의 빈 곳: 이때 %1 은 그 폴더), "drive", "assoc:ID"(어느 [assoc.*] 의 파일 형식, 그 prog-id 아래). multi 는 여러 개를 골랐을 때의 뜻이다: "each"(기본: 항목마다 한 번 실행), "one"(전부를 넘겨 한 번 실행: args 에 %* 를 쓰면 모든 경로), "single"(하나를 골랐을 때만 항목이 나온다). extended = true 면 Shift 를 누른 채일 때만 나온다. 하위 메뉴는 한 단계까지다. 하위 메뉴에 들지 않은 항목의 ID 는 Windows 11 메뉴에서 그 항목의 이름이 되는데, 거기서는 영문자와 숫자만 된다(Open_2 가 아니라 Open2). 항목은 ID 순서로 놓인다.

같은 표가 Windows 의 두 메뉴에 다 쓰인다:

Windows 11 에서 알아 둘 것: 항목은 설치가 끝나고 몇 초 뒤에 나타난다. 한 패키지의 항목이 같은 종류의 대상에 둘 이상이면 Windows 가 제품 이름을 단 항목 하나 아래로 묶는다(hide-when 이 하나만 남기고 숨겨도 묶음은 남는다). MSIX 에서는 컴퓨터의 어떤 프로그램도 등록하지 않은 파일 형식(Windows 가 "RPX 파일" 처럼만 부르는 확장자)의 항목은 나오지 않았고, 같은 항목이 .txt 에서는 나왔다. "drive" 와 windows11 = false 인 항목은 옛 메뉴에만 나온다. 메뉴 부품이 무엇을 하는지 보려면 HKCU\Software\rubrapack 아래에 MenuLog 값(파일 경로)을 둔다: 고른 항목과 시작한 명령이 그 파일에 덧붙는다.

Windows 11 메뉴에만 해당하는 키가 둘 있다:

[menu.Open]
on = [".png", ".jpg"]
text = "Open with Example"
target = "file:MainExe"
args = "--open %*"
multi = "one"
list = "--paths-from=%L"          # 경로가 명령줄에 다 들어가지 않을 때
hide-when = "HKCU\\Software\\Example\\MenuRegistered"

COM 클래스: [com.ID]#

[com.Widget]
file = "file:WidgetDll"           # 이 패키지의 .exe 나 .dll 이 클래스를 제공한다
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D35}"
description = "Example widget"
prog-id = "Example.Widget"        # 스크립트가 부르는 이름
threading = "both"                # DLL 의 아파트: sta(기본), mta, both, neutral
app-id = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D36}"
surrogate = true                  # 호출한 프로세스 밖 dllhost 에서 돌 수 있다
typelib = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D37}"
typelib-version = "1.2"           # 16진 숫자의 major.minor; typelib-file 로 다른 파일을 가리킨다

[com.Server]
file = "file:MainExe"             # 프로그램: LocalServer32, args 를 붙여 시작한다
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D38}"
args = "-Embedding"

MSI 에서는 파일 형식처럼 서버의 구성 요소에 든 HKEY_CLASSES_ROOT 아래 레지스트리 값이 된다: 설명을 단 CLSID\{class}, InprocServer32(DLL 과 ThreadingModel) 또는 LocalServer32(따옴표 친 프로그램 뒤에 args), prog-id 가 있으면 CLSID\{class}\ProgID 와 <prog-id>\CLSID, app-id(또는 app-id 가 없을 때 클래스 ID 를 쓰는 surrogate)가 있으면 클래스의 AppID 값과 AppID\{app-id}(서로게이트면 DllSurrogate 도), typelib 이 있으면 CLSID\{class}\TypeLib 와 TypeLib\{typelib}\<version>(패키지 아키텍처에 따라 0\win64 또는 0\win32, FLAGS, HELPDIR). 제거하면 함께 사라진다. 값은 Class·ProgId·TypeLib·AppId 표가 아니라 Registry 행으로 쓴다 - Microsoft 도구가 광고하지 않는 클래스를 쓰는 방식이다. 그 표들의 클래스는 Windows Installer 가 광고된 것(처음 쓸 때 설치)으로 등록하기 때문이다. 사용자별 설치면 HKCU\Software\Classes 에 들어간다. Microsoft 검증(ICE33)은 표를 쓰라며 이런 행에 경고를 내는데, [com] 에서는 예상된 경고다. 클래스 ID, prog-id, ID 는 각각 한 번만 쓴다(RP1301). threading 과 surrogate 는 DLL 의, args 는 프로그램의 것이다(RP1316).

MSIX 에서 [com] 은 패키지 COM 카탈로그의 클래스(com:ComServer: 프로그램은 ExeServer, DLL 은 SurrogateServer)가 되고 prog-id 는 com:ProgId 가 된다. app-id 와 typelib 은 MSI 에만 들어가며 경고가 그렇게 알린다(RP1612).

탐색기 처리기: [handler.ID]#

[handler.Thumbs]
kind = "thumbnail"                # 탐색기가 파일에 보이는 그림
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D41}"   # 패키지 DLL 의 [com.*] 클래스
types = [".exdoc"]

[handler.Preview]
kind = "preview"                  # 미리 보기 창에 보이는 것
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D43}"
types = [".exdoc"]
description = "Example document preview"
msi-only = true

[handler.Props]
kind = "property"                 # 탐색기와 검색이 쓰는 파일 속성(제목, 작성자 ...)
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D42}"
types = [".exdoc"]
msi-only = true

처리기는 패키지 DLL 안의 COM 클래스로 [com.*](threading 포함)에 적고, [handler] 는 그것이 어떤 파일 형식을 맡는지 Windows 에 알린다. MSI 에서: 썸네일과 미리 보기 처리기는 HKEY_CLASSES_ROOT 아래 형식마다의 ShellEx 값이다(썸네일 {E357FCCD-...}, 미리 보기 {8895B1C6-...}). 미리 보기 처리기는 PreviewHandlers 목록에도 오르고, 그 클래스는 Windows 미리 보기 호스트(prevhost.exe, 패키지 아키텍처에 따라 64비트나 32비트)의 AppID 를 받으므로 그 [com] 에는 app-id 와 surrogate 를 쓰지 않는다. 속성 처리기는 HKLM 의 PropertySystem\PropertyHandlers 아래이며 Windows 가 컴퓨터 단위로만 읽으므로 패키지는 scope = "machine" 이어야 한다. 한 형식에는 종류마다 처리기 하나다(RP1301).

MSIX 에서 썸네일·미리 보기 처리기는 패키지의 파일 형식 연결(desktop2:ThumbnailHandler, desktop2:DesktopPreviewHandler, 클래스는 패키지 COM 카탈로그)에 들어간다. 형식 묶음마다 연결 하나이며, 그 형식을 여는 [assoc] 가 있으면 거기에 들어간다. 패키지가 설치되어 있는 동안 탐색기가 쓴다. 패키지는 클래스를 대리 프로세스(dllhost)에서 돌리므로 미리 보기 처리기의 클래스에는 threading = "sta" 를 준다. 다른 모델이면 그 창이 메시지 루프 없는 스레드에 놓여 빈 채로 남을 수 있다(경고, RP1612). 패키지에 든 속성 처리기는 Windows 11 에서 탐색기에 값을 주지 못했다(같은 클래스를 MSI 로 설치하면 준다). 그래서 MSIX 는 그 종류를 거절하고(RP1612) msi-only = true 가 MSI 에만 남긴다.

MSIX 전용: [msix-extension.ID]#

[msix-extension.Cli]
kind = "alias"
alias = "example.exe"             # 콘솔에서 이 이름을 치면 앱이 시작된다

[msix-extension.Boot]
kind = "startup-task"             # 앱을 한 번 실행한 뒤로 Windows 와 함께 시작된다
display-name = "Example"          # 작업 관리자에 보이는 이름; task-id 기본값은 표 ID
enabled = true
[msix-extension.Web]
kind = "firewall"                 # 패키지가 설치돼 있는 동안의 Windows 방화벽 규칙
direction = "in"
protocol = "tcp"
ports = "8080"
profile = "private"               # 기본값 "all". 다른 프로그램이면 file = "file:ID"

MSI 빌드는 이것들을 뺀다. MSIX 에서 글꼴([font.*])은 패키지의 Fonts 폴더에서 다른 앱과 나눠 쓰며(uap4:SharedFonts) title 은 쓰지 않는다.

[msix-extension.Server]
kind = "com-server"               # 프로그램(또는 DLL)이 제공하는 COM 클래스
file = "file:MainExe"
class = "{7A1B2C3D-4E5F-4A6B-8C7D-9E0F1A2B3C41}"
args = "-Embedding"

[msix-extension.Toast]
kind = "toast"                    # 앱의 알림을 누르면 이 클래스로 앱이 시작된다
class = "{7A1B2C3D-4E5F-4A6B-8C7D-9E0F1A2B3C43}"

[msix-extension.Menu]
kind = "context-menu"             # DLL(IExplorerCommand)이 처리하는 탐색기 오른쪽 메뉴 항목
file = "file:MenuDll"
class = "{7A1B2C3D-4E5F-4A6B-8C7D-9E0F1A2B3C44}"
types = [".txt", "*"]             # 파일 형식. "*" 는 모든 파일

이것들은 앱의 com:ComServer(프로그램이면 ExeServer, DLL 이면 SurrogateServer), 프로그램을 그 클래스와 args 로 등록하는 desktop:ToastNotificationActivation, 그리고 DLL 을 클래스로 쓰는 desktop4:FileExplorerContextMenus 가 된다. 패키지가 설치돼 있는 동안 Windows 는 그 클래스들을 패키지 COM 목록에 올린다. 오른쪽 메뉴는 DLL 이 그 항목을 구현해야(IExplorerCommand) 보인다. rubrapack 은 등록할 뿐 구현하지 않는다.

[msix-extension.Plugin]
kind = "app-extension"            # 이 패키지가 "name" 범주의 확장을 제공한다
name = "com.example.editor.plugin"
public-folder = "Public"          # 호스트가 읽을 수 있는 파일. extension-id 의 기본은 표 ID

[msix-extension.Plugins]
kind = "app-extension-host"       # 이 앱이 이 범주들의 확장을 불러온다
names = ["com.example.editor.plugin"]

[msix-extension.Sync]
kind = "app-service"              # 다른 앱이 이름으로 부른다. entry-point 는 서비스를 맡는 클래스
name = "com.example.sync"
entry-point = "Example.SyncService"

[msix-extension.Share]
kind = "share-target"             # Windows 공유 창에 앱이 나온다(min-version 10.0.19041.0)
types = [".png", ".jpg"]          # "*" 는 모든 파일
formats = ["Text", "Uri"]

[msix-extension.Import]
kind = "autoplay"                 # 카메라 카드나 장치를 연결하면 나온다
action = "Import pictures"
content-events = ["ShowPicturesOnArrival"]
args = "--import"                 # 또는 class = "{GUID}": 프로그램의 IDropTarget 클래스

이들은 앱 안의 uap3:AppExtension, uap3:AppExtensionHost, uap:AppService, uap:ShareTarget, desktop3:AutoPlayHandler 가 된다. Windows 는 패키지가 설치된 동안 이들을 등록한다. 불렸을 때 하는 일(확장의 공개 폴더 읽기, 앱 서비스 응답, 공유된 데이터 받기, 가져오기)은 프로그램 자신의 코드다. device-events 는 장치를 처리하는 클래스(class, 그 IHWEventHandler)를 밝히고, 그 클래스는 패키지의 com-server 표가 제공한다. 앱 하나에 공유 대상은 하나다.

[service.*] 는 MSIX 에 패키지 서비스(desktop6:Service, 첫째 앱 안, 권한 packagedServices 와 account = "LocalSystem" 이면 localSystemServices)로 들어간다: 이름·시작·계정은 MSI 와 같고 args 는 평문이어야 한다. display-name 과 description 은 쓰지 않고, 서비스는 start-on-install 이 아니라 start 대로 시작한다. min-version = "10.0.19041.0" 이상이 필요하다(RP1614). 패키지를 지우면 서비스가 멈추고 지워지며, 방화벽 규칙도 함께 사라진다.

파일 옮기기, ODBC, 게시 구성 요소#

이 표들은 MSIX 에 대응이 없다. MSIX 를 만들 때는 빠진다.

[move.OldLogs]
from = "LegacyDir"        # dir ID, 또는 property:NAME([search] 가 찾은 폴더)
name = "*.log"            # 파일, 와일드카드 가능
to = "LogsDir"            # 이 패키지의 dir ID
# new-name = "one.log"    # 파일 하나만 이름 바꾸기(와일드카드 불가); copy = true 면 원본은 남는다

[odbc-driver.Mydb]
file = "file:DriverDll"
description = "My Database Driver"
attributes = ["APILevel=2", "ConnectFunctions=YYN"]

[odbc-source.Sample]
driver = "Mydb"           # 또는 다른 방식으로 설치된 드라이버면 driver-name = "..." 와 with = "file:ID"
name = "My Sample Data"
attributes = ["Description=a sample data source"]

[publish.Plugin]
file = "file:PluginDll"   # 이 DLL 의 구성 요소를 게시한다
category = "{8D1E2F30-...}"
qualifier = "image/png"

어셈블리: assembly#

[file.*] DLL 에 assembly 를 주면 Windows Installer 가 그것을 어셈블리로 설치한다(MsiAssembly·MsiAssemblyName 표와, 그것을 게시하는 동작):

[file.Lib]                       # 전역 어셈블리 캐시(GAC)에 들어가는 .NET 어셈블리
dir = "INSTALLDIR"
source = "bin/Contoso.Lib.dll"
assembly = "dotnet"

[file.Native]                    # 프로그램 전용 Win32 어셈블리
dir = "INSTALLDIR"
source = "bin/native.dll"
assembly = "win32"
manifest = "file:NativeManifest"
assembly-for = "file:App"

[file.NativeManifest]
dir = "INSTALLDIR"
source = "bin/Contoso.Native.manifest"

지우기와 복사: [remove.ID], [copy.ID]#

[remove.ID] 는 dir 에서 name 에 맞는 파일을 지운다 - 예를 들어 옛 판이 남긴 *.log 를 on = "install" 로, 프로그램이 실행 중에 만드는 파일을 on = "uninstall" 로. name 이 없으면 비어 있는 폴더 자체를 지운다. 설치가 실패하면 지운 파일은 되돌아온다. [copy.ID] 는 이 패키지의 파일을 다른 폴더에 한 벌 더 설치하고, 원본과 함께 들어오고 함께 나간다. 설치 전부터 있던 폴더는 절대 지우지 않는다.

업그레이드는 옛 판도 지우고, 그때 옛 판의 on = "uninstall" 행도 돈다. upgrade = false 는 그 행을 거기서 뺀다: 파일은 제품(또는 그 기능)을 정말 지울 때만 지워진다 - 새 판이 그대로 쓰는 파일이나, 옛 판을 아직 돌리는 프로그램이 다시 읽을 수 있는 파일에 쓴다. RemoveFile 표 대신 rubrapack 의 도우미 DLL 이 한다: 파일을 백업 폴더(그 볼륨의 Config.Msi, 사용자별 패키지는 임시 폴더)로 옮기고, 제거가 실패하면 되돌리고, 성공하면 지운다. 프로그램이 쥐고 있는 파일은 다음 재시작 때 지워진다. 업그레이드는 옛 판 자신의 행을 돌리므로, 이 키를 넣고 만든 첫 판부터 효과가 있다.

제거할 때 싹 지우기: recursive, on = "uninstall"#

제거는 패키지가 설치한 것을 지운다. 프로그램이 쓰이는 동안 스스로 만든 것 - 옆에 둔 설정 파일, AppData 아래 폴더, HKCU\Software 아래 키 - 은 남고, 파일이 하나라도 남으면 설치 폴더도 남는다. 그것까지 지우려면 자리를 적는다:

[remove.Everything]
dir = "INSTALLDIR"
on = "uninstall"
recursive = true            # 폴더와 그 안의 모든 파일·폴더

[dir.Settings]
path = "$(APPDATA)/Example/Tip"

[remove.Settings]
dir = "Settings"
on = "uninstall"
recursive = true

[registry.Settings]
root = "HKCU"
key = 'Software\Example\Tip'
remove = true
on = "uninstall"            # 키와 그 값, 그 아래 모든 키

모든 사용자의 것: users = "all"#

여러 사람이 쓴 컴퓨터 단위 제품은 사람마다 설정을 남긴다. 싹 지우는 AppData·LocalAppData 의 폴더나 HKCU 키에 users = "all" 을 적으면, 제거가 이 컴퓨터의 다른 사용자 모두의 같은 폴더·키도 지운다 - 제거하는 사람이 그러라고 할 때만:

[remove.Settings]
dir = "Settings"            # $(APPDATA)/Example/Tip
on = "uninstall"
recursive = true
users = "all"

[registry.Settings]
root = "HKCU"
key = 'Software\Example\Tip'
remove = true
on = "uninstall"
users = "all"

환경 변수: [env.ID]#

시스템(컴퓨터 전체) 변수다. mode = "set"(기본)은 값을 바꾸고, append/prepend 는 기존 값 끝에 ;value 를, 앞에 value; 를 붙인다. 제거는 정확히 그만큼 되돌린다: set 한 변수는 지우고, 붙인 부분은 떼어 내고 나머지는 둔다. keep = true 면 남긴다.

INI 파일: [ini.ID]#

mode = "set"(기본)은 파일의 [section] 에 key=value 를 쓰고, add 는 쉼표 목록에 값을 덧붙이며 (a 가 a,b 로), remove 는 설치 중에 키를 지운다. 제거하면 set 과 add 가 쓴 것을 뗀다. remove 는 파일 설치보다 먼저 실행되므로 옛 판이 남긴 INI 파일을 위한 것이다.

설치 프로그램 하나에 여러 패키지: [chain]#

[chain] 이 있는 원본은 패키지 대신 설치 프로그램을 만든다: rubrapack build suite.toml -o setup.exe. 설치 프로그램은 [chain-package.*] 패키지들을 원본에 적힌 차례로, 저마다의 SHA-256 과 함께 담는다. 실행하면 모두 확인한 뒤 Windows Installer 로 하나씩 차례로 설치한다. 이미 설치된 패키지(ProductCode 로 본다)는 건너뛴다. 모든 패키지가 설치된 상태에서 다시 실행하면 설치 프로그램이 할 일을 묻는다 - 복구(Repair), 제거(Remove), 취소(Cancel). /quiet 과 /passive 에서는 묻지 않는다. vital 패키지(기본)가 실패하면 체인이 멈추고 그 패키지의 오류를 돌려준다. 그 패키지는 스스로 되돌리고, 앞의 패키지들은 남는다. properties 는 msiexec 명령줄에서처럼 그 패키지에 건넨다.

병합 모듈: [merge.ID]#

병합 모듈(.msm)은 다른 회사가 자기 런타임이나 라이브러리를 위해 내놓는 설치 프로그램 조각이다. [merge.ID] 는 그 표들을 패키지에 옮겨 담는다: 모듈의 파일, 구성 요소, 레지스트리 값, 모듈 자신의 동작. 모듈의 뿌리 폴더는 dir 이 되고, 구성 요소는 feature(기본: 그 폴더의 기능, 없으면 Main)에 들어가며, 모듈의 캐비닛은 따로 두 번째 캐비닛으로 들어간다. 모듈의 표가 필요로 하는데 패키지에 없는 표준 동작(예를 들어 WriteRegistryValues)은 늘 쓰는 자리에 더한다. UTF-8 이 아닌 코드 페이지에 ASCII 밖의 글자가 있는 모듈은 합치지 않는다(RP1517). MSIX 에서 모듈은 파일(dir 아래, 또는 시스템 폴더 같은 표준 폴더로 가는 것은 가상 파일 시스템 안)과 레지스트리 값(패키지의 하이브 안)을 준다. 그 이상 - 사용자 지정 동작, 구성 요소의 조건, 환경 변수나 INI 항목, 서비스, 글꼴, COM 등록 - 이 필요한 모듈이나 설치 때 채우는 값은 거기서 거절한다(RP1517. msi-only = true 는 MSI 에만 둔다).

합친 모듈의 ModuleSignature 와 ModuleComponents 행은 Microsoft 의 병합 도구처럼 패키지에 남는다. 모듈이 ModuleIgnoreTable 에 적은 표는 들어가지 않는다.

설정할 수 있는 모듈(ModuleConfiguration 표가 있는 것)은 값 - 글, 수, 비트 묶음, 또는 모듈의 폴더 같은 키 - 을 묻고, config 가 항목마다 "이름=값" 문자열 하나로 준다:

[merge.Runtime]
source = "runtime.msm"
dir = "INSTALLDIR"
config = ["ServerName=example.com", "Port=8080", "DataDir=DataFolder.6E0A1C52_8F3B_4B7D_9A21_3C4D5E6F7A99"]

주지 않은 항목은 병합 도구가 답하지 않을 때처럼 모듈의 기본값을 쓴다. 값은 모듈의 ModuleSubstitution 표가 말하는 자리에, Microsoft 의 병합 도구가 넣는 방식대로 들어간다: 비트 묶음 항목은 마스크의 비트만 바꾸고, 키 항목은 [=Item;2] 로 키의 한 부분만 쓸 수 있고, 널 GUID 는 기능의 이름이 되며, 답을 받은 KeyNoOrphan 항목이 모두 대신한 기본 행은 빠진다. 키 값은 모듈 자신의 키(GUID 포함)로, 모듈의 이스케이프 꼴(세미콜론은 \;)로 쓴다. 모르는 항목, 수가 아닌 수 항목, 모듈이 값을 요구하는데 빈 값은 거절하고(RP1517), 설정을 받지 않는 모듈에 준 config 도 그렇다. 항목의 이름, 종류, 기본값은 모듈의 ModuleConfiguration 표에 있다: rubrapack inspect runtime.msm ModuleConfiguration.

병합 모듈 만들기: [module]#

[package] 대신 [module] 이 있는 원본은 남이 합칠 병합 모듈을 만든다: rubrapack build runtime.toml -o runtime.msm.

format = 1

[module]
name = "ExampleRuntime"
manufacturer = "Example"
version = "2.1.0"
arch = "x64"
id = "{6E0A1C52-8F3B-4B7D-9A21-3C4D5E6F7A99}"     # 모듈 자신의 GUID, 모든 판에서 그대로

[dir.RuntimeDir]
path = "$(TARGETDIR)/Example Runtime"             # TARGETDIR: 패키지가 모듈을 두는 곳

[files.Runtime]
dir = "RuntimeDir"
glob = "runtime/*"

[registry.Home]
root = "HKLM"
key = 'SOFTWARE\Example\Runtime'
name = "Home"
value = '$(RuntimeDir)'                           # 패키지가 정해 준 폴더

검색과 요구: [search.ID], [require.ID]#

검색은 무엇보다 먼저 실행되고 찾은 것을 공개 속성에 넣는다(못 찾으면 비어 있다): 레지스트리 값의 데이터, 파일의 전체 경로(path = 알려진 폴더와 상대 경로, 예 $(System) 이나 $(ProgramFiles)/Example; 프로그램 파일은 min-version), 폴더, 또는 다른 제품 구성 요소의 키 파일. 요구 사항은 condition 이 거짓이면 그 메시지로 첫 설치를 멈춘다(복구와 제거는 막지 않는다). 조건은 Windows Installer 의 문법을 쓰고 (VersionNT >= 603, FOUND_TOOL, NOT OLDSETTING) 검색 결과를 볼 수 있다.

[search.Tool]
property = "FOUND_TOOL"
kind = "file"
path = "$(System)"
file = "tool.exe"

[require.Tool]
condition = "FOUND_TOOL"
message = "[ProductName] needs tool.exe."

registry 나 dir 검색은 속성 대신 dir 을 가리킬 수 있다. 그러면 찾은 폴더가 그 dir 의 기본값이 된다. 사용자가 고른 폴더를 패키지가 기억하는 방법이 이것이다: 폴더를 적어 두고, 다음 판에서 찾아본다. 레지스트리 값은 그 폴더가 아직 있을 때만 쓰이고, 명령줄에 준 폴더(msiexec /i app.msi INSTALLDIR=D:\Apps\Example\)가 여전히 이긴다.

[registry.RememberDir]
root = "HKLM"
key = "Software\\Example"
name = "InstallDir"
value = "[INSTALLDIR]"

[search.PreviousDir]
property = "INSTALLDIR"           # dir: 레지스트리에 있는 폴더가 실제로 있으면 그것이 기본값
kind = "registry"
root = "HKLM"
key = "Software\\Example"
name = "InstallDir"

서비스, 글꼴, 권한#

[service.ID] 는 패키지의 .exe 가 돌리는 서비스를 설치한다: 파일이 바뀌기 전과 제거할 때 멈추고, 제거할 때 지우며, start-on-install = true 면 설치 뒤에 시작한다. [font.ID] 는 패키지가 Fonts 폴더에 설치하는 글꼴 파일을 등록한다. title 이 없으면 Windows 가 TrueType/OpenType 파일에서 이름을 읽는다. [permission.ID] 는 패키지가 만드는 폴더, 그 파일, 또는 그 레지스트리 값에 SDDL 보안 서술자를 건다(이것이 있으면 패키지는 Windows Installer 5.0 을 요구한다).

사용자별 패키지와 겸용 패키지: scope#

scope = "machine"(기본)은 모든 사용자에게 설치하고 관리자 권한이 필요하다. scope = "user" 는 권한 상승 없이 현재 사용자에게 설치한다: ProgramFiles 는 %LOCALAPPDATA%\Programs 가 되고, Programs 와 Desktop 은 그 사용자의 것, 레지스트리 값은 HKCU(또는 HKMU), 환경 변수는 사용자의 것이며 do/undo 동작은 그 사용자로 실행된다. 컴퓨터 전체 설치를 요청하면 거부한다. scope = "dual" 은 기본으로 사용자별로 설치하고, 관리자 명령창에서 msiexec /i x.msi ALLUSERS=1 MSIINSTALLPERUSER="" 로 하면 컴퓨터 전체로 설치한다. 그 레지스트리 값은 설치된 방식에 따라 HKLM 이나 HKCU 가 되는 HKMU 를 쓴다. 서비스, 글꼴, 권한과 컴퓨터 폴더(SystemRoot, System, Fonts, ProgramData)는 scope = "machine" 이 필요하다.

사용자 프로필의 폴더(AppData, LocalAppData, StartMenu, Programs, Desktop, Startup 과 그 아래, 범위와 관계없이)에 두는 파일, [folder], [ini], [remove] 는 바로 가기처럼 HKCU 값을 키 경로로 하는 구성 요소에 든다: Software\<manufacturer>\<name>\Components 에 구성 요소마다 값 하나를 두고, 구성 요소와 함께 쓰고 지운다(Windows 규칙 ICE38). 그 프로필 폴더들은 알려진 폴더까지 제거 때 비어 있으면 지운다(ICE64). 함께 쓰는 %LOCALAPPDATA%\Programs 도 다른 것이 없으면 지운다. rubrapack 0.46.0 까지는 이 구성 요소를 파일로 키 삼았고 GUID 도 달랐다. 그런 판으로 만든 패키지에서 올릴 때는 메이저 업그레이드여야 하고(rubrapack 이 만드는 업그레이드는 옛 판을 먼저 지운다), lint --previous 는 옛 구성 요소를 사라졌다고 알리며(RP2306), 패치는 거절한다. component-guid 를 준 파일은 키 경로가 파일 그대로이고 경고(RP1320)가 나온다. dual 패키지가 AppData 나 LocalAppData 에 파일을 두어도 경고가 나온다: 모든 사용자용으로 설치하면 설치하는 사용자의 프로필에만 들어간다.

캐비닛#

파일은 패키지에 넣는 캐비닛 하나로 MSZIP(deflate)이나 LZX(compress = "lzx": 대개 5~20% 더 작고, MSZIP 이 처리기를 모두 쓰는 데 비해 2 GiB 까지의 캐비닛 폴더마다 처리기 하나로 초당 4 MB 쯤 - 더 큰 패키지는 폴더들이 나란히 돈다)로 압축한다. cab-max-size = N 은 파일이 N MiB 를 넘을 때마다 새 캐비닛을 시작하고, cab = "external" 은 캐비닛을 패키지 옆에 <name>.cab(또는 <name>-1.cab, <name>-2.cab, ...)으로 쓴다. 그 캐비닛은 패키지와 함께 다녀야 하고, cab-max-size 가 없으면 캐비닛마다 2 GiB 전에 나눈다. Windows Installer 는 2 GiB 이상인 패키지를 열지 못하므로, 그만큼 큰 내장 캐비닛은 cab = "external" 을 권하는 오류(RP1516)다. rubrapack 은 있는 캐비닛을 덮어쓰지 않고 패키지를 맨 나중에 쓴다: 패키지는 <out>.rp-map 에서 만들어지고(바이트가 메모리가 아니라 그 파일로 바로 간다) 모든 일이 성공했을 때만 제 이름으로 바뀌므로, 실패한 빌드는 패키지를 남기지 않는다. 압축은 모든 프로세서에서 돈다(덜 쓰려면 --jobs N); 어느 쪽이든 바이트는 같다. 모든 패키지에는 관리 설치(msiexec /a, 압축을 푼 네트워크 이미지)와 광고(msiexec /jm) 순서도 들어 있다.

MSI 서식 문자열로 해석되는 곳은 이것뿐이다: 레지스트리 value(와 multi 항목), 바로가기 args, 환경 변수 value, INI value, 요구 사항 message, 서비스 args. 나머지는 적은 그대로 쓴다.

설치된 앱 항목과 속성#

[arp] 는 제품이 "설정 > 설치된 앱"에 어떻게 보이는지 정한다(no-modify, no-repair, help, about). [property.NAME] 은 공개 속성을 더한다. secure = true 는 그 값이 설치의 관리자 권한 부분까지 가게 하고, hidden = true 는 로그에 값이 남지 않게 한다. 설치 엔진이나 rubrapack 이 스스로 쓰는 이름(ARP*, MSI*, RP_*, ALLUSERS, REBOOT 등)은 거부한다.

"설치된 앱"의 제거는 msiexec /qb /x {ProductCode} 를 돌린다: 패키지의 대화창이 아니라 Windows Installer 자신의 축소 창이고, 창이 있는 프로그램이 파일을 쥐고 있으면 자신의 사용 중인 파일 상자 - 영어이고 '취소'가 기본이다('무시'면 계속된다) - 를 띄운다. no-remove = true 는 그 제품의 제거를 끈다. 남는 수정은 패키지의 대화창을 돌리고(msiexec /i), 거기서 제거를 고르면 패키지 자신의 사용 중인 파일 창('계속'이 기본)으로 간다. 거의 모든 프로그램이 쥐는 입력기나 셸 확장에 쓸 만하다. no-modify 와는 함께 쓸 수 없고, 사용 중인 파일 창은 대화창 세트의 것이다(ui = "none" 에서는 Windows Installer 자신의 것이 나온다).

명령줄로 설치하기#

대화창이 고르는 것은 모두 msiexec 에 줄 수 있다.

속성하는 일
INSTALLDIR=D:\Apps\Example\설치 폴더(대문자 ID 의 dir 모두)
ADDLOCAL=Core,Extra이 기능들을 설치(ADDLOCAL=ALL: 모든 기능)
REMOVE=Extra설치된 제품에서 이 기능들을 제거(REMOVE=ALL: 전부)
INSTALLLEVEL=3level 이 3 이하인 기능을 모두 설치
RPLANGUAGE=ko대화창 언어([ui] languages 가 있을 때)
ALLUSERS=1 MSIINSTALLPERUSER=""겸용 패키지를 모든 사용자에게(기본: 현재 사용자만)
DESK=1, APP_MODE=server원본이 정한 속성: when 조건, 대화창 값

창 없이 설치하기#

rubrapack 이 만드는 패키지는 모두 명령줄에서 사용자 화면 없이 설치·복구·업그레이드·제거된다:

msiexec /i example.msi /qn /l*v install.log
msiexec /x {ProductCode} /qn

종료 코드 0 은 성공, 3010 은 다시 시작이 필요한 성공(rubrapack 은 컴퓨터를 스스로 다시 시작하지 않는다), 그 밖의 값은 실패이며 그 뒤 컴퓨터는 설치 전과 같다.

대화창: ui#

[package] 의 ui 가 내장 대화창 세트 하나를 고른다. 없으면(none) Windows Installer 자신의 진행 막대만 보인다.

ui설치할 때이미 설치되어 있을 때
noneWindows Installer 의 진행 막대작은 창: 복구, 제거 또는 취소
basic진행, 끝(또는 오류)변경(고를 기능이 있으면), 복구 또는 제거
minimal환영, 사용권(있으면), 진행, 끝변경(고를 기능이 있으면), 복구 또는 제거
installdir환영, 사용권, 설치 폴더(폴더 찾아보기 포함), 준비, 진행, 끝변경(고를 기능이 있으면), 복구 또는 제거
featuresinstalldir 에 더해 기능 트리와 필요한 디스크 공간변경, 복구 또는 제거

"이미 설치되어 있을 때"는 제품 - 이 버전 - 이 설치된 상태에서 같은 패키지를 다시 연 경우다(두 번 누르기, msiexec /i, "설치된 앱"의 수정). 복구는 빠지거나 망가진 파일과 설정을 되살리고, 제거는 제품을 지운다. 제거는 지우기 전에 한 번 더 묻는다: 대화창 세트는 무엇이 지워지는지 말하는 페이지를 보이고(패키지가 프로그램의 설정과 데이터까지 지우면 그것도 말한다 - 제거할 때 싹 지우기 참고) 그 페이지의 기본 단추는 '뒤로'다. none 의 창은 예/아니요로 묻고 기본은 '아니요'다. 변경은 패키지에 사용자가 켜고 끌 수 있는 기능(hidden 도 required 도 아닌 기능)이 있으면 모든 세트에 나온다: 기능 트리로 가고, 거기서 '설치'를 누르면 켠 기능을 더하고 끈 기능을 뺀다. none 의 창에는 변경이 없다(트리를 보일 수 없다): 거기서는 명령줄로 한다(ADDLOCAL=, REMOVE=). 명령줄이 이미 골랐거나(REINSTALL, REMOVE, ADDLOCAL, msiexec /f, /x) /qn, /qb, /qr 이면 묻지 않고, 그때는 아무것도 복구하거나 바꾸지 않는다. 더 새 버전의 패키지는 업그레이드이지 이 경우가 아니다. none 의 창은 영어이고 한국어 사용자에게는 한국어다. 그 글은 아래의 MaintTitle, MaintText, Repair, Remove, Cancel, RemoveAsk(패키지가 싹 지우면 RemoveAskAll)다.

모든 세트에는 취소 확인, 오류 대화창, 사용 중인 파일 목록, 디스크 공간 부족 경고도 있다. 사용 중인 파일 목록은 창이 있는 프로그램이 바꾸거나 지울 파일을 쥐고 있을 때 나온다 - 입력기나 셸 확장이면 거의 모든 프로그램이다. 기본 단추는 '계속'이다: 파일은 곧바로 바뀌고, 이미 열린 프로그램은 다시 열 때까지 이전 것을 쓰며, reboot = "suppress"(기본)이면 다시 시작을 묻지 않는다(FilesInUseText; reboot = "allow" 면 Windows 가 다시 시작을 물을 수 있다고 적는 FilesInUseTextRestart). 대화창은 모두 기본값이 있는 값만 모으므로 /qn 은 여전히 창 없이 설치한다.

format = 1

[package]
ui = "installdir"
license = "LICENSE.txt"           # 동의할 때까지 설치/다음 단추가 꺼져 있다

[ui]
install-dir = "APPDIR"            # 사용자가 바꿀 수 있는 dir(기본 INSTALLDIR)
banner = "banner.bmp"             # 위쪽 띠; 약 493 x 58 화소

[ui-text.WelcomeText]
text = "This will install [ProductName]. Close other programs first."

.txt 나 .md 사용권은 평문으로 보여 준다(한글, 이모지 같은 어떤 글자도 그대로 둔다). .rtf 사용권은 그대로 쓴다. banner 가 없으면 띠는 흰색이다. [ui-text.ID] 는 문구 하나를 바꾼다. 문구는 MSI 서식 문자열이라 [ProductName] 은 치환되고 [ 자체는 [\[] 로 쓴다. ID 는 다음과 같다: Back, Next, Cancel, Install, Finish, OK, Yes, No, Retry, Ignore, Abort, Exit, Browse, WelcomeTitle, WelcomeText, LicenseTitle, LicenseText, LicenseAccept, DirTitle, DirText, DirLabel, BrowseTitle, BrowseText, BrowseLookIn, BrowseFolder, BrowseUp, BrowseNew, CustomizeTitle, CustomizeText, Reset, DiskCost, DiskCostTitle, DiskCostText, ReadyTitle, ReadyText, ProgressTitle, ProgressText, ProgressStatus, ExitTitle, ExitText, UserExitTitle, UserExitText, FatalTitle, FatalText, CancelText, FilesInUseTitle, FilesInUseText, FilesInUseTextRestart, Continue, OutOfDiskTitle, OutOfDiskText, MaintTitle, MaintText, Repair, RepairText, Remove, RemoveText, Change, ChangeText, RemoveConfirmTitle, RemoveConfirmText, RemoveConfirmTextAll, RemoveAsk, RemoveAskAll(제거 전의 두 번째 질문; 이 다섯을 주지 않는 다른 언어는 영어를 쓴다), DetailsFull(상세 상자의 확인란), LanguageTitle, LanguageText, LangGroup1~LangGroup9(묶은 언어 페이지의 지역; 주지 않는 언어는 영어를 쓴다), DirGuardText(아래 가드의 메시지), PreflightAsk, PreflightSilent, PreflightFolder, PreflightCache, PreflightExtra, PreflightExtraSilent, PreflightOtherStore, PreflightOtherStoreRefuse, PreflightOtherMsi, PreflightOtherMsiRefuse(사전 점검의 메시지), ReportInstall, ReportUpgrade, ReportRemove, ReportRepair(되돌리기가 실패했을 때의 보고서), 그리고 제거할 때 - 유지보수 페이지의 제거, 또는 REMOVE=ALL - 쓰는 RemovalProgressTitle, RemovalExitTitle, RemovalExitText, RemovalUserExitTitle, RemovalUserExitText, RemovalFatalTitle, RemovalFatalText(이것을 주지 않는 다른 언어는 설치 문구를 쓴다). 단추 문구에서 & 는 바로 가기 키를 표시한다(&Next 는 Alt+N).

embedded = "ui.dll" 은 작성자 자신의 UI 처리기(InitializeEmbeddedUI, EmbeddedUIHandler, ShutdownEmbeddedUI 를 내보내는 DLL)를 패키지에 담는다. Windows 가 대화형 설치에서 rubrapack 의 대화창 대신 그것을 돌리므로 ui = "none" 과 함께 줄 수 있다. 조용한 설치(/qn)와 밖에서 다른 프로그램이 UI 를 모는 경우(체인의 setup.exe 처럼)에는 건너뛴다. 그래서 체인 패키지는 그 창에 기대면 안 된다. 내장 UI 는 MSI 를 위한 것이다. MSIX 는 자체 설치 UI 가 없어 거절한다(RP1605).

여러 언어#

대화창은 영어다. [ui] languages 는 같은 패키지에 다른 언어를 덧붙인다. 그러면 첫 페이지가 언어를 묻고, 그 뒤의 모든 페이지 - 환영, 사용권, 폴더, 사용자 페이지, 준비, 진행, 완료, 그리고 취소·오류·사용 중 파일·디스크 공간·유지보수 페이지 - 가 고른 언어로 나온다. 미리 골라 두는 언어는 사용자의 지역 형식 (UserLanguageID), 그다음 시스템 로캘(SystemLanguageID)이 LANGID 에 드는 첫 덧붙인 언어이고, 없으면 영어다. 명령줄의 RPLANGUAGE=ko 는 그 선택을 대신 정한다(언어 페이지는 그대로 나오고 그것이 골라져 있다). 무인 설치(/qn)는 아무것도 보여 주지 않으니 고를 것도 없다. 영어만 있으면 언어 페이지도 없다.

[ui]
languages = ["ko"]                # 영어는 늘 있고 기본이다
# languages = "all"               # 또는: 내장 언어 모두, 지역 순서대로
license-ko = "LICENSE-ko.txt"     # 언어별 사용권(기본: [package] license)

[ui-text.WelcomeText]
text-ko = "[ProductName]을(를) 설치합니다."     # text = 모든 언어, text-xx = 한 언어

[dialog.Options]
after = "RpInstallDirDlg"
title = "Options"
title-ko = "선택 사항"

[dialog-control.Mode]
# ...
labels = ["&Typical", "&Portable"]
labels-ko = ["표준(&T)", "휴대용(&P)"]

언어팩#

언어 하나는 파일 하나다. 영어는 rubrapack 에 들어 있고, 마흔여덟 언어가 resources/lang/<코드>.po 로 함께 온다: ko, ja, zh(중국어 간체), zh_TW(중국어 번체 - 대만, 홍콩, 마카오), es, fr, de, pt_BR(브라질), pt_PT(포르투갈), ru, it, nl, sv, da, nb(노르웨이어 보크몰), fi, pl, cs, uk, hu, ro, tr, el, vi, th, id, ms, bg, hr, sk, sl, sr(라틴 문자 세르비아어), et, lv, lt, ar(아랍어), he(히브리어), hi(힌디어), fa(페르시아어), ur(우르두어), ca(카탈루냐어), eu(바스크어), gl(갈리시아어), bn(벵골어), ta(타밀어), kk(카자흐어), uz(라틴 문자 우즈베크어), az(라틴 문자 아제르바이잔어). 다른 언어는 그 언어의 파일 - 번역 도구들이 읽는 PO 형식의 <코드>.po - 을 더하기만 하면 지원된다. rubrapack 도 원본의 문구도 바꿀 필요가 없다. 코드는 소문자 2~3자이고, 지역 변형이면 _ 와 지역의 대문자 2자를 붙인다 (gettext 가 파일 이름을 짓는 방식). 패키지 하나에는 영어와 그 밖의 언어 63개까지 들어간다 - 내장 팩 48개가 모두 들어간다. 일곱 개까지는 언어마다 문구마다 동작이 하나씩이고, 여덟 개부터는 문구가 표(RpText)의 행이 되어 언어를 고를 때 rubrapack 의 도우미 DLL 이 제자리에 넣는다. 영어를 포함해 열두 개까지는 언어 페이지가 한 열로 늘어놓고, 그보다 많으면 지역별로 묶는다: 왼쪽에 지역, 오른쪽에 고른 지역의 언어. 팩은 X-Rubrapack-Group 으로 지역을 적는다(1 동아시아, 2 동남아시아, 3 남아시아, 4 중앙아시아·캅카스, 5 중동, 6 서유럽·남유럽 - 영어도 여기 -, 7 북유럽·발트, 8 중부·동유럽). 적지 않은 팩은 "기타"에 들어간다. 한 지역에는 열두 개까지다(RP1308). languages = "all" 은 내장 팩을 모두 넣는다. 순서는 위의 지역 순서이고, 한 지역 안에서는 널리 쓰는 언어가 먼저다 (ko ja zh zh_TW, 그다음 en de fr es it ...). 원본 옆의 팩은 여전히 내장 팩을 갈음한다. "all" 은 혼자 쓴다: 배열 안에 넣으면 오류다(RP1316).

msgid ""
msgstr ""
"Language: ja\n"
"X-Rubrapack-Name: 日本語\n"        # 언어 페이지에 보일 그 언어의 이름
"X-Rubrapack-LangIDs: 1041\n"      # 이 언어가 골라지는 Windows LANGID
"X-Rubrapack-Font: Yu Gothic UI\n"
"X-Rubrapack-Direction: ltr\n"     # 오른쪽에서 왼쪽으로 읽는 언어(ar, he, fa, ur)는 rtl
"X-Rubrapack-Group: 1\n"           # 묶은 언어 페이지에서 이 언어의 지역

# 명령줄: msgid 는 rubrapack 이 쓰는 영어 문구 그대로다.
msgid "cannot read '%s' (%s)"
msgstr "'{1}' を読めません ({2})"

# rubrapack 이 만드는 설치 파일: msgctxt "installer", msgid 는 문구의 ID 다.
msgctxt "installer"
msgid "WelcomeTitle"
msgstr "[ProductName] セットアップ"

아이콘, 완료 페이지, 설치 범위 페이지#

나만의 대화창 페이지: [dialog.ID], [dialog-control.ID]#

minimal, installdir, features 에서는 내장 흐름에 페이지를 더할 수 있다. 더한 페이지도 다른 페이지와 같은 띠와 뒤로/다음/취소 단추를 갖고, 본문에 컨트롤을 놓는다.

[dialog.Options]
title = "Options"                 # 띠의 제목; 없으면 [ProductName]
description = "Choose how to install."
after = "RpInstallDirDlg"         # 이 페이지 바로 다음에 나온다

[dialog-control.ModeLabel]
dialog = "Options"
type = "text"
x = 20
y = 55
width = 330
height = 12
text = "Installation &mode:"

[dialog-control.Mode]
dialog = "Options"
type = "radio"
x = 20
y = 70
width = 200
height = 42                       # 값 하나에 12 이상
property = "APP_MODE"
values = ["typical", "portable", "server"]
labels = ["&Typical", "&Portable", "&Server"]

[property.APP_MODE]
value = "typical"                 # 기본값, 창 없는 설치에서도 쓰인다

빌보드: [billboard.ID]#

[billboard.Welcome]
image = "ads/welcome.bmp"
feature = "Main"
order = 1

빌보드는 그 feature 의 파일이 설치되는 동안 진행 화면에 보이는 그림이다. order 순서대로 차례로 바뀐다(기본: 빌보드를 적은 순서). 각 빌보드는 image 에 .bmp 를 가리킨다(Windows Installer 는 BMP 만 보인다). 빌보드는 MSI 를 위한 것이다. MSIX 는 패키지 전체를 한 번에 설치하므로 [billboard.*] 는 거기서 거절된다(RP1605).

아키텍처와 업그레이드 계열#

arch 는 원본 자신의 아키텍처, upgrade-code 는 그 업그레이드 계열이다. 같은 원본을 다른 아키텍처로 지으려면(--arch) 그 아키텍처에 upgrade-code-x86, upgrade-code-arm64, upgrade-code-x64 로 제 계열을 주어야 하고, 없으면 빌드를 거부한다. Windows Installer 의 업그레이드 감지는 아키텍처를 가리지 못해서, 코드를 같이 쓰면 한 아키텍처를 설치할 때 다른 아키텍처가 지워진다. MSIX 에는 업그레이드 코드가 없으므로 .msix·.msixbundle 빌드에는 필요 없다.

먼저 지워야 하는 판#

refuse-upgrade-below = "1.0.0" 은 설치된 1.0.0 미만 판의 업그레이드를 거부하고 먼저 지우라고 알린다. 메시지(refuse-upgrade-message, 또는 기본 문구)는 늘 그 일을 하는 명령으로 끝난다: msiexec /x {ProductCode} /qn MSIRESTARTMANAGERCONTROL=Disable. 옛 판을 다른 도구가 MSIRESTARTMANAGERCONTROL=Disable 없이 만들었을 때 쓴다: 그런 판을 업그레이드 도중에 지우면 그 파일을 불러 쓰는 프로그램을 모두 닫으려 한다(formats/msi-package.md 의 "사용 중인 파일" 참고).

진행하기 전에: 사전 점검#

설치·업그레이드·제거가 무엇이든 바꾸기 전에 패키지가 살핀다:

close-programs = "always" 는 묻지 않고 종료하고, "never" 는 종료하지도 묻지도 않는다(/qn 에서도). 기본은 "ask" 다. 제거도 다르지 않다: "always"(또는 RPCLOSE=yes)면 제품의 실행 중인 프로그램에 닫으라고 하고 몇 초 뒤 남은 것을 끝낸 다음 제품을 제거한다. 아무 말도 없는 패키지의 조용한 제거(/qn)는 그 프로그램이 하나라도 열려 있으면 1603 으로 멈춘다 - 아무에게도 묻지 않은 프로그램을 끝내지 않기 위해서다. 입력기나 셸 확장에는 보통 "never" 를 쓴다: 열려 있는 거의 모든 프로그램이 그 DLL 을 싣고 있어 "ask" 면 업그레이드와 제거 때마다 질문이 나오고 조용한 실행은 매번 멈추는데, 파일은 아무것도 종료하지 않고도 안전하게 바뀐다(옛 사본은 정리 작업이 치운다). 명령줄의 RPCLOSE=yes 나 RPCLOSE=no 는 패키지의 설정보다 앞선다. extra-files = "keep" 은 함께 설치되지 않은 것을 묻지 않고 남기고(Windows Installer 자체의 동작), "delete" 는 묻지 않고 지운다. 기본은 "ask" 이고 RPEXTRA 가 그보다 앞선다. preflight = false 는 이 단계를 통째로 뺀다. 문구는 PreflightAsk, PreflightSilent, PreflightFolder, PreflightCache, PreflightExtra, PreflightExtraSilent 다([ui-text.*]; 영어와 언어팩이 있는 언어는 내장이고, 이것을 주지 않는 다른 언어는 영어를 쓴다).

다른 사본: [other-copy.ID]#

[other-copy.Store]                # 같은 프로그램의 Microsoft Store 패키지
msix = "Example.Example"          # Partner Center 가 준 식별 이름과 게시자
publisher = "CN=12345678-ABCD-..."
action = "warn"                   # 기본값. "refuse" 면 멈춘다

[other-copy.Old]                  # 다른 방식으로 만든 예전 설치 프로그램
upgrade-code = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D40}"
action = "refuse"
message = "[1] cannot be installed beside [2]. Remove [2] first."
message-ko = "[2] 이(가) 있으면 [1]을(를) 설치할 수 없습니다. 먼저 [2]을(를) 제거하세요."

처음 설치하기 전에 사전 점검이 가리킨 사본을 하나씩 찾는다: 설치하는 사용자에게 등록된 그 식별 이름의 앱 패키지(MSIX. Windows 는 앱 패키지를 사용자마다 둔다), 또는 그 업그레이드 코드로 설치된 제품. 있으면:

메시지는 이 제품([1])과 찾은 사본([2]: 앱 패키지의 전체 이름, 또는 다른 제품의 이름)을 말한다. message 와 message-xx 가 기본 문구 PreflightOtherStore, PreflightOtherStoreRefuse, PreflightOtherMsi, PreflightOtherMsiRefuse 를 대신한다. 사전 점검이 있어야 한다(preflight = false 면 거절). MSIX 를 만들 때는 빠진다: 앱 패키지는 추가될 때 아무것도 실행하지 않는다. 거꾸로 - 설치된 MSI 에 MSIX 가 비켜서야 할 때 - 는 메뉴 항목의 hide-when 과 프로그램 자신이 실행 중에 살피는 것으로 한다.

Windows Installer 가 스스로 돌보는 것이 둘 있다: 다른 설치가 도는 동안 두 번째 설치는 곧바로 1618 로 끝나고, 중간에 끊긴 설치(충돌, 정전) 뒤의 다음 설치는 끝나지 않은 것을 먼저 되돌린다. 끊긴 제거 뒤에는 그 되돌리기가 제품을 도로 놓고 제거는 1605 로 끝난다: 한 번 더 제거하면 된다.

나중에 정리하기: 정리 작업#

실행 중인 프로그램이 쥐고 있는 파일은 곧바로 지울 수 없을 때가 있다. Windows Installer 는 옮길 수 있는 것은 옆으로 옮기고 나머지는 다음 재시작 때 지우도록 걸어 두는데, 입력기나 셸 확장이 있으면 컴퓨터가 몇 주씩 재시작하지 않을 수 있다. 그래서 파일을 지우거나 바꾸는 설치(제거, 업그레이드, 복구)는 예약 작업 rubrapack cleanup {ProductCode}(사용자별이면 뒤에 사용자의 SID)를 건다. 작업은 2분 뒤, 그다음엔 로그온 때마다와 15분마다 돌아서, 이 설치가 재시작 때로 미룬 것 - 패키지 폴더의 파일과 그 파일의 Config.Msi 백업 사본 - 을 아무도 쥐지 않게 되는 대로 지우고, 그 때문에만 남았던 패키지 폴더를 지운 뒤 자신도 지운다. 할 일이 없으면 첫 실행에서 사라지고, 30일이 지나면 그만두고 나머지는 재시작에 맡긴다. 컴퓨터별 패키지는 SYSTEM 으로, 사용자별 패키지는 그 사용자로(권한 상승 없이) 돌고, 그 계정만 쓸 수 있는 폴더(%ProgramData%\rubrapack\cleanup\{ProductCode}, 또는 사용자의 %LOCALAPPDATA%)에서 돈다. 컴퓨터별의 경우 그 자리의 rubrapack 폴더를 다른 누가 먼저 만들어 두었으면 쓰지 않고 작업도 걸지 않는다. 다른 것은 지우지 않는다: 이 설치가 걸어 둔 것만, 같은 파일이 아직 그 자리에 있을 때만, 그리고 설치된 제품이 지금 그 자리에 가진 파일은 지우지 않는다. [package] cleanup = false 면 넣지 않는다.

관리자 권한 없이 지우는 사용자별 패키지에서는, 프로그램이 아직 쥐고 있는 파일을 Windows Installer 가 다음 재시작에 지우도록 예약할 수 없어 그 파일이 영영 남게 된다: 제거는 제품의 파일 가운데 아직 남아 있는 것을 적어 두고, 정리 작업이 그 파일이 풀리면 지운다.

컴퓨터별 패키지는 앞선 제거로부터 자기 파일도 지킨다: 다른 제품을 지울 때 프로그램이 그 파일을 쥐고 있었으면 그 파일의 삭제가 재시작 때로 걸려 남는데, 이 패키지가 같은 자리에 똑같은 파일을 설치하면 Windows Installer 는 이미 있는 파일을 그대로 둔다 - 그러면 재시작이 그 파일을 지운다. 패키지는 설치하면서 그런 삭제를 거둬들인다(설치가 실패하면 다시 걸어 둔다).

따로 제거 프로그램은 없다: Windows 의 "설치된 앱"은 MSI 를 Windows Installer 로 지우고, 제거가 남긴 것은 정리 작업이 맡는다. 프로그램이 스스로 쓴 것은 패키지의 것이 아니라서 남는다 - 패키지가 그 자리를 적지 않는 한: 제거할 때 싹 지우기.

제품과 함께 지워지는 추가 기능: parent, remove-addons#

추가 기능은 다른 제품을 넓히는 따로 된 패키지다 - 언어 팩, 플러그인 - 그 제품 없이는 쓸모가 없다. 추가 기능은 본체 제품을 업그레이드 코드로 적는다: [package] parent = "{...}". 설치하면 자기 제품 코드를 SOFTWARE\rubrapack\Addons\<그 업그레이드 코드> 에 적고(컴퓨터별은 HKLM, 사용자별은 HKCU), 지우면 그 값도 지운다. 본체는 remove-addons = true 로 둔다: 본체를 정말로 지울 때(업그레이드가 바꿔 넣을 때가 아니라) 곧바로 도는 정리 작업이 제거가 끝나기를 기다렸다가, 거기 적힌 추가 기능 가운데 아직 설치된 것을 msiexec /x {ProductCode} /qn 으로 지운다 - 본체를 어느 길로 지웠든(설치된 앱, 관리 화면, msiexec /x). 그 순간 지울 수 없는 추가 기능(다른 설치가 도는 중)은 작업의 다음 실행에서 다시 해 본다. Windows Installer 는 한 번에 두 패키지를 지울 수 없으므로 추가 기능은 본체보다 먼저가 아니라 몇 초 뒤에 지워진다 - 본체 없이도 지워지게 만든다. 이 키로 구운 본체의 첫 판부터 효과가 있고, 정리 작업이 있어야 한다(cleanup = false 와 함께면 오류). 두 키 모두 MSI 패키지에만 쓴다.

여러 인스턴스 나란히: instances#

[package] instances = N(2 이상)은 한 컴퓨터에 제품을 여러 번 설치할 수 있게 한다. rubrapack 은 N-1개의 인스턴스 변환을 패키지에 넣고 Instance1 .. Instance(N-1) 로 이름 짓는다. 본체는 평소대로 설치하고, 인스턴스는 이렇게 설치한다:

msiexec /i pkg.msi MSINEWINSTANCE=1 TRANSFORMS=:Instance1

각 인스턴스는 자기 ProductCode, 자기 이름(<이름> (instance K), 그래서 Installed apps 에 따로 나온다), 자기 구성 요소 GUID(Windows Installer 가 요구한다 — 한 자리의 두 제품은 같은 구성 요소를 가지면 안 된다), 자기 최상위 폴더(<폴더> (instance K)), 그리고 패키지의 것과 K 에서 끌어낸 자기 업그레이드 코드를 가진다 - 한 코드를 같이 쓰면 설치된 인스턴스 때문에 Windows 가 새 인스턴스를 다운그레이드로 거절하고, 다음 판이 형제 인스턴스를 지웠을 것이다. 같은 원본으로 지은 다음 판은 같은 코드를 가지므로 인스턴스 K 는 인스턴스 K 만 업그레이드한다(다시 MSINEWINSTANCE=1 TRANSFORMS=:InstanceK 로 설치한다). 컴퓨터 단위 패키지용이다(scope = "machine"). 변환은 rubrapack inspect pkg.msi --transform Instance1 로 본다.

다른 제품을 대신하기: replaces#

replaces = ["{UpgradeCode}", ...](16개까지)는 이 패키지가 대신하는 제품들이다 - 이를테면 따로 된 패키지였다가 이제 이 패키지의 기능이 된 추가 기능. 이 패키지를 설치하면 그 제품들의 설치된 판을 모두 같은 실행에서 지운다 - 자기 옛 판을 지우듯이(RemoveExistingProducts; 그들 자신의 제거가 UPGRADINGPRODUCTCODE 와 함께 돈다). 그래서 같은 파일의 주인이 둘이 되지 않는다. 이 패키지의 업그레이드 코드나 parent 는 쓸 수 없다. MSI 에만 쓴다.

와일드카드: [files.ID]#

glob 은 *(한 폴더 안의 아무 글자들), ?(한 글자), **(폴더 여러 단계)가 든 원본 경로다. 맞는 것은 이름순으로 정렬하므로 결과가 파일 시스템에 따라 달라지지 않는다. 첫 와일드카드 아래의 폴더는 dir 아래에 다시 만든다: glob = "dist/layouts/**/*.jmt" 는 dist/layouts/de/x.jmt 를 <dir>/de/x.jmt 로 설치한다. 맞는 것이 없거나, 심볼릭 링크이거나, 출력 파일 자신이 맞으면 오류다.

빈 폴더: [folder.ID]#

파일이 하나도 들지 않아도 dir 안에 name 폴더를 만든다. keep = true 면 제거한 뒤에도 폴더가 남는다(프로그램이 쓰는 데이터를 위해).

기능(feature)#

[feature.*] 표가 없으면 모든 것이 숨은 기능 하나에 든다. 기능을 하나라도 선언하면 모든 파일에 기능이 있어야 한다: 파일 자신의 feature 키, 또는 그 dir 의 feature. level 이 1 보다 큰 기능은 기본으로 설치하지 않는다. ui = "features" 면 사용자가 트리에서 기능을 고른다. 나중에는 - features 만이 아니라 모든 대화창 세트에서 - 같은 패키지를 다시 열면(또는 "설치된 앱"의 수정) "변경"이 나오고, 그것이 그 트리로 간다: 처음에 뺀 기능을 더하고, 필요 없어진 기능을 뺀다(대화창: ui 참고).

조건: when#

when 은 Windows Installer 조건(VersionNT64, DESK = "1", NOT OLDVERSION)을 받는다. 기능, 파일과 파일 묶음, 레지스트리 값(파일과 with 로 묶인 값은 안 된다: 파일 쪽에 둔다), 바로가기, 환경 변수, INI 값에 쓸 수 있다. 조건이 참일 때만 그것을 설치한다. 나만의 대화창 페이지에서 정한 속성, 명령줄에 준 속성, 검색 결과를 볼 수 있다. 조건은 그것을 처음 설치할 때(메이저 업그레이드 포함) 따진다 - 복구는 있는 그대로 둔다. MSIX 는 모두 설치하므로 when 을 거부한다.

[dialog-control.Desk]             # 나만의 페이지의 체크박스
dialog = "Options"
type = "checkbox"
x = 20
y = 60
width = 300
height = 16
text = "바탕화면 바로 가기 만들기(&D)"
property = "DESK"

[shortcut.Desk]
dir = "Desktop"
name = "Example"
target = "file:App"
when = "DESK"

제거할 때 파일 남기기: keep#

파일(또는 파일 묶음)에 keep = true 를 두면 제품을 제거해도 그 파일은 남는다 - 사용자가 바꿨을 수 있는 설정 파일용이다. 한 번 바뀐 파일은 복구나 다음 판이 덮어쓰지 않는다(판 없는 파일에 대한 Windows Installer 의 규칙). MSIX 는 파일을 모두 지우므로 keep 을 거부한다.

변수#

문자열에 이름을 넣는 방법은 $(NAME) 하나뿐이고, $$ 는 $ 한 글자다. 이름은 한 번만 바뀌고 그 결과는 다시 읽지 않는다($(X) 가 든 값은 그대로 남는다). 이름은 다음 가운데 하나다.

빌드 변수에는 Windows 이름이나 dir ID 를 붙일 수 없다(RP1404).

Windows 이름#

Windows 폴더와 환경 변수의 이름이고, Windows 의 철자로 쓴다. 대소문자는 가리지 않는다($(appdata) 는 $(APPDATA)). 값 안에서는 Windows Installer 가 설치할 때 설치하는 사용자의 것으로 채우는 것이 된다 - type = "expand" 레지스트리 값에서는, 읽는 프로그램이 실행할 때 실행하는 사용자의 것으로 푸는 환경 변수가 된다:

이름경로 기준설치할 때(64비트 / 32비트 패키지)실행할 때(type = "expand")
ProgramFiles예[ProgramFiles64Folder] / [ProgramFilesFolder]%ProgramFiles%
ProgramFiles(x86)예[ProgramFilesFolder]%ProgramFiles(x86)%
ProgramW6432-[ProgramFiles64Folder] / [%ProgramW6432]%ProgramW6432%
CommonProgramFiles예[CommonFiles64Folder] / [CommonFilesFolder]%CommonProgramFiles%
CommonProgramFiles(x86)-[CommonFilesFolder]%CommonProgramFiles(x86)%
CommonProgramW6432-[CommonFiles64Folder] / [%CommonProgramW6432]%CommonProgramW6432%
ProgramData, ALLUSERSPROFILE예[CommonAppDataFolder]%ProgramData% ...
APPDATA예[AppDataFolder]%APPDATA%
LOCALAPPDATA예[LocalAppDataFolder]%LOCALAPPDATA%
TEMP, TMP예[TempFolder]%TEMP% ...
SystemRoot, windir예[WindowsFolder]%SystemRoot% ...
System예[System64Folder] / [SystemFolder]-
Fonts, Desktop, StartMenu, Programs, Startup예[FontsFolder], [DesktopFolder], [StartMenuFolder], [ProgramMenuFolder], [StartupFolder]-
USERNAME-[LogonUser]%USERNAME%
COMPUTERNAME-[ComputerName]%COMPUTERNAME%
SystemDrive, USERPROFILE, PUBLIC, HOMEDRIVE, HOMEPATH, USERDOMAIN, LOGONSERVER, ComSpec, Path, PATHEXT, OS, PROCESSOR_ARCHITECTURE, NUMBER_OF_PROCESSORS-[%NAME]%NAME%

값 안의 dir ID 는 [ID], 곧 설치할 때의 그 폴더가 된다. 폴더의 Windows Installer 값은 \ 로 끝나므로 폴더 이름 바로 뒤의 \ 하나는 빠진다: '$(INSTALLDIR)\app.exe' 는 [INSTALLDIR]app.exe 가 된다. 환경 변수가 없는 폴더는 type = "expand" 값에 쓸 수 없다(RP1404). MSIX 에는 설치할 때가 없다: 그것이 필요한 값은 거기서 거부하고(RP1612, RP1613), 펼칠 수 있는 값 안의 %NAME% 은 그대로 쓸 수 있다.

있는 그대로 쓴 [NAME] 과 %NAME% 은 바뀌지 않고 지나간다. $(...) 이름이 없는 것을 위해서다: [#FileID], [ProductVersion], 글자 [ 를 뜻하는 [\[], 파일 형식 인수의 %1.

프로그램 파일#

PE 파일(.exe, .dll 등)은 검사한다: 머신 형식이 arch 와 맞아야 하고(x64 패키지의 x86 도우미는 any-arch = true 가 필요하다), 판 정보 리소스가 패키지 안 파일의 판이 된다. Windows Installer 는 그것을 보고 설치된 파일을 바꿀지 정한다.

설치 폴더 지키기#

다른 프로그램이 그 파일을 불러 쓰는 프로그램 - 입력기, 셸 확장, 서비스 - 은 남이 미리 만들어 둔 폴더에 설치되면 안 된다: 거기 있던 파일은 그대로 남고, 프로그램 옆에 심어 둔 DLL 이 그 프로그램의 권한으로 실리기 때문이다. dir 에 guard = true 를 두면, 그 폴더가 다음과 같을 때 첫 설치가 파일을 하나도 놓기 전에 멈춘다.

믿을 수 있는 주인의 폴더(예: Program Files) 아래의 아직 없는 폴더는 통과한다(설치 엔진이 만들고, [permission.*] 로 잠글 수 있다). 복구, 제자리 업그레이드, 제거는 검사하지 않는다. 메이저 업그레이드는 새 판의 첫 설치라 똑같이 검사한다(이전 판이 만든 폴더는 통과한다). 설치는 DirGuardText 메시지([1] 은 그 폴더; 대화창에서 고른 언어) 뒤에 1603 으로 끝나고, /qn 에서도 같으며, 로그에 찾은 주인이 적힌다. 검사는 rubrapack 의 도우미 DLL 이 하고, 패키지가 그 DLL 을 싣는다(REG_QWORD 값과 같다). 보는 것은 주인이다: 관리자 소유라도 권한이 누구나 쓸 수 있게 되어 있는 폴더는 거부하지 않는다([permission.*] 로 잠근다).

[dir.INSTALLDIR]
path = "$(ProgramFiles)/Example"
guard = true

경로#

설치될 경로 - [dir.*] 와 [search.*] 의 path - 는 폴더로 시작한다: dir ID 나 경로 기준이 되는 Windows 이름을 "$(ProgramFiles)/Example", "$(INSTALLDIR)/docs" 처럼 쓰고, 그다음 / 와 그 아래 폴더들을 쓴다. 뒤에는 빌드 변수가 와도 된다("$(INSTALLDIR)/v$(VERSION)").

원본 경로는 원본 파일 기준의 상대 경로이고 / 를 쓴다. 절대 경로, \, 심볼릭 링크, 없는 파일은 오류다. 설치될 이름에는 Windows 가 금하는 것(< > : " / \ | ? *, 제어 문자, 끝의 점이나 공백, CON 같은 장치 이름)만 빼고 어떤 유니코드 글자든 쓸 수 있고, 한 폴더 안의 두 이름이 대소문자만 달라서는 안 된다. Windows Installer 는 이름을 8.3 짧은 이름과 함께 UTF-16 255단위에 저장하므로, 긴 이름은 짧은 이름이 남긴 만큼만 쓸 수 있다: 세 글자 확장자의 name.ext 는 242단위, 확장자 없는 이름은 246단위(NTFS 는 255 를 받는다). 더 긴 이름은 RP1514 로 거부한다.

20.6 명령줄#

rubrapack build <src.toml> -o <out.msi|out.msm|out.msix|out.msixbundle|out.msixupload> [-D NAME=VALUE]...
                [--arch x64|arm64|x86 | --arch <목록> (.msixbundle, .msixupload)]
                [--compress none|mszip|mszip:N|lzx|lzx:N] [--jobs N] [--nfc] [--reproducible]
                [<키> [--cert <chain.pem>]
                 [--timestamp <URL> [--tsa-trust <인증서>] [--tls-trust <인증서>] [--system-roots]
                  [--proxy <URL>]] [--allow-unsigned-cabs]]
                [--unsigned-test] [--msix-compress deflate|store]      (.msix, .msixbundle, .msixupload)
                [--flat]                                               (.msixbundle)
                [--winget <dir>]                                       (.msi, .msix, .msixbundle)
rubrapack inspect <file.msi> [table | --summary | --files | --streams]
rubrapack inspect <file.msi> [table | --summary] --json
rubrapack inspect <file.msix|file.msixbundle> [--files | --manifest]
rubrapack inspect <file.cab>
rubrapack diff <a.msi> <b.msi>
rubrapack container <name> <package.msix|package.msixbundle|family name>... -o <out.xml>
rubrapack try <package.msi|package.msix|package.msixbundle> [-o <out.wsb>] [--host <folder>] [--network]
rubrapack new [msi] <name>
rubrapack new [<name>] [-i]
rubrapack new <name> [--dist <folder>] [--name ...] [--ui ...] [--optional ...] ...
rubrapack edit <file>.toml [--set <table>.<key>=<value>]... [--unset <table>.<key>]... [--sync]
rubrapack guid [--from <text>]
rubrapack lint <src.toml> [-D NAME=VALUE]... [--arch x64|arm64|x86] [--target msi|msix] [--nfc] [--strict] [--json]
rubrapack lint <file.msi> [--previous <old.msi>] [--strict] [--json]
rubrapack explain [RPnnnn]
rubrapack schema
rubrapack lint <file.msix|file.msixbundle> [--strict]
rubrapack extract <file.msi|file.msix|file.msixbundle|file.cab> -d <새 폴더> [--limit-entries N] [--limit-bytes N]
rubrapack sign <file.exe|.dll|.msi|.msp|.msix|.msixbundle> <키> [--cert <chain.pem>]
               [--timestamp <URL> [--tsa-trust <인증서>] [--tls-trust <인증서>] [--system-roots]
                [--proxy <URL>]] [--allow-unsigned-cabs] [-o <out>]
rubrapack keys list [--pkcs11 <모듈> [--token-label <이름>] [--pin-env VAR | --pin-file FILE]]
rubrapack verify <file.exe|.dll|.msi|.msp|.msix|.msixbundle> [--trust <인증서>]... [--system-roots] [--tsa-trust <인증서>]...
rubrapack version | help [command]

<키> 는 다음 가운데 하나:
  --key <key.pfx|.pem> [--pass-env VAR | --pass-file FILE]                   키 파일
  --pkcs11 <모듈> --key-label <이름> [--token-label <이름>]
           [--pin-env VAR | --pin-file FILE]                                  PKCS#11 토큰 안의 키
  --key-store <SHA-1 지문> [--machine-store]                                  Windows 저장소의 키

20.7 진단 코드#

rubrapack 이 알리는 모든 문제에는 파일, 줄, 열 다음에 코드가 붙는다: error[RPnnnn] 또는 warning[RPnnnn]. 앞의 두 자리가 문제의 종류를 말한다:

코드무엇이 잘못됐나볼 곳
RP00xx명령줄: 모르는 명령이나 옵션, 읽거나 쓸 수 없는 파일, 변환이 나를 수 없는 차이(RP0013)rubrapack help <명령>
RP10xx원본 파일의 인코딩: UTF-8(또는 BOM 있는 UTF-16)이 아님, 짝 없는 캐리지 리턴파일을 UTF-8 로 저장한다
RP11xxrubrapack 이 읽는 부분집합 밖의 TOML: 여러 줄 문자열, 인라인 표, 두 번 정의한 표; format 이 없거나 너무 새 것(RP1108)TOML 부분집합, 원본 형식
RP12xx표와 키: 모르는 표나 키(제안과 함께), 빠진 필수 키나 표, 기능이 생긴 뒤 기능 없는 항목표
RP13xx값: ID(모든 표에 걸쳐 유일, 예약어 아님), GUID, 판, 범위를 벗어난 수, 없는 것을 가리키는 참조그 표의 절
RP14xx변수: 값 없는 $(NAME), 닫히지 않은 $(, 쓸 수 없는 자리의 Windows 이름이나 dir ID변수
RP15xx설치할 파일: 없음, 폴더임, 링크임, 맞는 것 없는 글롭, 다른 아키텍처의 프로그램, 너무 긴 이름, 빌드 중에 바뀐 파일, 너무 큰 패키지경로, 프로그램 파일
RP16xxMSIX: MSIX 패키지에 필요한 것, 담을 수 없는 것MSIX 패키지
RP19xx이 rubrapack 이 제공하지 않는 기능-
RP20xx완성된 표가 Windows Installer 규칙을 어김. RP1xxx 검사를 통과한 원본은 여기에 걸리지 않아야 한다: 알려 주기 바란다-
RP21xx패키지 lint: 대화창, 코드 페이지, 글자 정규화명령줄의 lint
RP22xxMSIX 패키지나 묶음의 lint명령줄의 lint
RP23xxlint --previous: 이 패키지가 앞 판을 깨끗하게 업그레이드하지 못함판과 업그레이드

무엇을 고칠지는 메시지가 말한다. 매뉴얼에서 찾을 때는 위의 코드 범위를 쓴다.