Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions sources/assets/Stride.Core.Assets/PackageSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
using Stride.Core;
using Stride.Core.Assets.Analysis;
using Stride.Core.Assets.Diagnostics;
Expand Down Expand Up @@ -787,10 +788,12 @@ public static void Load(string filePath, PackageSessionResult sessionResult, Pac
SolutionProject? firstProject = null;

// If we have a solution, load all packages
if (string.Equals(Path.GetExtension(filePath), ".sln", StringComparison.InvariantCultureIgnoreCase))
if (VisualStudio.Solution.SolutionFileRegex.IsMatch(Path.GetExtension(filePath)))
{
// The session should save back its changes to the solution
var solution = session.VSSolution = VisualStudio.Solution.FromFile(filePath);
VisualStudio.Solution solution = session.VSSolution = Path.GetExtension(filePath).Equals(".sln", StringComparison.InvariantCultureIgnoreCase)
? VisualStudio.Solution.FromFile(filePath)
: VisualStudio.Solution.FromSolutionFilter(filePath);

// Keep header
var versionHeader = solution.Properties.FirstOrDefault(x => x.Name == "VisualStudioVersion");
Expand Down
68 changes: 45 additions & 23 deletions sources/assets/Stride.Core.Assets/PackageSessionHelper.Solution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.RegularExpressions;
using NuGet.ProjectModel;
using Stride.Core.Extensions;
using Stride.Core.VisualStudio;
Expand All @@ -18,33 +19,29 @@ internal partial class PackageSessionHelper
{
try
{
// Solution file: extract projects
var solutionDirectory = Path.GetDirectoryName(fullPath) ?? "";
var solution = Solution.FromFile(fullPath);

foreach (var project in solution.Projects)
if (Path.GetExtension(fullPath).Equals(Package.PackageFileExtension, StringComparison.InvariantCultureIgnoreCase))
{
if (project.TypeGuid == KnownProjectTypeGuid.CSharp || project.TypeGuid == KnownProjectTypeGuid.CSharpNewSystem)
var packageVersion = await TryGetPackageVersion(fullPath);
if (packageVersion is not null)
{
var projectPath = project.FullPath;
var projectAssetsJsonPath = Path.Combine(Path.GetDirectoryName(projectPath), "obj", LockFileFormat.AssetsFileName);
#if !STRIDE_LAUNCHER && !STRIDE_VSPACKAGE
if (!File.Exists(projectAssetsJsonPath))
{
var log = new Stride.Core.Diagnostics.LoggerResult();
await VSProjectHelper.RestoreNugetPackages(log, projectPath);
}
#endif
if (File.Exists(projectAssetsJsonPath))
return packageVersion;
}
}
else if (Solution.SolutionFileRegex.IsMatch(Path.GetExtension(fullPath)))
{
// Solution file: extract projects
var solution = Path.GetExtension(fullPath).Equals(".sln", StringComparison.InvariantCultureIgnoreCase)
? Solution.FromFile(fullPath)
: Solution.FromSolutionFilter(fullPath);
foreach (var project in solution.Projects)
{
if (project.TypeGuid == KnownProjectTypeGuid.CSharp || project.TypeGuid == KnownProjectTypeGuid.CSharpNewSystem)
{
var format = new LockFileFormat();
var projectAssets = format.Read(projectAssetsJsonPath);
foreach (var library in projectAssets.Libraries)
var projectPath = project.FullPath;
var projectVersion = await TryGetPackageVersion(projectPath);
if (projectVersion is not null)
{
if ((library.Type == "package" || library.Type == "project") && (library.Name == "Stride.Engine" || library.Name == "Xenko.Engine"))
{
return new PackageVersion(library.Version.ToString());
}
return projectVersion;
}
}
}
Expand Down Expand Up @@ -92,4 +89,29 @@ internal static void RemovePackageSections(Project project)
project.Sections.Remove(solutionPackageIdentifier);
}
}

private static async Task<PackageVersion?> TryGetPackageVersion(string projectPath)
{
var projectAssetsJsonPath = Path.Combine(Path.GetDirectoryName(projectPath), "obj", LockFileFormat.AssetsFileName);
#if !STRIDE_LAUNCHER && !STRIDE_VSPACKAGE
if (!File.Exists(projectAssetsJsonPath))
{
var log = new Stride.Core.Diagnostics.LoggerResult();
await VSProjectHelper.RestoreNugetPackages(log, projectPath);
}
#endif
if (File.Exists(projectAssetsJsonPath))
{
var format = new LockFileFormat();
var projectAssets = format.Read(projectAssetsJsonPath);
foreach (var library in projectAssets.Libraries)
{
if ((library.Type == "package" || library.Type == "project") && (library.Name == "Stride.Engine" || library.Name == "Xenko.Engine"))
{
return new PackageVersion(library.Version.ToString());
}
}
}
return null;
}
}
76 changes: 76 additions & 0 deletions sources/core/Stride.Core.Design/VisualStudio/Solution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#endregion

using System.Diagnostics;
using System.Text.RegularExpressions;

namespace Stride.Core.VisualStudio;

Expand All @@ -33,6 +34,8 @@ namespace Stride.Core.VisualStudio;
[DebuggerDisplay("Projects = [{Projects.Count}]")]
public class Solution
{
public static readonly Regex SolutionFileRegex = new(@"\.slnf?$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

/// <summary>
/// Initializes a new instance of the <see cref="Solution"/> class.
/// </summary>
Expand Down Expand Up @@ -195,4 +198,77 @@ public static Solution FromStream(string solutionFullPath, Stream stream)
solution.FullPath = solutionFullPath;
return solution;
}

/// <summary>
/// Loads a filtered solution from a solution filter file (.slnf).
/// </summary>
/// <param name="solutionFilterPath">The solution filter full path.</param>
/// <returns>A filtered Solution.</returns>
public static Solution FromSolutionFilter(string solutionFilterPath)
{
using var stream = new FileStream(solutionFilterPath, FileMode.Open, FileAccess.Read);
return FromSolutionFilterStream(solutionFilterPath, stream);
}

/// <summary>
/// Loads a filtered solution from a solution filter stream (.slnf).
/// </summary>
/// <param name="solutionFilterPath">The solution filter full path.</param>
/// <param name="stream">The stream containing the solution filter data.</param>
/// <returns>A filtered Solution.</returns>
public static Solution FromSolutionFilterStream(string solutionFilterPath, Stream stream)
{
var solutionFilter = SolutionFilter.FromStream(solutionFilterPath, stream);

// The solution filter contains a reference to the base solution
var baseSolutionPath = solutionFilter.SolutionPath;
var baseSolution = FromFile(baseSolutionPath);

// Create a new solution with only the filtered projects
var filteredSolution = new Solution();
filteredSolution.FullPath = solutionFilterPath;
filteredSolution.Headers.AddRange(baseSolution.Headers);
filteredSolution.Properties.AddRange(baseSolution.Properties);
filteredSolution.GlobalSections.AddRange(baseSolution.GlobalSections);

// Build a dictionary for quick project path lookup
var projectPathMap = baseSolution.Projects
.Where(project => !project.IsSolutionFolder)
.ToDictionary(
project => project.GetRelativePath(baseSolution).Replace('/', Path.DirectorySeparatorChar),
project => project,
StringComparer.OrdinalIgnoreCase);

// Add projects by path
foreach (var projectPath in solutionFilter.ProjectPaths)
{
if (projectPathMap.TryGetValue(projectPath, out var project))
{
// Only add if not already added by GUID
if (!filteredSolution.Projects.Contains(project.Guid))
{
filteredSolution.Projects.Add(project);
}
}
}

// Add solution folders that contain the included projects
var includedSolutionFolders = new HashSet<Guid>();

// For each project, make sure its parent folders are included
foreach (var project in filteredSolution.Projects.ToList())
{
var parent = project.GetParentProject(baseSolution);
while (parent is not null)
{
if (includedSolutionFolders.Add(parent.Guid))
{
filteredSolution.Projects.Add(parent);
}
parent = parent.GetParentProject(baseSolution);
}
}

return filteredSolution;
}
}
83 changes: 83 additions & 0 deletions sources/core/Stride.Core.Design/VisualStudio/SolutionFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#region License

// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// This file is distributed under MIT License. See LICENSE.md for details.

#endregion

namespace Stride.Core.VisualStudio;

/// <summary>
/// Represents a Visual Studio solution filter file (.slnf).
/// </summary>
public class SolutionFilter
{
/// <summary>
/// Initializes a new instance of the <see cref="SolutionFilter"/> class.
/// </summary>
public SolutionFilter()
{
SolutionPath = string.Empty;
ProjectPaths = [];
}

/// <summary>
/// Gets or sets the path to the solution file referenced by this solution filter.
/// </summary>
public string SolutionPath { get; set; }

/// <summary>
/// Gets the list of project paths included in the solution filter.
/// </summary>
public List<string> ProjectPaths { get; } = [];

/// <summary>
/// Loads a solution filter from a file path.
/// </summary>
/// <param name="solutionFilterPath">The full path to the solution filter file.</param>
/// <returns>A populated SolutionFilter instance.</returns>
public static SolutionFilter FromFile(string solutionFilterPath)
{
using var stream = new FileStream(solutionFilterPath, FileMode.Open, FileAccess.Read);
return FromStream(solutionFilterPath, stream);
}

/// <summary>
/// Loads a solution filter from a stream.
/// </summary>
/// <param name="solutionFilterPath">The full path to the solution filter file.</param>
/// <param name="stream">The stream containing the solution filter data.</param>
/// <returns>A populated SolutionFilter instance.</returns>
public static SolutionFilter FromStream(string solutionFilterPath, Stream stream)
{
using var filterReader = new SolutionFilterReader(solutionFilterPath, stream);
return filterReader.ReadSolutionFilterFile();
}
}

/// <summary>
/// JSON model for deserializing solution filter files.
/// </summary>
internal class SolutionFilterData
{
/// <summary>
/// Gets or sets the solution information.
/// </summary>
public SolutionInfo? Solution { get; set; }

/// <summary>
/// Represents solution information in a solution filter file.
/// </summary>
public class SolutionInfo
{
/// <summary>
/// Gets or sets the relative path to the solution file.
/// </summary>
public string? Path { get; set; }

/// <summary>
/// Gets or sets the list of project paths in the solution filter.
/// </summary>
public List<string>? Projects { get; set; }
}
}
106 changes: 106 additions & 0 deletions sources/core/Stride.Core.Design/VisualStudio/SolutionFilterReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#region License

// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// This file is distributed under MIT License. See LICENSE.md for details.

#endregion

using System.Text.Json;

namespace Stride.Core.VisualStudio;

internal sealed class SolutionFilterReader : IDisposable
{
private readonly string solutionFilterPath;
private readonly string solutionFilterDirectory;
private StreamReader? reader;
private bool disposed;

/// <summary>
/// Initializes a new instance of the <see cref="SolutionFilterReader"/> class.
/// </summary>
/// <param name="solutionFilterPath">The solution filter path.</param>
public SolutionFilterReader(string solutionFilterPath)
: this(solutionFilterPath, new FileStream(solutionFilterPath, FileMode.Open, FileAccess.Read))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SolutionFilterReader"/> class.
/// </summary>
/// <param name="solutionFilterPath">The solution filter path.</param>
/// <param name="stream">The stream containing the solution filter data.</param>
public SolutionFilterReader(string solutionFilterPath, Stream stream)
{
this.solutionFilterPath = solutionFilterPath;
solutionFilterDirectory = Path.GetDirectoryName(solutionFilterPath) ?? string.Empty;
reader = new StreamReader(stream);
}

/// <summary>
/// Reads the solution filter file and returns a SolutionFilter instance.
/// </summary>
/// <returns>A populated SolutionFilter instance.</returns>
public SolutionFilter ReadSolutionFilterFile()
{
#if NET7_0_OR_GREATER
ObjectDisposedException.ThrowIf(disposed, this);
#else
if (disposed) throw new ObjectDisposedException(nameof(SolutionFilterReader));
#endif

var solutionFilter = new SolutionFilter();

try
{
// Read and deserialize the JSON content
var jsonContent = reader!.ReadToEnd();
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

var filterData = JsonSerializer.Deserialize<SolutionFilterData>(jsonContent, options);

if (filterData?.Solution?.Path is null)
{
throw new SolutionFileException($"Invalid solution filter file: {solutionFilterPath}. Missing or invalid 'solution.path' property.");
}

// Resolve the solution path relative to the solution filter
var relativeSolutionPath = filterData.Solution.Path.Replace('\\', Path.DirectorySeparatorChar);
solutionFilter.SolutionPath = Path.GetFullPath(Path.Combine(solutionFilterDirectory, relativeSolutionPath));

// Process project paths
if (filterData.Solution.Projects is not null)
{
foreach (var projectPath in filterData.Solution.Projects)
{
if (!string.IsNullOrEmpty(projectPath))
{
solutionFilter.ProjectPaths.Add(projectPath.Replace('\\', Path.DirectorySeparatorChar));
}
}
}
}
catch (JsonException ex)
{
throw new SolutionFileException($"Error parsing solution filter file: {solutionFilterPath}", ex);
}

return solutionFilter;
}

/// <summary>
/// Disposes resources used by the reader.
/// </summary>
public void Dispose()
{
disposed = true;
if (reader is not null)
{
reader.Dispose();
reader = null;
}
}
}
Loading