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 undermodule/ClientResources
folder; - copy over those in
tmp/content/{version}/
- copy over also
module.config
file untmp/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”:
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:
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]
Comments powered by Disqus.