Files
LuaCsForBarotraumaEP/Libraries/MonoGame.Framework/Src/Tools/MGCB/BuildContent.cs
2019-06-25 16:00:44 +03:00

505 lines
20 KiB
C#

// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.Framework.Content.Pipeline.Builder;
namespace MGCB
{
class BuildContent
{
[CommandLineParameter(
Name = "launchdebugger",
Flag = "d",
Description = "Wait for debugger to attach before building content.")]
public bool LaunchDebugger = false;
[CommandLineParameter(
Name = "quiet",
Flag = "q",
Description = "Only output content build errors.")]
public bool Quiet = false;
[CommandLineParameter(
Name = "@",
Flag = "@",
ValueName = "responseFile",
Description = "Read a text response file with additional command line options and switches.")]
// This property only exists for documentation.
// The actual handling of '/@' is done in the preprocess step.
public List<string> ResponseFiles
{
get { throw new InvalidOperationException(); }
set { throw new InvalidOperationException(); }
}
[CommandLineParameter(
Name = "workingDir",
Flag = "w",
ValueName = "directoryPath",
Description = "The working directory where all source content is located.")]
public void SetWorkingDir(string path)
{
Directory.SetCurrentDirectory(path);
}
[CommandLineParameter(
Name = "outputDir",
Flag = "o",
ValueName = "path",
Description = "The directory where all content is written.")]
public string OutputDir = string.Empty;
[CommandLineParameter(
Name = "intermediateDir",
Flag = "n",
ValueName = "path",
Description = "The directory where all intermediate files are written.")]
public string IntermediateDir = string.Empty;
[CommandLineParameter(
Name = "rebuild",
Flag = "r",
Description = "Forces a full rebuild of all content.")]
public bool Rebuild = false;
[CommandLineParameter(
Name = "clean",
Flag = "c",
Description = "Delete all previously built content and intermediate files.")]
public bool Clean = false;
[CommandLineParameter(
Name = "incremental",
Flag = "I",
Description = "Skip cleaning files not included in the current build.")]
public bool Incremental = false;
[CommandLineParameter(
Name = "reference",
Flag = "f",
ValueName = "assembly",
Description = "Adds an assembly reference for resolving content importers, processors, and writers.")]
public readonly List<string> References = new List<string>();
[CommandLineParameter(
Name = "platform",
Flag = "t",
ValueName = "targetPlatform",
Description = "Set the target platform for this build. Defaults to Windows desktop DirectX.")]
public TargetPlatform Platform = TargetPlatform.Windows;
[CommandLineParameter(
Name = "profile",
Flag = "g",
ValueName = "graphicsProfile",
Description = "Set the target graphics profile for this build. Defaults to HiDef.")]
public GraphicsProfile Profile = GraphicsProfile.HiDef;
[CommandLineParameter(
Name = "config",
ValueName = "string",
Description = "The optional build config string from the build system.")]
public string Config = string.Empty;
[CommandLineParameter(
Name = "importer",
Flag = "i",
ValueName = "className",
Description = "Defines the class name of the content importer for reading source content.")]
public string Importer = null;
[CommandLineParameter(
Name = "processor",
Flag = "p",
ValueName = "className",
Description = "Defines the class name of the content processor for processing imported content.")]
public void SetProcessor(string processor)
{
_processor = processor;
// If you are changing the processor then reset all
// the processor parameters.
_processorParams.Clear();
}
private string _processor = null;
private readonly OpaqueDataDictionary _processorParams = new OpaqueDataDictionary();
[CommandLineParameter(
Name = "processorParam",
Flag = "P",
ValueName = "name=value",
Description = "Defines a parameter name and value to set on a content processor.")]
public void AddProcessorParam(string nameAndValue)
{
var keyAndValue = nameAndValue.Split('=', ':');
if (keyAndValue.Length != 2)
{
// Do we error out or something?
return;
}
_processorParams.Remove(keyAndValue[0]);
_processorParams.Add(keyAndValue[0], keyAndValue[1]);
}
[CommandLineParameter(
Name = "build",
Flag = "b",
ValueName = "sourceFile",
Description = "Build the content source file using the previously set switches and options. Optional destination path may be specified with \"sourceFile;destFile\" if you wish to change the output filepath.")]
public void OnBuild(string sourceFile)
{
string link = null;
if (sourceFile.Contains(";"))
{
var split = sourceFile.Split(';');
sourceFile = split[0];
if(split.Length > 0)
link = split[1];
}
// Make sure the source file is absolute.
if (!Path.IsPathRooted(sourceFile))
sourceFile = Path.Combine(Directory.GetCurrentDirectory(), sourceFile);
// link should remain relative, absolute path will get set later when the build occurs
sourceFile = PathHelper.Normalize(sourceFile);
// Remove duplicates... keep this new one.
var previous = _content.FindIndex(e => string.Equals(e.SourceFile, sourceFile, StringComparison.InvariantCultureIgnoreCase));
if (previous != -1)
_content.RemoveAt(previous);
// Create the item for processing later.
var item = new ContentItem
{
SourceFile = sourceFile,
OutputFile = link,
Importer = Importer,
Processor = _processor,
ProcessorParams = new OpaqueDataDictionary()
};
_content.Add(item);
// Copy the current processor parameters blind as we
// will validate and remove invalid parameters during
// the build process later.
foreach (var pair in _processorParams)
item.ProcessorParams.Add(pair.Key, pair.Value);
}
[CommandLineParameter(
Name = "copy",
ValueName = "sourceFile",
Description = "Copy the content source file verbatim to the output directory.")]
public void OnCopy(string sourceFile)
{
string link = null;
if (sourceFile.Contains(";"))
{
var split = sourceFile.Split(';');
sourceFile = split[0];
if (split.Length > 0)
link = split[1];
}
if (!Path.IsPathRooted(sourceFile))
sourceFile = Path.Combine(Directory.GetCurrentDirectory(), sourceFile);
sourceFile = PathHelper.Normalize(sourceFile);
// Remove duplicates... keep this new one.
var previous = _copyItems.FindIndex(e => string.Equals(e.SourceFile, sourceFile, StringComparison.InvariantCultureIgnoreCase));
if (previous != -1)
_copyItems.RemoveAt(previous);
_copyItems.Add(new CopyItem { SourceFile = sourceFile, Link = link });
}
[CommandLineParameter(
Name = "compress",
Description = "Compress the XNB files for smaller file sizes.")]
public bool CompressContent = false;
public class ContentItem
{
public string SourceFile;
// This refers to the "Link" which can override the default output location
public string OutputFile;
public string Importer;
public string Processor;
public OpaqueDataDictionary ProcessorParams;
}
public class CopyItem
{
public string SourceFile;
public string Link;
}
private readonly List<ContentItem> _content = new List<ContentItem>();
private readonly List<CopyItem> _copyItems = new List<CopyItem>();
private PipelineManager _manager;
public bool HasWork
{
get { return _content.Count > 0 || _copyItems.Count > 0 || Clean; }
}
string ReplaceSymbols(string parameter)
{
if (string.IsNullOrWhiteSpace(parameter))
return parameter;
return parameter
.Replace("$(Platform)", Platform.ToString())
.Replace("$(Configuration)", Config)
.Replace("$(Config)", Config)
.Replace("$(Profile)", this.Profile.ToString());
}
public void Build(out int successCount, out int errorCount)
{
var projectDirectory = PathHelper.Normalize(Directory.GetCurrentDirectory());
var outputPath = ReplaceSymbols (OutputDir);
if (!Path.IsPathRooted(outputPath))
outputPath = PathHelper.Normalize(Path.GetFullPath(Path.Combine(projectDirectory, outputPath)));
var intermediatePath = ReplaceSymbols (IntermediateDir);
if (!Path.IsPathRooted(intermediatePath))
intermediatePath = PathHelper.Normalize(Path.GetFullPath(Path.Combine(projectDirectory, intermediatePath)));
_manager = new PipelineManager(projectDirectory, outputPath, intermediatePath);
_manager.Logger = new ConsoleLogger();
_manager.CompressContent = CompressContent;
// If the intent is to debug build, break at the original location
// of any exception, eg, within the actual importer/processor.
if (LaunchDebugger)
_manager.RethrowExceptions = false;
// Feed all the assembly references to the pipeline manager
// so it can resolve importers, processors, writers, and types.
foreach (var r in References)
{
var assembly = r;
if (!Path.IsPathRooted(assembly))
assembly = Path.GetFullPath(Path.Combine(projectDirectory, assembly));
_manager.AddAssembly(assembly);
}
// Load the previously serialized list of built content.
var contentFile = Path.Combine(intermediatePath, PipelineBuildEvent.Extension);
var previousContent = SourceFileCollection.Read(contentFile);
// If the target changed in any way then we need to force
// a full rebuild even under incremental builds.
var targetChanged = previousContent.Config != Config ||
previousContent.Platform != Platform ||
previousContent.Profile != Profile;
// First clean previously built content.
for(int i = 0; i < previousContent.SourceFiles.Count; i++)
{
var sourceFile = previousContent.SourceFiles[i];
// This may be an old file (prior to MG 3.7) which doesn't have destination files:
string destFile = null;
if(i < previousContent.DestFiles.Count)
{
destFile = previousContent.DestFiles[i];
}
var inContent = _content.Any(e => string.Equals(e.SourceFile, sourceFile, StringComparison.InvariantCultureIgnoreCase));
var cleanOldContent = !inContent && !Incremental;
var cleanRebuiltContent = inContent && (Rebuild || Clean);
if (cleanRebuiltContent || cleanOldContent || targetChanged)
_manager.CleanContent(sourceFile, destFile);
}
// TODO: Should we be cleaning copy items? I think maybe we should.
var newContent = new SourceFileCollection
{
Profile = _manager.Profile = Profile,
Platform = _manager.Platform = Platform,
Config = _manager.Config = Config
};
errorCount = 0;
successCount = 0;
// Before building the content, register all files to be built. (Necessary to
// correctly resolve external references.)
foreach (var c in _content)
{
try
{
_manager.RegisterContent(c.SourceFile, c.OutputFile, c.Importer, c.Processor, c.ProcessorParams);
}
catch
{
// Ignore exception. Exception will be handled below.
}
}
foreach (var c in _content)
{
try
{
_manager.BuildContent(c.SourceFile,
c.OutputFile,
c.Importer,
c.Processor,
c.ProcessorParams);
newContent.SourceFiles.Add(c.SourceFile);
newContent.DestFiles.Add(c.OutputFile);
++successCount;
}
catch (InvalidContentException ex)
{
var message = string.Empty;
if (ex.ContentIdentity != null && !string.IsNullOrEmpty(ex.ContentIdentity.SourceFilename))
{
message = ex.ContentIdentity.SourceFilename;
if (!string.IsNullOrEmpty(ex.ContentIdentity.FragmentIdentifier))
message += "(" + ex.ContentIdentity.FragmentIdentifier + ")";
message += ": ";
}
message += ex.Message;
Console.WriteLine(message);
++errorCount;
}
catch (PipelineException ex)
{
Console.Error.WriteLine("{0}: error: {1}", c.SourceFile, ex.Message);
if (ex.InnerException != null)
Console.Error.WriteLine(ex.InnerException.ToString());
++errorCount;
}
catch (Exception ex)
{
Console.Error.WriteLine("{0}: error: {1}", c.SourceFile, ex.Message);
if (ex.InnerException != null)
Console.Error.WriteLine(ex.InnerException.ToString());
++errorCount;
}
}
// If this is an incremental build we merge the list
// of previous content with the new list.
if (Incremental && !targetChanged)
{
newContent.Merge(previousContent);
_manager.ContentStats.MergePreviousStats();
}
// Delete the old file and write the new content
// list if we have any to serialize.
FileHelper.DeleteIfExists(contentFile);
if (newContent.SourceFiles.Count > 0)
newContent.Write(contentFile);
// Process copy items (files that bypass the content pipeline)
foreach (var c in _copyItems)
{
try
{
// Figure out an asset name relative to the project directory,
// retaining the file extension.
// Note that replacing a sub-path like this requires consistent
// directory separator characters.
var relativeName = c.Link;
if (string.IsNullOrWhiteSpace(relativeName))
relativeName = c.SourceFile.Replace(projectDirectory, string.Empty)
.TrimStart(Path.DirectorySeparatorChar)
.TrimStart(Path.AltDirectorySeparatorChar);
var dest = Path.Combine(outputPath, relativeName);
// Only copy if the source file is newer than the destination.
// We may want to provide an option for overriding this, but for
// nearly all cases this is the desired behavior.
if (File.Exists(dest) && !Rebuild)
{
var srcTime = File.GetLastWriteTimeUtc(c.SourceFile);
var dstTime = File.GetLastWriteTimeUtc(dest);
if (srcTime <= dstTime)
{
if (string.IsNullOrEmpty(c.Link))
Console.WriteLine("Skipping {0}", c.SourceFile);
else
Console.WriteLine("Skipping {0} => {1}", c.SourceFile, c.Link);
// Copy the stats from the previous stats collection.
_manager.ContentStats.CopyPreviousStats(c.SourceFile);
continue;
}
}
var startTime = DateTime.UtcNow;
// Create the destination directory if it doesn't already exist.
var destPath = Path.GetDirectoryName(dest);
if (!Directory.Exists(destPath))
Directory.CreateDirectory(destPath);
File.Copy(c.SourceFile, dest, true);
// Destination file should not be read-only even if original was.
var fileAttr = File.GetAttributes(dest);
fileAttr = fileAttr & (~FileAttributes.ReadOnly);
File.SetAttributes(dest, fileAttr);
var buildTime = DateTime.UtcNow - startTime;
if (string.IsNullOrEmpty(c.Link))
Console.WriteLine("{0}", c.SourceFile);
else
Console.WriteLine("{0} => {1}", c.SourceFile, c.Link);
// Record content stats on the copy.
_manager.ContentStats.RecordStats(c.SourceFile, dest, "CopyItem", typeof(File), (float)buildTime.TotalSeconds);
++successCount;
}
catch (Exception ex)
{
Console.Error.WriteLine("{0}: error: {1}", c, ex.Message);
if (ex.InnerException != null)
Console.Error.WriteLine(ex.InnerException.ToString());
++errorCount;
}
}
// Dump the content build stats.
_manager.ContentStats.Write(intermediatePath);
}
[CommandLineParameter(
Name = "help",
Flag = "h",
Description = "Displays this help.")]
public void Help()
{
MGBuildParser.Instance.ShowError(null);
}
}
}