Post

How-to Pack Your Shell Module for Optimizely Content Cloud

Optimizely Content Cloud is approaching fast and partners are working on getting modules out on the feed for the rest of us to test.

As you know - Optimizely vNext is targeting .NET 5 and with this fundamental platform change - there are few changes also how packaging of the module’s assets should be done.

This blog post should cover the most common tasks for you to pack properly Optimizely Content Cloud module.

Thanks to Mark and Māris for initial version and ideas around how to pack our stuff.

If your module has Views - story is simpler there. More details here.

Todo List

During module packaging process there are few things that we need to do:

  • copy module.config any client-side assets to temporary directory (assets usually go into directory named after package version);
  • if your module has client-side assets - you have to ensure that module.config file points to correct path (including version number) e.g.:
1
2
3
4
<module
        loadFromBin="false"
        clientResourceRelativePath="7.0.0-pre-0002"
        ...
  • pack everything up into .zip file

NB! Even if your module does not have any client-side assets - you still need to include module.config file in .zip file. Here is a tracking issue (to install module without module.config file). And install module provider sounds hackish anyway :)

  • include .zip file in target NuGet package under specific paths.

So let’s get started.

The Pack Script

0. Create Build File and Include in Project

To get things unified and reusable - let’s define .proj file that we will include in .csproj file - for the project that requires to be packed.

Let’s name file pack.proj and place it in src/ folder.

Now we can include that one in .csproj file:

1
<Import Project="$(SolutionDir)\src\pack.proj" Condition="Exists('$(SolutionDir)\src\pack.proj')" />

1. Copy Stuff To Temp Directory

First we need to define what is temp directory

1
2
3
<PropertyGroup>
    <TmpOutDir>$(SolutionDir)\tmp</TmpOutDir>
</PropertyGroup>

Then we have to script how stuff is copied over to temp directory. This target will be called always after successful Build target invocation - basically when project is built.

This script assumes that your Optimizely client-side assets are in module\ folder in project structure.

1
2
3
4
5
6
7
8
9
10
11
12
13
  <Target Name="CreateZip" AfterTargets="Build">

    <MakeDir Directories="$(TmpOutDir)\content\$(Version)" />

    <ItemGroup>
      <ClientResources Include="$(SolutionDir)\src\$(MSBuildProjectName)\module\ClientResources\**\*" />
    </ItemGroup>

    <Copy SourceFiles="@(ClientResources)" DestinationFiles="@(ClientResources -> '$(TmpOutDir)\content\$(Version)\ClientResources\%(RecursiveDir)%(Filename)%(Extension)')" />

    <Copy SourceFiles="$(SolutionDir)\src\$(MSBuildProjectName)\module\module.config" DestinationFolder="$(TmpOutDir)\content" />

  </Target>

Notes:

  • we define ClientResources variable pointing to all files under module/ClientResources folder;
  • copy over those in tmp/content/{version}/
  • copy over also module.config file un tmp/content/ folder;

2. Set Client Resource Relative Path

Before we pack them up - we need to set correct client-side asset relative path in module.config file. This could be done by XmlPoke task:

1
2
3
4
<XmlPoke
    XmlInputPath="$(TmpOutDir)\content\module.config"
    Query="/module/@clientResourceRelativePath"
    Value="$(Version)" />

3. Zip It Up

Now we are ready to zip it up and remove temp folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<UsingTask TaskName="ZipDirectory" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
  <ParameterGroup>
    <InputPath ParameterType="System.String" Required="true" />
    <OutputFileName ParameterType="System.String" Required="true" />
    <OverwriteExistingFile ParameterType="System.Boolean" Required="false" />
  </ParameterGroup>
  <Task>
    <Using Namespace="System.IO" />
    <Using Namespace="System.IO.Compression" />
    <Code Type="Fragment" Language="cs">
      <![CDATA[
        if(this.OverwriteExistingFile) {
          File.Delete(this.OutputFileName);
        }

        ZipFile.CreateFromDirectory(this.InputPath, this.OutputFileName);
      ]]>
    </Code>
  </Task>
</UsingTask>

<ZipDirectory
  InputPath="$(TmpOutDir)\content"
  OutputFileName="$(OutDir)\$(MSBuildProjectName).zip"
  OverwriteExistingFile="true" />
 <RemoveDir Directories="$(TmpOutDir)" />

$(OutDir) - points to target folder where project is being packed.

4. Include .zip File in NuGet Package

In order to include .zip file in NuGet package we have to specify that file is part of the package and also specify its location within the package folder tree system. This is accomplished with help of Pack and PackagePath properties defined for the Content.

1
2
3
4
5
6
7
<Target Name="CreateZip" AfterTargets="Build">
  <MakeDir Directories="$(TmpOutDir)\content\$(Version)" />
  <ItemGroup>
    <Content Include="$(OutDir)\$(MSBuildProjectName).zip" >
    <Pack>true</Pack>
    <PackagePath>content\modules\_protected\$(MSBuildProjectName)\;contentFiles\any\net5.0\modules\_protected\$(MSBuildProjectName)\</PackagePath>
</Content>

NB! Shell .zip files must be present in two locations:

  • under content\modules\_protected\..
  • and under contentFiles\any\net5.0\modules\_protected\..

Copy Module Files On Host Project Build

Here “host” project is consuming project - one where the package has been installed.

There is a catch. When you install NuGet package and want to include some files in the host project - files are added as “shortcuts”:

added-module-shortcut

Checking file properties you can see that files are added (as shortcut) from NuGet cache folder: C:\Users\{user}\.nuget\packages\{module}\{version}\contentFiles\any\net5.0\modules\_protected\{module}\{module}.zip

Obviously this file is required to be present on the disk under project folder. We need additional file to get this file copied. We have to create CopyZipFiles.targets file. This file hook into different host project events and perform some actions. This time - we will copy over file before Build target.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
  <ItemGroup>
    <SourceScripts
      Include="$(MSBuildThisFileDirectory)..\..\contentFiles\any\net5.0\modules\_protected\**\*.zip"/>
  </ItemGroup>

  <Target Name="CopyZipFiles" BeforeTargets="Build">
    <Copy
      SourceFiles="@(SourceScripts)"
      DestinationFolder="$(MSBuildProjectDirectory)\modules\_protected\%(RecursiveDir)"
/>
  </Target>
</Project>

Here we basically take all files in NuGet package contentFiles\any\net5.0\modules\_protected\ folder and copy to host project file system.

For the package to be able to execute some actions “inside” host project context - this target file also needs to be included in NuGet package with specific name (same as package name) and under specific folder (build).

To achieve this - we can use Pack and PackagePath settings:

1
2
3
4
5
6
<ItemGroup>
  <Content Include="$(SolutionDir)\src\$(MSBuildProjectName)\CopyZipFiles.targets" >
    <Pack>true</Pack>
    <PackagePath>build\net5.0\$(MSBuildProjectName).targets</PackagePath>
  </Content>
</ItemGroup>

Resulting NuGet Package

Packaging process is exactly the same as for any other .NET project:

1
> dotnet pack

At the end NuGet package should look something like this:

package

I hope you will use other package ID as shown above :) otherwise we might collide..

Sample Working Project

Localization Provider beta version for upcoming Optimizely version is using this approach. Here is reference to GitHub repo.

Full File Versions

The Pack Script

Here is full pack script file for completeness:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?xml version="1.0" encoding="utf-8"?>

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="15.0">

  <UsingTask TaskName="ZipDirectory" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <InputPath ParameterType="System.String" Required="true" />
      <OutputFileName ParameterType="System.String" Required="true" />
      <OverwriteExistingFile ParameterType="System.Boolean" Required="false" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System.IO" />
      <Using Namespace="System.IO.Compression" />
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          if(this.OverwriteExistingFile) {
            File.Delete(this.OutputFileName);
          }
          ZipFile.CreateFromDirectory(this.InputPath, this.OutputFileName);
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <PropertyGroup>
    <SolutionDir Condition="$(SolutionDir) == ''">$(MSBuildProjectDirectory)\..\</SolutionDir>
    <TmpOutDir>$(SolutionDir)\tmp</TmpOutDir>
  </PropertyGroup>

  <Target Name="CreateZip" AfterTargets="Build">
    <MakeDir Directories="$(TmpOutDir)\content\$(Version)" />
    <ItemGroup>
      <ClientResources Include="$(SolutionDir)\src\$(MSBuildProjectName)\module\ClientResources\**\*" />
    </ItemGroup>
    <Copy SourceFiles="$(SolutionDir)\src\$(MSBuildProjectName)\module\module.config" DestinationFolder="$(TmpOutDir)\content" />
    <Copy SourceFiles="@(ClientResources)" DestinationFiles="@(ClientResources -> '$(TmpOutDir)\content\$(Version)\ClientResources\%(RecursiveDir)%(Filename)%(Extension)')" />
    <XmlPoke XmlInputPath="$(TmpOutDir)\content\module.config" Query="/module/@clientResourceRelativePath" Value="$(Version)" />
    <ZipDirectory
      InputPath="$(TmpOutDir)\content"
      OutputFileName="$(OutDir)\$(MSBuildProjectName).zip"
      OverwriteExistingFile="true" />

    <!-- <RemoveDir Directories="$(TmpOutDir)" /> -->
  </Target>

  <ItemGroup>
    <Content Include="$(OutDir)\$(MSBuildProjectName).zip" >
      <Pack>true</Pack>
      <PackagePath>content\modules\_protected\$(MSBuildProjectName)\;contentFiles\any\net5.0\modules\_protected\$(MSBuildProjectName)\</PackagePath>
    </Content>
    <Content Include="$(SolutionDir)\src\$(MSBuildProjectName)\CopyZipFiles.targets" >
      <Pack>true</Pack>
      <PackagePath>build\net5.0\$(MSBuildProjectName).targets</PackagePath>
    </Content>
  </ItemGroup>
</Project>

Copy Zip File Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
  <ItemGroup>
    <SourceScripts
      Include="$(MSBuildThisFileDirectory)..\..\contentFiles\any\net5.0\modules\_protected\**\*.zip"/>
  </ItemGroup>

  <Target Name="CopyZipFiles" BeforeTargets="Build">
    <Copy
      SourceFiles="@(SourceScripts)"
      DestinationFolder="$(MSBuildProjectDirectory)\modules\_protected\%(RecursiveDir)"
/>
  </Target>
</Project>

Happy packaging! [eof]

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.