using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using Microsoft.Build.Construction; using Microsoft.Xna.Framework.Content.Pipeline; using Newtonsoft.Json; using NuGet.Configuration; namespace Contentless; public static class Program { public static int Main(string[] args) { if (args.Length < 1) { Console.Error.WriteLine("Please specify the location of the content file you want to use"); return 1; } // find global packages folder and installed packages if we supplied a project file string packagesFolder = null; Dictionary installedPackages = null; if (args.Length >= 2) { var projectPath = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, args[1])); if (!File.Exists(projectPath) || Path.GetExtension(projectPath) != ".csproj") { Console.Error.WriteLine($"Unable to find valid project file at {projectPath}"); return 1; } installedPackages = Program.ExtractPackagesFromProject(projectPath); Console.WriteLine($"Found package dependencies {string.Join(", ", installedPackages.Select(kv => $"{kv.Key}@{kv.Value}"))} in project {projectPath}"); var settings = Settings.LoadDefaultSettings(Path.GetDirectoryName(projectPath)); packagesFolder = SettingsUtility.GetGlobalPackagesFolder(settings); Console.WriteLine($"Using global packages folder {packagesFolder}"); } // iterate all project files (semicolon-delimited) and process them var contentFiles = args[0].Split(';').Select(p => Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, p.Trim()))).Distinct(); foreach (var file in contentFiles) { var ret = Program.ProcessContentFile(new FileInfo(file), packagesFolder, installedPackages); if (ret != 0) return ret; } return 0; } private static int ProcessContentFile(FileInfo contentFile, string packagesFolder, Dictionary installedPackages) { if (!contentFile.Exists || contentFile.Extension != ".mgcb") { Console.Error.WriteLine($"Unable to find valid content file at {contentFile}"); return 1; } Console.WriteLine($"Using content file {contentFile}"); var content = Program.ReadContent(contentFile); // load config var config = new Config(); var configFile = new FileInfo(Path.Combine(contentFile.DirectoryName!, "Contentless.json")); if (configFile.Exists) { using var stream = configFile.OpenText(); try { config = JsonConvert.DeserializeObject(stream.ReadToEnd()); Console.WriteLine($"Using config from {configFile}"); } catch (Exception e) { Console.Error.WriteLine($"Error loading config from {configFile}: {e}"); return 1; } } else { Console.WriteLine("Using default config"); } if (config.References.Length > 0 && packagesFolder == null) Console.Error.WriteLine("The config file contains references, but no valid project data was found. Please specify the location of the project file you want to use for gathering references as the second argument."); var changed = false; // load refereces and replace reference paths if they're mismatched var referencesNotInContentFile = config.References.ToHashSet(); const string referenceHeader = "/reference:"; for (var i = 0; i < content.Count; i++) { var line = content[i]; if (!line.StartsWith(referenceHeader)) continue; var reference = line[referenceHeader.Length..]; var libraryName = Path.GetFileName(reference)[..^4]; if (referencesNotInContentFile.Remove(libraryName)) { if (installedPackages.TryGetValue(libraryName, out var version)) { var fullLibraryPath = Program.CalculateFullPathToLibrary(packagesFolder, libraryName, version); if (reference != fullLibraryPath) { Console.WriteLine($"Changing reference from {reference} to {fullLibraryPath}"); reference = fullLibraryPath; content[i] = referenceHeader + fullLibraryPath; changed = true; } else { if (config.LogSkipped) Console.WriteLine($"Skipping reference replacement for {fullLibraryPath} which already matched"); } } else { Console.Error.WriteLine($"Unable to find existing reference {libraryName} in project file"); } } var refPath = Path.GetFullPath(Path.Combine(contentFile.DirectoryName, reference)); Program.SafeAssemblyLoad(refPath); Console.WriteLine($"Using reference {refPath}"); } // find the line we want to start adding new references from var lastReferenceLine = 0; for (var i = 0; i < content.Count; i++) { var line = content[i]; if (line.StartsWith(referenceHeader)) { lastReferenceLine = i + 1; } else if (line.StartsWith("/importer:") || line.StartsWith("/processor:") || line.StartsWith("/build:") || line.Contains("-- Content --")) { if (lastReferenceLine == 0) lastReferenceLine = i; break; } } // add references that aren't in the content file yet foreach (var reference in referencesNotInContentFile) { if (installedPackages.TryGetValue(reference, out var version)) { try { var path = Program.CalculateFullPathToLibrary(packagesFolder, reference, version); content.Insert(lastReferenceLine++, referenceHeader + path); changed = true; Program.SafeAssemblyLoad(path); Console.WriteLine($"Adding reference {path}"); } catch (Exception e) { Console.Error.WriteLine($"Error adding reference {reference}: {e}"); } } else { Console.Error.WriteLine($"Unable to find configured reference {reference} in project file"); } } // load content importers var (importers, processors) = Program.GetContentData(); Console.WriteLine($"Found possible importer types {string.Join(", ", importers)}"); Console.WriteLine($"Found possible processor types {string.Join(", ", processors)}"); var excluded = config.ExcludedFiles.Select(Program.MakeFileRegex).ToArray(); var overrides = Program.GetOverrides(config.Overrides).ToArray(); foreach (var file in contentFile.Directory.EnumerateFiles("*", SearchOption.AllDirectories)) { // is the file the content or config file? if (file.Name == contentFile.Name || file.Name == configFile.Name) continue; var relative = Path.GetRelativePath(contentFile.DirectoryName, file.FullName).Replace('\\', '/'); // is the file in an excluded directory? if (excluded.Any(e => e.IsMatch(relative))) { if (config.LogSkipped) Console.WriteLine($"Skipping excluded file {relative}"); continue; } // is the file already in the content file? if (Program.HasEntry(content, relative)) { if (config.LogSkipped) Console.WriteLine($"Skipping file {relative} as it is already part of the content file"); continue; } ImporterInfo importer = null; string processor = null; Dictionary processorParams = null; // override importers var over = Program.GetOverrideFor(relative, overrides); if (over != null) { processorParams = over.Override.ProcessorParams; // copy special case if (over.Override.Copy) { Program.CopyFile(content, relative); changed = true; continue; } if (!string.IsNullOrEmpty(over.Override.Importer)) { importer = importers.Find(i => i.Type.Name == over.Override.Importer); if (importer == null) { Console.Error.WriteLine($"Override importer {over.Override.Importer} not found for file {relative}"); continue; } } if (!string.IsNullOrEmpty(over.Override.Processor)) { processor = processors.Find(p => p == over.Override.Processor); if (processor == null) { Console.Error.WriteLine($"Override processor {over.Override.Processor} not found for file {relative}"); continue; } } } // normal importers importer ??= Program.GetImporterFor(relative, importers); if (importer != null && processor == null) processor = processors.Find(p => p == importer.Importer.DefaultProcessor); // no importer found :( if (importer == null || processor == null) { Console.Error.WriteLine($"No importer or processor found for file {relative}"); continue; } Program.AddFile(content, relative, importer.Type.Name, processor, processorParams); changed = true; } if (changed) { contentFile.Delete(); using (var stream = contentFile.CreateText()) { foreach (var line in content) stream.WriteLine(line); } Console.WriteLine("Wrote changes to content file"); } Console.WriteLine("Done"); return 0; } private static void SafeAssemblyLoad(string refPath) { try { Assembly.LoadFrom(refPath); } catch (Exception e) { Console.Error.WriteLine($"Error loading reference {refPath}: {e}"); } } private static Dictionary ExtractPackagesFromProject(string csprojPath) { var ret = new Dictionary(); var projectRootElement = ProjectRootElement.Open(csprojPath); foreach (var property in projectRootElement.AllChildren.Where(x => x.ElementName == "PackageReference").Select(x => x as ProjectItemElement)) { var libraryName = property.Include; if (property.Children.FirstOrDefault(x => x.ElementName == "Version") is not ProjectMetadataElement versionElement) continue; ret.Add(libraryName, versionElement.Value); } return ret; } private static string CalculateFullPathToLibrary(string packageFolder, string libraryName, string referencesVersion) { return Path.Combine(packageFolder, libraryName.ToLowerInvariant(), referencesVersion, "tools", libraryName + ".dll").Replace('\\', '/'); } private static (List, List) GetContentData() { var importers = new List(); var processors = new List(); foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { try { foreach (var type in assembly.GetTypes()) { var importer = (ContentImporterAttribute) type.GetCustomAttribute(typeof(ContentImporterAttribute), true); if (importer != null) importers.Add(new ImporterInfo(importer, type)); var processor = type.GetCustomAttribute(typeof(ContentProcessorAttribute), true); if (processor != null) processors.Add(type.Name); } } catch (Exception e) { Console.Error.WriteLine($"Error gathering types in reference {assembly}: {e}"); } } return (importers, processors); } private static IEnumerable GetOverrides(Dictionary config) { foreach (var entry in config) yield return new OverrideInfo(Program.MakeFileRegex(entry.Key), entry.Value); } private static OverrideInfo GetOverrideFor(string file, IEnumerable overrides) { foreach (var over in overrides) { if (over.Regex.IsMatch(file)) return over; } return null; } private static ImporterInfo GetImporterFor(string file, IEnumerable importers) { var extension = Path.GetExtension(file); foreach (var importer in importers) { if (importer.Importer.FileExtensions.Contains(extension)) return importer; } return null; } private static bool HasEntry(IEnumerable content, string relativeFile) { foreach (var line in content) { if (line.StartsWith($"#begin {relativeFile}")) return true; } return false; } private static List ReadContent(FileInfo file) { var content = new List(); using var stream = file.OpenText(); while (stream.ReadLine() is {} line) content.Add(line); return content; } private static void AddFile(ICollection content, string relative, string importer, string processor, Dictionary processorParams) { content.Add($"#begin {relative}"); content.Add($"/importer:{importer}"); content.Add($"/processor:{processor}"); if (processorParams != null) { foreach (var kv in processorParams) content.Add($"/processorParam:{kv.Key}={kv.Value}"); } content.Add($"/build:{relative}"); content.Add(""); Console.WriteLine($"Adding file {relative} with importer {importer} and processor {processor}"); } private static void CopyFile(ICollection content, string relative) { content.Add($"#begin {relative}"); content.Add($"/copy:{relative}"); content.Add(""); Console.WriteLine($"Adding file {relative} with the Copy build action"); } private static Regex MakeFileRegex(string s) { return new Regex(s.Replace(".", "[.]").Replace("*", ".*").Replace("?", ".")); } }