using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using ProtoBuf.Meta;
namespace ProtoBuf.Precompile
{
class Program
{
static int Main(string[] args)
{
try
{
Console.WriteLine("protobuf-net pre-compiler");
PreCompileContext ctx;
if (!CommandLineAttribute.TryParse(args, out ctx))
{
return -1;
}
if (ctx.Help)
{
Console.WriteLine();
Console.WriteLine();
Console.WriteLine(ctx.GetUsage());
return -1;
}
if (!ctx.SanityCheck()) return -1;
bool allGood = ctx.Execute();
return allGood ? 0 : -1;
}
catch (Exception ex)
{
while (ex != null)
{
Console.Error.WriteLine(ex.Message);
Console.Error.WriteLine(ex.StackTrace);
Console.Error.WriteLine();
ex = ex.InnerException;
}
return -1;
}
}
}
///
/// Defines the rules for a precompilation operation
///
public class PreCompileContext
{
///
/// The target framework to use
///
[CommandLine("f"), CommandLine("framework")]
public string Framework { get; set; }
private readonly List probePaths = new List();
///
/// Locations to check for referenced assemblies
///
[CommandLine("p"), CommandLine("probe")]
public List ProbePaths { get { return probePaths; } }
private readonly List inputs = new List();
///
/// The paths for assemblies to process
///
[CommandLine("")]
public List Inputs { get { return inputs; } }
///
/// The type name of the serializer to generate
///
[CommandLine("t"), CommandLine("type")]
public string TypeName { get; set; }
///
/// The name of the assembly to generate
///
[CommandLine("o"), CommandLine("out")]
public string AssemblyName { get; set; }
///
/// Show help
///
[CommandLine("?"), CommandLine("help"), CommandLine("h")]
public bool Help { get; set; }
///
/// The accessibility of the generated type
///
[CommandLine("access")]
public ProtoBuf.Meta.RuntimeTypeModel.Accessibility Accessibility { get; set; }
///
/// The path to the file to use to sign the assembly
///
[CommandLine("keyfile")]
public string KeyFile { get; set; }
///
/// The container to use to sign the assembly
///
[CommandLine("keycontainer")]
public string KeyContainer { get; set; }
///
/// The public key (in hexadecimal) to use to sign the assembly
///
[CommandLine("publickey")]
public string PublicKey { get; set; }
///
/// Create a new instance of PreCompileContext
///
public PreCompileContext()
{
Accessibility = ProtoBuf.Meta.RuntimeTypeModel.Accessibility.Public;
}
static string TryInferFramework(string path)
{
string imageRuntimeVersion = null;
try
{
using (var uni = new IKVM.Reflection.Universe())
{
uni.AssemblyResolve += (s, a) => ((IKVM.Reflection.Universe)s).CreateMissingAssembly(a.Name);
var asm = uni.LoadFile(path);
imageRuntimeVersion = asm.ImageRuntimeVersion;
var attr = uni.GetType("System.Attribute, mscorlib");
foreach(var attrib in asm.__GetCustomAttributes(attr, false))
{
if (attrib.Constructor.DeclaringType.FullName == "System.Runtime.Versioning.TargetFrameworkAttribute"
&& attrib.ConstructorArguments.Count == 1)
{
var parts = ((string)attrib.ConstructorArguments[0].Value).Split(',');
string runtime = null, version = null, profile = null;
for (int i = 0; i < parts.Length; i++)
{
int idx = parts[i].IndexOf('=');
if (idx < 0)
{
runtime = parts[i];
} else
{
switch(parts[i].Substring(0,idx))
{
case "Version":
version = parts[i].Substring(idx + 1);
break;
case "Profile":
profile = parts[i].Substring(idx + 1);
break;
}
}
}
if(runtime != null)
{
var sb = new StringBuilder(runtime);
if(version != null)
{
sb.Append(Path.DirectorySeparatorChar).Append(version);
}
if (profile != null)
{
sb.Append(Path.DirectorySeparatorChar).Append("Profile").Append(Path.DirectorySeparatorChar).Append(profile);
}
string targetFramework = sb.ToString();
return targetFramework;
}
}
}
}
}
catch (Exception ex) {
// not really fussed; we could have multiple inputs to try, and the user
// can always use -f:blah to specify it explicitly
Debug.WriteLine(ex.Message);
}
if (!string.IsNullOrEmpty(imageRuntimeVersion))
{
string frameworkPath = Path.Combine(
Environment.ExpandEnvironmentVariables(@"%windir%\Microsoft.NET\Framework"),
imageRuntimeVersion);
if (Directory.Exists(frameworkPath)) return frameworkPath;
}
return null;
}
///
/// Check the context for obvious errrs
///
public bool SanityCheck()
{
bool allGood = true;
if (inputs.Count == 0)
{
Console.Error.WriteLine("No input assemblies");
allGood = false;
}
if (string.IsNullOrEmpty(TypeName))
{
Console.Error.WriteLine("No serializer type-name specified");
allGood = false;
}
if (string.IsNullOrEmpty(AssemblyName))
{
Console.Error.WriteLine("No output assembly file specified");
allGood = false;
}
if (string.IsNullOrEmpty(Framework))
{
foreach (var inp in inputs)
{
string tmp = TryInferFramework(inp);
if (tmp != null)
{
Console.WriteLine("Detected framework: " + tmp);
Framework = tmp;
break;
}
}
}
if (string.IsNullOrEmpty(Framework))
{
Console.WriteLine("No framework specified; defaulting to " + Environment.Version);
probePaths.Add(Path.GetDirectoryName(typeof(string).Assembly.Location));
}
else
{
if (Directory.Exists(Framework))
{ // very clear and explicit
probePaths.Add(Framework);
}
else
{
string root = Environment.GetEnvironmentVariable("ProgramFiles(x86)");
if (string.IsNullOrEmpty(root)) root = Environment.GetEnvironmentVariable("ProgramFiles");
root = Path.Combine(root, @"Reference Assemblies\Microsoft\Framework\");
if (!Directory.Exists(root))
{
Console.Error.WriteLine("Framework reference assemblies root folder could not be found");
allGood = false;
}
else
{
string frameworkRoot = Path.Combine(root, Framework);
if (Directory.Exists(frameworkRoot))
{
// fine
probePaths.Add(frameworkRoot);
}
else
{
Console.Error.WriteLine("Framework not found: " + Framework);
Console.Error.WriteLine("Available frameworks are:");
string[] files = Directory.GetFiles(root, "mscorlib.dll", SearchOption.AllDirectories);
foreach (var file in files)
{
string dir = Path.GetDirectoryName(file);
if (dir.StartsWith(root)) dir = dir.Substring(root.Length);
Console.Error.WriteLine(dir);
}
allGood = false;
}
}
}
}
if (!string.IsNullOrEmpty(KeyFile) && !File.Exists(KeyFile))
{
Console.Error.WriteLine("Key file not found: " + KeyFile);
allGood = false;
}
foreach (var inp in inputs)
{
if(File.Exists(inp)) {
string dir = Path.GetDirectoryName(inp);
if(!probePaths.Contains(dir)) probePaths.Add(dir);
}
else
{
Console.Error.WriteLine("Input not found: " + inp);
allGood = false;
}
}
return allGood;
}
IEnumerable ProbeForFiles(string file)
{
foreach (var probePath in probePaths)
{
string combined = Path.Combine(probePath, file);
if (File.Exists(combined))
{
yield return combined;
}
}
}
///
/// Perform the precompilation operation
///
public bool Execute()
{
// model to work with
var model = TypeModel.Create();
model.Universe.AssemblyResolve += (sender, args) =>
{
string nameOnly = args.Name.Split(',')[0];
if (nameOnly == "IKVM.Reflection" && args.RequestingAssembly != null && args.RequestingAssembly.FullName.StartsWith("protobuf-net"))
{
throw new InvalidOperationException("This operation needs access to the protobuf-net.dll used by your library, **in addition to** the protobuf-net.dll that is included with the precompiler; the easiest way to do this is to ensure the referenced protobuf-net.dll is in the same folder as your library.");
}
var uni = model.Universe;
foreach (var tmp in uni.GetAssemblies())
{
if (tmp.GetName().Name == nameOnly) return tmp;
}
var asm = ResolveNewAssembly(uni, nameOnly + ".dll");
if(asm != null) return asm;
asm = ResolveNewAssembly(uni, nameOnly + ".exe");
if(asm != null) return asm;
throw new InvalidOperationException("All assemblies must be resolved explicity; did not resolve: " + args.Name);
};
bool allGood = true;
var mscorlib = ResolveNewAssembly(model.Universe, "mscorlib.dll");
if (mscorlib == null)
{
Console.Error.WriteLine("mscorlib.dll not found!");
allGood = false;
}
ResolveNewAssembly(model.Universe, "System.dll"); // not so worried about whether that one exists...
if (ResolveNewAssembly(model.Universe, "protobuf-net.dll") == null)
{
Console.Error.WriteLine("protobuf-net.dll not found!");
allGood = false;
}
if (!allGood) return false;
var assemblies = new List();
MetaType metaType = null;
foreach (var file in inputs)
{
assemblies.Add(model.Load(file));
}
// scan for obvious protobuf types
var attributeType = model.Universe.GetType("System.Attribute, mscorlib");
var toAdd = new List();
foreach (var asm in assemblies)
{
foreach (var type in asm.GetTypes())
{
bool add = false;
if (!(type.IsClass || type.IsValueType)) continue;
foreach (var attrib in type.__GetCustomAttributes(attributeType, true))
{
string name = attrib.Constructor.DeclaringType.FullName;
switch(name)
{
case "ProtoBuf.ProtoContractAttribute":
add = true;
break;
}
if (add) break;
}
if (add) toAdd.Add(type);
}
}
if (toAdd.Count == 0)
{
Console.Error.WriteLine("No [ProtoContract] types found; nothing to do!");
return false;
}
// add everything we explicitly know about
toAdd.Sort((x, y) => string.Compare(x.FullName, y.FullName));
foreach (var type in toAdd)
{
Console.WriteLine("Adding " + type.FullName + "...");
var tmp = model.Add(type, true);
if (metaType == null) metaType = tmp; // use this as the template for the framework version
}
// add everything else we can find
model.Cascade();
var inferred = new List();
foreach (MetaType type in model.GetTypes())
{
if(!toAdd.Contains(type.Type)) inferred.Add(type.Type);
}
inferred.Sort((x, y) => string.Compare(x.FullName, y.FullName));
foreach (var type in inferred)
{
Console.WriteLine("Adding " + type.FullName + "...");
}
// configure the output file/serializer name, and borrow the framework particulars from
// the type we loaded
var options = new RuntimeTypeModel.CompilerOptions
{
TypeName = TypeName,
OutputPath = AssemblyName,
ImageRuntimeVersion = mscorlib.ImageRuntimeVersion,
MetaDataVersion = 0x20000, // use .NET 2 onwards
KeyContainer = KeyContainer,
KeyFile = KeyFile,
PublicKey = PublicKey
};
if (mscorlib.ImageRuntimeVersion == "v1.1.4322")
{ // .NET 1.1-style
options.MetaDataVersion = 0x10000;
}
if (metaType != null)
{
options.SetFrameworkOptions(metaType);
}
options.Accessibility = this.Accessibility;
Console.WriteLine("Compiling " + options.TypeName + " to " + options.OutputPath + "...");
// GO WORK YOUR MAGIC, CRAZY THING!!
model.Compile(options);
Console.WriteLine("All done");
return true;
}
private IKVM.Reflection.Assembly ResolveNewAssembly(IKVM.Reflection.Universe uni, string fileName)
{
foreach (var match in ProbeForFiles(fileName))
{
var asm = uni.LoadFile(match);
if (asm != null)
{
Console.WriteLine("Resolved " + match);
return asm;
}
}
return null;
}
///
/// Return the syntax guide for the utility
///
public string GetUsage()
{
return
@"Generates a serialization dll that can be used with just the
(platform-specific) protobuf-net core, allowing fast and efficient
serialization even on light frameworks (CF, SL, SP7, Metro, etc).
The input assembly(ies) is(are) anaylsed for types decorated with
[ProtoContract]. All such types are added to the model, as are any
types that they require.
Note: the compiler must be able to resolve a protobuf-net.dll
that is suitable for the target framework; this is done most simply
by ensuring that the appropriate protobuf-net.dll is next to the
input assembly.
Options:
-f[ramework]:
Can be an explicit path, or a path relative to:
Reference Assemblies\Microsoft\Framework
-o[ut]:
Output dll path
-t[ype]:
Type name of the serializer to generate
-p[robe]:
Additional directory to probe for assemblies
-access:
Specify accessibility of generated serializer
to 'Public' or 'Internal'
-keyfile:
Sign with the file (snk, etc) specified
-keycontainer:
Sign with the container specified
-publickey:
Sign with the public key specified (as hex)
Input file to analyse
Example:
precompile -f:.NETCore\v4.5 MyDtos\My.dll -o:MySerializer.dll
-t:MySerializer";
}
}
///
/// Defines a mapping from command-line attributes to properties
///
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class CommandLineAttribute : Attribute
{
///
/// Attempt to parse the incoming command-line switches, matching by prefix
/// onto properties of the specified type
///
public static bool TryParse(string[] args, out T result) where T : class, new()
{
result = new T();
bool allGood = true;
var props = typeof(T).GetProperties();
char[] leadChars = {'/', '+', '-'};
for (int i = 0; i < args.Length; i++)
{
string arg = args[i].Trim(), prefix, value;
if(arg.IndexOfAny(leadChars) == 0)
{
int idx = arg.IndexOf(':');
if (idx < 0)
{
prefix = arg.Substring(1);
value = "";
}
else
{
prefix = arg.Substring(1,idx - 1);
value = arg.Substring(idx + 1);
}
}
else
{
prefix = "";
value = arg;
}
System.Reflection.PropertyInfo foundProp = null;
foreach (var prop in props)
{
foreach (CommandLineAttribute atttib in prop.GetCustomAttributes(typeof(CommandLineAttribute), true))
{
if (atttib.Prefix == prefix)
{
foundProp = prop;
break;
}
}
if (foundProp != null) break;
}
if (foundProp == null)
{
allGood = false;
Console.Error.WriteLine("Argument not understood: " + arg);
}
else
{
if (foundProp.PropertyType == typeof(string))
{
foundProp.SetValue(result, value, null);
}
else if (foundProp.PropertyType == typeof(List))
{
((List)foundProp.GetValue(result, null)).Add(value);
}
else if (foundProp.PropertyType == typeof(bool))
{
foundProp.SetValue(result, true, null);
}
else if (foundProp.PropertyType.IsEnum)
{
object parsedValue;
try {
parsedValue = Enum.Parse(foundProp.PropertyType, value, true);
} catch {
Console.Error.WriteLine("Invalid option for: " + arg);
Console.Error.WriteLine("Options: " + string.Join(", ", Enum.GetNames(foundProp.PropertyType)));
allGood = false;
parsedValue = null;
}
if (parsedValue != null) foundProp.SetValue(result, parsedValue, null);
}
}
}
return allGood;
}
private readonly string prefix;
///
/// Create a new CommandLineAttribute object for the given prefix
///
public CommandLineAttribute(string prefix) { this.prefix = prefix; }
///
/// The prefix to recognise this command-line switch
///
public string Prefix { get { return prefix; } }
}
}