|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
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;
using Microsoft.CodeAnalysis.CommandLine;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.BuildTasks
{
/// <summary>
/// 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.
/// </summary>
public class Vbc : ManagedCompiler
{
private bool _useHostCompilerIfAvailable;
// The following 1 fields are used, set and re-set in LogEventsFromTextOutput()
/// <summary>
/// This stores the original lines and error priority together in the order in which they were received.
/// </summary>
private readonly Queue<VBError> _vbErrorLines = new Queue<VBError>();
// Used when parsing vbc output to determine the column number of an error
private bool _isDoneOutputtingErrorMessage;
private int _numberOfLinesInErrorMessage;
internal override RequestLanguage Language => RequestLanguage.VisualBasicCompile;
#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[nameof(BaseAddress)] = value; }
get { return (string?)_store[nameof(BaseAddress)]; }
}
public string? DisabledWarnings
{
set { _store[nameof(DisabledWarnings)] = value; }
get { return (string?)_store[nameof(DisabledWarnings)]; }
}
public bool DisableSdkPath
{
set { _store[nameof(DisableSdkPath)] = value; }
get { return _store.GetOrDefault(nameof(DisableSdkPath), false); }
}
public string? DocumentationFile
{
set { _store[nameof(DocumentationFile)] = value; }
get { return (string?)_store[nameof(DocumentationFile)]; }
}
public string? ErrorReport
{
set { _store[nameof(ErrorReport)] = value; }
get { return (string?)_store[nameof(ErrorReport)]; }
}
public bool GenerateDocumentation
{
set { _store[nameof(GenerateDocumentation)] = value; }
get { return _store.GetOrDefault(nameof(GenerateDocumentation), false); }
}
public ITaskItem[]? Imports
{
set { _store[nameof(Imports)] = value; }
get { return (ITaskItem[]?)_store[nameof(Imports)]; }
}
public string? ModuleAssemblyName
{
set { _store[nameof(ModuleAssemblyName)] = value; }
get { return (string?)_store[nameof(ModuleAssemblyName)]; }
}
public bool NoStandardLib
{
set { _store[nameof(NoStandardLib)] = value; }
get { return _store.GetOrDefault(nameof(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[nameof(NoVBRuntimeReference)] = value; }
get { return _store.GetOrDefault(nameof(NoVBRuntimeReference), false); }
}
public bool NoWarnings
{
set { _store[nameof(NoWarnings)] = value; }
get { return _store.GetOrDefault(nameof(NoWarnings), false); }
}
public string? OptionCompare
{
set { _store[nameof(OptionCompare)] = value; }
get { return (string?)_store[nameof(OptionCompare)]; }
}
public bool OptionExplicit
{
set { _store[nameof(OptionExplicit)] = value; }
get { return _store.GetOrDefault(nameof(OptionExplicit), true); }
}
public bool OptionStrict
{
set { _store[nameof(OptionStrict)] = value; }
get { return _store.GetOrDefault(nameof(OptionStrict), false); }
}
public bool OptionInfer
{
set { _store[nameof(OptionInfer)] = value; }
get { return _store.GetOrDefault(nameof(OptionInfer), false); }
}
// Currently only /optionstrict:custom
public string? OptionStrictType
{
set { _store[nameof(OptionStrictType)] = value; }
get { return (string?)_store[nameof(OptionStrictType)]; }
}
public bool RemoveIntegerChecks
{
set { _store[nameof(RemoveIntegerChecks)] = value; }
get { return _store.GetOrDefault(nameof(RemoveIntegerChecks), false); }
}
public string? RootNamespace
{
set { _store[nameof(RootNamespace)] = value; }
get { return (string?)_store[nameof(RootNamespace)]; }
}
public string? SdkPath
{
set { _store[nameof(SdkPath)] = value; }
get { return (string?)_store[nameof(SdkPath)]; }
}
/// <summary>
/// Name of the language passed to "/preferreduilang" compiler option.
/// </summary>
/// <remarks>
/// If set to null, "/preferreduilang" option is omitted, and vbc.exe uses its default setting.
/// Otherwise, the value is passed to "/preferreduilang" as is.
/// </remarks>
public string? PreferredUILang
{
set { _store[nameof(PreferredUILang)] = value; }
get { return (string?)_store[nameof(PreferredUILang)]; }
}
public string? VsSessionGuid
{
set { _store[nameof(VsSessionGuid)] = value; }
get { return (string?)_store[nameof(VsSessionGuid)]; }
}
public bool TargetCompactFramework
{
set { _store[nameof(TargetCompactFramework)] = value; }
get { return _store.GetOrDefault(nameof(TargetCompactFramework), false); }
}
public bool UseHostCompilerIfAvailable
{
set { _useHostCompilerIfAvailable = value; }
get { return _useHostCompilerIfAvailable; }
}
public string? VBRuntimePath
{
set { _store[nameof(VBRuntimePath)] = value; }
get { return (string?)_store[nameof(VBRuntimePath)]; }
}
public string? Verbosity
{
set { _store[nameof(Verbosity)] = value; }
get { return (string?)_store[nameof(Verbosity)]; }
}
public string? WarningsAsErrors
{
set { _store[nameof(WarningsAsErrors)] = value; }
get { return (string?)_store[nameof(WarningsAsErrors)]; }
}
public string? WarningsNotAsErrors
{
set { _store[nameof(WarningsNotAsErrors)] = value; }
get { return (string?)_store[nameof(WarningsNotAsErrors)]; }
}
public string? VBRuntime
{
set { _store[nameof(VBRuntime)] = value; }
get { return (string?)_store[nameof(VBRuntime)]; }
}
public string? PdbFile
{
set { _store[nameof(PdbFile)] = value; }
get { return (string?)_store[nameof(PdbFile)]; }
}
#endregion
#region Tool Members
private static readonly string[] s_separator = { Environment.NewLine };
internal override void LogCompilerOutput(string output, MessageImportance messageImportance)
{
var lines = output.Split(s_separator, StringSplitOptions.None);
foreach (string line in lines)
{
//Code below will parse the set of four lines that comprise a VB
//error message into a single object. The four-line format contains
//a second line that is blank. This must be passed to the code below
//to satisfy the parser. The parser needs to work with output from
//old compilers as well.
LogEventsFromTextOutput(line, messageImportance);
}
}
/// <summary>
/// Return the name of the tool to execute.
/// </summary>
protected override string ToolNameWithoutExtension
{
get
{
return "vbc";
}
}
/// <summary>
/// Override Execute so that we can moved the PDB file if we need to,
/// after the compiler is done.
/// </summary>
public override bool Execute()
{
if (!base.Execute())
{
return false;
}
if (!SkipCompilerExecution)
{
MovePdbFileIfNecessary(OutputAssembly?.ItemSpec);
}
return !Log.HasLoggedErrors;
}
/// <summary>
/// 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.
/// </summary>
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 (RoslynString.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);
}
}
/// <summary>
/// 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".
/// </summary>
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;
}
/// <summary>
/// Looks at all the parameters that have been set, and builds up the string
/// containing all the command-line switches.
/// </summary>
protected 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:<file> switch (which is handled in the
// ManagedCompiler.cs base class). /doc+ is really just an alias for /doc:<assemblyname>.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.AppendWhenTrue("/nosdkpath", _store, nameof(DisableSdkPath));
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("/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);
}
if ((bool?)this._store[nameof(GenerateDocumentation)] != false)
{
// Only provide the filename when GenerateDocumentation is not
// explicitly disabled. Otherwise, the /doc switch (which comes
// later in the command) overrides and re-enabled generating
// documentation.
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+:<list> after the /warnaserror- switch. The order of the switches on the command-line
// does matter.
//
// Note that
// /warnaserror+
// is just shorthand for:
// /warnaserror+:<all possible warnings>
//
// Similarly,
// /warnaserror-
// is just shorthand for:
// /warnaserror-:<all possible warnings>
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:<guid>
bool designTime = false;
if (this.HostObject is IVbcHostObject vbHost)
{
designTime = vbHost.IsDesignTime();
}
else if (this.HostObject != null)
{
throw new InvalidOperationException(string.Format(ErrorString.General_IncorrectHostObject, "Vbc", "IVbcHostObject"));
}
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);
}
}
}
private void AddReferencesToCommandLine(CommandLineBuilderExtension commandLine)
{
if ((this.References == null) || (this.References.Length == 0))
{
return;
}
var references = new List<ITaskItem>(this.References.Length);
var links = new List<ITaskItem>(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(), ",");
}
}
/// <summary>
/// Validate parameters, log errors and warnings and return true if
/// Execute should proceed.
/// </summary>
protected override 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;
}
/// <summary>
/// This method is called by MSBuild when running vbc as a separate process, it does not get called
/// for normal VBCSCompiler compilations.
///
/// The vbc process emits multi-line error messages and this method is called for every line of
/// output one at a time. This method must queue up the messages and re-hydrate them back into the
/// original vbc structure such that we can call <see cref="TaskLoggingHelper.LogMessageFromText(string, MessageImportance)" />
/// with the complete error message.
/// </summary>
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);
}
/// <summary>
/// Given a string, parses it to find out whether it's an error or warning and, if so,
/// make sure it's validated properly.
/// </summary>
/// <param name="singleLine">The line to parse</param>
/// <param name="messageImportance">The MessageImportance to use when reporting the error.</param>
private 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:
//
// <error message>
// <blank line>
// <line with the source text>
// <line with the '~'>
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(") :", StringComparison.Ordinal);
// If for some reason the line does not contain any ~ then something went wrong
// so abort and return the original 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 = 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
/// <summary>
/// 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 accidentally
/// 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.
/// </summary>
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();
}
/// <summary>
/// This method will initialize the host compiler object with all the switches,
/// parameters, resources, references, sources, etc.
///
/// It returns true if everything went according to plan. It returns false if the
/// host compiler had a problem with one of the parameters that was passed in.
///
/// This method also sets the "this.HostCompilerSupportsAllParameters" property
/// accordingly.
///
/// Example:
/// If we attempted to pass in Platform="goobar", then this method would
/// set HostCompilerSupportsAllParameters=true, but it would throw an
/// exception because the host compiler fully supports
/// the Platform parameter, but "goobar" is an illegal value.
///
/// Example:
/// If we attempted to pass in NoConfig=false, then this method would set
/// HostCompilerSupportsAllParameters=false, because while this is a legal
/// thing for csc.exe, the IDE compiler cannot support it. In this situation
/// the return value will also be false.
/// </summary>
private bool InitializeHostCompiler(IVbcHostObject vbcHostObject)
{
this.HostCompilerSupportsAllParameters = this.UseHostCompilerIfAvailable;
string param = "Unknown";
try
{
param = nameof(vbcHostObject.BeginInitialization);
vbcHostObject.BeginInitialization();
CheckHostObjectSupport(param = nameof(AdditionalLibPaths), vbcHostObject.SetAdditionalLibPaths(AdditionalLibPaths));
CheckHostObjectSupport(param = nameof(AddModules), vbcHostObject.SetAddModules(AddModules));
// For host objects which support them, set the analyzers, ruleset and additional files.
IAnalyzerHostObject? analyzerHostObject = vbcHostObject as IAnalyzerHostObject;
if (analyzerHostObject != null)
{
CheckHostObjectSupport(param = nameof(Analyzers), analyzerHostObject.SetAnalyzers(Analyzers));
CheckHostObjectSupport(param = nameof(CodeAnalysisRuleSet), analyzerHostObject.SetRuleSet(CodeAnalysisRuleSet));
CheckHostObjectSupport(param = nameof(AdditionalFiles), analyzerHostObject.SetAdditionalFiles(AdditionalFiles));
}
// For host objects which support them, set analyzer config files and potential analyzer config files
if (vbcHostObject is IAnalyzerConfigFilesHostObject analyzerConfigFilesHostObject)
{
CheckHostObjectSupport(param = nameof(AnalyzerConfigFiles), analyzerConfigFilesHostObject.SetAnalyzerConfigFiles(AnalyzerConfigFiles));
CheckHostObjectSupport(param = nameof(PotentialAnalyzerConfigFiles), analyzerConfigFilesHostObject.SetPotentialAnalyzerConfigFiles(PotentialAnalyzerConfigFiles));
}
CheckHostObjectSupport(param = nameof(BaseAddress), vbcHostObject.SetBaseAddress(TargetType, GetBaseAddressInHex()));
CheckHostObjectSupport(param = nameof(CodePage), vbcHostObject.SetCodePage(CodePage));
CheckHostObjectSupport(param = nameof(DebugType), vbcHostObject.SetDebugType(EmitDebugInformation, DebugType));
CheckHostObjectSupport(param = nameof(DefineConstants), vbcHostObject.SetDefineConstants(DefineConstants));
CheckHostObjectSupport(param = nameof(DelaySign), vbcHostObject.SetDelaySign(DelaySign));
CheckHostObjectSupport(param = nameof(DocumentationFile), vbcHostObject.SetDocumentationFile(DocumentationFile));
CheckHostObjectSupport(param = nameof(FileAlignment), vbcHostObject.SetFileAlignment(FileAlignment));
CheckHostObjectSupport(param = nameof(GenerateDocumentation), vbcHostObject.SetGenerateDocumentation(GenerateDocumentation));
CheckHostObjectSupport(param = nameof(Imports), vbcHostObject.SetImports(Imports));
CheckHostObjectSupport(param = nameof(KeyContainer), vbcHostObject.SetKeyContainer(KeyContainer));
CheckHostObjectSupport(param = nameof(KeyFile), vbcHostObject.SetKeyFile(KeyFile));
CheckHostObjectSupport(param = nameof(LinkResources), vbcHostObject.SetLinkResources(LinkResources));
CheckHostObjectSupport(param = nameof(MainEntryPoint), vbcHostObject.SetMainEntryPoint(MainEntryPoint));
CheckHostObjectSupport(param = nameof(NoConfig), vbcHostObject.SetNoConfig(NoConfig));
CheckHostObjectSupport(param = nameof(NoStandardLib), vbcHostObject.SetNoStandardLib(NoStandardLib));
CheckHostObjectSupport(param = nameof(NoWarnings), vbcHostObject.SetNoWarnings(NoWarnings));
CheckHostObjectSupport(param = nameof(Optimize), vbcHostObject.SetOptimize(Optimize));
CheckHostObjectSupport(param = nameof(OptionCompare), vbcHostObject.SetOptionCompare(OptionCompare));
CheckHostObjectSupport(param = nameof(OptionExplicit), vbcHostObject.SetOptionExplicit(OptionExplicit));
CheckHostObjectSupport(param = nameof(OptionStrict), vbcHostObject.SetOptionStrict(OptionStrict));
CheckHostObjectSupport(param = nameof(OptionStrictType), vbcHostObject.SetOptionStrictType(OptionStrictType));
CheckHostObjectSupport(param = nameof(OutputAssembly), vbcHostObject.SetOutputAssembly(OutputAssembly?.ItemSpec));
// For host objects which support them, set platform with 32BitPreference, HighEntropyVA, and SubsystemVersion
IVbcHostObject5? vbcHostObject5 = vbcHostObject as IVbcHostObject5;
if (vbcHostObject5 != null)
{
CheckHostObjectSupport(param = nameof(PlatformWith32BitPreference), vbcHostObject5.SetPlatformWith32BitPreference(PlatformWith32BitPreference));
CheckHostObjectSupport(param = nameof(HighEntropyVA), vbcHostObject5.SetHighEntropyVA(HighEntropyVA));
CheckHostObjectSupport(param = nameof(SubsystemVersion), vbcHostObject5.SetSubsystemVersion(SubsystemVersion));
}
else
{
CheckHostObjectSupport(param = nameof(Platform), vbcHostObject.SetPlatform(Platform));
}
IVbcHostObject6? vbcHostObject6 = vbcHostObject as IVbcHostObject6;
if (vbcHostObject6 != null)
{
CheckHostObjectSupport(param = nameof(ErrorLog), vbcHostObject6.SetErrorLog(ErrorLog));
CheckHostObjectSupport(param = nameof(ReportAnalyzer), vbcHostObject6.SetReportAnalyzer(ReportAnalyzer));
}
CheckHostObjectSupport(param = nameof(References), vbcHostObject.SetReferences(References));
CheckHostObjectSupport(param = nameof(RemoveIntegerChecks), vbcHostObject.SetRemoveIntegerChecks(RemoveIntegerChecks));
CheckHostObjectSupport(param = nameof(Resources), vbcHostObject.SetResources(Resources));
CheckHostObjectSupport(param = nameof(ResponseFiles), vbcHostObject.SetResponseFiles(ResponseFiles));
CheckHostObjectSupport(param = nameof(RootNamespace), vbcHostObject.SetRootNamespace(RootNamespace));
CheckHostObjectSupport(param = nameof(SdkPath), vbcHostObject.SetSdkPath(SdkPath));
CheckHostObjectSupport(param = nameof(Sources), vbcHostObject.SetSources(Sources));
CheckHostObjectSupport(param = nameof(TargetCompactFramework), vbcHostObject.SetTargetCompactFramework(TargetCompactFramework));
CheckHostObjectSupport(param = nameof(TargetType), vbcHostObject.SetTargetType(TargetType));
CheckHostObjectSupport(param = nameof(TreatWarningsAsErrors), vbcHostObject.SetTreatWarningsAsErrors(TreatWarningsAsErrors));
CheckHostObjectSupport(param = nameof(WarningsAsErrors), vbcHostObject.SetWarningsAsErrors(WarningsAsErrors));
CheckHostObjectSupport(param = nameof(WarningsNotAsErrors), vbcHostObject.SetWarningsNotAsErrors(WarningsNotAsErrors));
// DisabledWarnings needs to come after WarningsAsErrors and WarningsNotAsErrors, because
// of the way the host object works, and the fact that DisabledWarnings trump Warnings[Not]AsErrors.
CheckHostObjectSupport(param = nameof(DisabledWarnings), vbcHostObject.SetDisabledWarnings(DisabledWarnings));
CheckHostObjectSupport(param = nameof(Win32Icon), vbcHostObject.SetWin32Icon(Win32Icon));
CheckHostObjectSupport(param = nameof(Win32Resource), vbcHostObject.SetWin32Resource(Win32Resource));
// In order to maintain compatibility with previous host compilers, we must
// light-up for IVbcHostObject2
if (vbcHostObject is IVbcHostObject2)
{
IVbcHostObject2 vbcHostObject2 = (IVbcHostObject2)vbcHostObject;
CheckHostObjectSupport(param = nameof(ModuleAssemblyName), vbcHostObject2.SetModuleAssemblyName(ModuleAssemblyName));
CheckHostObjectSupport(param = nameof(OptionInfer), vbcHostObject2.SetOptionInfer(OptionInfer));
CheckHostObjectSupport(param = nameof(Win32Manifest), vbcHostObject2.SetWin32Manifest(GetWin32ManifestSwitch(NoWin32Manifest, Win32Manifest)));
// initialize option Infer
CheckHostObjectSupport(param = nameof(OptionInfer), vbcHostObject2.SetOptionInfer(OptionInfer));
}
else
{
// If we have been given a property that the host compiler doesn't support
// then we need to state that we are falling back to the command line compiler
if (!String.IsNullOrEmpty(ModuleAssemblyName))
{
CheckHostObjectSupport(param = nameof(ModuleAssemblyName), resultFromHostObjectSetOperation: false);
}
if (_store.ContainsKey(nameof(OptionInfer)))
{
CheckHostObjectSupport(param = nameof(OptionInfer), resultFromHostObjectSetOperation: false);
}
if (!String.IsNullOrEmpty(Win32Manifest))
{
CheckHostObjectSupport(param = nameof(Win32Manifest), resultFromHostObjectSetOperation: false);
}
}
// Check for support of the LangVersion property
if (vbcHostObject is IVbcHostObject3 && !DeferToICompilerOptionsHostObject(LangVersion, vbcHostObject))
{
IVbcHostObject3 vbcHostObject3 = (IVbcHostObject3)vbcHostObject;
CheckHostObjectSupport(param = nameof(LangVersion), vbcHostObject3.SetLanguageVersion(LangVersion));
}
else if (!String.IsNullOrEmpty(LangVersion) && !UsedCommandLineTool)
{
CheckHostObjectSupport(param = nameof(LangVersion), resultFromHostObjectSetOperation: false);
}
if (vbcHostObject is IVbcHostObject4)
{
IVbcHostObject4 vbcHostObject4 = (IVbcHostObject4)vbcHostObject;
CheckHostObjectSupport(param = nameof(VBRuntime), vbcHostObject4.SetVBRuntime(VBRuntime));
}
// Support for NoVBRuntimeReference was added to this task after IVbcHostObject was frozen. That doesn't matter much because the host
// compiler doesn't support it, and almost nobody uses it anyway. But if someone has set it, we need to hard code falling back to
// the command line compiler here.
if (NoVBRuntimeReference)
{
CheckHostObjectSupport(param = nameof(NoVBRuntimeReference), resultFromHostObjectSetOperation: false);
}
InitializeHostObjectSupportForNewSwitches(vbcHostObject, ref param);
// In general, we don't support preferreduilang with the in-proc compiler. It will always use the same locale as the
// host process, so in general, we have to fall back to the command line compiler if this option is specified.
// However, we explicitly allow two values (mostly for parity with C#):
// Null is supported because it means that option should be omitted, and compiler default used - obviously always valid.
// Explicitly specified name of current locale is also supported, since it is effectively a no-op.
if (!String.IsNullOrEmpty(PreferredUILang) && !String.Equals(PreferredUILang, System.Globalization.CultureInfo.CurrentUICulture.Name, StringComparison.OrdinalIgnoreCase))
{
CheckHostObjectSupport(param = nameof(PreferredUILang), resultFromHostObjectSetOperation: false);
}
}
catch (Exception e)
{
Log.LogErrorWithCodeFromResources("General_CouldNotSetHostObjectParameter", param, e.Message);
return false;
}
finally
{
// In the case of the VB host compiler, the EndInitialization method will
// throw (due to FAILED HRESULT) if there was a bad value for one of the
// parameters.
vbcHostObject.EndInitialization();
}
return true;
}
// VbcHostObject doesn't support VB versions beyond 15,
// so the LangVersion will be passed through ICompilerOptionsHostObject.SetCompilerOptions instead
private static bool DeferToICompilerOptionsHostObject(string? langVersion, IVbcHostObject vbcHostObject)
{
if (!(vbcHostObject is ICompilerOptionsHostObject))
{
return false;
}
if (langVersion == null)
{
// CVbcMSBuildHostObject::SetLanguageVersion can handle null
return false;
}
// CVbcMSBuildHostObject::SetLanguageVersion can handle versions up to 15
var supportedList = new[]
{
"9", "9.0",
"10", "10.0",
"11", "11.0",
"12", "12.0",
"14", "14.0",
"15", "15.0"
};
return Array.IndexOf(supportedList, langVersion) < 0;
}
/// <summary>
/// This method will get called during Execute() if a host object has been passed into the Vbc
/// task. Returns one of the following values to indicate what the next action should be:
/// UseHostObjectToExecute Host compiler exists and was initialized.
/// UseAlternateToolToExecute Host compiler doesn't exist or was not appropriate.
/// NoActionReturnSuccess Host compiler was already up-to-date, and we're done.
/// NoActionReturnFailure Bad parameters were passed into the task.
/// </summary>
protected override HostObjectInitializationStatus InitializeHostObject()
{
if (this.HostObject != null)
{
// When the host object was passed into the task, it was passed in as a generic
// "Object" (because ITask interface obviously can't have any Vbc-specific stuff
// in it, and each task is going to want to communicate with its host in a unique
// way). Now we cast it to the specific type that the Vbc task expects. If the
// host object does not match this type, the host passed in an invalid host object
// to Vbc, and we error out.
// NOTE: For compat reasons this must remain IVbcHostObject
// we can dynamically test for smarter interfaces later..
if (HostObject is IVbcHostObject hostObjectCOM)
{
using (RCWForCurrentContext<IVbcHostObject> hostObject = new RCWForCurrentContext<IVbcHostObject>(hostObjectCOM))
{
IVbcHostObject vbcHostObject = hostObject.RCW;
bool hostObjectSuccessfullyInitialized = InitializeHostCompiler(vbcHostObject);
// If we're currently only in design-time (as opposed to build-time),
// then we're done. We've initialized the host compiler as best we
// can, and we certainly don't want to actually do the final compile.
// So return true, saying we're done and successful.
if (vbcHostObject.IsDesignTime())
{
// If we are design-time then we do not want to continue the build at
// this time.
return hostObjectSuccessfullyInitialized ?
HostObjectInitializationStatus.NoActionReturnSuccess :
HostObjectInitializationStatus.NoActionReturnFailure;
}
if (!this.HostCompilerSupportsAllParameters)
{
// Since the host compiler has refused to take on the responsibility for this compilation,
// we're about to shell out to the command-line compiler to handle it. If some of the
// references don't exist on disk, we know the command-line compiler will fail, so save
// the trouble, and just throw a consistent error ourselves. This allows us to give
// more information than the compiler would, and also make things consistent across
// Vbc / Csc / etc. Actually, the real reason is bug 275726 (ddsuites\src\vs\env\vsproject\refs\ptp3).
// This suite behaves differently in localized builds than on English builds because
// VBC.EXE doesn't localize the word "error" when they emit errors and so we can't scan for it.
if (!CheckAllReferencesExistOnDisk())
{
return HostObjectInitializationStatus.NoActionReturnFailure;
}
// The host compiler doesn't support some of the switches/parameters
// being passed to it. Therefore, we resort to using the command-line compiler
// in this case.
UsedCommandLineTool = true;
return HostObjectInitializationStatus.UseAlternateToolToExecute;
}
// Ok, by now we validated that the host object supports the necessary switches
// and parameters. Last thing to check is whether the host object is up to date,
// and in that case, we will inform the caller that no further action is necessary.
if (hostObjectSuccessfullyInitialized)
{
return vbcHostObject.IsUpToDate() ?
HostObjectInitializationStatus.NoActionReturnSuccess :
HostObjectInitializationStatus.UseHostObjectToExecute;
}
else
{
return HostObjectInitializationStatus.NoActionReturnFailure;
}
}
}
else
{
Log.LogErrorWithCodeFromResources("General_IncorrectHostObject", "Vbc", "IVbcHostObject");
}
}
// No appropriate host object was found.
UsedCommandLineTool = true;
return HostObjectInitializationStatus.UseAlternateToolToExecute;
}
/// <summary>
/// This method will get called during Execute() if a host object has been passed into the Vbc
/// task. Returns true if an appropriate host object was found, it was called to do the compile,
/// and the compile succeeded. Otherwise, we return false.
/// </summary>
protected override bool CallHostObjectToExecute()
{
Debug.Assert(this.HostObject != null, "We should not be here if the host object has not been set.");
IVbcHostObject? vbcHostObject = this.HostObject as IVbcHostObject;
RoslynDebug.Assert(vbcHostObject != null, "Wrong kind of host object passed in!");
IVbcHostObject5? vbcHostObject5 = vbcHostObject as IVbcHostObject5;
Debug.Assert(vbcHostObject5 != null, "Wrong kind of host object passed in!");
// IVbcHostObjectFreeThreaded::Compile is the preferred way to compile the host object
// because while it is still synchronous it does its waiting on our BG thread
// (as opposed to the UI thread for IVbcHostObject::Compile)
if (vbcHostObject5 != null)
{
IVbcHostObjectFreeThreaded freeThreadedHostObject = vbcHostObject5.GetFreeThreadedHostObject();
return freeThreadedHostObject.Compile();
}
else
{
// If for some reason we can't get to IVbcHostObject5 we just fall back to the old
// Compile method. This method unfortunately allows for reentrancy on the UI thread.
return vbcHostObject.Compile();
}
}
/// <summary>
/// private class that just holds together name, value pair for the vbErrorLines Queue
/// </summary>
private class VBError
{
public string Message { get; }
public MessageImportance MessageImportance { get; }
public VBError(string message, MessageImportance importance)
{
this.Message = message;
this.MessageImportance = importance;
}
}
}
}
|