This document describes how the TALXIS build packages are layered, how their MSBuild files are distributed through NuGet, and what each project-type package actually wires into a consumer build. For the version-numbering rules themselves, see Versioning.md.
The repository is organized as three layers:
| Layer | Package(s) | Responsibility |
|---|---|---|
| Core tasks | TALXIS.DevKit.Build.Dataverse.Tasks |
Ships the task assembly plus reusable MSBuild targets/tasks such as Git version generation, XML/JSON validation, solution packaging helpers, assembly merge, CMT data handling, and GenPage helpers. |
| Project-type packages | ...Solution, ...PdPackage, ...Plugin, ...WorkflowActivity, ...Pcf, ...ScriptLibrary, ...CodeApp, ...GenPage |
Add the build hooks for one specific ProjectType. Most of them depend on the Tasks package and, where relevant, on Microsoft Power Apps MSBuild packages. |
| SDK | TALXIS.DevKit.Build.Sdk |
Entry point for consumers. It resolves TALXIS.DevKit.Build.Dataverse.$(ProjectType) and adds it as a package reference automatically. |
A project using the SDK sets ProjectType and uses TALXIS.DevKit.Build.Sdk as its MSBuild SDK. src/Sdk/Sdk/Sdk.targets computes the package name as:
TALXIS.DevKit.Build.Dataverse.$(ProjectType)
and adds it as a PackageReference with the same version as the SDK package itself. The version is derived from the SDK package directory in the NuGet cache, so the SDK and the resolved project-type package stay aligned.
The SDK also sets a default TargetFramework (net472 if the consumer does not set one) and enables GitVersionNumber=true by default.
The automatic wiring relies on NuGet/MSBuild conventions:
- files under
build/are imported automatically for a direct package reference - files under
buildTransitive/are imported automatically for transitive package references - the imported
.propsand.targetsfiles can thenImportthe real implementation from elsewhere in the package
That is exactly how these packages are built:
TALXIS.DevKit.Build.Dataverse.Tasks.csproj packs:
msbuild/build/*.*->build/msbuild/buildMultiTargeting/*.*->buildMultiTargeting/msbuild/buildTransitive/*.*->buildTransitive/msbuild/tasks/**/*->tasks/- the compiled task assembly ->
tasks/net10.0/
Its build, buildMultiTargeting, and buildTransitive files are thin forwarding imports to tasks/TALXIS.DevKit.Build.Dataverse.Tasks.props and .targets.
The real tasks/...Tasks.targets file then:
- imports
Props/*.propsandTargets/*.targets - registers the task assembly with many
UsingTaskentries (GenerateGitVersion,InvokeSolutionPackager,ValidateXmlFiles,MergeCmtDataXml,ValidatePcfDependencies,PatchGenPageCompiledCode, etc.)
Each project-type .nuspec maps:
msbuild\tasks\*.*->tasks/msbuild\build\*.*->build/
The build/*.props / build/*.targets files are again small forwarding imports to the real files in tasks/.
Because the project-type packages declare TALXIS.DevKit.Build.Dataverse.Tasks as a dependency, the Tasks package arrives transitively. Its buildTransitive/ files then make the shared task registrations available automatically in the consumer build.
A consumer typically only needs one of:
Sdk="TALXIS.DevKit.Build.Sdk"plus<ProjectType>...- or a direct
PackageReferenceto a project-type package
After restore, MSBuild imports the package's build/*.props/build/*.targets, those import the real tasks/*.props/tasks/*.targets, and the transitive Tasks package contributes the shared task assembly and common targets.
The Tasks package is mostly a library of callable targets and registered tasks, not a full build pipeline on its own.
The important point is that it does not automatically attach versioning/validation/packaging to the normal Build target. Targets such as GenerateVersionNumber, ApplyVersionNumber, ApplyPluginVersionNumber, ApplyPcfVersionNumber, ValidateSolutionComponentSchema, InitializeSolutionPackagerWorkingDirectory, and PackDataverseSolution are available to be called by higher-level packages or by a consumer project explicitly.
The one built-in hook in this package is PackCanvasApps, which runs BeforeTargets="InitializeSolutionPackagerWorkingDirectory" and cleans stale CanvasApps/*.msapp files when that helper target is used.
Several project types cooperate through helper targets rather than through normal .NET compilation alone:
- most project-type packages expose
GetProjectType PluginexposesGetPluginAssemblyInfoWorkflowActivityexposesGetWorkflowActivityAssemblyInfoScriptLibraryexposesGetScriptLibraryOutputsandGetSuppressedScriptLibraryReferencesCodeAppexposesGetCodeAppOutputsGenPageexposesGetGenPageOutputs
The Solution and PdPackage packages use these helper targets to classify ProjectReference entries and stage the correct outputs into solution/package metadata.
TALXIS.DevKit.Build.Dataverse.Solution depends on Microsoft.PowerApps.MSBuild.Solution and the shared Tasks package. Its build logic is split across several imported .targets files.
Main hooks added by the package:
| Target | Hook | Purpose |
|---|---|---|
PatchSolutionXmlWorkingDirectory |
AfterTargets="CopyCdsSolutionContent" / BeforeTargets="ProcessCdsProjectReferencesOutputs" |
Patches copied Other\Solution.xml in the metadata working directory. |
_PrepareSolutionContentBeforeProcessCds |
BeforeTargets="ProcessCdsProjectReferencesOutputs" |
Calls a placeholder ApplyPluginVersionNumberInSolution target after CopyCdsSolutionContent. |
_ApplySolutionVersionAfterProcessCds |
AfterTargets="ProcessCdsProjectReferencesOutputs" |
Runs GenerateVersionNumber and ApplyVersionNumber for the solution content. |
_EnsureCustomizationNodesBeforePackage |
AfterTargets="ProcessCdsProjectReferencesOutputs" / BeforeTargets="PowerAppsPackage" |
Ensures required nodes exist in Customizations.xml. |
_ValidateDuplicateGuidsBeforePackage |
AfterTargets="ProcessCdsProjectReferencesOutputs" / BeforeTargets="PowerAppsPackage" |
Runs duplicate-GUID validation unless opted out. |
_ValidateQuickFindViewsBeforePackage |
AfterTargets="ProcessCdsProjectReferencesOutputs" / BeforeTargets="PowerAppsPackage" |
Runs Quick Find validation unless opted out. |
ProcessCdsProjectReferencesOutputs |
BeforeTargets="PowerAppsPackage" |
Overrides the PAC-stage handling of project-reference outputs. |
PowerAppsPackage |
AfterTargets="AfterBuild" |
Packs the prepared metadata directory into the final solution zip with InvokeSolutionPackager. |
Specialized integration targets imported by the Solution package:
-
ScriptLibrary
ProbeScriptLibrariesBuildScriptLibrariesCopyScriptLibrariesToWebResourcesCopyScriptLibrariesToMetadata- detects referenced
ScriptLibraryprojects, removes them from normal reference handling, builds them, resolves Dataverse web-resource names, optionally generates missing.data.xml, adds root components toSolution.xml, and copies the JS outputs into solution metadata
-
CodeApp
ProbeCodeAppsBuildCodeAppsPrepareCodeAppsSourcesCopyCodeAppsToMetadata- detects referenced
CodeAppprojects, builds them withRunNodeBuild=true, adds a CanvasApp root component toSolution.xml, generates.meta.xml, ensures theCanvasAppsnode exists, and copiesdistoutput intoCanvasApps/<publisher>_<AppName>_CodeAppPackages/
-
GenPage
ProbeGenPagesBuildGenPagesCopyGenPagesToMetadata- detects referenced
GenPageprojects, builds them, ensures theuxagentprojectsnode exists, and generates/copiesuxagentproject.xml,page.tsx,page.compiled, andconfig.jsonmetadata
-
Plugin
ProbePluginLibrariesBuildPluginLibrariesAlignPluginAssemblyDataVersions- builds referenced plugin projects before solution packaging, gathers assembly metadata through
GetPluginAssemblyInfo, creates/updates plugin assembly.data.xml, and aligns plugin assembly versions in the staged solution metadata
-
WorkflowActivity
ProbeWorkflowActivityLibrariesBuildWorkflowActivityLibrariesCopyWorkflowActivityDllToMetadataAlignWorkflowActivityAssemblyDataVersions- builds referenced workflow activity projects, creates/updates workflow assembly
.data.xml, copies the workflow DLL intoPluginAssemblies, and aligns versions in staged metadata
Two details are easy to miss:
ValidateSolutionComponentSchemaexists in the shared Tasks package but is not auto-wired by the Solution package.- the Solution package overrides PAC-oriented handling by redefining
ProcessCdsProjectReferencesOutputsand filtering outScriptLibrary,WorkflowActivity, andCodeAppreferences for manual staging.
TALXIS.DevKit.Build.Dataverse.PdPackage depends on Microsoft.PowerApps.MSBuild.PDPackage and the shared Tasks package. Its build files import Microsoft's PDPackage props/targets first, then TALXIS targets on top.
Main hooks:
| Target | Hook | Purpose |
|---|---|---|
_OverridePdPackageZipName |
BeforeTargets="ComputePdPackageOutput" |
Sets the output file name to $(PackageId)$(PdPackageTargetExt) when not provided. |
_ApplyPdPackageVersionNumber |
BeforeTargets="BeforeBuild;GenerateNuspec" |
Runs GenerateVersionNumber for PDPackage builds/packs. |
_DetectPdProjectReferenceTypes |
BeforeTargets="ResolveProjectReferences" |
Calls GetProjectType on referenced projects and marks solution references as non-assembly references. |
_GetPdPackageItemsFromPpProjectReferences |
depends on _DetectPdProjectReferenceTypes |
Calls GetOutputsForPdPackage on references and collects PdSolution inputs. |
_GeneratePdPackageAfterPublish |
AfterTargets="Publish" |
Generates the .pdpkg.zip after publish. |
_ValidatePcfDependenciesAfterPackage |
AfterTargets="_GeneratePdPackageAfterPublish" |
Validates PCF dependencies across solution zips in PkgAssets. |
Additional PDPackage-specific processing:
_EnsureImportConfigBeforeGeneratecreates a skeletonImportConfig.xmlwhen one is missing_DetectCustomImportConfigdisables auto-generation when a user-suppliedPkgAssets/ImportConfig.xmlalready definesconfigsolutionfile_PostProcessImportConfigpost-processes generated import config with annotated solutions and CMT dataDiscoverCmtPackagesfinds CMT packages by locating directories with[Content_Types].xml,data.xml, anddata_schema.xml_ZipCmtPackagesAfterBuildzips discovered CMT packages afterBuild_PrepareCmtMetadataBeforePublishmerges CMTdata.xml/data_schema.xmlinto one metadata package and zips it_PublishCmtMetadataAfterComputePublishListadds that merged CMT zip to publish output
TALXIS.DevKit.Build.Dataverse.Plugin depends on Microsoft.PowerApps.MSBuild.Plugin, Microsoft.CrmSdk.CoreAssemblies, Microsoft.NETFramework.ReferenceAssemblies, ILRepack.Lib.MSBuild.Task, and the shared Tasks package.
Main hooks:
- imports
Microsoft.PowerApps.VisualStudio.Plugin.props/.targets _PublishPluginAfterBuildrunsPublishwithNoBuild=trueafterBuildwhenPublishOnBuild=true_ApplyPluginVersionBeforeBuildruns beforeBeforeBuild_AssemblyMergePluginDependenciesAfterBuildruns afterBuildand depends onAssemblyMergeDependenciesGetPluginAssemblyInfoexposes plugin metadata to other packages- sets
IsPackable=falseand hooks$(BeforePack)with_ErrorOnPluginPack, which raises a hard error before any nuspec/nupkg work starts
It also redirects ILRepackTargetsFile to a no-op file so TALXIS controls the merge step instead of ILRepack's default auto-hook.
TALXIS.DevKit.Build.Dataverse.WorkflowActivity is parallel to Plugin, but for workflow assemblies.
Main hooks:
- imports
Microsoft.PowerApps.VisualStudio.WorkflowActivity.props/.targets _ApplyWorkflowActivityVersionBeforeBuildruns beforeBeforeBuild_AssemblyMergeWorkflowActivityDependenciesAfterBuildruns afterBuildand depends onAssemblyMergeDependenciesGetWorkflowActivityAssemblyInfoexposes workflow metadata to other packages- sets
IsPackable=falseand hooks$(BeforePack)with_ErrorOnWorkflowActivityPack, which raises a hard error before any nuspec/nupkg work starts
Like the Plugin package, it replaces ILRepack's default auto-hook with a no-op target file and lets TALXIS drive the assembly merge.
TALXIS.DevKit.Build.Dataverse.Pcf depends on Microsoft.PowerApps.MSBuild.Pcf, Microsoft.NETFramework.ReferenceAssemblies, and the shared Tasks package.
Main hooks:
- imports
Microsoft.PowerApps.VisualStudio.Pcf.props/.targets NpmInstallrunsBeforeTargets="BeforeBuild"_ApplyPcfVersionBeforeBuildrunsBeforeTargets="BeforeBuild"and depends onNpmInstall_EnsurePcfStubAssemblyruns beforePublish/GetCopyToPublishDirectoryItemsand creates a stub DLL if neededPcfCopyToPublishrunsAfterTargets="Publish"and copies PCF output intoout\controls\publish- sets
IsPackable=falseand hooks$(BeforePack)with_ErrorOnPcfPack, which raises a hard error before any nuspec/nupkg work starts
This package also fixes the output layout by setting AppendTargetFrameworkToOutputPath=false and OutputPath=$(ProjectDirectory)\out\controls.
Because ProjectType=Pcf is built on Microsoft.NET.Sdk, it also sets EnableDefaultItems=false and no-ops the CreateManifestResourceNames/CoreCompile targets. PCF output is produced by webpack/pcf-scripts, not MSBuild's C#/resx compile pipeline, and the project's own <None Include="$(MSBuildProjectDirectory)\**"/> glob already covers every file (including .resx localization files). Without this, the SDK's default item globbing would independently pick up the same .resx files as EmbeddedResource, colliding with the manual glob and failing with MSB3577. Consumers can still set EnableDefaultItems=true in their own project to opt back in.
TALXIS.DevKit.Build.Dataverse.ScriptLibrary depends only on the shared Tasks package.
Main hooks:
CheckScriptLibraryPrereqsBuildTypeScript(BeforeTargets="Build")CopyScriptLibraryMainToOutput(AfterTargets="Build")GetScriptLibraryOutputsGetSuppressedScriptLibraryReferences
The package expects TypeScript sources under $(TypeScriptDir) (default TS), runs npm install and npm run build, copies the selected main JS file to $(TargetDir), and lets Solution builds query which referenced script libraries are CompileOnly and therefore should not be deployed as separate web resources. Standalone npm packaging of a ScriptLibrary is planned but not yet implemented, so it does not currently set IsPackable=false.
TALXIS.DevKit.Build.Dataverse.CodeApp depends only on the shared Tasks package.
Main hooks:
CheckCodeAppPrereqsBuildCodeApp(BeforeTargets="Build")CopyCodeAppDist(AfterTargets="Build")GetCodeAppOutputsCopyCodeAppDistPublish(AfterTargets="Publish")
The package runs npm install and npm run build, expects output under dist/, copies it into $(OutputPath)$(AppName)/ and $(PublishDir)$(AppName)/, and exposes the dist folder plus power.config.json to Solution packaging. CodeApp projects are not standalone components, so the package sets IsPackable=false and hooks $(BeforePack) with _ErrorOnCodeAppPack, which raises a hard error before any nuspec/nupkg work starts.
TALXIS.DevKit.Build.Dataverse.GenPage depends only on the shared Tasks package.
Main hooks:
CheckGenPagePrereqsTranspileGenPage(BeforeTargets="Build")CopyGenPageOutputs(AfterTargets="Build")GetGenPageOutputs
The package validates GenPageId, transpiles page.tsx with npx ... typescript@5.3.2 tsc, patches the compiled output into page.compiled, then stages page.tsx, page.compiled, and optional genpage.config.json into $(OutputPath)$(GenPageName)/ for later Solution integration. GenPage projects are not standalone components, so the package sets IsPackable=false and hooks $(BeforePack) with _ErrorOnGenPagePack, which raises a hard error before any nuspec/nupkg work starts.
Only Solution and PdPackage are packable and add custom pack file-inclusion targets. Solution uses _IncludeSolutionZipInPack with BeforeTargets="_GetPackageFiles" and DependsOnTargets="Build", so packing a solution package explicitly reuses the build pipeline that produces the solution zip; it also adds build/<PackageId>.props, which declares a PdSolution item over the packaged content/solution/*.zip for downstream PDPackage consumption. PdPackage uses _IncludePdPackageZipInPack with DependsOnTargets="_GeneratePdPackageAfterPublish", so its pack path is tied to publish/package generation rather than directly to Build. Both also hook GenerateNuspec for their version-apply targets, so the generated package version stays correct even under dotnet pack --no-build.
Plugin, WorkflowActivity, Pcf, CodeApp, and GenPage are consumed exclusively via <ProjectReference> from a Solution project - they are never published as standalone NuGet packages. Each of these sets IsPackable=false and appends its own target to the $(BeforePack) property, which MSBuild inserts as the very first step in the Pack target's dependency chain. That target raises a hard <Error> explaining that the project cannot be packed and should instead be referenced from a Solution project - the error fires before GenerateNuspec, _IntermediatePack, or any other pack-related work runs, so no .nuspec/.nupkg is ever produced.
ScriptLibrary does not yet set IsPackable=false - standalone npm packaging is planned for it, so it remains packable (with the default SDK behavior) until that support lands.