Créer un installateur qui choisit automatiquement les bons fichiers pour les plateformes x64 et x86
Le problème : Une application (en l'occurrence, il s'agit de Zappiti, le média manager pour platine multimédia Dune HDi) utilise une DLL s'appelant MediaInfo.dll et qui est fournie en version x86 et x64. Zappiti est compilé en mode AnyCPU et peux donc fonctionner avec l'une ou l'autre du moment que la DLL retenue corresponde à la plateforme du système d'exploitation courant.
L'objectif : Créer un installateur avec Visual Studio Installer qui détecte si l'application s'installe sur une architecture 32bit ou 64bit et qui installe la DLL correspondante.
Commençons par ajouter les DLL : Première étape, ajouter les deux versions de la DLL à notre installateur. Il n'est pas nécessaire de spécifier de Condition. Ma première approche fût justement de le faire en spécifiant TargetPlatform=x86 ou TargetPlatform=x64. Malheureusement, la valeur de cette propriété est définie dans les paramètres de l'installateur. De ce fait, il aurait fallut créer deux installateurs, un pour chaque plateforme, alors que c'est justement ce que je cherche à éviter. Non, pour choisir le bon fichier dynamiquement en fonction du système d'exploitation, il faut faire autrement.
La solution consiste à créer des Custom Actions pour choisir le bon fichier : Pour cela, commencez par ajouter un fichier de type de Installer Class dans votre projet comme indiqué ici. Il n'est pas nécessaire de créer un nouveau projet DLL, vous pouvez l'ajouter directement à votre exécutable. Visual Studio crée automatiquement une classe qui hérite de Installer. Ensuite, il suffit de surcharger la fonction OnBeforeInstall() de cette classe pour executer le code désiré au bon moment.
Etrangement, lors de l'installation OnBeforeInstall s'exécute après la copie des fichier. Ce n'est pas très grave ici, au contraire. Nous allons laisser l'installateur copier les deux DLL, puis en supprimer une et renommer l'autre. Maintenant, il faut dire à l'installer d'appeler cette fonction en créant un nouveau Custom Action comme indiqué ci dessous, et en choisissant pour cible le "Primary output" de notre projet qui contient la classe que nous venons de créer.
Détecter la plateforme de l'OS : En premier lieu, j'ai testé la valeur retournée par IntPtr.Size. En effet, sur une plateforme x86, la valeur retournée serait 4 et sur une plateforme x64 on obtiendrait un 8. Hélas, cette méthode retourne toujours 4 car souvenez vous, le TargetPlatform de notre installateur est définit à x86 pour pouvoir se lancer de n'importe quel PC.
Pour détecter la plateforme, nous allons utiliser la méthode GetNativeSystemInfo() en l'important de kernel32.dll.
private enum Platform
{
X86,
X64,
Unknown
}
internal const ushort PROCESSOR_ARCHITECTURE_INTEL = 0;
internal const ushort PROCESSOR_ARCHITECTURE_IA64 = 6;
internal const ushort PROCESSOR_ARCHITECTURE_AMD64 = 9;
internal const ushort PROCESSOR_ARCHITECTURE_UNKNOWN = 0xFFFF;
[StructLayout(LayoutKind.Sequential)]
internal struct SYSTEM_INFO
{
public ushort wProcessorArchitecture;
public ushort wReserved;
public uint dwPageSize;
public IntPtr lpMinimumApplicationAddress;
public IntPtr lpMaximumApplicationAddress;
public UIntPtr dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public ushort wProcessorLevel;
public ushort wProcessorRevision;
};
[DllImport("kernel32.dll")]
internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo);
private static Platform GetPlatform()
{
SYSTEM_INFO sysInfo = new SYSTEM_INFO();
GetNativeSystemInfo(ref sysInfo);
switch (sysInfo.wProcessorArchitecture)
{
case PROCESSOR_ARCHITECTURE_AMD64:
return Platform.X64;
case PROCESSOR_ARCHITECTURE_INTEL:
return Platform.X86;
default:
return Platform.Unknown;
}
}
Et voila, il reste juste à renommer la DLL correspondante à la plateforme choisie :
protected override void OnBeforeInstall(IDictionary savedState)
{
base.OnBeforeInstall(savedState);
string location = Path.GetDirectoryName(
System.Reflection.Assembly.GetAssembly(GetType()).Location);
if (File.Exists(Path.Combine(location, "MediaInfo.dll")))
File.Delete(Path.Combine(location, "MediaInfo.dll"));
Platform p = GetPlatform();
if (p == Platform.X86)
{
File.Move(Path.Combine(location, "MediaInfo-x86.dll"),
Path.Combine(location, "MediaInfo.dll"));
File.Delete(Path.Combine(location, "MediaInfo-x64.dll"));
}
else if (p == Platform.X64)
{
File.Move(Path.Combine(location, "MediaInfo-x64.dll"),
Path.Combine(location, "MediaInfo.dll"));
File.Delete(Path.Combine(location, "MediaInfo-x86.dll"));
}
}
Lorsqu'on désinstalle, il faut penser à supprimer "MediaInfo.dll" qui n'est pas connue de l'installer. On peut procéder comme précédemment en créant un nouveau Custom Action qui fera le ménage :
protected override void OnBeforeUninstall(IDictionary savedState)
{
base.OnBeforeUninstall(savedState);
string location = Path.GetDirectoryName(
System.Reflection.Assembly.GetAssembly(GetType()).Location);
File.Delete(Path.Combine(location, "MediaInfo.dll"));
}
C'est presque finit : L'installateur répond parfaitement au besoin de sélectionner automatiquement la DLL correspondante à la plateforme du système d'exploitation courant. Néanmoins, on se retrouve avec deux fichiers : un .MSI et un .EXE. Le premier est notre installateur et le second est un bootstrapper qui sert à vérifier et à installer les dépendances nécessaires pour que notre installateur puisse lui même se lancer tout les cas. C'est donc une bonne chose de les avoir, mais cela fait tout de même un fichier de trop à mon goût.
Il existe des solutions payantes pour fusionner les deux fichiers en un seul exécutable. Moi j'ai trouvé plus simple (et moins cher) : utiliser WinRar et générer un package avec l'option SFX : on peut lui indiquer d'extraire les fichiers dans un dossier temporaire, de lancer setup.exe et même personnaliser l'icone de notre exécutable. Maintenant, on a plus qu'un seul exécutable qui installe tout ce qu'il faut automatiquement. Il ne reste plus qu'a le distribuer !
