스파스 패키징: 패키지되지 않은 앱에 ID 부여

작동하는 엔드 투 엔드 예제(WPF 앱 + Inno 설치 관리자)는 스파스 앱 샘플을 참조하세요.

MSBuild, CMake 또는 기타 도구 체인으로 dotnet build빌드된 표준 데스크톱 실행 파일에는 패키지 ID가 없습니다. ID가 없으면 많은 최신 Windows API(알림 메시지, 백그라운드 작업, 공유 대상, 시작 작업, 앱 데이터 API 등)를 사용할 수 없습니다.

스파스 패키징은 이진 파일을 MSIX로 이동 하지 않고 앱에 ID를 부여합니다. 작은 ID만 포함된.msix 패키지(즉, 매니페스트만)를 배포하고, 이를 외부 위치를 통해 일반적으로 설치된 앱과 함께 등록합니다. 회원님의 .exe는 설치 관리자가 놓은 정확한 위치에 그대로 유지됩니다. 이것은 winapp create-debug-identity의 프로덕션용 대응 항목이며, winapp create-debug-identity는 개발 시 디버깅 전용입니다.

이 가이드에서는 공식 패키지되지 않은 앱에 ID를 부여 워크플로의 처음 세 단계에 해당하는 세 가지 CLI 단계를 다룹니다.

단계 Command Result
1. ID 매니페스트 만들기 winapp init --exe <exe> --sparse sparse/appxmanifest.xml + sparse/Assets/
2. ID 패키지 빌드 및 서명 winapp pack <appxmanifest.xml> --cert <pfx> <PackageName>.identity.msix
3. 앱에 ID 포함 winapp embed-identity <exe> <msix> exe의 fusion 매니페스트에 있는 요소

문서의 4~5단계(패키지 등록/등록 취소)는 설치 관리자의 책임입니다. 설치 관리자 통합을 참조하세요.

스파스 패키징을 사용하는 경우

  • 이미 완성도 높은 설치 관리자(Inno Setup, WiX, NSIS, MSI)가 있으며 배포를 위해 MSIX로 전환하지 않으려는 경우 ID 제어 Windows API가 필요합니다.
  • 앱은 MSIX에서 허용하지 않는 레이아웃 또는 경로에 설치해야 합니다.
  • 최소한의 추가 변경이 필요합니다. 기존 설치 흐름을 유지하고 등록 단계를 하나 .msix 추가합니다.

새로 시작하고 MSIX로 배포할 수 있는 경우 전체 패키지 앱(winapp init + winapp pack <folder>)이 더 간단합니다.

필수 조건

  1. Windows 10 버전 2004(빌드 19041) 이상 스파스 패키지는 uap10:AllowExternalContent에 의존하며, 이를 사용하려면 19041 이상이 필요합니다.
  2. winapp CLI - winget을 통해 설치(또는 이미 설치된 경우 업데이트):
    winget install Microsoft.WinApp --source winget
    
  3. 대상 컴퓨터에서 신뢰할 수 있는 코드 서명 인증서입니다. 로컬 테스트의 경우 개발 인증서 winapp cert generate 를 생성하고 신뢰합니다. 주체가 매니페스트 Publisher와 일치하는 인증서로 프로덕션 패키지에 서명해야 합니다.

Walkthrough

아래 예제에서는 ./bin/Release/net8.0-windows/MyApp.exe에 실행 파일이 빌드되어 있다고 가정합니다.

1단계 - 스파스 ID 매니페스트 만들기

winapp init --exe ./bin/Release/net8.0-windows/MyApp.exe --sparse

그러면 exe의 패키지 이름, 게시자, 버전 및 설명을 유추하고(파일 버전 정보를 통해) 수락하거나 재정의하라는 메시지가 표시됩니다. CI에서 프롬프트를 건너뛰려면 / (또는 --name)를 추가하고, 특정 값을 재정의하려면 --publisher--use-defaults--no-prompt를 추가하세요:

winapp init --exe ./bin/Release/net8.0-windows/MyApp.exe --sparse --use-defaults `
  --name "Contoso.MyApp" --publisher "CN=Contoso"

기본적으로 현재 디렉터리의 전용 sparse/ 폴더에 다음을 씁니다(재정의: --output-dir).

  • appxmanifest.xml<Properties>(win32App 아래의 요소), ProcessorArchitecture="neutral", Executable 애플리케이션, 그리고 <uap10:AllowExternalContent>true</uap10:AllowExternalContent>에 exe 이름이 채워진 스파스 매니페스트입니다.
  • Assets/ — 플레이스홀더 시각 자료(가능한 경우 .exe 아이콘에서 추출한 것).

sparse/ exe 옆에 폴더가 아닌 이유는 무엇인가요? 매니페스트와 Assets/winapp packwinapp embed-identity가 사용하는 빌드 시점 입력이며, 런타임에는 exe 옆에서 그것들을 읽는 것은 없습니다(런타임 식별 정보는 exe에 포함된 <msix> 요소와 등록된 패키지의 외부 위치에서 오며, 매니페스트는 exe를 name으로 참조하므로 그 위치는 exe가 있는 곳과 무관합니다). 그것들을 소스 제어 하에 있는 전용 폴더에 써 두면, 클린/리빌드 시 삭제되는 빌드 출력 디렉터리(예: bin/ 같은 위치)를 피할 수 있고, 해당 폴더에 바이너리가 없도록 유지하여 다음 단계도 깔끔하게 진행할 수 있습니다. winapp packwinapp embed-identity은(는) sparse/에서 자동으로 찾으므로 경로를 지정할 필요가 거의 없습니다.

참고: sparse init 흐름은 의도적으로 모든 SDK/패키지 설치를 건너뜁니다 — ID 전용 패키지에는 SDK 의존성이 없습니다.

대상 디렉터리에 appxmanifest.xml가 이미 있으면 init는 해당 항목(및 해당 Assets/)을 덮어쓰는 대신 중지합니다. --force로 다시 실행하여 재생성하세요.

생성된 매니페스트의 Publisher가 서명할 때 사용할 인증서와 일치하는지 확인하세요. 필요한 경우 appxmanifest.xml을 편집하거나, 생성할 때 --publisher을 전달하세요.

2단계 - ID 패키지 빌드 및 서명

winapp pack를 스파스 매니페스트(폴더가 아닌 파일)로 지정하세요:

winapp pack ./sparse/appxmanifest.xml --cert ./devcert.pfx

매니페스트에서 AllowExternalContent을 선언하므로 winapp pack는 매니페스트만 포함하는 ID 전용.msix를 빌드합니다. 바이너리도 에셋도 없습니다. 출력은 현재 디렉터리에서 기본값으로 <PackageName>.identity.msix을(를) 사용하며, 이를 변경하려면 --output을(를) 사용하세요. 서명은 --cert(또는 --generate-cert)를 전달할 때만 수행됩니다.

3단계 - 앱에 ID 포함

Windows가 실행 중인 exe를 ID 패키지에 연결할 수 있도록 <msix> 요소를 포함합니다.

# EXE mode — modify the built binary in place (uses mt.exe)
winapp embed-identity ./bin/Release/net8.0-windows/MyApp.exe

또는 병렬(side-by-side) 매니페스트를 체크인된 파일로 유지한 후 다시 빌드합니다.

# XML mode — update an external SxS manifest, then rebuild your app
winapp embed-identity ./app.manifest

XML 모드 <msix> 에서 요소는 대상 매니페스트에 삽입되거나 대체됩니다. 프로젝트에서 해당 매니페스트를 참조한 후(.NET의 경우 <ApplicationManifest>app.manifest</ApplicationManifest>로 설정), 해당 요소가 exe에 포함되도록 다시 빌드하세요.

두 모드 모두 스파스에서 ID를 읽습니다 appxmanifest.xml. --manifest를 생략하면 winapp은 먼저 대상 옆의 winapp init --exe --sparse 폴더(sparse/가 기본적으로 그 위치에 씀)를 찾고, 그다음 현재 디렉터리에서 찾은 후, 대상 옆과 현재 디렉터리로 대체합니다. 다른 위치를 지정하려면 --manifest를 전달하세요.

참고: EXE 모드는 이진 파일을 mt.exe다시 작성하여 기존 Authenticode 서명을 무효화합니다. exe를 배포하기 전에 exe(예: winapp sign ./MyApp.exe <cert.pfx>)에 다시 서명합니다.

4단계 - 등록(로컬 테스트용)

매니페스트의 로고는 ID 전용 이 아니라 런타임에 .msix에서 가져옵니다. 1단계에서 해당 항목을 ./sparse/Assets 아래에 기록했으므로 등록하기 전에 exe 파일 옆(외부 위치)에 복사하세요. 그렇지 않으면 Windows가 매니페스트에서 참조하는 모든 로고가 누락된 레이아웃을 등록하게 됩니다.

# Copy the generated assets into the external location (beside your exe)
Copy-Item ./sparse/Assets -Destination .\bin\Release\net8.0-windows\Assets -Recurse -Force

그런 다음 해당 폴더( 외부 위치)에 대해 ID 패키지를 등록합니다.

Add-AppxPackage -Path .\MyApp.identity.msix `
  -ExternalLocation (Resolve-Path .\bin\Release\net8.0-windows)

앱을 시작하고 ID가 있는지 확인합니다. 예를 들어 Windows.ApplicationModel.Package.Current.Id.FamilyName throw하는 대신 패키지 패밀리 이름을 반환해야 합니다.

정리하려면:

Remove-AppxPackage <full-package-name>

자산 처리

스파스 .msix식별자 전용입니다. 매니페스트에서 참조되는 시각적 자산(Assets\StoreLogo.png, 타일 등)은 런타임에 외부 콘텐츠 위치, 즉 앱의 설치 디렉터리에서 가져오며, 내부에서 가져오는 것은 .msix.

즉, 애플리케이션과 함께 폴더를 Assets/ 배포 해야 합니다(외부 위치를 기준으로 매니페스트가 예상하는 것과 동일한 레이아웃).

2단계에서는 매니페스트 .msix을 직접 패키징하며(), 해당 매니페스트만으로 ID 전용 winapp pack ./sparse/appxmanifest.xml를 빌드합니다. 같은 디렉터리의 다른 파일은 무시되므로 에셋이나 바이너리는 포함되지 않습니다. (대신 매니페스트에 winapp pack가 선언된 폴더AllowExternalContent 가리키면, 스파스 패키지의 경우 해당 자산 또는 이진 파일은 .msix 내부가 아니라 외부 위치에 있어야 하므로, 발견된 자산 또는 이진 파일에 대해 경고합니다.)

설치 프로그램 통합

등록 및 등록 취소는 설치 관리자의 작업입니다. 이 패턴은 설치 관리자 도구에서 동일합니다.

  • 설치: 앱 바이너리 파일, Assets/ 폴더 및 .msix를 설치 디렉터리에 복사한 다음 Add-AppxPackage -Path "<install-dir>\MyApp.identity.msix" -ExternalLocation "<install-dir>"를 실행합니다.
  • 제거: 파일을 삭제하기 전에 Remove-AppxPackage <full-package-name>을 실행합니다.

보안: 설치 디렉터리는 설치 시점에 결정되며 PowerShell 문자열 리터럴을 깨뜨리는 문자(예: 작은따옴표)를 포함할 수 있습니다. 경로를 -Command 문자열에 삽입하기 전에 항상 이스케이프하거나 유효성을 검사하세요. 아래의 WiX 및 NSIS 스니펫은 신뢰할 수 있는 설치 경로를 가정하는 반면, Inno Setup 예제는 안전하게 이스케이프하는 방법을 보여 줍니다. 인라인 -File 보간보다 -Command 스크립트에 경로를 인수로 전달하는 방식을 선호하세요.

Inno 설정

함수에서 PowerShell 인수를 [Code] 빌드하여 작은따옴표로 묶은 PowerShell 리터럴에 대해 런타임 설치 경로가 이스케이프되도록 합니다(스크립트를 포함하는 설치 디렉터리가 스크립트를 ' 삽입할 수 없어야 합니다.)

[Files]
Source: "dist\*"; DestDir: "{app}"; Flags: recursesubdirs
Source: "MyApp.identity.msix"; DestDir: "{app}"

[Run]
Filename: "powershell.exe"; Parameters: "{code:RegisterParams}"; Flags: runhidden

[UninstallRun]
Filename: "powershell.exe"; \
  Parameters: "-NoProfile -ExecutionPolicy Bypass -Command ""Get-AppxPackage -Name 'MyApp' | Remove-AppxPackage"""; \
  Flags: runhidden

[Code]
function EscapePSLiteral(const Value: string): string;
var S: string;
begin
  S := Value; StringChange(S, '''', ''''''); Result := S;
end;

function RegisterParams(Param: string): string;
var AppDir: string;
begin
  AppDir := ExpandConstant('{app}');
  { -ErrorAction Stop + try/catch make a registration failure terminating, so powershell.exe
    exits nonzero and the AfterInstall callback (see the full sample) can abort with rollback. }
  Result := '-NoProfile -ExecutionPolicy Bypass -Command "try { Add-AppxPackage -Path ''' +
    EscapePSLiteral(AppDir + '\MyApp.identity.msix') +
    ''' -ExternalLocation ''' + EscapePSLiteral(AppDir) + ''' -ErrorAction Stop } catch { Write-Error $_; exit 1 }"';
end;

완전하게 작동하는 setup.iss의 예는 sparse-app 샘플을 참조하세요.

아래의 WiX 및 NSIS 예제는 작은 -Command를 통해 호출하여 설치 경로가 register-sparse.ps1로 전달되도록 합니다(PowerShell은 이를 데이터로 바인딩함). 즉, -File 문자열에 보간되지 않습니다. 이렇게 하면 만들어진 설치 디렉터리(예: 따옴표 또는 $(...)다음이 포함된 폴더 이름)를 통한 스크립트 삽입을 방지할 수 있습니다.

# register-sparse.ps1 — ship this alongside your installer
param(
  [Parameter(Mandatory)] [string] $MsixPath,
  [Parameter(Mandatory)] [string] $ExternalLocation,
  [Parameter(Mandatory)] [string] $PackageName
)
$ErrorActionPreference = 'Stop'
try {
  # Add-AppxPackage emits NON-terminating errors by default, so a failure would otherwise leave
  # the process exit code at 0 and let the installer complete without identity. Try the add
  # directly first: a fresh install or a version-bumped upgrade registers/updates in place
  # without touching any existing registration. -ErrorAction Stop + the outer trap make a real
  # failure terminating so the installer (WiX Return="check" / NSIS) sees it.
  try {
    Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
  } catch {
    # Only ONE failure is safe to resolve by unregister+retry: the exact same version is already
    # registered (HRESULT 0x80073CFB, ERROR_PACKAGE_ALREADY_EXISTS — "already installed,
    # reinstallation blocked"), which Add-AppxPackage rejects. Re-throw everything else
    # (untrusted/corrupt .msix, unsupported OS, ...) so a bad new package can NEVER unregister a
    # working prior registration and strip the installed app of the identity it already had.
    if ($_.Exception.HResult -ne 0x80073CFB) { throw }
    Get-AppxPackage -Name $PackageName | Remove-AppxPackage -ErrorAction SilentlyContinue
    Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
  }
} catch {
  Write-Error $_
  exit 1
}

WiX(v3)

패키지를 실행하는 계정에 대한 패키지를 등록하기 때문에 Add-AppxPackage사용자별로 등록합니다(Impersonate="yes"). 이(가) 있는 지연된 작업은 LocalSystem(으)로 실행되며, 이는 설치 사용자에게 Impersonate="no" 않으므로(그리고 일반적으로 거부됩니다). 컴퓨터별 MSI의 경우 가장된 등록을 실행하여 호출하는 사용자에게 적용합니다.

지연된 사용자 지정 작업은 직접 읽을 INSTALLFOLDER 수 없으며(지연된 작업은 속성에 액세스하지 않고 컨텍스트에서 실행됨) 작업을 선언하기 만 하면 실행되지 않습니다. 따라서 Property 이름이 지연된 작업의 CustomActionData와 같은 즉시 Type 51 작업인 Id을 통해 경로를 마샬링하고, InstallFiles 뒤에 둘 다 예약합니다.

<!-- Immediate: stash the command line (with the resolved paths) into the deferred action's
     CustomActionData. Windows Installer copies the value of the property named the same as a
     deferred action into that action's CustomActionData. -->
<CustomAction Id="SetRegisterSparseCmd" Property="RegisterSparse" Execute="immediate"
  Value="powershell.exe -NoProfile -ExecutionPolicy Bypass -File &quot;[INSTALLFOLDER]register-sparse.ps1&quot; -MsixPath &quot;[INSTALLFOLDER]MyApp.identity.msix&quot; -ExternalLocation &quot;[INSTALLFOLDER]&quot; -PackageName &quot;MyPackageIdentityName&quot;" />

<!-- Deferred + impersonated: CAQuietExec reads its command line from CustomActionData when run
     deferred, so it registers the package for the invoking user. Return="check" fails the
     install if registration fails. -->
<CustomAction Id="RegisterSparse" BinaryKey="WixCA" DllEntry="CAQuietExec"
  Execute="deferred" Impersonate="yes" Return="check" />

<InstallExecuteSequence>
  <Custom Action="SetRegisterSparseCmd" After="InstallFiles">NOT Installed</Custom>
  <Custom Action="RegisterSparse" After="SetRegisterSparseCmd">NOT Installed</Custom>
</InstallExecuteSequence>

CAQuietExec는 WiX util 확장(WixUtilExtension)에 포함되어 있습니다. WixCA 바이너리를 사용할 수 있도록 이를 참조하세요.

가장된 단일 작업은 설치 관리자를 실행하는 사용자에 대해서만 ID를 등록합니다. 컴퓨터별 설치의 모든 사용자를 프로비전하려면 첫 번째 시작(사용자별)에 등록하거나 같은 프로비저닝 메커니즘 Add-AppxProvisionedPackage을 사용합니다.

NSIS

Section
  # Capture the PowerShell exit code and abort if registration failed. register-sparse.ps1 exits
  # nonzero on failure (it sets $ErrorActionPreference='Stop' and traps), so without this check the
  # installer would complete even though the app has no identity.
  ExecWait 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\register-sparse.ps1" -MsixPath "$INSTDIR\MyApp.identity.msix" -ExternalLocation "$INSTDIR" -PackageName "MyPackageIdentityName"' $0
  IntCmp $0 0 +2
    Abort "Registering the sparse identity package failed (exit code $0). The app requires package identity."
SectionEnd

문제 해결

Package.Current 런타임에 "패키지 ID가 없음" 오류 발생

  • ID 패키지가 등록되지 않았거나 exe의 Fusion 매니페스트에 <msix> 요소가 없습니다. 다시 실행 winapp embed-identity (XML 모드를 사용하는 경우 다시 빌드)한 다음 다시 등록합니다 Add-AppxPackage -ExternalLocation.
  • exe의 값은 <msix packageName> / applicationId / publisher등록된 패키지의 ID와 정확히 일치해야 합니다.

자산/로고가 표시되지 않음

  • Assets/ 폴더가 매니페스트가 예상하는 것과 동일한 상대 경로로 외부 위치에 배포되도록 하세요. 자산은 .msix가 아니라 외부 위치에서 해결됩니다.

Add-AppxPackage 서명/신뢰 오류와 함께 실패

  • .msix 컴퓨터에서 신뢰할 수 있고 주체가 매니페스트Publisher와 일치하는 인증서로 서명되어야 합니다. 로컬 테스트의 경우 개발 인증서 winapp cert generate를 생성 및 신뢰하고 매니페스트 Publisher 가 일치하는지 확인합니다.

MakeAppx: "RuntimeBehavior 값이 'win32App'인 애플리케이션은 EntryPoint를 선언해서는 안 됩니다."

  • 스파스 win32App 애플리케이션은 EntryPoint를 선언해서는 안 됩니다. winapp init --sparse에 의해 생성된 매니페스트는 이미 올바르므로, 매니페스트를 직접 편집한 경우 EntryPoint 특성을 제거하세요.

"입력은 파일이지만 스파스 매니페스트는 아닙니다."

  • winapp pack <file><uap10:AllowExternalContent>true</uap10:AllowExternalContent>을 선언하는 매니페스트만 허용합니다. winapp init --exe <exe> --sparse로 하나를 생성하거나, 전체 MSIX를 빌드하려면 입력 폴더를 전달하세요.

참고하십시오