@@ -15,12 +15,11 @@ namespace Microsoft.DotNet.Cli.Commands.Solution.Add;
15
15
16
16
internal class SolutionAddCommand : CommandBase
17
17
{
18
- private static readonly string [ ] _defaultPlatforms = [ "Any CPU" , "x64" , "x86" ] ;
19
- private static readonly string [ ] _defaultBuildTypes = [ "Debug" , "Release" ] ;
20
18
private readonly string _fileOrDirectory ;
21
19
private readonly bool _inRoot ;
22
20
private readonly IReadOnlyCollection < string > _projects ;
23
21
private readonly string ? _solutionFolderPath ;
22
+ private string _solutionFileFullPath = string . Empty ;
24
23
25
24
private static string GetSolutionFolderPathWithForwardSlashes ( string path )
26
25
{
@@ -29,13 +28,21 @@ private static string GetSolutionFolderPathWithForwardSlashes(string path)
29
28
return "/" + string . Join ( "/" , PathUtility . GetPathWithDirectorySeparator ( path ) . Split ( Path . DirectorySeparatorChar , StringSplitOptions . RemoveEmptyEntries ) ) + "/" ;
30
29
}
31
30
31
+ private static bool IsSolutionFolderPathInDirectoryScope ( string relativePath )
32
+ {
33
+ return ! string . IsNullOrWhiteSpace ( relativePath )
34
+ && ! Path . IsPathRooted ( relativePath ) // This means path is in a different volume
35
+ && ! relativePath . StartsWith ( ".." ) ; // This means path is outside the solution directory
36
+ }
37
+
32
38
public SolutionAddCommand ( ParseResult parseResult ) : base ( parseResult )
33
39
{
34
40
_fileOrDirectory = parseResult . GetValue ( SolutionCommandParser . SlnArgument ) ;
35
41
_projects = ( IReadOnlyCollection < string > ) ( parseResult . GetValue ( SolutionAddCommandParser . ProjectPathArgument ) ?? [ ] ) ;
36
42
_inRoot = parseResult . GetValue ( SolutionAddCommandParser . InRootOption ) ;
37
43
_solutionFolderPath = parseResult . GetValue ( SolutionAddCommandParser . SolutionFolderOption ) ;
38
44
SolutionArgumentValidator . ParseAndValidateArguments ( _fileOrDirectory , _projects , SolutionArgumentValidator . CommandType . Add , _inRoot , _solutionFolderPath ) ;
45
+ _solutionFileFullPath = SlnFileFactory . GetSolutionFileFullPath ( _fileOrDirectory ) ;
39
46
}
40
47
41
48
public override int Execute ( )
@@ -44,115 +51,135 @@ public override int Execute()
44
51
{
45
52
throw new GracefulException ( CliStrings . SpecifyAtLeastOneProjectToAdd ) ;
46
53
}
47
- string solutionFileFullPath = SlnFileFactory . GetSolutionFileFullPath ( _fileOrDirectory ) ;
48
54
49
- try
55
+ // Get project paths from the command line arguments
56
+ PathUtility . EnsureAllPathsExist ( _projects , CliStrings . CouldNotFindProjectOrDirectory , true ) ;
57
+
58
+ IEnumerable < string > fullProjectPaths = _projects . Select ( project =>
50
59
{
51
- PathUtility . EnsureAllPathsExist ( _projects , CliStrings . CouldNotFindProjectOrDirectory , true ) ;
52
- IEnumerable < string > fullProjectPaths = _projects . Select ( project =>
53
- {
54
- var fullPath = Path . GetFullPath ( project ) ;
55
- return Directory . Exists ( fullPath ) ? MsbuildProject . GetProjectFileFromDirectory ( fullPath ) . FullName : fullPath ;
56
- } ) ;
57
- AddProjectsToSolutionAsync ( solutionFileFullPath , fullProjectPaths , CancellationToken . None ) . GetAwaiter ( ) . GetResult ( ) ;
58
- return 0 ;
60
+ var fullPath = Path . GetFullPath ( project ) ;
61
+ return Directory . Exists ( fullPath ) ? MsbuildProject . GetProjectFileFromDirectory ( fullPath ) . FullName : fullPath ;
62
+ } ) ;
63
+
64
+ // Add projects to the solution
65
+ AddProjectsToSolutionAsync ( fullProjectPaths , CancellationToken . None ) . GetAwaiter ( ) . GetResult ( ) ;
66
+ return 0 ;
67
+ }
68
+
69
+ private SolutionFolderModel ? GenerateIntermediateSolutionFoldersForProjectPath ( SolutionModel solution , string relativeProjectPath )
70
+ {
71
+ if ( _inRoot )
72
+ {
73
+ return null ;
59
74
}
60
- catch ( Exception ex ) when ( ex is not GracefulException )
75
+
76
+ string relativeSolutionFolderPath = string . Empty ;
77
+
78
+ if ( string . IsNullOrEmpty ( _solutionFolderPath ) )
61
79
{
80
+ // Generate the solution folder path based on the project path
81
+ relativeSolutionFolderPath = Path . GetDirectoryName ( relativeProjectPath ) ;
82
+
83
+ // If the project is in a folder with the same name as the project, we need to go up one level
84
+ if ( relativeSolutionFolderPath . Split ( Path . DirectorySeparatorChar ) . LastOrDefault ( ) == Path . GetFileNameWithoutExtension ( relativeProjectPath ) )
85
+ {
86
+ relativeSolutionFolderPath = Path . Combine ( [ .. relativeSolutionFolderPath . Split ( Path . DirectorySeparatorChar ) . SkipLast ( 1 ) ] ) ;
87
+ }
88
+
89
+ // If the generated path is outside the solution directory, we need to set it to empty
90
+ if ( ! IsSolutionFolderPathInDirectoryScope ( relativeSolutionFolderPath ) )
62
91
{
63
- if ( ex is SolutionException || ex . InnerException is SolutionException )
64
- {
65
- throw new GracefulException ( CliStrings . InvalidSolutionFormatString , solutionFileFullPath , ex . Message ) ;
66
- }
67
- throw new GracefulException ( ex . Message , ex ) ;
92
+ relativeSolutionFolderPath = string . Empty ;
68
93
}
69
94
}
95
+ else
96
+ {
97
+ // Use the provided solution folder path
98
+ relativeSolutionFolderPath = _solutionFolderPath ;
99
+ }
100
+
101
+ return string . IsNullOrEmpty ( relativeSolutionFolderPath )
102
+ ? null
103
+ : solution . AddFolder ( GetSolutionFolderPathWithForwardSlashes ( relativeSolutionFolderPath ) ) ;
70
104
}
71
105
72
- private async Task AddProjectsToSolutionAsync ( string solutionFileFullPath , IEnumerable < string > projectPaths , CancellationToken cancellationToken )
106
+ private async Task AddProjectsToSolutionAsync ( IEnumerable < string > projectPaths , CancellationToken cancellationToken )
73
107
{
74
- SolutionModel solution = SlnFileFactory . CreateFromFileOrDirectory ( solutionFileFullPath ) ;
108
+ SolutionModel solution = SlnFileFactory . CreateFromFileOrDirectory ( _solutionFileFullPath ) ;
75
109
ISolutionSerializer serializer = solution . SerializerExtension . Serializer ;
110
+
76
111
// set UTF8 BOM encoding for .sln
77
112
if ( serializer is ISolutionSerializer < SlnV12SerializerSettings > v12Serializer )
78
113
{
79
114
solution . SerializerExtension = v12Serializer . CreateModelExtension ( new ( )
80
115
{
81
116
Encoding = new UTF8Encoding ( encoderShouldEmitUTF8Identifier : true )
82
117
} ) ;
118
+
83
119
// Set default configurations and platforms for sln file
84
- foreach ( var platform in _defaultPlatforms )
120
+ foreach ( var platform in SlnFileFactory . DefaultPlatforms )
85
121
{
86
122
solution . AddPlatform ( platform ) ;
87
123
}
88
- foreach ( var buildType in _defaultBuildTypes )
124
+
125
+ foreach ( var buildType in SlnFileFactory . DefaultBuildTypes )
89
126
{
90
127
solution . AddBuildType ( buildType ) ;
91
128
}
92
129
}
93
130
94
- SolutionFolderModel ? solutionFolder = ! _inRoot && ! string . IsNullOrEmpty ( _solutionFolderPath )
95
- ? solution . AddFolder ( GetSolutionFolderPathWithForwardSlashes ( _solutionFolderPath ) )
96
- : null ;
97
-
98
131
foreach ( var projectPath in projectPaths )
99
132
{
100
- string relativePath = Path . GetRelativePath ( Path . GetDirectoryName ( solutionFileFullPath ) , projectPath ) ;
101
- // Add fallback solution folder if relative path does not contain `..`.
102
- string relativeSolutionFolder = relativePath . Split ( Path . DirectorySeparatorChar ) . Any ( p => p == ".." )
103
- ? string . Empty : Path . GetDirectoryName ( relativePath ) ;
104
-
105
- if ( ! _inRoot && solutionFolder is null && ! string . IsNullOrEmpty ( relativeSolutionFolder ) )
106
- {
107
- if ( relativeSolutionFolder . Split ( Path . DirectorySeparatorChar ) . LastOrDefault ( ) == Path . GetFileNameWithoutExtension ( relativePath ) )
108
- {
109
- relativeSolutionFolder = Path . Combine ( [ .. relativeSolutionFolder . Split ( Path . DirectorySeparatorChar ) . SkipLast ( 1 ) ] ) ;
110
- }
111
- if ( ! string . IsNullOrEmpty ( relativeSolutionFolder ) )
112
- {
113
- solutionFolder = solution . AddFolder ( GetSolutionFolderPathWithForwardSlashes ( relativeSolutionFolder ) ) ;
114
- }
115
- }
116
-
117
- try
118
- {
119
- AddProject ( solution , relativePath , projectPath , solutionFolder , serializer ) ;
120
- }
121
- catch ( InvalidProjectFileException ex )
122
- {
123
- Reporter . Error . WriteLine ( string . Format ( CliStrings . InvalidProjectWithExceptionMessage , projectPath , ex . Message ) ) ;
124
- }
125
- catch ( SolutionArgumentException ex ) when ( solution . FindProject ( relativePath ) != null || ex . Type == SolutionErrorType . DuplicateProjectName )
126
- {
127
- Reporter . Output . WriteLine ( CliStrings . SolutionAlreadyContainsProject , solutionFileFullPath , relativePath ) ;
128
- }
133
+ AddProject ( solution , projectPath , serializer ) ;
129
134
}
130
- await serializer . SaveAsync ( solutionFileFullPath , solution , cancellationToken ) ;
135
+
136
+ await serializer . SaveAsync ( _solutionFileFullPath , solution , cancellationToken ) ;
131
137
}
132
138
133
- private static void AddProject ( SolutionModel solution , string solutionRelativeProjectPath , string fullPath , SolutionFolderModel ? solutionFolder , ISolutionSerializer serializer = null )
139
+ private void AddProject ( SolutionModel solution , string fullProjectPath , ISolutionSerializer serializer = null )
134
140
{
141
+ string solutionRelativeProjectPath = Path . GetRelativePath ( Path . GetDirectoryName ( _solutionFileFullPath ) , fullProjectPath ) ;
142
+
135
143
// Open project instance to see if it is a valid project
136
- ProjectRootElement projectRootElement = ProjectRootElement . Open ( fullPath ) ;
144
+ ProjectRootElement projectRootElement ;
145
+ try
146
+ {
147
+ projectRootElement = ProjectRootElement . Open ( fullProjectPath ) ;
148
+ }
149
+ catch ( InvalidProjectFileException ex )
150
+ {
151
+ Reporter . Error . WriteLine ( string . Format ( CliStrings . InvalidProjectWithExceptionMessage , fullProjectPath , ex . Message ) ) ;
152
+ return ;
153
+ }
154
+
137
155
ProjectInstance projectInstance = new ProjectInstance ( projectRootElement ) ;
156
+
157
+ string projectTypeGuid = solution . ProjectTypes . FirstOrDefault ( t => t . Extension == Path . GetExtension ( fullProjectPath ) ) ? . ProjectTypeId . ToString ( )
158
+ ?? projectRootElement . GetProjectTypeGuid ( ) ?? projectInstance . GetDefaultProjectTypeGuid ( ) ;
159
+
160
+ // Generate the solution folder path based on the project path
161
+ SolutionFolderModel ? solutionFolder = GenerateIntermediateSolutionFoldersForProjectPath ( solution , solutionRelativeProjectPath ) ;
162
+
138
163
SolutionProjectModel project ;
164
+
139
165
try
140
166
{
141
- project = solution . AddProject ( solutionRelativeProjectPath , null , solutionFolder ) ;
167
+ project = solution . AddProject ( solutionRelativeProjectPath , projectTypeGuid , solutionFolder ) ;
142
168
}
143
- catch ( SolutionArgumentException ex ) when ( ex . ParamName == "projectTypeName" )
169
+ catch ( SolutionArgumentException ex ) when ( ex . Type == SolutionErrorType . InvalidProjectTypeReference )
144
170
{
145
- // If guid is not identified by vs-solutionpersistence, check in project element itself
146
- var guid = projectRootElement . GetProjectTypeGuid ( ) ?? projectInstance . GetDefaultProjectTypeGuid ( ) ;
147
- if ( string . IsNullOrEmpty ( guid ) )
148
- {
149
- Reporter . Error . WriteLine ( CliStrings . UnsupportedProjectType , fullPath ) ;
150
- return ;
151
- }
152
- project = solution . AddProject ( solutionRelativeProjectPath , guid , solutionFolder ) ;
171
+ Reporter . Error . WriteLine ( CliStrings . UnsupportedProjectType , fullProjectPath ) ;
172
+ return ;
173
+ }
174
+ catch ( SolutionArgumentException ex ) when ( ex . Type == SolutionErrorType . DuplicateProjectName || solution . FindProject ( solutionRelativeProjectPath ) is not null )
175
+ {
176
+ Reporter . Output . WriteLine ( CliStrings . SolutionAlreadyContainsProject , _solutionFileFullPath , solutionRelativeProjectPath ) ;
177
+ return ;
153
178
}
179
+
154
180
// Add settings based on existing project instance
155
181
string projectInstanceId = projectInstance . GetProjectId ( ) ;
182
+
156
183
if ( ! string . IsNullOrEmpty ( projectInstanceId ) && serializer is ISolutionSerializer < SlnV12SerializerSettings > )
157
184
{
158
185
project . Id = new Guid ( projectInstanceId ) ;
@@ -164,7 +191,7 @@ private static void AddProject(SolutionModel solution, string solutionRelativePr
164
191
foreach ( var solutionPlatform in solution . Platforms )
165
192
{
166
193
var projectPlatform = projectInstancePlatforms . FirstOrDefault (
167
- platform => platform . Replace ( " " , string . Empty ) == solutionPlatform . Replace ( " " , string . Empty ) , projectInstancePlatforms . FirstOrDefault ( ) ) ;
194
+ platform => platform . Replace ( " " , string . Empty ) == solutionPlatform . Replace ( " " , string . Empty ) , projectInstancePlatforms . FirstOrDefault ( ) ) ;
168
195
project . AddProjectConfigurationRule ( new ConfigurationRule ( BuildDimension . Platform , "*" , solutionPlatform , projectPlatform ) ) ;
169
196
}
170
197
@@ -174,6 +201,7 @@ private static void AddProject(SolutionModel solution, string solutionRelativePr
174
201
buildType => buildType . Replace ( " " , string . Empty ) == solutionBuildType . Replace ( " " , string . Empty ) , projectInstanceBuildTypes . FirstOrDefault ( ) ) ;
175
202
project . AddProjectConfigurationRule ( new ConfigurationRule ( BuildDimension . BuildType , solutionBuildType , "*" , projectBuildType ) ) ;
176
203
}
204
+
177
205
Reporter . Output . WriteLine ( CliStrings . ProjectAddedToTheSolution , solutionRelativeProjectPath ) ;
178
206
}
179
207
}
0 commit comments