diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 393d698..07d0692 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -1,12 +1,11 @@
{
- "version": 1,
- "isRoot": true,
- "tools": {
- "jetbrains.resharper.globaltools": {
- "version": "2023.3.3",
- "commands": [
- "jb"
- ]
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "jetbrains.resharper.globaltools": {
+ "version": "2025.1.5",
+ "commands": ["jb"],
+ "rollForward": false
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/.github/workflows/activation.yml b/.github/workflows/activation.yml
index 8a7bb0b..b6f7e1e 100644
--- a/.github/workflows/activation.yml
+++ b/.github/workflows/activation.yml
@@ -12,7 +12,7 @@ jobs:
uses: game-ci/unity-request-activation-file@v2
# Upload artifact (Unity_v20XX.X.XXXX.alf)
- name: Expose as artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: ${{ steps.getManualLicenseFile.outputs.filePath }}
path: ${{ steps.getManualLicenseFile.outputs.filePath }}
diff --git a/src/TestProjects/RootTestProject/.gitignore b/src/TestProjects/RootTestProject/.gitignore
index 90d3fd9..75e6db1 100644
--- a/src/TestProjects/RootTestProject/.gitignore
+++ b/src/TestProjects/RootTestProject/.gitignore
@@ -36,7 +36,9 @@ ExportedObj/
*.csproj.meta
*.unityproj
*.sln
+*.slnx
*.sln.meta
+*.slnx.meta
*.suo
*.tmp
*.user
diff --git a/src/TestProjects/RootTestProject/Packages/manifest.json b/src/TestProjects/RootTestProject/Packages/manifest.json
index 795fc2c..0ea4893 100644
--- a/src/TestProjects/RootTestProject/Packages/manifest.json
+++ b/src/TestProjects/RootTestProject/Packages/manifest.json
@@ -1,6 +1,8 @@
{
"dependencies": {
"com.github-joc0de.visual-studio-solution-generator": "file:../../../UnityVisualStudioSolutionGenerator/Assets",
- "com.unity.modules.accessibility": "1.0.0"
+ "com.unity.modules.accessibility": "1.0.0",
+ "com.unity.modules.adaptiveperformance": "1.0.0",
+ "com.unity.modules.vectorgraphics": "1.0.0"
}
}
diff --git a/src/TestProjects/RootTestProject/Packages/packages-lock.json b/src/TestProjects/RootTestProject/Packages/packages-lock.json
index ee7ce11..093902e 100644
--- a/src/TestProjects/RootTestProject/Packages/packages-lock.json
+++ b/src/TestProjects/RootTestProject/Packages/packages-lock.json
@@ -5,43 +5,41 @@
"depth": 0,
"source": "local",
"dependencies": {
- "com.unity.ide.visualstudio": "2.0.22",
- "com.unity.settings-manager": "2.0.1"
+ "com.unity.ide.visualstudio": "2.0.26",
+ "com.unity.settings-manager": "2.1.1"
}
},
"com.unity.ext.nunit": {
"version": "2.0.5",
"depth": 3,
- "source": "registry",
- "dependencies": {},
- "url": "https://packages.unity.com"
+ "source": "builtin",
+ "dependencies": {}
},
"com.unity.ide.visualstudio": {
- "version": "2.0.22",
+ "version": "2.0.26",
"depth": 1,
"source": "registry",
"dependencies": {
- "com.unity.test-framework": "1.1.9"
+ "com.unity.test-framework": "1.1.33"
},
"url": "https://packages.unity.com"
},
"com.unity.settings-manager": {
- "version": "2.0.1",
+ "version": "2.1.1",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.test-framework": {
- "version": "1.3.9",
+ "version": "1.6.0",
"depth": 2,
- "source": "registry",
+ "source": "builtin",
"dependencies": {
"com.unity.ext.nunit": "2.0.3",
"com.unity.modules.imgui": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0"
- },
- "url": "https://packages.unity.com"
+ }
},
"com.unity.modules.accessibility": {
"version": "1.0.0",
@@ -49,17 +47,79 @@
"source": "builtin",
"dependencies": {}
},
+ "com.unity.modules.adaptiveperformance": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.subsystems": "1.0.0"
+ }
+ },
+ "com.unity.modules.hierarchycore": {
+ "version": "1.0.0",
+ "depth": 2,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.imageconversion": {
+ "version": "1.0.0",
+ "depth": 1,
+ "source": "builtin",
+ "dependencies": {}
+ },
"com.unity.modules.imgui": {
"version": "1.0.0",
- "depth": 3,
+ "depth": 1,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.jsonserialize": {
"version": "1.0.0",
- "depth": 3,
+ "depth": 2,
"source": "builtin",
"dependencies": {}
+ },
+ "com.unity.modules.physics": {
+ "version": "1.0.0",
+ "depth": 2,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.subsystems": {
+ "version": "1.0.0",
+ "depth": 1,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.jsonserialize": "1.0.0"
+ }
+ },
+ "com.unity.modules.ui": {
+ "version": "1.0.0",
+ "depth": 2,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.uielements": {
+ "version": "1.0.0",
+ "depth": 1,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.ui": "1.0.0",
+ "com.unity.modules.imgui": "1.0.0",
+ "com.unity.modules.jsonserialize": "1.0.0",
+ "com.unity.modules.hierarchycore": "1.0.0",
+ "com.unity.modules.physics": "1.0.0"
+ }
+ },
+ "com.unity.modules.vectorgraphics": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.uielements": "1.0.0",
+ "com.unity.modules.imageconversion": "1.0.0",
+ "com.unity.modules.imgui": "1.0.0"
+ }
}
}
}
diff --git a/src/TestProjects/RootTestProject/ProjectSettings/Packages/com.github-joc0de.visual-studio-solution-generator/Settings.json b/src/TestProjects/RootTestProject/ProjectSettings/Packages/com.github-joc0de.visual-studio-solution-generator/Settings.json
index 72a5be0..c8c8f9c 100644
--- a/src/TestProjects/RootTestProject/ProjectSettings/Packages/com.github-joc0de.visual-studio-solution-generator/Settings.json
+++ b/src/TestProjects/RootTestProject/ProjectSettings/Packages/com.github-joc0de.visual-studio-solution-generator/Settings.json
@@ -46,6 +46,11 @@
"key": "general.AdditionalIncludedSolutions",
"value": "{\"m_Value\":[]}"
},
+ {
+ "type": "System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
+ "key": "general.AdditionalIncludedProjectFiles",
+ "value": "{\"m_Value\":[]}"
+ },
{
"type": "System.Collections.Generic.List`1[[UnityVisualStudioSolutionGenerator.Configuration.PropertyGroupSetting, UnityVisualStudioSolutionGenerator, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"key": "sdk-style.SdkAdditionalProperties",
diff --git a/src/TestProjects/RootTestProject/ProjectSettings/ProjectSettings.asset b/src/TestProjects/RootTestProject/ProjectSettings/ProjectSettings.asset
index ab7af1a..20f0243 100644
--- a/src/TestProjects/RootTestProject/ProjectSettings/ProjectSettings.asset
+++ b/src/TestProjects/RootTestProject/ProjectSettings/ProjectSettings.asset
@@ -3,7 +3,7 @@
--- !u!129 &1
PlayerSettings:
m_ObjectHideFlags: 0
- serializedVersion: 27
+ serializedVersion: 28
productGUID: 982e26c401a2b7640af3d831aea59470
AndroidProfiler: 0
AndroidFilterTouchesWhenObscured: 0
@@ -49,6 +49,7 @@ PlayerSettings:
m_StereoRenderingPath: 0
m_ActiveColorSpace: 1
unsupportedMSAAFallback: 0
+ m_SpriteBatchMaxVertexCount: 65535
m_SpriteBatchVertexThreshold: 300
m_MTRendering: 1
mipStripping: 0
@@ -70,18 +71,18 @@ PlayerSettings:
androidRenderOutsideSafeArea: 1
androidUseSwappy: 1
androidBlitType: 0
- androidResizableWindow: 0
+ androidResizeableActivity: 0
androidDefaultWindowWidth: 1920
androidDefaultWindowHeight: 1080
androidMinimumWindowWidth: 400
androidMinimumWindowHeight: 300
androidFullscreenMode: 1
androidAutoRotationBehavior: 1
+ androidPredictiveBackSupport: 1
androidApplicationEntry: 1
defaultIsNativeResolution: 1
macRetinaSupport: 1
runInBackground: 1
- captureSingleScreen: 0
muteOtherAudioSources: 0
Prepare IOS For Recording: 0
Force IOS Speakers When Recording: 0
@@ -137,6 +138,8 @@ PlayerSettings:
vulkanEnableLateAcquireNextImage: 0
vulkanEnableCommandBufferRecycling: 1
loadStoreDebugModeEnabled: 0
+ visionOSBundleVersion: 1.0
+ tvOSBundleVersion: 1.0
bundleVersion: 0.1
preloadedAssets: []
metroInputSource: 0
@@ -163,6 +166,7 @@ PlayerSettings:
buildNumber:
Bratwurst: 0
Standalone: 0
+ VisionOS: 0
iPhone: 0
tvOS: 0
overrideDefaultApplicationIdentifier: 0
@@ -183,12 +187,14 @@ PlayerSettings:
strictShaderVariantMatching: 0
VertexChannelCompressionMask: 4054
iPhoneSdkVersion: 988
+ iOSSimulatorArchitecture: 0
iOSTargetOSVersionString: 13.0
tvOSSdkVersion: 0
+ tvOSSimulatorArchitecture: 0
tvOSRequireExtendedGameController: 0
tvOSTargetOSVersionString: 13.0
- bratwurstSdkVersion: 0
- bratwurstTargetOSVersionString: 13.0
+ VisionOSSdkVersion: 0
+ VisionOSTargetOSVersionString: 1.0
uIPrerenderedIcon: 0
uIRequiresPersistentWiFi: 0
uIRequiresFullScreen: 1
@@ -213,7 +219,6 @@ PlayerSettings:
rgba: 0
iOSLaunchScreenFillPct: 100
iOSLaunchScreenSize: 100
- iOSLaunchScreenCustomXibPath:
iOSLaunchScreeniPadType: 0
iOSLaunchScreeniPadImage: {fileID: 0}
iOSLaunchScreeniPadBackgroundColor:
@@ -221,7 +226,6 @@ PlayerSettings:
rgba: 0
iOSLaunchScreeniPadFillPct: 100
iOSLaunchScreeniPadSize: 100
- iOSLaunchScreeniPadCustomXibPath:
iOSLaunchScreenCustomStoryboardPath:
iOSLaunchScreeniPadCustomStoryboardPath:
iOSDeviceRequirements: []
@@ -231,15 +235,16 @@ PlayerSettings:
iOSMetalForceHardShadows: 0
metalEditorSupport: 1
metalAPIValidation: 1
+ metalCompileShaderBinary: 0
iOSRenderExtraFrameOnPause: 0
iosCopyPluginsCodeInsteadOfSymlink: 0
appleDeveloperTeamID:
iOSManualSigningProvisioningProfileID:
tvOSManualSigningProvisioningProfileID:
- bratwurstManualSigningProvisioningProfileID:
+ VisionOSManualSigningProvisioningProfileID:
iOSManualSigningProvisioningProfileType: 0
tvOSManualSigningProvisioningProfileType: 0
- bratwurstManualSigningProvisioningProfileType: 0
+ VisionOSManualSigningProvisioningProfileType: 0
appleEnableAutomaticSigning: 0
iOSRequireARKit: 0
iOSAutomaticallyDetectAndAddCapabilities: 1
@@ -257,7 +262,6 @@ PlayerSettings:
useCustomGradleSettingsTemplate: 0
useCustomProguardFile: 0
AndroidTargetArchitectures: 2
- AndroidTargetDevices: 0
AndroidSplashScreenScale: 0
androidSplashScreen: {fileID: 0}
AndroidKeystoreName:
@@ -276,12 +280,12 @@ PlayerSettings:
height: 180
banner: {fileID: 0}
androidGamepadSupportLevel: 0
- chromeosInputEmulation: 1
AndroidMinifyRelease: 0
AndroidMinifyDebug: 0
AndroidValidateAppBundleSize: 1
AndroidAppBundleSizeToValidate: 150
AndroidReportGooglePlayAppDependencies: 1
+ androidSymbolsSizeThreshold: 800
m_BuildTargetIcons: []
m_BuildTargetPlatformIcons:
- m_BuildTarget: iPhone
@@ -443,6 +447,9 @@ PlayerSettings:
- m_BuildTarget: WebGLSupport
m_APIs: 0b000000
m_Automatic: 1
+ - m_BuildTarget: WindowsStandaloneSupport
+ m_APIs: 0200000012000000
+ m_Automatic: 0
m_BuildTargetVRSettings:
- m_BuildTarget: Standalone
m_Enabled: 0
@@ -460,18 +467,24 @@ PlayerSettings:
iPhone: 1
tvOS: 1
m_BuildTargetGroupLightmapEncodingQuality:
- - m_BuildTarget: Android
+ - serializedVersion: 2
+ m_BuildTarget: Android
m_EncodingQuality: 1
- - m_BuildTarget: iPhone
+ - serializedVersion: 2
+ m_BuildTarget: iOS
m_EncodingQuality: 1
- - m_BuildTarget: tvOS
+ - serializedVersion: 2
+ m_BuildTarget: tvOS
m_EncodingQuality: 1
m_BuildTargetGroupHDRCubemapEncodingQuality:
- - m_BuildTarget: Android
+ - serializedVersion: 2
+ m_BuildTarget: Android
m_EncodingQuality: 1
- - m_BuildTarget: iPhone
+ - serializedVersion: 2
+ m_BuildTarget: iOS
m_EncodingQuality: 1
- - m_BuildTarget: tvOS
+ - serializedVersion: 2
+ m_BuildTarget: tvOS
m_EncodingQuality: 1
m_BuildTargetGroupLightmapSettings: []
m_BuildTargetGroupLoadStoreDebugModeSettings: []
@@ -483,12 +496,13 @@ PlayerSettings:
- m_BuildTarget: tvOS
m_Encoding: 1
m_BuildTargetDefaultTextureCompressionFormat:
- - serializedVersion: 2
+ - serializedVersion: 3
m_BuildTarget: Android
m_Formats: 03000000
playModeTestRunnerEnabled: 0
runPlayModeTestAsEditModeTest: 0
actionOnDotNetUnhandledException: 1
+ editorGfxJobOverride: 1
enableInternalProfiler: 0
logObjCUncaughtExceptions: 1
enableCrashReportAPI: 0
@@ -496,7 +510,7 @@ PlayerSettings:
locationUsageDescription:
microphoneUsageDescription:
bluetoothUsageDescription:
- macOSTargetOSVersion: 10.13.0
+ macOSTargetOSVersion: 11.0
switchNMETAOverride:
switchNetLibKey:
switchSocketMemoryPoolSize: 6144
@@ -641,6 +655,7 @@ PlayerSettings:
switchEnableRamDiskSupport: 0
switchMicroSleepForYieldTime: 25
switchRamDiskSpaceSize: 12
+ switchUpgradedPlayerSettingsToNMETA: 0
ps4NPAgeRating: 12
ps4NPTitleSecret:
ps4NPTrophyPackPath:
@@ -743,11 +758,12 @@ PlayerSettings:
webGLMemoryLinearGrowthStep: 16
webGLMemoryGeometricGrowthStep: 0.2
webGLMemoryGeometricGrowthCap: 96
- webGLEnableWebGPU: 0
webGLPowerPreference: 2
webGLWebAssemblyTable: 0
webGLWebAssemblyBigInt: 0
webGLCloseOnQuit: 0
+ webWasm2023: 0
+ webEnableSubmoduleStrippingCompatibility: 0
scriptingDefineSymbols: {}
additionalCompilerArguments: {}
platformArchitecture: {}
@@ -803,6 +819,7 @@ PlayerSettings:
metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0}
metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902, b: 0.21568628, a: 1}
metroSplashScreenUseBackgroundColor: 0
+ syncCapabilities: 0
platformCapabilities: {}
metroTargetDeviceFamilies: {}
metroFTAName:
@@ -871,3 +888,6 @@ PlayerSettings:
platformRequiresReadableAssets: 0
virtualTexturingSupportEnabled: 0
insecureHttpOption: 0
+ androidVulkanDenyFilterList: []
+ androidVulkanAllowFilterList: []
+ androidVulkanDeviceFilterListAsset: {fileID: 0}
diff --git a/src/TestProjects/RootTestProject/ProjectSettings/ProjectVersion.txt b/src/TestProjects/RootTestProject/ProjectSettings/ProjectVersion.txt
index 4b379c0..879b68f 100644
--- a/src/TestProjects/RootTestProject/ProjectSettings/ProjectVersion.txt
+++ b/src/TestProjects/RootTestProject/ProjectSettings/ProjectVersion.txt
@@ -1,2 +1,2 @@
-m_EditorVersion: 2023.2.6f1
-m_EditorVersionWithRevision: 2023.2.6f1 (57daeefc879b)
+m_EditorVersion: 6000.3.2f1
+m_EditorVersionWithRevision: 6000.3.2f1 (a9779f353c9b)
diff --git a/src/TestProjects/RootTestProject/RootTestProject.slnx b/src/TestProjects/RootTestProject/RootTestProject.slnx
new file mode 100644
index 0000000..f7402a4
--- /dev/null
+++ b/src/TestProjects/RootTestProject/RootTestProject.slnx
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/UnityVisualStudioSolutionGenerator.Tests/.gitignore b/src/UnityVisualStudioSolutionGenerator.Tests/.gitignore
index 90d3fd9..75e6db1 100644
--- a/src/UnityVisualStudioSolutionGenerator.Tests/.gitignore
+++ b/src/UnityVisualStudioSolutionGenerator.Tests/.gitignore
@@ -36,7 +36,9 @@ ExportedObj/
*.csproj.meta
*.unityproj
*.sln
+*.slnx
*.sln.meta
+*.slnx.meta
*.suo
*.tmp
*.user
diff --git a/src/UnityVisualStudioSolutionGenerator.Tests/Assets/Editor/Tests/SolutionFileTest.cs b/src/UnityVisualStudioSolutionGenerator.Tests/Assets/Editor/Tests/SolutionFileTest.cs
index bf1ea6e..88e2d51 100644
--- a/src/UnityVisualStudioSolutionGenerator.Tests/Assets/Editor/Tests/SolutionFileTest.cs
+++ b/src/UnityVisualStudioSolutionGenerator.Tests/Assets/Editor/Tests/SolutionFileTest.cs
@@ -56,7 +56,8 @@ public class SolutionFileTest
[Test]
public void ParseSolutionTest()
{
- var (projectFiles, _) = SolutionFileParser.Parse(TestSolutionContent, SolutionDirectoryPath, false);
+ var solutionFile = new SolutionFile(SolutionDirectoryPath, "UnityVisualStudioSolutionGenerator.Tests.sln");
+ var (projectFiles, _) = SolutionFileParser.Parse(TestSolutionContent, solutionFile, false);
Assert.That(projectFiles, Is.EqualTo(TestSolutionProjectFiles).Using(new ProjectFileEqualityComparer()));
}
@@ -64,7 +65,8 @@ public void ParseSolutionTest()
[Test]
public void WriteSolutionTest()
{
- var generatedSolutionContent = SolutionFileWriter.WriteToText(SolutionDirectoryPath, TestSolutionProjectFiles);
+ var solutionFile = new SolutionFile(SolutionDirectoryPath, "UnityVisualStudioSolutionGenerator.Tests.sln");
+ var generatedSolutionContent = SolutionFileWriter.WriteToText(solutionFile, TestSolutionProjectFiles);
Assert.That(generatedSolutionContent, Is.EqualTo(TestSolutionContent));
}
diff --git a/src/UnityVisualStudioSolutionGenerator/.gitignore b/src/UnityVisualStudioSolutionGenerator/.gitignore
index 90d3fd9..75e6db1 100644
--- a/src/UnityVisualStudioSolutionGenerator/.gitignore
+++ b/src/UnityVisualStudioSolutionGenerator/.gitignore
@@ -36,7 +36,9 @@ ExportedObj/
*.csproj.meta
*.unityproj
*.sln
+*.slnx
*.sln.meta
+*.slnx.meta
*.suo
*.tmp
*.user
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileParser.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileParser.cs
new file mode 100644
index 0000000..d037807
--- /dev/null
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileParser.cs
@@ -0,0 +1,137 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace UnityVisualStudioSolutionGenerator
+{
+ ///
+ /// Provides methods for parsing Visual Studio solution files (.sln) and extracting referenced project files.
+ ///
+ public static class LegacySolutionFileParser
+ {
+ private const string ProjectStartTagName = "Project";
+
+ private const string ProjectEndTagName = "EndProject";
+
+ ///
+ /// Parses the solution file and gets the project files referenced from the solution.
+ ///
+ /// The solution file content.
+ /// The absolute directory path of the solution file.
+ ///
+ /// Indicates whether to only include project files generated by Unity or files directly inside the solution directory.
+ ///
+ ///
+ /// A tuple containing the referenced project files and a flag indicating if the source contains duplicate projects.
+ ///
+ public static (IReadOnlyList ProjectFiles, bool SourceContainsDuplicateProjects) Parse(
+ string content,
+ string solutionDirectoryPath,
+ bool onlyIncludeUnityGeneratedProjects)
+ {
+ _ = content ?? throw new ArgumentNullException(nameof(content));
+ var projects = GetUsedProjectFiles(content, solutionDirectoryPath);
+ return SolutionFileParser.CreateFilteredProjectsResult(solutionDirectoryPath, onlyIncludeUnityGeneratedProjects, projects);
+ }
+
+ ///
+ /// Parses the specified and gets the project files referenced from the solution.
+ ///
+ /// The solution file to parse.
+ ///
+ /// Indicates whether to only include project files generated by Unity or files directly inside the solution directory.
+ ///
+ ///
+ /// A tuple containing the referenced project files and a flag indicating if the source contains duplicate projects.
+ ///
+ internal static (IReadOnlyList ProjectFiles, bool SourceContainsDuplicateProjects) Parse(
+ SolutionFile solutionFile,
+ bool onlyIncludeUnityGeneratedProjects)
+ {
+ return Parse(File.ReadAllText(solutionFile.SolutionFilePath), solutionFile.SolutionDirectoryPath, onlyIncludeUnityGeneratedProjects);
+ }
+
+ private static IEnumerable GetUsedProjectFiles(string content, string solutionDirectoryPath)
+ {
+ if (!Directory.Exists(solutionDirectoryPath))
+ {
+ LogHelper.LogError($"Generating a solution file inside a directory that doesn't exists. Directory path: {solutionDirectoryPath}");
+ }
+
+ var projectIndex = 0;
+ while (projectIndex < content.Length)
+ {
+ projectIndex = content.IndexOf(ProjectStartTagName, projectIndex, StringComparison.Ordinal);
+ if (projectIndex < 0)
+ {
+ break;
+ }
+
+ projectIndex += ProjectStartTagName.Length;
+ projectIndex = SkipWhiteSpaces(content, projectIndex);
+
+ if (content[projectIndex] != '(')
+ {
+ // not a project start tag just the word Project
+ continue;
+ }
+
+ var endIndex = content.IndexOf(ProjectEndTagName, projectIndex, StringComparison.Ordinal);
+ if (endIndex < 0)
+ {
+ LogHelper.LogError($"Found 'Project' start but no 'EndProject' starting at char-index: {projectIndex}");
+ continue;
+ }
+
+ var firstCommaIndex = content.IndexOf(',', projectIndex, endIndex - projectIndex);
+ if (firstCommaIndex < 0)
+ {
+ LogHelper.LogError($"Found no ',' inside Project -> EndProject section: {content[projectIndex..endIndex]}");
+ continue;
+ }
+
+ ++firstCommaIndex; // skip the comma
+ var secondCommaIndex = content.IndexOf(',', firstCommaIndex, endIndex - firstCommaIndex);
+ if (secondCommaIndex < 0)
+ {
+ LogHelper.LogError($"Found no second ',' inside Project -> EndProject section: {content[projectIndex..endIndex]}");
+ continue;
+ }
+
+ var projectFileNamePart = content[firstCommaIndex..secondCommaIndex].Trim('"', ' ');
+ if (string.IsNullOrEmpty(projectFileNamePart))
+ {
+ LogHelper.LogError($"Failed to extract csproj file name from Project -> EndProject section: {content[projectIndex..endIndex]}");
+ continue;
+ }
+
+ ++secondCommaIndex; // skip the comma
+ var projectIdEndIndex = content.LastIndexOf('"', endIndex, endIndex - secondCommaIndex);
+ if (projectIdEndIndex < 0)
+ {
+ LogHelper.LogError(
+ $"Found no ProjectId ('\"') after the second ',' inside Project -> EndProject section: {content[projectIndex..endIndex]}");
+ continue;
+ }
+
+ var projectId = content[secondCommaIndex..projectIdEndIndex].Trim('"', ' ');
+ projectIndex = endIndex + ProjectEndTagName.Length;
+
+ var projectFilePath = Path.GetFullPath(projectFileNamePart, solutionDirectoryPath);
+ yield return new ProjectFile(projectFilePath, projectId);
+ }
+ }
+
+ private static int SkipWhiteSpaces(string content, int projectIndex)
+ {
+ while (projectIndex < content.Length && char.IsWhiteSpace(content[projectIndex]))
+ {
+ ++projectIndex;
+ }
+
+ return projectIndex;
+ }
+ }
+}
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileParser.cs.meta b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileParser.cs.meta
new file mode 100644
index 0000000..de57c5c
--- /dev/null
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileParser.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4d84121e775dce74486b0891e4a91a09
\ No newline at end of file
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileWriter.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileWriter.cs
new file mode 100644
index 0000000..a548bd1
--- /dev/null
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileWriter.cs
@@ -0,0 +1,59 @@
+#nullable enable
+
+using System.Collections.Generic;
+using System.IO;
+
+namespace UnityVisualStudioSolutionGenerator
+{
+ ///
+ /// Writes a Visual Studio solution file (.sln).
+ ///
+ public static class LegacySolutionFileWriter
+ {
+ ///
+ /// Writes the solution file using legacy (.sln) format to the specified writer.
+ ///
+ /// The writer to write the solution file to.
+ /// The absolute path of the directory containing the .sln file.
+ /// All project files included in the solution.
+ internal static void WriteTo(TextWriter writer, string solutionDirectoryPath, IReadOnlyList allProjects)
+ {
+ writer.WriteLine("Microsoft Visual Studio Solution File, Format Version 12.00");
+ writer.WriteLine("# Visual Studio Version 17");
+ writer.WriteLine("VisualStudioVersion = 17.0.32014.148");
+ writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1");
+
+ foreach (var project in allProjects)
+ {
+ var projectName = project.ProjectName;
+ var relativeProjectFilePath = Path.GetRelativePath(solutionDirectoryPath, project.FilePath);
+ writer.WriteLine(
+ "Project(\"{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}\") = \"{0}\", \"{1}\", \"{2}\"",
+ projectName,
+ relativeProjectFilePath,
+ project.Id);
+ writer.WriteLine("EndProject");
+ }
+
+ writer.WriteLine("Global");
+ writer.WriteLine("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution");
+ writer.WriteLine("\t\tDebug|Any CPU = Debug|Any CPU");
+ writer.WriteLine("\t\tRelease|Any CPU = Release|Any CPU");
+ writer.WriteLine("\tEndGlobalSection");
+ writer.WriteLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution");
+ foreach (var project in allProjects)
+ {
+ writer.WriteLine("\t\t{0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU", project.Id);
+ writer.WriteLine("\t\t{0}.Debug|Any CPU.Build.0 = Debug|Any CPU", project.Id);
+ writer.WriteLine("\t\t{0}.Release|Any CPU.ActiveCfg = Release|Any CPU", project.Id);
+ writer.WriteLine("\t\t{0}.Release|Any CPU.Build.0 = Release|Any CPU", project.Id);
+ }
+
+ writer.WriteLine("\tEndGlobalSection");
+ writer.WriteLine("\tGlobalSection(SolutionProperties) = preSolution");
+ writer.WriteLine("\t\tHideSolutionNode = FALSE");
+ writer.WriteLine("\tEndGlobalSection");
+ writer.WriteLine("EndGlobal");
+ }
+ }
+}
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileWriter.cs.meta b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileWriter.cs.meta
new file mode 100644
index 0000000..c154b1d
--- /dev/null
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/LegacySolutionFileWriter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4f6ae96eab13d2a4e8df30b5141f0fba
\ No newline at end of file
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/MenuItemProvider.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/MenuItemProvider.cs
index b13a0c6..44b8f41 100644
--- a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/MenuItemProvider.cs
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/MenuItemProvider.cs
@@ -20,16 +20,6 @@ public static void OpenSolution()
EditorApplication.ExecuteMenuItem("Assets/Open C# Project");
}
- ///
- /// Returns whether or not the menu item should be enabled.
- ///
- /// True if the menu item should be enabled, False otherwise.
- [MenuItem("Visual Studio/Open Solution", true)]
- public static bool OpenSolutionEnabled()
- {
- return GeneratorSettings.IsVisualStudioEditorEnabled();
- }
-
///
/// Regenerates the Visual Studio solution file and the C# project files.
///
@@ -96,7 +86,14 @@ public static bool SyncSolutionLegacyStyleEnabled()
[MenuItem("Visual Studio/Apply enable nullable to all files", priority = 4)]
public static void EnableNullableOnAllFiles()
{
- SourceCodeFilesHandler.EnableNullableOnAllFiles(SolutionFile.CurrentProjectSolution);
+ var solutionFile = SolutionFile.CurrentProjectSolution;
+ if (solutionFile is null)
+ {
+ LogHelper.LogError($"No solution file found to apply 'nullable enable' to all files.");
+ return;
+ }
+
+ SourceCodeFilesHandler.EnableNullableOnAllFiles(solutionFile);
}
///
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/ProjectFile.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/ProjectFile.cs
index 5e607fe..c50c69f 100644
--- a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/ProjectFile.cs
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/ProjectFile.cs
@@ -29,6 +29,7 @@ public ProjectFile(string filePath, string id)
///
/// Gets the ID of the project file.
+ /// When using .slnx solution files we have no 'Id' it will always be empty string.
///
public string Id { get; }
@@ -46,17 +47,33 @@ public override bool Equals(object? obj)
///
public override int GetHashCode()
{
+ if (Id.Length == 0)
+ {
+ return ProjectName.GetHashCode(StringComparison.Ordinal);
+ }
+
return Id.GetHashCode(StringComparison.Ordinal);
}
///
public override string ToString()
{
+ if (Id.Length == 0)
+ {
+ return $"{nameof(FilePath)}: {FilePath}";
+ }
+
return $"{nameof(FilePath)}: {FilePath}, {nameof(Id)}: {Id}";
}
private bool Equals(ProjectFile other)
{
+ if (Id.Length + other.Id.Length == 0)
+ {
+ // if using slnx the Id is empty.
+ return ProjectName == other.ProjectName;
+ }
+
// we need to use the ProjectName as an alternative so we detect duplicate entries inside .sln
return Id == other.Id || ProjectName == other.ProjectName;
}
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/ProjectFileGeneratorBase.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/ProjectFileGeneratorBase.cs
index 1d5e754..c771b3b 100644
--- a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/ProjectFileGeneratorBase.cs
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/ProjectFileGeneratorBase.cs
@@ -107,8 +107,9 @@ protected static IEnumerable FindSubProjectFolders(string outputFileDire
outputFileDirectoryPath,
"*.asmdef",
new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive, RecurseSubdirectories = true })
- .Select(
- assemblyDefinitionFilePath => Path.GetRelativePath(outputFileDirectoryPath, Path.GetDirectoryName(assemblyDefinitionFilePath)))
+ .Select(assemblyDefinitionFilePath => Path.GetRelativePath(
+ outputFileDirectoryPath,
+ Path.GetDirectoryName(assemblyDefinitionFilePath)))
.Where(relativeSubProjectDirectory => !string.IsNullOrEmpty(relativeSubProjectDirectory) && relativeSubProjectDirectory != ".");
return foldersToIgnore;
}
@@ -132,14 +133,14 @@ private static bool MatchesOnePattern(string? value, List patterns)
return !string.IsNullOrWhiteSpace(value) && patterns.Exists(pattern => MatchesPattern(value!, pattern));
}
- private static bool MatchesPattern(string value, IReadOnlyList pattern)
+ private static bool MatchesPattern(string value, string[] pattern)
{
- if (pattern.Count == 0)
+ if (pattern.Length == 0)
{
return true;
}
- if (pattern.Count == 1)
+ if (pattern.Length == 1)
{
// no '*'
return string.Equals(value, pattern[0], StringComparison.OrdinalIgnoreCase);
@@ -152,7 +153,7 @@ private static bool MatchesPattern(string value, IReadOnlyList pattern)
}
var matchStartIndex = pattern[0].Length;
- var lastPatternIndex = pattern.Count - 1;
+ var lastPatternIndex = pattern.Length - 1;
// last pattern is a end with condition
if (pattern[lastPatternIndex].Length != 0 &&
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFile.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFile.cs
index e813511..060932c 100644
--- a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFile.cs
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFile.cs
@@ -2,7 +2,6 @@
using System;
using System.IO;
-using UnityEngine;
namespace UnityVisualStudioSolutionGenerator
{
@@ -11,15 +10,8 @@ namespace UnityVisualStudioSolutionGenerator
///
internal sealed class SolutionFile
{
- static SolutionFile()
- {
- var solutionDirectoryPath = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
- var solutionFilePath = $"{Path.Combine(solutionDirectoryPath, Path.GetFileName(solutionDirectoryPath))}.sln";
- CurrentProjectSolution = new SolutionFile(solutionDirectoryPath, solutionFilePath);
- }
-
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The directory that contains the .sln file.
/// The full path of the .sln file.
@@ -30,9 +22,9 @@ public SolutionFile(string solutionDirectoryPath, string solutionFilePath)
}
///
- /// Gets the information about the '.sln' file of the current Unity Project.
+ /// Gets or sets the information about the '.sln' of '.slnx' file of the current Unity Project.
///
- public static SolutionFile CurrentProjectSolution { get; }
+ public static SolutionFile? CurrentProjectSolution { get; set; }
///
/// Gets the absolute path to the directory containing the '.sln' file.
@@ -44,6 +36,11 @@ public SolutionFile(string solutionDirectoryPath, string solutionFilePath)
///
public string SolutionFilePath { get; }
+ ///
+ /// Gets a value indicating whether the solution file is in XML format (.slnx).
+ ///
+ public bool IsXmlSolution => Path.GetExtension(SolutionFilePath.AsSpan()).Equals(".slnx", StringComparison.OrdinalIgnoreCase);
+
///
public override string ToString()
{
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFileParser.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFileParser.cs
index 98deb73..c3149a7 100644
--- a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFileParser.cs
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFileParser.cs
@@ -3,56 +3,103 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
namespace UnityVisualStudioSolutionGenerator
{
///
- /// Parses a visual studio solution file and gets the project files referenced from the solution.
+ /// Provides methods for parsing Visual Studio solution files (.sln or .slnx) and extracting referenced project files.
///
- public static class SolutionFileParser
+ internal static class SolutionFileParser
{
- private const string ProjectStartTagName = "Project";
+ ///
+ /// Parses the specified and gets the project files referenced from the solution.
+ ///
+ /// The solution file to parse.
+ ///
+ /// Indicates whether to only include project files generated by Unity or files directly inside the solution directory.
+ ///
+ ///
+ /// A tuple containing the referenced project files and a flag indicating if the source contains duplicate projects.
+ ///
+ internal static (IReadOnlyList ProjectFiles, bool SourceContainsDuplicateProjects) Parse(
+ SolutionFile solutionFile,
+ bool onlyIncludeUnityGeneratedProjects)
+ {
+ if (solutionFile.IsXmlSolution)
+ {
+ return XmlSolutionFileParser.Parse(solutionFile, onlyIncludeUnityGeneratedProjects);
+ }
- private const string ProjectEndTagName = "EndProject";
+ return LegacySolutionFileParser.Parse(solutionFile, onlyIncludeUnityGeneratedProjects);
+ }
///
- /// Parses the solution file and gets the project files referenced from the solution.
+ /// Parses the solution file content and gets the project files referenced from the solution.
///
/// The solution file content.
- /// The absolute directory path of the solution file.
+ /// The solution file being parsed.
///
- /// Indicating whether we should only include project files that are generated by Unity / files that are
- /// directly inside the solution directory.
+ /// Indicates whether to only include project files generated by Unity or files directly inside the solution directory.
///
- /// The referenced project files.
- public static (IReadOnlyList ProjectFiles, bool SourceContainsDuplicateProjects) Parse(
+ ///
+ /// A tuple containing the referenced project files and a flag indicating if the source contains duplicate projects.
+ ///
+ internal static (IReadOnlyList ProjectFiles, bool SourceContainsDuplicateProjects) Parse(
string content,
- string solutionDirectoryPath,
+ SolutionFile solutionFile,
bool onlyIncludeUnityGeneratedProjects)
{
- _ = content ?? throw new ArgumentNullException(nameof(content));
+ if (solutionFile.IsXmlSolution)
+ {
+ return XmlSolutionFileParser.Parse(content, solutionFile.SolutionDirectoryPath, onlyIncludeUnityGeneratedProjects);
+ }
+
+ return LegacySolutionFileParser.Parse(content, solutionFile.SolutionDirectoryPath, onlyIncludeUnityGeneratedProjects);
+ }
+
+ ///
+ /// Filters and returns the list of project files, optionally including only Unity-generated projects, and indicates if duplicates exist.
+ ///
+ /// The absolute directory path of the solution file.
+ ///
+ /// Indicates whether to only include project files generated by Unity or files directly inside the solution directory.
+ ///
+ /// The enumerable of project files to filter.
+ ///
+ /// A tuple containing the filtered project files and a flag indicating if the source contains duplicate projects.
+ ///
+ internal static (IReadOnlyList ProjectFiles, bool SourceContainsDuplicateProjects) CreateFilteredProjectsResult(
+ string solutionDirectoryPath,
+ bool onlyIncludeUnityGeneratedProjects,
+ IEnumerable projects)
+ {
var allProjects = new List();
- var projects = GetUsedProjectFiles(content, solutionDirectoryPath);
var sourceContainsDuplicateProjects = false;
foreach (var projectFile in projects)
{
// unity didn't remove the project-entry generated from us from the solution so we need to handle cases where we have duplicate entries
// one that lies in the solution directory (the one generated by Unity / not changed by us) and one placed next to the .asmdef (generated by us)
- if (allProjects.Contains(projectFile))
+ var projectWithSameName = allProjects.FirstOrDefault(project => project.ProjectName == projectFile.ProjectName);
+ if (projectWithSameName is not null)
{
- sourceContainsDuplicateProjects = true;
- if (Path.GetDirectoryName(projectFile.FilePath.AsSpan()).Equals(solutionDirectoryPath, StringComparison.Ordinal))
+ if (IsProjectInSolutionDirectory(solutionDirectoryPath, projectFile))
{
// prefer the one generated by unity (placed in the solution directory)
allProjects.Remove(projectFile);
allProjects.Add(projectFile);
+ sourceContainsDuplicateProjects = true;
+ continue;
}
- continue;
+ if (IsProjectInSolutionDirectory(solutionDirectoryPath, projectWithSameName))
+ {
+ sourceContainsDuplicateProjects = true;
+ continue;
+ }
}
- if (onlyIncludeUnityGeneratedProjects &&
- !Path.GetDirectoryName(projectFile.FilePath.AsSpan()).Equals(solutionDirectoryPath, StringComparison.Ordinal))
+ if (onlyIncludeUnityGeneratedProjects && !IsProjectInSolutionDirectory(solutionDirectoryPath, projectFile))
{
continue;
}
@@ -63,101 +110,9 @@ public static (IReadOnlyList ProjectFiles, bool SourceContainsDupli
return (allProjects, sourceContainsDuplicateProjects);
}
- ///
- /// Parses the solution file and gets the project files referenced from the solution.
- ///
- /// The solution .
- ///
- /// Indicating whether we should only include project files that are generated by Unity / files that are
- /// directly inside the solution directory.
- ///
- /// The referenced project files.
- internal static (IReadOnlyList ProjectFiles, bool SourceContainsDuplicateProjects) Parse(
- SolutionFile solutionFile,
- bool onlyIncludeUnityGeneratedProjects)
- {
- return Parse(File.ReadAllText(solutionFile.SolutionFilePath), solutionFile.SolutionDirectoryPath, onlyIncludeUnityGeneratedProjects);
- }
-
- private static IEnumerable GetUsedProjectFiles(string content, string solutionDirectoryPath)
+ private static bool IsProjectInSolutionDirectory(string solutionDirectoryPath, ProjectFile projectFile)
{
- if (!Directory.Exists(solutionDirectoryPath))
- {
- LogHelper.LogError($"Generating a solution file inside a directory that doesn't exists. Directory path: {solutionDirectoryPath}");
- }
-
- var projectIndex = 0;
- while (projectIndex < content.Length)
- {
- projectIndex = content.IndexOf(ProjectStartTagName, projectIndex, StringComparison.Ordinal);
- if (projectIndex < 0)
- {
- break;
- }
-
- projectIndex += ProjectStartTagName.Length;
- projectIndex = SkipWhiteSpaces(content, projectIndex);
-
- if (content[projectIndex] != '(')
- {
- // not a project start tag just the word Project
- continue;
- }
-
- var endIndex = content.IndexOf(ProjectEndTagName, projectIndex, StringComparison.Ordinal);
- if (endIndex < 0)
- {
- LogHelper.LogError($"Found 'Project' start but no 'EndProject' starting at char-index: {projectIndex}");
- continue;
- }
-
- var firstCommaIndex = content.IndexOf(',', projectIndex, endIndex - projectIndex);
- if (firstCommaIndex < 0)
- {
- LogHelper.LogError($"Found no ',' inside Project -> EndProject section: {content[projectIndex..endIndex]}");
- continue;
- }
-
- ++firstCommaIndex; // skip the comma
- var secondCommaIndex = content.IndexOf(',', firstCommaIndex, endIndex - firstCommaIndex);
- if (secondCommaIndex < 0)
- {
- LogHelper.LogError($"Found no second ',' inside Project -> EndProject section: {content[projectIndex..endIndex]}");
- continue;
- }
-
- var projectFileNamePart = content[firstCommaIndex..secondCommaIndex].Trim('"', ' ');
- if (string.IsNullOrEmpty(projectFileNamePart))
- {
- LogHelper.LogError($"Failed to extract csproj file name from Project -> EndProject section: {content[projectIndex..endIndex]}");
- continue;
- }
-
- ++secondCommaIndex; // skip the comma
- var projectIdEndIndex = content.LastIndexOf('"', endIndex, endIndex - secondCommaIndex);
- if (projectIdEndIndex < 0)
- {
- LogHelper.LogError(
- $"Found no ProjectId ('\"') after the second ',' inside Project -> EndProject section: {content[projectIndex..endIndex]}");
- continue;
- }
-
- var projectId = content[secondCommaIndex..projectIdEndIndex].Trim('"', ' ');
- projectIndex = endIndex + ProjectEndTagName.Length;
-
- var projectFilePath = Path.GetFullPath(projectFileNamePart, solutionDirectoryPath);
- yield return new ProjectFile(projectFilePath, projectId);
- }
- }
-
- private static int SkipWhiteSpaces(string content, int projectIndex)
- {
- while (projectIndex < content.Length && char.IsWhiteSpace(content[projectIndex]))
- {
- ++projectIndex;
- }
-
- return projectIndex;
+ return Path.GetDirectoryName(projectFile.FilePath.AsSpan()).Equals(solutionDirectoryPath, StringComparison.Ordinal);
}
}
}
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFileWriter.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFileWriter.cs
index 92898f1..2b2ec11 100644
--- a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFileWriter.cs
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/SolutionFileWriter.cs
@@ -11,102 +11,89 @@
namespace UnityVisualStudioSolutionGenerator
{
///
- /// Writes a Visual Studio solution file.
+ /// Provides methods for writing Visual Studio solution files (.sln or .slnx) to disk or as text.
///
public static class SolutionFileWriter
{
///
- /// Generates a Visual Studio solution file and write it to a string.
+ /// The Windows standard new line string (\r\n).
///
- /// The absolute path of the solution directory.
- /// All projects that should be included inside the solution.
- /// The content of the generated solution as plain text.
- public static string WriteToText(string solutionDirectoryPath, IReadOnlyList allProjects)
+ internal const string WindowsNewLine = "\r\n";
+
+ ///
+ /// Generates a Visual Studio solution file content as a string.
+ ///
+ /// The solution file information.
+ /// All projects that should be included inside the solution.
+ /// The generated solution file content as a string.
+ internal static string WriteToText(SolutionFile solutionFile, IReadOnlyList newProjects)
{
- _ = allProjects ?? throw new ArgumentNullException(nameof(allProjects));
- using var writer = new StringWriter();
- GenerateVisualStudioSolution(writer, solutionDirectoryPath, allProjects);
- return writer.ToString();
+ using var stringWriter = new StringWriter();
+ var useXmlFormat = solutionFile.IsXmlSolution;
+ WriteTo(useXmlFormat, solutionFile.SolutionDirectoryPath, newProjects, stringWriter);
+
+ var result = stringWriter.ToString();
+ if (useXmlFormat && !result.EndsWith(WindowsNewLine, StringComparison.Ordinal))
+ {
+ result += WindowsNewLine;
+ }
+
+ return result;
}
///
/// Generates a Visual Studio solution file and write it to a file. The file is overwritten so, we first write it to a temp file so any exceptions
/// while generating the file don't lead to a incomplete file.
///
- /// The file path to write the generated solution file.
- /// The absolute path of the solution directory.
+ /// The solution file information.
/// All projects that should be included inside the solution.
[SuppressMessage("Security", "CA5351", Justification = "Hash is only used for comparison.")]
- public static void WriteToFileSafe(string outputFilePath, string solutionDirectoryPath, IReadOnlyList projectFiles)
+ internal static void WriteToFileSafe(SolutionFile solutionFile, IReadOnlyList projectFiles)
{
// we don't write directly to prevent exceptions
- var tempSolutionFilePath = $"{outputFilePath}.temp";
- WriteToFile(tempSolutionFilePath, solutionDirectoryPath, projectFiles);
+ var tempSolutionFileName = $"{solutionFile.SolutionFilePath}.temp";
+ WriteToFile(solutionFile.IsXmlSolution, tempSolutionFileName, solutionFile.SolutionDirectoryPath, projectFiles);
- if (File.Exists(outputFilePath))
+ if (File.Exists(solutionFile.SolutionFilePath))
{
// only write if the content has changed
using var md5Algorithm = MD5.Create();
- var hashOfNew = ComputeFileHash(tempSolutionFilePath, md5Algorithm);
- var hashOfOld = ComputeFileHash(outputFilePath, md5Algorithm);
+ var hashOfNew = ComputeFileHash(tempSolutionFileName, md5Algorithm);
+ var hashOfOld = ComputeFileHash(solutionFile.SolutionFilePath, md5Algorithm);
if (hashOfOld.SequenceEqual(hashOfNew))
{
// nothing changed -> don't overwrite original file (don't trigger reload in Visual Studio)
- File.Delete(tempSolutionFilePath);
+ File.Delete(tempSolutionFileName);
return;
}
- File.Delete(outputFilePath);
+ File.Delete(solutionFile.SolutionFilePath);
}
- File.Move(tempSolutionFilePath, outputFilePath);
+ File.Move(tempSolutionFileName, solutionFile.SolutionFilePath);
}
- private static void GenerateVisualStudioSolution(TextWriter writer, string solutionDirectoryPath, IReadOnlyList allProjects)
+ private static void WriteToFile(
+ bool useXmlFormat,
+ string solutionFileName,
+ string solutionDirectoryPath,
+ IReadOnlyList projectFiles)
{
- writer.WriteLine("Microsoft Visual Studio Solution File, Format Version 12.00");
- writer.WriteLine("# Visual Studio Version 17");
- writer.WriteLine("VisualStudioVersion = 17.0.32014.148");
- writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1");
+ using var solutionWriter = new StreamWriter(File.Create(solutionFileName), Encoding.UTF8);
+ WriteTo(useXmlFormat, solutionDirectoryPath, projectFiles, solutionWriter);
+ }
- foreach (var project in allProjects)
+ private static void WriteTo(bool useXmlFormat, string solutionDirectoryPath, IReadOnlyList projectFiles, TextWriter writer)
+ {
+ if (useXmlFormat)
{
- var projectName = project.ProjectName;
- var relativeProjectFilePath = Path.GetRelativePath(solutionDirectoryPath, project.FilePath);
- writer.WriteLine(
- "Project(\"{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}\") = \"{0}\", \"{1}\", \"{2}\"",
- projectName,
- relativeProjectFilePath,
- project.Id);
- writer.WriteLine("EndProject");
+ XmlSolutionFileWriter.WriteTo(writer, solutionDirectoryPath, projectFiles);
}
-
- writer.WriteLine("Global");
- writer.WriteLine("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution");
- writer.WriteLine("\t\tDebug|Any CPU = Debug|Any CPU");
- writer.WriteLine("\t\tRelease|Any CPU = Release|Any CPU");
- writer.WriteLine("\tEndGlobalSection");
- writer.WriteLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution");
- foreach (var project in allProjects)
+ else
{
- writer.WriteLine("\t\t{0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU", project.Id);
- writer.WriteLine("\t\t{0}.Debug|Any CPU.Build.0 = Debug|Any CPU", project.Id);
- writer.WriteLine("\t\t{0}.Release|Any CPU.ActiveCfg = Release|Any CPU", project.Id);
- writer.WriteLine("\t\t{0}.Release|Any CPU.Build.0 = Release|Any CPU", project.Id);
+ LegacySolutionFileWriter.WriteTo(writer, solutionDirectoryPath, projectFiles);
}
-
- writer.WriteLine("\tEndGlobalSection");
- writer.WriteLine("\tGlobalSection(SolutionProperties) = preSolution");
- writer.WriteLine("\t\tHideSolutionNode = FALSE");
- writer.WriteLine("\tEndGlobalSection");
- writer.WriteLine("EndGlobal");
- }
-
- private static void WriteToFile(string outputFilePath, string solutionDirectoryPath, IReadOnlyList projectFiles)
- {
- using var solutionWriter = new StreamWriter(File.Create(outputFilePath), Encoding.UTF8);
- GenerateVisualStudioSolution(solutionWriter, solutionDirectoryPath, projectFiles);
}
private static byte[] ComputeFileHash(string tempSolutionFilePath, HashAlgorithm md5Algorithm)
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/VisualStudioAssetPostprocessor.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/VisualStudioAssetPostprocessor.cs
index 65b6445..af0062a 100644
--- a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/VisualStudioAssetPostprocessor.cs
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/VisualStudioAssetPostprocessor.cs
@@ -7,9 +7,8 @@
using System.IO;
using System.Linq;
using System.Security.Cryptography;
-using System.Security.Policy;
+using System.Text;
using UnityEditor;
-using UnityEngine;
using UnityVisualStudioSolutionGenerator.Configuration;
namespace UnityVisualStudioSolutionGenerator
@@ -45,7 +44,7 @@ public static void MarkAsChanged()
private static void Initialize()
{
var solutionFile = SolutionFile.CurrentProjectSolution;
- if (!GeneratorSettings.IsEnabled || !File.Exists(solutionFile.SolutionFilePath))
+ if (!GeneratorSettings.IsEnabled || solutionFile is null || !File.Exists(solutionFile.SolutionFilePath))
{
return;
}
@@ -68,9 +67,9 @@ private static void Initialize()
// Sometimes 'OnGeneratedCSProjectFiles' is not called when the reload order is wrong so we regenerate it here.
// We detect this by checking if Unity generated the .sln and skipped all events like 'OnGeneratedCSProjectFiles'
// so the .sln contains both the .csproj from Unity and the one generated by GenerateNewProjects.
- var newProjects = GenerateNewProjects(allProjects, solutionFile.SolutionDirectoryPath);
+ var newProjects = GenerateNewProjects(allProjects, solutionFile);
- SolutionFileWriter.WriteToFileSafe(solutionFile.SolutionFilePath, solutionFile.SolutionDirectoryPath, newProjects);
+ SolutionFileWriter.WriteToFileSafe(solutionFile, newProjects);
lastSolutionGenerationTime = DateTime.UtcNow;
LogHelper.LogInformation(
$"Generated Visual Studio Solution in '{nameof(Initialize)}': '{solutionFile}' in {stopwatch.ElapsedMilliseconds} ms.");
@@ -91,13 +90,20 @@ private static void OnGeneratedCSProjectFiles()
return;
}
- var stopwatch = Stopwatch.StartNew();
var solutionFile = SolutionFile.CurrentProjectSolution;
+ if (solutionFile is null)
+ {
+ LogHelper.LogWarning(
+ $"Failed to generate solution on '{nameof(OnGeneratedCSProjectFiles)}' event because '{nameof(SolutionFile.CurrentProjectSolution)}' is null.");
+ return;
+ }
+
+ var stopwatch = Stopwatch.StartNew();
var (allProjects, _) = SolutionFileParser.Parse(solutionFile, false);
- var newProjects = GenerateNewProjects(allProjects, solutionFile.SolutionDirectoryPath);
+ var newProjects = GenerateNewProjects(allProjects, solutionFile);
- SolutionFileWriter.WriteToFileSafe(solutionFile.SolutionFilePath, solutionFile.SolutionDirectoryPath, newProjects);
+ SolutionFileWriter.WriteToFileSafe(solutionFile, newProjects);
lastSolutionGenerationTime = currentTime;
LogHelper.LogInformation($"Generated Visual Studio Solution: '{solutionFile}' in {stopwatch.ElapsedMilliseconds} ms.");
}
@@ -115,6 +121,8 @@ private static string OnGeneratedSlnSolution(string path, string content)
{
try
{
+ var solutionFile = new SolutionFile(GetDirectoryPath(path), path);
+ SolutionFile.CurrentProjectSolution = solutionFile;
if (!GeneratorSettings.IsEnabled)
{
return RemoveGeneratedProjectsFromSolution(path, content);
@@ -129,15 +137,14 @@ private static string OnGeneratedSlnSolution(string path, string content)
}
var stopwatch = Stopwatch.StartNew();
- var solutionDirectoryPath = GetDirectoryPath(path);
- var (allProjects, _) = SolutionFileParser.Parse(content, solutionDirectoryPath, false);
+ var (allProjects, _) = SolutionFileParser.Parse(content, solutionFile, false);
if (!allProjects.All(project => File.Exists(project.FilePath)))
{
return content;
}
- var newProjects = GenerateNewProjects(allProjects, solutionDirectoryPath);
- var newContent = SolutionFileWriter.WriteToText(solutionDirectoryPath, newProjects);
+ var newProjects = GenerateNewProjects(allProjects, solutionFile);
+ var newContent = SolutionFileWriter.WriteToText(solutionFile, newProjects);
lastSolutionGenerationTime = DateTime.UtcNow;
lastInputSolutionContent = content;
@@ -155,13 +162,13 @@ private static string OnGeneratedSlnSolution(string path, string content)
return content;
}
- private static List GenerateNewProjects(IReadOnlyList allProjects, string solutionDirectoryPath)
+ private static List GenerateNewProjects(IReadOnlyList allProjects, SolutionFile solutionFile)
{
var newProjects = new List();
foreach (var project in allProjects)
{
var projectFilePath = project.FilePath;
- var projectFilePathInSolutionDirectory = Path.Combine(solutionDirectoryPath, Path.GetFileName(projectFilePath));
+ var projectFilePathInSolutionDirectory = Path.Combine(solutionFile.SolutionDirectoryPath, Path.GetFileName(projectFilePath));
if (projectFilePathInSolutionDirectory != projectFilePath && File.Exists(projectFilePathInSolutionDirectory))
{
// prefer file from solution directory (the one generated by Unity), if it exists.
@@ -191,7 +198,7 @@ private static List GenerateNewProjects(IReadOnlyList
continue;
}
- var newProjectFilePath = generator.WriteProjectFile(solutionDirectoryPath);
+ var newProjectFilePath = generator.WriteProjectFile(solutionFile.SolutionDirectoryPath);
ReSharperProjectSettingsGenerator.WriteSettingsIfMissing(newProjectFilePath);
ProjectSourceCodeWatcherManager.AddSourceCodeWatcherForProject(GetDirectoryPath(newProjectFilePath));
@@ -207,10 +214,8 @@ private static List GenerateNewProjects(IReadOnlyList
continue;
}
- var (additionalProjects, _) = SolutionFileParser.Parse(
- File.ReadAllText(additionalIncludedSolution),
- GetDirectoryPath(additionalIncludedSolution),
- false);
+ var additionalSolution = new SolutionFile(GetDirectoryPath(additionalIncludedSolution), additionalIncludedSolution);
+ var (additionalProjects, _) = SolutionFileParser.Parse(additionalSolution, false);
foreach (var additionalProject in additionalProjects)
{
if (newProjects.Contains(additionalProject))
@@ -238,8 +243,19 @@ private static List GenerateNewProjects(IReadOnlyList
continue;
}
- using var hashAlgorithm = SHA256.Create();
- var projectId = new Guid(hashAlgorithm.ComputeHash(System.Text.Encoding.UTF8.GetBytes(additionalProjectFile)).AsSpan(0, 16)).ToString("d").ToUpperInvariant();
+ string projectId;
+ if (solutionFile.IsXmlSolution)
+ {
+ // XML solutions don't have / need project IDs
+ projectId = string.Empty;
+ }
+ else
+ {
+ using var hashAlgorithm = SHA256.Create();
+ projectId = new Guid(hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(additionalProjectFile)).AsSpan(0, 16)).ToString("d")
+ .ToUpperInvariant();
+ }
+
var additionalProject = new ProjectFile(additionalProjectFile, projectId);
ProjectSourceCodeWatcherManager.AddSourceCodeWatcherForProject(GetDirectoryPath(additionalProject.FilePath));
newProjects.Add(additionalProject);
@@ -257,8 +273,9 @@ private static string RemoveGeneratedProjectsFromSolution(string path, string co
}
var solutionDirectoryPath = GetDirectoryPath(path);
- var (allProjects, _) = SolutionFileParser.Parse(content, solutionDirectoryPath, !GeneratorSettings.IsEnabled);
- return SolutionFileWriter.WriteToText(solutionDirectoryPath, allProjects);
+ var solutionFile = new SolutionFile(solutionDirectoryPath, path);
+ var (allProjects, _) = SolutionFileParser.Parse(solutionFile, !GeneratorSettings.IsEnabled);
+ return SolutionFileWriter.WriteToText(solutionFile, allProjects);
}
private static string GetDirectoryPath(string path)
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileParser.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileParser.cs
new file mode 100644
index 0000000..ae5e52b
--- /dev/null
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileParser.cs
@@ -0,0 +1,73 @@
+#nullable enable
+
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Linq;
+
+namespace UnityVisualStudioSolutionGenerator
+{
+ ///
+ /// Provides methods for parsing Visual Studio solution files in XML format (.slnx).
+ ///
+ public static class XmlSolutionFileParser
+ {
+ ///
+ /// Parses the XML solution file content and gets the project files referenced from the solution.
+ ///
+ /// The XML content of the solution file.
+ /// The absolute directory path of the solution file.
+ ///
+ /// Indicates whether to only include project files generated by Unity or files directly inside the solution directory.
+ ///
+ ///
+ /// A tuple containing the referenced project files and a flag indicating if the source contains duplicate projects.
+ ///
+ public static (IReadOnlyList ProjectFiles, bool SourceContainsDuplicateProjects) Parse(
+ string content,
+ string solutionDirectoryPath,
+ bool onlyIncludeUnityGeneratedProjects)
+ {
+ var projects = GetUsedProjectFiles(content, solutionDirectoryPath);
+ return SolutionFileParser.CreateFilteredProjectsResult(solutionDirectoryPath, onlyIncludeUnityGeneratedProjects, projects);
+ }
+
+ ///
+ /// Parses the specified and gets the project files referenced from the solution.
+ ///
+ /// The solution file to parse.
+ ///
+ /// Indicates whether to only include project files generated by Unity or files directly inside the solution directory.
+ ///
+ ///
+ /// A tuple containing the referenced project files and a flag indicating if the source contains duplicate projects.
+ ///
+ internal static (IReadOnlyList ProjectFiles, bool SourceContainsDuplicateProjects) Parse(
+ SolutionFile solutionFile,
+ bool onlyIncludeUnityGeneratedProjects)
+ {
+ return Parse(File.ReadAllText(solutionFile.SolutionFilePath), solutionFile.SolutionDirectoryPath, onlyIncludeUnityGeneratedProjects);
+ }
+
+ private static IEnumerable GetUsedProjectFiles(string content, string solutionDirectoryPath)
+ {
+ if (!Directory.Exists(solutionDirectoryPath))
+ {
+ LogHelper.LogError($"Generating a solution file inside a directory that doesn't exists. Directory path: {solutionDirectoryPath}");
+ }
+
+ var document = XDocument.Parse(content);
+ foreach (var project in document.Descendants("Project"))
+ {
+ var projectFileRelativePath = project.Attribute("Path")?.Value;
+ if (string.IsNullOrEmpty(projectFileRelativePath))
+ {
+ LogHelper.LogError($"Failed to extract csproj file name from Project: {project}");
+ continue;
+ }
+
+ var projectFilePath = Path.GetFullPath(projectFileRelativePath, solutionDirectoryPath);
+ yield return new ProjectFile(projectFilePath, string.Empty);
+ }
+ }
+ }
+}
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileParser.cs.meta b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileParser.cs.meta
new file mode 100644
index 0000000..16c1970
--- /dev/null
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileParser.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: dd7c1c10fff23c54a85b0ead6032c11e
\ No newline at end of file
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileWriter.cs b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileWriter.cs
new file mode 100644
index 0000000..2732ad7
--- /dev/null
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileWriter.cs
@@ -0,0 +1,51 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Xml;
+
+namespace UnityVisualStudioSolutionGenerator
+{
+ ///
+ /// Provides methods for writing Visual Studio solution files in XML format (.slnx).
+ ///
+ public static class XmlSolutionFileWriter
+ {
+ ///
+ /// Writes a Visual Studio solution file in XML format (.slnx) to the specified writer.
+ ///
+ /// The text writer to write the solution content to.
+ /// The absolute directory path of the solution file.
+ /// All projects that should be included inside the solution.
+ public static void WriteTo(TextWriter writer, string solutionDirectoryPath, IReadOnlyList allProjects)
+ {
+ _ = allProjects ?? throw new ArgumentNullException(nameof(allProjects));
+
+ var settings = new XmlWriterSettings
+ {
+ Indent = true,
+ IndentChars = " ",
+ NewLineChars = SolutionFileWriter.WindowsNewLine,
+ NewLineHandling = NewLineHandling.Replace,
+ OmitXmlDeclaration = true,
+ Encoding = Encoding.UTF8,
+ };
+
+ using var xmlWriter = XmlWriter.Create(writer, settings);
+ xmlWriter.WriteStartElement("Solution");
+
+ foreach (var project in allProjects)
+ {
+ var relative = Path.GetRelativePath(solutionDirectoryPath, project.FilePath);
+ xmlWriter.WriteStartElement("Project");
+ xmlWriter.WriteAttributeString("Path", relative);
+ xmlWriter.WriteEndElement(); // Project
+ }
+
+ xmlWriter.WriteEndElement(); // Solution
+ xmlWriter.Flush();
+ }
+ }
+}
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileWriter.cs.meta b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileWriter.cs.meta
new file mode 100644
index 0000000..eda2b0b
--- /dev/null
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/Editor/XmlSolutionFileWriter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 991bb43b2e22f794880d8001c3655afd
\ No newline at end of file
diff --git a/src/UnityVisualStudioSolutionGenerator/Assets/package.json b/src/UnityVisualStudioSolutionGenerator/Assets/package.json
index 840933b..8a71e49 100644
--- a/src/UnityVisualStudioSolutionGenerator/Assets/package.json
+++ b/src/UnityVisualStudioSolutionGenerator/Assets/package.json
@@ -1,9 +1,9 @@
{
"name": "com.github-joc0de.visual-studio-solution-generator",
"displayName": "Unity Visual Studio Solution Generator",
- "version": "1.1.0",
+ "version": "1.2.0",
"description": "Visual Studio Solution Generator with improved developer productivity especially when working with multi-package unity projects",
- "unity": "2021.2",
+ "unity": "2021.3",
"keywords": [
"editor-extension",
"visual-studio"
@@ -12,7 +12,7 @@
"name": "JoC0de"
},
"dependencies": {
- "com.unity.ide.visualstudio": "2.0.25",
+ "com.unity.ide.visualstudio": "2.0.26",
"com.unity.settings-manager": "2.1.1"
},
"license": "MIT",
diff --git a/tools/resharper-cleanupcode.py b/tools/resharper-cleanupcode.py
index 7516195..e81c516 100644
--- a/tools/resharper-cleanupcode.py
+++ b/tools/resharper-cleanupcode.py
@@ -6,14 +6,17 @@
scriptLocation = os.path.dirname(os.path.realpath(sys.argv[0]))
repositoryRoot = os.path.dirname(scriptLocation)
-solutionFiles = ["src/UnityVisualStudioSolutionGenerator.Tests/UnityVisualStudioSolutionGenerator.Tests.sln"]
+solutionFiles = [["src/UnityVisualStudioSolutionGenerator.Tests/UnityVisualStudioSolutionGenerator.Tests.sln", "src/TestProjects/RootTestProject/RootTestProject.slnx"]]
toolsRoot = repositoryRoot
subprocess.run(["dotnet", "tool", "restore"], cwd = toolsRoot, check = True)
startTime = time.time()
try:
- for solutionFile in solutionFiles:
+ for solutionFileAlternatives in solutionFiles:
+ solutionFile = next((file for file in solutionFileAlternatives if os.path.isfile(os.path.realpath(os.path.join(repositoryRoot, file)))), None)
+ if solutionFile is None:
+ sys.exit(f"can't find the solution file in alternatives: {solutionFileAlternatives}")
relativeSolutionFile = solutionFile
solutionFile = os.path.realpath(os.path.join(repositoryRoot, solutionFile))
if not os.path.isfile(solutionFile):