// Copyright (c) 2010-2014 SharpDX - Alexandre Mutel // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Xml.Serialization; using SharpGen.Logging; using System.Reflection; namespace SharpGen.Config { /// /// Config File. /// [XmlRoot("config", Namespace=XmlNamespace)] public partial class ConfigFile { internal const string XmlNamespace = "urn:SharpGen.Config"; /// /// Gets dynamic variables used by dynamic variable substitution #(MyVariable) /// /// The dynamic variables. [XmlIgnore] public Dictionary DynamicVariables { get; private set; } = new Dictionary(); /// /// Gets or sets the parent of this mapping file. /// /// The parent. [XmlIgnore] public ConfigFile Parent { get; set; } /// /// Gets or sets the path of this MappingFile. If not null, used when saving this file. /// /// The path. [XmlIgnore] public string FilePath { get; set; } [XmlIgnore] public string AbsoluteFilePath { get { if (FilePath == null) return null; if (Path.IsPathRooted(FilePath)) return FilePath; if (Parent?.AbsoluteFilePath != null) return Path.Combine(Path.GetDirectoryName(Parent.AbsoluteFilePath), FilePath); return Path.GetFullPath(Path.Combine(".", FilePath)); } } [XmlAttribute("id")] public string Id { get => id ??= Guid.NewGuid().ToString(); set => id = value; } [XmlElement("depends")] public List Depends { get; set; } = new List(); [XmlElement("namespace")] public string Namespace { get; set; } [XmlElement("assembly")] public string Assembly { // The assembly attribute is no longer supported, but we leave the option // so that config files don't break when the option is specified. get => string.Empty; set { } } [XmlElement("var")] public List Variables { get; set; } = new List(); [XmlElement("file")] public List Files { get; set; } = new List(); [XmlIgnore] public List References { get; set; } = new List(); [XmlElement("sdk")] public List Sdks { get; set; } = new List(); [XmlElement("include-dir")] public List IncludeDirs { get; set; } = new List(); [XmlElement("include-prolog")] public List IncludeProlog { get; set; } = new List(); [XmlElement("include")] public List Includes { get; set; } = new List(); [XmlArray("naming"),XmlArrayItem(typeof(NamingRuleShort))] public List Naming { get; set; } = new List(); [XmlElement("context-set")] public List ContextSets { get; set; } [XmlArray("extension")] [XmlArrayItem(typeof(ContextRule))] [XmlArrayItem(typeof(ClearContextRule))] [XmlArrayItem(typeof(CreateExtensionRule))] [XmlArrayItem(typeof(CreateCppExtensionRule))] [XmlArrayItem(typeof(DefineExtensionRule))] [XmlArrayItem(typeof(ConstantRule))] public List Extension { get; set; } = new List(); [XmlIgnore] public string ExtensionId => Id + "-ext"; /// /// Gets the name of the extension header file. /// [XmlIgnore] public string ExtensionFileName => ExtensionId + ".h"; /// /// Gets the name of this configs' primary header file. /// [XmlIgnore] public string HeaderFileName => Id + ".h"; [XmlArray("bindings")] public List Bindings { get; set; } = new List(); [XmlArray("mapping")] [XmlArrayItem(typeof(MappingRule))] [XmlArrayItem(typeof(MoveRule))] [XmlArrayItem(typeof(RemoveRule))] [XmlArrayItem(typeof(ContextRule))] [XmlArrayItem(typeof(ClearContextRule))] public List Mappings { get; set; } = new List(); /// /// Finds all dependencies ConfigFile from this instance. /// /// The dependencies set to fill. public void FindAllDependencies(ISet dependencyListOutput) { ConfigFile ConfigSelector(string dependConfigFileId) => GetRoot().mapIdToFile[dependConfigFileId]; foreach (var linkedConfig in Depends.Select(ConfigSelector)) { dependencyListOutput.Add(linkedConfig); linkedConfig.FindAllDependencies(dependencyListOutput); } } /// /// Finds a context set by Id. /// /// The context set id. /// public ContextSetRule FindContextSetById(string contextSetId) { return ContextSets?.FirstOrDefault(contextSetRule => contextSetRule.Id == contextSetId); } /// /// Expands all dynamic variables used inside Bindings and Mappings tags. /// /// if set to true expand dynamic variables. public void ExpandVariables(bool expandDynamicVariable, Logger logger) { ExpandVariables(Variables, expandDynamicVariable, logger); ExpandVariables(Includes, expandDynamicVariable, logger); ExpandVariables(IncludeDirs, expandDynamicVariable, logger); ExpandVariables(Bindings, expandDynamicVariable, logger); ExpandVariables(Mappings, expandDynamicVariable, logger); // Do it recursively foreach (var configFile in References) configFile.ExpandVariables(expandDynamicVariable, logger); } /// /// Iterate on all objects and sub-objects to expand dynamic variable /// /// The object to expand. /// the expanded object private object ExpandVariables(object objectToExpand, bool expandDynamicVariable, Logger logger) { if (objectToExpand == null) return null; if (objectToExpand is string str) return ExpandString(str, expandDynamicVariable, logger); if (objectToExpand.GetType().GetTypeInfo().IsPrimitive) return objectToExpand; if (objectToExpand is IList list) { for (int i = 0; i < list.Count; i++) list[i] = ExpandVariables(list[i], expandDynamicVariable, logger); return list; } foreach (var propertyInfo in objectToExpand.GetType().GetRuntimeProperties()) { if (!propertyInfo.GetCustomAttributes(false).Any()) { // Check that this field is "ShouldSerializable" var method = objectToExpand.GetType().GetRuntimeMethod("ShouldSerialize" + propertyInfo.Name, Type.EmptyTypes); if (method != null && !((bool)method.Invoke(objectToExpand, null))) continue; propertyInfo.SetValue(objectToExpand, ExpandVariables(propertyInfo.GetValue(objectToExpand, null), expandDynamicVariable, logger), null); } } return objectToExpand; } /// /// Gets a variable value. Value is expanded if it contains any reference to other variables. /// /// Name of the variable. /// the value of this variable private string GetVariable(string variableName, Logger logger) { foreach (var keyValue in Variables) { if (keyValue.Name == variableName) return ExpandString(keyValue.Value, false, logger); } return Parent?.GetVariable(variableName, logger); } /// /// Regex used to expand variable /// static readonly Regex ReplaceVariableRegex = new Regex(@"\$\(([a-zA-Z_][\w_]*)\)", RegexOptions.Compiled); static readonly Regex ReplaceDynamicVariableRegex = new Regex(@"#\(([a-zA-Z_][\w_]*)\)", RegexOptions.Compiled); /// /// Expands a string using environment variable and variables defined in mapping files. /// /// The string to expand. /// the expanded string public string ExpandString(string str, bool expandDynamicVariable, Logger logger) { var result = str; // Perform Config Variable substitution if (ReplaceVariableRegex.Match(result).Success) { result = ReplaceVariableRegex.Replace( result, match => { string name = match.Groups[1].Value; string localResult = GetVariable(name, logger); if (localResult == null) localResult = Environment.GetEnvironmentVariable(name); if (localResult == null) { logger.Error(LoggingCodes.UnkownVariable, "Unable to substitute config/environment variable $({0}). Variable is not defined", name); return ""; } return localResult; }); } // Perform Dynamic Variable substitution if (expandDynamicVariable && ReplaceDynamicVariableRegex.Match(result).Success) { result = ReplaceDynamicVariableRegex.Replace( result, match => { string name = match.Groups[1].Value; string localResult; if (!GetRoot().DynamicVariables.TryGetValue(name, out localResult)) { logger.Error(LoggingCodes.UnkownDynamicVariable, "Unable to substitute dynamic variable #({0}). Variable is not defined", name); return ""; } localResult = localResult.Trim('"'); return localResult; }); } return result; } private void PostLoad(ConfigFile parent, string file, string[] macros, IEnumerable variables, Logger logger) { FilePath = file; Parent = parent; if (AbsoluteFilePath != null) { Variables.Add(new KeyValue("THIS_CONFIG_PATH", Path.GetDirectoryName(AbsoluteFilePath))); } Variables.AddRange(variables); // Load all dependencies foreach (var dependFile in Files) { var dependFilePath = ExpandString(dependFile, false, logger); if (!Path.IsPathRooted(dependFilePath) && AbsoluteFilePath != null) dependFilePath = Path.Combine(Path.GetDirectoryName(AbsoluteFilePath), dependFilePath); var subMapping = Load(this, dependFilePath, macros, variables, logger); if (subMapping != null) { subMapping.FilePath = dependFile; References.Add(subMapping); } } // Clear all depends file Files.Clear(); // Add this mapping file GetRoot().mapIdToFile.Add(Id, this); } public IReadOnlyCollection ConfigFilesLoaded => GetRoot().mapIdToFile.Values; /// /// Loads the specified config file attached to a parent config file. /// /// The parent. /// The file. /// The loaded config private static ConfigFile Load(ConfigFile parent, string file, string[] macros, IEnumerable variables, Logger logger) { if(!File.Exists(file)) { logger.Error(LoggingCodes.ConfigNotFound, "Configuration file {0} not found.", file); return null; } var deserializer = new XmlSerializer(typeof(ConfigFile)); ConfigFile config = null; try { logger.PushLocation(file); config = (ConfigFile)deserializer.Deserialize(new StringReader(Preprocessor.Preprocess(File.ReadAllText(file), macros))); config?.PostLoad(parent, file, macros, variables, logger); } catch (Exception ex) { logger.Error(LoggingCodes.UnableToParseConfig, "Unable to parse file [{0}]", ex, file); } finally { logger.PopLocation(); } return config; } public ConfigFile GetRoot() { var root = this; while (root.Parent != null) root = root.Parent; return root; } private readonly Dictionary mapIdToFile = new Dictionary(); private string id; private void Verify(Logger logger) { Depends.Remove(""); // TODO: verify Depends foreach (var depend in Depends) { if (GetRoot().mapIdToFile.ContainsKey(depend)) continue; logger.Error( LoggingCodes.MissingConfigDependency, "Unable to resolve dependency [{0}] for config file [{1}]", depend, Id ); } foreach (var includeDir in IncludeDirs) { includeDir.Path = ExpandString(includeDir.Path, false, logger); if (includeDir.Path.StartsWith("=") || Directory.Exists(includeDir.Path)) continue; logger.Error( LoggingCodes.IncludeDirectoryNotFound, "Include directory {0} from config file [{1}] not found", includeDir.Path, Id ); } // Verify all dependencies foreach (var mappingFile in References) mappingFile.Verify(logger); } public static ConfigFile Load(ConfigFile root, string[] macros, Logger logger, params KeyValue[] variables) { root.PostLoad(null, null, macros, variables, logger); root.Verify(logger); root.ExpandVariables(false, logger); return root; } /// /// Loads a specified MappingFile. /// /// The MappingFile. /// The MappingFile loaded public static ConfigFile Load(string file, string[] macros, Logger logger, params KeyValue[] variables) { var root = Load(null, file, macros, variables, logger); root.Verify(logger); root.ExpandVariables(false, logger); return root; } public void Write(string file) { using var output = File.Create(file); Write(output); } /// /// Writes this MappingFile to the disk. /// /// The writer. public void Write(Stream writer) { //Create our own namespaces for the output var ns = new XmlSerializerNamespaces(); ns.Add("", XmlNamespace); var serializer = new XmlSerializer(typeof(ConfigFile)); serializer.Serialize(writer, this, ns); } /// /// Returns a that represents this instance. /// /// /// A that represents this instance. /// [ExcludeFromCodeCoverage] public override string ToString() { return string.Format(System.Globalization.CultureInfo.InvariantCulture, "config {0}", Id); } } }