From 215b53c6ab8bee6b8818a0b9dfaa1fb8a089b193 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 9 Nov 2015 00:23:46 -0800 Subject: [PATCH] Added a basic workspace implementation - This is a port of a simple project.json workspace I wrote a while back. It's been updating to use the new APIs (which actually made it much easier to implement). --- .../ProjectJsonWorkspace.cs | 234 ++++++++++++++++++ .../SnkUtils.cs | 84 +++++++ .../project.json | 9 + 3 files changed, 327 insertions(+) create mode 100644 src/Microsoft.DotNet.ProjectModel.Workspace/ProjectJsonWorkspace.cs create mode 100644 src/Microsoft.DotNet.ProjectModel.Workspace/SnkUtils.cs create mode 100644 src/Microsoft.DotNet.ProjectModel.Workspace/project.json diff --git a/src/Microsoft.DotNet.ProjectModel.Workspace/ProjectJsonWorkspace.cs b/src/Microsoft.DotNet.ProjectModel.Workspace/ProjectJsonWorkspace.cs new file mode 100644 index 000000000..81436dad1 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Workspace/ProjectJsonWorkspace.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.PortableExecutable; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.ProjectModel; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Workspaces +{ + public class ProjectJsonWorkspace : Workspace + { + private Dictionary _cache = new Dictionary(); + + private readonly string[] _projectPaths; + + public ProjectJsonWorkspace(string projectPath) : this(new[] { projectPath }) + { + } + + public ProjectJsonWorkspace(string[] projectPaths) : base(MefHostServices.DefaultHost, "Custom") + { + _projectPaths = projectPaths; + + Initialize(); + } + + private void Initialize() + { + foreach (var projectPath in _projectPaths) + { + AddProject(projectPath); + } + } + + private void AddProject(string projectPath) + { + // Get all of the specific projects (there is a project per framework) + foreach (var p in ProjectContext.CreateContextForEachFramework(projectPath)) + { + AddProject(p); + } + } + + private ProjectId AddProject(ProjectContext project) + { + // Create the framework specific project and add it to the workspace + var projectInfo = ProjectInfo.Create( + ProjectId.CreateNewId(), + VersionStamp.Create(), + project.ProjectFile.Name + "+" + project.TargetFramework, + project.ProjectFile.Name, + LanguageNames.CSharp, + project.ProjectFile.ProjectFilePath); + + OnProjectAdded(projectInfo); + + // TODO: ctor argument? + var configuration = "Debug"; + + var compilationOptions = project.ProjectFile.GetCompilerOptions(project.TargetFramework, configuration); + + var compilationSettings = ToCompilationSettings(compilationOptions, project.TargetFramework, project.ProjectFile.ProjectDirectory); + + OnParseOptionsChanged(projectInfo.Id, new CSharpParseOptions(compilationSettings.LanguageVersion, preprocessorSymbols: compilationSettings.Defines)); + + OnCompilationOptionsChanged(projectInfo.Id, compilationSettings.CompilationOptions); + + foreach (var file in project.ProjectFile.Files.SourceFiles) + { + AddSourceFile(projectInfo, file); + } + + var exporter = project.CreateExporter(configuration); + + foreach (var dependency in exporter.GetDependencies()) + { + var projectDependency = dependency.Library as ProjectDescription; + if (projectDependency != null) + { + var projectDependencyContext = ProjectContext.Create(projectDependency.Project.ProjectFilePath, projectDependency.Framework); + + var id = AddProject(projectDependencyContext); + + OnProjectReferenceAdded(projectInfo.Id, new ProjectReference(id)); + } + else + { + foreach (var asset in dependency.CompilationAssemblies) + { + OnMetadataReferenceAdded(projectInfo.Id, GetMetadataReference(asset.ResolvedPath)); + } + } + + foreach (var file in dependency.SourceReferences) + { + AddSourceFile(projectInfo, file); + } + } + + return projectInfo.Id; + } + + private void AddSourceFile(ProjectInfo projectInfo, string file) + { + using (var stream = File.OpenRead(file)) + { + var sourceText = SourceText.From(stream, encoding: Encoding.UTF8); + var id = DocumentId.CreateNewId(projectInfo.Id); + var version = VersionStamp.Create(); + + var loader = TextLoader.From(TextAndVersion.Create(sourceText, version)); + OnDocumentAdded(DocumentInfo.Create(id, file, filePath: file, loader: loader)); + } + } + + private MetadataReference GetMetadataReference(string path) + { + AssemblyMetadata assemblyMetadata; + if (!_cache.TryGetValue(path, out assemblyMetadata)) + { + using (var stream = File.OpenRead(path)) + { + var moduleMetadata = ModuleMetadata.CreateFromStream(stream, PEStreamOptions.PrefetchMetadata); + assemblyMetadata = AssemblyMetadata.Create(moduleMetadata); + _cache[path] = assemblyMetadata; + } + } + + return assemblyMetadata.GetReference(); + } + + private static CompilationSettings ToCompilationSettings(CommonCompilerOptions compilerOptions, + NuGetFramework targetFramework, + string projectDirectory) + { + var options = GetCompilationOptions(compilerOptions, projectDirectory); + + // Disable 1702 until roslyn turns this off by default + options = options.WithSpecificDiagnosticOptions(new Dictionary + { + { "CS1701", ReportDiagnostic.Suppress }, // Binding redirects + { "CS1702", ReportDiagnostic.Suppress }, + { "CS1705", ReportDiagnostic.Suppress } + }); + + AssemblyIdentityComparer assemblyIdentityComparer = + targetFramework.IsDesktop() ? + DesktopAssemblyIdentityComparer.Default : + null; + + options = options.WithAssemblyIdentityComparer(assemblyIdentityComparer); + + LanguageVersion languageVersion; + if (!Enum.TryParse(value: compilerOptions.LanguageVersion, + ignoreCase: true, + result: out languageVersion)) + { + languageVersion = LanguageVersion.CSharp6; + } + + var settings = new CompilationSettings + { + LanguageVersion = languageVersion, + Defines = compilerOptions.Defines ?? Enumerable.Empty(), + CompilationOptions = options + }; + + return settings; + } + + private static CSharpCompilationOptions GetCompilationOptions(CommonCompilerOptions compilerOptions, string projectDirectory) + { + var outputKind = compilerOptions.EmitEntryPoint.GetValueOrDefault() ? + OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary; + var options = new CSharpCompilationOptions(outputKind); + + string platformValue = compilerOptions.Platform; + bool allowUnsafe = compilerOptions.AllowUnsafe ?? false; + bool optimize = compilerOptions.Optimize ?? false; + bool warningsAsErrors = compilerOptions.WarningsAsErrors ?? false; + + Platform platform; + if (!Enum.TryParse(value: platformValue, ignoreCase: true, result: out platform)) + { + platform = Platform.AnyCpu; + } + + options = options + .WithAllowUnsafe(allowUnsafe) + .WithPlatform(platform) + .WithGeneralDiagnosticOption(warningsAsErrors ? ReportDiagnostic.Error : ReportDiagnostic.Default) + .WithOptimizationLevel(optimize ? OptimizationLevel.Release : OptimizationLevel.Debug); + + return AddSigningOptions(options, compilerOptions, projectDirectory); + } + + private static CSharpCompilationOptions AddSigningOptions(CSharpCompilationOptions options, CommonCompilerOptions compilerOptions, string projectDirectory) + { + var useOssSigning = compilerOptions.UseOssSigning == true; + var keyFile = compilerOptions.KeyFile; + + if (!string.IsNullOrEmpty(keyFile)) + { + keyFile = Path.GetFullPath(Path.Combine(projectDirectory, compilerOptions.KeyFile)); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || useOssSigning) + { + return options.WithCryptoPublicKey( + SnkUtils.ExtractPublicKey(File.ReadAllBytes(keyFile))); + } + + options = options.WithCryptoKeyFile(keyFile); + + return options.WithDelaySign(compilerOptions.DelaySign); + } + + return options; + } + + private class CompilationSettings + { + public LanguageVersion LanguageVersion { get; set; } + public IEnumerable Defines { get; set; } + public CSharpCompilationOptions CompilationOptions { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel.Workspace/SnkUtils.cs b/src/Microsoft.DotNet.ProjectModel.Workspace/SnkUtils.cs new file mode 100644 index 000000000..9cacec4df --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Workspace/SnkUtils.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Immutable; +using System.IO; + +namespace Microsoft.DotNet.ProjectModel.Workspaces +{ + internal static class SnkUtils + { + const byte PUBLICKEYBLOB = 0x06; + const byte PRIVATEKEYBLOB = 0x07; + + private const uint CALG_RSA_SIGN = 0x00002400; + private const uint CALG_SHA = 0x00008004; + + private const uint RSA1 = 0x31415352; //"RSA1" publickeyblob + private const uint RSA2 = 0x32415352; //"RSA2" privatekeyblob + + private const int VersionOffset = 1; + private const int ModulusLengthOffset = 12; + private const int ExponentOffset = 16; + private const int MagicPrivateKeyOffset = 8; + private const int MagicPublicKeyOffset = 20; + + public static ImmutableArray ExtractPublicKey(byte[] snk) + { + ValidateBlob(snk); + + if (snk[0] != PRIVATEKEYBLOB) + { + return ImmutableArray.Create(snk); + } + + var version = snk[VersionOffset]; + int modulusBitLength = ReadInt32(snk, ModulusLengthOffset); + uint exponent = (uint)ReadInt32(snk, ExponentOffset); + var modulus = new byte[modulusBitLength >> 3]; + + Array.Copy(snk, 20, modulus, 0, modulus.Length); + + return CreatePublicKey(version, exponent, modulus); + } + + private static void ValidateBlob(byte[] snk) + { + // 160 - the size of public key + if (snk.Length >= 160) + { + if (snk[0] == PRIVATEKEYBLOB && ReadInt32(snk, MagicPrivateKeyOffset) == RSA2 || // valid private key + snk[12] == PUBLICKEYBLOB && ReadInt32(snk, MagicPublicKeyOffset) == RSA1) // valid public key + { + return; + } + } + + throw new InvalidOperationException("Invalid key file."); + } + + private static int ReadInt32(byte[] array, int index) + { + return array[index] | array[index + 1] << 8 | array[index + 2] << 16 | array[index + 3] << 24; + } + + private static ImmutableArray CreatePublicKey(byte version, uint exponent, byte[] modulus) + { + using (var ms = new MemoryStream(160)) + using (var binaryWriter = new BinaryWriter(ms)) + { + binaryWriter.Write(CALG_RSA_SIGN); + binaryWriter.Write(CALG_SHA); + // total size of the rest of the blob (20 - size of RSAPUBKEY) + binaryWriter.Write(modulus.Length + 20); + binaryWriter.Write(PUBLICKEYBLOB); + binaryWriter.Write(version); + binaryWriter.Write((ushort)0x00000000); // reserved + binaryWriter.Write(CALG_RSA_SIGN); + binaryWriter.Write(RSA1); + binaryWriter.Write(modulus.Length << 3); + binaryWriter.Write(exponent); + binaryWriter.Write(modulus); + return ImmutableArray.Create(ms.ToArray()); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel.Workspace/project.json b/src/Microsoft.DotNet.ProjectModel.Workspace/project.json new file mode 100644 index 000000000..2c0095e9c --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Workspace/project.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "Microsoft.DotNet.ProjectModel": "1.0.0-*", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "1.1.0-*" + }, + "frameworks": { + "dnxcore50": { } + } +} \ No newline at end of file