Skip to content

Commit 497e94e

Browse files
committed
add post about updating the build for json schema
1 parent 7fc9aee commit 497e94e

File tree

1 file changed

+181
-0
lines changed

1 file changed

+181
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
---
2+
title: ""
3+
date: 2024-10-26 09:00:00 +1200
4+
tags: [.net, github-actions, build, ci-cd, learning]
5+
toc: true
6+
pin: false
7+
---
8+
9+
Last week I discovered that my pack and publish builds for _JsonSchema.Net_ and its language packs were failing. Turns out _nuget.exe_ isn't supported in Ubuntu Linux anymore. In this post I'm going to describe the solution I found.
10+
11+
## The build that was
12+
13+
Rewind two and a half years. I've added the `ErrorMessages` class to _JsonSchema.Net_ and I want to be able to support multiple languages on-demand, the way [Humanizr](https://github.com/Humanizr/Humanizer) does: a base package that supports English, and satellite language packs. (They also publish a meta-package that pulls all of the languages, but I didn't want to do that.)
14+
15+
So the first thing to do was check out how they were managing their build process. After some investigation, it seemed they were using `nuget pack` along with a series of custom _.nuspec_ files. The big change for me was that they weren't using the built-in "pack on build" feature of `dotnet`, which is what I was using.
16+
17+
So I worked it up. The final solution had three parts:
18+
19+
- Build the library
20+
- Pack and push _JsonSchema.Net_
21+
- Pack and push the language packs
22+
23+
The first two steps were pretty straighforward. The language packs step utilized a [GitHub Actions matrix](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow) that I built by scanning the file system for _.nuspec_ files during the build step. And to run the pack and push, I used _nuget.exe_ which was provided by the [`nuget/setup-nuget`](https://github.com/NuGet/setup-nuget) action.
24+
25+
Everything was great.
26+
27+
Until it wasn't.
28+
29+
## Sadness ensues
30+
31+
As I mentioned, last week I discovered that the workflow was failing, so I went to investigate. Turns out the failing action was `nuget/setup-nuget`: simply installing the Nuget CLI.
32+
33+
After some investigation, I found that [the Nuget CLI requires Mono](https://github.com/NuGet/setup-nuget/issues/168#issuecomment-2573539599), and Mono is now out of support. I never had to install Mono, so either it was pre-installed on the Ubuntu image or the Nuget setup action installed it as a pre-requisite. Probably the latter. And now, since Mono is no longer supported, they don't do that anymore. Whatever the reason, the action wasn't working, so I couldn't use the Nuget CLI.
34+
35+
That means I need to figure out how to use the `dotnet` CLI to build a custom Nuget package. But there's a problem with that: `dotnet pack` doesn't support _.nuspec_ files; it only works on project files, like _.csproj_.
36+
37+
The help I needed came from [Glenn Watson](https://github.com/glennawatson) from the .Net Foundation. I happened to comment about the build not working and he was able to point me to another project that built custom Nuget packages with `dotnet pack` and the the project file.
38+
39+
After about four hours of playing with it, I finally landed on something that worked enough. It's not perfect, but it does the job.
40+
41+
## Building the base package
42+
43+
To start, I just wanted to see if I could get the main package built. Then I'd move on to the language packs.
44+
45+
I learned from the other project that to build a custom Nuget package, I need to do two things:
46+
47+
- Prevent the packing step from using the build output by using
48+
```xml
49+
<IncludeBuildOutput>false</IncludeBuildOutput>
50+
```
51+
- Create an `ItemGroup` with a bunch of entries to indicate the files that need to go into the package.
52+
```xml
53+
<None Include="README.md" Pack="true" PackagePath="\" />
54+
```
55+
56+
Doing it this way does mean that you have to explicitly list every file that is supposed to be in the package. This is basically the same as using _nuget.exe_ with a _.nuspec_ file, so I really already had the list of files I needed, just in a different format.
57+
58+
This new `ItemGroup` had a side effect, though. I could see all of these files in my project in Visual Studio. To fix this, I put a condition on the `ItemGroup` that defaults to false.
59+
60+
```xml
61+
<ItemGroup Condition="'$(ResourceLanguage)' == 'base'">
62+
```
63+
64+
This condition means the `ItemGroup` only applies when the `ResourceLanguage` property equals `base`, which we'll use to indicate the main library. What's the `ResourceLanguage` property? I made it up. Apparently you can just make up properties and then define them on various `dotnet` commands:
65+
66+
```sh
67+
dotnet pack -p:ResourceLanguage=base
68+
```
69+
70+
> The property's default value is nothing, which gives an empty string... and an empty string doesn't equal `base`, so we've successfully hidden the package files while still having access to them during the packing process.
71+
{: prompt-hint }
72+
73+
The new section now looks like this:
74+
75+
```xml
76+
<ItemGroup Condition="'$(ResourceLanguage)' == 'base'">
77+
<None Include="README.md" Pack="true" PackagePath="\" />
78+
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
79+
<None Include="..\..\Resources\json-logo-256.png"
80+
Pack="true" PackagePath="\" />
81+
<None Include="bin\$(Configuration)\netstandard2.0\JsonSchema.Net.dll"
82+
Pack="true" PackagePath="lib\netstandard2.0" />
83+
<None Include="bin\$(Configuration)\netstandard2.0\JsonSchema.Net.xml"
84+
Pack="true" PackagePath="lib\netstandard2.0" />
85+
<None Include="bin\$(Configuration)\netstandard2.0\JsonSchema.Net.pdb"
86+
Pack="true" PackagePath="lib\netstandard2.0" />
87+
<None Include="bin\$(Configuration)\net8.0\JsonSchema.Net.dll"
88+
Pack="true" PackagePath="lib\net8.0" />
89+
<None Include="bin\$(Configuration)\net8.0\JsonSchema.Net.xml"
90+
Pack="true" PackagePath="lib\net8.0" />
91+
<None Include="bin\$(Configuration)\net8.0\JsonSchema.Net.pdb"
92+
Pack="true" PackagePath="lib\net8.0" />
93+
<None Include="bin\$(Configuration)\net9.0\JsonSchema.Net.dll"
94+
Pack="true" PackagePath="lib\net9.0" />
95+
<None Include="bin\$(Configuration)\net9.0\JsonSchema.Net.xml"
96+
Pack="true" PackagePath="lib\net9.0" />
97+
<None Include="bin\$(Configuration)\net9.0\JsonSchema.Net.pdb"
98+
Pack="true" PackagePath="lib\net9.0" />
99+
</ItemGroup>
100+
```
101+
102+
Using the command line (because that's what's going to run in the GitHub workflow), I built the project and ran the pack command. Sure enough, I got a Nuget package that was properly versioned and contained all of the right files!
103+
104+
Step 1 complete.
105+
106+
## Building language packs
107+
108+
The language pack Nuget files carry different package names, versions, and descriptions. In order to support this, we need to isolate the properties for the base package by defining a `PropertyGroup` for the base package that also has the condition from before so that those properties don't get mixed into the language packs.
109+
110+
```xml
111+
<PropertyGroup Condition="'$(ResourceLanguage)' == 'base'">
112+
<IncludeSymbols>true</IncludeSymbols>
113+
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
114+
<PackageId>JsonSchema.Net</PackageId>
115+
<Description>JSON Schema built on the System.Text.Json namespace</Description>
116+
<Version>7.3.2</Version>
117+
<PackageTags>json-schema validation schema json</PackageTags>
118+
<EmbedUntrackedSources>true</EmbedUntrackedSources>
119+
</PropertyGroup>
120+
```
121+
122+
Now we can define an additional `PropertyGroup` and `ItemGroup` for when `ResourceLanguage` isn't nothing (remember, nothing is for Visual Studio and the code build) and isn't `base` (for the base package).
123+
124+
```xml
125+
<PropertyGroup Condition="'$(ResourceLanguage)' != '' And '$(ResourceLanguage)' != 'base'">
126+
<PackageId>JsonSchema.Net.$(ResourceLanguage)</PackageId>
127+
<PackageTags>json-schema validation schema json error language-pack</PackageTags>
128+
</PropertyGroup>
129+
130+
<ItemGroup Condition="'$(ResourceLanguage)' != '' And '$(ResourceLanguage)' != 'base'">
131+
<None Include="Localization\README.$(ResourceLanguage).md"
132+
Pack="true" PackagePath="\README.md" />
133+
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
134+
<None Include="..\..\Resources\json-logo-256.png"
135+
Pack="true" PackagePath="\" />
136+
<None Include="bin\$(Configuration)\netstandard2.0\$(ResourceLanguage)\JsonSchema.Net.resources.dll"
137+
Pack="true" PackagePath="lib\netstandard2.0\$(ResourceLanguage)" />
138+
<None Include="bin\$(Configuration)\net8.0\$(ResourceLanguage)\JsonSchema.Net.resources.dll"
139+
Pack="true" PackagePath="lib\net8.0\$(ResourceLanguage)" />
140+
<None Include="bin\$(Configuration)\net9.0\$(ResourceLanguage)\JsonSchema.Net.resources.dll"
141+
Pack="true" PackagePath="lib\net9.0\$(ResourceLanguage)" />
142+
</ItemGroup>
143+
```
144+
145+
> Also notice that I've also incorporated the `ResourceLanguage` property to identify the correct paths.
146+
{: .prompt-info}
147+
148+
And finally, I used an additional `PropertyGroup` for each language I support so that they can each get their own description and version:
149+
150+
```xml
151+
<PropertyGroup Condition="'$(ResourceLanguage)' == 'de'">
152+
<Description>JsonSchema.Net Locale German (de)</Description>
153+
<Version>1.0.1</Version>
154+
</PropertyGroup>
155+
```
156+
157+
Now I can run a similar `dotnet` command for each of the languages I support:
158+
159+
```sh
160+
dotnet pack -p:ResourceLanguage=de
161+
```
162+
163+
## Updating the workflow
164+
165+
The final thing I needed to update was the GH Actions workflow.
166+
167+
I still like the idea of using the matrix, but now I don't have the nuspec files I used previously to generate the list of languages. But I do know all of the languages I support, and that list doesn't update much, so I can just list it explicitly in the workflow file and update as needed.
168+
169+
Also, I found that including `base` as one of the options also packs the base library, so I don't need a separate job for it, which is nice.
170+
171+
Now I just have a single matrixed job that runs for `base` and all of the languages. (Link to the workflow at the end of the post.)
172+
173+
## That's good enough
174+
175+
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 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. Because of this and a feedback I got while trial-and-erroring this, I suspect it detects the dependencies from the `obj/` folder rather than from the _.csproj_ file.
176+
177+
You can view the final project file [here](https://github.com/json-everything/json-everything/blob/28090a609bcc39bbd77c3c28501b522dea600d34/src/JsonSchema/JsonSchema.csproj) and the GH Actions workflow file [here](https://github.com/json-everything/json-everything/blob/28090a609bcc39bbd77c3c28501b522dea600d34/.github/workflows/publish-schema.yml).
178+
179+
I've also opened an issue on Humanizr to let them know of the solution I found in case they encounter the same problem.
180+
181+
_If you like the work I put out, and would like to help ensure that I keep it up, please consider [becoming a sponsor](https://github.com/sponsors/gregsdennis)!_

0 commit comments

Comments
 (0)