// // ResolveAssemblyReference.cs: Searches for assembly files. // // Author: // Marek Sieradzki (marek.sieradzki@gmail.com) // Ankit Jain (jankit@novell.com) // // (C) 2006 Marek Sieradzki // Copyright 2009 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Security; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace Microsoft.Build.Tasks { public class ResolveAssemblyReference : TaskExtension { bool autoUnify; ITaskItem[] assemblyFiles; ITaskItem[] assemblies; string appConfigFile; string[] allowedAssemblyExtensions; string[] allowedRelatedFileExtensions; string[] candidateAssemblyFiles; ITaskItem[] copyLocalFiles; ITaskItem[] filesWritten; bool findDependencies; bool findRelatedFiles; bool findSatellites; bool findSerializationAssemblies; ITaskItem[] relatedFiles; ITaskItem[] resolvedDependencyFiles; ITaskItem[] resolvedFiles; ITaskItem[] satelliteFiles; ITaskItem[] scatterFiles; string[] searchPaths; ITaskItem[] serializationAssemblyFiles; bool silent; string stateFile; ITaskItem[] suggestedRedirects; string[] targetFrameworkDirectories; string targetProcessorArchitecture; static string [] default_assembly_extensions; AssemblyResolver assembly_resolver; List dependency_search_paths; Dictionary assemblyNameToResolvedRef; Dictionary tempSatelliteFiles, tempRelatedFiles, tempResolvedDepFiles, tempCopyLocalFiles; List tempResolvedFiles; List primaryReferences; Dictionary alreadyScannedAssemblyNames; Dictionary conflictWarningsCache; //FIXME: construct and use a graph of the dependencies, useful across projects static ResolveAssemblyReference () { default_assembly_extensions = new string [] { ".dll", ".exe" }; } public ResolveAssemblyReference () { assembly_resolver = new AssemblyResolver (); } //FIXME: make this reusable public override bool Execute () { if (assemblies == null && assemblyFiles == null) // nothing to resolve return true; LogTaskParameters (); assembly_resolver.Log = Log; tempResolvedFiles = new List (); tempCopyLocalFiles = new Dictionary (); tempSatelliteFiles = new Dictionary (); tempRelatedFiles = new Dictionary (); tempResolvedDepFiles = new Dictionary (); primaryReferences = new List (); assemblyNameToResolvedRef = new Dictionary (); conflictWarningsCache = new Dictionary (); ResolveAssemblies (); ResolveAssemblyFiles (); resolvedFiles = tempResolvedFiles.ToArray (); alreadyScannedAssemblyNames = new Dictionary (); // the first element is place holder for parent assembly's dir dependency_search_paths = new List () { String.Empty }; dependency_search_paths.AddRange (searchPaths); // resolve dependencies foreach (PrimaryReference pref in primaryReferences) ResolveAssemblyFileDependencies (pref.TaskItem, pref.ParentCopyLocal); copyLocalFiles = tempCopyLocalFiles.Values.ToArray (); satelliteFiles = tempSatelliteFiles.Values.ToArray (); relatedFiles = tempRelatedFiles.Values.ToArray (); resolvedDependencyFiles = tempResolvedDepFiles.Values.ToArray (); #if XBUILD_14 DependsOnSystemRuntime = resolvedDependencyFiles.Any (x => Path.GetFileName (x.ItemSpec) == "System.Runtime.dll").ToString (); #endif tempResolvedFiles.Clear (); tempCopyLocalFiles.Clear (); tempSatelliteFiles.Clear (); tempRelatedFiles.Clear (); tempResolvedDepFiles.Clear (); alreadyScannedAssemblyNames.Clear (); primaryReferences.Clear (); assemblyNameToResolvedRef.Clear (); conflictWarningsCache.Clear (); dependency_search_paths = null; return true; } void ResolveAssemblies () { if (assemblies == null || assemblies.Length == 0) return; foreach (ITaskItem item in assemblies) { if (!String.IsNullOrEmpty (item.GetMetadata ("SubType"))) { Log.LogWarning ("Reference '{0}' has non-empty SubType. Ignoring.", item.ItemSpec); continue; } LogWithPrecedingNewLine (MessageImportance.Low, "Primary Reference {0}", item.ItemSpec); ResolvedReference resolved_ref = ResolveReference (item, searchPaths, true); if (resolved_ref == null) resolved_ref = ResolveWithAlternateName (item, allowedAssemblyExtensions ?? default_assembly_extensions); if (resolved_ref == null) { Log.LogWarning ("Reference '{0}' not resolved", item.ItemSpec); assembly_resolver.LogSearchLoggerMessages (MessageImportance.Normal); } else { Log.LogMessage (MessageImportance.Low, "\tReference {0} resolved to {1}. CopyLocal = {2}", item.ItemSpec, resolved_ref.TaskItem, resolved_ref.TaskItem.GetMetadata ("CopyLocal")); Log.LogMessage (MessageImportance.Low, "\tReference found at search path {0}", resolved_ref.FoundInSearchPathAsString); assembly_resolver.LogSearchLoggerMessages (MessageImportance.Low); if (TryAddNewReference (tempResolvedFiles, resolved_ref) && !IsFromGacOrTargetFramework (resolved_ref) && resolved_ref.FoundInSearchPath != SearchPath.PkgConfig) { primaryReferences.Add (new PrimaryReference ( resolved_ref.TaskItem, resolved_ref.TaskItem.GetMetadata ("CopyLocal"))); } } } } ResolvedReference ResolveWithAlternateName (ITaskItem item, string[] extensions) { foreach (string extn in extensions) { if (item.ItemSpec.EndsWith (extn)) { ITaskItem altitem = new TaskItem (item.ItemSpec.Substring (0, item.ItemSpec.Length - extn.Length)); item.CopyMetadataTo (altitem); return ResolveReference (altitem, searchPaths, true); } } return null; } // Use @search_paths to resolve the reference ResolvedReference ResolveReference (ITaskItem item, IEnumerable search_paths, bool set_copy_local) { ResolvedReference resolved = null; bool specific_version; assembly_resolver.ResetSearchLogger (); if (!TryGetSpecificVersionValue (item, out specific_version)) return null; var spath_index = 0; foreach (string spath in search_paths) { if (string.IsNullOrEmpty (spath)) continue; assembly_resolver.LogSearchMessage ("For searchpath {0}", spath); // The first value of search_paths can be the parent assembly directory. // In that case the value would be treated as a directory. // This code checks if we should treat the value as a TargetFramework assembly. // Doing so avoids CopyLocal beeing set to true. if (spath_index++ == 0 && targetFrameworkDirectories != null) { foreach (string fpath in targetFrameworkDirectories) { if (string.IsNullOrEmpty (fpath)) continue; if (String.Compare ( Path.GetFullPath (spath).TrimEnd (Path.DirectorySeparatorChar), Path.GetFullPath (fpath).TrimEnd (Path.DirectorySeparatorChar), StringComparison.InvariantCulture) != 0) continue; resolved = assembly_resolver.FindInTargetFramework (item, fpath, specific_version); break; } if (resolved != null) break; } if (String.Compare (spath, "{HintPathFromItem}") == 0) { resolved = assembly_resolver.ResolveHintPathReference (item, specific_version); } else if (String.Compare (spath, "{TargetFrameworkDirectory}") == 0) { if (targetFrameworkDirectories == null) continue; foreach (string fpath in targetFrameworkDirectories) { resolved = assembly_resolver.FindInTargetFramework (item, fpath, specific_version); if (resolved != null) break; } } else if (String.Compare (spath, "{GAC}") == 0) { resolved = assembly_resolver.ResolveGacReference (item, specific_version); } else if (String.Compare (spath, "{RawFileName}") == 0) { //FIXME: identify assembly names, as extract the name, and try with that? AssemblyName aname; if (assembly_resolver.TryGetAssemblyNameFromFile (item.ItemSpec, out aname)) resolved = assembly_resolver.GetResolvedReference (item, item.ItemSpec, aname, true, SearchPath.RawFileName); } else if (String.Compare (spath, "{CandidateAssemblyFiles}") == 0) { assembly_resolver.LogSearchMessage ( "Warning: {{CandidateAssemblyFiles}} not supported currently"); } else if (String.Compare (spath, "{PkgConfig}") == 0) { resolved = assembly_resolver.ResolvePkgConfigReference (item, specific_version); } else { resolved = assembly_resolver.FindInDirectory ( item, spath, allowedAssemblyExtensions ?? default_assembly_extensions, specific_version); } if (resolved != null) break; } if (resolved != null && set_copy_local) SetCopyLocal (resolved.TaskItem, resolved.CopyLocal.ToString ()); return resolved; } bool TryGetSpecificVersionValue (ITaskItem item, out bool specific_version) { specific_version = true; string value = item.GetMetadata ("SpecificVersion"); if (String.IsNullOrEmpty (value)) { //AssemblyName name = new AssemblyName (item.ItemSpec); // If SpecificVersion is not specified, then // it is true if the Include is a strong name else false //specific_version = assembly_resolver.IsStrongNamed (name); // msbuild seems to just look for a ',' in the name :/ specific_version = item.ItemSpec.IndexOf (',') >= 0; return true; } if (Boolean.TryParse (value, out specific_version)) return true; Log.LogError ("Item '{0}' has attribute SpecificVersion with invalid value '{1}' " + "which could not be converted to a boolean.", item.ItemSpec, value); return false; } //FIXME: Consider CandidateAssemblyFiles also here void ResolveAssemblyFiles () { if (assemblyFiles == null) return; foreach (ITaskItem item in assemblyFiles) { assembly_resolver.ResetSearchLogger (); if (!File.Exists (item.ItemSpec)) { LogWithPrecedingNewLine (MessageImportance.Low, "Primary Reference from AssemblyFiles {0}, file not found. Ignoring", item.ItemSpec); continue; } LogWithPrecedingNewLine (MessageImportance.Low, "Primary Reference from AssemblyFiles {0}", item.ItemSpec); string copy_local; AssemblyName aname; if (!assembly_resolver.TryGetAssemblyNameFromFile (item.ItemSpec, out aname)) { Log.LogWarning ("Reference '{0}' not resolved", item.ItemSpec); assembly_resolver.LogSearchLoggerMessages (MessageImportance.Normal); continue; } assembly_resolver.LogSearchLoggerMessages (MessageImportance.Low); ResolvedReference rr = assembly_resolver.GetResolvedReference (item, item.ItemSpec, aname, true, SearchPath.RawFileName); copy_local = rr.CopyLocal.ToString (); if (!TryAddNewReference (tempResolvedFiles, rr)) // already resolved continue; SetCopyLocal (rr.TaskItem, copy_local); FindAndAddRelatedFiles (item.ItemSpec, copy_local); FindAndAddSatellites (item.ItemSpec, copy_local); if (FindDependencies && !IsFromGacOrTargetFramework (rr) && rr.FoundInSearchPath != SearchPath.PkgConfig) primaryReferences.Add (new PrimaryReference (item, copy_local)); } } // Tries to resolve assemblies referenced by @item // Skips gac references // @item : filename void ResolveAssemblyFileDependencies (ITaskItem item, string parent_copy_local) { Queue dependencies = new Queue (); dependencies.Enqueue (item.ItemSpec); while (dependencies.Count > 0) { string filename = Path.GetFullPath (dependencies.Dequeue ()); Assembly asm = Assembly.ReflectionOnlyLoadFrom (filename); if (alreadyScannedAssemblyNames.ContainsKey (asm.FullName)) continue; // set the 1st search path to this ref's base path // Will be used for resolving the dependencies dependency_search_paths [0] = Path.GetDirectoryName (filename); foreach (AssemblyName aname in asm.GetReferencedAssemblies ()) { if (alreadyScannedAssemblyNames.ContainsKey (aname.FullName)) continue; ResolvedReference resolved_ref = ResolveDependencyByAssemblyName ( aname, asm.FullName, parent_copy_local); if (IncludeDependencies (resolved_ref, aname.FullName)) { tempResolvedDepFiles[resolved_ref.AssemblyName.FullName] = resolved_ref.TaskItem; dependencies.Enqueue (resolved_ref.TaskItem.ItemSpec); } } alreadyScannedAssemblyNames.Add (asm.FullName, String.Empty); } } // Resolves by looking dependency_search_paths // which is dir of parent reference file, and // SearchPaths ResolvedReference ResolveDependencyByAssemblyName (AssemblyName aname, string parent_asm_name, string parent_copy_local) { // This will check for compatible assembly name/version ResolvedReference resolved_ref; if (TryGetResolvedReferenceByAssemblyName (aname, false, out resolved_ref)) return resolved_ref; LogWithPrecedingNewLine (MessageImportance.Low, "Dependency {0}", aname); Log.LogMessage (MessageImportance.Low, "\tRequired by {0}", parent_asm_name); ITaskItem item = new TaskItem (aname.FullName); item.SetMetadata ("SpecificVersion", "false"); resolved_ref = ResolveReference (item, dependency_search_paths, false); if (resolved_ref != null) { Log.LogMessage (MessageImportance.Low, "\tReference {0} resolved to {1}.", aname, resolved_ref.TaskItem.ItemSpec); Log.LogMessage (MessageImportance.Low, "\tReference found at search path {0}", resolved_ref.FoundInSearchPathAsString); assembly_resolver.LogSearchLoggerMessages (MessageImportance.Low); if (resolved_ref.FoundInSearchPath == SearchPath.Directory) { // override CopyLocal with parent's val SetCopyLocal (resolved_ref.TaskItem, parent_copy_local); Log.LogMessage (MessageImportance.Low, "\tThis is CopyLocal {0} as parent item has this value", parent_copy_local); if (TryAddNewReference (tempResolvedFiles, resolved_ref)) { FindAndAddRelatedFiles (resolved_ref.TaskItem.ItemSpec, parent_copy_local); FindAndAddSatellites (resolved_ref.TaskItem.ItemSpec, parent_copy_local); } } else { //gac or tgtfmwk Log.LogMessage (MessageImportance.Low, "\tThis is CopyLocal false as it is in the GAC," + "target framework directory or provided by a package."); TryAddNewReference (tempResolvedFiles, resolved_ref); } } else { Log.LogMessage (MessageImportance.Low, "Could not resolve the assembly \"{0}\".", aname); assembly_resolver.LogSearchLoggerMessages (MessageImportance.Low); } return resolved_ref; } void FindAndAddRelatedFiles (string filename, string parent_copy_local) { if (!findRelatedFiles || allowedRelatedFileExtensions == null) return; foreach (string ext in allowedRelatedFileExtensions) { string rfile = Path.ChangeExtension (filename, ext); if (File.Exists (rfile)) { ITaskItem item = new TaskItem (rfile); SetCopyLocal (item, parent_copy_local); tempRelatedFiles.AddUniqueFile (item); } } } void FindAndAddSatellites (string filename, string parent_copy_local) { if (!FindSatellites) return; string basepath = Path.GetDirectoryName (filename); string resource = String.Format ("{0}{1}{2}", Path.GetFileNameWithoutExtension (filename), ".resources", Path.GetExtension (filename)); string dir_sep = Path.DirectorySeparatorChar.ToString (); foreach (string dir in Directory.GetDirectories (basepath)) { string culture = Path.GetFileName (dir); if (!CultureNamesTable.ContainsKey (culture)) continue; string res_path = Path.Combine (dir, resource); if (File.Exists (res_path)) { ITaskItem item = new TaskItem (res_path); SetCopyLocal (item, parent_copy_local); item.SetMetadata ("DestinationSubdirectory", culture + dir_sep); tempSatelliteFiles.AddUniqueFile (item); } } } // returns true is it was new bool TryAddNewReference (List file_list, ResolvedReference key_ref) { ResolvedReference found_ref; if (!TryGetResolvedReferenceByAssemblyName (key_ref.AssemblyName, key_ref.IsPrimary, out found_ref)) { assemblyNameToResolvedRef [key_ref.AssemblyName.Name] = key_ref; file_list.Add (key_ref.TaskItem); return true; } return false; } void SetCopyLocal (ITaskItem item, string copy_local) { item.SetMetadata ("CopyLocal", copy_local); // Assumed to be valid value if (Boolean.Parse (copy_local)) tempCopyLocalFiles.AddUniqueFile (item); } bool TryGetResolvedReferenceByAssemblyName (AssemblyName key_aname, bool is_primary, out ResolvedReference found_ref) { found_ref = null; // Match by just name if (!assemblyNameToResolvedRef.TryGetValue (key_aname.Name, out found_ref)) // not there return false; // match for full name if (AssemblyResolver.AssemblyNamesCompatible (key_aname, found_ref.AssemblyName, true, false)) // exact match, so its already there, dont add anything return true; // we have a name match, but version mismatch! assembly_resolver.LogSearchMessage ("A conflict was detected between '{0}' and '{1}'", key_aname.FullName, found_ref.AssemblyName.FullName); if (is_primary == found_ref.IsPrimary) { assembly_resolver.LogSearchMessage ("Unable to choose between the two. " + "Choosing '{0}' arbitrarily.", found_ref.AssemblyName.FullName); return true; } // since all dependencies are processed after // all primary refererences, the one in the cache // has to be a primary // Prefer a primary reference over a dependency assembly_resolver.LogSearchMessage ("Choosing '{0}' as it is a primary reference.", found_ref.AssemblyName.FullName); // If we can successfully use the primary reference, don't log a warning. It's too // verbose. //LogConflictWarning (found_ref.AssemblyName.FullName, key_aname.FullName); return true; } void LogWithPrecedingNewLine (MessageImportance importance, string format, params object [] args) { Log.LogMessage (importance, String.Empty); Log.LogMessage (importance, format, args); } // conflict b/w @main and @conflicting, picking @main void LogConflictWarning (string main, string conflicting) { string key = main + ":" + conflicting; if (!conflictWarningsCache.ContainsKey (key)) { Log.LogWarning ("Found a conflict between : '{0}' and '{1}'. Using '{0}' reference.", main, conflicting); conflictWarningsCache [key] = key; } } bool IsFromGacOrTargetFramework (ResolvedReference rr) { return rr.FoundInSearchPath == SearchPath.Gac || rr.FoundInSearchPath == SearchPath.TargetFrameworkDirectory; } bool IncludeDependencies (ResolvedReference rr, string aname) { if (rr == null) return false; if (aname.Equals ("System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")) return true; return !IsFromGacOrTargetFramework (rr) && rr.FoundInSearchPath != SearchPath.PkgConfig; } void LogTaskParameters () { Log.LogMessage (MessageImportance.Low, "TargetFrameworkDirectories:"); if (TargetFrameworkDirectories != null) foreach (string dir in TargetFrameworkDirectories) Log.LogMessage (MessageImportance.Low, "\t{0}", dir); Log.LogMessage (MessageImportance.Low, "SearchPaths:"); if (SearchPaths != null) foreach (string path in SearchPaths) Log.LogMessage (MessageImportance.Low, "\t{0}", path); } public bool AutoUnify { get { return autoUnify; } set { autoUnify = value; } } public ITaskItem[] AssemblyFiles { get { return assemblyFiles; } set { assemblyFiles = value; } } public ITaskItem[] Assemblies { get { return assemblies; } set { assemblies = value; } } public string AppConfigFile { get { return appConfigFile; } set { appConfigFile = value; } } public string[] AllowedAssemblyExtensions { get { return allowedAssemblyExtensions; } set { allowedAssemblyExtensions = value; } } public string[] AllowedRelatedFileExtensions { get { return allowedRelatedFileExtensions; } set { allowedRelatedFileExtensions = value; } } public string[] CandidateAssemblyFiles { get { return candidateAssemblyFiles; } set { candidateAssemblyFiles = value; } } [Output] public ITaskItem[] CopyLocalFiles { get { return copyLocalFiles; } } #if XBUILD_14 [Output] public string DependsOnSystemRuntime { get; private set; } #endif [Output] public ITaskItem[] FilesWritten { get { return filesWritten; } set { filesWritten = value; } } public bool FindDependencies { get { return findDependencies; } set { findDependencies = value; } } public bool FindRelatedFiles { get { return findRelatedFiles; } set { findRelatedFiles = value; } } public bool FindSatellites { get { return findSatellites; } set { findSatellites = value; } } public bool FindSerializationAssemblies { get { return findSerializationAssemblies; } set { findSerializationAssemblies = value; } } public ITaskItem[] InstalledAssemblyTables { get; set; } [Output] public ITaskItem[] RelatedFiles { get { return relatedFiles; } } [Output] public ITaskItem[] ResolvedDependencyFiles { get { return resolvedDependencyFiles; } } [Output] public ITaskItem[] ResolvedFiles { get { return resolvedFiles; } } [Output] public ITaskItem[] SatelliteFiles { get { return satelliteFiles; } } [Output] public ITaskItem[] ScatterFiles { get { return scatterFiles; } } [Required] public string[] SearchPaths { get { return searchPaths; } set { searchPaths = value; } } [Output] public ITaskItem[] SerializationAssemblyFiles { get { return serializationAssemblyFiles; } } public bool Silent { get { return silent; } set { silent = value; } } public string StateFile { get { return stateFile; } set { stateFile = value; } } [Output] public ITaskItem[] SuggestedRedirects { get { return suggestedRedirects; } } public string TargetFrameworkMoniker { get; set; } public string TargetFrameworkMonikerDisplayName { get; set; } public string TargetFrameworkVersion { get; set; } public string[] TargetFrameworkDirectories { get { return targetFrameworkDirectories; } set { targetFrameworkDirectories = value; } } public string TargetProcessorArchitecture { get { return targetProcessorArchitecture; } set { targetProcessorArchitecture = value; } } static Dictionary cultureNamesTable; static Dictionary CultureNamesTable { get { if (cultureNamesTable == null) { cultureNamesTable = new Dictionary (); foreach (CultureInfo ci in CultureInfo.GetCultures (CultureTypes.AllCultures)) cultureNamesTable [ci.Name] = ci.Name; } return cultureNamesTable; } } } static class ResolveAssemblyReferenceHelper { public static void AddUniqueFile (this Dictionary dic, ITaskItem item) { if (dic == null) throw new ArgumentNullException ("dic"); if (item == null) throw new ArgumentNullException ("item"); string fullpath = Path.GetFullPath (item.ItemSpec); if (!dic.ContainsKey (fullpath)) dic [fullpath] = item; } } struct PrimaryReference { public ITaskItem TaskItem; public string ParentCopyLocal; public PrimaryReference (ITaskItem item, string parent_copy_local) { TaskItem = item; ParentCopyLocal = parent_copy_local; } } }