Packager un composant Windows Store apps avec Nuget. 2eme partie
Dans cette deuxième partie, nous allons approfondir les méthodes de création du package Nuget d’un composant WinRT.
Pour rappel, vous pouvez consulter la première partie de cet article ici, où l’on aborde les bases de la création d’un package Nuget.
Composant WinRT (*.winmd)
Il s’agit ici de comprendre comment se comporte Nuget avec un composant qui n’est pas une simple .dll, voir qui comporte plusieurs fichiers, dont certains ne doivent pas être ajouter en tant que référence, mais bien présents dans le répertoire bin final (je pense au couple .winmd <=> .dll)
Nous allons créer un nouveau projet qui ne sera plus un projet PCL mais un projet Windows Runtime Component. Pour ce premier exemple, nous partons sur du code managé C# :
Le code du projet évolue “un peu” pour s’adapter aux contraintes du développement WinRT :
public sealed class GamingHelper
{
public IAsyncOperation<String> GetGameAsync()
{
return GetGameInternal().AsAsyncOperation<String>();
}
private async Task<String> GetGameInternal()
{
try
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var uri = "https://babylonjs.azure-mobile.net/api/GetGameByName/Omega Crusher.json";
HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, uri);
hrm.Headers.Add("X-ZUMO-APPLICATION", "dHfEVuqRtCczLCvSdtmAdfVlrWpgfU55");
var gameString = await client.SendAsync(hrm);
return await gameString.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
return ex.Message;
}
}
}
Le déploiement de ce composant via Nuget nécessite:
- le fichier .winmd (qui contient l’IL)
- le fichier .pri (en gros les ressources, notamment de localisation)
L’arborescence de mon répertoire de sortie Nuget est classique. On retrouve le répertoire lib (contenant les 2 fichiers) et le fichier .nuspec :
Pour ce premier exemple, rien de bien compliqué. Même si le fichier .pri fait parti du répertoire /lib, Nuget ne tentera pas de le référencer dans le projet final. Nuget reconnaît l’extension et ne l’interprète pas comme un composant “à référencer”.
Optionnel : Intégration Continue avec MSBuild
Cette partie est optionnelle et vous pouvez passer au prochain chapitre si l’automatisation de création via MSBuild ne vous intéresse pas.
Si vous désirez automatiser la création du package, j’ai amélioré le script MSBuild qui va générer le fichier .nupkg via Nuget.exe.
L’ordre des Taches MSBuild:
- Créer un groupe de propriétés pour factoriser les divers chemins d’accès.
- Créer un groupe de fichier qui contient :
- Le fichier .nupkg généré.
- Les fichiers, dans le répertoire de travail, servant à générer le package (.winmd, .dll, .pri).
- Les fichiers présents dans le répertoire bin/Release
- Effacer les fichiers temporaires (résultat de la précédente génération)
- Copier les fichiers du répertoire bin/Release dans le répertoire de travail Nuget
- Exécuter Nuget.exe pour généer le .nupkg
- Déplacer le résultat dans le repository local Nuget.
En XML ça donne ça :
<Target Name="AfterBuild">
<!-- Working directories, local repository directory and nuget command line exe -->
<PropertyGroup>
<NugetOutputDir>$(SolutionDir)Nuget\$(ProjectName)\</NugetOutputDir>
<NugetOutputDirLibWinRT>$(NugetOutputDir)lib\portable-net4+wp7+win8\</NugetOutputDirLibWinRT>
<NugetRepositoryDir>C:\Users\spertus\Documents\Visual Studio 2012\Projects\PERSONAL\Nuget\
</NugetRepositoryDir>
<NugetCommandLine>C:\Program Files (x86)\NuGet\NuGet.exe</NugetCommandLine>
</PropertyGroup>
<!-- Input, output and .nupkg files-->
<ItemGroup>
<NugetPkgFileOutput Include="$(SolutionDir)Nuget\$(ProjectName)\*.nupkg" />
<NugetOutputFiles Include="$(NugetOutputDirLibWinRT)$(TargetFileName)" />
<NugetOutputFiles Include="$(NugetOutputDirLibWinRT)$(ProjectPriFileName)" />
<NugetInputFiles Include="$(TargetPath)" />
<NugetInputFiles Include="$(ProjectPriFullPath)" />
</ItemGroup>
<!-- Clean Output Nuget directory (clean .winmd and .pri files)-->
<Delete Files="@(NugetOutputFiles)" />
<!-- Copy the .winmd and .pri files to Output lib winrt -->
<Copy Condition="'$(Configuration)'=='Release'"
SourceFiles="@(NugetInputFiles)" ContinueOnError="true"
DestinationFolder="$(NugetOutputDirLibWinRT)" />
<!-- Launch nuget.exe pack -->
<Exec Condition="'$(Configuration)'=='Release'"
WorkingDirectory="$(NugetOutputDir)"
Command="%22$(NugetCommandLine)%22 pack $(ProjectName).nuspec" >
</Exec>
<!-- Copy the .nupkg to the local repository -->
<Copy Condition="'$(Configuration)'=='Release'"
SourceFiles="@(NugetPkgFileOutput)" ContinueOnError="true"
DestinationFiles="@(NugetPkgFileOutput->'$(NugetRepositoryDir)%(Filename)%(Extension)')" />
</Target>
Composant WinRT C++/CX
C’est la que ça se corse un peu.
Lorsque vous développez un composant WinRT C++/CX, la compilation vous génère plusieurs choses. Notamment un .dll et un .winmd.
Hors dans Visual Studio, on référence le .winmd et pas le .dll. Si vous tentez de référencer directement le .dll, Visual Studio va jetera comme une vieille chaussette
J’ai créé un simple projet C++/CX et je l’ai compilé. Voici le résultat dans le répertoire Release :
Ce qui est important ici :
- WinRTMobSCx.dll : Ce fichier doit être présent dans le répertoire Output, car il contient votre code. Attention ce n’est pas ce fichier qui doit être référencé dans Visual Studio (Je sais, je me répète, mais bon …)
- WinRTMobSCx.winmd : fichier d’exposition des métadonnées. C’est lui qui sert à référencer votre assembly dans votre projet.
- WinRTMobSCx.pri : Fichier des ressources (localisation notamment)
Gérer les fichiers .winmd et .dll
Nous allons tenter de créer un package Nuget.
En préambule, je précise que j’ai compilé en mode Win32, ce qui rend notre composant non compatible avec le mode ARM (enfin pour l’instant)
Voici mon répertoire Output Nuget :
Et voici mon fichier .nuspec :
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="https://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>WinRTMobCx</id>
<version>1.0.0</version>
<authors>Sébastien Pertus</authors>
<owners>Sébastien Pertus</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Simple WinRT C++/Cx component</description>
</metadata>
</package>
Exécution de la ligne de commande :
"C:\Program Files (x86)\NuGet\nuget.exe"
pack "C:\Users\spertus\Documents\Projects\WIN 8\DeployingWithNuget\Nuget\WinRTMobCx\WinRTMobS.nuspec"
-OutputDirectory "C:\Users\spertus\Documents\Projects\WIN 8\DeployingWithNuget\Nuget\WinRTMobCx"
Je déplace mon package dans le répertoire “local repository” et je teste mon package dans une application Windows Store Apps classique :
Hum.. Not very successful… Ok tentons d’active le mode –Verbose:
Encore une fois, difficile de trouver une réponse. On comprend tout de même qu’il arrive à installer notre composant mais qu’il n’arrive pas à rajouter la référence.
D’où vient le problème ?
Il suffit de bien comprendre ce qui se passe. Quand on se réfère à la documentation Nuget, il est dit :
Tous les fichiers du répertoire lib sont ajoutés en référence de mon projet. Hors mon projet contient le .dll et le .winmd, et seul le .winmd sert de référence à Visual Studio.
Lors du processus d’installation, Visual Studio tente de rajouter 2 références, dont une qui est incompatible, d’où l’erreur !
Il existe plusieurs solutions, la plus simple étant de préciser dans le fichier .nuspec, quels sont les fichiers à ajouter en tant que référence :
Pour cela, nous allons utiliser l’élément <references> du fichier .nuspec
Pour plus d’informations sur le fichier de configuration .nuspec, vous pouvez vous référer à cette documentation en ligne : https://docs.nuget.org/docs/reference/nuspec-reference
A partir de là, seules les assemblies explicitement référencées dans le fichier .nuspec seront rajoutées en tant que référence au projet Visual Studio. Par contre, toutes les assemblies continueront à être copier dans l’output du projet, ce qui nous va parfaitement !
Voici donc le nouveau fichier .nuspec, modifié :
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="https://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>WinRTMobCx</id>
<version>1.0.0</version>
<authors>Sébastien Pertus</authors>
<owners>Sébastien Pertus</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Simple WinRT C++/Cx component</description>
<references>
<reference file="WinRTMobSCx.winmd" />
</references>
</metadata>
</package>
A partir de là, nous reprenons la procédure de création du package et un nouveau test d’installation:
Great !
La référence est correcte :
Et le répertoire de compilation contient bien nos 3 fichiers (.pri, .dll, .winmd) :
Note : Le fichier .pri ne nous a pas posé de problème, car Nuget n’essaie de rajouter en tant que référence uniquement les fichiers “compatibles” tel que .dll, .exe ou .winmd
Gérer les versions x86, x64 et ARM
Voyons voir ce qu’il se passe si l’on tente de compiler le projet, en l’état, en mode ARM :
There was a mismatch between the processor architecture of the project being built "ARM" and the processor architecture, "x86", of the implementation file "…\WinRTMobSCx.dll" for "…\WinRTMobSCx.winmd". This mismatch may cause runtime failures.
Pour le moment, notre composant n’est compatible que x86 (et a fortiori x64). Nous allons corriger cela pour permettre notamment le déploiement sur une table surface ARM et, tant qu’à faire, gérer correctement le mode x64.
Tout d’abord il faut savoir qu’il n’existe pas “out of the box” de solution avec Nuget (genre un nouveau répertoire) pour gérer ce cas. Par contre nous avons à notre disposition pas mal d’outils proposés par Nuget qui vont nous aider.
Notamment le répertoire /build qui permet d’enrichir le fichier .csproj de destination, grace à l’adjonction de 2 fichiers qui vont contenir des instructions MSBuild supplémentaires.
Ce répertoire /build, et les possibilités qui vont avec, ont été introduits dans la version 2.5 de nuget. Du coup nous allons spécifier une version minimum de Nuget à utiliser pour faire fonctionner notre package :
minClientVersion="2.5"
Vous pouvez ajouter 2 fichiers dans ce répertoire :
- Un fichier .props : qui sera ajouté en début de votre .csproj
- Un fichier .targets qui sera ajouté en fin de votre .csproj
Par défaut, chacun des deux fichiers doit contenir au minimum ceci :
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="https://schemas.microsoft.com/developer/msbuild/2003">
</Project>
Voici mon répertoire modifié avec l’ajout de ce répertoire, et le contenu du répertoire:
Sans rien faire, sans rien ajouter au fichier .nuspec, lors du déploiement du package dans votre application cible, votre fichier .csproj est modifié de cette façon :
A partir de là, les étapes de construction de notre package :
Inclure les 3 versions de notre projet C++/Cx (x86, x64 et ARM) :
- La version x86 reste dans le répertoire lib : Elle permettra à Visual Studio de correctement fonctionner en mode Design.
- Création d’un répertoire x64, x86 et ARM dans le répertoire build : Ils serviront, au moment de la compilation, à déployer les bonnes versions des assemblies.
Note : Nous allons dupliquer les fichiers du mode x86 dans ce tutoriel, mais avec un peu plus d’efforts, il est possible d’éviter cette redondance !
Si on schématise, voici l’arborescence finale de mon répertoire de sortie :
Nous allons modifier le fichier WinRTMobCx.targets pour prendre en compte cette nouvelle architecture de compilation “conditionnelle” :
Il suffit de “surcharger” la valeur HintPath de la balise XML MSBuild Reference. Au passage je rajoute un Target conditionnel pour vérifier qu’on ne tente pas de compilation en AnyCPU :
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="https://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="PlatformCheck" BeforeTargets="InjectReference"
Condition=" ( ('$(Platform)' != 'x86') AND ('$(Platform)' != 'ARM') AND ('$(Platform)' != 'x64') )">
<Error Text="$(MSBuildThisFileName) does not work correctly on '$(Platform)'
platform. You need to specify platform (x86 / x64 or ARM)." />
</Target>
<Target Name="InjectReference" BeforeTargets="ResolveAssemblyReferences">
<ItemGroup Condition=" '$(Platform)' == 'x86' or '$(Platform)' == 'x64' or '$(Platform)' == 'ARM'">
<Reference Include="WinRTMobSCx">
<HintPath>$(MSBuildThisFileDirectory)$(Platform)\WinRTMobSCx.winmd</HintPath>
</Reference>
</ItemGroup>
</Target>
</Project>
Nous effectuons une “injection” de la bonne référence (tout du moins, le bon chemin) quand nous changeons de mode de compilation !
J’ai activé le mode “Verbose” de la Build du projet, pour que l’on comprenne bien ce qui se passe en interne : On voit bien que la propriété HintPath est surchargée au moment de l’exécution de la tache :
La compilation s’effectue correctement et votre composant Nuget est fin prêt à être déployer sur Nuget !
Bon déploiement