From 08a8686481f8c1a3bd49018e2dcad1f8fd3a4f68 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 10 Sep 2026 16:57:45 +0700 Subject: [PATCH 1/5] Port LCM domain-model codegen to a Roslyn source generator Replace the LcmGenerate MSBuild task (which ran the NVelocity templates in a child MSBuild process to avoid VS DLL-locking, writing 9 Generated*.cs files into the source tree) with a Roslyn incremental source generator that produces the domain model at compile time. The new SIL.LCModel.SourceGenerators project reuses the existing NVelocity templates and model-wrapper classes unchanged; only the host is rewritten: - LcmModelGenerator (IIncrementalGenerator) reads MasterLCModel.xml, HandGenerated.xml and IntPropTypeOverrides.xml as AdditionalFiles and emits via AddSource. - LcmGenerateImpl accumulates outputs in memory instead of writing files. - EmbeddedTemplateLoader serves the 49 templates from embedded resources, replacing the filesystem #parse + Directory.SetCurrentDirectory mechanism. NVelocity is a single self-contained DLL that runs inside the Roslyn analyzer host; it is flowed alongside the generator via GetDependencyTargetPaths. SIL.LCModel.Build.Tasks keeps only the IdlImp task (kernel-interface codegen, left as-is). Generated output is byte-for-byte identical to the previous task; SIL.LCModel.Tests pass (1709 passed, 0 failed, 18 skipped). Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 9 +- CHANGELOG.md | 1 + LCM.sln | 7 + src/SIL.LCModel.Build.Tasks/LcmGenerate.cs | 224 ------------------ .../SIL.LCModel.Build.Tasks.csproj | 4 +- .../SIL.LCModel.Build.Tasks.props | 1 - .../EmbeddedTemplateLoader.cs | 58 +++++ .../LcmGenerateImpl.cs | 115 ++++----- .../LcmModelGenerator.cs | 139 +++++++++++ .../LcmModelRunner.cs | 116 +++++++++ .../Model}/Base.cs | 4 +- .../Model}/CellarModule.cs | 2 +- .../Model}/Class.cs | 2 +- .../Model}/DummyClass.cs | 2 +- .../Model}/IClass.cs | 2 +- .../Model}/Model.cs | 2 +- .../Model}/Property.cs | 2 +- .../Model}/RelationalProperty.cs | 2 +- .../Model}/StringKeyCollection.cs | 2 +- .../Model}/TypeInfo.cs | 2 +- .../SIL.LCModel.SourceGenerators.csproj | 42 ++++ .../Templates/vm}/AllReferencedObjects.vm.cs | 0 .../Templates/vm}/BackendProvider.vm.cs | 0 .../vm}/ClearIncomingRefsOnOutgoingRefs.vm.cs | 0 .../Templates/vm}/Constants.vm.cs | 0 .../Templates/vm}/Constructors.vm.cs | 0 .../Templates/vm}/DeleteMethod.vm.cs | 0 .../vm}/FactoryImplementations.vm.cs | 0 .../Templates/vm}/FactoryInterfaces.vm.cs | 0 .../IDomainDataByFlidAccessorMethods.vm.cs | 0 .../vm}/InitializeFromBEPMethods.vm.cs | 0 .../Templates/vm}/Interfaces.vm.cs | 0 .../vm}/LcmServiceLocatorBootstrapper.vm.cs | 0 .../Templates/vm}/OtherMethods.vm.cs | 0 .../Templates/vm}/RemoveAReferenceCore.vm.cs | 0 .../Templates/vm}/RemoveOwneeMethod.vm.cs | 0 .../Templates/vm}/Repository.vm.cs | 0 .../vm}/RepositoryImplementations.vm.cs | 0 .../Templates/vm}/RepositoryInterfaces.vm.cs | 0 .../RestoreIncomingRefsOnOutgoingRefs.vm.cs | 0 .../vm}/SDABinaryPropertyMethods.vm.cs | 0 .../vm}/SDABooleanPropertyMethods.vm.cs | 0 .../vm}/SDADateTimePropertyMethods.vm.cs | 0 .../vm}/SDAGenDatePropertyMethods.vm.cs | 0 .../vm}/SDAGuidPropertyMethods.vm.cs | 0 .../vm}/SDAIntegerPropertyMethods.vm.cs | 0 .../vm}/SDAObjectPropertyMethods.vm.cs | 0 .../vm}/SDAStringPropertyMethods.vm.cs | 0 .../vm}/SDAVectorPropertyMethods.vm.cs | 0 .../Templates/vm}/class.vm.cs | 0 .../Templates/vm}/classInterface.vm.cs | 0 .../Templates/vm}/datamembers.vm.cs | 0 .../Templates/vm}/datamembers_atomic.vm.cs | 0 .../Templates/vm}/datamembers_rel.vm.cs | 0 .../Templates/vm}/datamembers_simple.vm.cs | 0 .../Templates/vm}/factory.vm.cs | 0 .../Templates/vm}/factoryInterface.vm.cs | 0 .../Templates/vm}/main.vm.cs | 0 .../Templates/vm}/module.vm.cs | 0 .../Templates/vm}/propaccessors_atomic.vm.cs | 0 .../Templates/vm}/propaccessors_rel.vm.cs | 0 .../Templates/vm}/propaccessors_simple.vm.cs | 0 .../Templates/vm}/propertyAccessors.vm.cs | 0 .../Templates/vm}/propertyInterface.vm.cs | 0 .../Templates/vm}/repositoryInterface.vm.cs | 0 src/SIL.LCModel/GenerateModel.proj | 8 - src/SIL.LCModel/SIL.LCModel.csproj | 45 ++-- 67 files changed, 438 insertions(+), 353 deletions(-) delete mode 100644 src/SIL.LCModel.Build.Tasks/LcmGenerate.cs create mode 100644 src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators}/LcmGenerateImpl.cs (64%) create mode 100644 src/SIL.LCModel.SourceGenerators/LcmModelGenerator.cs create mode 100644 src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/Base.cs (96%) rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/CellarModule.cs (98%) rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/Class.cs (99%) rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/DummyClass.cs (99%) rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/IClass.cs (99%) rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/Model.cs (97%) rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/Property.cs (99%) rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/RelationalProperty.cs (99%) rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/StringKeyCollection.cs (95%) rename src/{SIL.LCModel.Build.Tasks => SIL.LCModel.SourceGenerators/Model}/TypeInfo.cs (98%) create mode 100644 src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/AllReferencedObjects.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/BackendProvider.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/ClearIncomingRefsOnOutgoingRefs.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/Constants.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/Constructors.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/DeleteMethod.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/FactoryImplementations.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/FactoryInterfaces.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/IDomainDataByFlidAccessorMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/InitializeFromBEPMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/Interfaces.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/LcmServiceLocatorBootstrapper.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/OtherMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/RemoveAReferenceCore.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/RemoveOwneeMethod.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/Repository.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/RepositoryImplementations.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/RepositoryInterfaces.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/RestoreIncomingRefsOnOutgoingRefs.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/SDABinaryPropertyMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/SDABooleanPropertyMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/SDADateTimePropertyMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/SDAGenDatePropertyMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/SDAGuidPropertyMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/SDAIntegerPropertyMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/SDAObjectPropertyMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/SDAStringPropertyMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/SDAVectorPropertyMethods.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/class.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/classInterface.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/datamembers.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/datamembers_atomic.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/datamembers_rel.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/datamembers_simple.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/factory.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/factoryInterface.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/main.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/module.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/propaccessors_atomic.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/propaccessors_rel.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/propaccessors_simple.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/propertyAccessors.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/propertyInterface.vm.cs (100%) rename src/{SIL.LCModel/LcmGenerate => SIL.LCModel.SourceGenerators/Templates/vm}/repositoryInterface.vm.cs (100%) delete mode 100644 src/SIL.LCModel/GenerateModel.proj diff --git a/AGENTS.md b/AGENTS.md index b32e91b86..51a49ae7c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,13 +65,14 @@ No command timeouts were observed. - src/SIL.LCModel: main LCM library (net462; netstandard2.0). - src/SIL.LCModel.Core: core utilities and ICU data generation (netstandard2.0; net462; net8.0). - src/SIL.LCModel.Utils: shared utilities (net462; netstandard2.0). -- src/SIL.LCModel.Build.Tasks: MSBuild tasks used for code generation. +- src/SIL.LCModel.Build.Tasks: the IdlImp MSBuild task (kernel-interface code generation). +- src/SIL.LCModel.SourceGenerators: Roslyn source generator for the LCM domain model (MasterLCModel.xml → Generated*.cs), built on the NVelocity templates in its Templates/vm folder. - src/SIL.LCModel.FixData: data-fix utilities. - src/CSTools: auxiliary tools (pg/lg/Tools). -Code generation targets to know about: -- SIL.LCModel: GenerateModel (MasterLCModel.xml → Generated*.cs). -- SIL.LCModel.Core: GenerateKernelCs, GenerateIcuData. +Code generation to know about: +- SIL.LCModel: the LcmModelGenerator source generator turns MasterLCModel.xml (with HandGenerated.xml and IntPropTypeOverrides.xml, wired in as AdditionalFiles) into the domain-model Generated*.cs sources at compile time. +- SIL.LCModel.Core: GenerateKernelCs (IdlImp task), GenerateIcuData. ### Tests (tests/) - SIL.LCModel.Tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 854b36793..d79b889cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed +- [SIL.LCModel] Replaced the `LcmGenerate` MSBuild task (which ran the NVelocity templates in a child MSBuild process) with the `SIL.LCModel.SourceGenerators` Roslyn source generator. The domain-model `Generated*.cs` sources are now produced at compile time from `MasterLCModel.xml`; the templates and model wrappers are unchanged and the generated output is byte-for-byte identical. No public API change. - [SIL.LCModel] Replaced the internal StructureMap (`structuremap.patched`) IoC container with `Microsoft.Extensions.DependencyInjection` (8.x). No public API change. - [SIL.LCModel] Trim 12 overwordy semantic domain descriptions and fix 22 punctuation/whitespace issues in the SemDom.xml template, matching sillsdev/FwLocalizations#5 and sillsdev/FwLocalizations#7 - [SIL.LCModel] `FileUtils.IsFileUriOrPath` checks for the presence of "file:" rather than the absence of known non-file URI schemes diff --git a/LCM.sln b/LCM.sln index 91956680d..251be820f 100644 --- a/LCM.sln +++ b/LCM.sln @@ -53,6 +53,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel.FixData", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestHelper", "tests\TestHelper\TestHelper.csproj", "{99270559-03D8-49F7-B61B-22E9DBC5D42B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SIL.LCModel.SourceGenerators", "src\SIL.LCModel.SourceGenerators\SIL.LCModel.SourceGenerators.csproj", "{688D1F13-8FAC-4FED-A908-1274B576D81B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,6 +113,10 @@ Global {99270559-03D8-49F7-B61B-22E9DBC5D42B}.Debug|Any CPU.Build.0 = Debug|Any CPU {99270559-03D8-49F7-B61B-22E9DBC5D42B}.Release|Any CPU.ActiveCfg = Release|Any CPU {99270559-03D8-49F7-B61B-22E9DBC5D42B}.Release|Any CPU.Build.0 = Release|Any CPU + {688D1F13-8FAC-4FED-A908-1274B576D81B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {688D1F13-8FAC-4FED-A908-1274B576D81B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {688D1F13-8FAC-4FED-A908-1274B576D81B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {688D1F13-8FAC-4FED-A908-1274B576D81B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -130,6 +136,7 @@ Global {8ECAF4C5-E651-4EA0-91FF-9251428E3890} = {B9B859EC-6E0D-447E-A0CD-ABF16E7278B1} {8E5F8AF6-6ED0-4205-826C-6BD80D04BDD1} = {040CD95F-B637-493B-9B1C-2989DF3A0D91} {99270559-03D8-49F7-B61B-22E9DBC5D42B} = {B9B859EC-6E0D-447E-A0CD-ABF16E7278B1} + {688D1F13-8FAC-4FED-A908-1274B576D81B} = {040CD95F-B637-493B-9B1C-2989DF3A0D91} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C625E82E-381D-4590-A382-0B4EF1EF916E} diff --git a/src/SIL.LCModel.Build.Tasks/LcmGenerate.cs b/src/SIL.LCModel.Build.Tasks/LcmGenerate.cs deleted file mode 100644 index 20f9df14d..000000000 --- a/src/SIL.LCModel.Build.Tasks/LcmGenerate.cs +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) 2006-2015 SIL International -// This software is licensed under the LGPL, version 2.1 or later -// (http://www.gnu.org/licenses/lgpl-2.1.html) - -using System; -using System.Collections.Generic; -using System.IO; -using System.Xml; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace SIL.LCModel.Build.Tasks -{ - /// ---------------------------------------------------------------------------------------- - /// - /// - /// - /// ---------------------------------------------------------------------------------------- - public class LcmGenerate: Task - { - /// ------------------------------------------------------------------------------------ - /// - /// Gets or sets the XML file. - /// - /// The XML file. - /// ------------------------------------------------------------------------------------ - [Required] - public string XmlFile { get; set; } - - /// ------------------------------------------------------------------------------------ - /// - /// Gets or sets the base dir for the output. - /// - /// The output directory. - /// ------------------------------------------------------------------------------------ - [Required] - public string OutputDir { get; set; } - - /// ------------------------------------------------------------------------------------ - /// - /// Gets or sets the file for the output. - /// - /// The output file name. - /// ------------------------------------------------------------------------------------ - [Required] - public string OutputFile { get; set; } - - /// ------------------------------------------------------------------------------------ - /// - /// Gets or sets the template file. - /// - /// The template file. - /// ------------------------------------------------------------------------------------ - [Required] - public string TemplateFile { get; set; } - - /// ------------------------------------------------------------------------------------ - /// - /// Gets or sets the template file. - /// - /// The template file. - /// ------------------------------------------------------------------------------------ - public string BackendTemplateFiles { get; set; } - - /// - /// Gets or sets the working directory. - /// - /// The working directory. - public string WorkingDirectory { get; set; } - - /// - /// Gets or sets the directory that contains HandGenerated.xml and IntPropTypeOverrides.xml - /// - public string HandGeneratedDir { get; set; } - - /// ------------------------------------------------------------------------------------ - /// - /// Executes the task. - /// - /// ------------------------------------------------------------------------------------ - public override bool Execute() - { - string origDir = Directory.GetCurrentDirectory(); - string oldDir; - if (!String.IsNullOrEmpty(WorkingDirectory)) - oldDir = WorkingDirectory; - else - oldDir = origDir; - try - { - var doc = new XmlDocument(); - string xmlPath = XmlFile; - if (!Path.IsPathRooted(xmlPath)) - xmlPath = Path.Combine(oldDir, XmlFile); - try - { - Log.LogMessage(MessageImportance.Low, "Loading XML file {0}.", xmlPath); - doc.Load(xmlPath); - } - catch (XmlException e) - { - Log.LogMessage(MessageImportance.High, $"Error loading XML file {xmlPath} {e.Message}"); - return false; - } - - var handGeneratedFilesDir = string.IsNullOrEmpty(HandGeneratedDir) - ? Path.Combine(oldDir, "LcmGenerate") - : HandGeneratedDir; - - var config = new XmlDocument(); - var handGeneratedClasses = new Dictionary>(); - try - { - var handGeneratedFile = Path.Combine(handGeneratedFilesDir, "HandGenerated.xml"); - Log.LogMessage(MessageImportance.Low, $"Loading hand generated classes from \"{handGeneratedFile}\"."); - config.Load(handGeneratedFile); - foreach (XmlElement node in config.GetElementsByTagName("Class")) - { - var props = new List(); -// ReSharper disable PossibleNullReferenceException - foreach (XmlNode propertyNode in node.SelectNodes("property")) -// ReSharper restore PossibleNullReferenceException - { - props.Add(propertyNode.Attributes["name"].Value); - } - if (props.Count > 0) - { - handGeneratedClasses.Add(node.Attributes["id"].Value, props); - } - } - } - catch (XmlException e) - { - Log.LogMessage(MessageImportance.High, $"Error loading hand generated classes {e.Message}"); - return false; - } - - // Dictionary - var intPropTypeOverridesClasses = new Dictionary>(); - try - { - var handGeneratedFile = Path.Combine(handGeneratedFilesDir, "IntPropTypeOverrides.xml"); - Log.LogMessage(MessageImportance.Low, - $"Loading hand generated classes from \"{handGeneratedFile}\"."); - config.Load(handGeneratedFile); - foreach (XmlElement node in config.GetElementsByTagName("Class")) - { - // Dictionary - var props = new Dictionary(); -// ReSharper disable PossibleNullReferenceException - foreach (XmlNode propertyNode in node.SelectNodes("property")) -// ReSharper restore PossibleNullReferenceException - { - props.Add(propertyNode.Attributes["name"].Value, - propertyNode.Attributes["type"].Value); - } - if (props.Count > 0) - { - intPropTypeOverridesClasses.Add(node.Attributes["id"].Value, props); - } - } - } - catch (XmlException e) - { - Log.LogMessage(MessageImportance.High, $"Error loading IntPropTypeOverrides classes {e.Message}"); - return false; - } - - - try - { - // Remember current directory. - var originalCurrentDirectory = Directory.GetCurrentDirectory(); - - Log.LogMessage(MessageImportance.Low, "Processing template {0}.", TemplateFile); - string outputDirPath = OutputDir; - if (!Path.IsPathRooted(OutputDir)) - outputDirPath = Path.Combine(oldDir, OutputDir); - var lcmGenerate = new LcmGenerateImpl(doc, outputDirPath) - { - Overrides = handGeneratedClasses, - IntPropTypeOverrides = intPropTypeOverridesClasses - }; - string outputPath = OutputFile; - if (!Path.IsPathRooted(outputPath)) - outputPath = Path.Combine(outputDirPath, OutputFile); - // Generate the main code. - if (Path.GetDirectoryName(TemplateFile).Length > 0) - Directory.SetCurrentDirectory(Path.GetDirectoryName(TemplateFile)); - lcmGenerate.SetOutput(outputPath); - lcmGenerate.Process(Path.GetFileName(TemplateFile)); - - // Generate the backend provider(s) code. - if (!string.IsNullOrEmpty(BackendTemplateFiles)) - { - foreach (var backendDir in BackendTemplateFiles.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) - { - var beDir = backendDir.Trim(); - if (beDir == string.Empty) continue; - - var curDir = Path.Combine(Path.Combine(OutputDir, "LcmGenerate"), beDir); - Directory.SetCurrentDirectory(curDir); - lcmGenerate.SetOutput(Path.Combine(beDir, beDir + @"Generated.cs")); - lcmGenerate.Process("Main" + beDir + ".vm.cs"); - } - } - - // Restore original directory. - Directory.SetCurrentDirectory(originalCurrentDirectory); - } - catch (Exception e) - { - Log.LogMessage(MessageImportance.High, "Error processing template" + " " + e.Message); - return false; - } - } - finally - { - Directory.SetCurrentDirectory(origDir); - } - return true; - } - } -} diff --git a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj index f4cab1210..e0455a946 100644 --- a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj +++ b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj @@ -4,7 +4,7 @@ SIL.LCModel.Build.Tasks net462;netstandard2.0 The liblcm library is the core FieldWorks model for linguistic analyses of languages. Tools in this library provide the ability to store and interact with language and culture data, including anthropological, text corpus, and linguistics data. -SIL.LCModel.Build.Tasks provides msbuild tasks for generating C# classes for the FieldWorks model: IdlImp and LcmGenerate. +SIL.LCModel.Build.Tasks provides the IdlImp msbuild task for generating C# kernel interfaces for the FieldWorks model. (The LCM domain model is generated by the SIL.LCModel.SourceGenerators Roslyn source generator.) tools/$(TargetFramework) true true @@ -13,7 +13,6 @@ SIL.LCModel.Build.Tasks provides msbuild tasks for generating C# classes for the - @@ -40,7 +39,6 @@ SIL.LCModel.Build.Tasks provides msbuild tasks for generating C# classes for the - diff --git a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.props b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.props index a417b1424..e1bd39ef7 100644 --- a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.props +++ b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.props @@ -4,6 +4,5 @@ - \ No newline at end of file diff --git a/src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs b/src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs new file mode 100644 index 000000000..eeab5c278 --- /dev/null +++ b/src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2025 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Commons.Collections; +using NVelocity.Exception; +using NVelocity.Runtime.Resource; +using NVelocity.Runtime.Resource.Loader; + +namespace SIL.LCModel.SourceGenerators +{ + /// + /// An NVelocity that serves templates from an in-memory + /// dictionary keyed by their bare file name (e.g. "class.vm.cs"). This replaces the old + /// FileResourceLoader + Directory.SetCurrentDirectory mechanism so the engine + /// can run inside a Roslyn source generator, where templates live as embedded resources and + /// there is no meaningful current directory. + /// + /// + /// The template set is supplied via the static property. Because the + /// NVelocity RuntimeSingleton and this loader are process-global, generation is + /// serialized by the host (see ), so a static template set is safe. + /// + internal sealed class EmbeddedTemplateLoader : ResourceLoader + { + public const string TemplatesKey = "lcm-templates"; + /// The templates to serve, keyed by bare file name (e.g. "main.vm.cs"). + public IReadOnlyDictionary Templates { get; set; } + + public override void Init(ExtendedProperties configuration) + { + Templates = configuration.GetProperty(TemplatesKey) as IReadOnlyDictionary ?? throw new InvalidOperationException($"No templates supplied (key: {TemplatesKey})"); + } + + public override Stream GetResourceStream(string source) + { + // NVelocity may hand us names with directory separators or leading "./"; normalize + // to the bare file name we key on. + var name = source.Replace('\\', '/'); + var slash = name.LastIndexOf('/'); + if (slash >= 0) + name = name.Substring(slash + 1); + + if (Templates != null && Templates.TryGetValue(name, out var text)) + return new MemoryStream(Encoding.UTF8.GetBytes(text)); + + throw new ResourceNotFoundException($"EmbeddedTemplateLoader: cannot locate template '{source}'."); + } + + public override bool IsSourceModified(Resource resource) => false; + + public override long GetLastModified(Resource resource) => 0; + } +} diff --git a/src/SIL.LCModel.Build.Tasks/LcmGenerateImpl.cs b/src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs similarity index 64% rename from src/SIL.LCModel.Build.Tasks/LcmGenerateImpl.cs rename to src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs index 5766d7344..afba27c3b 100644 --- a/src/SIL.LCModel.Build.Tasks/LcmGenerateImpl.cs +++ b/src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2006-2015 SIL International +// Copyright (c) 2006-2025 SIL International // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) @@ -10,11 +10,13 @@ using NVelocity.App; using NVelocity.Runtime; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// - /// LCM code generator + /// LCM code generator. Runs the NVelocity templates over the LCM model and collects the + /// generated sources in memory (keyed by their logical output path) instead of writing files, + /// so it can be hosted inside a Roslyn source generator. /// /// ---------------------------------------------------------------------------------------- internal class LcmGenerateImpl @@ -22,7 +24,6 @@ internal class LcmGenerateImpl /// public static LcmGenerateImpl Generator; - private readonly string m_OutputDir; private string m_OutputFileName; private readonly VelocityEngine m_Engine; private readonly VelocityContext m_Context; @@ -31,42 +32,47 @@ internal class LcmGenerateImpl private readonly Model m_Model; private readonly XmlDocument m_Document; - /// ------------------------------------------------------------------------------------ - /// - /// Initializes a new instance of the class. - /// - /// The XMI document. - /// The output dir. - /// ------------------------------------------------------------------------------------ - public LcmGenerateImpl(XmlDocument doc, string outputDir) - : this(doc, outputDir, null) - { - } + /// Collected outputs, keyed by the logical output path passed to SetOutput + /// (e.g. "GeneratedConstants.cs", "DomainImpl/GeneratedClasses.cs"). + private readonly Dictionary m_Outputs = + new Dictionary(StringComparer.Ordinal); /// ------------------------------------------------------------------------------------ /// /// Initializes a new instance of the class. /// - /// The XMI document. - /// The output dir. - /// The output file name. + /// The model document. + /// The templates, keyed by bare file name (e.g. "main.vm.cs"). /// ------------------------------------------------------------------------------------ - public LcmGenerateImpl(XmlDocument doc, string outputDir, string outputFile) + public LcmGenerateImpl(XmlDocument doc, IReadOnlyDictionary templates) { Generator = this; m_Document = doc; - m_OutputDir = outputDir; - m_OutputFileName = outputFile; var entireModel = (XmlElement)doc.GetElementsByTagName("EntireModel")[0]; m_Model = new Model(entireModel); m_Engine = new VelocityEngine(); + // Serve templates from the embedded-resource loader rather than the file system. + // NVelocity's ExtendedProperties treats commas as list separators, so the loader class + // name must use a semicolon between the type and assembly names (getLoader converts the + // ';' back to a ',' before calling Type.GetType). The simple "Type, Assembly" form is + // enough for Type.GetType to locate the already-loaded generator assembly. + var loaderType = typeof(EmbeddedTemplateLoader); + m_Engine.SetProperty("resource.loader", "embedded"); + m_Engine.SetProperty("embedded.resource.loader.class", + loaderType.FullName + "; " + loaderType.Assembly.GetName().Name); + m_Engine.SetProperty("embedded.resource.loader.cache", "true"); + m_Engine.SetProperty("embedded.resource.loader.modificationCheckInterval", "0"); + m_Engine.SetProperty($"embedded.resource.loader.{EmbeddedTemplateLoader.TemplatesKey}", templates); m_Engine.Init(); m_Context = new VelocityContext(); m_Context.Put("lcmgenerate", this); m_Context.Put("model", m_Model); + // The model wrappers (e.g. Property) look up the override lists through this global + // attribute, so keep publishing it. Generation is serialized by the host, so the use + // of the process-global RuntimeSingleton is safe. RuntimeSingleton.RuntimeServices.SetApplicationAttribute("LcmGenerate.Engine", m_Engine); RuntimeSingleton.RuntimeServices.SetApplicationAttribute("LcmGenerate.Context", m_Context); } @@ -75,7 +81,6 @@ public LcmGenerateImpl(XmlDocument doc, string outputDir, string outputFile) /// /// Gets or sets the list with the class names that we want to override. /// - /// The list of override class names. /// ------------------------------------------------------------------------------------ public Dictionary> Overrides { @@ -88,7 +93,6 @@ public Dictionary> Overrides /// Gets or sets the list with the names and types of integer properties we want to /// override. /// - /// The list of override class names. /// ------------------------------------------------------------------------------------ public Dictionary> IntPropTypeOverrides { @@ -97,10 +101,7 @@ public Dictionary> IntPropTypeOverrides } /// ------------------------------------------------------------------------------------ - /// - /// Gets the model. - /// - /// The model. + /// Gets the model. /// ------------------------------------------------------------------------------------ public Model Model { @@ -108,21 +109,18 @@ public Model Model } /// ------------------------------------------------------------------------------------ - /// - /// Gets the modules. - /// - /// The modules. + /// Gets the modules. /// ------------------------------------------------------------------------------------ public StringKeyCollection Modules { get { return Model.Modules; } } + /// The generated outputs, keyed by their logical output path. + public IReadOnlyDictionary Outputs => m_Outputs; + /// ------------------------------------------------------------------------------------ - /// - /// Sets the output file name. - /// - /// The output file name. + /// Sets the (logical) output name that the next Process call writes to. /// ------------------------------------------------------------------------------------ public void SetOutput(string outputFile) { @@ -131,45 +129,30 @@ public void SetOutput(string outputFile) /// ------------------------------------------------------------------------------------ /// - /// Processes the specified template. + /// Merges the specified template and stores the result under the output name that was in + /// effect when this call started. The output name is captured up front because the + /// template being processed (notably main.vm.cs) mutates it via nested SetOutput/Process + /// calls while this merge is still running. /// - /// Name of the template. /// ------------------------------------------------------------------------------------ public void Process(string templateName) { - Stream stream = null; - try - { - stream = string.IsNullOrEmpty(m_OutputFileName) - ? (Stream) new MemoryStream() - : new FileStream(Path.Combine(m_OutputDir, m_OutputFileName), - FileMode.Create, FileAccess.Write); - - using (var writer = new StreamWriter(stream)) - { - m_Engine.MergeTemplate(templateName, "UTF-8", m_Context, writer); - } - } - finally + var outputName = m_OutputFileName; + using (var writer = new StringWriter()) { - if (stream != null) - stream.Dispose(); + m_Engine.MergeTemplate(templateName, "UTF-8", m_Context, writer); + if (!string.IsNullOrEmpty(outputName)) + m_Outputs[outputName] = writer.ToString(); } } /// ------------------------------------------------------------------------------------ - /// - /// Gets the module. - /// - /// Name of the module. - /// + /// Gets the module. /// ------------------------------------------------------------------------------------ public CellarModule GetModule(string moduleName) { var query = string.Format("//CellarModule[@id='{0}']", moduleName); -// ReSharper disable PossibleNullReferenceException var iterator = m_Document.CreateNavigator().Select(query); -// ReSharper restore PossibleNullReferenceException if (iterator.MoveNext()) { var module = (XmlElement)iterator.Current.UnderlyingObject; @@ -179,18 +162,12 @@ public CellarModule GetModule(string moduleName) } /// ------------------------------------------------------------------------------------ - /// - /// Gets the class. - /// - /// Name of the class. - /// + /// Gets the class. /// ------------------------------------------------------------------------------------ public IClass GetClass(string className) { var query = string.Format("//CellarModule[class/@id='{0}']", className); -// ReSharper disable PossibleNullReferenceException var iterator = m_Document.CreateNavigator().Select(query); -// ReSharper restore PossibleNullReferenceException if (iterator.MoveNext()) { var module = (XmlElement)iterator.Current.UnderlyingObject; @@ -205,12 +182,8 @@ public IClass GetClass(string className) /// Put '/// ' at the start of each line in , /// including at the start of the string. /// - /// - /// public string StringAsMSComment(string commentData) { - //var chunks = Regex.Split(commentData, "\r\n"); - var chunks = commentData.Trim().Split(new[] { '\n', '\r' diff --git a/src/SIL.LCModel.SourceGenerators/LcmModelGenerator.cs b/src/SIL.LCModel.SourceGenerators/LcmModelGenerator.cs new file mode 100644 index 000000000..7d750e3cc --- /dev/null +++ b/src/SIL.LCModel.SourceGenerators/LcmModelGenerator.cs @@ -0,0 +1,139 @@ +// Copyright (c) 2025 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace SIL.LCModel.SourceGenerators +{ + /// + /// Roslyn incremental source generator that produces the LCM domain-model sources from + /// MasterLCModel.xml (plus HandGenerated.xml and IntPropTypeOverrides.xml). It replaces the + /// former LcmGenerate MSBuild task and its child-process build step. + /// + /// + /// Wire the model files into the consuming project as AdditionalFiles marked with the + /// LcmModel metadata, e.g.: + /// + /// <AdditionalFiles Include="MasterLCModel.xml" LcmModel="true" /> + /// <CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="LcmModel" /> + /// + /// + [Generator(LanguageNames.CSharp)] + public sealed class LcmModelGenerator : IIncrementalGenerator + { + private const string MasterModelFileName = "MasterLCModel.xml"; + private const string HandGeneratedFileName = "HandGenerated.xml"; + private const string IntPropTypeOverridesFileName = "IntPropTypeOverrides.xml"; + + private static readonly DiagnosticDescriptor s_generationFailed = new DiagnosticDescriptor( + id: "LCM001", + title: "LCM model generation failed", + messageFormat: "LCM model generation failed: {0}", + category: "SIL.LCModel.SourceGenerators", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor s_missingInput = new DiagnosticDescriptor( + id: "LCM002", + title: "LCM model input missing", + messageFormat: "LCM model generation requires an AdditionalFiles entry named '{0}' with LcmModel=\"true\"", + category: "SIL.LCModel.SourceGenerators", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Project each LcmModel-marked AdditionalFile to (fileName, contents). Using the + // string contents as the pipeline value lets Roslyn cache when nothing changed. + var modelFiles = context.AdditionalTextsProvider + .Combine(context.AnalyzerConfigOptionsProvider) + .Where(pair => + { + var options = pair.Right.GetOptions(pair.Left); + return options.TryGetValue("build_metadata.AdditionalFiles.LcmModel", out var flag) + && flag.Equals("true", StringComparison.OrdinalIgnoreCase); + }) + .Select((pair, ct) => + { + var fileName = System.IO.Path.GetFileName(pair.Left.Path); + var text = pair.Left.GetText(ct)?.ToString() ?? string.Empty; + return new ModelInput(fileName, text); + }) + .Collect(); + + context.RegisterSourceOutput(modelFiles, Execute); + } + + private static void Execute(SourceProductionContext context, ImmutableArray inputs) + { + var master = Find(inputs, MasterModelFileName); + var handGenerated = Find(inputs, HandGeneratedFileName); + var intPropOverrides = Find(inputs, IntPropTypeOverridesFileName); + + // The master model must be present for the generator to do anything. The two override + // files are optional in principle but expected in the LCM build; treat a missing one + // as empty so generation still runs. + if (master == null) + { + // Only report if some LcmModel file was supplied (avoids noise in unrelated projects). + if (!inputs.IsDefaultOrEmpty) + context.ReportDiagnostic(Diagnostic.Create(s_missingInput, Location.None, MasterModelFileName)); + return; + } + + try + { + var outputs = LcmModelRunner.Generate( + master, + handGenerated ?? "", + intPropOverrides ?? ""); + + foreach (var kvp in outputs) + { + var hintName = kvp.Key.Replace('/', '.').Replace('\\', '.'); + context.AddSource(hintName, SourceText.From(kvp.Value, System.Text.Encoding.UTF8)); + } + } + catch (Exception ex) + { + context.ReportDiagnostic(Diagnostic.Create(s_generationFailed, Location.None, ex.Message)); + } + } + + private static string Find(ImmutableArray inputs, string fileName) + { + foreach (var input in inputs) + { + if (string.Equals(input.FileName, fileName, StringComparison.OrdinalIgnoreCase)) + return input.Contents; + } + return null; + } + + private readonly struct ModelInput : IEquatable + { + public ModelInput(string fileName, string contents) + { + FileName = fileName; + Contents = contents; + } + + public string FileName { get; } + public string Contents { get; } + + public bool Equals(ModelInput other) => + FileName == other.FileName && Contents == other.Contents; + + public override bool Equals(object obj) => obj is ModelInput other && Equals(other); + + public override int GetHashCode() => + unchecked((FileName?.GetHashCode() ?? 0) * 397 ^ (Contents?.GetHashCode() ?? 0)); + } + } +} diff --git a/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs b/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs new file mode 100644 index 000000000..17e51df37 --- /dev/null +++ b/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs @@ -0,0 +1,116 @@ +// Copyright (c) 2025 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Xml; + +namespace SIL.LCModel.SourceGenerators +{ + /// + /// Hosts the NVelocity-based LCM code generation independent of any particular driver + /// (MSBuild task, console harness, or Roslyn source generator). Loads the embedded templates, + /// parses the override configuration, and returns the generated sources keyed by logical + /// output path. + /// + internal static class LcmModelRunner + { + // NVelocity's RuntimeSingleton and the EmbeddedTemplateLoader template set are + // process-global, so serialize generation to keep concurrent compilations from + // interfering with each other. + private static readonly object s_gate = new object(); + + private static IReadOnlyDictionary s_templates; + + /// + /// Runs the generator over the given model inputs and returns the generated sources, + /// keyed by the logical output path used inside the templates + /// (e.g. "GeneratedConstants.cs", "DomainImpl/GeneratedClasses.cs"). + /// + /// Contents of MasterLCModel.xml. + /// Contents of HandGenerated.xml. + /// Contents of IntPropTypeOverrides.xml. + public static IReadOnlyDictionary Generate( + string masterModelXml, string handGeneratedXml, string intPropTypeOverridesXml) + { + lock (s_gate) + { + var templates = s_templates ??= LoadEmbeddedTemplates(); + + var doc = new XmlDocument(); + doc.LoadXml(masterModelXml); + + var handGenerated = ParseHandGenerated(handGeneratedXml); + var intPropOverrides = ParseIntPropTypeOverrides(intPropTypeOverridesXml); + + var impl = new LcmGenerateImpl(doc, templates) + { + Overrides = handGenerated, + IntPropTypeOverrides = intPropOverrides + }; + + // main.vm.cs writes the class implementations to this output and, via nested + // SetOutput/Process calls, produces the other eight outputs. + impl.SetOutput("DomainImpl/GeneratedClasses.cs"); + impl.Process("main.vm.cs"); + + return impl.Outputs; + } + } + + /// Loads every embedded "*.vm.cs" template, keyed by its bare file name. + private static IReadOnlyDictionary LoadEmbeddedTemplates() + { + var result = new Dictionary(StringComparer.Ordinal); + var asm = typeof(LcmModelRunner).Assembly; + foreach (var name in asm.GetManifestResourceNames()) + { + if (!name.EndsWith(".vm.cs", StringComparison.Ordinal)) + continue; + using (var stream = asm.GetManifestResourceStream(name)) + using (var reader = new StreamReader(stream)) + { + result[name] = reader.ReadToEnd(); + } + } + return result; + } + + /// Parses HandGenerated.xml into a map of class name to hand-generated property names. + private static Dictionary> ParseHandGenerated(string xml) + { + var result = new Dictionary>(); + var config = new XmlDocument(); + config.LoadXml(xml); + foreach (XmlElement node in config.GetElementsByTagName("Class")) + { + var props = new List(); + foreach (XmlNode propertyNode in node.SelectNodes("property")) + props.Add(propertyNode.Attributes["name"].Value); + if (props.Count > 0) + result.Add(node.Attributes["id"].Value, props); + } + return result; + } + + /// Parses IntPropTypeOverrides.xml into a map of class name to (property name to type). + private static Dictionary> ParseIntPropTypeOverrides(string xml) + { + var result = new Dictionary>(); + var config = new XmlDocument(); + config.LoadXml(xml); + foreach (XmlElement node in config.GetElementsByTagName("Class")) + { + var props = new Dictionary(); + foreach (XmlNode propertyNode in node.SelectNodes("property")) + props.Add(propertyNode.Attributes["name"].Value, propertyNode.Attributes["type"].Value); + if (props.Count > 0) + result.Add(node.Attributes["id"].Value, props); + } + return result; + } + } +} diff --git a/src/SIL.LCModel.Build.Tasks/Base.cs b/src/SIL.LCModel.SourceGenerators/Model/Base.cs similarity index 96% rename from src/SIL.LCModel.Build.Tasks/Base.cs rename to src/SIL.LCModel.SourceGenerators/Model/Base.cs index 99e13095c..8941a02fe 100644 --- a/src/SIL.LCModel.Build.Tasks/Base.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/Base.cs @@ -5,7 +5,7 @@ using System; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// @@ -82,7 +82,7 @@ protected static string AsMSString(string tabs, XmlNode parentNode) var retval = string.Empty; foreach (XmlNode paraNode in parentNode.SelectNodes("para")) - retval = retval + tabs+ "/// " + paraNode.OuterXml + Environment.NewLine; + retval = retval + tabs+ "/// " + paraNode.OuterXml + "\r\n"; return retval.TrimEnd(); } } diff --git a/src/SIL.LCModel.Build.Tasks/CellarModule.cs b/src/SIL.LCModel.SourceGenerators/Model/CellarModule.cs similarity index 98% rename from src/SIL.LCModel.Build.Tasks/CellarModule.cs rename to src/SIL.LCModel.SourceGenerators/Model/CellarModule.cs index 7a4144ba7..d0dbea070 100644 --- a/src/SIL.LCModel.Build.Tasks/CellarModule.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/CellarModule.cs @@ -5,7 +5,7 @@ using System; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/Class.cs b/src/SIL.LCModel.SourceGenerators/Model/Class.cs similarity index 99% rename from src/SIL.LCModel.Build.Tasks/Class.cs rename to src/SIL.LCModel.SourceGenerators/Model/Class.cs index 5a335f84f..3aa190013 100644 --- a/src/SIL.LCModel.Build.Tasks/Class.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/Class.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/DummyClass.cs b/src/SIL.LCModel.SourceGenerators/Model/DummyClass.cs similarity index 99% rename from src/SIL.LCModel.Build.Tasks/DummyClass.cs rename to src/SIL.LCModel.SourceGenerators/Model/DummyClass.cs index 9dd2d6677..388e3407f 100644 --- a/src/SIL.LCModel.Build.Tasks/DummyClass.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/DummyClass.cs @@ -2,7 +2,7 @@ // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/IClass.cs b/src/SIL.LCModel.SourceGenerators/Model/IClass.cs similarity index 99% rename from src/SIL.LCModel.Build.Tasks/IClass.cs rename to src/SIL.LCModel.SourceGenerators/Model/IClass.cs index c799cbb11..22365f1bc 100644 --- a/src/SIL.LCModel.Build.Tasks/IClass.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/IClass.cs @@ -2,7 +2,7 @@ // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/Model.cs b/src/SIL.LCModel.SourceGenerators/Model/Model.cs similarity index 97% rename from src/SIL.LCModel.Build.Tasks/Model.cs rename to src/SIL.LCModel.SourceGenerators/Model/Model.cs index d7a239f97..35a938f6f 100644 --- a/src/SIL.LCModel.Build.Tasks/Model.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/Model.cs @@ -5,7 +5,7 @@ using System; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { #region StringKeyCollection diff --git a/src/SIL.LCModel.Build.Tasks/Property.cs b/src/SIL.LCModel.SourceGenerators/Model/Property.cs similarity index 99% rename from src/SIL.LCModel.Build.Tasks/Property.cs rename to src/SIL.LCModel.SourceGenerators/Model/Property.cs index 63e075477..70d22af3d 100644 --- a/src/SIL.LCModel.Build.Tasks/Property.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/Property.cs @@ -7,7 +7,7 @@ using NVelocity; using NVelocity.Runtime; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/RelationalProperty.cs b/src/SIL.LCModel.SourceGenerators/Model/RelationalProperty.cs similarity index 99% rename from src/SIL.LCModel.Build.Tasks/RelationalProperty.cs rename to src/SIL.LCModel.SourceGenerators/Model/RelationalProperty.cs index 92ec3531b..86367d483 100644 --- a/src/SIL.LCModel.Build.Tasks/RelationalProperty.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/RelationalProperty.cs @@ -5,7 +5,7 @@ using System.Text; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/StringKeyCollection.cs b/src/SIL.LCModel.SourceGenerators/Model/StringKeyCollection.cs similarity index 95% rename from src/SIL.LCModel.Build.Tasks/StringKeyCollection.cs rename to src/SIL.LCModel.SourceGenerators/Model/StringKeyCollection.cs index 0d81aceed..fcfa6f21c 100644 --- a/src/SIL.LCModel.Build.Tasks/StringKeyCollection.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/StringKeyCollection.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/TypeInfo.cs b/src/SIL.LCModel.SourceGenerators/Model/TypeInfo.cs similarity index 98% rename from src/SIL.LCModel.Build.Tasks/TypeInfo.cs rename to src/SIL.LCModel.SourceGenerators/Model/TypeInfo.cs index 1c7c8a80a..2933d4089 100644 --- a/src/SIL.LCModel.Build.Tasks/TypeInfo.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/TypeInfo.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.SourceGenerators { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj b/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj new file mode 100644 index 000000000..cf36b5ef7 --- /dev/null +++ b/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj @@ -0,0 +1,42 @@ + + + + netstandard2.0 + SIL.LCModel.SourceGenerators + Roslyn source generator that produces the LCM domain model classes from MasterLCModel.xml. Replaces the former LcmGenerate MSBuild task. + liblcm + true + true + false + false + + $(NoWarn);NU5128;RS2008 + + + + + + + + + + + + %(Filename)%(Extension) + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + diff --git a/src/SIL.LCModel/LcmGenerate/AllReferencedObjects.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/AllReferencedObjects.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/AllReferencedObjects.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/AllReferencedObjects.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/BackendProvider.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/BackendProvider.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/BackendProvider.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/BackendProvider.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/ClearIncomingRefsOnOutgoingRefs.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/ClearIncomingRefsOnOutgoingRefs.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/ClearIncomingRefsOnOutgoingRefs.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/ClearIncomingRefsOnOutgoingRefs.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/Constants.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/Constants.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/Constants.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/Constants.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/Constructors.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/Constructors.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/Constructors.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/Constructors.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/DeleteMethod.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/DeleteMethod.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/DeleteMethod.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/DeleteMethod.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/FactoryImplementations.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/FactoryImplementations.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/FactoryImplementations.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/FactoryImplementations.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/FactoryInterfaces.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/FactoryInterfaces.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/FactoryInterfaces.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/FactoryInterfaces.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/IDomainDataByFlidAccessorMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/IDomainDataByFlidAccessorMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/IDomainDataByFlidAccessorMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/IDomainDataByFlidAccessorMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/InitializeFromBEPMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/InitializeFromBEPMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/InitializeFromBEPMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/InitializeFromBEPMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/Interfaces.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/Interfaces.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/Interfaces.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/Interfaces.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/LcmServiceLocatorBootstrapper.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/LcmServiceLocatorBootstrapper.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/LcmServiceLocatorBootstrapper.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/LcmServiceLocatorBootstrapper.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/OtherMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/OtherMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/OtherMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/OtherMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/RemoveAReferenceCore.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/RemoveAReferenceCore.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/RemoveAReferenceCore.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/RemoveAReferenceCore.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/RemoveOwneeMethod.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/RemoveOwneeMethod.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/RemoveOwneeMethod.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/RemoveOwneeMethod.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/Repository.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/Repository.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/Repository.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/Repository.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/RepositoryImplementations.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/RepositoryImplementations.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/RepositoryImplementations.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/RepositoryImplementations.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/RepositoryInterfaces.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/RepositoryInterfaces.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/RepositoryInterfaces.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/RepositoryInterfaces.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/RestoreIncomingRefsOnOutgoingRefs.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/RestoreIncomingRefsOnOutgoingRefs.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/RestoreIncomingRefsOnOutgoingRefs.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/RestoreIncomingRefsOnOutgoingRefs.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/SDABinaryPropertyMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/SDABinaryPropertyMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/SDABinaryPropertyMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/SDABinaryPropertyMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/SDABooleanPropertyMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/SDABooleanPropertyMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/SDABooleanPropertyMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/SDABooleanPropertyMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/SDADateTimePropertyMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/SDADateTimePropertyMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/SDADateTimePropertyMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/SDADateTimePropertyMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/SDAGenDatePropertyMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/SDAGenDatePropertyMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/SDAGenDatePropertyMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/SDAGenDatePropertyMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/SDAGuidPropertyMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/SDAGuidPropertyMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/SDAGuidPropertyMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/SDAGuidPropertyMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/SDAIntegerPropertyMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/SDAIntegerPropertyMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/SDAIntegerPropertyMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/SDAIntegerPropertyMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/SDAObjectPropertyMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/SDAObjectPropertyMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/SDAObjectPropertyMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/SDAObjectPropertyMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/SDAStringPropertyMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/SDAStringPropertyMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/SDAStringPropertyMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/SDAStringPropertyMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/SDAVectorPropertyMethods.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/SDAVectorPropertyMethods.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/SDAVectorPropertyMethods.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/SDAVectorPropertyMethods.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/class.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/class.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/class.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/class.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/classInterface.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/classInterface.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/classInterface.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/classInterface.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/datamembers.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/datamembers.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/datamembers.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/datamembers.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/datamembers_atomic.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/datamembers_atomic.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/datamembers_atomic.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/datamembers_atomic.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/datamembers_rel.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/datamembers_rel.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/datamembers_rel.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/datamembers_rel.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/datamembers_simple.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/datamembers_simple.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/datamembers_simple.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/datamembers_simple.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/factory.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/factory.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/factory.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/factory.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/factoryInterface.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/factoryInterface.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/factoryInterface.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/factoryInterface.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/main.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/main.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/main.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/main.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/module.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/module.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/module.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/module.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/propaccessors_atomic.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/propaccessors_atomic.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/propaccessors_atomic.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/propaccessors_atomic.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/propaccessors_rel.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/propaccessors_rel.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/propaccessors_rel.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/propaccessors_rel.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/propaccessors_simple.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/propaccessors_simple.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/propaccessors_simple.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/propaccessors_simple.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/propertyAccessors.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/propertyAccessors.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/propertyAccessors.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/propertyAccessors.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/propertyInterface.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/propertyInterface.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/propertyInterface.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/propertyInterface.vm.cs diff --git a/src/SIL.LCModel/LcmGenerate/repositoryInterface.vm.cs b/src/SIL.LCModel.SourceGenerators/Templates/vm/repositoryInterface.vm.cs similarity index 100% rename from src/SIL.LCModel/LcmGenerate/repositoryInterface.vm.cs rename to src/SIL.LCModel.SourceGenerators/Templates/vm/repositoryInterface.vm.cs diff --git a/src/SIL.LCModel/GenerateModel.proj b/src/SIL.LCModel/GenerateModel.proj deleted file mode 100644 index 8617aada0..000000000 --- a/src/SIL.LCModel/GenerateModel.proj +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/SIL.LCModel/SIL.LCModel.csproj b/src/SIL.LCModel/SIL.LCModel.csproj index 854f5faec..05e4dd3fc 100644 --- a/src/SIL.LCModel/SIL.LCModel.csproj +++ b/src/SIL.LCModel/SIL.LCModel.csproj @@ -8,12 +8,24 @@ liblcm + - + + + + + - + + + + + @@ -88,35 +100,6 @@ - - dotnet build - "$(MSBuildBinPath)\msbuild.exe" - msbuild - - - - - - - - - - - - - - - - - - - - - - - - From 7cd99e3cb0f0605710bf06ced63f904b68fcebb8 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 10 Sep 2026 17:10:13 +0700 Subject: [PATCH 2/5] Remove RuntimeSingleton coupling and the generation lock Property looked up the current generator via NVelocity's process-global RuntimeSingleton to read the override lists, which forced generation to be serialized. Store the LcmGenerateImpl on the root Model instead and have Property reach it through the existing parent chain (Property -> Class -> CellarModule -> Model). With that global gone (and the never-read "LcmGenerate.Engine" attribute and the write-only static Generator field removed), a generation run is entirely instance-local, so LcmModelRunner no longer needs a lock; the immutable template set is cached once via Lazy. Verified with a concurrency stress test (32 simultaneous generations, repeated) all producing byte-identical output, and the output remains byte-for-byte identical to the previous commit. SIL.LCModel.Tests pass (1709 passed, 0 failed, 18 skipped). Co-Authored-By: Claude Opus 4.8 --- .../EmbeddedTemplateLoader.cs | 7 +-- .../LcmGenerateImpl.cs | 13 +----- .../LcmModelRunner.cs | 46 +++++++++---------- .../Model/Model.cs | 11 ++++- .../Model/Property.cs | 12 ++--- 5 files changed, 40 insertions(+), 49 deletions(-) diff --git a/src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs b/src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs index eeab5c278..af4341d09 100644 --- a/src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs +++ b/src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs @@ -21,9 +21,10 @@ namespace SIL.LCModel.SourceGenerators /// there is no meaningful current directory. /// /// - /// The template set is supplied via the static property. Because the - /// NVelocity RuntimeSingleton and this loader are process-global, generation is - /// serialized by the host (see ), so a static template set is safe. + /// The template set is supplied per engine through the embedded.resource.loader.lcm-templates + /// property (see ), which NVelocity hands to as + /// this loader instance's configuration. Nothing here is shared between engines, so concurrent + /// generations are independent. /// internal sealed class EmbeddedTemplateLoader : ResourceLoader { diff --git a/src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs b/src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs index afba27c3b..0c52c616d 100644 --- a/src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs +++ b/src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs @@ -8,7 +8,6 @@ using System.Xml; using NVelocity; using NVelocity.App; -using NVelocity.Runtime; namespace SIL.LCModel.SourceGenerators { @@ -21,9 +20,6 @@ namespace SIL.LCModel.SourceGenerators /// ---------------------------------------------------------------------------------------- internal class LcmGenerateImpl { - /// - public static LcmGenerateImpl Generator; - private string m_OutputFileName; private readonly VelocityEngine m_Engine; private readonly VelocityContext m_Context; @@ -46,10 +42,9 @@ internal class LcmGenerateImpl /// ------------------------------------------------------------------------------------ public LcmGenerateImpl(XmlDocument doc, IReadOnlyDictionary templates) { - Generator = this; m_Document = doc; var entireModel = (XmlElement)doc.GetElementsByTagName("EntireModel")[0]; - m_Model = new Model(entireModel); + m_Model = new Model(entireModel, this); m_Engine = new VelocityEngine(); // Serve templates from the embedded-resource loader rather than the file system. @@ -69,12 +64,6 @@ public LcmGenerateImpl(XmlDocument doc, IReadOnlyDictionary temp m_Context = new VelocityContext(); m_Context.Put("lcmgenerate", this); m_Context.Put("model", m_Model); - - // The model wrappers (e.g. Property) look up the override lists through this global - // attribute, so keep publishing it. Generation is serialized by the host, so the use - // of the process-global RuntimeSingleton is safe. - RuntimeSingleton.RuntimeServices.SetApplicationAttribute("LcmGenerate.Engine", m_Engine); - RuntimeSingleton.RuntimeServices.SetApplicationAttribute("LcmGenerate.Context", m_Context); } /// ------------------------------------------------------------------------------------ diff --git a/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs b/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs index 17e51df37..343434f83 100644 --- a/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs +++ b/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs @@ -18,12 +18,13 @@ namespace SIL.LCModel.SourceGenerators /// internal static class LcmModelRunner { - // NVelocity's RuntimeSingleton and the EmbeddedTemplateLoader template set are - // process-global, so serialize generation to keep concurrent compilations from - // interfering with each other. - private static readonly object s_gate = new object(); - - private static IReadOnlyDictionary s_templates; + // The embedded template set is immutable, so cache it once (thread-safely) and share it. + // Everything else in a generation run is instance-local: each call gets its own + // VelocityEngine, model wrappers and output dictionary, and the model reaches its override + // lists through the object graph rather than any process-global state, so concurrent + // generations do not interfere and no lock is needed. + private static readonly Lazy> s_templates = + new Lazy>(LoadEmbeddedTemplates); /// /// Runs the generator over the given model inputs and returns the generated sources, @@ -36,29 +37,24 @@ internal static class LcmModelRunner public static IReadOnlyDictionary Generate( string masterModelXml, string handGeneratedXml, string intPropTypeOverridesXml) { - lock (s_gate) - { - var templates = s_templates ??= LoadEmbeddedTemplates(); - - var doc = new XmlDocument(); - doc.LoadXml(masterModelXml); + var doc = new XmlDocument(); + doc.LoadXml(masterModelXml); - var handGenerated = ParseHandGenerated(handGeneratedXml); - var intPropOverrides = ParseIntPropTypeOverrides(intPropTypeOverridesXml); + var handGenerated = ParseHandGenerated(handGeneratedXml); + var intPropOverrides = ParseIntPropTypeOverrides(intPropTypeOverridesXml); - var impl = new LcmGenerateImpl(doc, templates) - { - Overrides = handGenerated, - IntPropTypeOverrides = intPropOverrides - }; + var impl = new LcmGenerateImpl(doc, s_templates.Value) + { + Overrides = handGenerated, + IntPropTypeOverrides = intPropOverrides + }; - // main.vm.cs writes the class implementations to this output and, via nested - // SetOutput/Process calls, produces the other eight outputs. - impl.SetOutput("DomainImpl/GeneratedClasses.cs"); - impl.Process("main.vm.cs"); + // main.vm.cs writes the class implementations to this output and, via nested + // SetOutput/Process calls, produces the other eight outputs. + impl.SetOutput("DomainImpl/GeneratedClasses.cs"); + impl.Process("main.vm.cs"); - return impl.Outputs; - } + return impl.Outputs; } /// Loads every embedded "*.vm.cs" template, keyed by its bare file name. diff --git a/src/SIL.LCModel.SourceGenerators/Model/Model.cs b/src/SIL.LCModel.SourceGenerators/Model/Model.cs index 35a938f6f..2b0ca8b94 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/Model.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/Model.cs @@ -21,15 +21,24 @@ internal class Model private StringKeyCollection m_modules; private readonly XmlElement m_node; + /// + /// The generator that owns this model. Nodes reach it through the parent chain + /// (e.g. Property -> Class -> CellarModule -> Model) to read the override lists, + /// which avoids any process-global state. + /// + public LcmGenerateImpl LcmGenerate { get; } + /// ------------------------------------------------------------------------------------ /// /// Initializes a new instance of the class. /// /// The node. + /// The owning generator. /// ------------------------------------------------------------------------------------ - public Model(XmlElement node) + public Model(XmlElement node, LcmGenerateImpl lcmGenerate) { m_node = node; + LcmGenerate = lcmGenerate; } /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/Property.cs b/src/SIL.LCModel.SourceGenerators/Model/Property.cs index 70d22af3d..23a1389ff 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/Property.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/Property.cs @@ -4,8 +4,6 @@ using System; using System.Xml; -using NVelocity; -using NVelocity.Runtime; namespace SIL.LCModel.SourceGenerators { @@ -198,9 +196,8 @@ public bool IsHandGenerated { get { - var context = (VelocityContext) - RuntimeSingleton.GetApplicationAttribute("LcmGenerate.Context"); - var lcmGenerate = (LcmGenerateImpl)context.Get("lcmgenerate"); + // Property -> Class -> CellarModule -> Model, which holds the generator. + var lcmGenerate = Parent.Parent.Parent.LcmGenerate; var className = Parent.Name; return lcmGenerate.Overrides.ContainsKey(className) @@ -219,9 +216,8 @@ public string OverridenType { get { - var context = (VelocityContext) - RuntimeSingleton.GetApplicationAttribute("LcmGenerate.Context"); - var lcmGenerate = (LcmGenerateImpl)context.Get("lcmgenerate"); + // Property -> Class -> CellarModule -> Model, which holds the generator. + var lcmGenerate = Parent.Parent.Parent.LcmGenerate; var className = Parent.Name; if (lcmGenerate.IntPropTypeOverrides.ContainsKey(className)) From f869fa838b4a6f9bfc2b9e61fd290c77b5985310 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 14 Sep 2026 10:27:50 +0700 Subject: [PATCH 3/5] user interlocked to increment property ID static --- src/SIL.LCModel.SourceGenerators/Model/Property.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SIL.LCModel.SourceGenerators/Model/Property.cs b/src/SIL.LCModel.SourceGenerators/Model/Property.cs index 23a1389ff..ff0891cc1 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/Property.cs +++ b/src/SIL.LCModel.SourceGenerators/Model/Property.cs @@ -3,6 +3,7 @@ // (http://www.gnu.org/licenses/lgpl-2.1.html) using System; +using System.Threading; using System.Xml; namespace SIL.LCModel.SourceGenerators @@ -42,7 +43,7 @@ public enum Card public Property(XmlElement node, Class parent) : base(node, parent) { - m_id = s_id++; + m_id = Interlocked.Increment(ref s_id); } /// ------------------------------------------------------------------------------------ From 90066242f9cf47f2441391a343698f89c29b2d1d Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Sat, 19 Sep 2026 11:36:52 +0700 Subject: [PATCH 4/5] Extract shared engine into SIL.LCModel.ModelGeneration; restore LcmGenerate task FieldWorks uses the LcmGenerate MSBuild task to render a C++ header (CellarConstants.vm.h) from the LCM model, so removing it would break the FieldWorks native build on the next SilLcmVersion bump (reported by Jason on the PR). Restore the task, but without going back to the old design. Move the NVelocity engine (LcmGenerateImpl, EmbeddedTemplateLoader, and the model wrappers) out of the source generator into a new netstandard2.0 project, SIL.LCModel.ModelGeneration, whose only dependency is NVelocity. Both the source generator and Build.Tasks reference it by project reference: - SIL.LCModel.SourceGenerators keeps the Roslyn driver, the runner and the embedded templates; it flows the engine + NVelocity into the analyzer load context as analyzer assets. - SIL.LCModel.Build.Tasks gains a thin LcmGenerate task over the same engine (original property surface, so FieldWorks needs no changes) and repacks the engine + NVelocity into tools/, with the UsingTask restored. The engine stays internal, shared via InternalsVisibleTo, rather than source-linking the files into two assemblies. Domain-model output is byte-for- byte identical to the original task (verified against a fresh master build); the LcmGenerate task renders a template end-to-end through MSBuild; the full solution builds and SIL.LCModel.Tests pass (1709 passed, 0 failed, 18 skipped). Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 5 +- CHANGELOG.md | 2 +- LCM.sln | 7 + src/SIL.LCModel.Build.Tasks/LcmGenerate.cs | 123 ++++++++++++++++++ .../SIL.LCModel.Build.Tasks.csproj | 9 +- .../SIL.LCModel.Build.Tasks.props | 1 + .../EmbeddedTemplateLoader.cs | 2 +- .../LcmGenerateImpl.cs | 2 +- .../Model/Base.cs | 2 +- .../Model/CellarModule.cs | 2 +- .../Model/Class.cs | 2 +- .../Model/DummyClass.cs | 2 +- .../Model/IClass.cs | 2 +- .../Model/Model.cs | 2 +- .../Model/Property.cs | 2 +- .../Model/RelationalProperty.cs | 2 +- .../Model/StringKeyCollection.cs | 2 +- .../Model/TypeInfo.cs | 2 +- .../SIL.LCModel.ModelGeneration.csproj | 21 +++ .../LcmModelRunner.cs | 1 + .../SIL.LCModel.SourceGenerators.csproj | 14 +- 21 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 src/SIL.LCModel.Build.Tasks/LcmGenerate.cs rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/EmbeddedTemplateLoader.cs (98%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/LcmGenerateImpl.cs (99%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/Base.cs (98%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/CellarModule.cs (98%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/Class.cs (99%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/DummyClass.cs (99%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/IClass.cs (99%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/Model.cs (98%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/Property.cs (99%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/RelationalProperty.cs (99%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/StringKeyCollection.cs (95%) rename src/{SIL.LCModel.SourceGenerators => SIL.LCModel.ModelGeneration}/Model/TypeInfo.cs (98%) create mode 100644 src/SIL.LCModel.ModelGeneration/SIL.LCModel.ModelGeneration.csproj diff --git a/AGENTS.md b/AGENTS.md index 51a49ae7c..de1a24c35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,8 +65,9 @@ No command timeouts were observed. - src/SIL.LCModel: main LCM library (net462; netstandard2.0). - src/SIL.LCModel.Core: core utilities and ICU data generation (netstandard2.0; net462; net8.0). - src/SIL.LCModel.Utils: shared utilities (net462; netstandard2.0). -- src/SIL.LCModel.Build.Tasks: the IdlImp MSBuild task (kernel-interface code generation). -- src/SIL.LCModel.SourceGenerators: Roslyn source generator for the LCM domain model (MasterLCModel.xml → Generated*.cs), built on the NVelocity templates in its Templates/vm folder. +- src/SIL.LCModel.Build.Tasks: the IdlImp (kernel-interface codegen) and LcmGenerate (single-template rendering, e.g. FieldWorks C++ headers) MSBuild tasks. +- src/SIL.LCModel.ModelGeneration: the shared NVelocity engine (LcmGenerateImpl + model wrappers) that renders LCM model templates; referenced by both SourceGenerators and Build.Tasks. NVelocity-only dependency. +- src/SIL.LCModel.SourceGenerators: Roslyn source generator for the LCM domain model (MasterLCModel.xml → Generated*.cs), with the NVelocity templates embedded from its Templates/vm folder. - src/SIL.LCModel.FixData: data-fix utilities. - src/CSTools: auxiliary tools (pg/lg/Tools). diff --git a/CHANGELOG.md b/CHANGELOG.md index d79b889cf..5a172e5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed -- [SIL.LCModel] Replaced the `LcmGenerate` MSBuild task (which ran the NVelocity templates in a child MSBuild process) with the `SIL.LCModel.SourceGenerators` Roslyn source generator. The domain-model `Generated*.cs` sources are now produced at compile time from `MasterLCModel.xml`; the templates and model wrappers are unchanged and the generated output is byte-for-byte identical. No public API change. +- [SIL.LCModel] The domain-model `Generated*.cs` sources are now produced at compile time by the new `SIL.LCModel.SourceGenerators` Roslyn source generator, instead of the `LcmGenerate` MSBuild task running the NVelocity templates in a child MSBuild process. The NVelocity engine and model wrappers moved to a shared `SIL.LCModel.ModelGeneration` project used by both the source generator and the (retained) `LcmGenerate` task, which still renders other templates from the model (e.g. the FieldWorks C++ cellar-constants header). Templates and generated output are byte-for-byte identical. No public API change. - [SIL.LCModel] Replaced the internal StructureMap (`structuremap.patched`) IoC container with `Microsoft.Extensions.DependencyInjection` (8.x). No public API change. - [SIL.LCModel] Trim 12 overwordy semantic domain descriptions and fix 22 punctuation/whitespace issues in the SemDom.xml template, matching sillsdev/FwLocalizations#5 and sillsdev/FwLocalizations#7 - [SIL.LCModel] `FileUtils.IsFileUriOrPath` checks for the presence of "file:" rather than the absence of known non-file URI schemes diff --git a/LCM.sln b/LCM.sln index 251be820f..d1cf78966 100644 --- a/LCM.sln +++ b/LCM.sln @@ -55,6 +55,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestHelper", "tests\TestHel EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SIL.LCModel.SourceGenerators", "src\SIL.LCModel.SourceGenerators\SIL.LCModel.SourceGenerators.csproj", "{688D1F13-8FAC-4FED-A908-1274B576D81B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SIL.LCModel.ModelGeneration", "src\SIL.LCModel.ModelGeneration\SIL.LCModel.ModelGeneration.csproj", "{5F120076-4431-480D-84F9-EFB60DE4667B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -117,6 +119,10 @@ Global {688D1F13-8FAC-4FED-A908-1274B576D81B}.Debug|Any CPU.Build.0 = Debug|Any CPU {688D1F13-8FAC-4FED-A908-1274B576D81B}.Release|Any CPU.ActiveCfg = Release|Any CPU {688D1F13-8FAC-4FED-A908-1274B576D81B}.Release|Any CPU.Build.0 = Release|Any CPU + {5F120076-4431-480D-84F9-EFB60DE4667B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F120076-4431-480D-84F9-EFB60DE4667B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F120076-4431-480D-84F9-EFB60DE4667B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F120076-4431-480D-84F9-EFB60DE4667B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -137,6 +143,7 @@ Global {8E5F8AF6-6ED0-4205-826C-6BD80D04BDD1} = {040CD95F-B637-493B-9B1C-2989DF3A0D91} {99270559-03D8-49F7-B61B-22E9DBC5D42B} = {B9B859EC-6E0D-447E-A0CD-ABF16E7278B1} {688D1F13-8FAC-4FED-A908-1274B576D81B} = {040CD95F-B637-493B-9B1C-2989DF3A0D91} + {5F120076-4431-480D-84F9-EFB60DE4667B} = {040CD95F-B637-493B-9B1C-2989DF3A0D91} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C625E82E-381D-4590-A382-0B4EF1EF916E} diff --git a/src/SIL.LCModel.Build.Tasks/LcmGenerate.cs b/src/SIL.LCModel.Build.Tasks/LcmGenerate.cs new file mode 100644 index 000000000..c359d92b2 --- /dev/null +++ b/src/SIL.LCModel.Build.Tasks/LcmGenerate.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2006-2025 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using SIL.LCModel.ModelGeneration; + +namespace SIL.LCModel.Build.Tasks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// Renders a single NVelocity template (e.g. a C++ header such as CellarConstants.vm.h) from + /// the LCM model. The LCM domain-model C# is produced by the SIL.LCModel.SourceGenerators + /// source generator; this task remains for consumers (notably the FieldWorks native build) + /// that render other artifacts from the same model through the shared engine. + /// + /// ---------------------------------------------------------------------------------------- + public class LcmGenerate : Task + { + /// The model XML file (MasterLCModel.xml). + [Required] + public string XmlFile { get; set; } + + /// The base directory for the output. + [Required] + public string OutputDir { get; set; } + + /// The output file, relative to . + [Required] + public string OutputFile { get; set; } + + /// The template file to render. + [Required] + public string TemplateFile { get; set; } + + /// + /// Retained for backwards compatibility with existing call sites; no longer used now that + /// the domain model is produced by the source generator. + /// + public string BackendTemplateFiles { get; set; } + + /// + /// Retained for backwards compatibility with existing call sites. The engine no longer + /// relies on the current directory (see EmbeddedTemplateLoader), so this is ignored. + /// + public string WorkingDirectory { get; set; } + + /// + /// Retained for backwards compatibility with existing call sites; not needed for + /// single-template rendering. + /// + public string HandGeneratedDir { get; set; } + + public override bool Execute() + { + try + { + var baseDir = string.IsNullOrEmpty(WorkingDirectory) + ? Directory.GetCurrentDirectory() + : WorkingDirectory; + + var xmlPath = Path.IsPathRooted(XmlFile) ? XmlFile : Path.Combine(baseDir, XmlFile); + var templatePath = Path.IsPathRooted(TemplateFile) ? TemplateFile : Path.Combine(baseDir, TemplateFile); + var outputDirPath = Path.IsPathRooted(OutputDir) ? OutputDir : Path.Combine(baseDir, OutputDir); + + var doc = new XmlDocument(); + Log.LogMessage(MessageImportance.Low, "Loading XML file {0}.", xmlPath); + doc.Load(xmlPath); + + // The engine resolves templates (including any #parse siblings) by bare file name, + // so key the map on the template's file name and include its directory siblings. + var templateName = Path.GetFileName(templatePath); + var templates = LoadTemplates(templatePath); + + var impl = new LcmGenerateImpl(doc, templates) + { + // Empty override maps: the rendered artifacts (e.g. C++ headers) do not use the + // hand-generated / int-type-override metadata, but set them so the model + // wrappers never dereference a null. + Overrides = new Dictionary>(), + IntPropTypeOverrides = new Dictionary>() + }; + + Log.LogMessage(MessageImportance.Low, "Processing template {0}.", templateName); + impl.SetOutput(OutputFile); + impl.Process(templateName); + + Directory.CreateDirectory(outputDirPath); + File.WriteAllText(Path.Combine(outputDirPath, OutputFile), impl.Outputs[OutputFile]); + return true; + } + catch (Exception e) + { + Log.LogError("LcmGenerate failed: {0}", e.Message); + return false; + } + } + + /// + /// Loads the target template plus any sibling "*.vm.*" templates in the same directory, + /// keyed by bare file name, so #parse references resolve without touching the + /// current directory. + /// + private static IReadOnlyDictionary LoadTemplates(string templatePath) + { + var result = new Dictionary(StringComparer.Ordinal); + var dir = Path.GetDirectoryName(templatePath); + if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) + { + foreach (var file in Directory.GetFiles(dir, "*.vm.*")) + result[Path.GetFileName(file)] = File.ReadAllText(file); + } + // Ensure the target template is present even if its extension is not "*.vm.*". + result[Path.GetFileName(templatePath)] = File.ReadAllText(templatePath); + return result; + } + } +} diff --git a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj index e0455a946..462c139a1 100644 --- a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj +++ b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj @@ -4,7 +4,7 @@ SIL.LCModel.Build.Tasks net462;netstandard2.0 The liblcm library is the core FieldWorks model for linguistic analyses of languages. Tools in this library provide the ability to store and interact with language and culture data, including anthropological, text corpus, and linguistics data. -SIL.LCModel.Build.Tasks provides the IdlImp msbuild task for generating C# kernel interfaces for the FieldWorks model. (The LCM domain model is generated by the SIL.LCModel.SourceGenerators Roslyn source generator.) +SIL.LCModel.Build.Tasks provides the IdlImp and LcmGenerate msbuild tasks for generating C# kernel interfaces and rendering LCM model templates (e.g. FieldWorks C++ headers). The LCM domain model itself is generated by the SIL.LCModel.SourceGenerators Roslyn source generator. tools/$(TargetFramework) true true @@ -17,6 +17,11 @@ SIL.LCModel.Build.Tasks provides the IdlImp msbuild task for generating C# kerne + + + + + @@ -40,6 +45,8 @@ SIL.LCModel.Build.Tasks provides the IdlImp msbuild task for generating C# kerne + + diff --git a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.props b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.props index e1bd39ef7..a417b1424 100644 --- a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.props +++ b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.props @@ -4,5 +4,6 @@ + \ No newline at end of file diff --git a/src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs b/src/SIL.LCModel.ModelGeneration/EmbeddedTemplateLoader.cs similarity index 98% rename from src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs rename to src/SIL.LCModel.ModelGeneration/EmbeddedTemplateLoader.cs index af4341d09..ca6ff695d 100644 --- a/src/SIL.LCModel.SourceGenerators/EmbeddedTemplateLoader.cs +++ b/src/SIL.LCModel.ModelGeneration/EmbeddedTemplateLoader.cs @@ -11,7 +11,7 @@ using NVelocity.Runtime.Resource; using NVelocity.Runtime.Resource.Loader; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// /// An NVelocity that serves templates from an in-memory diff --git a/src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs b/src/SIL.LCModel.ModelGeneration/LcmGenerateImpl.cs similarity index 99% rename from src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs rename to src/SIL.LCModel.ModelGeneration/LcmGenerateImpl.cs index 0c52c616d..ceb0c0616 100644 --- a/src/SIL.LCModel.SourceGenerators/LcmGenerateImpl.cs +++ b/src/SIL.LCModel.ModelGeneration/LcmGenerateImpl.cs @@ -9,7 +9,7 @@ using NVelocity; using NVelocity.App; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/Base.cs b/src/SIL.LCModel.ModelGeneration/Model/Base.cs similarity index 98% rename from src/SIL.LCModel.SourceGenerators/Model/Base.cs rename to src/SIL.LCModel.ModelGeneration/Model/Base.cs index 8941a02fe..28e5c4b09 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/Base.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/Base.cs @@ -5,7 +5,7 @@ using System; using System.Xml; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/CellarModule.cs b/src/SIL.LCModel.ModelGeneration/Model/CellarModule.cs similarity index 98% rename from src/SIL.LCModel.SourceGenerators/Model/CellarModule.cs rename to src/SIL.LCModel.ModelGeneration/Model/CellarModule.cs index d0dbea070..15577d209 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/CellarModule.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/CellarModule.cs @@ -5,7 +5,7 @@ using System; using System.Xml; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/Class.cs b/src/SIL.LCModel.ModelGeneration/Model/Class.cs similarity index 99% rename from src/SIL.LCModel.SourceGenerators/Model/Class.cs rename to src/SIL.LCModel.ModelGeneration/Model/Class.cs index 3aa190013..27da4150d 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/Class.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/Class.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Xml; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/DummyClass.cs b/src/SIL.LCModel.ModelGeneration/Model/DummyClass.cs similarity index 99% rename from src/SIL.LCModel.SourceGenerators/Model/DummyClass.cs rename to src/SIL.LCModel.ModelGeneration/Model/DummyClass.cs index 388e3407f..5bd37db21 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/DummyClass.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/DummyClass.cs @@ -2,7 +2,7 @@ // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/IClass.cs b/src/SIL.LCModel.ModelGeneration/Model/IClass.cs similarity index 99% rename from src/SIL.LCModel.SourceGenerators/Model/IClass.cs rename to src/SIL.LCModel.ModelGeneration/Model/IClass.cs index 22365f1bc..988748f20 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/IClass.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/IClass.cs @@ -2,7 +2,7 @@ // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/Model.cs b/src/SIL.LCModel.ModelGeneration/Model/Model.cs similarity index 98% rename from src/SIL.LCModel.SourceGenerators/Model/Model.cs rename to src/SIL.LCModel.ModelGeneration/Model/Model.cs index 2b0ca8b94..6549599c1 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/Model.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/Model.cs @@ -5,7 +5,7 @@ using System; using System.Xml; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { #region StringKeyCollection diff --git a/src/SIL.LCModel.SourceGenerators/Model/Property.cs b/src/SIL.LCModel.ModelGeneration/Model/Property.cs similarity index 99% rename from src/SIL.LCModel.SourceGenerators/Model/Property.cs rename to src/SIL.LCModel.ModelGeneration/Model/Property.cs index ff0891cc1..834ac78fa 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/Property.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/Property.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Xml; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/RelationalProperty.cs b/src/SIL.LCModel.ModelGeneration/Model/RelationalProperty.cs similarity index 99% rename from src/SIL.LCModel.SourceGenerators/Model/RelationalProperty.cs rename to src/SIL.LCModel.ModelGeneration/Model/RelationalProperty.cs index 86367d483..0c5fbc8e1 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/RelationalProperty.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/RelationalProperty.cs @@ -5,7 +5,7 @@ using System.Text; using System.Xml; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/StringKeyCollection.cs b/src/SIL.LCModel.ModelGeneration/Model/StringKeyCollection.cs similarity index 95% rename from src/SIL.LCModel.SourceGenerators/Model/StringKeyCollection.cs rename to src/SIL.LCModel.ModelGeneration/Model/StringKeyCollection.cs index fcfa6f21c..d5c13498f 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/StringKeyCollection.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/StringKeyCollection.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.SourceGenerators/Model/TypeInfo.cs b/src/SIL.LCModel.ModelGeneration/Model/TypeInfo.cs similarity index 98% rename from src/SIL.LCModel.SourceGenerators/Model/TypeInfo.cs rename to src/SIL.LCModel.ModelGeneration/Model/TypeInfo.cs index 2933d4089..4af324619 100644 --- a/src/SIL.LCModel.SourceGenerators/Model/TypeInfo.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/TypeInfo.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; -namespace SIL.LCModel.SourceGenerators +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.ModelGeneration/SIL.LCModel.ModelGeneration.csproj b/src/SIL.LCModel.ModelGeneration/SIL.LCModel.ModelGeneration.csproj new file mode 100644 index 000000000..27224d0b2 --- /dev/null +++ b/src/SIL.LCModel.ModelGeneration/SIL.LCModel.ModelGeneration.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + SIL.LCModel.ModelGeneration + The NVelocity-based engine that renders LCM model templates. Shared by the SIL.LCModel.SourceGenerators source generator (domain model) and the SIL.LCModel.Build.Tasks LcmGenerate task (FieldWorks C++ headers). + liblcm + false + + + + + + + + + + + + + diff --git a/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs b/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs index 343434f83..9a0beb810 100644 --- a/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs +++ b/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs @@ -7,6 +7,7 @@ using System.IO; using System.Reflection; using System.Xml; +using SIL.LCModel.ModelGeneration; namespace SIL.LCModel.SourceGenerators { diff --git a/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj b/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj index cf36b5ef7..0be67b952 100644 --- a/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj +++ b/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj @@ -9,14 +9,12 @@ true false false - $(NoWarn);NU5128;RS2008 - + + $(GetTargetPathDependsOn);GetDependencyTargetPaths - + - + From 0b659b190833d62570c0a08face94dc6ea3be4ad Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 21 Sep 2026 14:04:57 +0700 Subject: [PATCH 5/5] Keep SIL.LCModel.ModelGeneration out of package dependency lists SIL.LCModel.ModelGeneration is IsPackable=false (its assembly is bundled directly into the analyzer and into SIL.LCModel.Build.Tasks tools/), but the plain ProjectReference from Build.Tasks emitted a on it in the Build.Tasks nuspec. Consumers pull it transitively (SIL.LCModel -> Core -> Build.Tasks -> ModelGeneration), so restore fails with NU1101: package SIL.LCModel.ModelGeneration not found. A full FieldWorks build against a local package feed surfaced this. Mark the ModelGeneration project references PrivateAssets="all" in both Build.Tasks and SourceGenerators so the engine is not advertised as a package dependency; the DLL continues to flow via the explicit tools/ pack and the analyzer's GetDependencyTargetPaths. SIL.LCModel still builds with the generator (ModelGeneration.dll + NVelocity.dll reach the analyzer load context). Co-Authored-By: Claude Opus 4.8 --- src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj | 6 ++++-- .../SIL.LCModel.SourceGenerators.csproj | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj index 462c139a1..643f0d6c3 100644 --- a/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj +++ b/src/SIL.LCModel.Build.Tasks/SIL.LCModel.Build.Tasks.csproj @@ -17,9 +17,11 @@ SIL.LCModel.Build.Tasks provides the IdlImp and LcmGenerate msbuild tasks for ge - + - + diff --git a/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj b/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj index 0be67b952..83817220f 100644 --- a/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj +++ b/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj @@ -14,7 +14,7 @@ - +