ILRepack/Steps/ResourceProcessing/BamlGenerator.cs (213 lines of code) (raw):

// // Copyright (c) 2015 Timotei Dolean // // 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 Confuser.Renamer.BAML; using Fasterflect; using Mono.Cecil; using Mono.Collections.Generic; using System; using System.Collections.Generic; using System.Linq; namespace ILRepacking.Steps.ResourceProcessing { internal class BamlGenerator { private const int ResourceDictionaryTypeId = 65012; private const string ComponentString = ";component/"; private static readonly BamlDocument.BamlVersion BamlVersion = new BamlDocument.BamlVersion { Major = 0, Minor = 96 }; private readonly ILogger _logger; private readonly Collection<AssemblyNameReference> _targetAssemblyReferences; private readonly string _mainAssemblyName; public BamlGenerator( ILogger logger, Collection<AssemblyNameReference> targetAssemblyReferences, AssemblyDefinition mainAssembly) { _logger = logger; _targetAssemblyReferences = targetAssemblyReferences; _mainAssemblyName = mainAssembly.Name.Name; } public BamlDocument GenerateThemesGenericXaml(IEnumerable<string> importedFiles) { BamlDocument document = new BamlDocument { Signature = "MSBAML", ReaderVersion = BamlVersion, WriterVersion = BamlVersion, UpdaterVersion = BamlVersion }; document.Add(new DocumentStartRecord()); AddAssemblyInfos(document); document.AddRange(GetMergedDictionariesAttributes()); document.Add(new ElementStartRecord { TypeId = ResourceDictionaryTypeId }); document.Add(new XmlnsPropertyRecord { Prefix = string.Empty, XmlNamespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation", AssemblyIds = document.OfType<AssemblyInfoRecord>().Select(asm => asm.AssemblyId).ToArray() }); document.AddRange(GetDictionariesList(importedFiles)); ElementEndRecord lastEndRecord = new ElementEndRecord(); document.Add(new DeferableContentStartRecord { Record = lastEndRecord }); document.Add(lastEndRecord); document.Add(new DocumentEndRecord()); return document; } public void AddMergedDictionaries( BamlDocument document, IEnumerable<string> importedFiles) { BamlRecord mergedDictionaryRecord = document.FirstOrDefault(IsMergedDictionaryAttribute); if (mergedDictionaryRecord != null) { HandleMergedDictionary(document, importedFiles, mergedDictionaryRecord as AttributeInfoRecord); } else { if (document.FindIndex(IsResourceDictionaryElementStart) == -1) { //TODO: throw? (Let's hope people read the logs ^_^) _logger.Error(string.Format( "Existing 'Themes/generic.xaml' in {0} is *not* a ResourceDictionary. " + "This will prevent proper WPF application merging.", _mainAssemblyName)); return; } int attributeInfosStartIndex = document.FindLastIndex(r => r is AssemblyInfoRecord); if (attributeInfosStartIndex == -1) { _logger.Error("Invalid BAML detected. (no AssemblyInfoRecord)"); return; } var extraAttributes = GetMergedDictionariesAttributes().ToList(); AdjustAttributeIds(document, (ushort)extraAttributes.Count); document.InsertRange(attributeInfosStartIndex + 1, extraAttributes); int defferableRecordIndex = document.FindIndex(r => r is DeferableContentStartRecord); if (attributeInfosStartIndex == -1) { _logger.Error("Invalid BAML detected. (No DeferableContentStartRecord)"); } document.InsertRange(defferableRecordIndex, GetDictionariesList(importedFiles)); } } private void AddAssemblyInfos(BamlDocument document) { var assemblyNames = new[] { "WindowsBase", "PresentationCore", "PresentationFramework" }; var references = _targetAssemblyReferences. Where(asm => assemblyNames.Any(prefix => asm.Name.Equals(prefix))); ushort assemblyId = 0; foreach (AssemblyNameReference reference in references) { document.Add(new AssemblyInfoRecord { AssemblyFullName = reference.FullName, AssemblyId = assemblyId }); ++assemblyId; } } private static void AdjustAttributeIds(BamlDocument document, ushort offset) { const string AttributeIdPropertyName = "AttributeId"; var existingAttributeInfoRecords = document.OfType<AttributeInfoRecord>().ToList(); foreach (var record in document) { ushort? attributeId = record.TryGetPropertyValue(AttributeIdPropertyName) as ushort?; if (attributeId == null || record is AttributeInfoRecord || !existingAttributeInfoRecords.Any(r => r.AttributeId == attributeId)) { continue; } record.TrySetPropertyValue(AttributeIdPropertyName, (ushort)(attributeId.Value + offset)); } foreach (var attributeInfoRecord in existingAttributeInfoRecords) { attributeInfoRecord.AttributeId += offset; } } private static IEnumerable<BamlRecord> GetMergedDictionariesAttributes() { yield return new AttributeInfoRecord { Name = "MergedDictionaries", OwnerTypeId = ResourceDictionaryTypeId, }; yield return new AttributeInfoRecord { Name = "Source", OwnerTypeId = ResourceDictionaryTypeId, AttributeId = 1, }; } private void HandleMergedDictionary( BamlDocument document, IEnumerable<string> importedFiles, AttributeInfoRecord mergedDictionariesRecord) { int indexStart = document.FindIndex( r => r is PropertyListStartRecord && ((PropertyListStartRecord)r).AttributeId == mergedDictionariesRecord.AttributeId); int insertIndex = indexStart + 1; if (document[insertIndex] is LineNumberAndPositionRecord) insertIndex++; List<string> existingUris = document.Skip(indexStart) .TakeWhile(r => !(r is PropertyListEndRecord)) .OfType<PropertyWithConverterRecord>() .Select(GetFileNameFromPropertyRecord) .ToList(); document.InsertRange(insertIndex, GetImportRecords(importedFiles.Except(existingUris))); } private static string GetFileNameFromPropertyRecord(PropertyWithConverterRecord record) { int fileNameStartIndex = record.Value.IndexOf(ComponentString, StringComparison.Ordinal) + ComponentString.Length; return record.Value.Substring(fileNameStartIndex); } private static bool IsMergedDictionaryAttribute(BamlRecord record) { AttributeInfoRecord attributeRecord = record as AttributeInfoRecord; if (attributeRecord == null) return false; return attributeRecord.Name.Equals("MergedDictionaries") && attributeRecord.OwnerTypeId == ResourceDictionaryTypeId; } private static bool IsResourceDictionaryElementStart(BamlRecord record) { return record is ElementStartRecord && ((ElementStartRecord)record).TypeId == ResourceDictionaryTypeId; } private string GetPackUri(string file) { return string.Format( "pack://application:,,,/{0};component/{1}", _mainAssemblyName, file); } private List<BamlRecord> GetDictionariesList(IEnumerable<string> importedFiles) { List<BamlRecord> records = new List<BamlRecord>(); records.Add(new PropertyListStartRecord()); records.AddRange(GetImportRecords(importedFiles)); records.Add(new PropertyListEndRecord()); return records; } private List<BamlRecord> GetImportRecords(IEnumerable<string> importedFiles) { List<BamlRecord> records = new List<BamlRecord>(); foreach (string file in importedFiles) { records.Add(new ElementStartRecord { TypeId = ResourceDictionaryTypeId, }); records.Add(new PropertyWithConverterRecord { AttributeId = 1, ConverterTypeId = 64831, Value = GetPackUri(file) }); records.Add(new ElementEndRecord()); } return records; } } }