Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Dataverse/PDPackage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ All `ProjectReference` items default to `ReferenceOutputAssembly=false` via `Ite

`_DetectPdProjectReferenceTypes` probes all `ProjectReference` items for `GetProjectType`. Solution-type references have `ReferenceOutputAssembly` set to `false` so their DLLs are not included in the package output.

### ImportConfig auto-generation

`ImportConfig.xml` is not required to exist in the project. If `<AssetsSrcDirectory>ImportConfig.xml` is missing, a skeleton (`<configdatastorage>` with empty `<solutions>`, `<filestoimport>`, `<filesmapstoimport>`) is generated in `$(IntermediateOutputPath)<FolderName>/ImportConfig.xml` before MS validation runs, and `@(PdImportConfig)` is rewired to point at it. `<FolderName>` is read from the `GetImportPackageDataFolderName` property of any `ImportExtension`-derived class found in the project's top-level `.cs` files (default `PkgAssets`).

When `AutoGeneratePdImportConfig` is `true` (default) and the source `ImportConfig.xml` contains no `<configsolutionfile>` entries, the `<solutions>` section is generated automatically from the project's solution references. Both `<ProjectReference>` items pointing at Solution projects and `<PackageReference>` items pointing at `pp-solution` NuGet packages are picked up, and each one produces a `<configsolutionfile>` entry with `requiredimportmode="async"`.

The generated entries appear in the **same order as the references are declared in the `.csproj` file**, walking `<PackageReference>` and `<ProjectReference>` items in document order (interleaved). A `PackageReference` is matched by its package id; a `ProjectReference` is matched by the csproj file name (without extension), which by convention equals the solution unique name. Any `<configsolutionfile>` element that can't be matched against a csproj reference is moved to the end while keeping its relative position.

If you ship a hand-written `PkgAssets/ImportConfig.xml` with explicit `<configsolutionfile>` entries, auto-generation is skipped entirely and your file is used as-is.

### ILRepack

`DataverseILRepack` (runs after `Build`) merges all non-Microsoft DLLs (excluding reference assemblies and `Newtonsoft.Json`) into the main output assembly using ILRepack.exe. Can be disabled with `DataversePackageRunILRepack=false` or `SkipPackageILRepack=true`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,37 @@
<_TalxisSourceImportConfig>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','PkgAssets','ImportConfig.xml'))</_TalxisSourceImportConfig>
</PropertyGroup>

<Target Name="TalxisEnsureImportConfigExists"
BeforeTargets="_CheckForInvalidPdProjectCore;PrepareForPdBuild;GeneratePdImportConfig">

<PropertyGroup>
<_TalxisCurrentPdImportConfig>@(PdImportConfig->'%(FullPath)')</_TalxisCurrentPdImportConfig>
<_TalxisSkipImportConfigSkeleton Condition="'$(_TalxisCurrentPdImportConfig)' != '' and Exists('$(_TalxisCurrentPdImportConfig)')">true</_TalxisSkipImportConfigSkeleton>
</PropertyGroup>

<ExtractPdPackageDataFolderName
ProjectDirectory="$(MSBuildProjectDirectory)"
DefaultFolderName="PkgAssets"
Condition="'$(_TalxisSkipImportConfigSkeleton)' != 'true'">
<Output TaskParameter="FolderName" PropertyName="_TalxisPdDataFolderName" />
</ExtractPdPackageDataFolderName>

<PropertyGroup Condition="'$(_TalxisSkipImportConfigSkeleton)' != 'true'">
<_TalxisGeneratedImportConfigDir>$([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','$(IntermediateOutputPath)','$(_TalxisPdDataFolderName)'))))</_TalxisGeneratedImportConfigDir>
<_TalxisGeneratedImportConfigPath>$([System.IO.Path]::Combine('$(_TalxisGeneratedImportConfigDir)','ImportConfig.xml'))</_TalxisGeneratedImportConfigPath>
</PropertyGroup>

<PostProcessImportConfig
ImportConfigPath="$(_TalxisGeneratedImportConfigPath)"
CreateSkeletonIfMissing="true"
Condition="'$(_TalxisSkipImportConfigSkeleton)' != 'true'" />

<ItemGroup Condition="'$(_TalxisSkipImportConfigSkeleton)' != 'true'">
<PdImportConfig Remove="@(PdImportConfig)" />
<PdImportConfig Include="$(_TalxisGeneratedImportConfigPath)" />
</ItemGroup>
</Target>

<Target Name="TalxisDetectCustomImportConfig"
BeforeTargets="GeneratePdImportConfig"
Condition="'$(AutoGeneratePdImportConfig)'=='true' and Exists('$(_TalxisSourceImportConfig)')">
Expand Down Expand Up @@ -34,6 +65,7 @@
<PostProcessImportConfig
ImportConfigPath="$(_GeneratedPdImportConfig)"
Solutions="@(_AnnotatedSolution)"
CmtDataFileName="$(_TalxisCmtDataFileForImportConfig)" />
CmtDataFileName="$(_TalxisCmtDataFileForImportConfig)"
CsprojPath="$(MSBuildProjectFullPath)" />
</Target>
</Project>
63 changes: 63 additions & 0 deletions src/Dataverse/Tasks/Tasks/ExtractPdPackageDataFolderName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class ExtractPdPackageDataFolderName : Task
{
[Required]
public string ProjectDirectory { get; set; } = "";

public string DefaultFolderName { get; set; } = "PkgAssets";

[Output]
public string FolderName { get; private set; } = "";

private static readonly Regex PropertyRegex = new Regex(
@"GetImportPackageDataFolderName\s*(?:=>\s*""([^""]+)""|\{[^}]*?return\s*""([^""]+)"")",
RegexOptions.Singleline | RegexOptions.Compiled);

public override bool Execute()
{
FolderName = string.IsNullOrWhiteSpace(DefaultFolderName) ? "PkgAssets" : DefaultFolderName;

try
{
var dir = (ProjectDirectory ?? "").Trim();
if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir))
{
Log.LogMessage(MessageImportance.Low,
$"ProjectDirectory is empty or missing — using default folder name '{FolderName}'.");
return true;
}

foreach (var file in Directory.EnumerateFiles(dir, "*.cs", SearchOption.TopDirectoryOnly))
{
string content;
try { content = File.ReadAllText(file); }
catch { continue; }

var match = PropertyRegex.Match(content);
if (!match.Success) continue;

var value = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value;
if (string.IsNullOrWhiteSpace(value)) continue;

FolderName = value;
Log.LogMessage(MessageImportance.Normal,
$"Detected GetImportPackageDataFolderName='{FolderName}' from '{file}'.");
return true;
}

Log.LogMessage(MessageImportance.Low,
$"GetImportPackageDataFolderName not found in any .cs file under '{dir}'. Using default '{FolderName}'.");
return true;
}
catch (Exception ex)
{
Log.LogWarningFromException(ex);
return true;
}
}
}
217 changes: 217 additions & 0 deletions src/Dataverse/Tasks/Tasks/PostProcessImportConfig.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
Expand All @@ -13,6 +15,10 @@ public class PostProcessImportConfig : Task

public string CmtDataFileName { get; set; } = "";

public string CsprojPath { get; set; } = "";

public bool CreateSkeletonIfMissing { get; set; }

[Output]
public string UpdatedImportConfig { get; private set; } = "";

Expand All @@ -28,12 +34,26 @@ public override bool Execute()
}

configPath = Path.GetFullPath(configPath);

if (!File.Exists(configPath))
{
if (CreateSkeletonIfMissing)
{
WriteSkeleton(configPath);
UpdatedImportConfig = configPath;
return !Log.HasLoggedErrors;
}

Log.LogError($"ImportConfig file not found: {configPath}");
return false;
}

if (CreateSkeletonIfMissing)
{
UpdatedImportConfig = configPath;
return true;
}

var doc = new XmlDocument { PreserveWhitespace = true };
doc.Load(configPath);

Expand All @@ -45,6 +65,7 @@ public override bool Execute()
}

AnnotateSolutionFiles(doc, root);
ReorderSolutionFiles(root);
SetCmtDataImportFile(root);

doc.Save(configPath);
Expand All @@ -58,6 +79,40 @@ public override bool Execute()
}
}

private void WriteSkeleton(string configPath)
{
var dir = Path.GetDirectoryName(configPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);

var doc = new XmlDocument();
var decl = doc.CreateXmlDeclaration("1.0", "utf-8", null);
doc.AppendChild(decl);

var root = doc.CreateElement("configdatastorage");
root.SetAttribute("installsampledata", "false");
root.SetAttribute("waitforsampledatatoinstall", "true");
doc.AppendChild(root);

root.AppendChild(doc.CreateElement("solutions"));
root.AppendChild(doc.CreateElement("filestoimport"));
root.AppendChild(doc.CreateElement("filesmapstoimport"));

var settings = new System.Xml.XmlWriterSettings
{
Indent = true,
IndentChars = " ",
Encoding = new System.Text.UTF8Encoding(false),
};
using (var writer = System.Xml.XmlWriter.Create(configPath, settings))
{
doc.Save(writer);
}

Log.LogMessage(MessageImportance.High,
$"Generated skeleton ImportConfig.xml at: {configPath}");
}

private void AnnotateSolutionFiles(XmlDocument doc, XmlElement root)
{
var solutionNodes = root.SelectNodes("//configsolutionfile");
Expand Down Expand Up @@ -114,6 +169,168 @@ private string LookupSolutionZipFilename(string uniqueName)
return uniqueName + ".zip";
}

private void ReorderSolutionFiles(XmlElement root)
{
var csproj = (CsprojPath ?? "").Trim();
if (string.IsNullOrWhiteSpace(csproj) || !File.Exists(csproj))
{
Log.LogMessage(MessageImportance.Low,
"CsprojPath not provided or missing — skipping configsolutionfile reorder.");
return;
}

var orderedNames = ReadReferenceOrderFromCsproj(csproj);
if (orderedNames.Count == 0)
return;

var container = root.SelectSingleNode("solutions") as XmlElement;
if (container == null)
return;

var bundles = new List<Bundle>();
XmlNode pendingWhitespace = null;
XmlNode trailingWhitespace = null;

foreach (XmlNode child in container.ChildNodes)
{
if (child is XmlElement el &&
string.Equals(el.LocalName, "configsolutionfile", StringComparison.OrdinalIgnoreCase))
{
bundles.Add(new Bundle(pendingWhitespace, el));
pendingWhitespace = null;
}
else if (IsWhitespaceNode(child))
{
pendingWhitespace = child;
}
}
trailingWhitespace = pendingWhitespace;

if (bundles.Count == 0)
return;

var ordered = bundles
.Select((b, idx) => new
{
Bundle = b,
Rank = RankConfigSolutionFile(b.Element, orderedNames),
OriginalIndex = idx
})
.OrderBy(x => x.Rank)
.ThenBy(x => x.OriginalIndex)
.Select(x => x.Bundle)
.ToList();

bool alreadyOrdered = true;
for (int i = 0; i < bundles.Count; i++)
{
if (!ReferenceEquals(bundles[i].Element, ordered[i].Element))
{
alreadyOrdered = false;
break;
}
}
if (alreadyOrdered)
return;

foreach (var bundle in bundles)
{
if (bundle.Whitespace != null && bundle.Whitespace.ParentNode == container)
container.RemoveChild(bundle.Whitespace);
if (bundle.Element.ParentNode == container)
container.RemoveChild(bundle.Element);
}

foreach (var bundle in ordered)
{
if (bundle.Whitespace != null)
container.InsertBefore(bundle.Whitespace, trailingWhitespace);
container.InsertBefore(bundle.Element, trailingWhitespace);
}

Log.LogMessage(MessageImportance.High,
"Reordered configsolutionfile elements to match the order of " +
"PackageReference / ProjectReference items in the csproj.");
}

private static bool IsWhitespaceNode(XmlNode node)
{
if (node == null) return false;
return node.NodeType == XmlNodeType.Whitespace
|| node.NodeType == XmlNodeType.SignificantWhitespace
|| (node.NodeType == XmlNodeType.Text && string.IsNullOrWhiteSpace(node.Value));
}

private sealed class Bundle
{
public XmlNode Whitespace { get; }
public XmlElement Element { get; }
public Bundle(XmlNode whitespace, XmlElement element)
{
Whitespace = whitespace;
Element = element;
}
}

private static int RankConfigSolutionFile(XmlElement node, List<string> orderedNames)
{
var unique = ExtractUniqueName(node);
if (string.IsNullOrWhiteSpace(unique))
return int.MaxValue;

for (int i = 0; i < orderedNames.Count; i++)
{
if (string.Equals(orderedNames[i], unique, StringComparison.OrdinalIgnoreCase))
return i;
}
return int.MaxValue;
}

private static string ExtractUniqueName(XmlElement configSolutionFile)
{
var unique = configSolutionFile.GetAttribute("solutionpackageuniquename");
if (!string.IsNullOrWhiteSpace(unique))
return unique;

var filename = configSolutionFile.GetAttribute("solutionpackagefilename");
if (!string.IsNullOrWhiteSpace(filename))
return Path.GetFileNameWithoutExtension(filename);

return "";
}

private static List<string> ReadReferenceOrderFromCsproj(string csprojPath)
{
var result = new List<string>();
var doc = new XmlDocument();
doc.Load(csprojPath);

var itemGroups = doc.GetElementsByTagName("ItemGroup");
foreach (XmlNode ig in itemGroups)
{
foreach (XmlNode child in ig.ChildNodes)
{
if (!(child is XmlElement el))
continue;

var include = el.GetAttribute("Include");
if (string.IsNullOrWhiteSpace(include))
continue;

if (string.Equals(el.LocalName, "PackageReference", StringComparison.OrdinalIgnoreCase))
{
result.Add(include);
}
else if (string.Equals(el.LocalName, "ProjectReference", StringComparison.OrdinalIgnoreCase))
{
result.Add(Path.GetFileNameWithoutExtension(include));
}
}
}

return result;
}

private void SetCmtDataImportFile(XmlElement root)
{
var cmtFileName = (CmtDataFileName ?? "").Trim();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@
<UsingTask TaskName="GenerateGenPageConfigJson" AssemblyFile="$(TasksAssembly)" Runtime="NET" />
<UsingTask TaskName="GenerateGenPageProjectXml" AssemblyFile="$(TasksAssembly)" Runtime="NET" />
<UsingTask TaskName="GenerateGenPageFileXml" AssemblyFile="$(TasksAssembly)" Runtime="NET" />
<UsingTask TaskName="ExtractPdPackageDataFolderName" AssemblyFile="$(TasksAssembly)" Runtime="NET" />
</Project>