/**************************************************************************** ** ** Copyright (C) 2020 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt VS Tools. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using EnvDTE; using Microsoft.Internal.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.TemplateWizard; using QtVsTools.Common; using QtVsTools.VisualStudio; using QtVsTools.Core; using QtVsTools.Core.QtMsBuild; namespace QtVsTools.Wizards.ProjectWizard { using static EnumExt; using WhereConfig = Func; public interface IWizardConfiguration { string Name { get; } VersionInformation QtVersion { get; } string QtVersionName { get; } string QtVersionPath { get; } string Target { get; } string Platform { get; } bool IsDebug { get; } IEnumerable Modules { get; } } public enum ProjectTargets { Windows, [String("Windows Store")] WindowsStore, [String("Linux (SSH)")] LinuxSSH, [String("Linux (WSL)")] LinuxWSL } public enum ProjectPlatforms { [String("x64")] X64, Win32, ARM64, ARM, } public abstract class ProjectTemplateWizard : IWizard { protected readonly WhereConfig WhereConfig_SelectAll = (x => true); protected struct ItemProperty { public string Key { get; } public string Value { get; } public WhereConfig WhereConfig { get; } public ItemProperty(string key, string value, WhereConfig whereConfig = null) { Key = key; Value = value; WhereConfig = whereConfig; } public static implicit operator ItemProperty[](ItemProperty that) { return new[] { that }; } } protected class ItemGlobalDef { public string ItemType { get; set; } public ItemProperty[] Properties { get; set; } } protected class ItemDef { public string ItemType { get; set; } public string Include { get; set; } public ItemProperty[] Properties { get; set; } public string Filter { get; set; } public WhereConfig WhereConfig { get; set; } } // keep in sync with QtVsTools.Core.TemplateType [Flags] protected enum Options : uint { Application = 0x000, DynamicLibrary = 0x001, StaticLibrary = 0x002, GUISystem = 0x004, ConsoleSystem = 0x008, PluginProject = 0x100 } protected abstract Options TemplateType { get; } protected abstract WizardData WizardData { get; } protected abstract WizardWindow WizardWindow { get; } protected virtual IDictionary ItemGlobals => null; protected virtual IEnumerable ExtraItems => Enumerable.Empty(); protected virtual IEnumerable ExtraModules => Enumerable.Empty(); protected virtual IEnumerable ExtraDefines => Enumerable.Empty(); protected virtual IEnumerable Configurations => WizardData.Configs; protected virtual bool UsePrecompiledHeaders => WizardData.UsePrecompiledHeader; protected Dictionary ParameterValues { get; private set; } protected EnvDTE.DTE Dte { get; private set; } ItemDef _PrecompiledHeaderFile; protected virtual ItemDef PrecompiledHeader => _PrecompiledHeaderFile ?? (_PrecompiledHeaderFile = new ItemDef { ItemType = "ClInclude", Include = "stdafx.h", Filter = "Header Files" }); ItemDef _PrecompiledHeaderSourceFile; protected virtual ItemDef PrecompiledHeaderSource => _PrecompiledHeaderSourceFile ?? (_PrecompiledHeaderSourceFile = new ItemDef { ItemType = "ClCompile", Include = "stdafx.cpp", Properties = new ItemProperty("PrecompiledHeader", "Create"), Filter = "Source Files", }); protected class TemplateParameters { public ProjectTemplateWizard Template { get; set; } string ParamKey(Enum param) { return string.Format("${0}$", param.Cast()); } public string this[Enum param] { get => Template.ParameterValues[ParamKey(param)]; set => Template.ParameterValues[ParamKey(param)] = value; } } protected enum NewProject { // Read-only parameters [String("projectname")] Name, [String("safeprojectname")] SafeName, [String("destinationdirectory")] DestinationDirectory, [String("solutiondirectory")] SolutionDirectory, // Custom parameters ToolsVersion, ProjectConfigurations, Properties, ProjectGuid, Keyword, Globals, Configurations, PropertySheets, QtSettings, BuildSettings, ProjectItems, FilterItems, } TemplateParameters _Parameter; protected TemplateParameters Parameter => _Parameter ?? (_Parameter = new TemplateParameters { Template = this }); protected QtVersionManager VersionManager => QtVersionManager.The(); public virtual void ProjectItemFinishedGenerating(ProjectItem projectItem) { } public virtual void BeforeOpeningFile(ProjectItem projectItem) { } public virtual void RunFinished() { } public virtual bool ShouldAddProjectItem(string filePath) => true; protected virtual void BeforeWizardRun() { } protected virtual void BeforeTemplateExpansion() { } protected virtual void OnProjectGenerated(Project project) { } public virtual void RunStarted( object automationObject, Dictionary parameterValues, WizardRunKind runKind, object[] customParams) { Dte = automationObject as DTE; ParameterValues = parameterValues; Debug.Assert(WizardWindow != null); BeforeWizardRun(); var iVsUIShell = VsServiceProvider.GetService(); if (iVsUIShell == null) throw new NullReferenceException("IVsUIShell"); try { IntPtr hwnd; iVsUIShell.EnableModeless(0); iVsUIShell.GetDialogOwnerHwnd(out hwnd); WindowHelper.ShowModal(WizardWindow, hwnd); } catch (QtVSException exception) { Messages.DisplayErrorMessage(exception.Message); throw; } finally { iVsUIShell.EnableModeless(1); } if (!WizardWindow.DialogResult ?? false) { try { Directory.Delete(Parameter[NewProject.DestinationDirectory]); Directory.Delete(Parameter[NewProject.SolutionDirectory]); } catch { } throw new WizardBackoutException(); } BeforeTemplateExpansion(); Expand(); } public virtual void ProjectFinishedGenerating(Project project) { OnProjectGenerated(project); } protected static bool IsLinux(IWizardConfiguration wizConfig) { return wizConfig.Target.EqualTo(ProjectTargets.LinuxSSH) || wizConfig.Target.EqualTo(ProjectTargets.LinuxWSL); } protected static string GetLinuxCompilerPath(IWizardConfiguration wizConfig) { if (!IsLinux(wizConfig)) return string.Empty; if (string.IsNullOrEmpty(wizConfig.QtVersionPath)) return string.Empty; string[] linuxPaths = wizConfig.QtVersionPath.Split(':'); if (linuxPaths == null || linuxPaths.Length <= 2) return string.Empty; return linuxPaths[2]; } protected virtual void Expand() { Debug.Assert(ParameterValues != null); Debug.Assert(Dte != null); Debug.Assert(Configurations != null); Debug.Assert(ExtraItems != null); StringBuilder xml; /////////////////////////////////////////////////////////////////////////////////////// // Tools version = VS version // Parameter[NewProject.ToolsVersion] = Dte.Version; /////////////////////////////////////////////////////////////////////////////////////// // Configurations // xml = new StringBuilder(); foreach (IWizardConfiguration c in Configurations) { xml.AppendLine(string.Format(@" {0} {1} ", /*{0}*/ c.Name, /*{1}*/ c.Platform)); } Parameter[NewProject.ProjectConfigurations] = FormatParam(xml); /////////////////////////////////////////////////////////////////////////////////////// // Properties // xml = new StringBuilder(); foreach (IWizardConfiguration c in Configurations) { xml.AppendLine(string.Format(@" ", /*{0}*/ c.Name, /*{1}*/ c.Platform)); if (IsLinux(c)) { var compilerPath = GetLinuxCompilerPath(c); if (!string.IsNullOrEmpty(compilerPath)) xml.AppendLine(string.Format(@" {0} {0} {0}", /*{0}*/ compilerPath)); } xml.AppendLine(@" "); } Parameter[NewProject.Properties] = FormatParam(xml); /////////////////////////////////////////////////////////////////////////////////////// // Globals // xml = new StringBuilder(); Parameter[NewProject.ProjectGuid] = HelperFunctions.NewProjectGuid(); Parameter[NewProject.Keyword] = Resources.QtVSVersionTag; /////////////////////////////////////////////////////////////////////////////////////// // Globals: Windows // foreach (IWizardConfiguration c in Configurations .Where(c => c.Target.EqualTo(ProjectTargets.Windows))) { if (!string.IsNullOrEmpty(c.QtVersion.VC_WindowsTargetPlatformVersion)) { xml.AppendLine(string.Format(@" {2}", /*{0}*/ c.Name, /*{1}*/ c.Platform, /*{2}*/ c.QtVersion.VC_WindowsTargetPlatformVersion)); } } /////////////////////////////////////////////////////////////////////////////////////// // Globals: Windows Store // foreach (IWizardConfiguration c in Configurations .Where(c => c.Target.EqualTo(ProjectTargets.WindowsStore))) { xml.AppendLine(string.Format(@" Windows Store {2} {3} {4} {5} en true", /*{0}*/ c.Name, /*{1}*/ c.Platform, /*{2}*/ c.QtVersion.VC_WindowsTargetPlatformVersion, /*{3}*/ c.QtVersion.VC_WindowsTargetPlatformMinVersion, /*{4}*/ c.QtVersion.VC_MinimumVisualStudioVersion, /*{5}*/ c.QtVersion.VC_ApplicationTypeRevision)); } /////////////////////////////////////////////////////////////////////////////////////// // Globals: Linux // foreach (IWizardConfiguration c in Configurations.Where(c => IsLinux(c))) { xml.AppendLine(string.Format(@" Linux 1.0 Generic {{D51BCBC9-82E9-4017-911E-C93873C4EA2B}}", /*{0}*/ c.Name, /*{1}*/ c.Platform)); } Parameter[NewProject.Globals] = FormatParam(xml); /////////////////////////////////////////////////////////////////////////////////////// // VC Configurations // xml = new StringBuilder(); foreach (IWizardConfiguration c in Configurations) { ProjectTargets target; if (!c.Target.TryCast(out target)) continue; xml.AppendLine(string.Format(@" ", /*{0}*/ c.Name, /*{1}*/ c.Platform)); if (TemplateType.HasFlag(Options.DynamicLibrary)) { xml.AppendLine(@" DynamicLibrary"); } else if (TemplateType.HasFlag(Options.StaticLibrary)) { xml.AppendLine(@" StaticLibrary"); } else { xml.AppendLine(@" Application"); } switch (target) { case ProjectTargets.Windows: case ProjectTargets.WindowsStore: xml.AppendLine(string.Format(@" v{0}", /*{0}*/ BuildConfig.PlatformToolset)); break; case ProjectTargets.LinuxSSH: xml.AppendLine(@" Remote_GCC_1_0"); break; case ProjectTargets.LinuxWSL: xml.AppendLine(@" WSL_1_0"); break; } if (IsLinux(c)) { xml.AppendLine(string.Format(@" {0}", /*{0}*/ c.IsDebug ? "true" : "false")); } else if (target == ProjectTargets.WindowsStore) { xml.AppendLine(@" false false"); } xml.AppendLine(@" "); } Parameter[NewProject.Configurations] = FormatParam(xml); /////////////////////////////////////////////////////////////////////////////////////// // Property sheets // xml = new StringBuilder(); foreach (IWizardConfiguration c in Configurations) { xml.AppendLine(string.Format(@" ", /*{0}*/ c.Name, /*{1}*/ c.Platform)); } Parameter[NewProject.PropertySheets] = FormatParam(xml); /////////////////////////////////////////////////////////////////////////////////////// // Qt settings // xml = new StringBuilder(); foreach (IWizardConfiguration c in Configurations) { xml.AppendLine(string.Format(@" {2} {3} {4}", /*{0}*/ c.Name, /*{1}*/ c.Platform, /*{2}*/ c.QtVersionName, /*{3}*/ string.Join(";", c.Modules.Union(ExtraModules)), /*{4}*/ c.IsDebug ? "debug" : "release")); if (c.Target.EqualTo(ProjectTargets.WindowsStore)) { xml.AppendLine(@" true true true"); } xml.AppendLine(@" "); } Parameter[NewProject.QtSettings] = FormatParam(xml); /////////////////////////////////////////////////////////////////////////////////////// // Build settings // IEnumerable mocProperties = ItemGlobals?[QtMoc.ItemTypeName]?.Properties ?? Enumerable.Empty(); IEnumerable clProperties = ItemGlobals?["ClCompile"]?.Properties ?? Enumerable.Empty(); IEnumerable linkProperties = ItemGlobals?["Link"]?.Properties ?? Enumerable.Empty(); xml = new StringBuilder(); foreach (IWizardConfiguration c in Configurations) { xml.AppendLine(string.Format(@" ", /*{0}*/ c.Name, /*{1}*/ c.Platform)); /////////////////////////////////////////////////////////////////////////////////// // Build settings: C++ compiler // if (!IsLinux(c)) { // Windows xml.AppendLine(@" true true"); if (c.IsDebug) { xml.AppendLine(@" ProgramDatabase Disabled MultiThreadedDebugDLL"); } else { xml.AppendLine(@" None MaxSpeed MultiThreadedDLL"); } if (c.Target.EqualTo(ProjectTargets.WindowsStore)) { xml.AppendLine(@" false NotUsing true"); } if (UsePrecompiledHeaders) { xml.AppendLine(string.Format(@" Use {0}", /*{0}*/ PrecompiledHeader.Include)); } if (ExtraDefines?.Any() == true) { xml.AppendLine(string.Format(@" {0};%(PreprocessorDefinitions)", /*{0}*/ string.Join(";", ExtraDefines))); } foreach (ItemProperty p in clProperties) { xml.AppendLine(string.Format(@" <{0}>{1}", /*{0}*/ p.Key, /*{1}*/ p.Value)); } xml.AppendLine(@" "); } else { // Linux xml.AppendLine(@" true "); } /////////////////////////////////////////////////////////////////////////////////// // Build settings: Linker // if (!IsLinux(c)) { // Windows xml.AppendLine(string.Format(@" {0} {1}", /*{0}*/ TemplateType.HasFlag(Options.ConsoleSystem) ? "Console" : "Windows", /*{1}*/ c.IsDebug ? "true" : "false")); if (c.Target.EqualTo(ProjectTargets.WindowsStore)) { xml.AppendLine(string.Format(@" /APPCONTAINER %(AdditionalOptions) false false {0}", /*{0}*/ c.QtVersion.VC_Link_TargetMachine)); } foreach (ItemProperty p in linkProperties) { xml.AppendLine(string.Format(@" <{0}>{1}", /*{0}*/ p.Key, /*{1}*/ p.Value)); } xml.AppendLine(@" "); } /////////////////////////////////////////////////////////////////////////////////// // Build settings: moc // if (UsePrecompiledHeaders || mocProperties.Any()) { xml.AppendLine(string.Format(@" <{0}>", QtMoc.ItemTypeName)); foreach (ItemProperty p in mocProperties) { xml.AppendLine(string.Format(@" <{0}>{1}", /*{0}*/ p.Key, /*{1}*/ p.Value)); } if (UsePrecompiledHeaders) { xml.AppendLine(string.Format(@" <{0}>{1};%({0})", /*{0}*/ QtMoc.Property.PrependInclude, /*{1}*/ PrecompiledHeader.Include)); } xml.AppendLine(string.Format(@" ", QtMoc.ItemTypeName)); } /////////////////////////////////////////////////////////////////////////////////// // Build settings: remaining item types // if (ItemGlobals != null) { foreach (string itemType in ItemGlobals.Keys .Except(new[] { "ClCompile", "Link", QtMoc.ItemTypeName })) { xml.AppendLine(string.Format(@" <{0}>", /*{0}*/ itemType)); foreach (ItemProperty p in ItemGlobals[itemType].Properties) { xml.AppendLine(string.Format(@" <{0}>{1}", /*{0}*/ p.Key, /*{1}*/ p.Value)); } xml.AppendLine(string.Format(@" ", /*{0}*/ itemType)); } } xml.AppendLine(@" "); } Parameter[NewProject.BuildSettings] = FormatParam(xml); /////////////////////////////////////////////////////////////////////////////////////// // Project items // IEnumerable projectItems = ExtraItems .Where((ItemDef item) => item.WhereConfig == null || Configurations.Where(item.WhereConfig).Any()) .Union(UsePrecompiledHeaders ? new[] { PrecompiledHeader, PrecompiledHeaderSource } : Enumerable.Empty()); xml = new StringBuilder(); foreach (ItemDef item in projectItems) { bool itemHasProperties = (item.WhereConfig != null || item.Properties != null); xml.Append(string.Format(@" <{0} Include=""{1}""{2}", /*{0}*/ item.ItemType, /*{1}*/ item.Include, /*{2}*/ itemHasProperties ? ">" : " />")); if (item.Properties != null) { foreach (ItemProperty property in item.Properties) { IEnumerable configs = Configurations .Where(property.WhereConfig ?? WhereConfig_SelectAll); foreach (IWizardConfiguration config in configs) { xml.AppendLine(string.Format(@" <{0} Condition=""'$(Configuration)|$(Platform)' == '{1}|{2}'"">{3}", /*{0}*/ property.Key, /*{1}*/ config.Name, /*{2}*/ config.Platform, /*{3}*/ property.Value)); } } } if (item.WhereConfig != null) { IEnumerable excludedConfigs = Configurations .Where(config => !item.WhereConfig(config)); foreach (var excludedConfig in excludedConfigs) { xml.AppendLine(string.Format(@" true", /*{0}*/ excludedConfig.Name, /*{1}*/ excludedConfig.Platform)); } } if (itemHasProperties) { xml.AppendLine(string.Format(@" ", /*{0}*/ item.ItemType)); } } Parameter[NewProject.ProjectItems] = FormatParam(xml); /////////////////////////////////////////////////////////////////////////////////////// // Project items: filters // xml = new StringBuilder(); foreach (ItemDef item in projectItems) { xml.Append(string.Format(@" <{0} Include=""{1}"">", /*{0}*/ item.ItemType, /*{1}*/ item.Include)); xml.AppendLine(string.Format(@" {0}", /*{0}*/ item.Filter)); xml.AppendLine(string.Format(@" ", /*{0}*/ item.ItemType)); } Parameter[NewProject.FilterItems] = FormatParam(xml); } // Matches empty lines; captures first newline static readonly Regex patternEmptyLines = new Regex(@"(?:^|(?\r\n))(?:\r\n)+(?![\r\n]|$)|(?:\r\n)+$"); protected static string FormatParam(StringBuilder paramValue) { return FormatParam(paramValue.ToString()); } protected static string FormatParam(string paramValue) { // Remove empty lines; replace with first newline (if any) paramValue = patternEmptyLines.Replace(paramValue, (Match m) => m.Groups["FIRST_NL"].Value); return paramValue; } } }