A couple years ago, I set up my build to create separate packages for a main library a number of language resource files.
Project structure:
- proj
- localization
- lib.nuspec
- lib.de.nuspec
- lib.es.nuspec
- ...
One of the language nuspec files
<?xml version="1.0" encoding="utf-8"?>
<package>
<metadata minClientVersion="2.12">
<id>JsonSchema.Net.de</id>
<version>1.0.0</version>
<!-- ... -->
<language>de</language>
<dependencies>
<dependency id="JsonSchema.Net" version="6.0.0" />
</dependencies>
</metadata>
<files>
<file src="..\bin\Release\netstandard2.0\de\lib.resources.dll" target="lib\netstandard2.0\de" />
</files>
</package>
Creates multiple nuget packages
lib.nupkg - contains lib.dll and lib.resources.dll
lib.de.nupkg - contains only de/lib.resources.dll
lib.es.nupkg - contains only es/lib.resources.dll
...
I use a GitHub Actions matrix build to fan out packing the languages using nuget pack
, passing the nuspec file, and everything worked really well.
But recently, nuget.exe
stopped working on ubuntu because apparently it requires mono, which isn't supported anymore and has been removed from the ubuntu image.
Now I have to figure out how to do this with dotnet pack
, which requires a csproj file and doesn't support nuspec files.
I've tried creating a lib.de.csproj
file that just includes the resx file. It builds and includes the de/lib.de.resources.dll
, but it also includes a lib.de.dll
file, and I'm guessing that de/lib.de.resources.dll
won't be loadable by the lib.dll
. I don't think this is going to work in the end.
I've also considered creating different build profiles to the lib project, but I can't figure out how to exclude the lib.dll from the language packs, or if doing this is even supported.
I may end up needing to raise an issue in the dotnet cli repo, but I thought I'd ask here first.
It turns out there is some support for building custom packages directly from the project file. Here's what I did.
To start, in the .csproj file, I set <IncludeBuildOutput>false</IncludeBuildOutput>
. This prevents the packing step from using the build output.
The next step is to define what goes into the nuget files. This can be done with a bunch of this line inside an ItemGroup
:
<None Include="README.md" Pack="true" PackagePath="\" />
You have to include all of the files since there's no dedicated properties like in the nuspec file. The dotnet pack
command will automatically infer the files' purposes so long as you get them all in the right places.
Next up, you don't want all of these files to appear in your project in Visual Studio, so you'll want to put a condition on the ItemGroup
:
<ItemGroup Condition="'$(ResourceLanguage)' == 'base'">
The condition means the ItemGroup
applies only when the ResourceLanguage
property equals base
, which we'll use to indicate the main library. What is the ResourceLanguage
property? I made it up. It's not actually defined until you include it in the dotnet pack
command:
dotnet pack -p:ResourceLanguage=base
The new section should look something like this:
<ItemGroup Condition="'$(ResourceLanguage)' == 'base'">
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
<None Include="..\..\Resources\logo-256.png" Pack="true" PackagePath="\" />
<None Include="bin\$(Configuration)\netstandard2.0\lib.dll" Pack="true" PackagePath="lib\netstandard2.0" />
<None Include="bin\$(Configuration)\netstandard2.0\lib.xml" Pack="true" PackagePath="lib\netstandard2.0" />
<None Include="bin\$(Configuration)\netstandard2.0\lib.pdb" Pack="true" PackagePath="lib\netstandard2.0" />
<None Include="bin\$(Configuration)\net8.0\lib.dll" Pack="true" PackagePath="lib\net8.0" />
<None Include="bin\$(Configuration)\net8.0\lib.xml" Pack="true" PackagePath="lib\net8.0" />
<None Include="bin\$(Configuration)\net8.0\lib.pdb" Pack="true" PackagePath="lib\net8.0" />
<None Include="bin\$(Configuration)\net9.0\lib.dll" Pack="true" PackagePath="lib\net9.0" />
<None Include="bin\$(Configuration)\net9.0\lib.xml" Pack="true" PackagePath="lib\net9.0" />
<None Include="bin\$(Configuration)\net9.0\lib.pdb" Pack="true" PackagePath="lib\net9.0" />
</ItemGroup>
You'll also want to include a PropertyGroup
with the same condition that contains any package properties unique to the base package.
<PropertyGroup Condition="'$(ResourceLanguage)' == 'base'">
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageId>lib-name</PackageId>
<Description>lib description</Description>
<Version>1.2.3</Version>
<PackageTags>some tags</PackageTags>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
That takes care of the main package. Now we need to do this again for the language packs.
<PropertyGroup Condition="'$(ResourceLanguage)' != '' And '$(ResourceLanguage)' != 'base'">
<PackageId>lib.$(ResourceLanguage)</PackageId>
<PackageTags>some tags language pack</PackageTags>
</PropertyGroup>
<ItemGroup Condition="'$(ResourceLanguage)' != '' And '$(ResourceLanguage)' != 'base'">
<None Include="README.$(ResourceLanguage).md" Pack="true" PackagePath="\README.md" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
<None Include="..\..\Resources\logo-256.png" Pack="true" PackagePath="\" />
<None Include="bin\$(Configuration)\netstandard2.0\$(ResourceLanguage)\lib.resources.dll" Pack="true" PackagePath="lib\netstandard2.0\$(ResourceLanguage)" />
<None Include="bin\$(Configuration)\net8.0\$(ResourceLanguage)\lib.resources.dll" Pack="true" PackagePath="lib\net8.0\$(ResourceLanguage)" />
<None Include="bin\$(Configuration)\net9.0\$(ResourceLanguage)\lib.resources.dll" Pack="true" PackagePath="lib\net9.0\$(ResourceLanguage)" />
</ItemGroup>
And finally, because each language pack has its own version, and I like to have the language in the description, I used additional PropertyGroup
s for each language I support:
<PropertyGroup Condition="'$(ResourceLanguage)' == 'de'">
<Description>lib Locale German (de)</Description>
<Version>1.0.1</Version>
</PropertyGroup>
Now I can run a similar dotnet
command for each of the languages I support:
dotnet pack -p:ResourceLanguage=de
The final process for my GH Action is
cd lib-proj-folder
dotnet restore # restore packages
dotnet build -c Release --no-restore # build
dotnet pack -c Release --no-build -p:ResourceLanguage=base # pack base lib
dotnet pack -c Release --no-build -p:ResourceLanguage=de # pack language de
# ...
The only thing I wasn't able to figure out is the dependencies for the language packs. They're currently the dependencies of the main lib. I think it detects the dependencies from the obj/
folder. I tried putting the condition on the ItemGroup
s with the project and package references, but it didn't have any effect on the pack command.
You can view the final project file here and the GH Action file here.