Skip to content

Commit 59931b0

Browse files
committed
Its a Christmas Miracle
1 parent d47c989 commit 59931b0

File tree

5 files changed

+276
-1
lines changed

5 files changed

+276
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ pnpm-debug.log*
2222

2323
# jetbrains setting folder
2424
.idea/
25+
26+
.claude/

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "astro dev",
77
"build": "astro build",
88
"preview": "astro preview",
9-
"astro": "astro"
9+
"astro": "astro",
10+
"new": "node scripts/new-post.js"
1011
},
1112
"dependencies": {
1213
"@astrojs/rss": "^4.0.14",

scripts/new-post.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { createInterface } from "readline";
2+
import { mkdir, writeFile } from "fs/promises";
3+
import { existsSync } from "fs";
4+
5+
const rl = createInterface({
6+
input: process.stdin,
7+
output: process.stdout,
8+
});
9+
10+
function ask(question) {
11+
return new Promise((resolve) => {
12+
rl.question(question, (answer) => resolve(answer.trim()));
13+
});
14+
}
15+
16+
function slugify(text) {
17+
return text
18+
.toLowerCase()
19+
.replace(/[^a-z0-9]+/g, "-")
20+
.replace(/^-|-$/g, "");
21+
}
22+
23+
function getISODate() {
24+
const now = new Date();
25+
const offset = -now.getTimezoneOffset();
26+
const sign = offset >= 0 ? "+" : "-";
27+
const pad = (n) => String(Math.abs(n)).padStart(2, "0");
28+
const offsetStr = `${sign}${pad(Math.floor(offset / 60))}:${pad(offset % 60)}`;
29+
return now.toISOString().replace("Z", "").split(".")[0] + offsetStr;
30+
}
31+
32+
async function main() {
33+
console.log("\n📝 New Blog Post\n");
34+
35+
const title = await ask("Title: ");
36+
if (!title) {
37+
console.log("Title is required.");
38+
rl.close();
39+
return;
40+
}
41+
42+
const defaultSlug = slugify(title);
43+
const slugInput = await ask(`Slug [${defaultSlug}]: `);
44+
const slug = slugInput || defaultSlug;
45+
46+
const categoriesInput = await ask("Categories (comma-separated): ");
47+
const categories = categoriesInput
48+
? categoriesInput.split(",").map((c) => c.trim().toLowerCase())
49+
: [];
50+
51+
const description = await ask("Description (optional): ");
52+
53+
rl.close();
54+
55+
const year = new Date().getFullYear();
56+
const dir = `src/content/blog/${year}/${slug}`;
57+
58+
if (existsSync(dir)) {
59+
console.log(`\n❌ Directory already exists: ${dir}`);
60+
return;
61+
}
62+
63+
const frontmatter = [
64+
"---",
65+
`title: "${title}"`,
66+
`date: "${getISODate()}"`,
67+
`categories: [${categories.join(", ")}]`,
68+
];
69+
70+
if (description) {
71+
frontmatter.push(`description: "${description}"`);
72+
}
73+
74+
frontmatter.push("---", "", "Your content here...", "");
75+
76+
await mkdir(dir, { recursive: true });
77+
await writeFile(`${dir}/index.md`, frontmatter.join("\n"));
78+
79+
console.log(`\n✅ Created: ${dir}/index.md`);
80+
}
81+
82+
main();
295 KB
Loading
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
---
2+
title: "Creating Your Own MSBuild SDK (It's Easier Than You Think)"
3+
date: "2025-12-25T21:58:58-05:00"
4+
categories: [msbuild, dotnet]
5+
description: "An MSBuild SDK is basically a NuGet package that automatically imports .props and .targets files into your project. That's it. That's the whole thing."
6+
---
7+
8+
I'll be honest - I put off learning how MSBuild SDKs work for way too long. Every time I saw that `Sdk="Microsoft.NET.Sdk"` attribute at the top of a `.csproj` file, I just accepted it as magic and moved on. But recently I needed to create a custom SDK, and after banging my head against the wall for a bit, I finally figured it out.
9+
10+
Spoiler: it's not nearly as scary as I thought.
11+
12+
## What Even Is an MSBuild SDK?
13+
14+
Before we dive in, let's clear up what we're actually talking about. An MSBuild SDK is basically a NuGet package that automatically imports `.props` and `.targets` files into your project. That's it. That's the whole thing.
15+
16+
When you write:
17+
18+
```xml
19+
<Project Sdk="MyAwesome.Sdk/1.0.0">
20+
```
21+
22+
MSBuild goes "oh, you want that SDK?" and then imports `Sdk.props` at the very beginning and `Sdk.targets` at the very end of your project. Everything in between is your actual project content.
23+
24+
## The Folder Structure
25+
26+
Here's what your SDK package needs to look like:
27+
28+
```
29+
MyAwesome.Sdk/
30+
├── Sdk/
31+
│ ├── Sdk.props ← Imported first
32+
│ └── Sdk.targets ← Imported last
33+
└── MyAwesome.Sdk.csproj
34+
```
35+
36+
The `Sdk/` folder is the magic folder. MSBuild looks there specifically.
37+
38+
## Creating the Props File
39+
40+
The `.props` file runs before anything else in the project. This is where you set up defaults:
41+
42+
```xml
43+
<Project>
44+
<PropertyGroup>
45+
<!-- Set defaults that users can override -->
46+
<TargetFramework Condition="'$(TargetFramework)' == ''">net8.0</TargetFramework>
47+
<ImplicitUsings Condition="'$(ImplicitUsings)' == ''">enable</ImplicitUsings>
48+
49+
<!-- Properties you always want set -->
50+
<MyCustomProperty>true</MyCustomProperty>
51+
</PropertyGroup>
52+
</Project>
53+
```
54+
55+
See those `Condition` attributes? That's the key pattern. You're saying "only set this if the user hasn't already set it." This lets your SDK provide sensible defaults while still allowing customization.
56+
57+
## Creating the Targets File
58+
59+
The `.targets` file runs after the project content. This is where you do the real work:
60+
61+
```xml
62+
<Project>
63+
<!-- Auto-include certain files -->
64+
<ItemGroup Condition="'$(EnableDefaultMyItems)' != 'false'">
65+
<None Include="**/*.config" />
66+
</ItemGroup>
67+
68+
<!-- Add custom build targets -->
69+
<Target Name="MyCustomTarget" BeforeTargets="Build">
70+
<Message Importance="high" Text="Look ma, I'm in a custom SDK!" />
71+
</Target>
72+
73+
<!-- Validate configuration -->
74+
<Target Name="ValidateStuff" BeforeTargets="BeforeBuild">
75+
<Warning Condition="'$(SomeProperty)' == ''"
76+
Text="Hey, you probably want to set SomeProperty." />
77+
</Target>
78+
</Project>
79+
```
80+
81+
## Wrapping Other SDKs
82+
83+
Here's where it gets interesting. You probably don't want to recreate everything from scratch - you want to build *on top of* the existing .NET SDK. The pattern looks like this:
84+
85+
**Sdk.props:**
86+
87+
```xml
88+
<Project>
89+
<!-- Import the base SDK props first -->
90+
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
91+
92+
<!-- Then add your customizations -->
93+
<PropertyGroup>
94+
<MyCustomDefault>true</MyCustomDefault>
95+
</PropertyGroup>
96+
</Project>
97+
```
98+
99+
**Sdk.targets:**
100+
101+
```xml
102+
<Project>
103+
<!-- Your custom logic first -->
104+
<ItemGroup>
105+
<None Include="**/*.special" />
106+
</ItemGroup>
107+
108+
<!-- Then import base SDK targets -->
109+
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
110+
</Project>
111+
```
112+
113+
The order matters here. Props imports happen outside-in (base first, then yours), and targets happen inside-out (yours first, then base). At least, that's what made sense for my use case - your mileage may vary.
114+
115+
## The .csproj for Your SDK Package
116+
117+
Your SDK project itself is pretty minimal:
118+
119+
```xml
120+
<Project Sdk="Microsoft.NET.Sdk">
121+
<PropertyGroup>
122+
<TargetFramework>netstandard2.0</TargetFramework>
123+
<PackageId>MyAwesome.Sdk</PackageId>
124+
<Version>1.0.0</Version>
125+
<PackageType>MSBuildSdk</PackageType>
126+
<IncludeBuildOutput>false</IncludeBuildOutput>
127+
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
128+
</PropertyGroup>
129+
130+
<ItemGroup>
131+
<None Include="Sdk\**" Pack="true" PackagePath="Sdk" />
132+
</ItemGroup>
133+
</Project>
134+
```
135+
136+
The important bits:
137+
138+
- `PackageType>MSBuildSdk` - tells NuGet this is an SDK package
139+
- `IncludeBuildOutput>false` - you're not shipping a DLL, just the props/targets
140+
- That `ItemGroup` makes sure your `Sdk/` folder ends up in the right place in the package
141+
142+
## Testing Locally (The Part I Struggled With)
143+
144+
This is where I wasted the most time. You can't just `dotnet build` and expect Visual Studio to find your SDK. Here's what actually works:
145+
146+
1. Build your SDK package: `dotnet pack -c Release`
147+
2. Add your output folder as a local NuGet source
148+
3. Reference the exact version in your test project
149+
150+
Or, the sneaky way - during development, just use `Microsoft.NET.Sdk` in your test project and manually import your props/targets:
151+
152+
**Directory.Build.props:**
153+
154+
```xml
155+
<Project>
156+
<Import Project="../src/MyAwesome.Sdk/Sdk/Sdk.props" />
157+
</Project>
158+
```
159+
160+
**Directory.Build.targets:**
161+
162+
```xml
163+
<Project>
164+
<Import Project="../src/MyAwesome.Sdk/Sdk/Sdk.targets" />
165+
</Project>
166+
```
167+
168+
This way you can iterate without constantly rebuilding packages. Just switch to the real `Sdk="..."` reference when you're ready to ship.
169+
170+
## Common Gotchas
171+
172+
A few things that tripped me up:
173+
174+
1. **Props vs Targets confusion** - If your defaults aren't working, you probably put them in targets instead of props. Properties need to be set before the project content, not after.
175+
176+
2. **Condition syntax** - It's `Condition="'$(Prop)' == ''"` with single quotes inside double quotes. I mess this up constantly.
177+
178+
3. **Import order matters** - Some properties from other SDKs or build tools need to be set super early in props, before they get evaluated. If something isn't working, try moving it earlier in the import chain.
179+
180+
4. **The casing matters** - The folder must be `Sdk/` with a capital S. Lowercase won't work on case-sensitive file systems.
181+
182+
5. **NuGet caching will drive you insane** - When testing locally, NuGet aggressively caches packages. Either bump your version number every time, or clear the cache with `dotnet nuget locals all --clear`. Trust me on this one.
183+
184+
## Wrapping Up
185+
186+
Creating an MSBuild SDK isn't rocket science - it's really just packaging up props and targets files in a specific folder structure. The hardest part is figuring out which properties go where and what order to import things.
187+
188+
If you're building something that needs consistent project configuration across multiple projects, or you're wrapping complex build tooling behind a simpler interface, an SDK is a great way to go.
189+
190+
If you have questions or run into issues I didn't cover, let me know - I'm still learning this stuff myself and always happy to compare notes.

0 commit comments

Comments
 (0)