Création d’un package APPX Windows 8 via les APIs C++

 

Dans mon précédant billet Déploiement d’une application Windows 8 (Interface Moderne) en entreprise, j’expliquais qu’il était possible de créer un Package de 3 manières différentes.

Via l’outil MAKEAPPX.EXE, via Visual Studio 2012, et enfin via des APIs C++.

Dans ce billet, je vais me concentrer sur la création d’un package avec les APIs C++ , afin de pouvoir “scripter” via des commandes Powershell (sans passer par des outils du Kit de développement Windows 8 donc), la création du package.

    

L’architecture de la solution se décline de la manière suivante :

Télécharger la solution

Une librairie C++/CLI (PowerShellAppxPackage), qui est une extension à Powershell une CmdLet que je nomme New-AppxPackage. Je ne rentre pas dans les détails de création d’une CmdLet ici, mais vous retrouverez les bases de création d’une CmdLet en C# en VB.NET ou en C++/CLI, sur mon article Développer une extension à Powershell.

#include "..\NativeAppxPackage\NativeAppxPackage.h"
#include <memory>
#include <msclr\marshal.h>

using namespace System;
using namespace System::Management::Automation;
using namespace System::Configuration::Install;
using namespace System::Configuration;
using namespace System::ComponentModel;
using namespace msclr::interop;
namespace PowerShellAppxPackage {
    [Cmdlet(VerbsCommon::New,"AppxPackage")]
    public ref class NewAppPackage :public PSCmdlet
    {
    public :
        NewAppPackage()
        {
            _native=new NativeAppxPackage();
            if (_native==nullptr)
            {
                CheckAndThrowError(S_FALSE,"Could not instanciate the NativeAppxPackage class",ErrorCategory::InvalidOperation,"0");            
            }
            CheckAndThrowError(_native->ComInitialize(),"Could not instanciate COM",ErrorCategory::InvalidOperation,"1");            
        }
        ~NewAppPackage()
        {
            if (_native!=nullptr)
                delete _native;
        }
        private :
            NativeAppxPackage* _native;
            void CheckAndThrowError(HRESULT hr,String^ error,ErrorCategory category, String^ errorId)
            {
                if (FAILED(hr))
                {
                    
                    
                    System::Exception^ exception=gcnew System::Exception(error);
                    
                    ErrorRecord^ record=gcnew ErrorRecord(exception,errorId,category,nullptr);
                    ThrowTerminatingError(record);
                }    
            }
        protected : virtual void ProcessRecord() override
                    {
                        
                        marshal_context ctx;
                        AppxProfile appxProfile;
                        appxProfile.OutPutAppxPackageFile= ctx.marshal_as<LPCWSTR>(_outPutAppxPackageFile);
                        appxProfile.InputDirectory=ctx.marshal_as<LPCWSTR>(_inputDirectory);
                        appxProfile.Hash=ctx.marshal_as<LPCWSTR>(_hash);

                        CheckAndThrowError(_native->CreateAppxPackage(appxProfile),"Creating Package Failed!",ErrorCategory::NotSpecified,"2");                                                                        
                        WriteObject("Create package succeeded!");        
                    }
        private:
            String^ _hash;
            String^ _outPutAppxPackageFile;
            String^ _inputDirectory;

    
    public :
            
        [Parameter(Position=0)]
        property String^ Hash
        {
            String^ get() {return _hash;}
            void set (String^ value) { _hash=value;}
        }
        [Parameter(Position=1)]
        property String^ InputDirectory
        {
            String^ get() {return _inputDirectory;}
            void set (String^ value) { _inputDirectory=value;}
        }

        [Parameter(Position=2)]
        property String^ OutPutAppxPackageFile
        {
            String^ get() {return _outPutAppxPackageFile;}
            void set (String^ value) { _outPutAppxPackageFile=value;}
        }

    };
    [RunInstaller(true)]
    public ref class NewAppxPackageSnapIn : PSSnapIn
    {
    public :
        property String^ Name
        {
            virtual String^ get() override
            {
                return "PowershellAppxPackage";
            }
        }
        property String^ Vendor
        {
            virtual String^ get() override
            {
                return "Eric Verni";
            }
        }
        property String^ VendorResource
        {
            virtual String^ get() override
            {
                return "PowershellAppxPackage";
            }
        }
        property String^ Description
        {
            virtual String^ get() override
            {
                return "This CmdLet create an AppxPackage";
            }
        }
        property String^ DescriptionResource
        {
            virtual String^ get() override
            {
                return "Check my blog https:///blogs.msdn.com//b/devosaure";
            }
        }
    };
}

En résumé, notre code CmdLet, possède deux classes

  • L’une NewAppPackage, qui définie notre CmdLet. Elle dérive de PSCmdlet, et surcharge la méthode ProcessRecord qui sera appelée lors de l’exécution de la commande New-AppXPackage
    De plus elle expose 3 paramètres
    • Hash pour l’algorithme de Hashage à utiliser
    • InputDirectory, qui sera le chemin d’accès aux fichiers de l’application à packager
    • OutPutAppxPackageFile, Nom du fichier Appx.
  • La seconde classe NewAppxPackageSnapin qui dérive de la classe PSSnapin, permettra d’enregistrer la commande. Cette classe expose entre autre la propriété Name qui est à utiliser lors de l’enregistrement du Snapin. (Dans notre exemple le nom c’est PowershellAppxPackage)

Une librairie .LIB en ISO C++ (NativeAppxPackage), qui utilise les APIs de création de package et qui sera liée statiquement à notre librairie Powershell.

Pour cette librairie, je suis partie de l’exemple How to create an app package, que j’ai légèrement modifié, pour parcourir de manière dynamique la liste des fichiers et des répertoires à incorporer dans le package.

En résumé, pour créer un package il faut :

- Créer une fabrique de package IAppxFactory

CComPtr<IAppxFactory> PackageFactory;
    hr=PackageFactory.CoCreateInstance(__uuidof(AppxFactory),
                                        nullptr,
                                        CLSCTX_INPROC_SERVER);

-Créer le PackageWriter via la fabrique

CComPtr<IAppxPackageWriter> PackageWriter;
    CComPtr<IStream> OutPutStream;
    APPX_PACKAGE_SETTINGS PackageSettings={0};
    hr=SHCreateStreamOnFileEx(
        appxprofile.OutPutAppxPackageFile,
            STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
            0,    
            TRUE,  
            NULL,  
            &OutPutStream);
    if (FAILED(hr))
    {
        return hr;
    }

    PackageSettings.hashMethod=HashMethod;
    PackageSettings.forceZip32=TRUE;
    hr = PackageFactory->CreatePackageWriter(
                OutPutStream,
                &PackageSettings,
                &PackageWriter);

- Enfin ajouter les fichiers au PackageWriter via la méthode AddPayloadFile

HRESULT hr=S_OK;
    FileInfo ManifestInfo;
    auto VectorFileInfo=_vectorfiles.get();
    std::for_each(VectorFileInfo->begin(),VectorFileInfo->end(),[&](FileInfo fileinfo)->HRESULT
    {
        if (fileinfo.IsManifest()==false)
            {
                HRESULT hr=S_OK;
                //TODO: quit the loop
                CComPtr<IStream> FileStream=nullptr;
                hr = SHCreateStreamOnFileEx(fileinfo.GetCurrentPath().c_str(),
                                            STGM_READ | STGM_SHARE_EXCLUSIVE,
                                            0,      
                                            FALSE,  
                                            NULL,   
                                            &FileStream);
                    if (FAILED(hr))
                    {
                        return hr;
                    }            
                    //TODO: Get The Type of the file
                    hr=packagewriter->AddPayloadFile(fileinfo.GetFileName().c_str(),
                                                            L"Text/html",
                                                            APPX_COMPRESSION_OPTION_NORMAL,
                                                            FileStream);
                    if (FAILED(hr))
                    {
                        return hr;
                    }
                
            }
        else
        {
            ManifestInfo=fileinfo;    
        }
        return hr;
    });
    

    
    CComPtr<IStream> manifestStream = nullptr;

    hr = SHCreateStreamOnFileEx(ManifestInfo.GetCurrentPath().c_str(),
                                STGM_READ | STGM_SHARE_EXCLUSIVE,
                                0,      
                                FALSE,  
                                NULL,   
                                &manifestStream);

    if (SUCCEEDED(hr))
    {
        hr = packagewriter->Close(manifestStream);
    }

    return hr;

Vous trouverez également dans la solution, une troisième librairie réservée aux tests (Cela permet d’éviter de repasser dans l’environnement Powershell pour tester la création du Package).

Une fois la solution compilée, il faut installer notre extension Powershell.

1. Lancez Powershell en mode Administrateur

2. Utilisez l’utilitaire INSTALLUTIL.EXE, pour inscrire l’extension. (se trouve dans les répertoires
C:\Windows\Microsoft.NET\Framework64\v4.0.30319 ou C:\Windows\Microsoft.NET\Framework\v4.0.30319) 

PS: Installutil.exe PowerShellAppxPackage.dll

 

3. Ensuite il faut enregistrer le snapin contenu dans notre extension
PS: Add-PssnapIn PowershellAppxPackage

4. Il est possible de vérifier sa présence
PS: Get-PSSnapin

Name        : PowershellAppxPackage
PSVersion   : 3.0
Description : This CmdLet create an AppxPackage

5. D’invoquer notre extension avec les bons paramètres

PS: New-AppxPackage -Hash shA256 -OutPutAppxPackageFile c:\temp\TestDeploiement.appx -InputDirectory c:\temp\manuel

Create Package succeeded !

Note : C:\temp\manuel doit contenir une arborescence valide, comme je le précise dans mon précédant billet Déploiement d’une application Windows 8 (Interface Moderne) en entreprise

Télécharger la solution

 

Dans un prochain article nous détaillerons la manière de signer ce package à l’aide d’API.

 

Eric Vernié