using System; using System.IO; using System.Text; using System.Collections.Generic; using System.Globalization; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.Build.Tasks.Hosting; namespace Microsoft.CodeAnalysis.BuildTask { /// /// This class defines the "Vbc" XMake task, which enables building assemblies from VB /// source files by invoking the VB compiler. This is the new Roslyn XMake task, /// meaning that the code is compiled by using the Roslyn compiler server, rather /// than vbc.exe. The two should be functionally identical, but the compiler server /// should be significantly faster with larger projects and have a smaller memory /// footprint. /// public class Vbc : ManagedCompiler { bool useHostCompilerIfAvailable = false; // The following 1 fields are used, set and re-set in LogEventsFromTextOutput() /// /// This stores the origional lines and error priority together in the order in which they were recieved. /// private Queue vbErrorLines = new Queue(); // Used when parsing vbc output to determine the column number of an error private bool isDoneOutputtingErrorMessage = false; private int numberOfLinesInErrorMessage = 0; #region Properties // Please keep these alphabetized. These are the parameters specific to Vbc. The // ones shared between Vbc and Csc are defined in ManagedCompiler.cs, which is // the base class. public string BaseAddress { set { _store["BaseAddress"] = value; } get { return (string)_store["BaseAddress"]; } } public string DisabledWarnings { set { _store["DisabledWarnings"] = value; } get { return (string)_store["DisabledWarnings"]; } } public string DocumentationFile { set { _store["DocumentationFile"] = value; } get { return (string)_store["DocumentationFile"]; } } public string ErrorReport { set { _store["ErrorReport"] = value; } get { return (string)_store["ErrorReport"]; } } public bool GenerateDocumentation { set { _store["GenerateDocumentation"] = value; } get { return _store.GetOrDefault("GenerateDocumentation", false); } } public ITaskItem[] Imports { set { _store["Imports"] = value; } get { return (ITaskItem[])_store["Imports"]; } } public string LangVersion { set { _store["LangVersion"] = value; } get { return (string)_store["LangVersion"]; } } public string ModuleAssemblyName { set { _store["ModuleAssemblyName"] = value; } get { return (string)_store["ModuleAssemblyName"]; } } public bool NoStandardLib { set { _store["NoStandardLib"] = value; } get { return _store.GetOrDefault("NoStandardLib", false); } } // This is not a documented switch. It prevents the automatic reference to Microsoft.VisualBasic.dll. // The VB team believes the only scenario for this is when you are building that assembly itself. // We have to support the switch here so that we can build the SDE and VB trees, which need to build this assembly. // Although undocumented, it cannot be wrapped with #if BUILDING_DF_LKG because this would prevent dogfood builds // within VS, which must use non-LKG msbuild bits. public bool NoVBRuntimeReference { set { _store["NoVBRuntimeReference"] = value; } get { return _store.GetOrDefault("NoVBRuntimeReference", false); } } public bool NoWarnings { set { _store["NoWarnings"] = value; } get { return _store.GetOrDefault("NoWarnings", false); } } public string OptionCompare { set { _store["OptionCompare"] = value; } get { return (string)_store["OptionCompare"]; } } public bool OptionExplicit { set { _store["OptionExplicit"] = value; } get { return _store.GetOrDefault("OptionExplicit", true); } } public bool OptionStrict { set { _store["OptionStrict"] = value; } get { return _store.GetOrDefault("OptionStrict", false); } } public bool OptionInfer { set { _store["OptionInfer"] = value; } get { return _store.GetOrDefault("OptionInfer", false); } } // Currently only /optionstrict:custom public string OptionStrictType { set { _store["OptionStrictType"] = value; } get { return (string)_store["OptionStrictType"]; } } public bool RemoveIntegerChecks { set { _store["RemoveIntegerChecks"] = value; } get { return _store.GetOrDefault("RemoveIntegerChecks", false); } } public string RootNamespace { set { _store["RootNamespace"] = value; } get { return (string)_store["RootNamespace"]; } } public string SdkPath { set { _store["SdkPath"] = value; } get { return (string)_store["SdkPath"]; } } /// /// Name of the language passed to "/preferreduilang" compiler option. /// /// /// If set to null, "/preferreduilang" option is omitted, and vbc.exe uses its default setting. /// Otherwise, the value is passed to "/preferreduilang" as is. /// public string PreferredUILang { set { _store["PreferredUILang"] = value; } get { return (string)_store["PreferredUILang"]; } } public string VsSessionGuid { set { _store["VsSessionGuid"] = value; } get { return (string)_store["VsSessionGuid"]; } } public bool TargetCompactFramework { set { _store["TargetCompactFramework"] = value; } get { return _store.GetOrDefault("TargetCompactFramework", false); } } public bool UseHostCompilerIfAvailable { set { this.useHostCompilerIfAvailable = value; } get { return this.useHostCompilerIfAvailable; } } public string VBRuntimePath { set { _store["VBRuntimePath"] = value; } get { return (string)_store["VBRuntimePath"]; } } public string Verbosity { set { _store["Verbosity"] = value; } get { return (string)_store["Verbosity"]; } } public string WarningsAsErrors { set { _store["WarningsAsErrors"] = value; } get { return (string)_store["WarningsAsErrors"]; } } public string WarningsNotAsErrors { set { _store["WarningsNotAsErrors"] = value; } get { return (string)_store["WarningsNotAsErrors"]; } } public string VBRuntime { set { _store["VBRuntime"] = value; } get { return (string)_store["VBRuntime"]; } } public string PdbFile { set { _store["PdbFile"] = value; } get { return (string)_store["PdbFile"]; } } #endregion #region Tool Members /// /// Return the name of the tool to execute. /// override protected string ToolName { get { return "vbc2.exe"; } } /// /// Override Execute so that we can moved the PDB file if we need to, /// after the compiler is done. /// public override bool Execute() { if (!base.Execute()) { return false; } MovePdbFileIfNecessary(OutputAssembly.ItemSpec); return !Log.HasLoggedErrors; } /// /// Move the PDB file if the PDB file that was generated by the compiler /// is not at the specified path, or if it is newer than the one there. /// VBC does not have a switch to specify the PDB path, so we are essentially implementing that for it here. /// We need make this possible to avoid colliding with the PDB generated by WinMDExp. /// /// If at some future point VBC.exe offers a /pdbfile switch, this function can be removed. /// internal void MovePdbFileIfNecessary(string outputAssembly) { // Get the name of the output assembly because the pdb will be written beside it and will have the same name if (String.IsNullOrEmpty(PdbFile) || String.IsNullOrEmpty(outputAssembly)) { return; } try { string actualPdb = Path.ChangeExtension(outputAssembly, ".pdb"); // This is the pdb that the compiler generated FileInfo actualPdbInfo = new FileInfo(actualPdb); string desiredLocation = PdbFile; if (!desiredLocation.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase)) { desiredLocation += ".pdb"; } FileInfo desiredPdbInfo = new FileInfo(desiredLocation); // If the compiler generated a pdb.. if (actualPdbInfo.Exists) { // .. and the desired one does not exist or it's older... if (!desiredPdbInfo.Exists || (desiredPdbInfo.Exists && actualPdbInfo.LastWriteTime > desiredPdbInfo.LastWriteTime)) { // Delete the existing one if it's already there, as Move would otherwise fail if (desiredPdbInfo.Exists) { Utilities.DeleteNoThrow(desiredPdbInfo.FullName); } // Move the file to where we actually wanted VBC to put it File.Move(actualPdbInfo.FullName, desiredLocation); } } } catch (Exception e) when (Utilities.IsIoRelatedException(e)) { Log.LogErrorWithCodeFromResources("VBC.RenamePDB", PdbFile, e.Message); } } /// /// Generate the path to the tool /// override protected string GenerateFullPathToTool() { string pathToTool = ToolLocationHelper.GetPathToBuildToolsFile(ToolName, ToolLocationHelper.CurrentToolsVersion); if (null == pathToTool) { pathToTool = ToolLocationHelper.GetPathToDotNetFrameworkFile(ToolName, TargetDotNetFrameworkVersion.VersionLatest); if (null == pathToTool) { Log.LogErrorWithCodeFromResources("General.FrameworksFileNotFound", ToolName, ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.VersionLatest)); } } return pathToTool; } /// /// vbc.exe only takes the BaseAddress in hexadecimal format. But we allow the caller /// of the task to pass in the BaseAddress in either decimal or hexadecimal format. /// Examples of supported hex formats include "0x10000000" or "&H10000000". /// internal string GetBaseAddressInHex() { string originalBaseAddress = this.BaseAddress; if (originalBaseAddress != null) { if (originalBaseAddress.Length > 2) { string twoLetterPrefix = originalBaseAddress.Substring(0, 2); if ( (0 == String.Compare(twoLetterPrefix, "0x", StringComparison.OrdinalIgnoreCase)) || (0 == String.Compare(twoLetterPrefix, "&h", StringComparison.OrdinalIgnoreCase)) ) { // The incoming string is already in hex format ... we just need to // remove the 0x or &H from the beginning. return originalBaseAddress.Substring(2); } } // The incoming BaseAddress is not in hexadecimal format, so we need to // convert it to hex. try { uint baseAddressDecimal = UInt32.Parse(originalBaseAddress, CultureInfo.InvariantCulture); return baseAddressDecimal.ToString("X", CultureInfo.InvariantCulture); } catch (FormatException e) { throw Utilities.GetLocalizedArgumentException(e, ErrorString.Vbc_ParameterHasInvalidValue, "BaseAddress", originalBaseAddress); } } return null; } /// /// Looks at all the parameters that have been set, and builds up the string /// containing all the command-line switches. /// /// /// RGoel, JomoF protected internal override void AddResponseFileCommands(CommandLineBuilderExtension commandLine) { commandLine.AppendSwitchIfNotNull("/baseaddress:", this.GetBaseAddressInHex()); commandLine.AppendSwitchIfNotNull("/libpath:", this.AdditionalLibPaths, ","); commandLine.AppendSwitchIfNotNull("/imports:", this.Imports, ","); // Make sure this /doc+ switch comes *before* the /doc: switch (which is handled in the // ManagedCompiler.cs base class). /doc+ is really just an alias for /doc:.xml, // and the last /doc switch on the command-line wins. If the user provided a specific doc filename, // we want that one to win. commandLine.AppendPlusOrMinusSwitch("/doc", this._store, "GenerateDocumentation"); commandLine.AppendSwitchIfNotNull("/optioncompare:", this.OptionCompare); commandLine.AppendPlusOrMinusSwitch("/optionexplicit", this._store, "OptionExplicit"); // Make sure this /optionstrict+ switch appears *before* the /optionstrict:xxxx switch below /* twhitney: In Orcas a change was made for devdiv bug 16889 that set Option Strict-, whenever this.DisabledWarnings was * empty. That was clearly the wrong thing to do and we found it when we had a project with all the warning configuration * entries set to WARNING. Because this.DisabledWarnings was empty in that case we would end up sending /OptionStrict- * effectively silencing all the warnings that had been selected. * * Now what we do is: * If option strict+ is specified, that trumps everything and we just set option strict+ * Otherwise, just set option strict:custom. * You may wonder why we don't try to set Option Strict- The reason is that Option Strict- just implies a certain * set of warnings that should be disabled (there's ten of them today) You get the same effect by sending * option strict:custom on along with the correct list of disabled warnings. * Rather than make this code know the current set of disabled warnings that comprise Option strict-, we just send * option strict:custom on with the understanding that we'll get the same behavior as option strict- since we are passing * the /nowarn line on that contains all the warnings OptionStrict- would disable anyway. The IDE knows what they are * and puts them in the project file so we are good. And by not making this code aware of which warnings comprise * Option Strict-, we have one less place we have to keep up to date in terms of what comprises option strict- */ // Decide whether we are Option Strict+ or Option Strict:custom object optionStrictSetting = this._store["OptionStrict"]; bool optionStrict = optionStrictSetting != null ? (bool)optionStrictSetting : false; if (optionStrict) { commandLine.AppendSwitch("/optionstrict+"); } else // OptionStrict+ wasn't specified so use :custom. { commandLine.AppendSwitch("/optionstrict:custom"); } commandLine.AppendSwitchIfNotNull("/optionstrict:", this.OptionStrictType); commandLine.AppendWhenTrue("/nowarn", this._store, "NoWarnings"); commandLine.AppendSwitchWithSplitting("/nowarn:", this.DisabledWarnings, ",", ';', ','); commandLine.AppendPlusOrMinusSwitch("/optioninfer", this._store, "OptionInfer"); commandLine.AppendWhenTrue("/nostdlib", this._store, "NoStandardLib"); commandLine.AppendWhenTrue("/novbruntimeref", this._store, "NoVBRuntimeReference"); commandLine.AppendSwitchIfNotNull("/errorreport:", this.ErrorReport); commandLine.AppendSwitchIfNotNull("/platform:", this.PlatformWith32BitPreference); commandLine.AppendPlusOrMinusSwitch("/removeintchecks", this._store, "RemoveIntegerChecks"); commandLine.AppendSwitchIfNotNull("/rootnamespace:", this.RootNamespace); commandLine.AppendSwitchIfNotNull("/sdkpath:", this.SdkPath); commandLine.AppendSwitchIfNotNull("/langversion:", this.LangVersion); commandLine.AppendSwitchIfNotNull("/moduleassemblyname:", this.ModuleAssemblyName); commandLine.AppendWhenTrue("/netcf", this._store, "TargetCompactFramework"); commandLine.AppendSwitchIfNotNull("/preferreduilang:", this.PreferredUILang); commandLine.AppendPlusOrMinusSwitch("/highentropyva", this._store, "HighEntropyVA"); if (0 == String.Compare(this.VBRuntimePath, this.VBRuntime, StringComparison.OrdinalIgnoreCase)) { commandLine.AppendSwitchIfNotNull("/vbruntime:", this.VBRuntimePath); } else if (this.VBRuntime != null) { string vbRuntimeSwitch = this.VBRuntime; if (0 == String.Compare(vbRuntimeSwitch, "EMBED", StringComparison.OrdinalIgnoreCase)) { commandLine.AppendSwitch("/vbruntime*"); } else if (0 == String.Compare(vbRuntimeSwitch, "NONE", StringComparison.OrdinalIgnoreCase)) { commandLine.AppendSwitch("/vbruntime-"); } else if (0 == String.Compare(vbRuntimeSwitch, "DEFAULT", StringComparison.OrdinalIgnoreCase)) { commandLine.AppendSwitch("/vbruntime+"); } else { commandLine.AppendSwitchIfNotNull("/vbruntime:", vbRuntimeSwitch); } } // Verbosity if ( (this.Verbosity != null) && ( (0 == String.Compare(this.Verbosity, "quiet", StringComparison.OrdinalIgnoreCase)) || (0 == String.Compare(this.Verbosity, "verbose", StringComparison.OrdinalIgnoreCase)) ) ) { commandLine.AppendSwitchIfNotNull("/", this.Verbosity); } commandLine.AppendSwitchIfNotNull("/doc:", this.DocumentationFile); commandLine.AppendSwitchUnquotedIfNotNull("/define:", Vbc.GetDefineConstantsSwitch(this.DefineConstants)); AddReferencesToCommandLine(commandLine); commandLine.AppendSwitchIfNotNull("/win32resource:", this.Win32Resource); // Special case for "Sub Main" (See VSWhidbey 381254) if (0 != String.Compare("Sub Main", this.MainEntryPoint, StringComparison.OrdinalIgnoreCase)) { commandLine.AppendSwitchIfNotNull("/main:", this.MainEntryPoint); } base.AddResponseFileCommands(commandLine); // This should come after the "TreatWarningsAsErrors" flag is processed (in managedcompiler.cs). // Because if TreatWarningsAsErrors=false, then we'll have a /warnaserror- on the command-line, // and then any specific warnings that should be treated as errors should be specified with // /warnaserror+: after the /warnaserror- switch. The order of the switches on the command-line // does matter. // // Note that // /warnaserror+ // is just shorthand for: // /warnaserror+: // // Similarly, // /warnaserror- // is just shorthand for: // /warnaserror-: commandLine.AppendSwitchWithSplitting("/warnaserror+:", this.WarningsAsErrors, ",", ';', ','); commandLine.AppendSwitchWithSplitting("/warnaserror-:", this.WarningsNotAsErrors, ",", ';', ','); // If not design time build and the globalSessionGuid property was set then add a -globalsessionguid: bool designTime = false; if (this.HostObject != null) { var vbHost = this.HostObject as IVbcHostObject; designTime = vbHost.IsDesignTime(); } if (!designTime) { if (!string.IsNullOrWhiteSpace(this.VsSessionGuid)) { commandLine.AppendSwitchIfNotNull("/sqmsessionguid:", this.VsSessionGuid); } } // It's a good idea for the response file to be the very last switch passed, just // from a predictability perspective. It also solves the problem that a dogfooder // ran into, which is described in an email thread attached to bug VSWhidbey 146883. // See also bugs 177762 and 118307 for additional bugs related to response file position. if (this.ResponseFiles != null) { foreach (ITaskItem response in this.ResponseFiles) { commandLine.AppendSwitchIfNotNull("@", response.ItemSpec); } } } void AddReferencesToCommandLine(CommandLineBuilderExtension commandLine) { if ((this.References == null) || (this.References.Length == 0)) { return; } var references = new List(this.References.Length); var links = new List(this.References.Length); foreach (ITaskItem reference in this.References) { bool embed = Utilities.TryConvertItemMetadataToBool(reference, "EmbedInteropTypes"); if (embed) { links.Add(reference); } else { references.Add(reference); } } if (links.Count > 0) { commandLine.AppendSwitchIfNotNull("/link:", links.ToArray(), ","); } if (references.Count > 0) { commandLine.AppendSwitchIfNotNull("/reference:", references.ToArray(), ","); } } /// /// Validate parameters, log errors and warnings and return true if /// Execute should proceed. /// override protected bool ValidateParameters() { if (!base.ValidateParameters()) { return false; } // Validate that the "Verbosity" parameter is one of "quiet", "normal", or "verbose". if (this.Verbosity != null) { if ((0 != String.Compare(Verbosity, "normal", StringComparison.OrdinalIgnoreCase)) && (0 != String.Compare(Verbosity, "quiet", StringComparison.OrdinalIgnoreCase)) && (0 != String.Compare(Verbosity, "verbose", StringComparison.OrdinalIgnoreCase))) { Log.LogErrorWithCodeFromResources("Vbc.EnumParameterHasInvalidValue", "Verbosity", this.Verbosity, "Quiet, Normal, Verbose"); return false; } } return true; } /// /// This method intercepts the lines to be logged coming from STDOUT from VBC. /// Once we see a standard vb warning or error, then we capture it and grab the next 3 /// lines so we can transform the string form the form of FileName.vb(line) to FileName.vb(line,column) /// which will allow us to report the line and column to the IDE, and thus filter the error /// in the duplicate case for multi-targeting, or just squiggle the appropriate token /// instead of the entire line. /// /// A single line from the STDOUT of the vbc compiler /// High,Low,Normal protected override void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance) { // We can return immediately if this was not called by the out of proc compiler if (!this.UsedCommandLineTool) { base.LogEventsFromTextOutput(singleLine, messageImportance); return; } // We can also return immediately if the current string is not a warning or error // and we have not seen a warning or error yet. 'Error' and 'Warning' are not localized. if (vbErrorLines.Count == 0 && singleLine.IndexOf("warning", StringComparison.OrdinalIgnoreCase) == -1 && singleLine.IndexOf("error", StringComparison.OrdinalIgnoreCase) == -1) { base.LogEventsFromTextOutput(singleLine, messageImportance); return; } ParseVBErrorOrWarning(singleLine, messageImportance); } /// /// Given a string, parses it to find out whether it's an error or warning and, if so, /// make sure it's validated properly. /// /// /// INTERNAL FOR UNITTESTING ONLY /// /// The line to parse /// The MessageImportance to use when reporting the error. internal void ParseVBErrorOrWarning(string singleLine, MessageImportance messageImportance) { // if this string is empty then we haven't seen the first line of an error yet if (vbErrorLines.Count > 0) { // vbc separates the error message from the source text with an empty line, so // we can check for an empty line to see if vbc finished outputting the error message if (!isDoneOutputtingErrorMessage && singleLine.Length == 0) { isDoneOutputtingErrorMessage = true; numberOfLinesInErrorMessage = vbErrorLines.Count; } vbErrorLines.Enqueue(new VBError(singleLine, messageImportance)); // We are looking for the line that indicates the column (contains the '~'), // which vbc outputs 3 lines below the error message: // // // // // if (isDoneOutputtingErrorMessage && vbErrorLines.Count == numberOfLinesInErrorMessage + 3) { // Once we have the 4th line (error line + 3), then parse it for the first ~ // which will correspond to the column of the token with the error because // VBC respects the users's indentation settings in the file it is compiling // and only outputs SPACE chars to STDOUT. // The +1 is to translate the index into user columns which are 1 based. VBError originalVBError = vbErrorLines.Dequeue(); string originalVBErrorString = originalVBError.Message; int column = singleLine.IndexOf('~') + 1; int endParenthesisLocation = originalVBErrorString.IndexOf(')'); // If for some reason the line does not contain any ~ then something went wrong // so abort and return the origional string. if (column < 0 || endParenthesisLocation < 0) { // we need to output all of the original lines we ate. Log.LogMessageFromText(originalVBErrorString, originalVBError.MessageImportance); foreach (VBError vberror in vbErrorLines) { base.LogEventsFromTextOutput(vberror.Message, vberror.MessageImportance); } vbErrorLines.Clear(); return; } string newLine = null; newLine = originalVBErrorString.Substring(0, endParenthesisLocation) + "," + column + originalVBErrorString.Substring(endParenthesisLocation); // Output all of the lines of the error, but with the modified first line as well. Log.LogMessageFromText(newLine, originalVBError.MessageImportance); foreach (VBError vberror in vbErrorLines) { base.LogEventsFromTextOutput(vberror.Message, vberror.MessageImportance); } vbErrorLines.Clear(); } } else { CanonicalError.Parts parts = CanonicalError.Parse(singleLine); if (parts == null) { base.LogEventsFromTextOutput(singleLine, messageImportance); } else if ((parts.category == CanonicalError.Parts.Category.Error || parts.category == CanonicalError.Parts.Category.Warning) && parts.column == CanonicalError.Parts.numberNotSpecified) { if (parts.line != CanonicalError.Parts.numberNotSpecified) { // If we got here, then this is a standard VBC error or warning. vbErrorLines.Enqueue(new VBError(singleLine, messageImportance)); isDoneOutputtingErrorMessage = false; numberOfLinesInErrorMessage = 0; } else { // Project-level errors don't have line numbers -- just output now. base.LogEventsFromTextOutput(singleLine, messageImportance); } } } } #endregion /// /// Many VisualStudio VB projects have values for the DefineConstants property that /// contain quotes and spaces. Normally we don't allow parameters passed into the /// task to contain quotes, because if we weren't careful, we might accidently /// allow a parameter injection attach. But for "DefineConstants", we have to allow /// it. /// So this method prepares the string to be passed in on the /define: command-line /// switch. It does that by quoting the entire string, and escaping the embedded /// quotes. /// internal static string GetDefineConstantsSwitch ( string originalDefineConstants ) { if ((originalDefineConstants == null) || (originalDefineConstants.Length == 0)) { return null; } StringBuilder finalDefineConstants = new StringBuilder(originalDefineConstants); // Replace slash-quote with slash-slash-quote. finalDefineConstants.Replace("\\\"", "\\\\\""); // Replace quote with slash-quote. finalDefineConstants.Replace("\"", "\\\""); // Surround the whole thing with a pair of double-quotes. finalDefineConstants.Insert(0, '"'); finalDefineConstants.Append('"'); // Now it's ready to be passed in to the /define: switch. return finalDefineConstants.ToString(); } /// /// private class that just holds together name, value pair for the vbErrorLines Queue /// private class VBError { public string Message { get; set; } public MessageImportance MessageImportance { get; set; } public VBError(string message, MessageImportance importance) { this.Message = message; this.MessageImportance = importance; } } } }