Skip to content

Commit

Permalink
Fix analysis on MacOSX with .NET 8 when begin runtime doesn't match w…
Browse files Browse the repository at this point in the history
…ith build runtime (#1863)

Co-authored-by: Zsolt Kolbay <zsolt.kolbay@sonarsource.com>
  • Loading branch information
antonioaversa and zsolt-kolbay-sonarsource committed Feb 16, 2024
1 parent 53e50fc commit b7b0400
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 152 deletions.
@@ -0,0 +1,43 @@
/*
* SonarScanner for .NET
* Copyright (C) 2016-2024 SonarSource SA
* mailto: info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static SonarScanner.MSBuild.Common.EnvironmentBasedPlatformHelper;

namespace SonarScanner.MSBuild.Common.Test;

[TestClass]
public class EnvironmentBasedPlatformHelperTests
{
[TestMethod]
public void GetFolderPath_WithUserProfile()
{
Instance.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.None)
.Should().Be(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.None));
}

[TestMethod]
public void DirectoryExists_WithCurrentDirectory()
{
Instance.DirectoryExists(Environment.CurrentDirectory).Should().BeTrue();
}
}
185 changes: 101 additions & 84 deletions Tests/SonarScanner.MSBuild.Common.Test/MsBuildPathSettingsTests.cs

Large diffs are not rendered by default.

22 changes: 19 additions & 3 deletions Tests/SonarScanner.MSBuild.Shim.Test/SonarScannerWrapperTests.cs
Expand Up @@ -32,8 +32,6 @@ namespace SonarScanner.MSBuild.Shim.Test
[TestClass]
public class SonarScannerWrapperTests
{
private const string ExpectedConsoleMessagePrefix = "Args passed to dummy scanner: ";

public TestContext TestContext { get; set; }

#region Tests
Expand Down Expand Up @@ -282,6 +280,24 @@ public void FindScannerExe_ReturnsScannerCliBat()
scannerCliScriptPath.Should().EndWithEquivalentOf(@"\bin\sonar-scanner.bat");
}

[TestMethod]
public void FindScannerExe_WhenNonWindows_ReturnsNoExtension()
{
// Act
var scannerCliScriptPath = SonarScannerWrapper.FindScannerExe(new UnixTestPlatformHelper());

// Assert
Path.GetExtension(scannerCliScriptPath).Should().BeNullOrEmpty();
}

private sealed class UnixTestPlatformHelper : IPlatformHelper
{
public PlatformOS OperatingSystem => PlatformOS.Unix;

public string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option) => throw new NotImplementedException();
public bool DirectoryExists(string path) => throw new NotImplementedException();
}

#endregion Tests

#region Private methods
Expand Down Expand Up @@ -359,7 +375,7 @@ private static void CheckArgDoesNotExist(string argToCheck, MockProcessRunner mo

private static void CheckEnvVarExists(string varName, string expectedValue, MockProcessRunner mockRunner)
{
mockRunner.SuppliedArguments.EnvironmentVariables.ContainsKey(varName).Should().BeTrue();
mockRunner.SuppliedArguments.EnvironmentVariables.Should().ContainKey(varName);
mockRunner.SuppliedArguments.EnvironmentVariables[varName].Should().Be(expectedValue);
}
}
Expand Down
74 changes: 74 additions & 0 deletions src/SonarScanner.MSBuild.Common/EnvironmentBasedPlatformHelper.cs
@@ -0,0 +1,74 @@
/*
* SonarScanner for .NET
* Copyright (C) 2016-2024 SonarSource SA
* mailto: info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;

namespace SonarScanner.MSBuild.Common;

public sealed class EnvironmentBasedPlatformHelper : IPlatformHelper
{
public static IPlatformHelper Instance { get; } = new EnvironmentBasedPlatformHelper();
private readonly Lazy<PlatformOS> operatingSystem = new(CurrentOperatingSystem);

public PlatformOS OperatingSystem => operatingSystem.Value;

private EnvironmentBasedPlatformHelper()
{
}

public string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option) => Environment.GetFolderPath(folder, option);
public bool DirectoryExists(string path) => Directory.Exists(path);

// Not stable testable, manual testing was done by running the scanner on Windows, Mac OS X and Linux.
[ExcludeFromCodeCoverage]
private static PlatformOS CurrentOperatingSystem()
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
return PlatformOS.Windows;
}
else if (IsMacOSX())
{
return PlatformOS.MacOSX;
}
// Note: the Check for Mac OS X must preceed the check for Unix, because Environment.OSVersion.Platform returns PlatformID.Unix on Mac OS X
else if (Environment.OSVersion.Platform == PlatformID.Unix)
{
return PlatformOS.Unix;
}
else
{
return PlatformOS.Unknown;
}
}

// RuntimeInformation.IsOSPlatform is not suported in .NET Framework 4.6.2, it's only available from 4.7.1
// SystemVersion.plist exists on Mac OS X (and iOS) at least since 2002, so it's safe to check it, even though it's not a robust, future-proof solution.
// See: https://stackoverflow.com/a/38795621
// TODO: once we drop support for .NET Framework 4.6.2 remove the call to File.Exists and use RuntimeInformation.IsOSPlatform instead of the Environment.OSVersion.Platform property
private static bool IsMacOSX() =>
#if NETSTANDARD1_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX);
#else
File.Exists("/System/Library/CoreServices/SystemVersion.plist");
#endif
}
Expand Up @@ -20,13 +20,19 @@

using System;

namespace SonarScanner.MSBuild.Common
namespace SonarScanner.MSBuild.Common;

public enum PlatformOS
{
Unknown,
Windows,
Unix,
MacOSX,
}

public interface IPlatformHelper
{
public static class PlatformHelper
{
public static bool IsWindows()
{
return Environment.OSVersion.Platform == PlatformID.Win32NT;
}
}
PlatformOS OperatingSystem { get; }
string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option);
bool DirectoryExists(string path);
}
105 changes: 53 additions & 52 deletions src/SonarScanner.MSBuild.Common/MsBuildPathSettings.cs
Expand Up @@ -45,22 +45,15 @@ public class MsBuildPathSettings : IMsBuildPathsSettings
/// </remarks>
private readonly string[] msBuildVersions = new[] { "4.0", "10.0", "11.0", "12.0", "14.0", "15.0", "Current" };

private readonly Func<Environment.SpecialFolder, Environment.SpecialFolderOption, string> environmentGetFolderPath;
private readonly Func<bool> isWindows;
private readonly Func<string, bool> directoryExists;
private readonly IPlatformHelper platformHelper;

public MsBuildPathSettings() : this(Environment.GetFolderPath, PlatformHelper.IsWindows, Directory.Exists)
public MsBuildPathSettings() : this(EnvironmentBasedPlatformHelper.Instance)
{
}

public /* for testing purposes */ MsBuildPathSettings(
Func<Environment.SpecialFolder, Environment.SpecialFolderOption, string> environmentGetFolderPath,
Func<bool> isWindows,
Func<string, bool> directoryExists)
public /* for testing purposes */ MsBuildPathSettings(IPlatformHelper platformHelper)
{
this.environmentGetFolderPath = environmentGetFolderPath;
this.isWindows = isWindows;
this.directoryExists = directoryExists;
this.platformHelper = platformHelper;
}

public IEnumerable<string> GetImportBeforePaths()
Expand All @@ -82,14 +75,14 @@ public IEnumerable<string> GetImportBeforePaths()

private IEnumerable<string> DotnetImportBeforePathsLinuxMac()
{
if (this.isWindows())
if (platformHelper.OperatingSystem == PlatformOS.Windows)
{
return Enumerable.Empty<string>();
}

// We don't need to create the paths here - the ITargetsInstaller will do it.
// Also, see bug #681: Environment.SpecialFolderOption.Create fails on some versions of NET Core on Linux
var userProfilePath = this.environmentGetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify);
var userProfilePath = platformHelper.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify);

if (string.IsNullOrEmpty(userProfilePath))
{
Expand Down Expand Up @@ -120,9 +113,7 @@ private IEnumerable<string> DotnetImportBeforePathsLinuxMac()
/// </summary>
private IEnumerable<string> GetLocalApplicationDataPaths()
{
var localAppData = environmentGetFolderPath(
Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.DoNotVerify);
var localAppData = platformHelper.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify);

// Return empty enumerable when Local AppData is empty. In this case an exception should be thrown at the call site.
if (string.IsNullOrWhiteSpace(localAppData))
Expand All @@ -132,52 +123,62 @@ private IEnumerable<string> GetLocalApplicationDataPaths()

yield return localAppData;

// The code below is Windows-specific, no need to be executed on non-Windows platforms.
if (!isWindows())
if (platformHelper.OperatingSystem == PlatformOS.MacOSX)
{
yield break;
// Target files need to be placed under LocalApplicationData, to be picked up by MSBuild.
// Due to the breaking change of GetFolderPath on MacOSX in .NET8, we need to make sure we copy the targets file
// both to the old and to the new location, because we don't know what runtime the build will be run on, and that
// may differ from the runtime of the scanner.
// See https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/8.0/getfolderpath-unix#macos
var userProfile = platformHelper.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify);
yield return Path.Combine(userProfile, ".local", "share"); // LocalApplicationData on .Net 7 and earlier
yield return Path.Combine(userProfile, "Library", "Application Support"); // LocalApplicationData on .Net 8 and later
}

// When running under Local System account on a 64bit OS, the local application data folder
// is inside %windir%\system32
// When a process copies a file in this location, the OS will automatically redirect it to:
// for 32bit processes - %windir%\sysWOW64\...
// for 64bit processes - %windir%\system32\...
// Nice explanation could be found here:
// https://www.howtogeek.com/326509/whats-the-difference-between-the-system32-and-syswow64-folders-in-windows/
// If a 32bit process needs to copy files to %windir%\system32, it should use %windir%\Sysnative
// to avoid the redirection:
// https://docs.microsoft.com/en-us/windows/desktop/WinProg64/file-system-redirector
// We need to copy the ImportBefore.targets in both locations to ensure that both the 32bit and 64bit versions
// of MSBuild will be able to pick them up.
var systemPath = environmentGetFolderPath(
Environment.SpecialFolder.System,
Environment.SpecialFolderOption.None); // %windir%\System32
if (!string.IsNullOrWhiteSpace(systemPath) &&
localAppData.StartsWith(systemPath)) // We are under %windir%\System32 => we are running as System Account
else if (platformHelper.OperatingSystem == PlatformOS.Windows)
{
var systemX86Path = environmentGetFolderPath(
Environment.SpecialFolder.SystemX86,
Environment.SpecialFolderOption.None); // %windir%\SysWOW64 (or System32 on 32bit windows)
var localAppDataX86 = localAppData.ReplaceCaseInsensitive(systemPath, systemX86Path);

if (directoryExists(localAppDataX86))
{
yield return localAppDataX86;
}

var sysNativePath = Path.Combine(Path.GetDirectoryName(systemPath), "Sysnative"); // %windir%\Sysnative
var localAppDataX64 = localAppData.ReplaceCaseInsensitive(systemPath, sysNativePath);
if (directoryExists(localAppDataX64))
// The code below is Windows-specific, no need to be executed on non-Windows platforms.
// When running under Local System account on a 64bit OS, the local application data folder
// is inside %windir%\system32
// When a process copies a file in this location, the OS will automatically redirect it to:
// for 32bit processes - %windir%\sysWOW64\...
// for 64bit processes - %windir%\system32\...
// Nice explanation could be found here:
// https://www.howtogeek.com/326509/whats-the-difference-between-the-system32-and-syswow64-folders-in-windows/
// If a 32bit process needs to copy files to %windir%\system32, it should use %windir%\Sysnative
// to avoid the redirection:
// https://docs.microsoft.com/en-us/windows/desktop/WinProg64/file-system-redirector
// We need to copy the ImportBefore.targets in both locations to ensure that both the 32bit and 64bit versions
// of MSBuild will be able to pick them up.
var systemPath = platformHelper.GetFolderPath(
Environment.SpecialFolder.System,
Environment.SpecialFolderOption.None); // %windir%\System32
if (!string.IsNullOrWhiteSpace(systemPath) &&
localAppData.StartsWith(systemPath, StringComparison.OrdinalIgnoreCase))
{
yield return localAppDataX64;
// We are under %windir%\System32 => we are running as System Account
var systemX86Path = platformHelper.GetFolderPath(
Environment.SpecialFolder.SystemX86,
Environment.SpecialFolderOption.None); // %windir%\SysWOW64 (or System32 on 32bit windows)
var localAppDataX86 = localAppData.ReplaceCaseInsensitive(systemPath, systemX86Path);

if (platformHelper.DirectoryExists(localAppDataX86))
{
yield return localAppDataX86;
}

var sysNativePath = Path.Combine(Path.GetDirectoryName(systemPath), "Sysnative"); // %windir%\Sysnative
var localAppDataX64 = localAppData.ReplaceCaseInsensitive(systemPath, sysNativePath);
if (platformHelper.DirectoryExists(localAppDataX64))
{
yield return localAppDataX64;
}
}
}
}

public IEnumerable<string> GetGlobalTargetsPaths()
{
var programFiles = this.environmentGetFolderPath(Environment.SpecialFolder.ProgramFiles, Environment.SpecialFolderOption.None);
var programFiles = platformHelper.GetFolderPath(Environment.SpecialFolder.ProgramFiles, Environment.SpecialFolderOption.None);

if (string.IsNullOrWhiteSpace(programFiles))
{
Expand Down
9 changes: 5 additions & 4 deletions src/SonarScanner.MSBuild.Shim/SonarScanner.Wrapper.cs
Expand Up @@ -93,10 +93,11 @@ private static bool InternalExecute(AnalysisConfig config, IEnumerable<string> u
return ExecuteJavaRunner(config, userCmdLineArguments, logger, exeFileName, fullPropertiesFilePath, new ProcessRunner(logger));
}

internal /* for testing */ static string FindScannerExe()
internal /* for testing */ static string FindScannerExe(IPlatformHelper platformHelper = null)
{
platformHelper ??= EnvironmentBasedPlatformHelper.Instance;
var binFolder = Path.GetDirectoryName(typeof(SonarScannerWrapper).Assembly.Location);
var fileExtension = PlatformHelper.IsWindows() ? ".bat" : "";
var fileExtension = platformHelper.OperatingSystem == PlatformOS.Windows ? ".bat" : string.Empty;
return Path.Combine(binFolder, $"sonar-scanner-{SonarScannerVersion}", "bin", $"sonar-scanner{fileExtension}");
}

Expand All @@ -110,14 +111,14 @@ private static bool InternalExecute(AnalysisConfig config, IEnumerable<string> u
var allCmdLineArgs = GetAllCmdLineArgs(propertiesFileName, userCmdLineArguments, config, logger);

var envVarsDictionary = GetAdditionalEnvVariables(logger);
Debug.Assert(envVarsDictionary != null);
Debug.Assert(envVarsDictionary != null, "Unable to retrieve additional environment variables");

logger.LogInfo(Resources.MSG_SonarScannerCalling);

Debug.Assert(!string.IsNullOrWhiteSpace(config.SonarScannerWorkingDirectory), "The working dir should have been set in the analysis config");
Debug.Assert(Directory.Exists(config.SonarScannerWorkingDirectory), "The working dir should exist");

var scannerArgs = new ProcessRunnerArguments(exeFileName, PlatformHelper.IsWindows())
var scannerArgs = new ProcessRunnerArguments(exeFileName, EnvironmentBasedPlatformHelper.Instance.OperatingSystem == PlatformOS.Windows)
{
CmdLineArgs = allCmdLineArgs,
WorkingDirectory = config.SonarScannerWorkingDirectory,
Expand Down
2 changes: 1 addition & 1 deletion src/SonarScanner.MSBuild.Shim/TFSProcessor.Wrapper.cs
Expand Up @@ -75,7 +75,7 @@ private static string FindProcessorExe()
Debug.Assert(!string.IsNullOrWhiteSpace(config.SonarScannerWorkingDirectory), "The working dir should have been set in the analysis config");
Debug.Assert(Directory.Exists(config.SonarScannerWorkingDirectory), "The working dir should exist");

var converterArgs = new ProcessRunnerArguments(exeFileName, !PlatformHelper.IsWindows())
var converterArgs = new ProcessRunnerArguments(exeFileName, EnvironmentBasedPlatformHelper.Instance.OperatingSystem != PlatformOS.Windows)
{
CmdLineArgs = userCmdLineArguments,
WorkingDirectory = config.SonarScannerWorkingDirectory,
Expand Down

0 comments on commit b7b0400

Please sign in to comment.