diff --git a/src/chocolatey.tests/infrastructure.app/services/TemplateServiceSpecs.cs b/src/chocolatey.tests/infrastructure.app/services/TemplateServiceSpecs.cs index 7f9f2802be..b9e42f493f 100644 --- a/src/chocolatey.tests/infrastructure.app/services/TemplateServiceSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/services/TemplateServiceSpecs.cs @@ -26,6 +26,7 @@ namespace chocolatey.tests.infrastructure.app.services using chocolatey.infrastructure.app.services; using chocolatey.infrastructure.app.templates; using chocolatey.infrastructure.filesystem; + using chocolatey.infrastructure.services; using Moq; using NUnit.Framework; using Should; @@ -36,12 +37,14 @@ public abstract class TemplateServiceSpecsBase : TinySpec { protected TemplateService service; protected Mock fileSystem = new Mock(); + protected Mock XmlService = new Mock(); public override void Context() { - fileSystem.ResetCalls(); + fileSystem.ResetCalls(); + XmlService.ResetCalls(); - service = new TemplateService(fileSystem.Object); + service = new TemplateService(fileSystem.Object, XmlService.Object); } } diff --git a/src/chocolatey/infrastructure.app/services/TemplateService.cs b/src/chocolatey/infrastructure.app/services/TemplateService.cs index 224b287b30..50aec2fb62 100644 --- a/src/chocolatey/infrastructure.app/services/TemplateService.cs +++ b/src/chocolatey/infrastructure.app/services/TemplateService.cs @@ -23,6 +23,7 @@ namespace chocolatey.infrastructure.app.services using System.Reflection; using System.Text; using configuration; + using infrastructure.services; using logging; using templates; using tokens; @@ -35,6 +36,7 @@ public class TemplateService : ITemplateService private readonly UTF8Encoding utf8WithoutBOM = new UTF8Encoding(false); private readonly IFileSystem _fileSystem; private readonly ILogger _nugetLogger; + private readonly IXmlService _xmlService; private readonly IList _templateBinaryExtensions = new List { ".exe", ".msi", ".msu", ".msp", ".mst", @@ -45,10 +47,12 @@ public class TemplateService : ITemplateService private readonly string _builtInTemplateOverrideName = "default"; private readonly string _builtInTemplateName = "built-in"; + private readonly string _templateParameterCacheFilename = ".parameters"; - public TemplateService(IFileSystem fileSystem) + public TemplateService(IFileSystem fileSystem, IXmlService xmlService) { _fileSystem = fileSystem; + _xmlService = xmlService; } public void generate_noop(ChocolateyConfiguration configuration) @@ -148,6 +152,7 @@ public void generate(ChocolateyConfiguration configuration) configuration.NewCommand.TemplateName = string.IsNullOrWhiteSpace(configuration.NewCommand.TemplateName) ? "default" : configuration.NewCommand.TemplateName; var templatePath = _fileSystem.combine_paths(ApplicationParameters.TemplatesLocation, configuration.NewCommand.TemplateName); + var templateParameterCachePath = _fileSystem.combine_paths(templatePath, _templateParameterCacheFilename); if (!_fileSystem.directory_exists(templatePath)) throw new ApplicationException("Unable to find path to requested template '{0}'. Path should be '{1}'".format_with(configuration.NewCommand.TemplateName, templatePath)); this.Log().Info(configuration.QuietOutput ? logger : ChocolateyLoggers.Important, "Generating package from custom template at '{0}'.".format_with(templatePath)); @@ -174,6 +179,10 @@ public void generate(ChocolateyConfiguration configuration) this.Log().Debug(" Treating template file ('{0}') as binary instead of replacing templated values.".format_with(_fileSystem.get_file_name(file))); _fileSystem.copy_file(file, packageFileLocation, overwriteExisting:true); } + else if (templateParameterCachePath.is_equal_to(file)) + { + this.Log().Debug("{0} is the parameter cache file, ignoring".format_with(file)); + } else { generate_file_from_template(configuration, tokens, _fileSystem.read_file(file), packageFileLocation, Encoding.UTF8); @@ -280,6 +289,7 @@ protected void list_custom_template_info(ChocolateyConfiguration configuration, .combine_paths(ApplicationParameters.TemplatesLocation, configuration.TemplateCommand.Name), "*", SearchOption.AllDirectories))); var isOverridingBuiltIn = configuration.TemplateCommand.Name == _builtInTemplateOverrideName; var isDefault = string.IsNullOrWhiteSpace(configuration.DefaultTemplateName) ? isOverridingBuiltIn : (configuration.DefaultTemplateName == configuration.TemplateCommand.Name); + var templateParams = " {0}".format_with(string.Join("{0} ".format_with(Environment.NewLine), get_template_parameters(configuration, templateInstalledViaPackage))); if (configuration.RegularOutput) { @@ -292,6 +302,8 @@ protected void list_custom_template_info(ChocolateyConfiguration configuration, {5}{6} List of files: {7} +List of Parameters: +{8} ".format_with(configuration.TemplateCommand.Name, pkgVersion, isDefault, @@ -299,7 +311,8 @@ protected void list_custom_template_info(ChocolateyConfiguration configuration, pkgTitle, string.IsNullOrEmpty(pkgSummary) ? "Template not installed as a package" : "Summary: {0}".format_with(pkgSummary), string.IsNullOrEmpty(pkgDescription) ? string.Empty : "{0}Description:{0} {1}".format_with(Environment.NewLine, pkgDescription), - pkgFiles)); + pkgFiles, + templateParams)); } else { @@ -345,5 +358,53 @@ protected void list_built_in_template_info(ChocolateyConfiguration configuration } } } + + protected IEnumerable get_template_parameters(ChocolateyConfiguration configuration, bool templateInstalledViaPackage) + { + // If the template was installed via package, the cache file gets removed on upgrade, so the cache file would be up to date if it exists + if (templateInstalledViaPackage) + { + var templateDirectory = _fileSystem.combine_paths(ApplicationParameters.TemplatesLocation, configuration.TemplateCommand.Name); + var cacheFilePath = _fileSystem.combine_paths(templateDirectory, _templateParameterCacheFilename); + + if (!_fileSystem.file_exists(cacheFilePath)) + { + _xmlService.serialize(get_template_parameters_from_files(configuration).ToList(), cacheFilePath); + } + + return _xmlService.deserialize>(cacheFilePath); + } + // If the template is not installed via a package, always read the parameters directly as the template may have been updated manually + else + { + return get_template_parameters_from_files(configuration).ToList(); + } + } + + protected HashSet get_template_parameters_from_files(ChocolateyConfiguration configuration) + { + var filesList = _fileSystem.get_files(_fileSystem.combine_paths(ApplicationParameters.TemplatesLocation, configuration.TemplateCommand.Name), "*", SearchOption.AllDirectories); + var parametersList = new HashSet(); + + foreach(var filePath in filesList) + { + if (_templateBinaryExtensions.Contains(_fileSystem.get_file_extension(filePath))) + { + this.Log().Debug("{0} is a binary file, not reading parameters".format_with(filePath)); + continue; + } + + if (_fileSystem.get_file_name(filePath) == _templateParameterCacheFilename) + { + this.Log().Debug("{0} is the parameter cache file, not reading parameters".format_with(filePath)); + continue; + } + + var fileContents = _fileSystem.read_file(filePath); + parametersList.UnionWith(TokenReplacer.get_tokens(fileContents, "[[", "]]")); + } + + return parametersList; + } } } diff --git a/src/chocolatey/infrastructure/tokens/TokenReplacer.cs b/src/chocolatey/infrastructure/tokens/TokenReplacer.cs index 14f37fa96f..9bbc3b628f 100644 --- a/src/chocolatey/infrastructure/tokens/TokenReplacer.cs +++ b/src/chocolatey/infrastructure/tokens/TokenReplacer.cs @@ -1,13 +1,13 @@ // Copyright © 2017 - 2021 Chocolatey Software, Inc // Copyright © 2011 - 2017 RealDimensions Software, LLC -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. -// +// // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -61,5 +61,16 @@ private static IDictionary create_dictionary_from_configuration< return propertyDictionary; } + + public static IEnumerable get_tokens(string textWithTokens, string tokenPrefix = "[[", string tokenSuffix = "]]") + { + var regexMatches = Regex.Matches(textWithTokens, "{0}(?\\w+){1}" + .format_with(Regex.Escape(tokenPrefix), Regex.Escape(tokenSuffix)) + ); + foreach (Match regexMatch in regexMatches) + { + yield return regexMatch.Groups["key"].to_string(); + } + } } } \ No newline at end of file