Search code examples
.netnugetnuget-package

Create a Nuget package with just a resources file using dotnet pack


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.


Solution

  • 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 PropertyGroups 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 ItemGroups 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.