Microsoft.DotNet.ProjectJsonMigration core library
This commit is contained in:
parent
be8428cb6c
commit
46818ff3fa
32 changed files with 2320 additions and 0 deletions
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs
Normal file
114
src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs
Normal file
|
@ -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<ProjectPropertyElement> 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<ProjectItemElement> ItemsWithoutConditions(
|
||||||
|
this ProjectRootElement projectRoot)
|
||||||
|
{
|
||||||
|
return projectRoot.Items
|
||||||
|
.Where(p => string.IsNullOrEmpty(p.Condition)
|
||||||
|
&& p.AllParents.All(parent => string.IsNullOrEmpty(parent.Condition)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<string> Includes(
|
||||||
|
this ProjectItemElement item)
|
||||||
|
{
|
||||||
|
return item.Include.Equals(string.Empty) ? Enumerable.Empty<string>() : item.Include.Split(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<string> Excludes(
|
||||||
|
this ProjectItemElement item)
|
||||||
|
{
|
||||||
|
return item.Exclude.Equals(string.Empty) ? Enumerable.Empty<string>() : item.Exclude.Split(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<string> AllConditions(this ProjectElement projectElement)
|
||||||
|
{
|
||||||
|
return new string[] { projectElement.Condition }.Concat(projectElement.AllParents.Select(p=> p.Condition));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<string> CommonIncludes(this ProjectItemElement item, ProjectItemElement otherItem)
|
||||||
|
{
|
||||||
|
return item.Includes().Intersect(otherItem.Includes());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RemoveIncludes(this ProjectItemElement item, IEnumerable<string> includesToRemove)
|
||||||
|
{
|
||||||
|
item.Include = string.Join(";", item.Includes().Except(includesToRemove));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddIncludes(this ProjectItemElement item, IEnumerable<string> includes)
|
||||||
|
{
|
||||||
|
item.Include = string.Join(";", item.Includes().Union(includes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddExcludes(this ProjectItemElement item, IEnumerable<string> 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<ProjectMetadataElement> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||||
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectGuid>0E083818-2320-4388-8007-4F720FD5C634</ProjectGuid>
|
||||||
|
<RootNamespace>Microsoft.DotNet.ProjectJsonMigration</RootNamespace>
|
||||||
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||||
|
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin</OutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
</Project>
|
|
@ -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<ProjectContext> ProjectContexts { get; }
|
||||||
|
|
||||||
|
public ProjectContext DefaultProjectContext
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ProjectContexts.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MigrationRuleInputs(
|
||||||
|
IEnumerable<ProjectContext> projectContexts,
|
||||||
|
ProjectRootElement outputProject,
|
||||||
|
ProjectItemGroupElement commonItemGroup,
|
||||||
|
ProjectPropertyGroupElement commonPropertyGroup)
|
||||||
|
{
|
||||||
|
ProjectContexts = projectContexts;
|
||||||
|
OutputMSBuildProject = outputProject;
|
||||||
|
CommonItemGroup = commonItemGroup;
|
||||||
|
CommonPropertyGroup = commonPropertyGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
src/Microsoft.DotNet.ProjectJsonMigration/MigrationTrace.cs
Normal file
45
src/Microsoft.DotNet.ProjectJsonMigration/MigrationTrace.cs
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.ProjectJsonMigration
|
||||||
|
{
|
||||||
|
public class ItemMetadataValue<T>
|
||||||
|
{
|
||||||
|
public string MetadataName { get; }
|
||||||
|
|
||||||
|
private string _metadataValue;
|
||||||
|
private Func<T, string> _metadataValueFunc;
|
||||||
|
|
||||||
|
public ItemMetadataValue(string metadataName, string metadataValue)
|
||||||
|
{
|
||||||
|
MetadataName = metadataName;
|
||||||
|
_metadataValue = metadataValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemMetadataValue(string metadataName, Func<T, string> metadataValueFunc)
|
||||||
|
{
|
||||||
|
MetadataName = metadataName;
|
||||||
|
_metadataValueFunc = metadataValueFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetMetadataValue(T source)
|
||||||
|
{
|
||||||
|
return _metadataValue ?? _metadataValueFunc(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs
Normal file
83
src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs
Normal file
|
@ -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<ProjectContext> 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Microsoft.DotNet.ProjectJsonMigration.Tests")]
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<CommonCompilerOptions>[] EmitEntryPointTransforms
|
||||||
|
=> new AddPropertyTransform<CommonCompilerOptions>[]
|
||||||
|
{
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("OutputType", "Exe",
|
||||||
|
compilerOptions => compilerOptions.EmitEntryPoint != null && compilerOptions.EmitEntryPoint.Value),
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("TargetExt", ".dll",
|
||||||
|
compilerOptions => compilerOptions.EmitEntryPoint != null && compilerOptions.EmitEntryPoint.Value),
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("OutputType", "Library",
|
||||||
|
compilerOptions => compilerOptions.EmitEntryPoint == null || !compilerOptions.EmitEntryPoint.Value)
|
||||||
|
};
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions>[] KeyFileTransforms
|
||||||
|
=> new AddPropertyTransform<CommonCompilerOptions>[]
|
||||||
|
{
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("AssemblyOriginatorKeyFile",
|
||||||
|
compilerOptions => compilerOptions.KeyFile,
|
||||||
|
compilerOptions => !string.IsNullOrEmpty(compilerOptions.KeyFile)),
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("SignAssembly",
|
||||||
|
"true",
|
||||||
|
compilerOptions => !string.IsNullOrEmpty(compilerOptions.KeyFile))
|
||||||
|
};
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> DefineTransform => new AddPropertyTransform<CommonCompilerOptions>(
|
||||||
|
"DefineConstants",
|
||||||
|
compilerOptions => string.Join(";", compilerOptions.Defines),
|
||||||
|
compilerOptions => compilerOptions.Defines != null && compilerOptions.Defines.Any());
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> NoWarnTransform => new AddPropertyTransform<CommonCompilerOptions>(
|
||||||
|
"NoWarn",
|
||||||
|
compilerOptions => string.Join(";", compilerOptions.SuppressWarnings),
|
||||||
|
compilerOptions => compilerOptions.SuppressWarnings != null && compilerOptions.SuppressWarnings.Any());
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> PreserveCompilationContextTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("PreserveCompilationContext",
|
||||||
|
compilerOptions => compilerOptions.PreserveCompilationContext.ToString().ToLower(),
|
||||||
|
compilerOptions => compilerOptions.PreserveCompilationContext != null && compilerOptions.PreserveCompilationContext.Value);
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> WarningsAsErrorsTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("WarningsAsErrors",
|
||||||
|
compilerOptions => compilerOptions.WarningsAsErrors.ToString().ToLower(),
|
||||||
|
compilerOptions => compilerOptions.WarningsAsErrors != null && compilerOptions.WarningsAsErrors.Value);
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> AllowUnsafeTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("AllowUnsafeBlocks",
|
||||||
|
compilerOptions => compilerOptions.AllowUnsafe.ToString().ToLower(),
|
||||||
|
compilerOptions => compilerOptions.AllowUnsafe != null && compilerOptions.AllowUnsafe.Value);
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> OptimizeTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("Optimize",
|
||||||
|
compilerOptions => compilerOptions.Optimize.ToString().ToLower(),
|
||||||
|
compilerOptions => compilerOptions.Optimize != null && compilerOptions.Optimize.Value);
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> PlatformTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("PlatformTarget",
|
||||||
|
compilerOptions => compilerOptions.Platform,
|
||||||
|
compilerOptions => !string.IsNullOrEmpty(compilerOptions.Platform));
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> LanguageVersionTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("LangVersion",
|
||||||
|
compilerOptions => compilerOptions.LanguageVersion,
|
||||||
|
compilerOptions => !string.IsNullOrEmpty(compilerOptions.LanguageVersion));
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> DelaySignTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("DelaySign",
|
||||||
|
compilerOptions => compilerOptions.DelaySign.ToString().ToLower(),
|
||||||
|
compilerOptions => compilerOptions.DelaySign != null && compilerOptions.DelaySign.Value);
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> PublicSignTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("PublicSign",
|
||||||
|
compilerOptions => compilerOptions.PublicSign.ToString().ToLower(),
|
||||||
|
compilerOptions => compilerOptions.PublicSign != null && compilerOptions.PublicSign.Value);
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> DebugTypeTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("DebugType",
|
||||||
|
compilerOptions => compilerOptions.DebugType,
|
||||||
|
compilerOptions => !string.IsNullOrEmpty(compilerOptions.DebugType));
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> XmlDocTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("GenerateDocumentationFile",
|
||||||
|
compilerOptions => compilerOptions.GenerateXmlDocumentation.ToString().ToLower(),
|
||||||
|
compilerOptions => compilerOptions.GenerateXmlDocumentation != null && compilerOptions.GenerateXmlDocumentation.Value);
|
||||||
|
|
||||||
|
// TODO: https://github.com/dotnet/sdk/issues/67
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> XmlDocTransformFilePath =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("DocumentationFile",
|
||||||
|
@"$(OutputPath)\$(AssemblyName).xml",
|
||||||
|
compilerOptions => compilerOptions.GenerateXmlDocumentation != null && compilerOptions.GenerateXmlDocumentation.Value);
|
||||||
|
|
||||||
|
private AddPropertyTransform<CommonCompilerOptions> OutputNameTransform =>
|
||||||
|
new AddPropertyTransform<CommonCompilerOptions>("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<CommonCompilerOptions, string, IEnumerable<ProjectItemElement>> CompileFilesTransformExecute =>
|
||||||
|
(compilerOptions, projectDirectory) =>
|
||||||
|
CompileFilesTransform.Transform(GetCompileIncludeContext(compilerOptions, projectDirectory));
|
||||||
|
|
||||||
|
private Func<CommonCompilerOptions, string, IEnumerable<ProjectItemElement>> EmbedFilesTransformExecute =>
|
||||||
|
(compilerOptions, projectDirectory) =>
|
||||||
|
EmbedFilesTransform.Transform(GetEmbedIncludeContext(compilerOptions, projectDirectory));
|
||||||
|
|
||||||
|
private Func<CommonCompilerOptions, string, IEnumerable<ProjectItemElement>> CopyToOutputFilesTransformExecute =>
|
||||||
|
(compilerOptions, projectDirectory) =>
|
||||||
|
CopyToOutputFilesTransform.Transform(GetCopyToOutputIncludeContext(compilerOptions, projectDirectory));
|
||||||
|
|
||||||
|
private string _configuration;
|
||||||
|
private ProjectPropertyGroupElement _configurationPropertyGroup;
|
||||||
|
private ProjectItemGroupElement _configurationItemGroup;
|
||||||
|
|
||||||
|
private List<AddPropertyTransform<CommonCompilerOptions>> _propertyTransforms;
|
||||||
|
private List<Func<CommonCompilerOptions, string, IEnumerable<ProjectItemElement>>> _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<AddPropertyTransform<CommonCompilerOptions>>()
|
||||||
|
{
|
||||||
|
DefineTransform,
|
||||||
|
NoWarnTransform,
|
||||||
|
WarningsAsErrorsTransform,
|
||||||
|
AllowUnsafeTransform,
|
||||||
|
OptimizeTransform,
|
||||||
|
PlatformTransform,
|
||||||
|
LanguageVersionTransform,
|
||||||
|
DelaySignTransform,
|
||||||
|
PublicSignTransform,
|
||||||
|
DebugTypeTransform,
|
||||||
|
OutputNameTransform,
|
||||||
|
XmlDocTransform,
|
||||||
|
XmlDocTransformFilePath,
|
||||||
|
PreserveCompilationContextTransform
|
||||||
|
};
|
||||||
|
|
||||||
|
_propertyTransforms.AddRange(EmitEntryPointTransforms);
|
||||||
|
_propertyTransforms.AddRange(KeyFileTransforms);
|
||||||
|
|
||||||
|
_includeContextTransformExecutes = new List<Func<CommonCompilerOptions, string, IEnumerable<ProjectItemElement>>>()
|
||||||
|
{
|
||||||
|
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<ProjectItemElement> configurationOutput, IEnumerable<ProjectItemElement> nonConfigurationOutput)
|
||||||
|
{
|
||||||
|
foreach (var item1 in configurationOutput)
|
||||||
|
{
|
||||||
|
if (item1 == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item2Excludes = new HashSet<string>();
|
||||||
|
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<ProjectItemElement> itemsToRemoveFrom,
|
||||||
|
IEnumerable<ProjectItemElement> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<bool> AutoUnifyTransform => new AddPropertyTransform<bool>(
|
||||||
|
"AutoUnify",
|
||||||
|
"true",
|
||||||
|
b => true);
|
||||||
|
|
||||||
|
private AddPropertyTransform<bool> DesignTimeAutoUnifyTransform => new AddPropertyTransform<bool>(
|
||||||
|
"DesignTimeAutoUnify",
|
||||||
|
"true",
|
||||||
|
b => true);
|
||||||
|
|
||||||
|
private AddItemTransform<LibraryExport> ProjectDependencyTransform => new AddItemTransform<LibraryExport>(
|
||||||
|
"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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Project>[] _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<Project> DescriptionTransform => new AddPropertyTransform<Project>("Description",
|
||||||
|
project => project.Description,
|
||||||
|
project => !string.IsNullOrEmpty(project.Description));
|
||||||
|
|
||||||
|
private AddPropertyTransform<Project> CopyrightTransform => new AddPropertyTransform<Project>("Copyright",
|
||||||
|
project => project.Copyright,
|
||||||
|
project => !string.IsNullOrEmpty(project.Copyright));
|
||||||
|
|
||||||
|
private AddPropertyTransform<Project> TitleTransform => new AddPropertyTransform<Project>("AssemblyTitle",
|
||||||
|
project => project.Title,
|
||||||
|
project => !string.IsNullOrEmpty(project.Title));
|
||||||
|
|
||||||
|
private AddPropertyTransform<Project> LanguageTransform => new AddPropertyTransform<Project>("NeutralLanguage",
|
||||||
|
project => project.Language,
|
||||||
|
project => !string.IsNullOrEmpty(project.Language));
|
||||||
|
|
||||||
|
private AddPropertyTransform<Project> VersionTransform => new AddPropertyTransform<Project>("VersionPrefix",
|
||||||
|
project => project.Version.ToString(), p => true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<string> 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<string, TargetHookInfo> ScriptSetToMSBuildHookTargetMap => new Dictionary<string, TargetHookInfo>()
|
||||||
|
{
|
||||||
|
{ "precompile", new TargetHookInfo(true, "Build") },
|
||||||
|
{ "postcompile", new TargetHookInfo(false, "Build") },
|
||||||
|
{ "prepublish", new TargetHookInfo(true, "Publish") },
|
||||||
|
{ "postpublish", new TargetHookInfo(false, "Publish") }
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Dictionary<string, string> ScriptVariableToMSBuildMap => new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<NuGetFramework>[] _transforms;
|
||||||
|
|
||||||
|
public MigrateTFMRule(TransformApplicator transformApplicator = null)
|
||||||
|
{
|
||||||
|
_transformApplicator = transformApplicator ?? new TransformApplicator();
|
||||||
|
|
||||||
|
_transforms = new AddPropertyTransform<NuGetFramework>[]
|
||||||
|
{
|
||||||
|
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<NuGetFramework> OutputPathTransform =>
|
||||||
|
new AddPropertyTransform<NuGetFramework>("OutputPath",
|
||||||
|
f => $"bin/$(Configuration)/{f.GetShortFolderName()}",
|
||||||
|
f => true);
|
||||||
|
|
||||||
|
private AddPropertyTransform<NuGetFramework> FrameworkIdentifierTransform =>
|
||||||
|
new AddPropertyTransform<NuGetFramework>("TargetFrameworkIdentifier",
|
||||||
|
f => f.Framework,
|
||||||
|
f => true);
|
||||||
|
|
||||||
|
private AddPropertyTransform<NuGetFramework> FrameworkVersionTransform =>
|
||||||
|
new AddPropertyTransform<NuGetFramework>("TargetFrameworkVersion",
|
||||||
|
f => "v" + GetDisplayVersion(f.Version),
|
||||||
|
f => true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<JObject>();
|
||||||
|
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<normalizedPaths.Count(); ++i)
|
||||||
|
{
|
||||||
|
var path1 = normalizedPaths[i - 1];
|
||||||
|
var path2 = normalizedPaths[i];
|
||||||
|
if (!string.Equals(path1, path2, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
src/Microsoft.DotNet.ProjectJsonMigration/project.json
Normal file
32
src/Microsoft.DotNet.ProjectJsonMigration/project.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"version": "1.0.0-featmsbuild-*",
|
||||||
|
"buildOptions": {
|
||||||
|
"warningsAsErrors": true
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.DotNet.Compiler.Common": {
|
||||||
|
"target": "project"
|
||||||
|
},
|
||||||
|
"Microsoft.DotNet.Cli.Utils": {
|
||||||
|
"target": "project"
|
||||||
|
},
|
||||||
|
"Microsoft.DotNet.ProjectModel": {
|
||||||
|
"target": "project"
|
||||||
|
},
|
||||||
|
"Microsoft.Win32.Registry": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"exclude": "compile"
|
||||||
|
},
|
||||||
|
"MSBuild": "0.1.0-preview-00029-160805",
|
||||||
|
"Microsoft.Build.Framework": "0.1.0-preview-00029-160805",
|
||||||
|
"Microsoft.Build.Tasks.Core": "0.1.0-preview-00029-160805",
|
||||||
|
"Microsoft.Build.Utilities.Core": "0.1.0-preview-00029-160805",
|
||||||
|
"Microsoft.Build.Targets": "0.1.0-preview-00029-160805",
|
||||||
|
"Microsoft.Build": "0.1.0-preview-00029-160805",
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"netcoreapp1.0": {
|
||||||
|
"imports": ["dnxcore50","netstandardapp1.5","portable-net45+win8", "portable-net45+wp80+win8+wpa81+dnxcore50"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 AddBoolPropertyTransform : AddPropertyTransform<bool>
|
||||||
|
{
|
||||||
|
public AddBoolPropertyTransform(string propertyName)
|
||||||
|
: base(propertyName, b => b.ToString(), b => b) { }
|
||||||
|
|
||||||
|
public AddBoolPropertyTransform(string propertyName, Func<bool, bool> condition)
|
||||||
|
: base(propertyName, b => b.ToString(), condition) { }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T> : ConditionalTransform<T, ProjectItemElement>
|
||||||
|
{
|
||||||
|
private ProjectRootElement _itemObjectGenerator = ProjectRootElement.Create();
|
||||||
|
|
||||||
|
private string _itemName;
|
||||||
|
private string _includeValue;
|
||||||
|
private string _excludeValue;
|
||||||
|
|
||||||
|
private Func<T, string> _includeValueFunc;
|
||||||
|
private Func<T, string> _excludeValueFunc;
|
||||||
|
|
||||||
|
private bool _mergeExisting;
|
||||||
|
|
||||||
|
private List<ItemMetadataValue<T>> _metadata = new List<ItemMetadataValue<T>>();
|
||||||
|
|
||||||
|
public AddItemTransform(
|
||||||
|
string itemName,
|
||||||
|
IEnumerable<string> includeValues,
|
||||||
|
IEnumerable<string> excludeValues,
|
||||||
|
Func<T, bool> condition,
|
||||||
|
bool mergeExisting = false)
|
||||||
|
: this(itemName, string.Join(";", includeValues), string.Join(";", excludeValues), condition, mergeExisting) { }
|
||||||
|
|
||||||
|
public AddItemTransform(
|
||||||
|
string itemName,
|
||||||
|
Func<T, string> includeValueFunc,
|
||||||
|
Func<T, string> excludeValueFunc,
|
||||||
|
Func<T, bool> condition)
|
||||||
|
: base(condition)
|
||||||
|
{
|
||||||
|
_itemName = itemName;
|
||||||
|
_includeValueFunc = includeValueFunc;
|
||||||
|
_excludeValueFunc = excludeValueFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddItemTransform(
|
||||||
|
string itemName,
|
||||||
|
string includeValue,
|
||||||
|
Func<T, string> excludeValueFunc,
|
||||||
|
Func<T, bool> condition)
|
||||||
|
: base(condition)
|
||||||
|
{
|
||||||
|
_itemName = itemName;
|
||||||
|
_includeValue = includeValue;
|
||||||
|
_excludeValueFunc = excludeValueFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddItemTransform(
|
||||||
|
string itemName,
|
||||||
|
Func<T, string> includeValueFunc,
|
||||||
|
string excludeValue,
|
||||||
|
Func<T, bool> condition)
|
||||||
|
: base(condition)
|
||||||
|
{
|
||||||
|
_itemName = itemName;
|
||||||
|
_includeValueFunc = includeValueFunc;
|
||||||
|
_excludeValue = excludeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddItemTransform(
|
||||||
|
string itemName,
|
||||||
|
string includeValue,
|
||||||
|
string excludeValue,
|
||||||
|
Func<T, bool> condition,
|
||||||
|
bool mergeExisting=false)
|
||||||
|
: base(condition)
|
||||||
|
{
|
||||||
|
_itemName = itemName;
|
||||||
|
_includeValue = includeValue;
|
||||||
|
_excludeValue = excludeValue;
|
||||||
|
_mergeExisting = mergeExisting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddItemTransform<T> WithMetadata(string metadataName, string metadataValue)
|
||||||
|
{
|
||||||
|
_metadata.Add(new ItemMetadataValue<T>(metadataName, metadataValue));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddItemTransform<T> WithMetadata(string metadataName, Func<T, string> metadataValueFunc)
|
||||||
|
{
|
||||||
|
_metadata.Add(new ItemMetadataValue<T>(metadataName, metadataValueFunc));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddItemTransform<T> WithMetadata(ItemMetadataValue<T> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T> : ConditionalTransform<T, ProjectPropertyElement>
|
||||||
|
{
|
||||||
|
public string PropertyName { get; }
|
||||||
|
|
||||||
|
private readonly ProjectRootElement _propertyObjectGenerator = ProjectRootElement.Create();
|
||||||
|
private readonly string _propertyValue;
|
||||||
|
private readonly Func<T,string> _propertyValueFunc;
|
||||||
|
|
||||||
|
public AddPropertyTransform(string propertyName, string propertyValue, Func<T,bool> condition)
|
||||||
|
: base(condition)
|
||||||
|
{
|
||||||
|
PropertyName = propertyName;
|
||||||
|
_propertyValue = propertyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddPropertyTransform(string propertyName, Func<T, string> propertyValueFunc, Func<T,bool> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<string>
|
||||||
|
{
|
||||||
|
public AddStringPropertyTransform(string propertyName)
|
||||||
|
: base(propertyName, s => s, s => !string.IsNullOrEmpty(s)) { }
|
||||||
|
|
||||||
|
public AddStringPropertyTransform(string propertyName, Func<string, bool> condition)
|
||||||
|
: base(propertyName, s => s, s => !string.IsNullOrEmpty(s) && condition(s)) { }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T, U> : ITransform<T, U>
|
||||||
|
{
|
||||||
|
private Func<T, bool> _condition;
|
||||||
|
|
||||||
|
public ConditionalTransform(Func<T,bool> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using Microsoft.Build.Construction;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.ProjectJsonMigration
|
||||||
|
{
|
||||||
|
public interface ITransform<T, U>
|
||||||
|
{
|
||||||
|
U Transform(T source);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Build.Construction;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.ProjectJsonMigration
|
||||||
|
{
|
||||||
|
public interface ITransformApplicator
|
||||||
|
{
|
||||||
|
void Execute<T, U>(
|
||||||
|
T element,
|
||||||
|
U destinationElement) where T : ProjectElement where U : ProjectElementContainer;
|
||||||
|
|
||||||
|
void Execute<T, U>(
|
||||||
|
IEnumerable<T> elements,
|
||||||
|
U destinationElement) where T : ProjectElement where U : ProjectElementContainer;
|
||||||
|
|
||||||
|
void Execute(
|
||||||
|
ProjectItemElement item,
|
||||||
|
ProjectItemGroupElement destinationItemGroup,
|
||||||
|
bool mergeExisting);
|
||||||
|
|
||||||
|
void Execute(
|
||||||
|
IEnumerable<ProjectItemElement> items,
|
||||||
|
ProjectItemGroupElement destinationItemGroup,
|
||||||
|
bool mergeExisting);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<IncludeContext, IEnumerable<ProjectItemElement>>
|
||||||
|
{
|
||||||
|
// 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<ItemMetadataValue<IncludeContext>> _metadata = new List<ItemMetadataValue<IncludeContext>>();
|
||||||
|
private AddItemTransform<IncludeContext>[] _transformSet;
|
||||||
|
|
||||||
|
public IncludeContextTransform(
|
||||||
|
string itemName,
|
||||||
|
bool transformMappings = true,
|
||||||
|
Func<IncludeContext, bool> condition = null) : base(condition)
|
||||||
|
{
|
||||||
|
_itemName = itemName;
|
||||||
|
_transformMappings = transformMappings;
|
||||||
|
|
||||||
|
CreateTransformSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IncludeContextTransform WithMetadata(string metadataName, string metadataValue)
|
||||||
|
{
|
||||||
|
_metadata.Add(new ItemMetadataValue<IncludeContext>(metadataName, metadataValue));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IncludeContextTransform WithMetadata(string metadataName, Func<IncludeContext, string> metadataValueFunc)
|
||||||
|
{
|
||||||
|
_metadata.Add(new ItemMetadataValue<IncludeContext>(metadataName, metadataValueFunc));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateTransformSet()
|
||||||
|
{
|
||||||
|
var includeFilesExcludeFilesTransformation = new AddItemTransform<IncludeContext>(
|
||||||
|
_itemName,
|
||||||
|
includeContext => FormatPatterns(includeContext.IncludeFiles, includeContext.SourceBasePath),
|
||||||
|
includeContext => FormatPatterns(includeContext.ExcludeFiles, includeContext.SourceBasePath),
|
||||||
|
includeContext => includeContext != null && includeContext.IncludeFiles.Count > 0);
|
||||||
|
|
||||||
|
var includeExcludeTransformation = new AddItemTransform<IncludeContext>(
|
||||||
|
_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<IncludeContext>[]
|
||||||
|
{
|
||||||
|
includeFilesExcludeFilesTransformation,
|
||||||
|
includeExcludeTransformation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatPatterns(IEnumerable<string> patterns, string projectDirectory)
|
||||||
|
{
|
||||||
|
List<string> mutatedPatterns = new List<string>(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<ProjectItemElement> ConditionallyTransform(IncludeContext source)
|
||||||
|
{
|
||||||
|
return _transformSet.Select(t => t.Transform(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, U>(
|
||||||
|
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<T, U>(
|
||||||
|
IEnumerable<T> 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<ProjectItemElement> items,
|
||||||
|
ProjectItemGroupElement destinationItemGroup,
|
||||||
|
bool mergeExisting)
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
Execute(item, destinationItemGroup, mergeExisting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<ProjectItemElement> 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue