diff --git a/AGENTS.md b/AGENTS.md index b32e91b86..de1a24c35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,13 +65,15 @@ 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 (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). -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..5a172e5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed +- [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 91956680d..d1cf78966 100644 --- a/LCM.sln +++ b/LCM.sln @@ -53,6 +53,10 @@ 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 +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 @@ -111,6 +115,14 @@ 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 + {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 @@ -130,6 +142,8 @@ 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} + {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 index 20f9df14d..c359d92b2 100644 --- a/src/SIL.LCModel.Build.Tasks/LcmGenerate.cs +++ b/src/SIL.LCModel.Build.Tasks/LcmGenerate.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) @@ -8,217 +8,116 @@ 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 + public class LcmGenerate : Task { - /// ------------------------------------------------------------------------------------ - /// - /// Gets or sets the XML file. - /// - /// The XML file. - /// ------------------------------------------------------------------------------------ + /// The model XML file (MasterLCModel.xml). [Required] public string XmlFile { get; set; } - /// ------------------------------------------------------------------------------------ - /// - /// Gets or sets the base dir for the output. - /// - /// The output directory. - /// ------------------------------------------------------------------------------------ + /// The base directory for the output. [Required] public string OutputDir { get; set; } - /// ------------------------------------------------------------------------------------ - /// - /// Gets or sets the file for the output. - /// - /// The output file name. - /// ------------------------------------------------------------------------------------ + /// The output file, relative to . [Required] public string OutputFile { get; set; } - /// ------------------------------------------------------------------------------------ - /// - /// Gets or sets the template file. - /// - /// The template file. - /// ------------------------------------------------------------------------------------ + /// The template file to render. [Required] public string TemplateFile { get; set; } - /// ------------------------------------------------------------------------------------ /// - /// Gets or sets the template file. + /// Retained for backwards compatibility with existing call sites; no longer used now that + /// the domain model is produced by the source generator. /// - /// The template file. - /// ------------------------------------------------------------------------------------ public string BackendTemplateFiles { get; set; } /// - /// Gets or sets the working directory. + /// Retained for backwards compatibility with existing call sites. The engine no longer + /// relies on the current directory (see EmbeddedTemplateLoader), so this is ignored. /// - /// The working directory. public string WorkingDirectory { get; set; } /// - /// Gets or sets the directory that contains HandGenerated.xml and IntPropTypeOverrides.xml + /// Retained for backwards compatibility with existing call sites; not needed for + /// single-template rendering. /// 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 baseDir = string.IsNullOrEmpty(WorkingDirectory) + ? Directory.GetCurrentDirectory() + : WorkingDirectory; - var handGeneratedFilesDir = string.IsNullOrEmpty(HandGeneratedDir) - ? Path.Combine(oldDir, "LcmGenerate") - : HandGeneratedDir; + 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 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; - } + 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); - 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) + var impl = new LcmGenerateImpl(doc, templates) { - Log.LogMessage(MessageImportance.High, "Error processing template" + " " + e.Message); - return false; - } + // 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; } - finally + 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)) { - Directory.SetCurrentDirectory(origDir); + foreach (var file in Directory.GetFiles(dir, "*.vm.*")) + result[Path.GetFileName(file)] = File.ReadAllText(file); } - return true; + // 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 f4cab1210..643f0d6c3 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 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 @@ -13,11 +13,17 @@ SIL.LCModel.Build.Tasks provides msbuild tasks for generating C# classes for the - + + + + + @@ -40,8 +46,9 @@ SIL.LCModel.Build.Tasks provides msbuild tasks for generating C# classes for the - + + diff --git a/src/SIL.LCModel.ModelGeneration/EmbeddedTemplateLoader.cs b/src/SIL.LCModel.ModelGeneration/EmbeddedTemplateLoader.cs new file mode 100644 index 000000000..ca6ff695d --- /dev/null +++ b/src/SIL.LCModel.ModelGeneration/EmbeddedTemplateLoader.cs @@ -0,0 +1,59 @@ +// 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.ModelGeneration +{ + /// + /// 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 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 + { + 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.ModelGeneration/LcmGenerateImpl.cs similarity index 63% rename from src/SIL.LCModel.Build.Tasks/LcmGenerateImpl.cs rename to src/SIL.LCModel.ModelGeneration/LcmGenerateImpl.cs index 5766d7344..ceb0c0616 100644 --- a/src/SIL.LCModel.Build.Tasks/LcmGenerateImpl.cs +++ b/src/SIL.LCModel.ModelGeneration/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) @@ -8,21 +8,18 @@ using System.Xml; using NVelocity; using NVelocity.App; -using NVelocity.Runtime; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// - /// 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 { - /// - public static LcmGenerateImpl Generator; - - private readonly string m_OutputDir; private string m_OutputFileName; private readonly VelocityEngine m_Engine; private readonly VelocityContext m_Context; @@ -31,51 +28,48 @@ 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_Model = new Model(entireModel, this); 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); - - RuntimeSingleton.RuntimeServices.SetApplicationAttribute("LcmGenerate.Engine", m_Engine); - RuntimeSingleton.RuntimeServices.SetApplicationAttribute("LcmGenerate.Context", m_Context); } /// ------------------------------------------------------------------------------------ /// /// 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 +82,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 +90,7 @@ public Dictionary> IntPropTypeOverrides } /// ------------------------------------------------------------------------------------ - /// - /// Gets the model. - /// - /// The model. + /// Gets the model. /// ------------------------------------------------------------------------------------ public Model Model { @@ -108,21 +98,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 +118,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 +151,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 +171,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.Build.Tasks/Base.cs b/src/SIL.LCModel.ModelGeneration/Model/Base.cs similarity index 96% rename from src/SIL.LCModel.Build.Tasks/Base.cs rename to src/SIL.LCModel.ModelGeneration/Model/Base.cs index 99e13095c..28e5c4b09 100644 --- a/src/SIL.LCModel.Build.Tasks/Base.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/Base.cs @@ -5,7 +5,7 @@ using System; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// @@ -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.ModelGeneration/Model/CellarModule.cs similarity index 98% rename from src/SIL.LCModel.Build.Tasks/CellarModule.cs rename to src/SIL.LCModel.ModelGeneration/Model/CellarModule.cs index 7a4144ba7..15577d209 100644 --- a/src/SIL.LCModel.Build.Tasks/CellarModule.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/CellarModule.cs @@ -5,7 +5,7 @@ using System; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/Class.cs b/src/SIL.LCModel.ModelGeneration/Model/Class.cs similarity index 99% rename from src/SIL.LCModel.Build.Tasks/Class.cs rename to src/SIL.LCModel.ModelGeneration/Model/Class.cs index 5a335f84f..27da4150d 100644 --- a/src/SIL.LCModel.Build.Tasks/Class.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/Class.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/DummyClass.cs b/src/SIL.LCModel.ModelGeneration/Model/DummyClass.cs similarity index 99% rename from src/SIL.LCModel.Build.Tasks/DummyClass.cs rename to src/SIL.LCModel.ModelGeneration/Model/DummyClass.cs index 9dd2d6677..5bd37db21 100644 --- a/src/SIL.LCModel.Build.Tasks/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.Build.Tasks +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/IClass.cs b/src/SIL.LCModel.ModelGeneration/Model/IClass.cs similarity index 99% rename from src/SIL.LCModel.Build.Tasks/IClass.cs rename to src/SIL.LCModel.ModelGeneration/Model/IClass.cs index c799cbb11..988748f20 100644 --- a/src/SIL.LCModel.Build.Tasks/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.Build.Tasks +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/Model.cs b/src/SIL.LCModel.ModelGeneration/Model/Model.cs similarity index 78% rename from src/SIL.LCModel.Build.Tasks/Model.cs rename to src/SIL.LCModel.ModelGeneration/Model/Model.cs index d7a239f97..6549599c1 100644 --- a/src/SIL.LCModel.Build.Tasks/Model.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/Model.cs @@ -5,7 +5,7 @@ using System; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.ModelGeneration { #region StringKeyCollection @@ -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.Build.Tasks/Property.cs b/src/SIL.LCModel.ModelGeneration/Model/Property.cs similarity index 94% rename from src/SIL.LCModel.Build.Tasks/Property.cs rename to src/SIL.LCModel.ModelGeneration/Model/Property.cs index 63e075477..834ac78fa 100644 --- a/src/SIL.LCModel.Build.Tasks/Property.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/Property.cs @@ -3,11 +3,10 @@ // (http://www.gnu.org/licenses/lgpl-2.1.html) using System; +using System.Threading; using System.Xml; -using NVelocity; -using NVelocity.Runtime; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// @@ -44,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); } /// ------------------------------------------------------------------------------------ @@ -198,9 +197,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 +217,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)) diff --git a/src/SIL.LCModel.Build.Tasks/RelationalProperty.cs b/src/SIL.LCModel.ModelGeneration/Model/RelationalProperty.cs similarity index 99% rename from src/SIL.LCModel.Build.Tasks/RelationalProperty.cs rename to src/SIL.LCModel.ModelGeneration/Model/RelationalProperty.cs index 92ec3531b..0c5fbc8e1 100644 --- a/src/SIL.LCModel.Build.Tasks/RelationalProperty.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/RelationalProperty.cs @@ -5,7 +5,7 @@ using System.Text; using System.Xml; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/StringKeyCollection.cs b/src/SIL.LCModel.ModelGeneration/Model/StringKeyCollection.cs similarity index 95% rename from src/SIL.LCModel.Build.Tasks/StringKeyCollection.cs rename to src/SIL.LCModel.ModelGeneration/Model/StringKeyCollection.cs index 0d81aceed..d5c13498f 100644 --- a/src/SIL.LCModel.Build.Tasks/StringKeyCollection.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/StringKeyCollection.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; -namespace SIL.LCModel.Build.Tasks +namespace SIL.LCModel.ModelGeneration { /// ---------------------------------------------------------------------------------------- /// diff --git a/src/SIL.LCModel.Build.Tasks/TypeInfo.cs b/src/SIL.LCModel.ModelGeneration/Model/TypeInfo.cs similarity index 98% rename from src/SIL.LCModel.Build.Tasks/TypeInfo.cs rename to src/SIL.LCModel.ModelGeneration/Model/TypeInfo.cs index 1c7c8a80a..4af324619 100644 --- a/src/SIL.LCModel.Build.Tasks/TypeInfo.cs +++ b/src/SIL.LCModel.ModelGeneration/Model/TypeInfo.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; -namespace SIL.LCModel.Build.Tasks +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/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..9a0beb810 --- /dev/null +++ b/src/SIL.LCModel.SourceGenerators/LcmModelRunner.cs @@ -0,0 +1,113 @@ +// 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; +using SIL.LCModel.ModelGeneration; + +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 + { + // 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, + /// 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) + { + var doc = new XmlDocument(); + doc.LoadXml(masterModelXml); + + var handGenerated = ParseHandGenerated(handGeneratedXml); + var intPropOverrides = ParseIntPropTypeOverrides(intPropTypeOverridesXml); + + 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"); + + 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.SourceGenerators/SIL.LCModel.SourceGenerators.csproj b/src/SIL.LCModel.SourceGenerators/SIL.LCModel.SourceGenerators.csproj new file mode 100644 index 000000000..83817220f --- /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 - - - - - - - - - - - - - - - - - - - - - - - -