From 46818ff3fabbe3939701ee2f00ab4501ee27992a Mon Sep 17 00:00:00 2001 From: Bryan Thornbury Date: Mon, 22 Aug 2016 12:21:34 -0700 Subject: [PATCH] Microsoft.DotNet.ProjectJsonMigration core library --- .../DefaultMigrationRuleSet.cs | 41 ++ .../MSBuildExtensions.cs | 114 +++++ ...icrosoft.DotNet.ProjectJsonMigration.xproj | 18 + .../MigrationRuleInputs.cs | 46 ++ .../MigrationSettings.cs | 58 +++ .../MigrationTrace.cs | 45 ++ .../Models/ItemMetadataValue.cs | 32 ++ .../Models/ProjectProperty.cs | 14 + .../ProjectMigrator.cs | 83 ++++ .../Properties/AssemblyInfo.cs | 3 + .../Rules/IMigrationRule.cs | 21 + .../Rules/MigrateBuildOptionsRule.cs | 405 ++++++++++++++++++ .../Rules/MigrateConfigurationsRule.cs | 70 +++ .../Rules/MigrateProjectDependenciesRule.cs | 95 ++++ .../Rules/MigratePublishOptionsRule.cs | 52 +++ .../Rules/MigrateRootOptionsRule.cs | 73 ++++ .../Rules/MigrateRuntimeOptionsRule.cs | 38 ++ .../Rules/MigrateScriptsRule.cs | 229 ++++++++++ .../Rules/MigrateTFMRule.cs | 93 ++++ .../MigrateXprojProjectReferencesRule.cs | 16 + .../Rules/SaveOutputProjectRule.cs | 29 ++ .../Rules/TemporaryMutateProjectJsonRule.cs | 98 +++++ .../project.json | 32 ++ .../transforms/AddBoolPropertyTransform.cs | 25 ++ .../transforms/AddItemTransform.cs | 124 ++++++ .../transforms/AddPropertyTransform.cs | 54 +++ .../transforms/AddStringPropertyTransform.cs | 25 ++ .../transforms/ConditionalTransform.cs | 38 ++ .../transforms/ITransform.cs | 9 + .../transforms/ITransformApplicator.cs | 26 ++ .../transforms/IncludeContextTransform.cs | 144 +++++++ .../transforms/TransformApplicator.cs | 170 ++++++++ 32 files changed, 2320 insertions(+) create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/DefaultMigrationRuleSet.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Microsoft.DotNet.ProjectJsonMigration.xproj create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/MigrationRuleInputs.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/MigrationSettings.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/MigrationTrace.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Models/ItemMetadataValue.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Models/ProjectProperty.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/IMigrationRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateBuildOptionsRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateConfigurationsRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateProjectDependenciesRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigratePublishOptionsRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateRootOptionsRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateRuntimeOptionsRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateScriptsRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateTFMRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateXprojProjectReferencesRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/SaveOutputProjectRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/Rules/TemporaryMutateProjectJsonRule.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/project.json create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddBoolPropertyTransform.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddItemTransform.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddPropertyTransform.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddStringPropertyTransform.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/transforms/ConditionalTransform.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/transforms/ITransform.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/transforms/ITransformApplicator.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/transforms/IncludeContextTransform.cs create mode 100644 src/Microsoft.DotNet.ProjectJsonMigration/transforms/TransformApplicator.cs diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/DefaultMigrationRuleSet.cs b/src/Microsoft.DotNet.ProjectJsonMigration/DefaultMigrationRuleSet.cs new file mode 100644 index 000000000..be900ff02 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/DefaultMigrationRuleSet.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class DefaultMigrationRuleSet : IMigrationRule + { + private IMigrationRule[] Rules => new IMigrationRule[] + { + new MigrateRootOptionsRule(), + new MigrateTFMRule(), + new MigrateBuildOptionsRule(), + new MigrateRuntimeOptionsRule(), + new MigratePublishOptionsRule(), + new MigrateProjectDependenciesRule(), + new MigrateConfigurationsRule(), + new MigrateScriptsRule(), + new TemporaryMutateProjectJsonRule(), + new SaveOutputProjectRule() + }; + + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + foreach (var rule in Rules) + { + MigrationTrace.Instance.WriteLine($"{nameof(DefaultMigrationRuleSet)}: Executing migration rule {nameof(rule)}"); + rule.Apply(migrationSettings, migrationRuleInputs); + } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs b/src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs new file mode 100644 index 000000000..a25306535 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs @@ -0,0 +1,114 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public static class MSBuildExtensions + { + public static IEnumerable PropertiesWithoutConditions( + this ProjectRootElement projectRoot) + { + return projectRoot.Properties + .Where(p => p.Condition == string.Empty + && p.AllParents.Count(parent => parent.Condition != string.Empty) == 0); + } + + public static IEnumerable ItemsWithoutConditions( + this ProjectRootElement projectRoot) + { + return projectRoot.Items + .Where(p => string.IsNullOrEmpty(p.Condition) + && p.AllParents.All(parent => string.IsNullOrEmpty(parent.Condition))); + } + + public static IEnumerable Includes( + this ProjectItemElement item) + { + return item.Include.Equals(string.Empty) ? Enumerable.Empty() : item.Include.Split(';'); + } + + public static IEnumerable Excludes( + this ProjectItemElement item) + { + return item.Exclude.Equals(string.Empty) ? Enumerable.Empty() : item.Exclude.Split(';'); + } + + public static IEnumerable AllConditions(this ProjectElement projectElement) + { + return new string[] { projectElement.Condition }.Concat(projectElement.AllParents.Select(p=> p.Condition)); + } + + public static IEnumerable CommonIncludes(this ProjectItemElement item, ProjectItemElement otherItem) + { + return item.Includes().Intersect(otherItem.Includes()); + } + + public static void RemoveIncludes(this ProjectItemElement item, IEnumerable includesToRemove) + { + item.Include = string.Join(";", item.Includes().Except(includesToRemove)); + } + + public static void AddIncludes(this ProjectItemElement item, IEnumerable includes) + { + item.Include = string.Join(";", item.Includes().Union(includes)); + } + + public static void AddExcludes(this ProjectItemElement item, IEnumerable excludes) + { + item.Exclude = string.Join(";", item.Excludes().Union(excludes)); + } + + public static ProjectMetadataElement GetMetadataWithName(this ProjectItemElement item, string name) + { + return item.Metadata.FirstOrDefault(m => m.Name.Equals(name, StringComparison.Ordinal)); + } + + public static bool ValueEquals(this ProjectMetadataElement metadata, ProjectMetadataElement otherMetadata) + { + return metadata.Value.Equals(otherMetadata.Value, StringComparison.Ordinal); + } + + public static void AddMetadata(this ProjectItemElement item, ICollection metadatas) + { + foreach (var metadata in metadatas) + { + item.AddMetadata(metadata); + } + } + + public static void RemoveIfEmpty(this ProjectElementContainer container) + { + if (!container.Children.Any()) + { + container.Parent.RemoveChild(container); + } + } + + public static void AddMetadata(this ProjectItemElement item, ProjectMetadataElement metadata) + { + var existingMetadata = item.GetMetadataWithName(metadata.Name); + + if (existingMetadata != default(ProjectMetadataElement) && !existingMetadata.ValueEquals(metadata)) + { + throw new Exception("Cannot merge metadata with the same name and different values"); + } + + if (existingMetadata == null) + { + Console.WriteLine(metadata.Name); + item.AddMetadata(metadata.Name, metadata.Value); + } + } + + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Microsoft.DotNet.ProjectJsonMigration.xproj b/src/Microsoft.DotNet.ProjectJsonMigration/Microsoft.DotNet.ProjectJsonMigration.xproj new file mode 100644 index 000000000..0bc54ed18 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Microsoft.DotNet.ProjectJsonMigration.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 0E083818-2320-4388-8007-4F720FD5C634 + Microsoft.DotNet.ProjectJsonMigration + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/MigrationRuleInputs.cs b/src/Microsoft.DotNet.ProjectJsonMigration/MigrationRuleInputs.cs new file mode 100644 index 000000000..45dde00d0 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/MigrationRuleInputs.cs @@ -0,0 +1,46 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class MigrationRuleInputs + { + public ProjectRootElement OutputMSBuildProject { get; } + + public ProjectItemGroupElement CommonItemGroup { get; } + + public ProjectPropertyGroupElement CommonPropertyGroup { get; } + + public IEnumerable ProjectContexts { get; } + + public ProjectContext DefaultProjectContext + { + get + { + return ProjectContexts.First(); + } + } + + public MigrationRuleInputs( + IEnumerable projectContexts, + ProjectRootElement outputProject, + ProjectItemGroupElement commonItemGroup, + ProjectPropertyGroupElement commonPropertyGroup) + { + ProjectContexts = projectContexts; + OutputMSBuildProject = outputProject; + CommonItemGroup = commonItemGroup; + CommonPropertyGroup = commonPropertyGroup; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/MigrationSettings.cs b/src/Microsoft.DotNet.ProjectJsonMigration/MigrationSettings.cs new file mode 100644 index 000000000..10d7646ba --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/MigrationSettings.cs @@ -0,0 +1,58 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class MigrationSettings + { + public string ProjectDirectory { get; } + public string OutputDirectory { get; } + public string SdkPackageVersion { get; } + public ProjectRootElement MSBuildProjectTemplate { get; } + + public MigrationSettings( + string projectDirectory, + string outputDirectory, + string sdkPackageVersion) + { + ProjectDirectory = projectDirectory; + OutputDirectory = outputDirectory; + SdkPackageVersion = sdkPackageVersion; + MSBuildProjectTemplate = null; + } + + public MigrationSettings( + string projectDirectory, + string outputDirectory, + string sdkPackageVersion, + ProjectRootElement msBuildProjectTemplate) + { + ProjectDirectory = projectDirectory; + OutputDirectory = outputDirectory; + SdkPackageVersion = sdkPackageVersion; + MSBuildProjectTemplate = msBuildProjectTemplate; + } + + public MigrationSettings( + string projectDirectory, + string outputDirectory, + string sdkPackageVersion, + string msbuildProjectTemplateFilePath) + { + ProjectDirectory = projectDirectory; + OutputDirectory = outputDirectory; + SdkPackageVersion = sdkPackageVersion; + MSBuildProjectTemplate = ProjectRootElement.Open(msbuildProjectTemplateFilePath); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/MigrationTrace.cs b/src/Microsoft.DotNet.ProjectJsonMigration/MigrationTrace.cs new file mode 100644 index 000000000..05cd7e7db --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/MigrationTrace.cs @@ -0,0 +1,45 @@ +using System; +using System.Text.RegularExpressions; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class MigrationTrace + { + public static MigrationTrace Instance { get; set; } + + static MigrationTrace () + { + Instance = new MigrationTrace(); + } + + public string EnableEnvironmentVariable => "DOTNET_MIGRATION_TRACE"; + + public bool IsEnabled + { + get + { +#if DEBUG + return true; +#else + return Environment.GetEnvironmentVariable(EnableEnvironmentVariable) != null; +#endif + } + } + + public void Write(string message) + { + if (IsEnabled) + { + Console.Write(message); + } + } + + public void WriteLine(string message) + { + if (IsEnabled) + { + Console.WriteLine(message); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Models/ItemMetadataValue.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Models/ItemMetadataValue.cs new file mode 100644 index 000000000..536defd6d --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Models/ItemMetadataValue.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class ItemMetadataValue + { + public string MetadataName { get; } + + private string _metadataValue; + private Func _metadataValueFunc; + + public ItemMetadataValue(string metadataName, string metadataValue) + { + MetadataName = metadataName; + _metadataValue = metadataValue; + } + + public ItemMetadataValue(string metadataName, Func metadataValueFunc) + { + MetadataName = metadataName; + _metadataValueFunc = metadataValueFunc; + } + + public string GetMetadataValue(T source) + { + return _metadataValue ?? _metadataValueFunc(source); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Models/ProjectProperty.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Models/ProjectProperty.cs new file mode 100644 index 000000000..9b7ddc395 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Models/ProjectProperty.cs @@ -0,0 +1,14 @@ +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class ProjectProperty + { + public string Name { get; } + public string Value { get; } + + public ProjectProperty(string name, string value) + { + Name = name; + Value = value; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs b/src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs new file mode 100644 index 000000000..b89c9f03b --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs @@ -0,0 +1,83 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json.Linq; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class ProjectMigrator + { + // TODO: Migrate PackOptions + // TODO: Support Mappings in IncludeContext Transformations + // TODO: Migrate Multi-TFM projects + // TODO: Tests + // TODO: Out of Scope + // - Globs that resolve to directories: /some/path/**/somedir + // - Migrating Deprecated project.jsons + // - Configuration dependent source exclusion + + public void Migrate(MigrationSettings migrationSettings) + { + var projectDirectory = migrationSettings.ProjectDirectory; + EnsureDirectoryExists(migrationSettings.OutputDirectory); + + var migrationRuleInputs = ComputeMigrationRuleInputs(migrationSettings); + VerifyInputs(migrationRuleInputs); + + new DefaultMigrationRuleSet().Apply(migrationSettings, migrationRuleInputs); + } + + private void EnsureDirectoryExists(string outputDirectory) + { + if (!Directory.Exists(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + } + } + + private MigrationRuleInputs ComputeMigrationRuleInputs(MigrationSettings migrationSettings) + { + var projectContexts = ProjectContext.CreateContextForEachFramework(migrationSettings.ProjectDirectory); + + var templateMSBuildProject = migrationSettings.MSBuildProjectTemplate ?? ProjectRootElement.Create(); + + var propertyGroup = templateMSBuildProject.AddPropertyGroup(); + var itemGroup = templateMSBuildProject.AddItemGroup(); + + return new MigrationRuleInputs(projectContexts, templateMSBuildProject, itemGroup, propertyGroup); + } + + private void VerifyInputs(MigrationRuleInputs migrationRuleInputs) + { + VerifyProject(migrationRuleInputs.ProjectContexts); + } + + private void VerifyProject(IEnumerable projectContexts) + { + if (projectContexts.Count() > 1) + { + throw new Exception("MultiTFM projects currently not supported."); + } + + if (projectContexts.Count() == 0) + { + throw new Exception("No projects found"); + } + + if (projectContexts.First().LockFile == null) + { + throw new Exception("Restore must be run prior to project migration."); + } + } + + + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Properties/AssemblyInfo.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..9ede032e7 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.DotNet.ProjectJsonMigration.Tests")] diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/IMigrationRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/IMigrationRule.cs new file mode 100644 index 000000000..a6283e180 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/IMigrationRule.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public interface IMigrationRule + { + void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs); + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateBuildOptionsRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateBuildOptionsRule.cs new file mode 100644 index 000000000..c66ce8005 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateBuildOptionsRule.cs @@ -0,0 +1,405 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; +using Microsoft.DotNet.ProjectModel.Files; +using Microsoft.DotNet.Cli.Utils; +using Newtonsoft.Json.Linq; + +using Project = Microsoft.DotNet.ProjectModel.Project; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + // TODO: Should All build options be protected by a configuration condition? + // This will prevent the entire merge issue altogether and sidesteps the problem of having a duplicate include with different excludes... + public class MigrateBuildOptionsRule : IMigrationRule + { + private AddPropertyTransform[] EmitEntryPointTransforms + => new AddPropertyTransform[] + { + new AddPropertyTransform("OutputType", "Exe", + compilerOptions => compilerOptions.EmitEntryPoint != null && compilerOptions.EmitEntryPoint.Value), + new AddPropertyTransform("TargetExt", ".dll", + compilerOptions => compilerOptions.EmitEntryPoint != null && compilerOptions.EmitEntryPoint.Value), + new AddPropertyTransform("OutputType", "Library", + compilerOptions => compilerOptions.EmitEntryPoint == null || !compilerOptions.EmitEntryPoint.Value) + }; + + private AddPropertyTransform[] KeyFileTransforms + => new AddPropertyTransform[] + { + new AddPropertyTransform("AssemblyOriginatorKeyFile", + compilerOptions => compilerOptions.KeyFile, + compilerOptions => !string.IsNullOrEmpty(compilerOptions.KeyFile)), + new AddPropertyTransform("SignAssembly", + "true", + compilerOptions => !string.IsNullOrEmpty(compilerOptions.KeyFile)) + }; + + private AddPropertyTransform DefineTransform => new AddPropertyTransform( + "DefineConstants", + compilerOptions => string.Join(";", compilerOptions.Defines), + compilerOptions => compilerOptions.Defines != null && compilerOptions.Defines.Any()); + + private AddPropertyTransform NoWarnTransform => new AddPropertyTransform( + "NoWarn", + compilerOptions => string.Join(";", compilerOptions.SuppressWarnings), + compilerOptions => compilerOptions.SuppressWarnings != null && compilerOptions.SuppressWarnings.Any()); + + private AddPropertyTransform PreserveCompilationContextTransform => + new AddPropertyTransform("PreserveCompilationContext", + compilerOptions => compilerOptions.PreserveCompilationContext.ToString().ToLower(), + compilerOptions => compilerOptions.PreserveCompilationContext != null && compilerOptions.PreserveCompilationContext.Value); + + private AddPropertyTransform WarningsAsErrorsTransform => + new AddPropertyTransform("WarningsAsErrors", + compilerOptions => compilerOptions.WarningsAsErrors.ToString().ToLower(), + compilerOptions => compilerOptions.WarningsAsErrors != null && compilerOptions.WarningsAsErrors.Value); + + private AddPropertyTransform AllowUnsafeTransform => + new AddPropertyTransform("AllowUnsafeBlocks", + compilerOptions => compilerOptions.AllowUnsafe.ToString().ToLower(), + compilerOptions => compilerOptions.AllowUnsafe != null && compilerOptions.AllowUnsafe.Value); + + private AddPropertyTransform OptimizeTransform => + new AddPropertyTransform("Optimize", + compilerOptions => compilerOptions.Optimize.ToString().ToLower(), + compilerOptions => compilerOptions.Optimize != null && compilerOptions.Optimize.Value); + + private AddPropertyTransform PlatformTransform => + new AddPropertyTransform("PlatformTarget", + compilerOptions => compilerOptions.Platform, + compilerOptions => !string.IsNullOrEmpty(compilerOptions.Platform)); + + private AddPropertyTransform LanguageVersionTransform => + new AddPropertyTransform("LangVersion", + compilerOptions => compilerOptions.LanguageVersion, + compilerOptions => !string.IsNullOrEmpty(compilerOptions.LanguageVersion)); + + private AddPropertyTransform DelaySignTransform => + new AddPropertyTransform("DelaySign", + compilerOptions => compilerOptions.DelaySign.ToString().ToLower(), + compilerOptions => compilerOptions.DelaySign != null && compilerOptions.DelaySign.Value); + + private AddPropertyTransform PublicSignTransform => + new AddPropertyTransform("PublicSign", + compilerOptions => compilerOptions.PublicSign.ToString().ToLower(), + compilerOptions => compilerOptions.PublicSign != null && compilerOptions.PublicSign.Value); + + private AddPropertyTransform DebugTypeTransform => + new AddPropertyTransform("DebugType", + compilerOptions => compilerOptions.DebugType, + compilerOptions => !string.IsNullOrEmpty(compilerOptions.DebugType)); + + private AddPropertyTransform XmlDocTransform => + new AddPropertyTransform("GenerateDocumentationFile", + compilerOptions => compilerOptions.GenerateXmlDocumentation.ToString().ToLower(), + compilerOptions => compilerOptions.GenerateXmlDocumentation != null && compilerOptions.GenerateXmlDocumentation.Value); + + // TODO: https://github.com/dotnet/sdk/issues/67 + private AddPropertyTransform XmlDocTransformFilePath => + new AddPropertyTransform("DocumentationFile", + @"$(OutputPath)\$(AssemblyName).xml", + compilerOptions => compilerOptions.GenerateXmlDocumentation != null && compilerOptions.GenerateXmlDocumentation.Value); + + private AddPropertyTransform OutputNameTransform => + new AddPropertyTransform("AssemblyName", + compilerOptions => compilerOptions.OutputName, + compilerOptions => !string.IsNullOrEmpty(compilerOptions.OutputName)); + + private IncludeContextTransform CompileFilesTransform => + new IncludeContextTransform("Compile", transformMappings: false); + + private IncludeContextTransform EmbedFilesTransform => + new IncludeContextTransform("EmbeddedResource", transformMappings: false); + + private IncludeContextTransform CopyToOutputFilesTransform => + new IncludeContextTransform("Content", transformMappings: true) + .WithMetadata("CopyToOutputDirectory", "PreserveNewest"); + + private Func> CompileFilesTransformExecute => + (compilerOptions, projectDirectory) => + CompileFilesTransform.Transform(GetCompileIncludeContext(compilerOptions, projectDirectory)); + + private Func> EmbedFilesTransformExecute => + (compilerOptions, projectDirectory) => + EmbedFilesTransform.Transform(GetEmbedIncludeContext(compilerOptions, projectDirectory)); + + private Func> CopyToOutputFilesTransformExecute => + (compilerOptions, projectDirectory) => + CopyToOutputFilesTransform.Transform(GetCopyToOutputIncludeContext(compilerOptions, projectDirectory)); + + private string _configuration; + private ProjectPropertyGroupElement _configurationPropertyGroup; + private ProjectItemGroupElement _configurationItemGroup; + + private List> _propertyTransforms; + private List>> _includeContextTransformExecutes; + + private ITransformApplicator _transformApplicator; + + public MigrateBuildOptionsRule(ITransformApplicator transformApplicator = null) + { + _transformApplicator = transformApplicator ?? new TransformApplicator(); + ConstructTransformLists(); + } + + public MigrateBuildOptionsRule( + string configuration, + ProjectPropertyGroupElement configurationPropertyGroup, + ProjectItemGroupElement configurationItemGroup, + ITransformApplicator transformApplicator = null) + { + _configuration = configuration; + _configurationPropertyGroup = configurationPropertyGroup; + _configurationItemGroup = configurationItemGroup; + _transformApplicator = transformApplicator ?? new TransformApplicator(); + + ConstructTransformLists(); + } + + private void ConstructTransformLists() + { + _propertyTransforms = new List>() + { + DefineTransform, + NoWarnTransform, + WarningsAsErrorsTransform, + AllowUnsafeTransform, + OptimizeTransform, + PlatformTransform, + LanguageVersionTransform, + DelaySignTransform, + PublicSignTransform, + DebugTypeTransform, + OutputNameTransform, + XmlDocTransform, + XmlDocTransformFilePath, + PreserveCompilationContextTransform + }; + + _propertyTransforms.AddRange(EmitEntryPointTransforms); + _propertyTransforms.AddRange(KeyFileTransforms); + + _includeContextTransformExecutes = new List>>() + { + CompileFilesTransformExecute, + EmbedFilesTransformExecute, + CopyToOutputFilesTransformExecute + }; + } + + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + var csproj = migrationRuleInputs.OutputMSBuildProject; + var projectContext = migrationRuleInputs.DefaultProjectContext; + + var propertyGroup = _configurationPropertyGroup ?? migrationRuleInputs.CommonPropertyGroup; + var itemGroup = _configurationItemGroup ?? migrationRuleInputs.CommonItemGroup; + + var compilerOptions = projectContext.ProjectFile.GetCompilerOptions(projectContext.TargetFramework, null); + var configurationCompilerOptions = + projectContext.ProjectFile.GetCompilerOptions(projectContext.TargetFramework, _configuration); + + // If we're in a configuration, we need to be careful not to overwrite values from BuildOptions + // without a configuration + if (_configuration == null) + { + CleanExistingProperties(csproj); + + PerformPropertyAndItemMappings( + compilerOptions, + propertyGroup, + itemGroup, + _transformApplicator, + migrationSettings.ProjectDirectory); + } + else + { + PerformConfigurationPropertyAndItemMappings( + compilerOptions, + configurationCompilerOptions, + propertyGroup, + itemGroup, + _transformApplicator, + migrationSettings.ProjectDirectory); + } + } + + private void PerformConfigurationPropertyAndItemMappings( + CommonCompilerOptions compilerOptions, + CommonCompilerOptions configurationCompilerOptions, + ProjectPropertyGroupElement propertyGroup, + ProjectItemGroupElement itemGroup, + ITransformApplicator transformApplicator, + string projectDirectory) + { + foreach (var transform in _propertyTransforms) + { + var nonConfigurationOutput = transform.Transform(compilerOptions); + var configurationOutput = transform.Transform(configurationCompilerOptions); + + if (!PropertiesAreEqual(nonConfigurationOutput, configurationOutput)) + { + transformApplicator.Execute(configurationOutput, propertyGroup); + } + } + + foreach (var includeContextTransformExecute in _includeContextTransformExecutes) + { + var nonConfigurationOutput = includeContextTransformExecute(compilerOptions, projectDirectory); + var configurationOutput = includeContextTransformExecute(configurationCompilerOptions, projectDirectory).ToArray(); + + if (configurationOutput != null && nonConfigurationOutput != null) + { + // TODO: HACK: this is leaky, see top comments, the throw at least covers the scenario + ThrowIfConfigurationHasAdditionalExcludes(configurationOutput, nonConfigurationOutput); + RemoveCommonIncludes(configurationOutput, nonConfigurationOutput); + configurationOutput = configurationOutput.Where(i => i != null && !string.IsNullOrEmpty(i.Include)).ToArray(); + } + + // Don't merge with existing items when doing a configuration + transformApplicator.Execute(configurationOutput, itemGroup, mergeExisting: false); + } + } + + private void ThrowIfConfigurationHasAdditionalExcludes(IEnumerable configurationOutput, IEnumerable nonConfigurationOutput) + { + foreach (var item1 in configurationOutput) + { + if (item1 == null) + { + continue; + } + + var item2Excludes = new HashSet(); + foreach (var item2 in nonConfigurationOutput) + { + if (item2 != null) + { + item2Excludes.UnionWith(item2.Excludes()); + } + } + var configurationHasAdditionalExclude = + item1.Excludes().Any(exclude => item2Excludes.All(item2Exclude => item2Exclude != exclude)); + + if (configurationHasAdditionalExclude) + { + Console.WriteLine("EXCLUDE"); + Console.WriteLine(item1.Exclude); + Console.WriteLine(item2Excludes.ToString()); + throw new Exception("Unable to migrate projects with excluded files in configurations."); + } + } + } + + private void RemoveCommonIncludes(IEnumerable itemsToRemoveFrom, + IEnumerable otherItems) + { + foreach (var item1 in itemsToRemoveFrom) + { + if (item1 == null) + { + continue; + } + foreach ( + var item2 in + otherItems.Where( + i => i != null && string.Equals(i.ItemType, item1.ItemType, StringComparison.Ordinal))) + { + item1.Include = string.Join(";", item1.Includes().Except(item2.Includes())); + } + } + } + + private bool PropertiesAreEqual(ProjectPropertyElement nonConfigurationOutput, ProjectPropertyElement configurationOutput) + { + if (configurationOutput != null && nonConfigurationOutput != null) + { + return string.Equals(nonConfigurationOutput.Value, configurationOutput.Value, StringComparison.Ordinal); + } + + return configurationOutput == nonConfigurationOutput; + } + + private void PerformPropertyAndItemMappings( + CommonCompilerOptions compilerOptions, + ProjectPropertyGroupElement propertyGroup, + ProjectItemGroupElement itemGroup, + ITransformApplicator transformApplicator, + string projectDirectory) + { + foreach (var transform in _propertyTransforms) + { + transformApplicator.Execute(transform.Transform(compilerOptions), propertyGroup); + } + + foreach (var includeContextTransformExecute in _includeContextTransformExecutes) + { + transformApplicator.Execute( + includeContextTransformExecute(compilerOptions, projectDirectory), + itemGroup, + mergeExisting: true); + } + } + + private void CleanExistingProperties(ProjectRootElement csproj) + { + var existingPropertiesToRemove = new [] {"OutputType", "TargetExt"}; + + foreach (var propertyName in existingPropertiesToRemove) + { + var properties = csproj.Properties.Where(p => p.Name == propertyName); + + foreach (var property in properties) + { + property.Parent.RemoveChild(property); + } + } + } + + private IncludeContext GetCompileIncludeContext(CommonCompilerOptions compilerOptions, string projectDirectory) + { + // Defaults from src/Microsoft.DotNet.ProjectModel/ProjectReader.cs #L596 + return compilerOptions.CompileInclude ?? + new IncludeContext( + projectDirectory, + "compile", + new JObject(), + ProjectFilesCollection.DefaultCompileBuiltInPatterns, + ProjectFilesCollection.DefaultBuiltInExcludePatterns); + } + + private IncludeContext GetEmbedIncludeContext(CommonCompilerOptions compilerOptions, string projectDirectory) + { + // Defaults from src/Microsoft.DotNet.ProjectModel/ProjectReader.cs #L602 + return compilerOptions.EmbedInclude ?? + new IncludeContext( + projectDirectory, + "embed", + new JObject(), + ProjectFilesCollection.DefaultResourcesBuiltInPatterns, + ProjectFilesCollection.DefaultBuiltInExcludePatterns); + } + + private IncludeContext GetCopyToOutputIncludeContext(CommonCompilerOptions compilerOptions, string projectDirectory) + { + // Defaults from src/Microsoft.DotNet.ProjectModel/ProjectReader.cs #608 + return compilerOptions.CopyToOutputInclude ?? + new IncludeContext( + projectDirectory, + "copyToOutput", + new JObject(), + null, + ProjectFilesCollection.DefaultPublishExcludePatterns); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateConfigurationsRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateConfigurationsRule.cs new file mode 100644 index 000000000..c9b85916c --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateConfigurationsRule.cs @@ -0,0 +1,70 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class MigrateConfigurationsRule : IMigrationRule + { + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + var projectContext = migrationRuleInputs.DefaultProjectContext; + var configurations = projectContext.ProjectFile.GetConfigurations().ToList(); + + if (!configurations.Any()) + { + return; + } + + foreach (var configuration in configurations) + { + MigrateConfiguration(configuration, migrationSettings, migrationRuleInputs); + } + } + + private void MigrateConfiguration( + string configuration, + MigrationSettings migrationSettings, + MigrationRuleInputs migrationRuleInputs) + { + var csproj = migrationRuleInputs.OutputMSBuildProject; + + var propertyGroup = CreatePropertyGroupAtEndOfProject(csproj); + var itemGroup = CreateItemGroupAtEndOfProject(csproj); + + var configurationCondition = $" '$(Configuration)' == '{configuration}' "; + propertyGroup.Condition = configurationCondition; + itemGroup.Condition = configurationCondition; + + new MigrateBuildOptionsRule(configuration, propertyGroup, itemGroup) + .Apply(migrationSettings, migrationRuleInputs); + + propertyGroup.RemoveIfEmpty(); + itemGroup.RemoveIfEmpty(); + } + + private ProjectPropertyGroupElement CreatePropertyGroupAtEndOfProject(ProjectRootElement csproj) + { + var propertyGroup = csproj.CreatePropertyGroupElement(); + csproj.InsertBeforeChild(propertyGroup, csproj.LastChild); + return propertyGroup; + } + + private ProjectItemGroupElement CreateItemGroupAtEndOfProject(ProjectRootElement csproj) + { + var itemGroup = csproj.CreateItemGroupElement(); + csproj.InsertBeforeChild(itemGroup, csproj.LastChild); + return itemGroup; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateProjectDependenciesRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateProjectDependenciesRule.cs new file mode 100644 index 000000000..0b78b044e --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateProjectDependenciesRule.cs @@ -0,0 +1,95 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; +using Microsoft.DotNet.ProjectModel.Compilation; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.DotNet.Tools.Common; +using Project = Microsoft.DotNet.ProjectModel.Project; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class MigrateProjectDependenciesRule : IMigrationRule + { + private readonly ITransformApplicator _transformApplicator; + private string _projectDirectory; + + public MigrateProjectDependenciesRule(TransformApplicator transformApplicator = null) + { + _transformApplicator = transformApplicator ?? new TransformApplicator(); + } + + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + _projectDirectory = migrationSettings.ProjectDirectory; + + var csproj = migrationRuleInputs.OutputMSBuildProject; + var projectContext = migrationRuleInputs.DefaultProjectContext; + var projectExports = projectContext.CreateExporter("_").GetDependencies(LibraryType.Project); + + var projectDependencyTransformResults = + projectExports.Select(projectExport => ProjectDependencyTransform.Transform(projectExport)); + var propertyTransformResults = new [] + { + AutoUnifyTransform.Transform(true), + DesignTimeAutoUnifyTransform.Transform(true) + }; + + if (projectDependencyTransformResults.Any()) + { + // Use a new item group for the project references, but the common for properties + var propertyGroup = migrationRuleInputs.CommonPropertyGroup; + var itemGroup = csproj.AddItemGroup(); + + foreach (var projectDependencyTransformResult in projectDependencyTransformResults) + { + _transformApplicator.Execute(projectDependencyTransformResult, itemGroup); + } + + foreach (var propertyTransformResult in propertyTransformResults) + { + _transformApplicator.Execute(propertyTransformResult, propertyGroup); + } + } + + } + + private AddPropertyTransform AutoUnifyTransform => new AddPropertyTransform( + "AutoUnify", + "true", + b => true); + + private AddPropertyTransform DesignTimeAutoUnifyTransform => new AddPropertyTransform( + "DesignTimeAutoUnify", + "true", + b => true); + + private AddItemTransform ProjectDependencyTransform => new AddItemTransform( + "ProjectReference", + export => + { + if (!export.Library.Resolved) + { + throw new Exception("Cannot migrate unresolved project dependency, please ensure restore has been run."); + } + + var projectFile = ((ProjectDescription)export.Library).Project.ProjectFilePath; + var projectDir = Path.GetDirectoryName(projectFile); + var migratedProjectFileName = Path.GetFileName(projectDir) + ".csproj"; + var relativeProjectDir = PathUtility.GetRelativePath(_projectDirectory + "/", projectDir); + + return Path.Combine(relativeProjectDir, migratedProjectFileName); + }, + export => "", + export => true); + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigratePublishOptionsRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigratePublishOptionsRule.cs new file mode 100644 index 000000000..52a00095e --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigratePublishOptionsRule.cs @@ -0,0 +1,52 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; +using Microsoft.DotNet.ProjectModel.Files; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class MigratePublishOptionsRule : IMigrationRule + { + private readonly ITransformApplicator _transformApplicator; + + public MigratePublishOptionsRule(TransformApplicator transformApplicator = null) + { + _transformApplicator = transformApplicator ?? new TransformApplicator(); + } + + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + var csproj = migrationRuleInputs.OutputMSBuildProject; + var projectContext = migrationRuleInputs.DefaultProjectContext; + + var transformResults = new[] + { + CopyToOutputFilesTransform.Transform(projectContext.ProjectFile.PublishOptions) + }; + + if (transformResults.Any(t => t != null && t.Any())) + { + var itemGroup = migrationRuleInputs.CommonItemGroup; + _transformApplicator.Execute( + CopyToOutputFilesTransform.Transform(projectContext.ProjectFile.PublishOptions), + itemGroup, + mergeExisting: true); + } + } + + private IncludeContextTransform CopyToOutputFilesTransform => + new IncludeContextTransform("Content", transformMappings: true) + .WithMetadata("CopyToOutputDirectory", "None") + .WithMetadata("CopyToPublishDirectory", "PreserveNewest"); + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateRootOptionsRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateRootOptionsRule.cs new file mode 100644 index 000000000..357c1901e --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateRootOptionsRule.cs @@ -0,0 +1,73 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; +using Microsoft.DotNet.ProjectModel.Files; +using NuGet.Versioning; +using Project = Microsoft.DotNet.ProjectModel.Project; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class MigrateRootOptionsRule : IMigrationRule + { + private readonly ITransformApplicator _transformApplicator; + private readonly AddPropertyTransform[] _transforms; + + public MigrateRootOptionsRule(TransformApplicator transformApplicator = null) + { + _transformApplicator = transformApplicator ?? new TransformApplicator(); + + _transforms = new[] + { + DescriptionTransform, + CopyrightTransform, + TitleTransform, + LanguageTransform, + VersionTransform + }; + } + + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + var projectContext = migrationRuleInputs.DefaultProjectContext; + + var transformResults = _transforms.Select(t => t.Transform(projectContext.ProjectFile)).ToArray(); + if (transformResults.Any()) + { + var propertyGroup = migrationRuleInputs.CommonPropertyGroup; + + foreach (var transformResult in transformResults) + { + _transformApplicator.Execute(transformResult, propertyGroup); + } + } + } + + private AddPropertyTransform DescriptionTransform => new AddPropertyTransform("Description", + project => project.Description, + project => !string.IsNullOrEmpty(project.Description)); + + private AddPropertyTransform CopyrightTransform => new AddPropertyTransform("Copyright", + project => project.Copyright, + project => !string.IsNullOrEmpty(project.Copyright)); + + private AddPropertyTransform TitleTransform => new AddPropertyTransform("AssemblyTitle", + project => project.Title, + project => !string.IsNullOrEmpty(project.Title)); + + private AddPropertyTransform LanguageTransform => new AddPropertyTransform("NeutralLanguage", + project => project.Language, + project => !string.IsNullOrEmpty(project.Language)); + + private AddPropertyTransform VersionTransform => new AddPropertyTransform("VersionPrefix", + project => project.Version.ToString(), p => true); + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateRuntimeOptionsRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateRuntimeOptionsRule.cs new file mode 100644 index 000000000..3b59c7a63 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateRuntimeOptionsRule.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class MigrateRuntimeOptionsRule : IMigrationRule + { + private static readonly string s_runtimeOptionsFileName = "runtimeconfig.template.json"; + + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + var projectContext = migrationRuleInputs.DefaultProjectContext; + var raw = projectContext.ProjectFile.RawRuntimeOptions; + var outputRuntimeOptionsFile = Path.Combine(migrationSettings.OutputDirectory, s_runtimeOptionsFileName); + + if (!string.IsNullOrEmpty(raw)) + { + if (File.Exists(outputRuntimeOptionsFile)) + { + throw new Exception("Runtime options file already exists. Has migration already been run?"); + } + + File.WriteAllText(outputRuntimeOptionsFile, raw); + } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateScriptsRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateScriptsRule.cs new file mode 100644 index 000000000..9eb18cf72 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateScriptsRule.cs @@ -0,0 +1,229 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Microsoft.DotNet.Cli.Utils.CommandParsing; +using Newtonsoft.Json; +using Microsoft.DotNet.ProjectModel.Files; +using Microsoft.DotNet.Tools.Common; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class MigrateScriptsRule : IMigrationRule + { + private static readonly string s_unixScriptExtension = ".sh"; + private static readonly string s_windowsScriptExtension = ".cmd"; + + private readonly ITransformApplicator _transformApplicator; + + public MigrateScriptsRule(TransformApplicator transformApplicator = null) + { + _transformApplicator = transformApplicator ?? new TransformApplicator(); + } + + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + var csproj = migrationRuleInputs.OutputMSBuildProject; + var projectContext = migrationRuleInputs.DefaultProjectContext; + var scripts = projectContext.ProjectFile.Scripts; + + foreach (var scriptSet in scripts) + { + MigrateScriptSet(csproj, migrationRuleInputs.CommonPropertyGroup, scriptSet.Value, scriptSet.Key); + } + } + + public ProjectTargetElement MigrateScriptSet(ProjectRootElement csproj, + ProjectPropertyGroupElement propertyGroup, + IEnumerable scriptCommands, + string scriptSetName) + { + var target = CreateTarget(csproj, scriptSetName); + var count = 0; + foreach (var scriptCommand in scriptCommands) + { + var scriptExtensionPropertyName = AddScriptExtension(propertyGroup, scriptCommand, (++count).ToString()); + AddExec(target, FormatScriptCommand(scriptCommand, scriptExtensionPropertyName)); + } + + return target; + } + + private string AddScriptExtension(ProjectPropertyGroupElement propertyGroup, string scriptCommandline, string scriptId) + { + var scriptArguments = CommandGrammar.Process( + scriptCommandline, + (s) => null, + preserveSurroundingQuotes: false); + + scriptArguments = scriptArguments.Where(argument => !string.IsNullOrEmpty(argument)).ToArray(); + var scriptCommand = scriptArguments.First(); + var propertyName = $"MigratedScriptExtension_{scriptId}"; + + var windowsScriptExtensionProperty = propertyGroup.AddProperty(propertyName, + s_windowsScriptExtension); + var unixScriptExtensionProperty = propertyGroup.AddProperty(propertyName, + s_unixScriptExtension); + + windowsScriptExtensionProperty.Condition = + $" '$(OS)' == 'Windows_NT' and Exists('{scriptCommand}{s_windowsScriptExtension}') "; + unixScriptExtensionProperty.Condition = + $" '$(OS)' != 'Windows_NT' and Exists('{scriptCommand}{s_unixScriptExtension}') "; + + return propertyName; + } + + internal string FormatScriptCommand(string scriptCommandline, string scriptExtensionPropertyName) + { + var command = AddScriptExtensionPropertyToCommandLine(scriptCommandline, scriptExtensionPropertyName); + return ReplaceScriptVariables(command); + } + + internal string AddScriptExtensionPropertyToCommandLine(string scriptCommandline, + string scriptExtensionPropertyName) + { + var scriptArguments = CommandGrammar.Process( + scriptCommandline, + (s) => null, + preserveSurroundingQuotes: true); + + scriptArguments = scriptArguments.Where(argument => !string.IsNullOrEmpty(argument)).ToArray(); + + var scriptCommand = scriptArguments.First(); + var trimmedCommand = scriptCommand.Trim('\"').Trim('\''); + + // Path.IsPathRooted only looks at paths conforming to the current os, + // we need to account for all things + if (!IsPathRootedForAnyOS(trimmedCommand)) + { + scriptCommand = @".\" + scriptCommand; + } + + if (scriptCommand.EndsWith("\"") || scriptCommand.EndsWith("'")) + { + var endChar = scriptCommand.Last(); + scriptCommand = $"{scriptCommand.TrimEnd(endChar)}$({scriptExtensionPropertyName}){endChar}"; + } + else + { + scriptCommand += $"$({scriptExtensionPropertyName})"; + } + + var command = string.Join(" ", new[] {scriptCommand}.Concat(scriptArguments.Skip(1))); + return command; + } + + internal string ReplaceScriptVariables(string command) + { + foreach (var scriptVariableEntry in ScriptVariableToMSBuildMap) + { + var scriptVariableName = scriptVariableEntry.Key; + var msbuildMapping = scriptVariableEntry.Value; + + if (command.Contains($"%{scriptVariableName}%")) + { + if (msbuildMapping == null) + { + throw new Exception( + $"{scriptVariableName} is currently an unsupported script variable for project migration"); + } + + command = command.Replace($"%{scriptVariableName}%", msbuildMapping); + } + } + + return command; + } + + private bool IsPathRootedForAnyOS(string path) + { + return path.StartsWith("/") || path.Substring(1).StartsWith(":\\"); + } + + private ProjectTargetElement CreateTarget(ProjectRootElement csproj, string scriptSetName) + { + var targetName = $"{scriptSetName[0].ToString().ToUpper()}{string.Concat(scriptSetName.Skip(1))}Script"; + var targetHookInfo = ScriptSetToMSBuildHookTargetMap[scriptSetName]; + + var target = csproj.AddTarget(targetName); + if (targetHookInfo.IsRunBefore) + { + target.BeforeTargets = targetHookInfo.TargetName; + } + else + { + target.AfterTargets = targetHookInfo.TargetName; + } + + return target; + } + + private void AddExec(ProjectTargetElement target, string command) + { + var task = target.AddTask("Exec"); + task.SetParameter("Command", command); + } + + // ProjectJson Script Set Name to + private static Dictionary ScriptSetToMSBuildHookTargetMap => new Dictionary() + { + { "precompile", new TargetHookInfo(true, "Build") }, + { "postcompile", new TargetHookInfo(false, "Build") }, + { "prepublish", new TargetHookInfo(true, "Publish") }, + { "postpublish", new TargetHookInfo(false, "Publish") } + }; + + private static Dictionary ScriptVariableToMSBuildMap => new Dictionary() + { + { "compile:TargetFramework", null }, // TODO: Need Short framework name in CSProj + { "compile:ResponseFile", null }, // Not migrated + { "compile:CompilerExitCode", null }, // Not migrated + { "compile:RuntimeOutputDir", null }, // Not migrated + { "compile:RuntimeIdentifier", null },// TODO: Need Rid in CSProj + + { "publish:TargetFramework", null }, // TODO: Need Short framework name in CSProj + { "publish:Runtime", null }, // TODO: Need Rid in CSProj + + { "compile:FullTargetFramework", "$(TargetFrameworkIdentifier)=$(TargetFrameworkVersion)" }, + { "compile:Configuration", "$(Configuration)" }, + { "compile:OutputFile", "$(TargetPath)" }, + { "compile:OutputDir", "$(TargetDir)" }, + + { "publish:ProjectPath", "$(MSBuildThisFileDirectory)" }, + { "publish:Configuration", "$(Configuration)" }, + { "publish:OutputPath", "$(TargetDir)" }, + { "publish:FullTargetFramework", "$(TargetFrameworkIdentifier)=$(TargetFrameworkVersion)" }, + + { "project:Directory", "$(MSBuildProjectDirectory)" }, + { "project:Name", "$(MSBuildThisFileName)" }, + { "project:Version", "$(Version)" } + }; + + private class TargetHookInfo + { + public bool IsRunBefore { get; } + public string TargetName { get; } + + public string BeforeAfterTarget + { + get + { + return IsRunBefore ? "BeforeTargets" : "AfterTargets"; + } + } + + public TargetHookInfo(bool isRunBefore, string targetName) + { + IsRunBefore = isRunBefore; + TargetName = targetName; + } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateTFMRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateTFMRule.cs new file mode 100644 index 000000000..343c33667 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateTFMRule.cs @@ -0,0 +1,93 @@ +using System; +using System.Text; +using System.Globalization; +using Microsoft.Build.Construction; +using System.Linq; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + // TODO: Support Multi-TFM + public class MigrateTFMRule : IMigrationRule + { + private readonly ITransformApplicator _transformApplicator; + private readonly AddPropertyTransform[] _transforms; + + public MigrateTFMRule(TransformApplicator transformApplicator = null) + { + _transformApplicator = transformApplicator ?? new TransformApplicator(); + + _transforms = new AddPropertyTransform[] + { + OutputPathTransform, + FrameworkIdentifierTransform, + FrameworkVersionTransform + }; + } + + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + var csproj = migrationRuleInputs.OutputMSBuildProject; + var propertyGroup = migrationRuleInputs.CommonPropertyGroup; + + CleanExistingProperties(csproj); + + foreach (var transform in _transforms) + { + _transformApplicator.Execute( + transform.Transform(migrationRuleInputs.DefaultProjectContext.TargetFramework), + propertyGroup); + } + } + + private void CleanExistingProperties(ProjectRootElement csproj) + { + var existingPropertiesToRemove = new string[] { "TargetFrameworkIdentifier", "TargetFrameworkVersion" }; + foreach (var propertyName in existingPropertiesToRemove) + { + var properties = csproj.Properties.Where(p => p.Name == propertyName); + + foreach (var property in properties) + { + property.Parent.RemoveChild(property); + } + } + } + + // Taken from private NuGet.Frameworks method + // https://github.com/NuGet/NuGet.Client/blob/33b8f85a94b01f805f1e955f9b68992b297fad6e/src/NuGet.Core/NuGet.Frameworks/NuGetFramework.cs#L234 + private static string GetDisplayVersion(Version version) + { + var sb = new StringBuilder(String.Format(CultureInfo.InvariantCulture, "{0}.{1}", version.Major, version.Minor)); + + if (version.Build > 0 + || version.Revision > 0) + { + sb.AppendFormat(CultureInfo.InvariantCulture, ".{0}", version.Build); + + if (version.Revision > 0) + { + sb.AppendFormat(CultureInfo.InvariantCulture, ".{0}", version.Revision); + } + } + + return sb.ToString(); + } + + // TODO: When we have this inferred in the sdk targets, we won't need this + private AddPropertyTransform OutputPathTransform => + new AddPropertyTransform("OutputPath", + f => $"bin/$(Configuration)/{f.GetShortFolderName()}", + f => true); + + private AddPropertyTransform FrameworkIdentifierTransform => + new AddPropertyTransform("TargetFrameworkIdentifier", + f => f.Framework, + f => true); + + private AddPropertyTransform FrameworkVersionTransform => + new AddPropertyTransform("TargetFrameworkVersion", + f => "v" + GetDisplayVersion(f.Version), + f => true); + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateXprojProjectReferencesRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateXprojProjectReferencesRule.cs new file mode 100644 index 000000000..1b5eee495 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateXprojProjectReferencesRule.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + // TODO: XProj ProjectToProject references + public class MigrateXprojProjectReferencesRule : IMigrationRule + { + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/SaveOutputProjectRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/SaveOutputProjectRule.cs new file mode 100644 index 000000000..adfba4fbb --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/SaveOutputProjectRule.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; +using Microsoft.DotNet.ProjectModel.Files; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class SaveOutputProjectRule : IMigrationRule + { + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + var outputName = Path.GetFileNameWithoutExtension( + migrationRuleInputs.DefaultProjectContext.GetOutputPaths("_").CompilationFiles.Assembly); + + var outputProject = Path.Combine(migrationSettings.OutputDirectory, outputName + ".csproj"); + + migrationRuleInputs.OutputMSBuildProject.Save(outputProject); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/TemporaryMutateProjectJsonRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/TemporaryMutateProjectJsonRule.cs new file mode 100644 index 000000000..c15f751b6 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/TemporaryMutateProjectJsonRule.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using System.IO; +using Newtonsoft.Json.Linq; +using System.Text; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + /// + /// This rule is temporary while project.json still exists in the new project system. + /// It renames your existing project.json (if output directory is the current project directory), + /// creates a copy, then mutates that copy. + /// + /// Mutations: + /// - inject a dependency on the Microsoft.SDK targets + /// - removing the "runtimes" node. + /// + public class TemporaryMutateProjectJsonRule : IMigrationRule + { + private static string s_sdkPackageName => "Microsoft.DotNet.Core.Sdk"; + + + public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + bool shouldRenameOldProject = PathsAreEqual(migrationSettings.OutputDirectory, migrationSettings.ProjectDirectory); + + if (!shouldRenameOldProject && File.Exists(Path.Combine(migrationSettings.OutputDirectory, "project.json"))) + { + // TODO: should there be a setting to overwrite anything in output directory? + throw new Exception("Existing project.json found in output directory."); + } + + var sourceProjectFile = Path.Combine(migrationSettings.ProjectDirectory, "project.json"); + var destinationProjectFile = Path.Combine(migrationSettings.OutputDirectory, "project.json"); + if (shouldRenameOldProject) + { + var renamedProjectFile = Path.Combine(migrationSettings.ProjectDirectory, "project.migrated.json"); + File.Move(sourceProjectFile, renamedProjectFile); + sourceProjectFile = renamedProjectFile; + } + + var json = CreateDestinationProjectFile(sourceProjectFile, destinationProjectFile); + InjectSdkReference(json, s_sdkPackageName, migrationSettings.SdkPackageVersion); + RemoveRuntimesNode(json); + + File.WriteAllText(destinationProjectFile, json.ToString()); + } + + private JObject CreateDestinationProjectFile(string sourceProjectFile, string destinationProjectFile) + { + File.Copy(sourceProjectFile, destinationProjectFile); + return JObject.Parse(File.ReadAllText(destinationProjectFile)); + } + + private void InjectSdkReference(JObject json, string sdkPackageName, string sdkPackageVersion) + { + JToken dependenciesNode; + if (json.TryGetValue("dependencies", out dependenciesNode)) + { + var dependenciesNodeObject = dependenciesNode.Value(); + dependenciesNodeObject.Add(sdkPackageName, sdkPackageVersion); + } + else + { + var dependenciesNodeObject = new JObject(); + dependenciesNodeObject.Add(sdkPackageName, sdkPackageVersion); + + json.Add("dependencies", dependenciesNodeObject); + } + } + + private void RemoveRuntimesNode(JObject json) + { + json.Remove("runtimes"); + } + + private bool PathsAreEqual(params string[] paths) + { + var normalizedPaths = paths.Select(path => Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar)).ToList(); + + for (int i=1; i + { + public AddBoolPropertyTransform(string propertyName) + : base(propertyName, b => b.ToString(), b => b) { } + + public AddBoolPropertyTransform(string propertyName, Func condition) + : base(propertyName, b => b.ToString(), condition) { } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddItemTransform.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddItemTransform.cs new file mode 100644 index 000000000..fde54f3ed --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddItemTransform.cs @@ -0,0 +1,124 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class AddItemTransform : ConditionalTransform + { + private ProjectRootElement _itemObjectGenerator = ProjectRootElement.Create(); + + private string _itemName; + private string _includeValue; + private string _excludeValue; + + private Func _includeValueFunc; + private Func _excludeValueFunc; + + private bool _mergeExisting; + + private List> _metadata = new List>(); + + public AddItemTransform( + string itemName, + IEnumerable includeValues, + IEnumerable excludeValues, + Func condition, + bool mergeExisting = false) + : this(itemName, string.Join(";", includeValues), string.Join(";", excludeValues), condition, mergeExisting) { } + + public AddItemTransform( + string itemName, + Func includeValueFunc, + Func excludeValueFunc, + Func condition) + : base(condition) + { + _itemName = itemName; + _includeValueFunc = includeValueFunc; + _excludeValueFunc = excludeValueFunc; + } + + public AddItemTransform( + string itemName, + string includeValue, + Func excludeValueFunc, + Func condition) + : base(condition) + { + _itemName = itemName; + _includeValue = includeValue; + _excludeValueFunc = excludeValueFunc; + } + + public AddItemTransform( + string itemName, + Func includeValueFunc, + string excludeValue, + Func condition) + : base(condition) + { + _itemName = itemName; + _includeValueFunc = includeValueFunc; + _excludeValue = excludeValue; + } + + public AddItemTransform( + string itemName, + string includeValue, + string excludeValue, + Func condition, + bool mergeExisting=false) + : base(condition) + { + _itemName = itemName; + _includeValue = includeValue; + _excludeValue = excludeValue; + _mergeExisting = mergeExisting; + } + + public AddItemTransform WithMetadata(string metadataName, string metadataValue) + { + _metadata.Add(new ItemMetadataValue(metadataName, metadataValue)); + return this; + } + + public AddItemTransform WithMetadata(string metadataName, Func metadataValueFunc) + { + _metadata.Add(new ItemMetadataValue(metadataName, metadataValueFunc)); + return this; + } + + public AddItemTransform WithMetadata(ItemMetadataValue metadata) + { + _metadata.Add(metadata); + return this; + } + + public override ProjectItemElement ConditionallyTransform(T source) + { + string includeValue = _includeValue ?? _includeValueFunc(source); + string excludeValue = _excludeValue ?? _excludeValueFunc(source); + + var item = _itemObjectGenerator.AddItem(_itemName, includeValue); + item.Exclude = excludeValue; + + foreach (var metadata in _metadata) + { + item.AddMetadata(metadata.MetadataName, metadata.GetMetadataValue(source)); + } + + return item; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddPropertyTransform.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddPropertyTransform.cs new file mode 100644 index 000000000..48231ee40 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddPropertyTransform.cs @@ -0,0 +1,54 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class AddPropertyTransform : ConditionalTransform + { + public string PropertyName { get; } + + private readonly ProjectRootElement _propertyObjectGenerator = ProjectRootElement.Create(); + private readonly string _propertyValue; + private readonly Func _propertyValueFunc; + + public AddPropertyTransform(string propertyName, string propertyValue, Func condition) + : base(condition) + { + PropertyName = propertyName; + _propertyValue = propertyValue; + } + + public AddPropertyTransform(string propertyName, Func propertyValueFunc, Func condition) + : base(condition) + { + PropertyName = propertyName; + _propertyValueFunc = propertyValueFunc; + } + + public override ProjectPropertyElement ConditionallyTransform(T source) + { + string propertyValue = GetPropertyValue(source); + + var property = _propertyObjectGenerator.CreatePropertyElement(PropertyName); + property.Value = propertyValue; + + return property; + } + + public string GetPropertyValue(T source) + { + return _propertyValue ?? _propertyValueFunc(source); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddStringPropertyTransform.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddStringPropertyTransform.cs new file mode 100644 index 000000000..facca2e4d --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/AddStringPropertyTransform.cs @@ -0,0 +1,25 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class AddStringPropertyTransform : AddPropertyTransform + { + public AddStringPropertyTransform(string propertyName) + : base(propertyName, s => s, s => !string.IsNullOrEmpty(s)) { } + + public AddStringPropertyTransform(string propertyName, Func condition) + : base(propertyName, s => s, s => !string.IsNullOrEmpty(s) && condition(s)) { } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/ConditionalTransform.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/ConditionalTransform.cs new file mode 100644 index 000000000..0ccd10225 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/ConditionalTransform.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public abstract class ConditionalTransform : ITransform + { + private Func _condition; + + public ConditionalTransform(Func condition) + { + _condition = condition; + } + + public U Transform(T source) + { + if (_condition == null || _condition(source)) + { + return ConditionallyTransform(source); + } + + return default(U); + } + + public abstract U ConditionallyTransform(T source); + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/ITransform.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/ITransform.cs new file mode 100644 index 000000000..ab9b37804 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/ITransform.cs @@ -0,0 +1,9 @@ +using Microsoft.Build.Construction; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public interface ITransform + { + U Transform(T source); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/ITransformApplicator.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/ITransformApplicator.cs new file mode 100644 index 000000000..b2c7bf307 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/ITransformApplicator.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Microsoft.Build.Construction; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public interface ITransformApplicator + { + void Execute( + T element, + U destinationElement) where T : ProjectElement where U : ProjectElementContainer; + + void Execute( + IEnumerable elements, + U destinationElement) where T : ProjectElement where U : ProjectElementContainer; + + void Execute( + ProjectItemElement item, + ProjectItemGroupElement destinationItemGroup, + bool mergeExisting); + + void Execute( + IEnumerable items, + ProjectItemGroupElement destinationItemGroup, + bool mergeExisting); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/IncludeContextTransform.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/IncludeContextTransform.cs new file mode 100644 index 000000000..c935d34e5 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/IncludeContextTransform.cs @@ -0,0 +1,144 @@ +using Microsoft.DotNet.ProjectModel.Files; +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Build.Construction; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class IncludeContextTransform : ConditionalTransform> + { + // TODO: If a directory is specified in project.json does this need to be replaced with a glob in msbuild? + // - Partially solved, what if the resolved glob is a directory? + // TODO: Support mappings + + private string _itemName; + private bool _transformMappings; + private List> _metadata = new List>(); + private AddItemTransform[] _transformSet; + + public IncludeContextTransform( + string itemName, + bool transformMappings = true, + Func condition = null) : base(condition) + { + _itemName = itemName; + _transformMappings = transformMappings; + + CreateTransformSet(); + } + + public IncludeContextTransform WithMetadata(string metadataName, string metadataValue) + { + _metadata.Add(new ItemMetadataValue(metadataName, metadataValue)); + return this; + } + + public IncludeContextTransform WithMetadata(string metadataName, Func metadataValueFunc) + { + _metadata.Add(new ItemMetadataValue(metadataName, metadataValueFunc)); + return this; + } + + private void CreateTransformSet() + { + var includeFilesExcludeFilesTransformation = new AddItemTransform( + _itemName, + includeContext => FormatPatterns(includeContext.IncludeFiles, includeContext.SourceBasePath), + includeContext => FormatPatterns(includeContext.ExcludeFiles, includeContext.SourceBasePath), + includeContext => includeContext != null && includeContext.IncludeFiles.Count > 0); + + var includeExcludeTransformation = new AddItemTransform( + _itemName, + includeContext => + { + var fullIncludeSet = includeContext.IncludePatterns.OrEmptyIfNull() + .Union(includeContext.BuiltInsInclude.OrEmptyIfNull()); + + return FormatPatterns(fullIncludeSet, includeContext.SourceBasePath); + }, + includeContext => + { + var fullExcludeSet = includeContext.ExcludePatterns.OrEmptyIfNull() + .Union(includeContext.BuiltInsExclude.OrEmptyIfNull()) + .Union(includeContext.ExcludeFiles.OrEmptyIfNull()); + + return FormatPatterns(fullExcludeSet, includeContext.SourceBasePath); + }, + includeContext => + { + return includeContext != null && + ( + (includeContext.IncludePatterns != null && includeContext.IncludePatterns.Count > 0) + || + (includeContext.BuiltInsInclude != null && includeContext.BuiltInsInclude.Count > 0) + ); + }); + + foreach (var metadata in _metadata) + { + includeFilesExcludeFilesTransformation.WithMetadata(metadata); + includeExcludeTransformation.WithMetadata(metadata); + } + + _transformSet = new AddItemTransform[] + { + includeFilesExcludeFilesTransformation, + includeExcludeTransformation + }; + } + + private string FormatPatterns(IEnumerable patterns, string projectDirectory) + { + List mutatedPatterns = new List(patterns.Count()); + + foreach (var pattern in patterns) + { + // Do not use forward slashes + // https://github.com/Microsoft/msbuild/issues/724 + var mutatedPattern = pattern.Replace('/', '\\'); + + // MSBuild cannot copy directories + mutatedPattern = ReplaceDirectoriesWithGlobs(mutatedPattern, projectDirectory); + + mutatedPatterns.Add(mutatedPattern); + } + + return string.Join(";", mutatedPatterns); + } + + private string ReplaceDirectoriesWithGlobs(string pattern, string projectDirectory) + { + if (PatternIsDirectory(pattern, projectDirectory)) + { + return $"{pattern.TrimEnd(new char[] { '\\' })}\\**\\*"; + } + else + { + return pattern; + } + } + + private bool PatternIsDirectory(string pattern, string projectDirectory) + { + // TODO: what about /some/path/**/somedir? + // Should this even be migrated? + var path = pattern; + + if (!Path.IsPathRooted(path)) + { + path = Path.Combine(projectDirectory, path); + } + + return Directory.Exists(path); + } + + public override IEnumerable ConditionallyTransform(IncludeContext source) + { + return _transformSet.Select(t => t.Transform(source)); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/TransformApplicator.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/TransformApplicator.cs new file mode 100644 index 000000000..6a13154a3 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/TransformApplicator.cs @@ -0,0 +1,170 @@ +// 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 Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using System.Linq; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectJsonMigration +{ + public class TransformApplicator : ITransformApplicator + { + private readonly ProjectRootElement _projectElementGenerator = ProjectRootElement.Create(); + + public void Execute( + T element, + U destinationElement) where T : ProjectElement where U : ProjectElementContainer + { + if (element != null) + { + if (typeof(T) == typeof(ProjectItemElement)) + { + var item = destinationElement.ContainingProject.CreateItemElement("___TEMP___"); + item.CopyFrom(element); + + destinationElement.AppendChild(item); + item.AddMetadata((element as ProjectItemElement).Metadata); + } + else if (typeof(T) == typeof(ProjectPropertyElement)) + { + var property = destinationElement.ContainingProject.CreatePropertyElement("___TEMP___"); + property.CopyFrom(element); + + destinationElement.AppendChild(property); + } + else + { + throw new Exception("Unable to add unknown project element to project"); + } + } + } + + public void Execute( + IEnumerable elements, + U destinationElement) where T : ProjectElement where U : ProjectElementContainer + { + foreach (var element in elements) + { + Execute(element, destinationElement); + } + } + + public void Execute( + ProjectItemElement item, + ProjectItemGroupElement destinationItemGroup, + bool mergeExisting) + { + if (item == null) + { + return; + } + + if (mergeExisting) + { + var existingItems = FindExistingItems(item, destinationItemGroup.ContainingProject); + + foreach (var existingItem in existingItems) + { + var mergeResult = MergeItems(item, existingItem); + item = mergeResult.InputItem; + + // Existing Item is null when it's entire set of includes has been merged with the MergeItem + if (mergeResult.ExistingItem == null) + { + existingItem.Parent.RemoveChild(existingItem); + } + + Execute(mergeResult.MergedItem, destinationItemGroup); + } + + // Item will be null only when it's entire set of includes is merged with existing items + if (item != null) + { + Execute(item, destinationItemGroup); + } + } + else + { + Execute(item, destinationItemGroup); + } + } + + public void Execute( + IEnumerable items, + ProjectItemGroupElement destinationItemGroup, + bool mergeExisting) + { + foreach (var item in items) + { + Execute(item, destinationItemGroup, mergeExisting); + } + } + + /// + /// Merges two items on their common sets of includes. + /// The output is 3 items, the 2 input items and the merged items. If the common + /// set of includes spans the entirety of the includes of either of the 2 input + /// items, that item will be returned as null. + /// + /// The 3rd output item, the merged item, will have the Union of the excludes and + /// metadata from the 2 input items. If any metadata between the 2 input items is different, + /// this will throw. + /// + /// This function will mutate the Include property of the 2 input items, removing the common subset. + /// + private MergeResult MergeItems(ProjectItemElement item, ProjectItemElement existingItem) + { + if (!string.Equals(item.ItemType, existingItem.ItemType, StringComparison.Ordinal)) + { + throw new InvalidOperationException("Cannot merge items of different types."); + } + + if (!item.CommonIncludes(existingItem).Any()) + { + throw new InvalidOperationException("Cannot merge items without a common include."); + } + + var commonIncludes = item.CommonIncludes(existingItem).ToList(); + item.RemoveIncludes(commonIncludes); + existingItem.RemoveIncludes(commonIncludes); + + var mergedItem = _projectElementGenerator.AddItem(item.ItemType, string.Join(";", commonIncludes)); + + mergedItem.AddExcludes(existingItem.Excludes()); + mergedItem.AddExcludes(item.Excludes()); + + mergedItem.AddMetadata(existingItem.Metadata); + mergedItem.AddMetadata(item.Metadata); + + var mergeResult = new MergeResult + { + InputItem = string.IsNullOrEmpty(item.Include) ? null : item, + ExistingItem = string.IsNullOrEmpty(existingItem.Include) ? null : existingItem, + MergedItem = mergedItem + }; + + return mergeResult; + } + + private IEnumerable FindExistingItems(ProjectItemElement item, ProjectRootElement project) + { + return project.ItemsWithoutConditions() + .Where(i => string.Equals(i.ItemType, item.ItemType, StringComparison.Ordinal)) + .Where(i => i.CommonIncludes(item).Any()); + } + + private class MergeResult + { + public ProjectItemElement InputItem { get; set; } + public ProjectItemElement ExistingItem { get; set; } + public ProjectItemElement MergedItem { get; set; } + } + } +}