-
Notifications
You must be signed in to change notification settings - Fork 222
/
Copy pathCodeConvProgram.cs
199 lines (167 loc) · 11.7 KB
/
CodeConvProgram.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
using System;
using System.ComponentModel.DataAnnotations;
using McMaster.Extensions.CommandLineUtils;
using System.Threading;
using ICSharpCode.CodeConverter.Common;
using System.Threading.Tasks;
using ICSharpCode.CodeConverter.DotNetTool.Util;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using ICSharpCode.CodeConverter.CommandLine.Util;
using System.Reflection;
using System.Diagnostics;
using Microsoft.VisualStudio.Threading;
// ReSharper disable UnassignedGetOnlyAutoProperty - Command line framework initializes these by reflection
namespace ICSharpCode.CodeConverter.CommandLine;
[Command(Name = "codeconv", Description = "Convert code from VB.NET to C# or C# to VB.NET",
ExtendedHelpText = @"
Remarks:
Converts all projects in a solution from VB.NET to C#.
Please backup / commit your files to source control before use.
We recommend running the conversion in-place (i.e. not specifying an output directory) for best performance.
See https://github.com/icsharpcode/CodeConverter for the source code, issues, Visual Studio extension and other info.
")]
[HelpOption("-h|--help")]
public partial class CodeConvProgram
{
public const string CoreOptionDefinition = "--core-only";
/// <remarks>Calls <see cref="OnExecuteAsync(CommandLineApplication)"/> by reflection</remarks>
public static async Task<int> Main(string[] args) => await CommandLineApplication.ExecuteAsync<CodeConvProgram>(args);
// ReSharper disable once UnusedMember.Local - Used by reflection in CommandLineApplication.ExecuteAsync
#pragma warning disable IDE0052 // Remove unread private members - Used by reflection in CommandLineApplication.ExecuteAsync
// ReSharper disable once UnusedParameter.Local - Used by reflection in CommandLineApplication.ExecuteAsync
private async Task<int> OnExecuteAsync(CommandLineApplication _) => await ExecuteAsync();
#pragma warning restore IDE0052 // Remove unread private members
[FileExists]
[Required]
[Argument(0, "Source solution path", "The solution containing project(s) to be converted.")]
public string SolutionPath { get; } = "";
[Option("-i|--include", "Regex matching project file paths to convert. Can be used multiple times", CommandOptionType.MultipleValue)]
public string[] Include { get; } = Array.Empty<string>();
[Option("-e|--exclude", "Regex matching project file paths to exclude from conversion. Can be used multiple times", CommandOptionType.MultipleValue)]
public string[] Exclude { get; } = Array.Empty<string>();
[Option("-t|--target-language", "The language to convert to.", CommandOptionType.SingleValue, ValueName = nameof(Language.CS) + " | " + nameof(Language.VB))]
public Language? TargetLanguage { get; }
[Option("-f|--force", "Wipe the output directory before conversion", CommandOptionType.NoValue)]
public bool Force { get; }
[Option(CoreOptionDefinition, "Force dot net core build if converting only .NET Core projects and seeing pre-conversion compile errors", CommandOptionType.NoValue)]
public bool CoreOnlyProjects { get; }
[Option("-b|--best-effort", "Overrides warnings about compilation issues with input, and attempts a best effort conversion anyway", CommandOptionType.NoValue)]
public bool BestEffort { get; }
[FileNotExists]
[Option("-o|--output-directory", "Empty or non-existent directory to copy the solution directory to, then write the output.", CommandOptionType.SingleValue)]
public string? OutputDirectory { get; }
/// <remarks>
/// Also allows semicolon and comma splitting of build properties to be compatible with https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-command-line-reference?view=vs-2019#switches
/// </remarks>
[Option("-p|--build-property", "Set build properties in format: propertyName=propertyValue. Can be used multiple times", CommandOptionType.MultipleValue, ValueName = "Configuration=Release")]
public string[] BuildProperty { get; } = Array.Empty<string>();
private async Task<int> ExecuteAsync()
{
// Ideally we'd be able to use MSBuildLocator.QueryVisualStudioInstances(DiscoveryType.VisualStudioSetup) from .NET core, but it will never be supported: https://github.com/microsoft/MSBuildLocator/issues/61
// Instead, if MSBuild 16.0+ is available, start a .NET framework process and let it run with that
if (_runningInNetCore && !CoreOnlyProjects) {
if (await GetLatestMsBuildExePathAsync() is { } latestMsBuildExePath) {
return await RunNetFrameworkExeAsync(latestMsBuildExePath);
} else {
Console.WriteLine($"Using dot net SDK MSBuild which only works for dot net core projects.");
}
}
try {
Progress<ConversionProgress> progress = new Progress<ConversionProgress>(s => Console.Out.WriteLine(s.ToString()));
await ConvertAsync(progress, CancellationToken.None);
} catch (Exception ex) {
await Task.Delay(100); // Give any async progress updates a moment to flush so they don't clash with this:
if (!(ex is ValidationException)) {
await Console.Error.WriteLineAsync(Environment.NewLine + ex.GetType() + ex.StackTrace);
if (ex is ReflectionTypeLoadException rtle && rtle.LoaderExceptions is IEnumerable<Exception> loaderExceptions) {
foreach (var e in loaderExceptions) {
await Console.Error.WriteLineAsync(e.Message);
}
}
}
await Console.Error.WriteLineAsync(Environment.NewLine + ex.Message + Environment.NewLine +
"Please report issues at github.com/icsharpcode/CodeConverter"
);
return ProgramExitCodes.EX_SOFTWARE;
}
Console.WriteLine();
Console.WriteLine("Exiting successfully. Report any issues at github.com/icsharpcode/CodeConverter to help us improve the accuracy of future conversions");
return 0;
}
private static async Task<int> RunNetFrameworkExeAsync(string latestMsBuildExePath)
{
Console.WriteLine($"Using .NET Framework MSBuild from {latestMsBuildExePath}");
var assemblyDirectoryPath = Path.GetDirectoryName(Environment.GetCommandLineArgs()[0]);
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
if (string.IsNullOrWhiteSpace(assemblyDirectoryPath)) throw new InvalidOperationException("Could not retrieve executing assembly directory");
var netFrameworkExe = Path.Combine(assemblyDirectoryPath, "..", "..", "NetFramework", "ICSharpCode.CodeConverter.CodeConv.NetFramework.exe");
if (!File.Exists(netFrameworkExe) && Path.GetDirectoryName(assemblyDirectoryPath) is { } assemblyDirectoryParentPath) {
var debugAssemblyDirectoryPath = assemblyDirectoryParentPath.Replace("CommandLine\\CodeConv\\", "CommandLine\\CodeConv.NetFramework\\");
var debugNetFrameworkExe = Path.Combine(debugAssemblyDirectoryPath, "ICSharpCode.CodeConverter.CodeConv.NetFramework.exe");
netFrameworkExe = File.Exists(debugNetFrameworkExe) ? debugNetFrameworkExe : throw new FileNotFoundException($"Cannot find net framework exe at `{netFrameworkExe}`. Using the --core-only flag to get work around this.");
}
var exitCode = await ProcessRunner.ConnectConsoleGetExitCodeAsync(netFrameworkExe, args);
return exitCode;
}
private async Task ConvertAsync(IProgress<ConversionProgress> progress, CancellationToken cancellationToken)
{
string finalSolutionPath = Path.IsPathRooted(SolutionPath)
? SolutionPath
: Path.Combine(Environment.CurrentDirectory, SolutionPath);
IProgress<string> strProgress = new Progress<string>(p => progress.Report(new ConversionProgress(p)));
if (!string.Equals(Path.GetExtension(finalSolutionPath), ".sln", StringComparison.OrdinalIgnoreCase)) {
throw new ValidationException("Solution path must end in `.sln`");
}
string? directoryName = string.IsNullOrWhiteSpace(OutputDirectory) ? Path.GetDirectoryName(finalSolutionPath) : OutputDirectory;
var outputDirectory = new DirectoryInfo(directoryName ?? throw new InvalidOperationException("Output directory could not be determined"));
if (await CouldOverwriteUncommittedFilesAsync(outputDirectory)) {
var action = string.IsNullOrWhiteSpace(OutputDirectory) ? "may be overwritten" : "will be deleted";
strProgress.Report($"WARNING: There are files in {outputDirectory.FullName} which {action}, and aren't committed to git");
if (Force) strProgress.Report("Continuing with possibility of data loss due to force option.");
else throw new ValidationException("Aborting to avoid data loss (see above warning). Commit the files to git, remove them, or use the --force option to override this check.");
}
var properties = ParsedProperties();
var joinableTaskFactory = new JoinableTaskFactory(new JoinableTaskContext());
var msbuildWorkspaceConverter = new MSBuildWorkspaceConverter(finalSolutionPath, CoreOnlyProjects, joinableTaskFactory, BestEffort, properties);
var converterResultsEnumerable = msbuildWorkspaceConverter.ConvertProjectsWhereAsync(ShouldIncludeProject, TargetLanguage, progress, cancellationToken);
await ConversionResultWriter.WriteConvertedAsync(converterResultsEnumerable, finalSolutionPath, outputDirectory, Force, true, strProgress, cancellationToken);
}
private static async Task<bool> CouldOverwriteUncommittedFilesAsync(DirectoryInfo outputDirectory)
{
if (!outputDirectory.Exists || !outputDirectory.ContainsDataOtherThanGitDir()) return false;
return !await outputDirectory.IsGitDiffEmptyAsync();
}
private Dictionary<string, string> ParsedProperties()
{
var props = BuildProperty.SelectMany(bp => bp.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Split('=')));
return props.ToLookup(s => s[0], GetValidatedPropertyValue).ToDictionary();
}
private string GetValidatedPropertyValue(string[] s)
{
return s.Length == 2 ? s[1] : throw new ValidationException($"Build property {s[0]} must have exactly one value, e.g. `{s[0]}=1`");
}
private bool ShouldIncludeProject(Project project)
{
string projectFilePath = project.FilePath ?? "";
var isIncluded = !Include.Any() || Include.Any(regex => Regex.IsMatch(projectFilePath, regex));
return isIncluded && Exclude.All(regex => !Regex.IsMatch(projectFilePath, regex));
}
private static async Task<string?> GetLatestMsBuildExePathAsync()
{
// The second path here is available on github action agents: https://github.com/microsoft/setup-msbuild#how-does-this-work
var pathsToCheck = new[] {@"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe", @"%ProgramData%\chocolatey\bin"}
.Select(Environment.ExpandEnvironmentVariables).ToArray();
var vsWhereExe = pathsToCheck.FirstOrDefault(File.Exists)
?? throw new FileNotFoundException($"Could not find VSWhere in: {string.Join(", ", pathsToCheck.Select(p => $"`{p}`"))}");
var args = new[] { "-latest", "-prerelease", "-products", "*", "-requires", "Microsoft.Component.MSBuild", "-version", "[16.0,]", "-find", @"MSBuild\**\Bin\MSBuild.exe" };
var (exitCode, stdOut, _) = await new ProcessStartInfo(vsWhereExe) {
Arguments = ArgumentEscaper.EscapeAndConcatenate(args)
}.GetOutputAsync();
if (exitCode == 0 && !string.IsNullOrWhiteSpace(stdOut)) return stdOut;
return null;
}
}