Skip to content

Commit 460181f

Browse files
committed
Significant overhaul of RegFree and manifests
Will open up, but many UI controls broken.
1 parent 34c2cbb commit 460181f

File tree

21 files changed

+1003
-298
lines changed

21 files changed

+1003
-298
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,4 @@ __pycache__/
165165
.venv/
166166
venv/
167167
*.egg-info/
168+
CrashLog.txt

Build/RegFree.targets

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@
4646
<DependentAssemblies Include="$(OutDir)Views.X.manifest" />
4747
</ItemGroup>
4848
<ItemGroup Condition="'@(NativeComDlls)' == ''">
49-
<NativeComDlls Include="$(OutDir)*.dll" />
49+
<!-- Explicitly list known native COM servers instead of wildcard inclusion -->
50+
<NativeComDlls Include="$(OutDir)GraphiteEngine.dll" />
51+
<NativeComDlls Include="$(OutDir)UniscribeEngine.dll" />
5052
</ItemGroup>
5153
<ItemGroup>
5254
<NativeComDlls Remove="$(OutDir)*.resources.dll" />
5355
<NativeComDlls Remove="$(OutDir)*.ni.dll" />
54-
<!-- Exclude DLLs that have their own manifests -->
56+
<!-- Exclude DLLs that have their own manifests (redundant if not in include, but safe) -->
5557
<NativeComDlls Remove="$(OutDir)Views.dll" />
5658
<!-- FwKernel.dll is covered by FwKernel.X.manifest, but we need to ensure that manifest is populated.
5759
For now, we assume it is or will be fixed. -->
@@ -61,18 +63,54 @@
6163
<Fragments Include="$(DistFilesDir)/*.fragment.manifest" />
6264
</ItemGroup>
6365
<ItemGroup Condition="'@(ManagedComAssemblies)' == ''">
64-
<ManagedComAssemblies Include="$(OutDir)*.dll" />
66+
<!-- Explicitly list managed assemblies that expose COM types -->
67+
<ManagedComAssemblies Include="$(OutDir)FwUtils.dll" />
68+
<ManagedComAssemblies Include="$(OutDir)SimpleRootSite.dll" />
69+
<ManagedComAssemblies Include="$(OutDir)ManagedVwDrawRootBuffered.dll" />
70+
<ManagedComAssemblies Include="$(OutDir)ManagedLgIcuCollator.dll" />
71+
<ManagedComAssemblies Include="$(OutDir)ManagedVwWindow.dll" />
6572
</ItemGroup>
6673
<ItemGroup>
6774
<ManagedComAssemblies Remove="$(OutDir)*.resources.dll" />
6875
<ManagedComAssemblies Remove="$(OutDir)*.ni.dll" />
76+
<!-- Exclude native DLLs that are handled separately -->
77+
<ManagedComAssemblies Remove="$(OutDir)FwKernel.dll" />
78+
<ManagedComAssemblies Remove="$(OutDir)Views.dll" />
79+
<!-- Exclude 3rd party DLLs that cause manifest issues and don't need RegFree COM -->
80+
<ManagedComAssemblies Remove="$(OutDir)DotNetZip.dll" />
6981
</ItemGroup>
70-
<Target Name="CreateManifest" Condition="'$(OS)'=='Windows_NT'">
82+
83+
<!-- Ensure we don't process managed assemblies as native type libraries in the main manifest -->
84+
<ItemGroup>
85+
<NativeComDlls Remove="@(ManagedComAssemblies)" />
86+
</ItemGroup>
87+
88+
<!-- Phase 1: Generate individual manifests for managed assemblies -->
89+
<Target Name="CreateComponentManifests"
90+
Inputs="%(ManagedComAssemblies.Identity)"
91+
Outputs="$(OutDir)%(ManagedComAssemblies.Filename).manifest">
92+
<Message Text="Generating component manifest for %(ManagedComAssemblies.Filename) with Platform=$(Platform)" Importance="high" />
93+
<RegFree
94+
Executable="%(ManagedComAssemblies.Identity)"
95+
Output="$(OutDir)%(ManagedComAssemblies.Filename).manifest"
96+
ManagedAssemblies="%(ManagedComAssemblies.Identity)"
97+
Platform="$(Platform)"
98+
Condition="'$(OS)'=='Windows_NT'"
99+
/>
100+
</Target>
101+
102+
<Target Name="CreateManifest" Condition="'$(OS)'=='Windows_NT'" DependsOnTargets="CreateComponentManifests">
103+
<ItemGroup>
104+
<ManagedComponentManifests Include="@(ManagedComAssemblies -> '$(OutDir)%(Filename).manifest')" />
105+
<ManagedComponentManifests Remove="@(ManagedComponentManifests)" Condition="!Exists('%(ManagedComponentManifests.Identity)')" />
106+
<!-- Add the generated component manifests as dependencies -->
107+
<DependentAssemblies Include="@(ManagedComponentManifests)" />
108+
</ItemGroup>
71109
<RegFree
72110
Executable="$(Executable)"
73111
DependentAssemblies="@(DependentAssemblies)"
74112
Dlls="@(NativeComDlls)"
75-
ManagedAssemblies="@(ManagedComAssemblies)"
113+
ManagedAssemblies=""
76114
AsIs="@(Fragments)"
77115
Platform="$(Platform)"
78116
/>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) 2025 SIL International
2+
// This software is licensed under the LGPL, version 2.1 or later
3+
// (http://www.gnu.org/licenses/lgpl-2.1.html)
4+
5+
using System;
6+
using System.CodeDom.Compiler;
7+
using System.IO;
8+
using System.Linq;
9+
using System.Runtime.InteropServices;
10+
using System.Xml;
11+
using FwBuildTasks;
12+
using Microsoft.Build.Utilities;
13+
using Microsoft.CSharp;
14+
using NUnit.Framework;
15+
using SIL.FieldWorks.Build.Tasks;
16+
using SIL.TestUtilities;
17+
18+
namespace SIL.FieldWorks.Build.Tasks.FwBuildTasksTests
19+
{
20+
[TestFixture]
21+
public sealed class RegFreeCreatorTests
22+
{
23+
private const string AsmNamespace = "urn:schemas-microsoft-com:asm.v1";
24+
25+
[Test]
26+
public void ProcessManagedAssembly_NestsClrClassUnderFile()
27+
{
28+
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
29+
Directory.CreateDirectory(tempDir);
30+
var assemblyPath = Path.Combine(tempDir, "SampleComClass.dll");
31+
32+
try
33+
{
34+
CompileComVisibleAssembly(assemblyPath);
35+
36+
var doc = new XmlDocument();
37+
var root = doc.CreateElement("assembly", AsmNamespace);
38+
doc.AppendChild(root);
39+
var logger = new TaskLoggingHelper(new TestBuildEngine(), nameof(RegFreeCreatorTests));
40+
var creator = new RegFreeCreator(doc, logger);
41+
42+
var foundClrClass = creator.ProcessManagedAssembly(root, assemblyPath);
43+
Assert.That(foundClrClass, Is.True, "Test assembly should produce clrClass entries.");
44+
45+
var ns = new XmlNamespaceManager(doc.NameTable);
46+
ns.AddNamespace("asmv1", AsmNamespace);
47+
var fileNode = root.SelectSingleNode("asmv1:file", ns);
48+
Assert.That(fileNode, Is.Not.Null, "Managed manifest entries must create a file node.");
49+
50+
var nestedClrClass = fileNode.SelectSingleNode("asmv1:clrClass", ns);
51+
Assert.That(nestedClrClass, Is.Not.Null, "clrClass should live under its file element.");
52+
53+
var orphanClrClass = root.SelectSingleNode("asmv1:clrClass", ns);
54+
Assert.That(orphanClrClass, Is.Null, "clrClass elements must not be direct children of the assembly root.");
55+
}
56+
finally
57+
{
58+
if (Directory.Exists(tempDir))
59+
{
60+
Directory.Delete(tempDir, true);
61+
}
62+
}
63+
}
64+
65+
private static void CompileComVisibleAssembly(string outputPath)
66+
{
67+
const string source = @"using System.Runtime.InteropServices;
68+
[assembly: ComVisible(true)]
69+
[assembly: Guid(""3D757DD4-8985-4CA6-B2C4-FA2B950C9F6D"")]
70+
namespace RegFreeCreatorTestAssembly
71+
{
72+
[ComVisible(true)]
73+
[Guid(""3EF2F542-4954-4B13-8B8D-A68E4D50D7A3"")]
74+
[ProgId(""RegFreeCreator.SampleClass"")]
75+
public class SampleComClass
76+
{
77+
}
78+
}";
79+
80+
var provider = new CSharpCodeProvider();
81+
var parameters = new CompilerParameters
82+
{
83+
GenerateExecutable = false,
84+
OutputAssembly = outputPath,
85+
CompilerOptions = "/target:library"
86+
};
87+
parameters.ReferencedAssemblies.Add(typeof(object).Assembly.Location);
88+
parameters.ReferencedAssemblies.Add(typeof(GuidAttribute).Assembly.Location);
89+
90+
var results = provider.CompileAssemblyFromSource(parameters, source);
91+
if (results.Errors.HasErrors)
92+
{
93+
var message = string.Join(Environment.NewLine, results.Errors.Cast<CompilerError>().Select(e => e.ToString()));
94+
throw new InvalidOperationException($"Failed to compile COM-visible test assembly:{Environment.NewLine}{message}");
95+
}
96+
}
97+
}
98+
}

Build/Src/FwBuildTasks/RegFree.cs

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// </remarks>
1616
// ---------------------------------------------------------------------------------------------
1717
using System;
18+
using System.Collections.Generic;
1819
using System.Collections.Specialized;
1920
using System.Diagnostics;
2021
using System.IO;
@@ -103,6 +104,15 @@ public RegFree()
103104
/// ------------------------------------------------------------------------------------
104105
public ITaskItem[] NoTypeLib { get; set; }
105106

107+
/// ------------------------------------------------------------------------------------
108+
/// <summary>
109+
/// Gets or sets the CLSIDs to exclude from the manifest.
110+
/// This is useful when a CLSID is defined in a TypeLib but implemented in a managed assembly
111+
/// that provides its own manifest entry.
112+
/// </summary>
113+
/// ------------------------------------------------------------------------------------
114+
public ITaskItem[] ExcludedClsids { get; set; }
115+
106116
/// ------------------------------------------------------------------------------------
107117
/// <summary>
108118
/// Gets or sets manifest fragment files that will be included in the resulting manifest
@@ -153,16 +163,20 @@ public override bool Execute()
153163
Path.GetFileName(Executable)
154164
);
155165

156-
StringCollection dllPaths = GetFilesFrom(Dlls);
157-
if (dllPaths.Count == 0)
166+
var itemsToProcess = new List<ITaskItem>(Dlls);
167+
if (itemsToProcess.Count == 0)
158168
{
159169
string ext = Path.GetExtension(Executable);
160170
if (
161171
ext != null
162172
&& ext.Equals(".dll", StringComparison.InvariantCultureIgnoreCase)
173+
&& (ManagedAssemblies == null || !ManagedAssemblies.Any(m => m.ItemSpec.Equals(Executable, StringComparison.OrdinalIgnoreCase)))
163174
)
164-
dllPaths.Add(Executable);
175+
{
176+
itemsToProcess.Add(new TaskItem(Executable));
177+
}
165178
}
179+
166180
string manifestFile = string.IsNullOrEmpty(Output)
167181
? Executable + ".manifest"
168182
: Output;
@@ -192,14 +206,19 @@ public override bool Execute()
192206

193207
// Process all DLLs using direct type library parsing (no registry redirection needed)
194208
var creator = new RegFreeCreator(doc, Log);
195-
var filesToRemove = dllPaths
196-
.Cast<string>()
197-
.Where(fileName => !File.Exists(fileName))
198-
.ToList();
199-
foreach (var file in filesToRemove)
200-
dllPaths.Remove(file);
209+
if (ExcludedClsids != null)
210+
{
211+
creator.AddExcludedClsids(GetFilesFrom(ExcludedClsids));
212+
}
213+
214+
// Remove non-existing files from the list
215+
itemsToProcess.RemoveAll(item => !File.Exists(item.ItemSpec));
201216

202217
string assemblyName = Path.GetFileNameWithoutExtension(manifestFile);
218+
if (assemblyName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
219+
{
220+
assemblyName = Path.GetFileNameWithoutExtension(assemblyName);
221+
}
203222
Debug.Assert(assemblyName != null);
204223
// The C++ test programs won't run if an assemblyIdentity element exists.
205224
//if (assemblyName.StartsWith("test"))
@@ -214,33 +233,65 @@ public override bool Execute()
214233
// just ignore
215234
}
216235
if (string.IsNullOrEmpty(assemblyVersion))
236+
{
217237
assemblyVersion = "1.0.0.0";
238+
}
239+
else
240+
{
241+
// Ensure version has 4 parts for manifest compliance (Major.Minor.Build.Revision)
242+
// Some assemblies might have 3-part versions (e.g. 1.1.0) which are invalid in manifests.
243+
// We also strip any non-numeric suffix if present, though FileVersion is usually clean.
244+
var parts = assemblyVersion.Split('.');
245+
if (parts.Length != 4)
246+
{
247+
var newParts = new string[4];
248+
for (int i = 0; i < 4; i++)
249+
{
250+
// Simple parsing to ensure we only get numbers
251+
string part = "0";
252+
if (i < parts.Length)
253+
{
254+
// Take only the leading digits
255+
var digits = new string(parts[i].TakeWhile(char.IsDigit).ToArray());
256+
if (!string.IsNullOrEmpty(digits))
257+
part = digits;
258+
}
259+
newParts[i] = part;
260+
}
261+
assemblyVersion = string.Join(".", newParts);
262+
}
263+
}
218264

219265
XmlElement root = creator.CreateExeInfo(assemblyName, assemblyVersion, Platform);
220266

221-
foreach (string fileName in dllPaths)
267+
foreach (string fileName in GetFilesFrom(ManagedAssemblies))
222268
{
223-
if (NoTypeLib.Count(f => f.ItemSpec == fileName) != 0)
224-
continue;
225-
226269
Log.LogMessage(
227270
MessageImportance.Low,
228-
"\tProcessing library {0}",
271+
"\tProcessing managed assembly {0}",
229272
Path.GetFileName(fileName)
230273
);
231-
232-
// Process type library directly (no registry redirection needed)
233-
creator.ProcessTypeLibrary(root, fileName);
274+
creator.ProcessManagedAssembly(root, fileName);
234275
}
235276

236-
foreach (string fileName in GetFilesFrom(ManagedAssemblies))
277+
foreach (var item in itemsToProcess)
237278
{
279+
string fileName = item.ItemSpec;
280+
if (NoTypeLib.Count(f => f.ItemSpec == fileName) != 0)
281+
continue;
282+
283+
string server = item.GetMetadata("Server");
284+
if (string.IsNullOrEmpty(server))
285+
server = null;
286+
238287
Log.LogMessage(
239288
MessageImportance.Low,
240-
"\tProcessing managed assembly {0}",
289+
"\tProcessing library {0}",
241290
Path.GetFileName(fileName)
242291
);
243-
creator.ProcessManagedAssembly(root, fileName);
292+
293+
// Process type library directly (no registry redirection needed)
294+
creator.ProcessTypeLibrary(root, fileName, server);
244295
}
245296

246297
// Process classes and interfaces from HKCR (where COM is already registered)
@@ -277,6 +328,18 @@ public override bool Execute()
277328
creator.AddDependentAssembly(root, assemblyFileName);
278329
}
279330

331+
if (!HasRegFreeContent(doc))
332+
{
333+
Log.LogMessage(
334+
MessageImportance.Low,
335+
"\tNo registration-free content found for {0}; manifest will not be emitted.",
336+
Path.GetFileName(manifestFile)
337+
);
338+
if (File.Exists(manifestFile))
339+
File.Delete(manifestFile);
340+
return true;
341+
}
342+
280343
var settings = new XmlWriterSettings
281344
{
282345
OmitXmlDeclaration = false,
@@ -301,9 +364,25 @@ public override bool Execute()
301364
return true;
302365
}
303366

304-
private static StringCollection GetFilesFrom(ITaskItem[] source)
367+
private static bool HasRegFreeContent(XmlDocument doc)
368+
{
369+
if (doc.DocumentElement == null)
370+
return false;
371+
372+
var namespaceManager = new XmlNamespaceManager(doc.NameTable);
373+
namespaceManager.AddNamespace("asmv1", "urn:schemas-microsoft-com:asm.v1");
374+
375+
bool HasNode(string xpath) => doc.SelectSingleNode(xpath, namespaceManager) != null;
376+
377+
return HasNode("//asmv1:clrClass")
378+
|| HasNode("//asmv1:comClass")
379+
|| HasNode("//asmv1:typelib")
380+
|| HasNode("//asmv1:dependentAssembly");
381+
}
382+
383+
private static List<string> GetFilesFrom(ITaskItem[] source)
305384
{
306-
var result = new StringCollection();
385+
var result = new List<string>();
307386
if (source == null)
308387
return result;
309388
foreach (var item in source)

0 commit comments

Comments
 (0)