using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using Mono.Cecil; using MonoTouch.Tuner; using ObjCRuntime; using Xamarin.Utils; #if MONOTOUCH using PlatformException = Xamarin.Bundler.MonoTouchException; #else using PlatformException = Xamarin.Bundler.MonoMacException; #endif namespace Xamarin.Bundler { public partial class Assembly { public List Satellites; public Application App { get { return Target.App; } } string full_path; bool? is_framework_assembly; public AssemblyDefinition AssemblyDefinition; public Target Target; public bool IsFrameworkAssembly { get { return is_framework_assembly.Value; } } public string FullPath { get { return full_path; } set { full_path = value; if (!is_framework_assembly.HasValue) { var real_full_path = Target.GetRealPath (full_path); is_framework_assembly = real_full_path.StartsWith (Path.GetDirectoryName (Path.GetDirectoryName (Target.Resolver.FrameworkDirectory)), StringComparison.Ordinal); } } } public string FileName { get { return Path.GetFileName (FullPath); } } public string Identity { get { return GetIdentity (FullPath); } } public static string GetIdentity (AssemblyDefinition ad) { return Path.GetFileNameWithoutExtension (ad.MainModule.FileName); } public static string GetIdentity (string path) { return Path.GetFileNameWithoutExtension (path); } public bool EnableCxx; public bool NeedsGccExceptionHandling; public bool ForceLoad; public HashSet Frameworks = new HashSet (); public HashSet WeakFrameworks = new HashSet (); public List LinkerFlags = new List (); // list of extra linker flags public List LinkWith = new List (); // list of paths to native libraries to link with, from LinkWith attributes public HashSet UnresolvedModuleReferences; public bool HasLinkWithAttributes { get; private set; } bool? symbols_loaded; List link_with_resources; // a list of resources that must be removed from the app public Assembly (Target target, string path) { this.Target = target; this.FullPath = path; } public Assembly (Target target, AssemblyDefinition definition) { this.Target = target; this.AssemblyDefinition = definition; this.FullPath = definition.MainModule.FileName; } public void LoadSymbols () { if (symbols_loaded.HasValue) return; symbols_loaded = false; try { var pdb = Path.ChangeExtension (FullPath, ".pdb"); if (File.Exists (pdb) || File.Exists (FullPath + ".mdb")) { AssemblyDefinition.MainModule.ReadSymbols (); symbols_loaded = true; } } catch { // do not let stale file crash us Driver.Log (3, "Invalid debugging symbols for {0} ignored", FullPath); } } void AddResourceToBeRemoved (string resource) { if (link_with_resources == null) link_with_resources = new List (); link_with_resources.Add (resource); } public void ExtractNativeLinkInfo () { // ignore framework assemblies, they won't have any LinkWith attributes if (IsFrameworkAssembly) return; var assembly = AssemblyDefinition; if (!assembly.HasCustomAttributes) return; var exceptions = new List (); string path; // // Tasks: // * Remove LinkWith attribute: this is done in the linker. // * Remove embedded resources related to LinkWith attribute from assembly: this is done at a later stage, // here we just compile a list of resources to remove. // * Extract embedded resources related to LinkWith attribute to a file // * Modify the linker flags used to build/link the dylib (if fastdev) or the main binary (if !fastdev) // for (int i = 0; i < assembly.CustomAttributes.Count; i++) { CustomAttribute attr = assembly.CustomAttributes[i]; if (attr.Constructor == null) continue; TypeReference type = attr.Constructor.DeclaringType; if (!type.IsPlatformType ("ObjCRuntime", "LinkWithAttribute")) continue; // Let the linker remove it the attribute from the assembly HasLinkWithAttributes = true; LinkWithAttribute linkWith = GetLinkWithAttribute (attr); string libraryName = linkWith.LibraryName; // Remove the resource from the assembly at a later stage. if (!string.IsNullOrEmpty (libraryName)) AddResourceToBeRemoved (libraryName); // We can't add -dead_strip if there are any LinkWith attributes where smart linking is disabled. if (!linkWith.SmartLink) App.DeadStrip = false; // Don't add -force_load if the binding's SmartLink value is set and the static registrar is being used. if (linkWith.ForceLoad && !(linkWith.SmartLink && App.Registrar == RegistrarMode.Static)) ForceLoad = true; if (!string.IsNullOrEmpty (linkWith.LinkerFlags)) { if (LinkerFlags == null) LinkerFlags = new List (); LinkerFlags.Add (linkWith.LinkerFlags); } if (!string.IsNullOrEmpty (linkWith.Frameworks)) { foreach (var f in linkWith.Frameworks.Split (new char[] { ' ' })) { if (Frameworks == null) Frameworks = new HashSet (); Frameworks.Add (f); } } if (!string.IsNullOrEmpty (linkWith.WeakFrameworks)) { foreach (var f in linkWith.WeakFrameworks.Split (new char[] { ' ' })) { if (WeakFrameworks == null) WeakFrameworks = new HashSet (); WeakFrameworks.Add (f); } } if (linkWith.NeedsGccExceptionHandling) NeedsGccExceptionHandling = true; if (linkWith.IsCxx) EnableCxx = true; #if MONOTOUCH if (linkWith.Dlsym != DlsymOption.Default) App.SetDlsymOption (FullPath, linkWith.Dlsym == DlsymOption.Required); #endif if (!string.IsNullOrEmpty (libraryName)) { path = Path.Combine (App.Cache.Location, libraryName); if (path.EndsWith (".framework", StringComparison.Ordinal)) { #if MONOTOUCH if (App.Platform == Xamarin.Utils.ApplePlatform.iOS && App.DeploymentTarget.Major < 8) { throw ErrorHelper.CreateError (1305, "The binding library '{0}' contains a user framework ({0}), but embedded user frameworks require iOS 8.0 (the deployment target is {1}). Please set the deployment target in the Info.plist file to at least 8.0.", FileName, Path.GetFileName (path), App.DeploymentTarget); } #endif var zipPath = path + ".zip"; if (!Application.IsUptodate (FullPath, zipPath)) { Application.ExtractResource (assembly.MainModule, libraryName, zipPath, false); Driver.Log (3, "Extracted third-party framework '{0}' from '{1}' to '{2}'", libraryName, FullPath, zipPath); LogLinkWithAttribute (linkWith); } else { Driver.Log (3, "Target '{0}' is up-to-date.", path); } if (!File.Exists (zipPath)) { ErrorHelper.Warning (1302, "Could not extract the native framework '{0}' from '{1}'. " + "Please ensure the native framework was properly embedded in the managed assembly " + "(if the assembly was built using a binding project, the native framework must be included in the project, and its Build Action must be 'ObjcBindingNativeFramework').", libraryName, zipPath); } else { if (!Directory.Exists (path)) Directory.CreateDirectory (path); if (Driver.RunCommand ("/usr/bin/unzip", string.Format ("-u -o -d {0} {1}", StringUtils.Quote (path), StringUtils.Quote (zipPath))) != 0) throw ErrorHelper.CreateError (1303, "Could not decompress the native framework '{0}' from '{1}'. Please review the build log for more information from the native 'unzip' command.", libraryName, zipPath); } Frameworks.Add (path); } else { if (!Application.IsUptodate (FullPath, path)) { Application.ExtractResource (assembly.MainModule, libraryName, path, false); Driver.Log (3, "Extracted third-party binding '{0}' from '{1}' to '{2}'", libraryName, FullPath, path); LogLinkWithAttribute (linkWith); } else { Driver.Log (3, "Target '{0}' is up-to-date.", path); } if (!File.Exists (path)) ErrorHelper.Warning (1302, "Could not extract the native library '{0}' from '{1}'. " + "Please ensure the native library was properly embedded in the managed assembly " + "(if the assembly was built using a binding project, the native library must be included in the project, and its Build Action must be 'ObjcBindingNativeLibrary').", libraryName, path); LinkWith.Add (path); } } } if (exceptions != null && exceptions.Count > 0) throw new AggregateException (exceptions); // Make sure there are no duplicates between frameworks and weak frameworks. // Keep the weak ones. if (Frameworks != null && WeakFrameworks != null) Frameworks.ExceptWith (WeakFrameworks); if (NeedsGccExceptionHandling) { if (LinkerFlags == null) LinkerFlags = new List (); LinkerFlags.Add ("-lgcc_eh"); } } static void LogLinkWithAttribute (LinkWithAttribute linkWith) { Driver.Log (3, " ForceLoad: {0}", linkWith.ForceLoad); Driver.Log (3, " Frameworks: {0}", linkWith.Frameworks); Driver.Log (3, " IsCxx: {0}", linkWith.IsCxx); Driver.Log (3, " LinkerFlags: {0}", linkWith.LinkerFlags); Driver.Log (3, " LinkTarget: {0}", linkWith.LinkTarget); Driver.Log (3, " NeedsGccExceptionHandling: {0}", linkWith.NeedsGccExceptionHandling); Driver.Log (3, " SmartLink: {0}", linkWith.SmartLink); Driver.Log (3, " WeakFrameworks: {0}", linkWith.WeakFrameworks); } public static LinkWithAttribute GetLinkWithAttribute (CustomAttribute attr) { LinkWithAttribute linkWith; var cargs = attr.ConstructorArguments; switch (cargs.Count) { case 3: linkWith = new LinkWithAttribute ((string) cargs [0].Value, (LinkTarget) cargs [1].Value, (string) cargs [2].Value); break; case 2: linkWith = new LinkWithAttribute ((string) cargs [0].Value, (LinkTarget) cargs [1].Value); break; case 0: linkWith = new LinkWithAttribute (); break; default: case 1: linkWith = new LinkWithAttribute ((string) cargs [0].Value); break; } foreach (var property in attr.Properties) { switch (property.Name) { case "NeedsGccExceptionHandling": linkWith.NeedsGccExceptionHandling = (bool) property.Argument.Value; break; case "WeakFrameworks": linkWith.WeakFrameworks = (string) property.Argument.Value; break; case "Frameworks": linkWith.Frameworks = (string) property.Argument.Value; break; case "LinkerFlags": linkWith.LinkerFlags = (string) property.Argument.Value; break; case "LinkTarget": linkWith.LinkTarget = (LinkTarget) property.Argument.Value; break; case "ForceLoad": linkWith.ForceLoad = (bool) property.Argument.Value; break; case "IsCxx": linkWith.IsCxx = (bool) property.Argument.Value; break; case "SmartLink": linkWith.SmartLink = (bool) property.Argument.Value; break; case "Dlsym": linkWith.Dlsym = (DlsymOption) property.Argument.Value; break; default: break; } } return linkWith; } void AddFramework (string file) { if (Driver.GetFrameworks (App).TryGetValue (file, out var framework) && framework.Version > App.SdkVersion) ErrorHelper.Warning (135, "Did not link system framework '{0}' (referenced by assembly '{1}') because it was introduced in {2} {3}, and we're using the {2} {4} SDK.", file, FileName, App.PlatformName, framework.Version, App.SdkVersion); else if (Frameworks.Add (file)) Driver.Log (3, "Linking with the framework {0} because it's referenced by a module reference in {1}", file, FileName); } public void ComputeLinkerFlags () { foreach (var m in AssemblyDefinition.Modules) { if (!m.HasModuleReferences) continue; foreach (var mr in m.ModuleReferences) { string name = mr.Name; if (string.IsNullOrEmpty (name)) continue; // obfuscated assemblies. string file = Path.GetFileNameWithoutExtension (name); switch (file) { // special case case "__Internal": // well known libs case "libc": case "libSystem": case "libobjc": case "libdyld": case "libsystem_kernel": break; case "sqlite3": LinkerFlags.Add ("-lsqlite3"); Driver.Log (3, "Linking with {0} because it's referenced by a module reference in {1}", file, FileName); break; case "libsqlite3": // remove lib prefix LinkerFlags.Add ("-l" + file.Substring (3)); Driver.Log (3, "Linking with {0} because it's referenced by a module reference in {1}", file, FileName); break; case "libGLES": case "libGLESv2": // special case for OpenGLES.framework if (Frameworks.Add ("OpenGLES")) Driver.Log (3, "Linking with the framework OpenGLES because {0} is referenced by a module reference in {1}", file, FileName); break; case "vImage": case "vecLib": // sub-frameworks if (Frameworks.Add ("Accelerate")) Driver.Log (3, "Linking with the framework Accelerate because {0} is referenced by a module reference in {1}", file, FileName); break; case "CoreAudioKit": case "Metal": case "MetalKit": case "MetalPerformanceShaders": case "CoreNFC": case "DeviceCheck": // some frameworks do not exists on simulators and will result in linker errors if we include them #if MTOUCH if (!App.IsSimulatorBuild) { #endif AddFramework (file); #if MTOUCH } #endif break; case "openal32": if (Frameworks.Add ("OpenAL")) Driver.Log (3, "Linking with the framework OpenAL because {0} is referenced by a module reference in {1}", file, FileName); break; default: #if MONOMAC string path = Path.GetDirectoryName (name); if (!path.StartsWith ("/System/Library/Frameworks", StringComparison.Ordinal)) continue; // CoreServices has multiple sub-frameworks that can be used by customer code if (path.StartsWith ("/System/Library/Frameworks/CoreServices.framework/", StringComparison.Ordinal)) { if (Frameworks.Add ("CoreServices")) Driver.Log (3, "Linking with the framework CoreServices because {0} is referenced by a module reference in {1}", file, FileName); break; } // ApplicationServices has multiple sub-frameworks that can be used by customer code if (path.StartsWith ("/System/Library/Frameworks/ApplicationServices.framework/", StringComparison.Ordinal)) { if (Frameworks.Add ("ApplicationServices")) Driver.Log (3, "Linking with the framework ApplicationServices because {0} is referenced by a module reference in {1}", file, FileName); break; } #endif // detect frameworks int f = name.IndexOf (".framework/", StringComparison.Ordinal); if (f > 0) { AddFramework (file); } else { if (UnresolvedModuleReferences == null) UnresolvedModuleReferences = new HashSet (); UnresolvedModuleReferences.Add (mr); Driver.Log (3, "Could not resolve the module reference {0} in {1}", file, FileName); } break; } } } } public override string ToString () { return FileName; } // This returns the path to all related files: // * The assembly itself // * Any debug files (mdb/pdb) // * Any config files // * Any satellite assemblies public IEnumerable GetRelatedFiles () { yield return FullPath; var mdb = FullPath + ".mdb"; if (File.Exists (mdb)) yield return mdb; var pdb = Path.ChangeExtension (FullPath, ".pdb"); if (File.Exists (pdb)) yield return pdb; var config = FullPath + ".config"; if (File.Exists (config)) yield return config; if (Satellites != null) { foreach (var satellite in Satellites) yield return satellite; } } public void ComputeSatellites () { var path = Path.GetDirectoryName (FullPath); var satellite_name = Path.GetFileNameWithoutExtension (FullPath) + ".resources.dll"; foreach (var subdir in Directory.GetDirectories (path)) { var culture_name = Path.GetFileName (subdir); CultureInfo ci; if (culture_name.IndexOf ('.') >= 0) continue; // cultures can't have dots. This way we don't check every *.app directory try { ci = CultureInfo.GetCultureInfo (culture_name); } catch { // nope, not a resource language continue; } if (ci == null) continue; var satellite = Path.Combine (subdir, satellite_name); if (File.Exists (satellite)) { if (Satellites == null) Satellites = new List (); Satellites.Add (satellite); } } } public void CopySatellitesToDirectory (string directory) { if (Satellites == null) return; foreach (var a in Satellites) { string target_dir = Path.Combine (directory, Path.GetFileName (Path.GetDirectoryName (a))); string target_s = Path.Combine (target_dir, Path.GetFileName (a)); if (!Directory.Exists (target_dir)) Directory.CreateDirectory (target_dir); CopyAssembly (a, target_s); } } } public sealed class NormalizedStringComparer : IEqualityComparer { public static readonly NormalizedStringComparer OrdinalIgnoreCase = new NormalizedStringComparer (StringComparer.OrdinalIgnoreCase); StringComparer comparer; public NormalizedStringComparer (StringComparer comparer) { this.comparer = comparer; } public bool Equals (string x, string y) { // From what I gather it doesn't matter which normalization form // is used, but I chose Form D because HFS normalizes to Form D. if (x != null) x = x.Normalize (System.Text.NormalizationForm.FormD); if (y != null) y = y.Normalize (System.Text.NormalizationForm.FormD); return comparer.Equals (x, y); } public int GetHashCode (string obj) { return comparer.GetHashCode (obj?.Normalize (System.Text.NormalizationForm.FormD)); } } public class AssemblyCollection : IEnumerable { Dictionary HashedAssemblies = new Dictionary (NormalizedStringComparer.OrdinalIgnoreCase); public void Add (Assembly assembly) { Assembly other; if (HashedAssemblies.TryGetValue (assembly.Identity, out other)) throw ErrorHelper.CreateError (2018, "The assembly '{0}' is referenced from two different locations: '{1}' and '{2}'.", assembly.Identity, other.FullPath, assembly.FullPath); HashedAssemblies.Add (assembly.Identity, assembly); } public void AddRange (AssemblyCollection assemblies) { foreach (var a in assemblies) Add (a); } public int Count { get { return HashedAssemblies.Count; } } public IDictionary Hashed { get { return HashedAssemblies; } } public bool TryGetValue (string identity, out Assembly assembly) { return HashedAssemblies.TryGetValue (identity, out assembly); } public bool TryGetValue (AssemblyDefinition asm, out Assembly assembly) { return HashedAssemblies.TryGetValue (Assembly.GetIdentity (asm), out assembly); } public bool Contains (AssemblyDefinition asm) { return HashedAssemblies.ContainsKey (Assembly.GetIdentity (asm)); } public bool ContainsKey (string identity) { return HashedAssemblies.ContainsKey (identity); } public void Remove (string identity) { HashedAssemblies.Remove (identity); } public void Remove (Assembly assembly) { Remove (assembly.Identity); } public Assembly this [string key] { get { return HashedAssemblies [key]; } set { HashedAssemblies [key] = value; } } public void Update (Target target, IEnumerable assemblies) { // This function will remove any assemblies not in 'assemblies', and add any new assemblies. var current = new HashSet (HashedAssemblies.Keys, HashedAssemblies.Comparer); foreach (var assembly in assemblies) { var identity = Assembly.GetIdentity (assembly); if (!current.Remove (identity)) { // new assembly var asm = new Assembly (target, assembly); Add (asm); Driver.Log (1, "The linker added the assembly '{0}' to '{1}' to satisfy a reference.", asm.Identity, target.App.Name); } else { this [identity].AssemblyDefinition = assembly; } } foreach (var removed in current) { Driver.Log (1, "The linker removed the assembly '{0}' from '{1}' since there is no more reference to it.", this [removed].Identity, target.App.Name); Remove (removed); } } #region Interface implementations IEnumerator IEnumerable.GetEnumerator () { return GetEnumerator (); } public IEnumerator GetEnumerator () { return HashedAssemblies.Values.GetEnumerator (); } #endregion } }