Skip to content

Commit

Permalink
Merge pull request #747 from DustinCampbell/package-restore
Browse files Browse the repository at this point in the history
Implement package auto-restore for .csproj projects
  • Loading branch information
DustinCampbell authored Feb 4, 2017
2 parents c19a58c + 9cc7aac commit 0c6d19e
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 73 deletions.
31 changes: 22 additions & 9 deletions src/OmniSharp.Abstractions/Services/DotNetCliService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Composition;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
Expand All @@ -25,39 +27,50 @@ public DotNetCliService(ILoggerFactory loggerFactory, IEventEmitter eventEmitter
this._semaphore = new SemaphoreSlim(Environment.ProcessorCount / 2);
}

public void Restore(string projectPath, Action onFailure)
private static void RemoveMSBuildEnvironmentVariables(IDictionary<string, string> environment)
{
// Remove various MSBuild environment variables set by OmniSharp to ensure that
// the .NET CLI is not launched with the wrong values.
environment.Remove("MSBUILD_EXE_PATH");
environment.Remove("MSBuildExtensionsPath");
environment.Remove("MSBuildSDKsPath");
}

public void Restore(string projectFilePath, Action onFailure = null)
{
var projectDirectory = Path.GetDirectoryName(projectFilePath);

Task.Factory.StartNew(() =>
{
_logger.LogInformation($"Begin restoring project {projectPath}");
_logger.LogInformation($"Begin restoring project {projectFilePath}");
var restoreLock = _locks.GetOrAdd(projectPath, new object());
var restoreLock = _locks.GetOrAdd(projectFilePath, new object());
lock (restoreLock)
{
var exitStatus = new ProcessExitStatus(-1);
_eventEmitter.RestoreStarted(projectPath);
_eventEmitter.RestoreStarted(projectFilePath);
_semaphore.Wait();
try
{
// A successful restore will update the project lock file which is monitored
// by the dotnet project system which eventually update the Roslyn model
exitStatus = ProcessHelper.Run("dotnet", "restore", projectPath);
exitStatus = ProcessHelper.Run("dotnet", "restore", projectDirectory, updateEnvironment: RemoveMSBuildEnvironmentVariables);
}
finally
{
_semaphore.Release();
object removedLock;
_locks.TryRemove(projectPath, out removedLock);
_locks.TryRemove(projectFilePath, out removedLock);
_eventEmitter.RestoreFinished(projectPath, exitStatus.Succeeded);
_eventEmitter.RestoreFinished(projectFilePath, exitStatus.Succeeded);
if (exitStatus.Failed)
if (exitStatus.Failed && onFailure != null)
{
onFailure();
}
_logger.LogInformation($"Finish restoring project {projectPath}. Exit code {exitStatus}");
_logger.LogInformation($"Finish restoring project {projectFilePath}. Exit code {exitStatus}");
}
}
});
Expand Down
15 changes: 15 additions & 0 deletions src/OmniSharp.Abstractions/Utilities/ProcessExitStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,20 @@ public ProcessExitStatus(int code, bool started = true, bool timedOut = false)
this.Started = started;
this.TimedOut = timedOut;
}

public override string ToString()
{
var suffix = string.Empty;
if (!Started)
{
suffix = " (not started)";
}
else if (TimedOut)
{
suffix = " (timed out)";
}

return Code.ToString() + suffix;
}
}
}
10 changes: 7 additions & 3 deletions src/OmniSharp.Abstractions/Utilities/ProcessHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

Expand Down Expand Up @@ -41,17 +42,20 @@ public static ProcessExitStatus Run(
string arguments,
string workingDirectory = null,
Action<string> outputDataReceived = null,
Action<string> errorDataReceived = null)
Action<string> errorDataReceived = null,
Action<IDictionary<string, string>> updateEnvironment = null)
{
var startInfo = new ProcessStartInfo(fileName, arguments)
{
RedirectStandardOutput = outputDataReceived != null,
RedirectStandardError = errorDataReceived != null,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
UseShellExecute = false,
WorkingDirectory = workingDirectory ?? string.Empty,
};

updateEnvironment(startInfo.Environment);

var process = new Process();
process.StartInfo = startInfo;

Expand Down
2 changes: 1 addition & 1 deletion src/OmniSharp.DotNet/DotNetProjectSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ private void UpdateUnresolvedDependencies(ProjectState state, bool allowRestore)
{
if (allowRestore && _enableRestorePackages)
{
_dotNetCliService.Restore(state.ProjectContext.ProjectDirectory, onFailure: () =>
_dotNetCliService.Restore(state.ProjectContext.ProjectFile.ProjectFilePath, onFailure: () =>
{
_eventEmitter.Emit(EventTypes.UnresolvedDependencies, new UnresolvedDependenciesMessage()
{
Expand Down
32 changes: 0 additions & 32 deletions src/OmniSharp.Host/Loader/AssemblyLoader.cs~RF6bf1656.TMP

This file was deleted.

18 changes: 11 additions & 7 deletions src/OmniSharp.MSBuild/MSBuildEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ namespace OmniSharp.MSBuild
{
public static class MSBuildEnvironment
{
public const string MSBuildExePathName = "MSBUILD_EXE_PATH";
public const string MSBuildExtensionsPathName = "MSBuildExtensionsPath";
public const string MSBuildSDKsPathName = "MSBuildSDKsPath";

private static bool s_isInitialized;
private static string s_msbuildFolder;
private static string s_msbuildExtensionsPath;
Expand Down Expand Up @@ -77,24 +81,24 @@ public static void Initialize(ILogger logger)
return;
}

Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", msbuildExePath);
logger.LogInformation($"MSBUILD_EXE_PATH environment variable set to {msbuildExePath}");
Environment.SetEnvironmentVariable(MSBuildExePathName, msbuildExePath);
logger.LogInformation($"{MSBuildExePathName} environment variable set to {msbuildExePath}");

// Set the MSBuildExtensionsPath environment variable to the msbuild folder.
Environment.SetEnvironmentVariable("MSBuildExtensionsPath", msbuildFolder);
logger.LogInformation($"MSBuildExtensionsPath environment variable set to {msbuildFolder}");
Environment.SetEnvironmentVariable(MSBuildExtensionsPathName, msbuildFolder);
logger.LogInformation($"{MSBuildExtensionsPathName} environment variable set to {msbuildFolder}");

// Set the MSBuildSDKsPath environment variable to the location of the SDKs.
var msbuildSdksFolder = Path.Combine(msbuildFolder, "Sdks");
if (Directory.Exists(msbuildSdksFolder))
{
s_msbuildSDKsPath = msbuildSdksFolder;
Environment.SetEnvironmentVariable("MSBuildSDKsPath", msbuildSdksFolder);
logger.LogInformation($"MSBuildSDKsPath environment variable set to {msbuildSdksFolder}");
Environment.SetEnvironmentVariable(MSBuildSDKsPathName, msbuildSdksFolder);
logger.LogInformation($"{MSBuildSDKsPathName} environment variable set to {msbuildSdksFolder}");
}
else
{
logger.LogError("Could not locate MSBuild Sdks path to set MSBuildSDKsPath");
logger.LogError($"Could not locate MSBuild Sdks path to set {MSBuildSDKsPathName}");
}

s_msbuildFolder = msbuildFolder;
Expand Down
123 changes: 115 additions & 8 deletions src/OmniSharp.MSBuild/MSBuildProjectSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NuGet.ProjectModel;
using OmniSharp.Models;
using OmniSharp.Models.v1;
using OmniSharp.MSBuild.ProjectFile;
Expand All @@ -25,6 +26,7 @@ public class MSBuildProjectSystem : IProjectSystem
{
private readonly IOmniSharpEnvironment _environment;
private readonly OmniSharpWorkspace _workspace;
private readonly DotNetCliService _dotNetCliService;
private readonly IMetadataFileReferenceCache _metadataFileReferenceCache;
private readonly IEventEmitter _eventEmitter;
private readonly IFileSystemWatcher _fileSystemWatcher;
Expand Down Expand Up @@ -52,13 +54,15 @@ public class MSBuildProjectSystem : IProjectSystem
public MSBuildProjectSystem(
IOmniSharpEnvironment environment,
OmniSharpWorkspace workspace,
DotNetCliService dotNetCliService,
IMetadataFileReferenceCache metadataFileReferenceCache,
IEventEmitter eventEmitter,
IFileSystemWatcher fileSystemWatcher,
ILoggerFactory loggerFactory)
{
_environment = environment;
_workspace = workspace;
_dotNetCliService = dotNetCliService;
_metadataFileReferenceCache = metadataFileReferenceCache;
_eventEmitter = eventEmitter;
_fileSystemWatcher = fileSystemWatcher;
Expand Down Expand Up @@ -88,22 +92,26 @@ public void Initalize(IConfiguration configuration)

foreach (var projectFileInfo in _projects)
{
UpdateProject(projectFileInfo);
var projectFilePath = projectFileInfo.ProjectFilePath;
var projectAssetsFile = projectFileInfo.ProjectAssetsFile;

// TODO: This needs some improvement. Currently, it tracks both deletions and changes
// as "updates". We should properly remove projects that are deleted.
_fileSystemWatcher.Watch(projectFileInfo.ProjectFilePath, file =>
_fileSystemWatcher.Watch(projectFilePath, file =>
{
OnProjectChanged(projectFileInfo.ProjectFilePath);
OnProjectChanged(projectFilePath, allowAutoRestore: true);
});

if (!string.IsNullOrEmpty(projectFileInfo.ProjectAssetsFile))
if (!string.IsNullOrEmpty(projectAssetsFile))
{
_fileSystemWatcher.Watch(projectFileInfo.ProjectAssetsFile, file =>
_fileSystemWatcher.Watch(projectAssetsFile, file =>
{
OnProjectChanged(projectFileInfo.ProjectFilePath);
OnProjectChanged(projectFilePath, allowAutoRestore: false);
});
}

UpdateProject(projectFileInfo);
CheckForUnresolvedDependences(projectFileInfo, allowAutoRestore: true);
}
}

Expand Down Expand Up @@ -319,11 +327,10 @@ private ProjectFileInfo CreateProjectFileInfo(string projectFilePath, bool isUni
return projectFileInfo;
}

private void OnProjectChanged(string projectFilePath)
private void OnProjectChanged(string projectFilePath, bool allowAutoRestore)
{
var newProjectFileInfo = CreateProjectFileInfo(projectFilePath);

// TODO: Should we remove the entry if the project is malformed?
if (newProjectFileInfo != null)
{
lock (_gate)
Expand All @@ -333,7 +340,9 @@ private void OnProjectChanged(string projectFilePath)
{
_projects[projectFilePath] = newProjectFileInfo;
newProjectFileInfo.SetProjectId(oldProjectFileInfo.ProjectId);

UpdateProject(newProjectFileInfo);
CheckForUnresolvedDependences(newProjectFileInfo, oldProjectFileInfo, allowAutoRestore);
}
}
}
Expand Down Expand Up @@ -483,6 +492,104 @@ private void UpdateReferences(Project project, IList<string> references)
}
}

private List<PackageDependency> CreatePackageDependencies(IEnumerable<PackageReference> packageReferences)
{
var list = new List<PackageDependency>();

foreach (var packageReference in packageReferences)
{
var dependency = new PackageDependency
{
Name = packageReference.Identity.Id,
Version = packageReference.Identity.Version?.ToNormalizedString()
};

list.Add(dependency);
}

return list;
}

private void CheckForUnresolvedDependences(ProjectFileInfo projectFileInfo, ProjectFileInfo previousProjectFileInfo = null, bool allowAutoRestore = false)
{
List<PackageDependency> unresolvedDependencies;

if (!File.Exists(projectFileInfo.ProjectAssetsFile))
{
// Simplest case: If there's no lock file and the project file has package references,
// there are certainly unresolved dependencies.
unresolvedDependencies = CreatePackageDependencies(projectFileInfo.PackageReferences);
}
else
{
// Note: This is a bit of misnmomer. It's entirely possible that a package reference has been removed
// and a restore needs to happen in order to update project.assets.json file. Otherwise, the MSBuild targets
// will still resolve the removed reference as a reference in the user's project. In that case, the package
// reference isn't so much "unresolved" as "incorrectly resolved".
IEnumerable<PackageReference> unresolvedPackageReferences;

// Did the project file change? Diff the package references and see if there are unresolved dependencies.
if (previousProjectFileInfo != null)
{
var packageReferencesToRemove = new HashSet<PackageReference>(previousProjectFileInfo.PackageReferences);
var packageReferencesToAdd = new HashSet<PackageReference>();

foreach (var packageReference in projectFileInfo.PackageReferences)
{
if (packageReferencesToRemove.Contains(packageReference))
{
packageReferencesToRemove.Remove(packageReference);
}
else
{
packageReferencesToAdd.Add(packageReference);
}
}

unresolvedPackageReferences = packageReferencesToAdd.Concat(packageReferencesToRemove);
}
else
{
// Finally, if the project.assets.json file exists but there's no old project to compare against,
// we'll just check to ensure that all of the project's package references can be found in the
// current project.assets.json file.

var lockFileFormat = new LockFileFormat();
var lockFile = lockFileFormat.Read(projectFileInfo.ProjectAssetsFile);

unresolvedPackageReferences = projectFileInfo.PackageReferences
.Where(pr => lockFile.GetLibrary(pr.Identity.Id, pr.Identity.Version) == null);
}

unresolvedDependencies = CreatePackageDependencies(unresolvedPackageReferences);
}

if (unresolvedDependencies.Count > 0)
{
if (allowAutoRestore && _options.EnablePackageAutoRestore)
{
_dotNetCliService.Restore(projectFileInfo.ProjectFilePath, onFailure: () =>
{
FireUnresolvedDependenciesEvent(projectFileInfo, unresolvedDependencies);
});
}
else
{
FireUnresolvedDependenciesEvent(projectFileInfo, unresolvedDependencies);
}
}
}

private void FireUnresolvedDependenciesEvent(ProjectFileInfo projectFileInfo, List<PackageDependency> unresolvedDependencies)
{
_eventEmitter.Emit(EventTypes.UnresolvedDependencies,
new UnresolvedDependenciesMessage()
{
FileName = projectFileInfo.ProjectFilePath,
UnresolvedDependencies = unresolvedDependencies
});
}

private ProjectFileInfo GetProjectFileInfo(string path)
{
ProjectFileInfo projectFileInfo;
Expand Down
Loading

0 comments on commit 0c6d19e

Please sign in to comment.