diff --git a/src/Microsoft.DotNet.Compiler.Common/ContentFiles.cs b/src/Microsoft.DotNet.Compiler.Common/ContentFiles.cs new file mode 100644 index 000000000..afba12c84 --- /dev/null +++ b/src/Microsoft.DotNet.Compiler.Common/ContentFiles.cs @@ -0,0 +1,96 @@ +// 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.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Common; + +namespace Microsoft.Dotnet.Cli.Compiler.Common +{ + public class ContentFiles + { + private readonly ProjectContext _context; + + public ContentFiles(ProjectContext context) + { + _context = context; + } + + public void StructuredCopyTo(string targetDirectory) + { + var sourceFiles = _context + .ProjectFile + .Files + .GetContentFiles(); + + var sourceDirectory = _context.ProjectDirectory; + + if (sourceFiles == null) + { + throw new ArgumentNullException(nameof(sourceFiles)); + } + + sourceDirectory = EnsureTrailingSlash(sourceDirectory); + targetDirectory = EnsureTrailingSlash(targetDirectory); + + var pathMap = sourceFiles + .ToDictionary(s => s, + s => Path.Combine(targetDirectory, + PathUtility.GetRelativePath(sourceDirectory, s))); + + foreach (var targetDir in pathMap.Values + .Select(Path.GetDirectoryName) + .Distinct() + .Where(t => !Directory.Exists(t))) + { + Directory.CreateDirectory(targetDir); + } + + foreach (var sourceFilePath in pathMap.Keys) + { + File.Copy( + sourceFilePath, + pathMap[sourceFilePath], + overwrite: true); + } + + RemoveAttributeFromFiles(pathMap.Values, FileAttributes.ReadOnly); + } + + private static void RemoveAttributeFromFiles(IEnumerable files, FileAttributes attribute) + { + foreach (var file in files) + { + var fileAttributes = File.GetAttributes(file); + if ((fileAttributes & attribute) == attribute) + { + File.SetAttributes(file, fileAttributes & ~attribute); + } + } + } + + private static string EnsureTrailingSlash(string path) + { + return EnsureTrailingCharacter(path, Path.DirectorySeparatorChar); + } + + private static string EnsureTrailingCharacter(string path, char trailingCharacter) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // if the path is empty, we want to return the original string instead of a single trailing character. + if (path.Length == 0 || path[path.Length - 1] == trailingCharacter) + { + return path; + } + + return path + trailingCharacter; + } + } +} diff --git a/src/Microsoft.DotNet.Compiler.Common/Executable.cs b/src/Microsoft.DotNet.Compiler.Common/Executable.cs new file mode 100644 index 000000000..f296e9ffe --- /dev/null +++ b/src/Microsoft.DotNet.Compiler.Common/Executable.cs @@ -0,0 +1,129 @@ +// 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.IO; +using System.Linq; +using System.Xml.Linq; +using Microsoft.DotNet.Cli.Compiler.Common; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Compilation; +using Microsoft.DotNet.ProjectModel.Graph; +using NuGet.Frameworks; + +namespace Microsoft.Dotnet.Cli.Compiler.Common +{ + public class Executable + { + private readonly ProjectContext _context; + + private readonly OutputPathCalculator _calculator; + + public Executable(ProjectContext context, OutputPathCalculator calculator) + { + _context = context; + + _calculator = calculator; + } + + public void MakeCompilationOutputRunnable(string configuration) + { + var outputPath = _calculator.GetOutputDirectoryPath(configuration); + + CopyContentFiles(outputPath); + + ExportRuntimeAssets(outputPath, configuration); + } + + private void ExportRuntimeAssets(string outputPath, string configuration) + { + var exporter = _context.CreateExporter(configuration); + + if (_context.TargetFramework.IsDesktop()) + { + MakeCompilationOutputRunnableForFullFramework(outputPath, configuration, exporter); + } + else + { + MakeCompilationOutputRunnableForCoreCLR(outputPath, exporter); + } + } + + private void MakeCompilationOutputRunnableForFullFramework( + string outputPath, + string configuration, + LibraryExporter exporter) + { + CopyAllDependencies(outputPath, exporter); + + GenerateBindingRedirects(exporter, configuration); + } + + private void MakeCompilationOutputRunnableForCoreCLR(string outputPath, LibraryExporter exporter) + { + WriteDepsFileAndCopyProjectDependencies(exporter, _context.ProjectFile.Name, outputPath); + + // TODO: Pick a host based on the RID + CoreHost.CopyTo(outputPath, _context.ProjectFile.Name + Constants.ExeSuffix); + } + + private void CopyContentFiles(string outputPath) + { + var contentFiles = new ContentFiles(_context); + contentFiles.StructuredCopyTo(outputPath); + } + + private static void CopyAllDependencies(string outputPath, LibraryExporter exporter) + { + exporter + .GetDependencies() + .SelectMany(e => e.RuntimeAssets()) + .CopyTo(outputPath); + } + + private static void WriteDepsFileAndCopyProjectDependencies( + LibraryExporter exporter, + string projectFileName, + string outputPath) + { + exporter + .GetDependencies(LibraryType.Package) + .WriteDepsTo(Path.Combine(outputPath, projectFileName + FileNameSuffixes.Deps)); + + exporter + .GetDependencies(LibraryType.Project) + .SelectMany(e => e.RuntimeAssets()) + .CopyTo(outputPath); + } + + public void GenerateBindingRedirects(LibraryExporter exporter, string configuration) + { + var outputName = _calculator.GetAssemblyPath(configuration); + + var existingConfig = new DirectoryInfo(_context.ProjectDirectory) + .EnumerateFiles() + .FirstOrDefault(f => f.Name.Equals("app.config", StringComparison.OrdinalIgnoreCase)); + + XDocument baseAppConfig = null; + + if (existingConfig != null) + { + using (var fileStream = File.OpenRead(existingConfig.FullName)) + { + baseAppConfig = XDocument.Load(fileStream); + } + } + + var appConfig = exporter.GetAllExports().GenerateBindingRedirects(baseAppConfig); + + if (appConfig == null) { return; } + + var path = outputName + ".config"; + using (var stream = File.Create(path)) + { + appConfig.Save(stream); + } + } + } +} diff --git a/src/Microsoft.DotNet.Compiler.Common/LibraryExporterExtensions.cs b/src/Microsoft.DotNet.Compiler.Common/LibraryExporterExtensions.cs index c5291bb75..9fd81a3ef 100644 --- a/src/Microsoft.DotNet.Compiler.Common/LibraryExporterExtensions.cs +++ b/src/Microsoft.DotNet.Compiler.Common/LibraryExporterExtensions.cs @@ -9,9 +9,17 @@ namespace Microsoft.DotNet.Cli.Compiler.Common { public static void WriteDepsTo(this IEnumerable exports, string path) { + CreateDirectoryIfNotExists(path); + File.WriteAllLines(path, exports.SelectMany(GenerateLines)); } - + + private static void CreateDirectoryIfNotExists(string path) + { + var depsFile = new FileInfo(path); + depsFile.Directory.Create(); + } + private static IEnumerable GenerateLines(LibraryExport export) { return GenerateLines(export, export.RuntimeAssemblies, "runtime") diff --git a/src/Microsoft.DotNet.Compiler.Common/ProjectContextExtensions.cs b/src/Microsoft.DotNet.Compiler.Common/ProjectContextExtensions.cs index 7132633e4..0129fcada 100644 --- a/src/Microsoft.DotNet.Compiler.Common/ProjectContextExtensions.cs +++ b/src/Microsoft.DotNet.Compiler.Common/ProjectContextExtensions.cs @@ -2,16 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Xml.Linq; -using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectModel; -using Microsoft.DotNet.ProjectModel.Compilation; -using Microsoft.DotNet.ProjectModel.Graph; -using Microsoft.DotNet.Tools.Common; using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.Compiler.Common @@ -21,141 +14,7 @@ namespace Microsoft.DotNet.Cli.Compiler.Common public static string ProjectName(this ProjectContext context) => context.RootProject.Identity.Name; public static string GetDisplayName(this ProjectContext context) => $"{context.RootProject.Identity.Name} ({context.TargetFramework})"; - public static void MakeCompilationOutputRunnable(this ProjectContext context, string outputPath, string configuration) - { - // REVIEW: This shouldn't be copied on compile - context - .ProjectFile - .Files - .GetContentFiles() - .StructuredCopyTo(context.ProjectDirectory, outputPath) - .RemoveAttribute(FileAttributes.ReadOnly); - - var exporter = context.CreateExporter(configuration); - - if (context.TargetFramework.IsDesktop()) - { - // On full framework, copy all dependencies to the output path - exporter - .GetDependencies() - .SelectMany(e => e.RuntimeAssets()) - .CopyTo(outputPath); - - // Generate binding redirects - var outputName = context.GetOutputPathCalculator(outputPath).GetAssemblyPath(configuration); - context.GenerateBindingRedirects(exporter, outputName); - } - else - { - exporter - .GetDependencies(LibraryType.Package) - .WriteDepsTo(Path.Combine(outputPath, context.ProjectFile.Name + FileNameSuffixes.Deps)); - - // On core clr, only copy project references - exporter.GetDependencies(LibraryType.Project) - .SelectMany(e => e.RuntimeAssets()) - .CopyTo(outputPath); - - // TODO: Pick a host based on the RID - CoreHost.CopyTo(outputPath, context.ProjectFile.Name + Constants.ExeSuffix); - } - } - - private static IEnumerable StructuredCopyTo(this IEnumerable sourceFiles, string sourceDirectory, string targetDirectory) - { - if (sourceFiles == null) - { - throw new ArgumentNullException(nameof(sourceFiles)); - } - - sourceDirectory = EnsureTrailingSlash(sourceDirectory); - targetDirectory = EnsureTrailingSlash(targetDirectory); - - var pathMap = sourceFiles - .ToDictionary(s => s, - s => Path.Combine(targetDirectory, - PathUtility.GetRelativePath(sourceDirectory, s))); - - foreach (var targetDir in pathMap.Values - .Select(Path.GetDirectoryName) - .Distinct() - .Where(t => !Directory.Exists(t))) - { - Directory.CreateDirectory(targetDir); - } - - foreach (var sourceFilePath in pathMap.Keys) - { - File.Copy( - sourceFilePath, - pathMap[sourceFilePath], - overwrite: true); - } - - return pathMap.Values; - } - - private static string EnsureTrailingSlash(string path) - { - return EnsureTrailingCharacter(path, Path.DirectorySeparatorChar); - } - - private static string EnsureTrailingCharacter(string path, char trailingCharacter) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - // if the path is empty, we want to return the original string instead of a single trailing character. - if (path.Length == 0 || path[path.Length - 1] == trailingCharacter) - { - return path; - } - - return path + trailingCharacter; - } - - private static IEnumerable RemoveAttribute(this IEnumerable files, FileAttributes attribute) - { - foreach (var file in files) - { - var fileAttributes = File.GetAttributes(file); - if ((fileAttributes & attribute) == attribute) - { - File.SetAttributes(file, fileAttributes & ~attribute); - } - } - - return files; - } - - public static void GenerateBindingRedirects(this ProjectContext context, LibraryExporter exporter, string outputName) - { - var existingConfig = new DirectoryInfo(context.ProjectDirectory) - .EnumerateFiles() - .FirstOrDefault(f => f.Name.Equals("app.config", StringComparison.OrdinalIgnoreCase)); - - XDocument baseAppConfig = null; - - if (existingConfig != null) - { - using (var fileStream = File.OpenRead(existingConfig.FullName)) - { - baseAppConfig = XDocument.Load(fileStream); - } - } - - var appConfig = exporter.GetAllExports().GenerateBindingRedirects(baseAppConfig); - - if (appConfig == null) { return; } - - var path = outputName + ".config"; - using (var stream = File.Create(path)) - { - appConfig.Save(stream); - } - } + public static bool IsTestProject(this ProjectContext context) => !string.IsNullOrEmpty(context.ProjectFile.TestRunner); public static CommonCompilerOptions GetLanguageSpecificCompilerOptions(this ProjectContext context, NuGetFramework framework, string configurationName) { diff --git a/src/dotnet-build/CompileContext.cs b/src/dotnet-build/CompileContext.cs index 3eba3d4ef..a70c92aac 100644 --- a/src/dotnet-build/CompileContext.cs +++ b/src/dotnet-build/CompileContext.cs @@ -5,11 +5,13 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Dotnet.Cli.Compiler.Common; using Microsoft.DotNet.Cli.Compiler.Common; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectModel; using Microsoft.DotNet.ProjectModel.Utilities; using Microsoft.DotNet.Tools.Compiler; +using Microsoft.Extensions.PlatformAbstractions; namespace Microsoft.DotNet.Tools.Build { @@ -273,13 +275,7 @@ namespace Microsoft.DotNet.Tools.Build var args = new List(); args.Add("--framework"); - args.Add($"{projectDependency.Framework}"); - - if (!string.IsNullOrEmpty(_args.RuntimeValue)) - { - args.Add("--runtime"); - args.Add(_args.RuntimeValue); - } + args.Add($"{projectDependency.Framework}"); args.Add("--configuration"); args.Add(_args.ConfigValue); @@ -298,12 +294,7 @@ namespace Microsoft.DotNet.Tools.Build // todo: add methods to CompilerCommandApp to generate the arg string? var args = new List(); args.Add("--framework"); - args.Add(_rootProject.TargetFramework.ToString()); - if (!string.IsNullOrEmpty(_args.RuntimeValue)) - { - args.Add("--runtime"); - args.Add(_args.RuntimeValue); - } + args.Add(_rootProject.TargetFramework.ToString()); args.Add("--configuration"); args.Add(_args.ConfigValue); args.Add("--output"); @@ -353,7 +344,41 @@ namespace Microsoft.DotNet.Tools.Build .ForwardStdErr() .Execute(); - return compileResult.ExitCode == 0; + var succeeded = compileResult.ExitCode == 0; + + if (succeeded) + { + MakeRunnableIfNecessary(); + } + + return succeeded; + } + + private void MakeRunnableIfNecessary() + { + var compilationOptions = CompilerUtil.ResolveCompilationOptions(_rootProject, _args.ConfigValue); + + // TODO: Make this opt in via another mechanism + var makeRunnable = compilationOptions.EmitEntryPoint.GetValueOrDefault() || + _rootProject.IsTestProject(); + + if (makeRunnable) + { + var outputPathCalculator = _rootProject.GetOutputPathCalculator(_args.OutputValue); + var rids = new List(); + if (string.IsNullOrEmpty(_args.RuntimeValue)) + { + rids.AddRange(PlatformServices.Default.Runtime.GetAllCandidateRuntimeIdentifiers()); + } + else + { + rids.Add(_args.RuntimeValue); + } + + var runtimeContext = ProjectContext.Create(_rootProject.ProjectDirectory, _rootProject.TargetFramework, rids); + var executable = new Executable(runtimeContext, outputPathCalculator); + executable.MakeCompilationOutputRunnable(_args.ConfigValue); + } } private static ISet Sort(Dictionary projects) diff --git a/src/dotnet-compile/Program.cs b/src/dotnet-compile/Program.cs index 41cb9699f..71aba04ca 100644 --- a/src/dotnet-compile/Program.cs +++ b/src/dotnet-compile/Program.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using Microsoft.Dotnet.Cli.Compiler.Common; using Microsoft.DotNet.Cli.Compiler.Common; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectModel; @@ -315,31 +316,7 @@ namespace Microsoft.DotNet.Tools.Compiler { success &= GenerateCultureResourceAssemblies(context.ProjectFile, dependencies, outputPath); } - - if (success) - { - // TODO: Make this opt in via another mechanism - var makeRunnable = compilationOptions.EmitEntryPoint.GetValueOrDefault() || - !string.IsNullOrEmpty(context.ProjectFile.TestRunner); - - if (makeRunnable) - { - var rids = new List(); - if (string.IsNullOrEmpty(args.RuntimeValue)) - { - rids.AddRange(PlatformServices.Default.Runtime.GetAllCandidateRuntimeIdentifiers()); - } - else - { - rids.Add(args.RuntimeValue); - } - - var runtimeContext = ProjectContext.Create(context.ProjectDirectory, context.TargetFramework, rids); - runtimeContext - .MakeCompilationOutputRunnable(outputPath, args.ConfigValue); - } - } - + return PrintSummary(diagnostics, sw, success); } diff --git a/src/dotnet-restore/Program.cs b/src/dotnet-restore/Program.cs index 5406b6d7e..fac03bdfd 100644 --- a/src/dotnet-restore/Program.cs +++ b/src/dotnet-restore/Program.cs @@ -9,6 +9,7 @@ using System.Text; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Compiler.Common; using Microsoft.Dnx.Runtime.Common.CommandLine; +using Microsoft.Dotnet.Cli.Compiler.Common; using Microsoft.DotNet.ProjectModel; using Microsoft.DotNet.ProjectModel.Graph; using NuGet.Frameworks; @@ -169,12 +170,15 @@ namespace Microsoft.DotNet.Tools.Restore toolDescription.Path, Path.GetDirectoryName(toolDescription.Target.RuntimeAssemblies.First().Path), toolDescription.Identity.Name + FileNameSuffixes.Deps); - - context.MakeCompilationOutputRunnable(context.ProjectDirectory, Constants.DefaultConfiguration); + + var calculator = context.GetOutputPathCalculator(context.ProjectDirectory); + var executable = new Executable(context, calculator); + + executable.MakeCompilationOutputRunnable(Constants.DefaultConfiguration); if (File.Exists(depsPath)) File.Delete(depsPath); - File.Move(Path.Combine(context.ProjectDirectory, "bin" + FileNameSuffixes.Deps), depsPath); + File.Move(Path.Combine(calculator.GetOutputDirectoryPath(Constants.DefaultConfiguration), "bin" + FileNameSuffixes.Deps), depsPath); } private static void RestoreToolToPath(LibraryRange tooldep, IEnumerable args, string tempPath, bool quiet)