diff --git a/tools/StaticAnalysis/AnalysisLogger.cs b/tools/StaticAnalysis/AnalysisLogger.cs new file mode 100644 index 000000000000..f6aa7b493f3e --- /dev/null +++ b/tools/StaticAnalysis/AnalysisLogger.cs @@ -0,0 +1,120 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace StaticAnalysis +{ + /// + /// Abstract class to implement the report logging structure + /// + public abstract class AnalysisLogger + { + string _baseDirectory; + + /// + /// Create an analysis logger that will write reports to the given directory + /// + /// + public AnalysisLogger(string baseDirectory) + { + _baseDirectory = baseDirectory; + } + + IList _loggers = new List(); + protected virtual IList Loggers { get { return _loggers; } } + + /// + /// Write an error report. + /// + /// The message to write + public abstract void WriteError(string error); + + public virtual void WriteError(string format, params object[] args) + { + WriteError(string.Format(format, args)); + } + + /// + /// Write an informational message. + /// + /// The message to write + public abstract void WriteMessage(string message); + + public virtual void WriteMessage(string format, params object[] args) + { + WriteMessage(string.Format(format, args)); + } + + /// + /// Write a warning. + /// + /// The warning text + public abstract void WriteWarning(string message); + + public virtual void WriteWarning(string format, params object[] args) + { + WriteWarning(string.Format(format, args)); + } + + /// + /// Write a report file to the given file, using the given file contents. + /// + /// The path to the file + /// The contents of the report + public abstract void WriteReport(string name, string contents); + + /// + /// Create a logger for a particular report + /// + /// The type of records written to the log + /// The filename (without file path) where the report will be written + /// The given logger. Analyzer may write records to this logger and they will be written to + /// the report file. + public virtual ReportLogger CreateLogger(string fileName) where T : IReportRecord, new() + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException("fileName"); + } + + var filePath = Path.Combine(_baseDirectory, fileName); + var logger = new ReportLogger(filePath, this); + Loggers.Add(logger); + return logger; + } + + /// + /// Write out the report files for each of the added report loggers. + /// + public void WriteReports() + { + foreach (var logger in Loggers.Where(l => l.Records.Any())) + { + StringBuilder reportText = new StringBuilder(); + reportText.AppendLine(logger.Records.First().PrintHeaders()); + foreach (var reportRecord in logger.Records) + { + reportText.AppendLine(reportRecord.FormatRecord()); + } + + WriteReport(logger.FileName, reportText.ToString()); + } + } + } +} diff --git a/tools/StaticAnalysis/ConsoleLogger.cs b/tools/StaticAnalysis/ConsoleLogger.cs new file mode 100644 index 000000000000..1cb18d070d64 --- /dev/null +++ b/tools/StaticAnalysis/ConsoleLogger.cs @@ -0,0 +1,51 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.IO; + +namespace StaticAnalysis +{ + /// + /// Simple class for logging errors and warnings to the console and writing reports to the file system. + /// + public class ConsoleLogger : AnalysisLogger + { + + public ConsoleLogger(string baseDirectory) + : base(baseDirectory) + { + } + + public override void WriteError(string error) + { + Console.WriteLine("### ERROR {0}", error); + } + + public override void WriteMessage(string message) + { + Console.WriteLine(message); + } + + public override void WriteWarning(string message) + { + Console.WriteLine("Warning: {0}", message); + } + + public override void WriteReport(string name, string records) + { + File.WriteAllText(name, records); + } + } +} diff --git a/tools/StaticAnalysis/Decorator.cs b/tools/StaticAnalysis/Decorator.cs new file mode 100644 index 000000000000..48ce7600f477 --- /dev/null +++ b/tools/StaticAnalysis/Decorator.cs @@ -0,0 +1,88 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; + +namespace StaticAnalysis +{ + /// + /// Abstract class to implement the Decorator pattern + /// + /// + public class Decorator + { + Action _action; + string _name; + + protected Decorator(Action action, string name) + { + _action = action; + _name = name; + Inner = null; + } + + public static Decorator Create() + { + return new Decorator(r => { }, "default"); + } + + public void Apply(T record) + { + _action(record); + if (Inner != null) + { + Inner.Apply(record); + } + } + + public void AddDecorator(Action action, string name) + { + if (action == null) + { + throw new ArgumentNullException("action"); + } + + if (name == null) + { + throw new ArgumentNullException("name"); + } + + if (Inner == null) + { + Inner = new Decorator(action, name); + } + else + { + Inner.AddDecorator(action, name); + } + } + + public void Remove(string name) + { + if (Inner != null) + { + if (string.Equals(Inner._name, name)) + { + Inner = Inner.Inner; + } + else + { + Inner.Remove(name); + } + } + } + + protected Decorator Inner { get; set; } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/AssemblyLoader.cs b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyLoader.cs new file mode 100644 index 000000000000..15cd6731a4c8 --- /dev/null +++ b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyLoader.cs @@ -0,0 +1,101 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Reflection; + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// A class using .Net Remoting to load assemblies and retrieve information in a separate app domain + /// + public class AssemblyLoader : MarshalByRefObject + { + /// + /// Load the assembly in the reflection context by name. Will succeed if the referenced assembly name can + /// be found using default assembly loading rules (i.e. it is in the current directory or the GAC) + /// + /// The full name of the assembly + /// Information on the given assembly, if it was loaded successfully, or null if there is an + /// assembly loading issue. + public AssemblyMetadata GetReflectedAssemblyInfo(string assemblyName) + { + if (string.IsNullOrWhiteSpace(assemblyName)) + { + throw new ArgumentException("assemblyName"); + } + + AssemblyMetadata result = null; + try + { + result = new AssemblyMetadata(Assembly.ReflectionOnlyLoad(assemblyName)); + } + catch + { + } + + return result; + } + + /// + /// Load the assembly found at the given path in the reflection context and return assembly metadata + /// + /// The full path to the assembly file. + /// Assembly metadata if the assembly is loaded successfully, or null if there are load errors. + public AssemblyMetadata GetReflectedAssemblyFromFile(string assemblyPath) + { + if (string.IsNullOrWhiteSpace(assemblyPath)) + { + throw new ArgumentException("assemblyPath"); + } + + AssemblyMetadata result = null; + try + { + return new AssemblyMetadata(Assembly.ReflectionOnlyLoadFrom(assemblyPath)); + } + catch + { + } + + return result; + } + + /// + /// Create a new AppDomain and create a remote instance of AssemblyLoader we can use there + /// + /// directory containing assemblies + /// A new AppDomain, where assemblies can be loaded + /// A proxy to the AssemblyLoader running in the newly created app domain + public static AssemblyLoader Create(string directoryPath, out AppDomain testDomain) + { + if (string.IsNullOrWhiteSpace(directoryPath)) + { + throw new ArgumentException("directoryPath"); + } + + var setup = new AppDomainSetup(); + setup.ApplicationBase = directoryPath; + setup.ApplicationName = "TestDomain"; + setup.ApplicationTrust = AppDomain.CurrentDomain.ApplicationTrust; + setup.DisallowApplicationBaseProbing = false; + setup.DisallowCodeDownload = false; + setup.DisallowBindingRedirects = false; + setup.DisallowPublisherPolicy = false; + testDomain = AppDomain.CreateDomain("TestDomain", null, setup); + return testDomain.CreateInstanceFromAndUnwrap(typeof(AssemblyLoader).Assembly.Location, + typeof(AssemblyLoader).FullName) as AssemblyLoader; + } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/AssemblyMetadata.cs b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyMetadata.cs new file mode 100644 index 000000000000..101cd1d99a2a --- /dev/null +++ b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyMetadata.cs @@ -0,0 +1,70 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// Serializable assembly metadata class, used to return assembly information from a remote AppDomain + /// + [Serializable] + public class AssemblyMetadata + { + AssemblyName _name; + string _location; + IList _references; + + public AssemblyMetadata(Assembly assembly) + { + if (assembly == null) + { + throw new ArgumentNullException("assembly"); + } + + _name = assembly.GetName(); + _location = assembly.Location; + _references = new List(); + foreach (var child in assembly.GetReferencedAssemblies()) + { + _references.Add(child); + } + } + + /// + /// Path to the assembly. + /// + public string Location { get { return _location; } } + + /// + /// The assembly name + /// + /// The assembly name for this assembly, including name and the version + public AssemblyName GetName() + { + return _name; + } + + /// + /// The list of referenced assemblies + /// + /// A list of assembly name references for all assemblies referenced in the assembly manifest + public IEnumerable GetReferencedAssemblies() + { + return _references; + } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/AssemblyNameComparer.cs b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyNameComparer.cs new file mode 100644 index 000000000000..1f0d1fa87414 --- /dev/null +++ b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyNameComparer.cs @@ -0,0 +1,56 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// Equality comparer, used to uniquely store assembly records by assembly name + /// + public class AssemblyNameComparer : IEqualityComparer + { + private static string GetComparisonName(AssemblyName name) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + return string.Format("{0}.v{1}", name.Name, name.Version); + } + + public bool Equals(AssemblyName assembly1, AssemblyName assembly2) + { + if (assembly1 == null) + { + throw new ArgumentNullException("assembly1"); + } + + if (assembly2 == null) + { + throw new ArgumentNullException("assembly2"); + } + + return StringComparer.OrdinalIgnoreCase.Equals(GetComparisonName(assembly1), GetComparisonName(assembly2)); + } + + public int GetHashCode(AssemblyName obj) + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(GetComparisonName(obj)); + } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/AssemblyRecord.cs b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyRecord.cs new file mode 100644 index 000000000000..a6a0c41e9e53 --- /dev/null +++ b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyRecord.cs @@ -0,0 +1,137 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// Information about assemblies + /// + public class AssemblyRecord : ICloneable + { + HashSet _parents = new HashSet(); + IList _children = new List(); + /// + /// The path to the assembly + /// + public string Location { get; set; } + + public string Name { get { return AssemblyName.Name; } } + public Version Version { get { return AssemblyName.Version; } } + public HashSet ReferencingAssembly { get { return _parents; } } + + public IList Children { get { return _children; } } + + public AssemblyName AssemblyName { get; set; } + + /// + /// The majorVersion portion of the file version for the assembly. This may or may not match the assembly version + /// + public int AssemblyFileMajorVersion { get; set; } + + /// + /// The minorVersion portion of the file version for the assembly file. This may or may not match the corresponding + /// part of the assembly version. + /// + public int AssemblyFileMinorVersion { get; set; } + + public override bool Equals(object obj) + { + var assembly = obj as AssemblyRecord; + if (assembly != null) + { + return string.Equals(assembly.Name, Name, StringComparison.OrdinalIgnoreCase) && + assembly.Version == Version; + } + + return false; + } + + public object Clone() + { + var copiedParents = new HashSet(); + foreach (var parent in _parents) + { + copiedParents.Add(parent.Clone() as AssemblyRecord); + } + + return new AssemblyRecord() + { + _parents = copiedParents, + AssemblyFileMajorVersion = AssemblyFileMajorVersion, + AssemblyFileMinorVersion = AssemblyFileMinorVersion, + AssemblyName = AssemblyName, + Location = Location + }; + + } + + /// + /// Compare the assembly record to an assembly name + /// + /// The assembly name to compare with + /// True if the assembly name is a reference to this assembly, otherwise false + public bool Equals(AssemblyName assembly) + { + return string.Equals(assembly.Name, Name, StringComparison.OrdinalIgnoreCase) && + assembly.Version == Version; + } + + public bool Equals(AssemblyRecord record) + { + return Equals(record.AssemblyName) + && AssemblyFileMajorVersion == record.AssemblyFileMajorVersion + && AssemblyFileMinorVersion == record.AssemblyFileMinorVersion; + } + + public override string ToString() + { + StringBuilder output = new StringBuilder(); + output.AppendLine(string.Format("AssemblyName: {0}, Version:{1}, FileVersion: {2}.{3}, Location:{4}", + Name, Version, AssemblyFileMajorVersion, AssemblyFileMinorVersion, Location)); + if (ReferencingAssembly.Any()) + { + output.AppendFormat("-> Parents: ({0})", string.Join(", ", ReferencingAssembly)); + } + + return output.ToString(); + + } + + /// + /// Get all the ancestors in the ancestor tree + /// + /// The full set of ancestors in the ancestor tree. + public HashSet GetAncestors() + { + var result = new HashSet(); + foreach (var parent in ReferencingAssembly) + { + result.Add(parent); + foreach (var grandParent in parent.GetAncestors()) + { + result.Add(grandParent); + } + + } + + return result; + } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/AssemblyVersionConflict.cs b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyVersionConflict.cs new file mode 100644 index 000000000000..7c54d20f5571 --- /dev/null +++ b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyVersionConflict.cs @@ -0,0 +1,82 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// Indicates a conflict between assembly reference and actual assembly in a directory + /// + public class AssemblyVersionConflict : IReportRecord + { + /// + /// The directory containing a conflict + /// + public string Directory { get; set; } + + /// + /// The name of the assembly + /// + public string AssemblyName { get; set; } + + /// + /// The version referenced in the parent assembly manifest + /// + public Version ExpectedVersion { get; set; } + + /// + /// The version of the assembly on disk + /// + public Version ActualVersion { get; set; } + + /// + /// The identity of the parent assembly + /// + public string ParentAssembly { get; set; } + + /// + /// A textual description of the problem + /// + public string Description { get; set; } + + /// + /// A textual description of steps to remediate the issue + /// + public string Remediation { get; set; } + + public int Severity { get; set; } + + public string PrintHeaders() + { + return + "\"Directory\",\"AssemblyName\",\"Expected Version\",\"Actual Version\",\"Parent Assembly\",\"Severity\"," + + "\"Description\",\"Remediation\""; + } + + public string FormatRecord() + { + return + string.Format( + "\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\",\"{6}\",\"{7}\"", + Directory, AssemblyName, ExpectedVersion, ActualVersion, ParentAssembly, Severity, + Description, Remediation); + } + + public override string ToString() + { + return FormatRecord(); + } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/DependencyAnalyzer.cs b/tools/StaticAnalysis/DependencyAnalyzer/DependencyAnalyzer.cs new file mode 100644 index 000000000000..829b78e3e343 --- /dev/null +++ b/tools/StaticAnalysis/DependencyAnalyzer/DependencyAnalyzer.cs @@ -0,0 +1,331 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// The static analysis tool for ensuring no runtime conflicts in assembly dependencies + /// + public class DependencyAnalyzer : IStaticAnalyzer + { + private Dictionary _assemblies = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private Dictionary _sharedAssemblyReferences = + new Dictionary(new AssemblyNameComparer()); + private Dictionary _identicalSharedAssemblies = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + private AppDomain _testDomain; + private AssemblyLoader _loader; + private ReportLogger _versionConflictLogger; + private ReportLogger _sharedConflictLogger; + private ReportLogger _missingAssemblyLogger; + private ReportLogger _extraAssemblyLogger; + + public DependencyAnalyzer() + { + Name = "Dependency Analyzer"; + } + + public AnalysisLogger Logger { get; set; } + public string Name { get; private set; } + + public void Analyze(IEnumerable directories) + { + if (directories == null) + { + throw new ArgumentNullException("directories"); + } + + _versionConflictLogger = Logger.CreateLogger("AssemblyVersionConflict.csv"); + _sharedConflictLogger = Logger.CreateLogger("SharedAssemblyConflict.csv"); + _missingAssemblyLogger = Logger.CreateLogger("MissingAssemblies.csv"); + _extraAssemblyLogger = Logger.CreateLogger("ExtraAssemblies.csv"); + foreach (var baseDirectory in directories) + { + foreach (var directoryPath in Directory.EnumerateDirectories(baseDirectory)) + { + if (!Directory.Exists(directoryPath)) + { + throw new InvalidOperationException("Please pass a valid directory name as the first parameter"); + } + + Logger.WriteMessage("Processing Directory {0}", directoryPath); + _assemblies.Clear(); + _versionConflictLogger.Decorator.AddDecorator(r => { r.Directory = directoryPath; }, "Directory"); + _missingAssemblyLogger.Decorator.AddDecorator(r => { r.Directory = directoryPath; }, "Directory"); + _extraAssemblyLogger.Decorator.AddDecorator(r => { r.Directory = directoryPath; }, "Directory"); + ProcessDirectory(directoryPath); + _versionConflictLogger.Decorator.Remove("Directory"); + _missingAssemblyLogger.Decorator.Remove("Directory"); + _extraAssemblyLogger.Decorator.Remove("Directory"); + } + } + } + + private AssemblyRecord CreateAssemblyRecord(string path) + { + AssemblyRecord result = null; + var fullPath = Path.GetFullPath(path); + try + { + var assembly = LoadByReflectionFromFile(fullPath); + var versionInfo = FileVersionInfo.GetVersionInfo(fullPath); + result = new AssemblyRecord() + { + AssemblyName = assembly.GetName(), + AssemblyFileMajorVersion = versionInfo.FileMajorPart, + AssemblyFileMinorVersion = versionInfo.FileMinorPart, + Location = fullPath + }; + + foreach (var child in assembly.GetReferencedAssemblies()) + { + result.Children.Add(child); + } + } + catch + { + Logger.WriteError("Error loading assembly {0}", fullPath); + } + + return result; + } + + private bool AddSharedAssembly(AssemblyRecord assembly) + { + if (_sharedAssemblyReferences.ContainsKey(assembly.AssemblyName)) + { + var stored = _sharedAssemblyReferences[assembly.AssemblyName]; + if (!assembly.Equals(stored) && !(IsFrameworkAssembly(assembly.AssemblyName) && assembly.Version.Major <= 4)) + { + _sharedConflictLogger.LogRecord(new SharedAssemblyConflict + { + AssemblyName = assembly.Name, + AssemblyPathsAndFileVersions = new List>() + { + new Tuple(assembly.Location, new Version(assembly.AssemblyFileMajorVersion, + assembly.AssemblyFileMinorVersion)), + new Tuple(stored.Location, new Version(stored.AssemblyFileMajorVersion, + stored.AssemblyFileMinorVersion)) + + }, + AssemblyVersion = assembly.Version, + Severity = 0, + Description = "Shared assembly conflict, shared assemblies with the same assembly " + + "version have differing file versions", + Remediation = string.Format("Update the assembly reference for {0} in one of the " + + "referring assemblies", assembly.Name) + }); + + return false; + } + } + else + { + _sharedAssemblyReferences[assembly.AssemblyName] = assembly; + } + + return true; + } + + private AssemblyMetadata LoadByReflectionFromFile(string assemblyPath) + { + var info = _loader.GetReflectedAssemblyFromFile(assemblyPath); + if (info == null) + { + throw new InvalidOperationException(); + } + + return info; + } + + private bool AddSharedAssemblyExactVersion(AssemblyRecord record) + { + if (_identicalSharedAssemblies.ContainsKey(record.Name)) + { + var stored = _identicalSharedAssemblies[record.Name]; + if (!record.Equals(stored) && !(IsFrameworkAssembly(record.AssemblyName))) + { + _sharedConflictLogger.LogRecord(new SharedAssemblyConflict + { + AssemblyName = record.Name, + AssemblyVersion = record.Version, + Severity = 0, + AssemblyPathsAndFileVersions = new List>() + { + new Tuple(record.Location, new Version(record.AssemblyFileMajorVersion, + record.AssemblyFileMinorVersion)), + new Tuple(stored.Location, new Version(stored.AssemblyFileMajorVersion, + stored.AssemblyFileMinorVersion)), + }, + Description = string.Format("Assembly {0} has multiple versions as specified in 'Target'", + record.Name), + Remediation = string.Format("Ensure that all packages reference exactly the same package " + + "version of {0}", record.Name) + + }); + + return false; + } + } + else + { + _identicalSharedAssemblies[record.Name] = record; + } + + return true; + } + + private static bool RequiresExactVersionMatch(AssemblyRecord name) + { + return name.Name.Contains("Microsoft.Azure.Common.Authentication"); + } + + private static bool IsFrameworkAssembly(AssemblyName name) + { + return name.Name.StartsWith("System") || name.Name.Equals("mscorlib"); + } + + private void ProcessDirectory(string directoryPath) + { + var savedDirectory = Directory.GetCurrentDirectory(); + Directory.SetCurrentDirectory(directoryPath); + _loader = AssemblyLoader.Create(directoryPath, out _testDomain); + foreach (var file in Directory.GetFiles(directoryPath).Where(file => file.EndsWith(".dll"))) + { + AssemblyRecord assembly = CreateAssemblyRecord(file); + _assemblies[assembly.Name] = assembly; + if (RequiresExactVersionMatch(assembly)) + { + AddSharedAssemblyExactVersion(assembly); + } + else + { + AddSharedAssembly(assembly); + } + } + + // Now check for assembly mismatches + foreach (var assembly in _assemblies.Values) + { + foreach (var reference in assembly.Children) + { + CheckAssemblyReference(reference, assembly); + } + } + + FindExtraAssemblies(); + + AppDomain.Unload(_testDomain); + Directory.SetCurrentDirectory(savedDirectory); + } + + private static bool IsCommandAssembly(AssemblyRecord assembly) + { + return assembly.Name.Contains("Commands") || assembly.Name.Contains("Cmdlets"); + } + + private void FindExtraAssemblies() + { + if (_assemblies.Values.Any(a => !IsCommandAssembly(a) && (a.ReferencingAssembly == null + || a.ReferencingAssembly.Count == 0 || !a.GetAncestors().Any(IsCommandAssembly)))) + { + foreach ( + var assembly in + _assemblies.Values.Where(a => !IsCommandAssembly(a) && (a.ReferencingAssembly == null || + a.ReferencingAssembly.Count == 0 || !a.GetAncestors().Any(IsCommandAssembly)))) + { + _extraAssemblyLogger.LogRecord(new ExtraAssembly + { + AssemblyName = assembly.Name, + Severity = 2, + Description = string.Format("Assembly {0} is not referenced from any cmdlets assembly", + assembly.Name), + Remediation = string.Format("Remove assembly {0} from the project and regenerate the Wix " + + "file", assembly.Name) + }); + } + } + } + + private void CheckAssemblyReference(AssemblyName reference, AssemblyRecord parent) + { + if (_assemblies.ContainsKey(reference.Name)) + { + var stored = _assemblies[reference.Name]; + if (stored.Equals(reference)) + { + stored.ReferencingAssembly.Add(parent); + } + else if (reference.Version.Major == 0 && reference.Version.Minor == 0) + { + Logger.WriteWarning("{0}.dll has reference to assembly {1} without any version specification.", + parent.Name, reference.Name); + _versionConflictLogger.LogRecord(new AssemblyVersionConflict() + { + AssemblyName = reference.Name, + ActualVersion = stored.Version, + ExpectedVersion = reference.Version, + ParentAssembly = parent.Name, + Severity = 2, + Description = string.Format("Assembly {0} referenced from {1}.dll does not specify any " + + "assembly version evidence. The assembly will use version " + + "{2} from disk.", reference.Name, parent.Name, stored.Version), + Remediation = string.Format("Update the reference to assembly {0} from {1} so that " + + "assembly version evidence is supplied", reference.Name, + parent.Name) + }); + } + else + { + var minVersion = (stored.Version < reference.Version) ? stored.Version : reference.Version; + _versionConflictLogger.LogRecord(new AssemblyVersionConflict() + { + AssemblyName = reference.Name, + ActualVersion = stored.Version, + ExpectedVersion = reference.Version, + ParentAssembly = parent.Name, + Severity = 1, + Description = string.Format("Assembly {0} version {1} referenced from {2}.dll does " + + "not match assembly version on disk: {3}", + reference.Name, reference.Version, parent.Name, stored.Version), + Remediation = string.Format("Update any references to version {0} of assembly {1}", + minVersion, reference.Name) + }); + } + } + else if (!IsFrameworkAssembly(reference)) + { + _missingAssemblyLogger.LogRecord(new MissingAssembly + { + AssemblyName = reference.Name, + AssemblyVersion = reference.Version.ToString(), + ReferencingAssembly = parent.Name, + Severity = 0, + Description = string.Format("Missing assembly {0} referenced from {1}", reference.Name, + parent.Name), + Remediation = "Ensure that the assembly is included in the Wix file or directory" + }); + } + } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/ExtraAssembly.cs b/tools/StaticAnalysis/DependencyAnalyzer/ExtraAssembly.cs new file mode 100644 index 000000000000..677bb9271054 --- /dev/null +++ b/tools/StaticAnalysis/DependencyAnalyzer/ExtraAssembly.cs @@ -0,0 +1,49 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// Record for reporting assemblies that are not used by a Cmdlet assembly or its dependencies + /// + public class ExtraAssembly : IReportRecord + { + public string Directory { get; set; } + + public string AssemblyName { get; set; } + + public int Severity { get; set; } + + public string Description { get; set; } + + public string Remediation { get; set; } + + + public string PrintHeaders() + { + return "\"Directory\",\"AssemblyName\",\"Severity\",\"Description\",\"Remediation\""; + } + + public string FormatRecord() + { + return string.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\"", + Directory, AssemblyName, Severity, Description, Remediation); + } + + public override string ToString() + { + return FormatRecord(); + } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/MissingAssembly.cs b/tools/StaticAnalysis/DependencyAnalyzer/MissingAssembly.cs new file mode 100644 index 000000000000..ea73fd482404 --- /dev/null +++ b/tools/StaticAnalysis/DependencyAnalyzer/MissingAssembly.cs @@ -0,0 +1,48 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// Record to indicate an assembly that is not in the dependency tree for a cmdlet assembly + /// + public class MissingAssembly : IReportRecord + { + public string Directory { get; set; } + public string AssemblyName { get; set; } + public string AssemblyVersion { get; set; } + public string ReferencingAssembly { get; set; } + public string Description { get; set; } + public string Remediation { get; set; } + public int Severity { get; set; } + + public string PrintHeaders() + { + return "\"Directory\",\"Assembly Name\",\"Assembly Version\",\"Referencing Assembly\"," + + "\"Severity\",\"Description\",\"Remediation\""; + } + + public string FormatRecord() + { + return string.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\",\"{6}\"", + Directory, AssemblyName, AssemblyVersion, ReferencingAssembly, + Severity, Description, Remediation); + } + + public override string ToString() + { + return FormatRecord(); + } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/SharedAssemblyConflict.cs b/tools/StaticAnalysis/DependencyAnalyzer/SharedAssemblyConflict.cs new file mode 100644 index 000000000000..f2b8b0b9b4e6 --- /dev/null +++ b/tools/StaticAnalysis/DependencyAnalyzer/SharedAssemblyConflict.cs @@ -0,0 +1,53 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// Record to indicate a difference in assembly file versions for shared assemblies with the same + /// assembly version. This could result in unexpected behavior, depending on the assembly load order. + /// + public class SharedAssemblyConflict : IReportRecord + { + public string AssemblyName { get; set; } + public Version AssemblyVersion { get; set; } + public List> AssemblyPathsAndFileVersions { get; set; } + public string Description { get; set; } + public string Remediation { get; set; } + public int Severity { get; set; } + + public string PrintHeaders() + { + return "\"Target\",\"AssemblyName\",\"AssemblyVersion\",\"Severity\",\"Description\",\"Remediation\""; + } + + public string FormatRecord() + { + var targets = + AssemblyPathsAndFileVersions.Select(s => string.Format("File version {0} in {1}", s.Item2, s.Item1)); + var targetString = string.Join(", ", targets); + return string.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\"", targetString, AssemblyName, + AssemblyVersion, Severity, Description, Remediation); + } + + public override string ToString() + { + return FormatRecord(); + } + } +} diff --git a/tools/StaticAnalysis/IReportRecord.cs b/tools/StaticAnalysis/IReportRecord.cs new file mode 100644 index 000000000000..ab0c97f50171 --- /dev/null +++ b/tools/StaticAnalysis/IReportRecord.cs @@ -0,0 +1,28 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace StaticAnalysis +{ + /// + /// Abstract interface for static analysis reports. + /// + public interface IReportRecord + { + string Description { get; set; } + string Remediation { get; set; } + int Severity { get; set; } + string PrintHeaders(); + string FormatRecord(); + } +} diff --git a/tools/StaticAnalysis/IStaticAnalyzer.cs b/tools/StaticAnalysis/IStaticAnalyzer.cs new file mode 100644 index 000000000000..8eb55a0ec43b --- /dev/null +++ b/tools/StaticAnalysis/IStaticAnalyzer.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace StaticAnalysis +{ + /// + /// Defines the public object model for a static analysis tool + /// + public interface IStaticAnalyzer + { + /// + /// The logger where validation records should be written + /// + AnalysisLogger Logger { get; set; } + + /// + /// The display name of the Analyzer + /// + string Name { get; } + + /// + /// Validate the given assembly in the given directory + /// + /// The analysis targets + void Analyze(IEnumerable scopes); + } +} diff --git a/tools/StaticAnalysis/Program.cs b/tools/StaticAnalysis/Program.cs new file mode 100644 index 000000000000..e4c94f164179 --- /dev/null +++ b/tools/StaticAnalysis/Program.cs @@ -0,0 +1,68 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; + +namespace StaticAnalysis.DependencyAnalyzer +{ + /// + /// Runner for all static analysis tools. + /// + public class Program + { + public static void Main(string[] args) + { + if (args == null || args.Length < 1) + { + throw new InvalidOperationException("Please pass a valid directory name as the first parameter"); + } + + var installDir = args[0]; + if (!Directory.Exists(installDir)) + { + throw new InvalidOperationException("You must pass a valid directory as the first parameter"); + } + + var directories = new List + { + Path.Combine(installDir, @"ResourceManager\AzureResourceManager\"), + Path.Combine(installDir, @"ServiceManagement\Azure\") + }; + + var reportsDirectory = Directory.GetCurrentDirectory(); + bool logReportsDirectoryWarning = true; + if (args.Length > 1 && Directory.Exists(args[1])) + { + reportsDirectory = args[1]; + logReportsDirectoryWarning = false; + } + + var logger = new ConsoleLogger(reportsDirectory); + + if (logReportsDirectoryWarning) + { + logger.WriteWarning("No logger specified in the second parameter, writing reports to {0}", + reportsDirectory); + } + + var analyzer = new DependencyAnalyzer { Logger = logger }; + logger.WriteMessage("Executing analyzer: {0}", analyzer.Name); + analyzer.Analyze(directories); + logger.WriteReports(); + logger.WriteMessage("Processing complete for analyzer: {0}", analyzer.Name); + } + } +} diff --git a/tools/StaticAnalysis/Properties/AssemblyInfo.cs b/tools/StaticAnalysis/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..8ae4b806c636 --- /dev/null +++ b/tools/StaticAnalysis/Properties/AssemblyInfo.cs @@ -0,0 +1,50 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DependencyChecker")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DependencyChecker")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4c8c8997-993d-4351-b7bc-ab423356fd7f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tools/StaticAnalysis/ReportLogger.cs b/tools/StaticAnalysis/ReportLogger.cs new file mode 100644 index 000000000000..c836be697c1c --- /dev/null +++ b/tools/StaticAnalysis/ReportLogger.cs @@ -0,0 +1,83 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; + +namespace StaticAnalysis +{ + /// + /// Abstract report logger - used as an abstraction over typed loggers. + /// + public abstract class ReportLogger + { + private AnalysisLogger _parent; + private string _outputFile; + public ReportLogger(string fileName, AnalysisLogger parent) + { + _parent = parent; + _outputFile = fileName; + } + + protected AnalysisLogger ParentLogger { get { return _parent; } } + public string FileName { get { return _outputFile; } } + public abstract IList Records { get; } + + public virtual void WriteError(string error) + { + ParentLogger.WriteError(error); + } + + public virtual void WriteMessage(string message) + { + ParentLogger.WriteMessage(message); + } + + public virtual void WriteWarning(string message) + { + ParentLogger.WriteWarning(message); + } + } + + /// + /// A typed report logger + /// + /// The type of the report this logger will log. + public class ReportLogger : ReportLogger where T : IReportRecord, new() + { + public ReportLogger(string fileName, AnalysisLogger logger) + : base(fileName, logger) + { + Decorator = Decorator.Create(); + } + + private IList _records = new List(); + public Decorator Decorator { get; protected set; } + + /// + /// Log a record to the report + /// + /// + public void LogRecord(T record) + { + Decorator.Apply(record); + _records.Add(record); + } + + public override IList Records + { + get { return _records.Select(r => r as IReportRecord).ToList(); } + } + } +} diff --git a/tools/StaticAnalysis/Static Analysis.csproj b/tools/StaticAnalysis/Static Analysis.csproj new file mode 100644 index 000000000000..63c60ce4ec69 --- /dev/null +++ b/tools/StaticAnalysis/Static Analysis.csproj @@ -0,0 +1,70 @@ + + + + + Debug + AnyCPU + {68384B59-BA0C-4B7B-B3F6-9C7988296C16} + Exe + Properties + StaticAnalysis + StaticAnalysis + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/StaticAnalysis/Static Analysis.sln b/tools/StaticAnalysis/Static Analysis.sln new file mode 100644 index 000000000000..8b59a0384e02 --- /dev/null +++ b/tools/StaticAnalysis/Static Analysis.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.40629.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Static Analysis", "Static Analysis.csproj", "{68384B59-BA0C-4B7B-B3F6-9C7988296C16}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {68384B59-BA0C-4B7B-B3F6-9C7988296C16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68384B59-BA0C-4B7B-B3F6-9C7988296C16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68384B59-BA0C-4B7B-B3F6-9C7988296C16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68384B59-BA0C-4B7B-B3F6-9C7988296C16}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal