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; } } } /// <summary> /// Defines the rules for a precompilation operation /// </summary> public class PreCompileContext { /// <summary> /// The target framework to use /// </summary> [CommandLine("f"), CommandLine("framework")] public string Framework { get; set; } private readonly List<string> probePaths = new List<string>(); /// <summary> /// Locations to check for referenced assemblies /// </summary> [CommandLine("p"), CommandLine("probe")] public List<string> ProbePaths { get { return probePaths; } } private readonly List<string> inputs = new List<string>(); /// <summary> /// The paths for assemblies to process /// </summary> [CommandLine("")] public List<string> Inputs { get { return inputs; } } /// <summary> /// The type name of the serializer to generate /// </summary> [CommandLine("t"), CommandLine("type")] public string TypeName { get; set; } /// <summary> /// The name of the assembly to generate /// </summary> [CommandLine("o"), CommandLine("out")] public string AssemblyName { get; set; } /// <summary> /// Show help /// </summary> [CommandLine("?"), CommandLine("help"), CommandLine("h")] public bool Help { get; set; } /// <summary> /// The accessibility of the generated type /// </summary> [CommandLine("access")] public ProtoBuf.Meta.RuntimeTypeModel.Accessibility Accessibility { get; set; } /// <summary> /// The path to the file to use to sign the assembly /// </summary> [CommandLine("keyfile")] public string KeyFile { get; set; } /// <summary> /// The container to use to sign the assembly /// </summary> [CommandLine("keycontainer")] public string KeyContainer { get; set; } /// <summary> /// The public key (in hexadecimal) to use to sign the assembly /// </summary> [CommandLine("publickey")] public string PublicKey { get; set; } /// <summary> /// Create a new instance of PreCompileContext /// </summary> 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; } /// <summary> /// Check the context for obvious errrs /// </summary> 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<string> ProbeForFiles(string file) { foreach (var probePath in probePaths) { string combined = Path.Combine(probePath, file); if (File.Exists(combined)) { yield return combined; } } } /// <summary> /// Perform the precompilation operation /// </summary> 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<IKVM.Reflection.Assembly>(); 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<IKVM.Reflection.Type>(); 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<IKVM.Reflection.Type>(); 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; } /// <summary> /// Return the syntax guide for the utility /// </summary> 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]:<framework> Can be an explicit path, or a path relative to: Reference Assemblies\Microsoft\Framework -o[ut]:<file> Output dll path -t[ype]:<typename> Type name of the serializer to generate -p[robe]:<path> Additional directory to probe for assemblies -access:<access> Specify accessibility of generated serializer to 'Public' or 'Internal' -keyfile:<file> Sign with the file (snk, etc) specified -keycontainer:<container> Sign with the container specified -publickey:<key> Sign with the public key specified (as hex) <file> Input file to analyse Example: precompile -f:.NETCore\v4.5 MyDtos\My.dll -o:MySerializer.dll -t:MySerializer"; } } /// <summary> /// Defines a mapping from command-line attributes to properties /// </summary> [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] public class CommandLineAttribute : Attribute { /// <summary> /// Attempt to parse the incoming command-line switches, matching by prefix /// onto properties of the specified type /// </summary> public static bool TryParse<T>(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<string>)) { ((List<string>)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; /// <summary> /// Create a new CommandLineAttribute object for the given prefix /// </summary> public CommandLineAttribute(string prefix) { this.prefix = prefix; } /// <summary> /// The prefix to recognise this command-line switch /// </summary> public string Prefix { get { return prefix; } } } }