Empaquetage partiel : attribuer une identité à une application non empaquetée

Pour obtenir un exemple fonctionnel de bout en bout (application WPF + programme d’installation Inno Setup), consultez l’exemple sparse-app.

Un exécutable de bureau standard ( généré avec dotnet buildMSBuild, CMake ou toute autre chaîne d’outils) n’a aucune identité de package. Sans identité, il ne peut pas utiliser de nombreuses API de Windows modernes (notifications toast, tâches en arrière-plan, cibles de partage, tâches de démarrage, API de données d’application, etc.).

L’empaquetage clairsemé confère une identité à une application sans déplacer ses fichiers binaires dans un MSIX. Vous fournissez un petit package uniquement d’identité.msix (un simple manifeste) et l’inscrivez avec votre application installée normalement à l’aide d’un emplacement externe. Votre .exe reste exactement là où votre installateur le place. Il s’agit de l’homologue de production de winapp create-debug-identity, qui est utilisé uniquement pour le débogage pendant le développement.

Ce guide couvre les trois étapes de l’interface de ligne de commande qui correspondent aux trois premières étapes du flux de travail officiel Accorder une identité aux applications non empaquetées :

Étape Commande Résultat
1. Créer le manifeste d’identité winapp init --exe <exe> --sparse sparse/appxmanifest.xml + sparse/Assets/
2. Générer et signer le package d’identité winapp pack <appxmanifest.xml> --cert <pfx> <PackageName>.identity.msix
3. Incorporer l’identité dans l’application winapp embed-identity <exe> <msix> élément dans le manifeste de fusion de l’exe

Les étapes 4 à 5 de la documentation (enregistrer / désenregistrer le package) relèvent de la responsabilité de votre programme d’installation — voir Intégration du programme d’installation.

Quand utiliser le packaging clairsemé

  • Vous disposez déjà d'un programme d'installation mature (Inno Setup, WiX, NSIS, MSI) et ne souhaitez pas basculer vers MSIX pour la distribution, mais vous avez besoin d'API de contrôle d'identité Windows.
  • Votre application doit être installée sur un chemin d’accès ou avec une disposition que MSIX n’autorise pas.
  • Vous souhaitez un changement minimal et additif : conservez votre flux d’installation existant et ajoutez une .msix étape d’inscription.

Si vous démarrez une nouvelle version et que vous pouvez distribuer en tant que MSIX, une application empaquetée complète (winapp init + winapp pack <folder>) est plus simple.

Prérequis

  1. Windows 10, version 2004 (build 19041) ou ultérieure. Les packages sparse reposent sur uap10:AllowExternalContent, qui nécessite 19041 ou version ultérieure.
  2. winapp CLI : installer via winget (ou mettre à jour s’il est déjà installé) :
    winget install Microsoft.WinApp --source winget
    
  3. Certificat de signature de code approuvé sur l’ordinateur cible. Pour les tests locaux, générez un certificat de développement avec winapp cert generate et approuvez-le. Les packages de production doivent être signés avec un certificat dont le sujet correspond au manifeste Publisher.

Walkthrough

Les exemples ci-dessous supposent un exécutable généré à l’adresse ./bin/Release/net8.0-windows/MyApp.exe.

Étape 1 : créer le manifeste d’identité éparse

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

Cela déduit le nom du package, l’éditeur, la version et la description de l’exe (via ses informations de version de fichier) et vous invite à les accepter ou à les remplacer. Ajoutez --use-defaults (ou --no-prompt) pour ignorer les invites dans CI et --name / --publisher pour remplacer des valeurs spécifiques :

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

Il écrit ce qui suit dans un dossier dédié sparse/ dans le répertoire actif par défaut (remplacer par --output-dir) :

  • appxmanifest.xml — un manifeste minimal avec <uap10:AllowExternalContent>true</uap10:AllowExternalContent> (un élément sous <Properties>), ProcessorArchitecture="neutral", une application win32App et le nom de l’exécutable indiqué dans Executable.
  • Assets/ — ressources visuelles de remplacement (extraites de l’icône du fichier exe lorsque possible).

Pourquoi un sparse/ dossier et non pas en regard de l’exe ? Le manifeste et Assets/ sont des entrées utilisées au moment de la génération, consommées par winapp pack et winapp embed-identity — rien ne les lit à côté de l’exe au moment de l’exécution (l’identité à l’exécution provient de l’élément <msix> incorporé dans l’exe, ainsi que de l’emplacement externe du package enregistré, et le manifeste fait référence à l’exe par son nom, de sorte que son emplacement est indépendant de l’endroit où se trouve l’exe). Les écrire dans un dossier dédié et géré dans le contrôle de code source permet de les tenir à l’écart d’un répertoire de sortie de build (comme bin/) qu’un nettoyage/rebuild supprimerait, et de conserver ce dossier sans fichiers binaires afin que les étapes suivantes restent propres. winapp pack et winapp embed-identity recherchent automatiquement dans sparse/, vous n’avez donc que rarement besoin de spécifier le chemin d’accès.

Remarque : Le flux d’initialisation minimal ignore volontairement toute installation du SDK et des packages — les packages d’identité seuls n’ont aucune dépendance au SDK.

Si un appxmanifest.xml existe déjà dans le répertoire cible, init s’arrête au lieu de l’écraser (ainsi que son Assets/). Relancez avec --force pour le régénérer.

Assurez-vous que le Publisher du manifeste généré correspond au certificat que vous utiliserez pour signer. Modifiez appxmanifest.xml si nécessaire ou passez --publisher lors de la génération.

Étape 2 : Générer et signer le package d’identité

Pointez winapp pack vers le manifeste fragmenté (un fichier, et non un dossier) :

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

Étant donné que le manifeste déclare AllowExternalContent, winapp pack génère une identité uniquement contenant uniquement.msix le manifeste , pas de fichiers binaires, pas de ressources. La sortie est par défaut <PackageName>.identity.msix dans le répertoire actif ; utilisez-la --output pour la modifier. La signature se produit uniquement lorsque vous passez --cert (ou --generate-cert).

Étape 3 : incorporer l’identité dans votre application

Incorporez l’élément <msix> afin que Windows connecte l’exe en cours d’exécution au package d’identité :

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

Vous pouvez aussi conserver le manifeste côte à côte comme fichier archivé dans le dépôt, puis regénérer :

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

En mode XML, l’élément <msix> est inséré dans (ou remplacé dans) le manifeste cible. Référencez ce manifeste à partir de votre projet (pour .NET, définissez<ApplicationManifest>app.manifest</ApplicationManifest>) et régénérez afin que l’élément soit incorporé dans l’exe.

Les deux modes lisent l’identité à partir d’un appxmanifest.xml sparse. Lorsque vous omettez --manifest, winapp recherche d’abord dans le dossier sparse/ (où winapp init --exe --sparse l’écrit par défaut) à côté de la cible, puis dans le répertoire actif, avant de se rabattre sur la cible et le répertoire actif ; passez --manifest pour indiquer un autre emplacement.

Remarque : Le mode EXE réécrit le binaire avec mt.exe, qui invalide toute signature Authenticode existante. Réinscrire l’exe (par exemple winapp sign ./MyApp.exe <cert.pfx>) avant de le distribuer.

Étape 4 : Inscrire (pour les tests locaux)

Les logos du manifeste sont résolus à partir de l’emplacement externe au runtime, et non à partir du .msix uniquement d’identité. L’étape 1 les a placés sous ./sparse/Assets, donc copiez-les à côté de votre fichier .exe (l’emplacement externe) avant de l’enregistrer — sinon, Windows enregistre une disposition dans laquelle il manque tous les logos référencés par le manifeste :

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

Enregistrez ensuite le package d’identification pour ce dossier (l’emplacement externe) :

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

Lancez l’application et vérifiez que l’identité est présente — par exemple, Windows.ApplicationModel.Package.Current.Id.FamilyName doit renvoyer le nom de famille de votre package au lieu de lever une exception.

Pour nettoyer :

Remove-AppxPackage <full-package-name>

Gestion des ressources

Le .msix sparse est uniquement d’identité. Les ressources visuelles référencées par le manifeste (Assets\StoreLogo.png, les vignettes, etc.) sont récupérées à partir de l’emplacement de contenu externe au moment de l’exécution, c’est-à-dire depuis le répertoire d’installation de votre application, et non depuis l’intérieur du .msix.

Cela signifie que vous devez déployer le Assets/ dossier en même temps que votre application (même disposition attendue par le manifeste, par rapport à l’emplacement externe).

L’étape 2 empaquette directement le fichier manifeste (winapp pack ./sparse/appxmanifest.xml), ce qui génère le .msix uniquement d’identité à partir de ce seul manifeste — les fichiers voisins sont ignorés, de sorte que vos ressources ou fichiers binaires ne sont jamais inclus. (Si vous faites plutôt pointer winapp pack vers un AllowExternalContent dont le manifeste déclare , il signale toutes les ressources ou tous les binaires qu’il trouve, car pour un package sparse, ceux-ci doivent se trouver à l’emplacement externe, et non à l’intérieur du .msix.)

Intégration du programme d’installation

L’inscription et la désinscription sont le travail du programme d’installation. Le modèle est le même dans les outils d’installation :

  • Installation : copiez vos fichiers binaires d’application, le Assets/ dossier et le .msix répertoire d’installation, puis exécutez Add-AppxPackage -Path "<install-dir>\MyApp.identity.msix" -ExternalLocation "<install-dir>".
  • Désinstaller : exécuter Remove-AppxPackage <full-package-name> avant de supprimer des fichiers.

Sécurité : le répertoire d’installation est résolu au moment de l’installation et peut contenir des caractères (par exemple une apostrophe simple) qui permettent de sortir d’un littéral de chaîne PowerShell. Toujours échapper ou valider le chemin avant de l’interpoler dans une chaîne -Command : les extraits de code WiX et NSIS ci-dessous supposent un chemin d’installation fiable, tandis que l’exemple Inno Setup montre comment l’échapper en toute sécurité. Préférez transmettre les chemins comme arguments à un script -File plutôt que d’utiliser une interpolation -Command inline.

Configuration d’Inno

Construisez les arguments PowerShell dans une fonction [Code] afin que le chemin d’installation au runtime soit échappé pour le littéral PowerShell entre apostrophes simples (un répertoire d’installation contenant un ' ne doit pas pouvoir injecter de script) :

[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;

Consultez l’exemple sparse-app pour voir un setup.isscomplet et fonctionnel.

Les exemples WiX et NSIS ci-dessous appellent un petit register-sparse.ps1 via -File afin que le chemin d’installation soit transmis en tant que paramètre (PowerShell le lie comme donnée) au lieu d’être interpolé dans une chaîne -Command. Cela évite l’injection de scripts via un répertoire d’installation spécialement conçu (par exemple, un nom de dossier contenant un guillemet ou $(...)) :

# 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)

Inscrivez-vous par utilisateur (Impersonate="yes"), car Add-AppxPackage inscrit le package pour le compte qui l’exécute. Une action différée avec Impersonate="no" s’exécute en tant que LocalSystem, ce qui n’attribue pas d’identité à l’utilisateur qui effectue l’installation (et est généralement rejeté). Pour un MSI par machine, exécutez l’inscription avec emprunt d’identité afin qu’elle s’applique à l’utilisateur appelant.

Une action personnalisée différée ne peut pas lire INSTALLFOLDER directement (les actions différées s’exécutent dans un contexte sans accès aux propriétés) et déclarent simplement que l’action ne l’exécute pas. Donc, acheminez les chemins d’accès via CustomActionData — une action immédiate de type 51 dont le nom Property est identique à celui de l’action différée Id — puis planifiez les deux après 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 est inclus dans l’extension utilitaire de WiX (WixUtilExtension) ; référencez-la afin que le binaire WixCA soit disponible.

Une seule action d’usurpation d’identité enregistre l’identité uniquement pour l’utilisateur qui exécute le programme d’installation. Pour approvisionner chaque utilisateur d’une installation par ordinateur, inscrivez-vous au premier lancement (par utilisateur) ou utilisez un mécanisme d’approvisionnement tel que 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

Résolution des problèmes

Package.Current génère / « absence d’identité de paquet » à l’exécution

  • Le package d’identité n’est pas inscrit ou le manifeste de fusion de l’exe manque l’élément <msix> . Relancez winapp embed-identity (et recompilez si vous utilisez le mode XML), puis réenregistrez avec Add-AppxPackage -ExternalLocation.
  • L’exe <msix packageName> / applicationId / publisherdoit correspondre exactement à l’identité du package inscrit.

Les ressources/logos n’apparaissent pas

  • Vérifiez que le Assets/ dossier est déployé à l’emplacement externe avec les mêmes chemins relatifs attendus par le manifeste. Les ressources sont résolues à partir de l’emplacement externe, et non du .msix.

Add-AppxPackage échoue avec une erreur de signature/approbation

  • Le .msix doit être signé par un certificat de confiance sur l’ordinateur et dont le sujet correspond à celui du manifeste Publisher. Pour les tests locaux, générez et approuvez un certificat de développement avec winapp cert generate, et assurez-vous que le manifeste Publisher le correspond.

MakeAppx : « Application avec la valeur RuntimeBehavior « win32App » ne doit pas déclarer EntryPoint »

  • Une application éparse win32App ne doit pas déclarer EntryPoint. Les manifestes générés par winapp init --sparse sont déjà corrects ; supprimez tout EntryPoint attribut si vous avez modifié manuellement le manifeste.

« L’entrée est un fichier, mais pas un manifeste éparse »

  • winapp pack <file> accepte uniquement un manifeste qui déclare <uap10:AllowExternalContent>true</uap10:AllowExternalContent>. Générez-en un avec winapp init --exe <exe> --sparse, ou transmettez un dossier d’entrée pour créer un MSIX complet.

Voir aussi