Blog

Automating SharePoint App Packages using PowerShell


by Tobias Lekman, 21 April, 2016

When you distribute an app through Visual Studio, you set the Client ID and redirect URL and this is then written inside the .app file. This file is in fact a .zip file and is then uploaded to the corporate app catalog. However, I wanted to automate this from my build server and/or for installation in different environments.

The app file can be unzipped using PowerShell and we can modify the AppManifest.xml file to hold updated Client ID and redirect URL. The elements files containing app parts also contain these redirect URLs and must be changed. I also change the title of the app as I use the same Office 365 tenant for my different environments.

function Open-Zip
 {
    Param([string]$file, [string]$destination)

    $shell = new-object -com shell.application
    if (Test-Path($destination)) {
        Remove-Item $destination -Recurse -Confirm:$false -ea SilentlyContinue
    }
    $out = New-Item -Path $destination -type Directory -ea SilentlyContinue
    $zip = $shell.NameSpace($file)
    foreach($item in $zip.items())
    {
    $shell.Namespace($destination).copyhere($item)
    }
 }

 function Package-App {
    Param(
        [Parameter(Mandatory=$true)]
        [string]$Path, 
        [Parameter(Mandatory=$false)]
        [string]$Title, 
        [Parameter(Mandatory=$true)]
        [string]$ClientId, 
        [Parameter(Mandatory=$true)]
        [string]$RedirectUrl)

    # Rename to ZIP and open the App package  
    $zip = $Path.Replace(".app", ".zip");
    $out = Copy-item $Path -Destination $zip;  
    $zipFolder = $Path.Replace(".app", "\");    
    Open-Zip $zip $zipFolder

    # Get the original info
    [xml]$appManifest = Get-Content ($zipFolder + "AppManifest.xml")
    $orgRedirect = $appManifest.App.Properties.StartPage
    $orgRedirect = $orgRedirect.Substring(0, $orgRedirect.LastIndexOf("/"))
    $orgClientId = $appManifest.App.AppPrincipal.RemoteWebApplication.ClientId
    $title = $appManifest.App.Properties.Title
    if ($title.Length -eq 0) {
        $title = $appManifest.App.Properties.Title
    }

    # Replace the path and client ID in files
    $redirect = $RedirectUrl
    if ($redirect.EndsWith("/")) {
        $redirect = $redirect.SubString(0, $redirect.length - 1);
    }

    # Fetch XML files, i.e. elements and AppManifest
    $files = Get-ChildItem $zipFolder -Recurse -Include *.xml
    foreach ($file in $files)
    {
        $package = [System.IO.Packaging.Package]::Open($zip, [System.IO.FileMode]::Open)
            
        $fileName = ("/" + $file.Name)
        $manifestUri = New-Object System.Uri($fileName, [System.UriKind]::Relative)
        $partNameUri = [System.IO.Packaging.PackUriHelper]::CreatePartUri($manifestUri)
        try {
            $part = $package.GetPart($partNameUri)
            $partStream = $part.GetStream()
 
            # Perform replacement
            $reader = New-Object -Type System.IO.StreamReader -ArgumentList $partStream
            $content = $reader.ReadToEnd()
            $content = $content.Replace($orgRedirect, $redirect)
            $content = $content.Replace($appManifest.App.Properties.Title, $title)
            $content = $content.Replace($orgClientId, $ClientId)
 
            $partStream.Position = 0
            $partStream.SetLength(0)
 
            $writer = New-Object -TypeName System.IO.StreamWriter -ArgumentList $partStream
            $writer.Write($content)
            $writer.Flush()
        }
        catch { } # Exception occurs on [Content Types].xml for example due to naming. Ignore.
        finally { 
            $package.Close()
        }
    }                     
    
    # Clean up and rename
    Remove-Item $Path -Force
    Remove-Item $zipFolder -Recurse -Force
    Move-Item $zip $Path
 }

The script above will then generate a new .app file with the correct information. We can then install it into the corporate App Catalog.

$site= Get-SPSite https://yoursite/apps
$appCatalog = $site.RootWeb.Lists["Apps for SharePoint"]
$appSource = Get-ChildItem $Path # FROM PREVIOUS OPERATION
$stream = $appSource.OpenRead()
$out = $appCatalog.RootFolder.Files.Add($app.Package, $stream, $true)
if ($stream) {$stream.Dispose()}