diff --git a/src/dotnet/Program.cs b/src/dotnet/Program.cs index 19eb6eb81..60ee18d42 100644 --- a/src/dotnet/Program.cs +++ b/src/dotnet/Program.cs @@ -21,6 +21,7 @@ using Microsoft.DotNet.Tools.Restore; using Microsoft.DotNet.Tools.Restore3; using Microsoft.DotNet.Tools.Run; using Microsoft.DotNet.Tools.Test; +using Microsoft.DotNet.Tools.Migrate; using NuGet.Frameworks; namespace Microsoft.DotNet.Cli @@ -42,6 +43,7 @@ namespace Microsoft.DotNet.Cli ["run3"] = Run3Command.Run, ["restore3"] = Restore3Command.Run, ["pack3"] = Pack3Command.Run, + ["migrate"] = MigrateCommand.Run }; public static int Main(string[] args) diff --git a/src/dotnet/Properties/AssemblyInfo.cs b/src/dotnet/Properties/AssemblyInfo.cs index ffdc13024..c6553d456 100644 --- a/src/dotnet/Properties/AssemblyInfo.cs +++ b/src/dotnet/Properties/AssemblyInfo.cs @@ -3,3 +3,4 @@ using System.Runtime.CompilerServices; [assembly: AssemblyMetadataAttribute("Serviceable", "True")] [assembly: InternalsVisibleTo("dotnet.Tests")] +[assembly: InternalsVisibleTo("Microsoft.DotNet.ProjectJsonMigration.Tests")] \ No newline at end of file diff --git a/src/dotnet/commands/dotnet-build3/Program.cs b/src/dotnet/commands/dotnet-build3/Program.cs index e9efe9698..7630df3c3 100644 --- a/src/dotnet/commands/dotnet-build3/Program.cs +++ b/src/dotnet/commands/dotnet-build3/Program.cs @@ -15,6 +15,5 @@ namespace Microsoft.DotNet.Cli { return new MSBuildForwardingApp(args).Execute(); } - } } diff --git a/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs b/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs new file mode 100644 index 000000000..430e486c1 --- /dev/null +++ b/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Microsoft.Build.Construction; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.ProjectJsonMigration; + +namespace Microsoft.DotNet.Tools.Migrate +{ + public partial class MigrateCommand + { + private string _templateFile; + private string _outputDirectory; + private string _projectJson; + private string _sdkVersion; + + private TemporaryDotnetNewTemplateProject _temporaryDotnetNewProject; + + public MigrateCommand(string templateFile, string outputDirectory, string projectJson, string sdkVersion) + { + _templateFile = templateFile; + _outputDirectory = outputDirectory; + _projectJson = projectJson; + _sdkVersion = sdkVersion; + + _temporaryDotnetNewProject = new TemporaryDotnetNewTemplateProject(); + } + + public int Start() + { + var project = GetProjectJsonPath(_projectJson) ?? _temporaryDotnetNewProject.ProjectJsonPath; + EnsureNotNull(project, "Unable to find project.json"); + var projectDirectory = Path.GetDirectoryName(project); + + var templateFile = _templateFile ?? _temporaryDotnetNewProject.MSBuildProjectPath; + EnsureNotNull(templateFile, "Unable to find default msbuild template"); + + var outputDirectory = _outputDirectory ?? Path.GetDirectoryName(project); + EnsureNotNull(outputDirectory, "Null output directory"); + + var sdkVersion = _sdkVersion ?? new ProjectJsonParser(_temporaryDotnetNewProject.ProjectJson).SdkPackageVersion; + EnsureNotNull(sdkVersion, "Null Sdk Version"); + + var migrationSettings = new MigrationSettings(projectDirectory, outputDirectory, sdkVersion, templateFile); + new ProjectMigrator().Migrate(migrationSettings); + return 0; + } + + private void EnsureNotNull(string variable, string message) + { + if (variable == null) + { + throw new Exception(message); + } + } + + private string GetProjectJsonPath(string projectJson) + { + if (projectJson == null) + { + return null; + } + + if (File.Exists(projectJson)) + { + return projectJson; + } + + if (Directory.Exists(projectJson)) + { + var projectCandidate = Path.Combine(projectJson, "project.json"); + + if (File.Exists(projectCandidate)) + { + return projectCandidate; + } + } + + throw new Exception($"Unable to find project file at {projectJson}"); + } + } +} diff --git a/src/dotnet/commands/dotnet-migrate/Program.cs b/src/dotnet/commands/dotnet-migrate/Program.cs new file mode 100644 index 000000000..8c2c5708e --- /dev/null +++ b/src/dotnet/commands/dotnet-migrate/Program.cs @@ -0,0 +1,55 @@ +// 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 Microsoft.DotNet.Cli.CommandLine; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Tools.Migrate +{ + public partial class MigrateCommand + { + public static int Run(string[] args) + { + DebugHelper.HandleDebugSwitch(ref args); + + CommandLineApplication app = new CommandLineApplication(throwOnUnexpectedArg: false); + app.Name = "dotnet migrate"; + app.FullName = ".NET Migrate Command"; + app.Description = "Command used to migrate project.json projects to msbuild"; + app.HandleResponseFiles = true; + app.AllowArgumentSeparator = true; + app.HelpOption("-h|--help"); + + CommandOption template = app.Option("-t|--template-file", "Base MSBuild template to use for migrated app. The default is the project included in dotnet new -t msbuild", CommandOptionType.SingleValue); + CommandOption output = app.Option("-o|--output", "Directory to output migrated project to. The default is the project directory", CommandOptionType.SingleValue); + CommandOption project = app.Option("-p|--project", "The path to the project to run (defaults to the current directory). Can be a path to a project.json or a project directory", CommandOptionType.SingleValue); + CommandOption sdkVersion = app.Option("-v|--sdk-package-version", "The version of the sdk package that will be referenced in the migrated app. The default is the version of the sdk in dotnet new -t msbuild", CommandOptionType.SingleValue); + + app.OnExecute(() => + { + MigrateCommand migrateCommand = new MigrateCommand( + template.Value(), + output.Value(), + project.Value(), + sdkVersion.Value()); + + return migrateCommand.Start(); + }); + + try + { + return app.Execute(args); + } + catch (Exception ex) + { +#if DEBUG + Reporter.Error.WriteLine(ex.ToString()); +#else + Reporter.Error.WriteLine(ex.Message); +#endif + return 1; + } + } + } +} diff --git a/src/dotnet/commands/dotnet-migrate/ProjectJsonParser.cs b/src/dotnet/commands/dotnet-migrate/ProjectJsonParser.cs new file mode 100644 index 000000000..4856a64aa --- /dev/null +++ b/src/dotnet/commands/dotnet-migrate/ProjectJsonParser.cs @@ -0,0 +1,81 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli +{ + /// + /// Parses select data from a project.json without relying on ProjectModel. + /// Used to parse simple information. + /// + internal class ProjectJsonParser + { + public static string SdkPackageName => "Microsoft.DotNet.Core.Sdk"; + + public string SdkPackageVersion { get; } + + public ProjectJsonParser(JObject projectJson) + { + SdkPackageVersion = GetSdkPackageVersion(projectJson); + } + + private string GetSdkPackageVersion(JObject projectJson) + { + var sdkPackageNode = SelectJsonNodes(projectJson, property => property.Name == SdkPackageName).First(); + + if (sdkPackageNode.Value.Type == JTokenType.String) + { + return (string)sdkPackageNode.Value; + } + else if (sdkPackageNode.Type == JTokenType.Object) + { + var sdkPackageNodeValue = (JObject)sdkPackageNode.Value; + + JToken sdkVersionNode; + if (sdkPackageNodeValue.TryGetValue("version", out sdkVersionNode)) + { + return sdkVersionNode.Value(); + } + else + { + throw new Exception("Unable to determine sdk version, no version node in default template."); + } + } + else + { + throw new Exception("Unable to determine sdk version, no version information found"); + } + } + + private IEnumerable SelectJsonNodes( + JToken jsonNode, + Func condition, + List nodeAccumulator = null) + { + nodeAccumulator = nodeAccumulator ?? new List(); + + if (jsonNode.Type == JTokenType.Object) + { + var eligibleNodes = jsonNode.Children().Where(j => condition(j)); + nodeAccumulator.AddRange(eligibleNodes); + + foreach (var child in jsonNode.Children()) + { + SelectJsonNodes(child.Value, condition, nodeAccumulator: nodeAccumulator); + } + } + else if (jsonNode.Type == JTokenType.Array) + { + foreach (var child in jsonNode.Children()) + { + SelectJsonNodes(child, condition, nodeAccumulator: nodeAccumulator); + } + } + + return nodeAccumulator; + } + } +} diff --git a/src/dotnet/commands/dotnet-migrate/TemporaryDotnetNewTemplateProject.cs b/src/dotnet/commands/dotnet-migrate/TemporaryDotnetNewTemplateProject.cs new file mode 100644 index 000000000..86a944c4b --- /dev/null +++ b/src/dotnet/commands/dotnet-migrate/TemporaryDotnetNewTemplateProject.cs @@ -0,0 +1,85 @@ +using Microsoft.Build.Construction; +using Microsoft.DotNet.Cli; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli +{ + internal class TemporaryDotnetNewTemplateProject + { + private static string s_temporaryDotnetNewMSBuildProjectName = "p"; + + public TemporaryDotnetNewTemplateProject() + { + ProjectDirectory = CreateDotnetNewMSBuild(s_temporaryDotnetNewMSBuildProjectName); + MSBuildProject = GetMSBuildProject(ProjectDirectory); + ProjectJson = GetProjectJson(ProjectDirectory); + } + + public ProjectRootElement MSBuildProject { get; } + public JObject ProjectJson { get; } + public string ProjectDirectory { get; } + + public string ProjectJsonPath => Path.Combine(ProjectDirectory, "project.json"); + public string MSBuildProjectPath => Path.Combine(ProjectDirectory, s_temporaryDotnetNewMSBuildProjectName); + + public void Clean() + { + Directory.Delete(ProjectDirectory, true); + } + + private string CreateDotnetNewMSBuild(string projectName) + { + var guid = Guid.NewGuid().ToString(); + var tempDir = Path.Combine( + Path.GetTempPath(), + this.GetType().Namespace, + guid, + s_temporaryDotnetNewMSBuildProjectName); + + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + Directory.CreateDirectory(tempDir); + + RunCommand("new", new string[] { "-t", "msbuild" }, tempDir); + + return tempDir; + } + + private ProjectRootElement GetMSBuildProject(string temporaryDotnetNewMSBuildDirectory) + { + var templateProjPath = Path.Combine(temporaryDotnetNewMSBuildDirectory, + s_temporaryDotnetNewMSBuildProjectName + ".csproj"); + + return ProjectRootElement.Open(templateProjPath); + } + + private JObject GetProjectJson(string temporaryDotnetNewMSBuildDirectory) + { + var projectJsonFile = Path.Combine(temporaryDotnetNewMSBuildDirectory, "project.json"); + return JObject.Parse(File.ReadAllText(projectJsonFile)); + } + + private void RunCommand(string commandToExecute, IEnumerable args, string workingDirectory) + { + var command = new DotNetCommandFactory() + .Create(commandToExecute, args) + .WorkingDirectory(workingDirectory) + .CaptureStdOut() + .CaptureStdErr(); + + var commandResult = command.Execute(); + + if (commandResult.ExitCode != 0) + { + throw new Exception($"Failed to run {commandToExecute} in directory: {workingDirectory}"); + } + } + } +} diff --git a/src/dotnet/project.json b/src/dotnet/project.json index 18f4ae78c..64590c7a4 100644 --- a/src/dotnet/project.json +++ b/src/dotnet/project.json @@ -45,6 +45,9 @@ "Microsoft.DotNet.Configurer": { "target": "project" }, + "Microsoft.DotNet.ProjectJsonMigration": { + "target": "project" + }, "Microsoft.DotNet.Tools.Test": { "target": "project" },