diff --git a/src/Microsoft.DotNet.Compiler.Common/AssemblyInfoFileGenerator.cs b/src/Microsoft.DotNet.Compiler.Common/AssemblyInfoFileGenerator.cs new file mode 100644 index 000000000..7fe9a8e29 --- /dev/null +++ b/src/Microsoft.DotNet.Compiler.Common/AssemblyInfoFileGenerator.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Resources; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.IO; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Dotnet.Cli.Compiler.Common +{ + public class AssemblyInfoFileGenerator + { + public static string Generate(AssemblyInfoOptions metadata, IEnumerable sourceFiles) + { + var projectAttributes = new Dictionary() + { + [typeof(AssemblyTitleAttribute)] = EscapeCharacters(metadata.Title), + [typeof(AssemblyDescriptionAttribute)] = EscapeCharacters(metadata.Description), + [typeof(AssemblyCopyrightAttribute)] = EscapeCharacters(metadata.Copyright), + [typeof(AssemblyFileVersionAttribute)] = EscapeCharacters(metadata.AssemblyFileVersion?.ToString()), + [typeof(AssemblyVersionAttribute)] = EscapeCharacters(metadata.AssemblyVersion?.ToString()), + [typeof(AssemblyInformationalVersionAttribute)] = EscapeCharacters(metadata.InformationalVersion), + [typeof(AssemblyCultureAttribute)] = EscapeCharacters(metadata.Culture), + [typeof(NeutralResourcesLanguageAttribute)] = EscapeCharacters(metadata.NeutralLanguage) + }; + + var existingAttributes = new List(); + foreach (var sourceFile in sourceFiles) + { + var tree = CSharpSyntaxTree.ParseText(File.ReadAllText(sourceFile)); + var root = tree.GetRoot(); + + // assembly attributes can be only on first level + foreach (var attributeListSyntax in root.ChildNodes().OfType()) + { + if (attributeListSyntax.Target.Identifier.Kind() == SyntaxKind.AssemblyKeyword) + { + foreach (var attributeSyntax in attributeListSyntax.Attributes) + { + var projectAttribute = projectAttributes.FirstOrDefault(attribute => IsSameAttribute(attribute.Key, attributeSyntax)); + if (projectAttribute.Key != null) + { + existingAttributes.Add(projectAttribute.Key); + } + } + } + } + } + + return string.Join(Environment.NewLine, projectAttributes + .Where(projectAttribute => projectAttribute.Value != null && !existingAttributes.Contains(projectAttribute.Key)) + .Select(projectAttribute => $"[assembly:{projectAttribute.Key.FullName}(\"{projectAttribute.Value}\")]")); + } + + private static bool IsSameAttribute(Type attributeType, AttributeSyntax attributeSyntax) + { + var name = attributeSyntax.Name.ToString(); + // This check is quite stupid but we can not do more without semantic model + return attributeType.FullName.StartsWith(name) || attributeType.Name.StartsWith(name); + } + + private static string EscapeCharacters(string str) + { + return str != null ? SymbolDisplay.FormatLiteral(str, quote: false) : null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Compiler.Common/AssemblyInfoOptions.cs b/src/Microsoft.DotNet.Compiler.Common/AssemblyInfoOptions.cs new file mode 100644 index 000000000..c8d8904a0 --- /dev/null +++ b/src/Microsoft.DotNet.Compiler.Common/AssemblyInfoOptions.cs @@ -0,0 +1,143 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.ProjectModel; +using System; +using System.Collections.Generic; +using System.CommandLine; + +namespace Microsoft.Dotnet.Cli.Compiler.Common +{ + public class AssemblyInfoOptions + { + private const string TitleOptionName = "title"; + + private const string DescriptionOptionName = "description"; + + private const string CopyrightOptionName = "copyright"; + + private const string AssemblyFileVersionOptionName = "file-version"; + + private const string AssemblyVersionOptionName = "version"; + + private const string InformationalVersionOptionName = "informational-version"; + + private const string CultureOptionName = "culture"; + + private const string NeutralCultureOptionName = "neutral-language"; + + public string Title { get; set; } + + public string Description { get; set; } + + public string Copyright { get; set; } + + public string AssemblyFileVersion { get; set; } + + public string AssemblyVersion { get; set; } + + public string InformationalVersion { get; set; } + + public string Culture { get; set; } + + public string NeutralLanguage { get; set; } + + public static AssemblyInfoOptions CreateForProject(Project project) + { + return new AssemblyInfoOptions() + { + AssemblyVersion = project.Version?.Version.ToString(), + AssemblyFileVersion = project.AssemblyFileVersion.ToString(), + InformationalVersion = project.Version.ToString(), + Copyright = project.Copyright, + Description = project.Description, + Title = project.Title, + NeutralLanguage = project.Language + }; + } + + public static AssemblyInfoOptions Parse(ArgumentSyntax syntax) + { + string version = null; + string informationalVersion = null; + string fileVersion = null; + string title = null; + string description = null; + string copyright = null; + string culture = null; + string neutralCulture = null; + + syntax.DefineOption(AssemblyVersionOptionName, ref version, "Assembly version"); + + syntax.DefineOption(TitleOptionName, ref title, "Assembly title"); + + syntax.DefineOption(DescriptionOptionName, ref description, "Assembly description"); + + syntax.DefineOption(CopyrightOptionName, ref copyright, "Assembly copyright"); + + syntax.DefineOption(NeutralCultureOptionName, ref neutralCulture, "Assembly neutral culture"); + + syntax.DefineOption(CultureOptionName, ref culture, "Assembly culture"); + + syntax.DefineOption(InformationalVersionOptionName, ref informationalVersion, "Assembly informational version"); + + syntax.DefineOption(AssemblyFileVersionOptionName, ref fileVersion, "Assembly title"); + + return new AssemblyInfoOptions() + { + AssemblyFileVersion = fileVersion, + AssemblyVersion = version, + Copyright = copyright, + NeutralLanguage = neutralCulture, + Description = description, + InformationalVersion = informationalVersion, + Title = title + }; + } + + public static IEnumerable SerializeToArgs(AssemblyInfoOptions assemblyInfoOptions) + { + var options = new List(); + + if (!string.IsNullOrWhiteSpace(assemblyInfoOptions.Title)) + { + options.Add(FormatOption(TitleOptionName, assemblyInfoOptions.Title)); + } + if (!string.IsNullOrWhiteSpace(assemblyInfoOptions.Description)) + { + options.Add(FormatOption(DescriptionOptionName, assemblyInfoOptions.Description)); + } + if (!string.IsNullOrWhiteSpace(assemblyInfoOptions.Copyright)) + { + options.Add(FormatOption(CopyrightOptionName, assemblyInfoOptions.Copyright)); + } + if (!string.IsNullOrWhiteSpace(assemblyInfoOptions.AssemblyFileVersion)) + { + options.Add(FormatOption(AssemblyFileVersionOptionName, assemblyInfoOptions.AssemblyFileVersion)); + } + if (!string.IsNullOrWhiteSpace(assemblyInfoOptions.AssemblyVersion)) + { + options.Add(FormatOption(AssemblyVersionOptionName, assemblyInfoOptions.AssemblyVersion)); + } + if (!string.IsNullOrWhiteSpace(assemblyInfoOptions.InformationalVersion)) + { + options.Add(FormatOption(InformationalVersionOptionName, assemblyInfoOptions.InformationalVersion)); + } + if (!string.IsNullOrWhiteSpace(assemblyInfoOptions.Culture)) + { + options.Add(FormatOption(CultureOptionName, assemblyInfoOptions.Culture)); + } + if (!string.IsNullOrWhiteSpace(assemblyInfoOptions.NeutralLanguage)) + { + options.Add(FormatOption(NeutralCultureOptionName, assemblyInfoOptions.NeutralLanguage)); + } + + return options; + } + + private static string FormatOption(string optionName, string value) + { + return $"--{optionName}:{value}"; + } + } +} diff --git a/src/Microsoft.DotNet.Compiler.Common/project.json b/src/Microsoft.DotNet.Compiler.Common/project.json index f65ba4523..e71070b93 100644 --- a/src/Microsoft.DotNet.Compiler.Common/project.json +++ b/src/Microsoft.DotNet.Compiler.Common/project.json @@ -4,7 +4,9 @@ "dependencies": { "NETStandard.Library": "1.0.0-rc2-23608", "System.Linq": "4.0.1-rc2-23608", + "System.Reflection": "4.0.10-rc2-23608", "System.CommandLine": "0.1.0-*", + "Microsoft.CodeAnalysis.CSharp": "1.1.1", "Microsoft.DotNet.ProjectModel": "1.0.0-*", "Microsoft.DotNet.Cli.Utils": { "type": "build", diff --git a/src/Microsoft.DotNet.Tools.Compiler.Csc/Program.cs b/src/Microsoft.DotNet.Tools.Compiler.Csc/Program.cs index 836b7d13b..f387d001f 100644 --- a/src/Microsoft.DotNet.Tools.Compiler.Csc/Program.cs +++ b/src/Microsoft.DotNet.Tools.Compiler.Csc/Program.cs @@ -13,6 +13,7 @@ using System.Text; using Microsoft.DotNet.Cli.Compiler.Common; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectModel; +using Microsoft.Dotnet.Cli.Compiler.Common; namespace Microsoft.DotNet.Tools.Compiler.Csc { @@ -25,6 +26,7 @@ namespace Microsoft.DotNet.Tools.Compiler.Csc DebugHelper.HandleDebugSwitch(ref args); CommonCompilerOptions commonOptions = null; + AssemblyInfoOptions assemblyInfoOptions = null; string tempOutDir = null; IReadOnlyList references = Array.Empty(); IReadOnlyList resources = Array.Empty(); @@ -37,6 +39,8 @@ namespace Microsoft.DotNet.Tools.Compiler.Csc { commonOptions = CommonCompilerOptionsExtensions.Parse(syntax); + assemblyInfoOptions = AssemblyInfoOptions.Parse(syntax); + syntax.DefineOption("temp-output", ref tempOutDir, "Compilation temporary directory"); syntax.DefineOption("out", ref outputName, "Name of the output assembly"); @@ -46,7 +50,6 @@ namespace Microsoft.DotNet.Tools.Compiler.Csc syntax.DefineOptionList("resource", ref resources, "Resources to embed"); syntax.DefineParameterList("source-files", ref sources, "Compilation sources"); - if (tempOutDir == null) { syntax.ReportError("Option '--temp-output' is required"); @@ -63,6 +66,11 @@ namespace Microsoft.DotNet.Tools.Compiler.Csc var allArgs = new List(translated); allArgs.AddRange(GetDefaultOptions()); + // Generate assembly info + var assemblyInfo = Path.Combine(tempOutDir, $"dotnet-compile.assemblyinfo.cs"); + File.WriteAllText(assemblyInfo, AssemblyInfoFileGenerator.Generate(assemblyInfoOptions, sources)); + allArgs.Add($"\"{assemblyInfo}\""); + if (outputName != null) { allArgs.Add($"-out:\"{outputName}\""); diff --git a/src/Microsoft.DotNet.Tools.Compiler/Program.cs b/src/Microsoft.DotNet.Tools.Compiler/Program.cs index 2012223c8..5d4b10015 100644 --- a/src/Microsoft.DotNet.Tools.Compiler/Program.cs +++ b/src/Microsoft.DotNet.Tools.Compiler/Program.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Xml.Linq; using Microsoft.Dnx.Runtime.Common.CommandLine; +using Microsoft.Dotnet.Cli.Compiler.Common; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Compiler.Common; using Microsoft.DotNet.Tools.Common; @@ -309,6 +310,9 @@ namespace Microsoft.DotNet.Tools.Compiler // Add compilation options to the args compilerArgs.AddRange(compilationOptions.SerializeToArgs()); + // Add metadata options + compilerArgs.AddRange(AssemblyInfoOptions.SerializeToArgs(AssemblyInfoOptions.CreateForProject(context.ProjectFile))); + foreach (var dependency in dependencies) { var projectDependency = dependency.Library as ProjectDescription; @@ -332,7 +336,6 @@ namespace Microsoft.DotNet.Tools.Compiler { return false; } - // Add project source files var sourceFiles = context.ProjectFile.Files.SourceFiles; compilerArgs.AddRange(sourceFiles); @@ -660,7 +663,7 @@ namespace Microsoft.DotNet.Tools.Compiler // {file}.resx -> {file}.resources var resourcesFile = Path.Combine(intermediateOutputPath, name); - var result = Command.Create("dotnet-resgen", $"\"{fileName}\" -o \"{resourcesFile}\"") + var result = Command.Create("dotnet-resgen", $"\"{fileName}\" -o \"{resourcesFile}\" -v \"{project.Version.Version}\"") .ForwardStdErr() .ForwardStdOut() .Execute(); @@ -725,6 +728,7 @@ namespace Microsoft.DotNet.Tools.Compiler arguments.AddRange(references.Select(r => $"-r \"{r.ResolvedPath}\"")); arguments.Add($"-o \"{resourceOuputFile}\""); arguments.Add($"-c {culture}"); + arguments.Add($"-v {project.Version.Version}"); foreach (var resourceFile in resourceFileGroup) { diff --git a/src/Microsoft.DotNet.Tools.Resgen/Program.cs b/src/Microsoft.DotNet.Tools.Resgen/Program.cs index be672be82..eacabc01f 100644 --- a/src/Microsoft.DotNet.Tools.Resgen/Program.cs +++ b/src/Microsoft.DotNet.Tools.Resgen/Program.cs @@ -6,6 +6,7 @@ using System.Linq; using Microsoft.Dnx.Runtime.Common.CommandLine; using Microsoft.DotNet.Cli.Utils; using System; +using Microsoft.Dotnet.Cli.Compiler.Common; namespace Microsoft.DotNet.Tools.Resgen { @@ -23,6 +24,7 @@ namespace Microsoft.DotNet.Tools.Resgen var ouputFile = app.Option("-o", "Output file name", CommandOptionType.SingleValue); var culture = app.Option("-c", "Ouput assembly culture", CommandOptionType.SingleValue); + var version = app.Option("-v", "Ouput assembly version", CommandOptionType.SingleValue); var references = app.Option("-r", "Compilation references", CommandOptionType.MultipleValue); var inputFiles = app.Argument("", "Input files", true); @@ -42,10 +44,14 @@ namespace Microsoft.DotNet.Tools.Resgen case ResourceFileType.Dll: using (var outputStream = outputResourceFile.File.Create()) { + var metadata = new AssemblyInfoOptions(); + metadata.Culture = culture.Value(); + metadata.AssemblyVersion = version.Value(); + ResourceAssemblyGenerator.Generate(intputResourceFiles, outputStream, + metadata, Path.GetFileNameWithoutExtension(outputResourceFile.File.Name), - culture.Value(), references.Values.ToArray() ); } diff --git a/src/Microsoft.DotNet.Tools.Resgen/ResourceAssemblyGenerator.cs b/src/Microsoft.DotNet.Tools.Resgen/ResourceAssemblyGenerator.cs index 80205d12a..319e7baf4 100644 --- a/src/Microsoft.DotNet.Tools.Resgen/ResourceAssemblyGenerator.cs +++ b/src/Microsoft.DotNet.Tools.Resgen/ResourceAssemblyGenerator.cs @@ -7,13 +7,14 @@ using System.IO; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Dotnet.Cli.Compiler.Common; using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Tools.Resgen { internal class ResourceAssemblyGenerator { - public static void Generate(ResourceSource[] sourceFiles, Stream outputStream, string asemblyName, string culture, string[] references) + public static void Generate(ResourceSource[] sourceFiles, Stream outputStream, AssemblyInfoOptions metadata, string assemblyName, string[] references) { if (sourceFiles == null) { @@ -51,17 +52,14 @@ namespace Microsoft.DotNet.Tools.Resgen } var compilationOptions = new CSharpCompilationOptions(outputKind: OutputKind.DynamicallyLinkedLibrary); - var compilation = CSharpCompilation.Create(asemblyName, + var compilation = CSharpCompilation.Create(assemblyName, references: references.Select(reference => MetadataReference.CreateFromFile(reference)), options: compilationOptions); - if (!string.IsNullOrEmpty(culture)) + compilation = compilation.AddSyntaxTrees(new[] { - compilation = compilation.AddSyntaxTrees(new[] - { - CSharpSyntaxTree.ParseText($"[assembly:System.Reflection.AssemblyCultureAttribute(\"{culture}\")]") - }); - } + CSharpSyntaxTree.ParseText(AssemblyInfoFileGenerator.Generate(metadata, Enumerable.Empty())) + }); var result = compilation.Emit(outputStream, manifestResources: resourceDescriptions); if (!result.Success) diff --git a/src/Microsoft.DotNet.Tools.Resgen/project.json b/src/Microsoft.DotNet.Tools.Resgen/project.json index fdb2e213b..57273d19b 100644 --- a/src/Microsoft.DotNet.Tools.Resgen/project.json +++ b/src/Microsoft.DotNet.Tools.Resgen/project.json @@ -11,6 +11,7 @@ "System.Resources.ReaderWriter": "4.0.0-rc2-23608", "Microsoft.CodeAnalysis.CSharp": "1.1.1", + "Microsoft.DotNet.Compiler.Common": "1.0.0-*", "Microsoft.DotNet.Cli.Utils": { "type": "build", "version": "1.0.0-*"