Synology DSM Package Template (Visual Studio extension)
A .vsix that adds a "Synology DSM Package (.spk)" entry to File → New → Project. The generated
project publishes a .NET app self-contained for a Synology NAS and assembles a DSM 7 .spk with the
pure-.NET spkpack tool — buildable entirely from Visual Studio, no Git Bash.
Prerequisites to build this extension
- Visual Studio 2022/2026 with the "Visual Studio extension development" workload.
- This is a classic VSIX project — build it with Visual Studio's MSBuild, not
dotnet build.
Build & install
- Open
Synology.PackageTemplate.Vsix.csproj in Visual Studio and build it (or run
MSBuild.exe Synology.PackageTemplate.Vsix.csproj /p:Configuration=Release).
- The build emits
bin\Release\Synology.PackageTemplate.Vsix.vsix. Double-click it to install into VS.
- Make the packaging package available: build/pack
Projects/Tungsten.Synology.Packaging so
Tungsten.Synology.Packaging lands on a feed the new project can restore from (this repo publishes
to \Source\Local NuGet). Run dotnet pack Projects/Tungsten.Synology.Packaging -c Release.
Use
File → New → Project → search "Synology" → Synology DSM Package (.spk). Then follow the generated
project's README.md: point SpkPublishProject at your app, set SpkAppExecutable, and build Release.
The creation wizard
The point of the wizard is that the developer needs to know nothing about DSM packaging. It asks
about the application, and derives the DSM concepts from the answers:
| You are asked |
What it decides |
| Display name / description / maintainer / version |
The INFO manifest and Package Center listing |
| Which project to ship — picked from a dropdown of the solution's projects (or "create a starter app for me") |
SpkPublishProject (as a relative path) and SpkAppExecutable (the linux apphost name) |
| "Does it have a web interface?" + port |
Web → DSM menu icon, pkg/ui/config, adminport/adminurl, and ASPNETCORE_URLS in the daemon script. Headless → none of that is generated at all |
| "What CPU does your Synology have?" |
SpkArch + SpkRuntime (x86_64/linux-x64 vs aarch64/linux-arm64) |
| What DSM should ask at install time |
WIZARD_UIFILES/install_uifile and the matching postinst that saves the answers to settings.env |
Implementation lives in Wizard/:
SpkModel — the answers plus the derived DSM values (arch, RID, app id) and validation.
SolutionScanner — walks the DTE solution (recursing solution folders) for candidate projects,
skipping package projects, and computes the relative path to the chosen one.
WizardWindow — the five-step WPF dialog.
GeneratedContent — writes the tailored start-stop-status, postinst, install_uifile,
pkg/ui/config and README.md. Always LF, never a BOM — DSM runs these with /bin/sh and
CRLF makes the shebang fail with a confusing "not found" on the NAS.
StarterApp — generates a working ASP.NET or worker project when asked, and adds it to the solution.
SpkProjectWizard — the IWizard: injects the $spk*$ replacements, uses ShouldAddProjectItem
to suppress boilerplate the chosen shape doesn't need, and writes the generated content in
RunFinished. Cancelling throws WizardBackoutException so VS cleans up.
Because the assembly now ships, IncludeAssemblyInVSIXContainer is true and the manifest carries a
Microsoft.VisualStudio.Assembly asset whose AssemblyName must match the .vstemplate's
<WizardExtension><Assembly> string exactly — otherwise VS cannot bind the wizard and project
creation fails.
The settings designer
Right-click a package project → Synology Package Settings… to edit its DSM settings as a form
instead of hand-editing Spk* properties. Same plain language as the wizard (web vs headless, NAS
processor), with a live "Builds to <id>-<version>-<rev>.spk" readout and a warning when
SpkPublishProject doesn't resolve to a real file.
The command is registered by a VSPackage (SynologyPackageVsPackage + SynologyPackage.vsct),
auto-loads on SolutionExists, and is hidden unless the selected project is actually a DSM package
(BeforeQueryStatus checks for SpkPackageId/SpkPublishProject).
Saving is surgical. SpkProjectFile loads the .csproj with PreserveWhitespace, changes only the
targeted property elements, and writes back with the original BOM/declaration/line-ending shape:
XDocument.Save would add a UTF-8 BOM and an <?xml?> declaration that SDK-style project
files don't have — which makes line 1 differ and turns every settings edit into a whole-file change
in source control. Both are matched to what the file actually had (OmitXmlDeclaration,
UTF8Encoding(hadBom), NewLineHandling.None).
- Comments, ordering, other properties and item groups are untouched. Verified against the real
Planetspite.Synology.csproj: editing five values produced a five-hunk diff and nothing else.
- Clearing "has a web interface" blanks rather than deletes
SpkDsmUiDir/SpkAdminPort/etc., so
the comments survive and switching back is one word. The packer omits empty INFO fields anyway.
The install-questions designer
Right-click a package project → DSM Install Questions… to design the form the NAS admin sees
during installation, without writing DSM's JSON. Add/remove/reorder questions, each with a label,
a variable name, a kind (Text, Password, Number), a default and a required flag.
Two things it does beyond editing JSON:
- Keeps
scripts/postinst in step. A question is useless if nothing saves the answer, so
postinst is regenerated to write every variable into settings.env. It is only rewritten while it
still carries the generator marker (GeneratedContent.GeneratedMarker) — a hand-customised script
is left alone and the dialog says so.
- Warns before it drops anything. Opening a file with rules the designer can't model (extra steps,
select lists, a custom regex, a max length) shows a warning that saving will replace them. Files the
designer itself wrote always reload losslessly.
Clearing every question deletes WIZARD_UIFILES entirely, so the package installs silently rather
than showing a blank page.
DSM validator spelling (important)
DSM's wizard validators are snake_case with a bare regex:
"validator": { "allow_blank": false, "min": 4, "regex": "^[0-9]+$" }
The ExtJS-style allowBlank / minLength / /^\d+$/ spellings are silently ignored by DSM — the
questions still render, but nothing is validated. This was found by parsing the shipping Planetspite
package (which uses the correct form) and was wrong in versions 1.1.0–1.2.0 of this extension.
Build notes (why the csproj looks the way it does)
Four non-obvious things are required to make a template-only VSIX build; don't "simplify" them away:
VSToolsPath is set explicitly from $(MSBuildExtensionsPath). VS 2022+ runs a 64-bit MSBuild,
where the classic MSBuildExtensionsPath32 default resolves to Program Files (x86) and misses
VSSDK\Microsoft.VsSDK.targets (error MSB4226).
Microsoft.CSharp.targets is imported — without it the project has no Build target (MSB4057) —
and Properties/AssemblyInfo.cs exists so the compiler has an input. The compiled assembly is not
shipped (IncludeAssemblyInVSIXContainer=false); it only exists to satisfy the C# build.
- Per-configuration
OutputPath blocks are declared, as classic csproj requires.
IncludeVSTemplatesInVSIXContainer (the custom target at the bottom) bridges the template into
the container. The VS SDK's d:Source="Project" asset flow assumes the VSIX references a separate
template project, so with the template in this same project the .vsix builds successfully but
silently without it. The target feeds TemplateProjectOutputGroup's zips into VSIXSourceItem
before GetVsixSourceItems runs.
Verify a build by listing the container — the template must be present, not just the manifest:
unzip -l bin\Release\Synology.PackageTemplate.Vsix.vsix
# expect ProjectTemplates/CSharp/SynologyDsmPackage/1033/... plus templateManifest0.1033.vstman
Other notes
- The generated project must stay a
.csproj. Visual Studio picks a project system purely by file
extension, and a custom extension (.spkproj) has no registered project type — a project
template does not register one. A solution referencing such a project fails to open entirely.
Microsoft.Build.NoTargets works fine under .csproj; Site/Planetspite.Site.csproj is the same
pattern. Giving the package project its own extension would require shipping a full CPS project-type
registration (pkgdef + project factory), which is far more than this is worth.
- The DSM control scripts are intentionally minimal. See
Server/synology/ (Planetspite Metrics) for
a production example with an install wizard, a supervisor/restart loop, and reverse-proxy support.
- To prompt for the package id/port/arch at creation time instead of editing the
.csproj afterward,
add an IWizard implementation and reference it from the .vstemplate <WizardExtension>.