Skip to content

Commit

Permalink
macOS port of the upgrade-assistant (#1409)
Browse files Browse the repository at this point in the history
* Use a hard-coded PackageId for Extensions.Default.Analyzers

Don't use $(MSBuildProjectFullPath)* because it includes the full path
and breaks NuGet caching logic on non-Windows platforms.

* Do not require the ENABLE_CROSS_PLATFORM feature flag to run on macOS

* Don't crash in VisualStudioFinder.Configure() due to COM exceptions

* Use sudo on macOS when running `dotnet workload install maui`

* Trim trailing whitespace (CRLF) from the using directive when searching for it in the template

This is an actual code-change fix that helps unit tests pass

* A bunch of fixes to the unit tests

Most of these fixes fit into the following categories:

1. File path construction (using the correct path separator for the platform)
2. Using .ReplaceLineEndings() on strings that represent file content so we get consistent line endings when comparing expected/actual results
3. Adding TextSpan file offsets to use on Unix platforms (e.g. macOS) which will be different from the TextSpans on Windows

* Fixed MappedSubTextTests (and found a legit bug in MappedSubText regarding line ending assumptions)

* Fixed RazorHelperUpdaterTests

File paths needed to be sorted. The returned order is different on macOS than on Windows for some reason.

* Fixed RazorMappedTextReplacerTests and RazorSourceUpdaterTests

Use .ReplaceLineEndings() on source strings

* Fixed WCFUpdaterTests

* Fixed pruning of duplicate Compile/None items on macOS

Needed to canonicalize the EvaluatedInclude paths before comparing them.

* More path directory separator fixes

* Need to compare item.Include/EvaluatedInclude using canonical paths

* Moved GetProjectName() call out of the inner loop

* More canonicalization of paths when used in comparisons

* More canonicalization of paths

* Fixed up paths in <ItemType Update=...> and <ItemType Remove=...>

* Needed to add more .ReplaceLineEndings() in the unit tests for Razor

* Only use the VisualStudioPath/Version on the Windows platform

* Create a temp MSBuildExtensionsPath on macOS

The upgrade-assistant uses dotnet's MSBuild while older Xamarin.iOS/Mac/Android/etc
projects used Mono's XBuild (an MSBuild clone).

Unfortunately, there's no way to specify that dotnet's MSBuild should look in both
/usr/local/share/dotnet/sdk/{version} directory *and* in the
/Library/Frameworks/Mono.framework/External/xbuild directories for the
$(MSBuildExtensionsPath) imports.

In order to be able to load Xamarin.* projects, we need to create a temp dir that
includes *both* the standard targets/props files *and* the Xamarin props/targets files
and the only way to do that seems to be to create a temp directory full of symlinks.

* Optimized ProjectRootElementExtensionsForConversion.GetProjectName()

Instead of using projectPath.Split('/').Last() and then result.Substring(),
just get the start/end indexes of the substring we want and only do 1
Substring() operation.

* Add a start-up warning for MacOS

* Use "dotnet-upgrade-assistant" in the temp directory path

* Use a predictable ~/.dotnet-upgrade-assistant/dotnet-sdk/{version} directory

* Use !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) when deciding whether or not to use sudo dotnet commands

* Disable the PCL test on non-Windows

* Disable some DependencyInjections Options tests on non-Windows platforms

* Disable WinUI and WPF migrations on non-Windows platforms.

These probably don't make sense to try to migrate on macOS.

* Disable Razor UpgradeSteps on non-Windows platforms

* Disable VisualBasic and WCF UpgradeSteps on non-Windows platforms

* Updated warning message for MacOS

* Reduce code duplication in conversion between file path separators

* Don't swallow exceptions thrown while creating MSBuildExtensionsPath symlinks

Surface these exceptions to the user.

* Removed FIXME that is no longer necessary

* File.Exists() returns false for symlinks to directories

Use a different approach to avoid exceptions trying to create a
symlink that already exists.
  • Loading branch information
jstedfast authored Mar 3, 2023
1 parent f7bea53 commit 5fd3400
Show file tree
Hide file tree
Showing 40 changed files with 775 additions and 305 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@
<data name="ListExtensionItem" xml:space="preserve">
<value>{Name}: {Source}</value>
</data>
<data name="MacOSWarning" xml:space="preserve">
<value>MacOS support for this tool is limited to migrating Xamarin.Forms to MAUI. Other migration paths are not supported and may or may not work correctly.</value>
</data>
<data name="NonWindowsWarning" xml:space="preserve">
<value>This tool is not supported on non-Windows platforms due to dependencies on Visual Studio.</value>
</data>
Expand Down
6 changes: 5 additions & 1 deletion src/cli/Microsoft.DotNet.UpgradeAssistant.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ public static class Program
{
public static Task<int> Main(string[] args)
{
if (FeatureFlags.IsWindowsCheckEnabled && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Console.WriteLine(LocalizedStrings.MacOSWarning);
}
else if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Console.WriteLine(LocalizedStrings.NonWindowsWarning);
return Task.FromResult(ErrorCodes.PlatformNotSupported);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ public static class FeatureFlags
{
private const string AnalyzeBinaries = "ANALYZE_BINARIES";
private const string SolutionWideSdkConversion = "SOLUTION_WIDE_SDK_CONVERSION";
private const string EnableCrossPlatform = "ENABLE_CROSS_PLATFORM";

public static readonly IReadOnlyCollection<string> RegisteredFeatures = new[]
{
SolutionWideSdkConversion,
EnableCrossPlatform,
AnalyzeBinaries
};

Expand All @@ -38,8 +36,6 @@ private static ICollection<string> CreateFeatures()

public static bool IsRegistered(string name) => _features.Contains(name);

public static bool IsWindowsCheckEnabled => !_features.Contains(EnableCrossPlatform);

public static bool IsAnalyzeBinariesEnabled => _features.Contains(AnalyzeBinaries);

public static bool IsSolutionWideSdkConversionEnabled => _features.Contains(SolutionWideSdkConversion);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;

namespace Microsoft.DotNet.UpgradeAssistant
{
public static class PathHelpers
{
public static string GetNativePath(string path)
{
if (Path.DirectorySeparatorChar == '/')
{
return path.Replace('\\', '/');
}

return path;
}

public static string GetIncludePath(string path)
{
if (Path.DirectorySeparatorChar == '/')
{
return path.Replace('/', '\\');
}

return path;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,17 +164,17 @@ public void AddItem(ProjectItemDescriptor projectItem)
var item = ProjectRoot.CreateItemElement(projectItem.ItemType.Name);
if (projectItem.Include is not null)
{
item.Include = projectItem.Include;
item.Include = PathHelpers.GetIncludePath(projectItem.Include);
}

if (projectItem.Exclude is not null)
{
item.Exclude = projectItem.Remove;
item.Exclude = PathHelpers.GetIncludePath(projectItem.Exclude);
}

if (projectItem.Remove is not null)
{
item.Remove = projectItem.Remove;
item.Remove = PathHelpers.GetIncludePath(projectItem.Remove);
}

itemGroup.AppendChild(item);
Expand Down Expand Up @@ -230,9 +230,11 @@ public void RemoveProperty(string propertyName)
}
}

private static string GetPathRelativeToProject(string path, string projectDir) =>
Path.IsPathFullyQualified(path)
? path
: Path.Combine(projectDir, path);
private static string GetPathRelativeToProject(string path, string projectDir)
{
path = PathHelpers.GetNativePath(path);

return Path.IsPathFullyQualified(path) ? path : Path.Combine(projectDir, path);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Evaluation;
Expand All @@ -18,6 +19,8 @@ namespace Microsoft.DotNet.UpgradeAssistant.MSBuild
{
internal sealed class MSBuildWorkspaceUpgradeContext : IUpgradeContext, IDisposable
{
private const string MacOSMonoFrameworkMSBuildExtensionsDir = "/Library/Frameworks/Mono.framework/External/xbuild";

private readonly ILogger<MSBuildWorkspaceUpgradeContext> _logger;
private readonly Dictionary<string, IProject> _projectCache;
private readonly IOptions<WorkspaceOptions> _options;
Expand Down Expand Up @@ -140,20 +143,119 @@ public IEnumerable<IProject> Projects
}
}

private static void CreateSymbolicLinks(string targetDir, string sourceDir)
{
foreach (var entry in Directory.EnumerateFileSystemEntries(sourceDir))
{
var target = Path.Combine(targetDir, Path.GetFileName(entry));

var fileInfo = new FileInfo(target);
if (fileInfo.Exists)
{
if (fileInfo.LinkTarget is not null && fileInfo.LinkTarget.Equals(entry, StringComparison.Ordinal))
{
continue;
}

File.Delete(target);
}
else
{
var dirInfo = new DirectoryInfo(target);
if (dirInfo.Exists)
{
if (dirInfo.LinkTarget is not null && dirInfo.LinkTarget.Equals(entry, StringComparison.Ordinal))
{
continue;
}

Directory.Delete(target);
}
}

File.CreateSymbolicLink(target, entry);
}
}

private static string? GetMacOSMSBuildExtensionsPath(WorkspaceOptions options)
{
const string DefaultDotnetSdkLocation = "/usr/local/share/dotnet/sdk/";

if (options.MSBuildPath == null || !options.MSBuildPath.StartsWith(DefaultDotnetSdkLocation, StringComparison.Ordinal))
{
return null;
}

string? msbuildExtensionsPath = null;

if (Directory.Exists(MacOSMonoFrameworkMSBuildExtensionsDir))
{
// Check to see if the specified MSBuildPath contains the Mono.framework build extensions.
var monoExtensionDirectories = Directory.GetDirectories(MacOSMonoFrameworkMSBuildExtensionsDir);
var createTempExtensionsDir = false;

foreach (var monoExtensionDir in monoExtensionDirectories)
{
var dotnetExtensionDir = Path.Combine(options.MSBuildPath, Path.GetFileName(monoExtensionDir));
if (!Directory.Exists(dotnetExtensionDir))
{
createTempExtensionsDir = true;
break;
}
}

// If the specified MSBuildPath does not contain the Mono.framework build extensions, create a temp
// directory that we'll use to symlink everything.
if (createTempExtensionsDir)
{
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var versionDir = Path.GetFileName(options.MSBuildPath.TrimEnd('/'));

msbuildExtensionsPath = Path.Combine(homeDir, ".dotnet-upgrade-assistant", "dotnet-sdk", versionDir);

if (!Directory.Exists(msbuildExtensionsPath))
{
Directory.CreateDirectory(msbuildExtensionsPath);
}

// First, create symbolic links to all of the dotnet MSBuild file system entries.
CreateSymbolicLinks(msbuildExtensionsPath, options.MSBuildPath);

// Then create the symbolic links to the Mono.framework/External/xbuild system entries.
CreateSymbolicLinks(msbuildExtensionsPath, MacOSMonoFrameworkMSBuildExtensionsDir);
}
}

return msbuildExtensionsPath;
}

private static Dictionary<string, string> CreateProperties(WorkspaceOptions options)
{
var properties = new Dictionary<string, string>();

if (options.VisualStudioPath is string vsPath)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
properties.Add("VSINSTALLDIR", vsPath);
properties.Add("MSBuildExtensionsPath32", Path.Combine(vsPath, "MSBuild"));
properties.Add("MSBuildExtensionsPath", Path.Combine(vsPath, "MSBuild"));
}
if (options.VisualStudioPath is string vsPath)
{
properties.Add("VSINSTALLDIR", vsPath);
properties.Add("MSBuildExtensionsPath32", Path.Combine(vsPath, "MSBuild"));
properties.Add("MSBuildExtensionsPath", Path.Combine(vsPath, "MSBuild"));
}

if (options.VisualStudioVersion is int version)
if (options.VisualStudioVersion is int version)
{
properties.Add("VisualStudioVersion", $"{version}.0");
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
properties.Add("VisualStudioVersion", $"{version}.0");
var msbuildExtensionsPath = GetMacOSMSBuildExtensionsPath(options);

if (msbuildExtensionsPath != null)
{
properties.Add("MSBuildExtensionsPath32", msbuildExtensionsPath);
properties.Add("MSBuildExtensionsPath", msbuildExtensionsPath);
}
}

return properties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
Expand Down Expand Up @@ -78,7 +79,8 @@ internal static void WorkAroundRoslynIssue36781(this ProjectRootElement rootElem
}

// Skip items that are only included once
if (project.Items.Count(i2 => i2.EvaluatedInclude.Equals(i.EvaluatedInclude, StringComparison.Ordinal)) <= 1)
var path = PathHelpers.GetIncludePath(i.EvaluatedInclude);
if (project.Items.Count(i2 => PathHelpers.GetIncludePath(i2.EvaluatedInclude).Equals(path, StringComparison.OrdinalIgnoreCase)) <= 1)
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,16 @@ public VisualStudioFinder(ILogger<VisualStudioFinder> logger)

public void Configure(WorkspaceOptions options)
{
(options.VisualStudioPath, options.VisualStudioVersion) = GetLatestVisualStudioPath(options.VisualStudioPath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
(options.VisualStudioPath, options.VisualStudioVersion) = GetLatestVisualStudioPath(options.VisualStudioPath);
}
else
{
// MSBuildWorkspaceUpgradeContext.CreateProperties() uses the VS path to set the MSBuildExtensionsPath[32]
// environment variables and there is some logging in UpgraderMsBuildExtensions.AddMsBuild().
_logger.LogInformation("Visual Studio path not required on macOS");
}
}

private (string? Path, int? Version) GetLatestVisualStudioPath(string? suppliedPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,20 @@ private static IEnumerable<Operation<NuGetReference>> UpdatePackageAddition(IDep
var containsService = false;
foreach (var f in files)
{
var root = CSharpSyntaxTree.ParseText(File.ReadAllText(f)).GetRoot();
if (ContainsIdentifier(root, "ChannelFactory") || ContainsIdentifier(root, "ClientBase"))
{
return packages.Additions;
}
var path = PathHelpers.GetNativePath(f);

if (!containsService && ContainsIdentifier(root, "ServiceHost"))
if (File.Exists(path))
{
containsService = true;
var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot();
if (ContainsIdentifier(root, "ChannelFactory") || ContainsIdentifier(root, "ClientBase"))
{
return packages.Additions;
}

if (!containsService && ContainsIdentifier(root, "ServiceHost"))
{
containsService = true;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<!-- Avoid ID conflicts with the package project. -->
<PackageId>*$(MSBuildProjectFullPath)*</PackageId>
<PackageId>Real.Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers</PackageId>
</PropertyGroup>
<ItemGroup>
<None Remove="DefaultApiAlerts.apitargets" />
Expand Down
Loading

0 comments on commit 5fd3400

Please sign in to comment.