Skip to content
This repository has been archived by the owner on Jul 12, 2022. It is now read-only.

Commit

Permalink
Fallback to mono when running on Linux or MacOS for Nuget.exe (#829)
Browse files Browse the repository at this point in the history
* When running not on windows try to fallback to mono for Nuget.exe

That makes nuget updates practically crossplatform.

* Review comments

Better error messages
Better method naming

* Add a missing dot

* Pass correct arguments to the external process

* Unit tests for Mono Executor

* Adjust integration tests to run cross platform

On mac restore command was required to run this integration test.
Otherwise `Unable to locate the packages folder. Verify all the packages are restored before running 'nuget.exe update'.` error was thrown.

* Update travis config to use mono

* Fix code style and typos

* Try to run azure pipeline on linux as well

* Run code coverage only on windows

* Add note about experimental support for mono

* Log error when NuGet.exe can't be executed

* Fix casing of macOS in docs

* Remove Task.FromResult in favour of NSubstitute native overload
  • Loading branch information
zubivan authored and MarcBruins committed Jul 1, 2019
1 parent fe974fa commit f8baf2a
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 35 deletions.
16 changes: 13 additions & 3 deletions .azure-build.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
queue:
name: Hosted VS2017
strategy:
matrix:
Linux:
imageName: 'ubuntu-16.04'
Windows:
imageName: 'vs2017-win2016'

pool:
vmImage: $(imageName)

variables:
buildConfiguration: Release
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
Expand Down Expand Up @@ -33,16 +41,18 @@ steps:
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:$(Build.SourcesDirectory)/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:HtmlInline_AzurePipelines;Cobertura
displayName: Create Code coverage report
condition: eq(variables['Agent.OS'], 'Windows_NT')

- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml'
reportDirectory: '$(Build.SourcesDirectory)/CodeCoverage'
condition: eq(variables['Agent.OS'], 'Windows_NT')

- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop for master'
inputs:
PathtoPublish: '$(Build.SourcesDirectory)'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
condition: and(and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')), eq(variables['Agent.OS'], 'Windows_NT'))
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: csharp
mono: none
mono: 5.18.1
dist: xenial
dotnet: 2.2.202
script:
- dotnet build -c Release NuKeeper.sln /m:1
- dotnet test -c Release NuKeeper.sln --filter "TestCategory!=WindowsOnly"
- dotnet test -c Release NuKeeper.sln
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@
namespace NuKeeper.Integration.Tests.NuGet.Process
{
[TestFixture]
[Category("WindowsOnly")] // Windows only due to NuGetUpdatePackageCommand
public class NuGetUpdatePackageCommandTests
{
private readonly string _testDotNetClassicProject =
@"<Project ToolsVersion=""15.0"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
@"<Project ToolsVersion=""15.0"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
<Import Project=""$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"" Condition=""Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"" />
<PropertyGroup>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
Expand All @@ -29,10 +28,10 @@ public class NuGetUpdatePackageCommandTests
</Project>";

private readonly string _testPackagesConfig =
@"<packages><package id=""Microsoft.AspNet.WebApi.Client"" version=""{packageVersion}"" targetFramework=""net47"" /></packages>";
@"<packages><package id=""Microsoft.AspNet.WebApi.Client"" version=""{packageVersion}"" targetFramework=""net47"" /></packages>";

private readonly string _nugetConfig =
@"<configuration><config><add key=""repositoryPath"" value="".\packages"" /></config></configuration>";
@"<configuration><config><add key=""repositoryPath"" value="".\packages"" /></config></configuration>";

private IFolder _uniqueTemporaryFolder = null;

Expand Down Expand Up @@ -64,28 +63,44 @@ public async Task ShouldUpdateDotnetClassicProject()
var packagesFolder = Path.Combine(workDirectory, "packages");
Directory.CreateDirectory(packagesFolder);

var projectContents = _testDotNetClassicProject.Replace("{packageVersion}", oldPackageVersion, StringComparison.OrdinalIgnoreCase);
var projectContents = _testDotNetClassicProject.Replace("{packageVersion}", oldPackageVersion,
StringComparison.OrdinalIgnoreCase);
var projectPath = Path.Combine(workDirectory, testProject);
await File.WriteAllTextAsync(projectPath, projectContents);

var packagesConfigContents = _testPackagesConfig.Replace("{packageVersion}", oldPackageVersion, StringComparison.OrdinalIgnoreCase);
var packagesConfigContents = _testPackagesConfig.Replace("{packageVersion}", oldPackageVersion,
StringComparison.OrdinalIgnoreCase);
var packagesConfigPath = Path.Combine(workDirectory, "packages.config");
await File.WriteAllTextAsync(packagesConfigPath, packagesConfigContents);

await File.WriteAllTextAsync(Path.Combine(workDirectory, "nuget.config"), _nugetConfig);

var logger = Substitute.For<INuKeeperLogger>();
var command = new NuGetUpdatePackageCommand(logger, new NuGetPath(logger), new ExternalProcess(logger));
var externalProcess = new ExternalProcess(logger);

var monoExecutor = new MonoExecutor(logger, externalProcess);

var nuGetPath = new NuGetPath(logger);
var nuGetVersion = new NuGetVersion(newPackageVersion);
var packageSource = new PackageSource(NuGetConstants.V3FeedUrl);

var restoreCommand = new NuGetFileRestoreCommand(logger, nuGetPath, monoExecutor, externalProcess);
var updateCommand = new NuGetUpdatePackageCommand(logger, nuGetPath, monoExecutor, externalProcess);

var packageToUpdate = new PackageInProject("Microsoft.AspNet.WebApi.Client", oldPackageVersion,
new PackagePath(workDirectory, testProject, PackageReferenceType.PackagesConfig));
new PackagePath(workDirectory, testProject, PackageReferenceType.PackagesConfig));

await restoreCommand.Invoke(packageToUpdate, nuGetVersion, packageSource, NuGetSources.GlobalFeed);

await command.Invoke(packageToUpdate, new NuGetVersion(newPackageVersion),
new PackageSource(NuGetConstants.V3FeedUrl), NuGetSources.GlobalFeed);
await updateCommand.Invoke(packageToUpdate, nuGetVersion, packageSource, NuGetSources.GlobalFeed);

var contents = await File.ReadAllTextAsync(packagesConfigPath);
Assert.That(contents, Does.Contain(expectedPackageString.Replace("{packageVersion}", newPackageVersion, StringComparison.OrdinalIgnoreCase)));
Assert.That(contents, Does.Not.Contain(expectedPackageString.Replace("{packageVersion}", oldPackageVersion, StringComparison.OrdinalIgnoreCase)));
Assert.That(contents,
Does.Contain(expectedPackageString.Replace("{packageVersion}", newPackageVersion,
StringComparison.OrdinalIgnoreCase)));
Assert.That(contents,
Does.Not.Contain(expectedPackageString.Replace("{packageVersion}", oldPackageVersion,
StringComparison.OrdinalIgnoreCase)));
}

private static IFolder UniqueTemporaryFolder()
Expand Down
83 changes: 83 additions & 0 deletions NuKeeper.Update.Tests/Process/MonoExecutorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Threading.Tasks;
using NSubstitute;
using NuKeeper.Abstractions.Logging;
using NuKeeper.Update.Process;
using NuKeeper.Update.ProcessRunner;
using NUnit.Framework;

namespace NuKeeper.Update.Tests.Process
{
[TestFixture]
public class MonoExecutorTests
{
[TestCase(0, true)]
[TestCase(1, false)]
public async Task WhenCallingCanRun_ShouldCheckExternalProcessResult(int exitCode, bool expectedCanExecute)
{
var nuKeeperLogger = Substitute.For<INuKeeperLogger>();
var externalProcess = Substitute.For<IExternalProcess>();

externalProcess.Run("","mono","--version",false).
Returns(new ProcessOutput("","",exitCode));

var monoExecutor = new MonoExecutor(nuKeeperLogger, externalProcess);

var canRun = await monoExecutor.CanRun();

Assert.AreEqual(expectedCanExecute, canRun);
}

[Test]
public async Task WhenCallingCanRun_ShouldOnlyCallExternalProcessOnce()
{
var nuKeeperLogger = Substitute.For<INuKeeperLogger>();
var externalProcess = Substitute.For<IExternalProcess>();

externalProcess.Run("","mono","--version",false).
Returns(new ProcessOutput("","",0));

var monoExecutor = new MonoExecutor(nuKeeperLogger, externalProcess);

await monoExecutor.CanRun();
await monoExecutor.CanRun();
await monoExecutor.CanRun();

await externalProcess.Received(1).Run(
"",
"mono",
"--version",
false);
}

[Test]
public void WhenCallingRun_ShouldThrowIfMonoWasNotFound()
{
var nuKeeperLogger = Substitute.For<INuKeeperLogger>();
var externalProcess = Substitute.For<IExternalProcess>();

externalProcess.Run("","mono","--version",false).
Returns(new ProcessOutput("","",1));

var monoExecutor = new MonoExecutor(nuKeeperLogger, externalProcess);

Assert.ThrowsAsync<InvalidOperationException>(async () =>
await monoExecutor.Run("wd", "command", "args", true));
}

[Test]
public async Task WhenCallingRun_ShouldPassArgumentToUnderlyingExternalProcess()
{
var nuKeeperLogger = Substitute.For<INuKeeperLogger>();
var externalProcess = Substitute.For<IExternalProcess>();

externalProcess.Run("","mono","--version",false).
Returns(new ProcessOutput("","",0));

var monoExecutor = new MonoExecutor(nuKeeperLogger, externalProcess);
await monoExecutor.Run("wd", "command", "args", true);

await externalProcess.Received(1).Run("wd", "mono", "command args", true);
}
}
}
10 changes: 10 additions & 0 deletions NuKeeper.Update/Process/IMonoExecutor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using NuKeeper.Update.ProcessRunner;

namespace NuKeeper.Update.Process
{
public interface IMonoExecutor: IExternalProcess
{
Task<bool> CanRun();
}
}
48 changes: 48 additions & 0 deletions NuKeeper.Update/Process/MonoExecutor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Threading.Tasks;
using NuGet.Common;
using NuKeeper.Abstractions.Logging;
using NuKeeper.Update.ProcessRunner;

namespace NuKeeper.Update.Process
{
public class MonoExecutor : IMonoExecutor
{
private readonly INuKeeperLogger _logger;
private readonly IExternalProcess _externalProcess;

private readonly AsyncLazy<bool> _checkMono;

public MonoExecutor(INuKeeperLogger logger, IExternalProcess externalProcess)
{
_logger = logger;
_externalProcess = externalProcess;
_checkMono = new AsyncLazy<bool>(CheckMonoExists);
}

public async Task<bool> CanRun()
{
return await _checkMono;
}

public async Task<ProcessOutput> Run(string workingDirectory, string command, string arguments, bool ensureSuccess)
{
_logger.Normal($"Using Mono to run '{command}'");

if (!await CanRun())
{
_logger.Error($"Cannot run '{command}' on Mono since Mono installation was not found");
throw new InvalidOperationException("Mono installation was not found");
}

return await _externalProcess.Run(workingDirectory, "mono", $"{command} {arguments}", ensureSuccess);
}

private async Task<bool> CheckMonoExists()
{
var result = await _externalProcess.Run("", "mono", "--version", false);

return result.Success;
}
}
}
39 changes: 29 additions & 10 deletions NuKeeper.Update/Process/NuGetFileRestoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,69 @@ public class NuGetFileRestoreCommand : IFileRestoreCommand
{
private readonly INuKeeperLogger _logger;
private readonly INuGetPath _nuGetPath;
private readonly IMonoExecutor _monoExecutor;
private readonly IExternalProcess _externalProcess;

public NuGetFileRestoreCommand(
INuKeeperLogger logger,
INuGetPath nuGetPath,
IMonoExecutor monoExecutor,
IExternalProcess externalProcess)
{
_logger = logger;
_nuGetPath = nuGetPath;
_monoExecutor = monoExecutor;
_externalProcess = externalProcess;
}

public async Task Invoke(FileInfo file, NuGetSources sources)
{
_logger.Normal($"Nuget restore on {file.DirectoryName} {file.Name}");

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_logger.Normal("Cannot run NuGet.exe file restore as OS Platform is not Windows");
return;
}

var nuget = _nuGetPath.Executable;

if (string.IsNullOrWhiteSpace(nuget))
{
_logger.Normal("Cannot find NuGet exe for solution restore");
_logger.Normal("Cannot find NuGet.exe for solution restore");
return;
}

var sourcesCommandLine = sources.CommandLine("-Source");

var restoreCommand = $"restore {file.Name} {sourcesCommandLine} -NonInteractive";

var processOutput = await _externalProcess.Run(file.DirectoryName, nuget,
restoreCommand, ensureSuccess: false);
ProcessOutput processOutput;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (await _monoExecutor.CanRun())
{
processOutput = await _monoExecutor.Run(file.DirectoryName,
nuget,
restoreCommand,
ensureSuccess: false);
}
else
{
_logger.Error("Cannot run NuGet.exe. It requires either Windows OS Platform or Mono installation");
return;
}
}
else
{
processOutput = await _externalProcess.Run(file.DirectoryName,
nuget,
restoreCommand,
ensureSuccess: false);
}

if (processOutput.Success)
{
_logger.Detailed($"Nuget restore on {file.Name} complete");
}
else
{
_logger.Detailed($"Nuget restore failed on {file.DirectoryName} {file.Name}:\n{processOutput.Output}\n{processOutput.ErrorOutput}");
_logger.Detailed(
$"Nuget restore failed on {file.DirectoryName} {file.Name}:\n{processOutput.Output}\n{processOutput.ErrorOutput}");
}
}

Expand Down
Loading

0 comments on commit f8baf2a

Please sign in to comment.