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.csprojin Visual Studio and build it (or runMSBuild.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.PackagingsoTungsten.Synology.Packaginglands on a feed the new project can restore from (this repo publishes to\Source\Local NuGet). Rundotnet 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 tailoredstart-stop-status,postinst,install_uifile,pkg/ui/configandREADME.md. Always LF, never a BOM — DSM runs these with/bin/shand 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— theIWizard: injects the$spk*$replacements, usesShouldAddProjectItemto suppress boilerplate the chosen shape doesn't need, and writes the generated content inRunFinished. Cancelling throwsWizardBackoutExceptionso 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.Savewould 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/postinstin step. A question is useless if nothing saves the answer, sopostinstis regenerated to write every variable intosettings.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:
VSToolsPathis set explicitly from$(MSBuildExtensionsPath). VS 2022+ runs a 64-bit MSBuild, where the classicMSBuildExtensionsPath32default resolves toProgram Files (x86)and missesVSSDK\Microsoft.VsSDK.targets(error MSB4226).Microsoft.CSharp.targetsis imported — without it the project has noBuildtarget (MSB4057) — andProperties/AssemblyInfo.csexists so the compiler has an input. The compiled assembly is not shipped (IncludeAssemblyInVSIXContainer=false); it only exists to satisfy the C# build.- Per-configuration
OutputPathblocks are declared, as classic csproj requires. IncludeVSTemplatesInVSIXContainer(the custom target at the bottom) bridges the template into the container. The VS SDK'sd:Source="Project"asset flow assumes the VSIX references a separate template project, so with the template in this same project the.vsixbuilds successfully but silently without it. The target feedsTemplateProjectOutputGroup's zips intoVSIXSourceItembeforeGetVsixSourceItemsruns.
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.NoTargetsworks fine under.csproj;Site/Planetspite.Site.csprojis 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
.csprojafterward, add anIWizardimplementation and reference it from the.vstemplate<WizardExtension>.