@@ -15,12 +15,11 @@ namespace Microsoft.DotNet.Cli.Commands.Solution.Add;
1515
1616internal class SolutionAddCommand : CommandBase
1717{
18- private static readonly string [ ] _defaultPlatforms = [ "Any CPU" , "x64" , "x86" ] ;
19- private static readonly string [ ] _defaultBuildTypes = [ "Debug" , "Release" ] ;
2018 private readonly string _fileOrDirectory ;
2119 private readonly bool _inRoot ;
2220 private readonly IReadOnlyCollection < string > _projects ;
2321 private readonly string ? _solutionFolderPath ;
22+ private string _solutionFileFullPath = string . Empty ;
2423
2524 private static string GetSolutionFolderPathWithForwardSlashes ( string path )
2625 {
@@ -29,13 +28,21 @@ private static string GetSolutionFolderPathWithForwardSlashes(string path)
2928 return "/" + string . Join ( "/" , PathUtility . GetPathWithDirectorySeparator ( path ) . Split ( Path . DirectorySeparatorChar , StringSplitOptions . RemoveEmptyEntries ) ) + "/" ;
3029 }
3130
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+
3238 public SolutionAddCommand ( ParseResult parseResult ) : base ( parseResult )
3339 {
3440 _fileOrDirectory = parseResult . GetValue ( SolutionCommandParser . SlnArgument ) ;
3541 _projects = ( IReadOnlyCollection < string > ) ( parseResult . GetValue ( SolutionAddCommandParser . ProjectPathArgument ) ?? [ ] ) ;
3642 _inRoot = parseResult . GetValue ( SolutionAddCommandParser . InRootOption ) ;
3743 _solutionFolderPath = parseResult . GetValue ( SolutionAddCommandParser . SolutionFolderOption ) ;
3844 SolutionArgumentValidator . ParseAndValidateArguments ( _fileOrDirectory , _projects , SolutionArgumentValidator . CommandType . Add , _inRoot , _solutionFolderPath ) ;
45+ _solutionFileFullPath = SlnFileFactory . GetSolutionFileFullPath ( _fileOrDirectory ) ;
3946 }
4047
4148 public override int Execute ( )
@@ -44,115 +51,135 @@ public override int Execute()
4451 {
4552 throw new GracefulException ( CliStrings . SpecifyAtLeastOneProjectToAdd ) ;
4653 }
47- string solutionFileFullPath = SlnFileFactory . GetSolutionFileFullPath ( _fileOrDirectory ) ;
4854
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 =>
5059 {
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 ;
5974 }
60- catch ( Exception ex ) when ( ex is not GracefulException )
75+
76+ string relativeSolutionFolderPath = string . Empty ;
77+
78+ if ( string . IsNullOrEmpty ( _solutionFolderPath ) )
6179 {
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 ) )
6291 {
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 ;
6893 }
6994 }
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 ) ) ;
70104 }
71105
72- private async Task AddProjectsToSolutionAsync ( string solutionFileFullPath , IEnumerable < string > projectPaths , CancellationToken cancellationToken )
106+ private async Task AddProjectsToSolutionAsync ( IEnumerable < string > projectPaths , CancellationToken cancellationToken )
73107 {
74- SolutionModel solution = SlnFileFactory . CreateFromFileOrDirectory ( solutionFileFullPath ) ;
108+ SolutionModel solution = SlnFileFactory . CreateFromFileOrDirectory ( _solutionFileFullPath ) ;
75109 ISolutionSerializer serializer = solution . SerializerExtension . Serializer ;
110+
76111 // set UTF8 BOM encoding for .sln
77112 if ( serializer is ISolutionSerializer < SlnV12SerializerSettings > v12Serializer )
78113 {
79114 solution . SerializerExtension = v12Serializer . CreateModelExtension ( new ( )
80115 {
81116 Encoding = new UTF8Encoding ( encoderShouldEmitUTF8Identifier : true )
82117 } ) ;
118+
83119 // Set default configurations and platforms for sln file
84- foreach ( var platform in _defaultPlatforms )
120+ foreach ( var platform in SlnFileFactory . DefaultPlatforms )
85121 {
86122 solution . AddPlatform ( platform ) ;
87123 }
88- foreach ( var buildType in _defaultBuildTypes )
124+
125+ foreach ( var buildType in SlnFileFactory . DefaultBuildTypes )
89126 {
90127 solution . AddBuildType ( buildType ) ;
91128 }
92129 }
93130
94- SolutionFolderModel ? solutionFolder = ! _inRoot && ! string . IsNullOrEmpty ( _solutionFolderPath )
95- ? solution . AddFolder ( GetSolutionFolderPathWithForwardSlashes ( _solutionFolderPath ) )
96- : null ;
97-
98131 foreach ( var projectPath in projectPaths )
99132 {
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 ) ;
129134 }
130- await serializer . SaveAsync ( solutionFileFullPath , solution , cancellationToken ) ;
135+
136+ await serializer . SaveAsync ( _solutionFileFullPath , solution , cancellationToken ) ;
131137 }
132138
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 )
134140 {
141+ string solutionRelativeProjectPath = Path . GetRelativePath ( Path . GetDirectoryName ( _solutionFileFullPath ) , fullProjectPath ) ;
142+
135143 // 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+
137155 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+
138163 SolutionProjectModel project ;
164+
139165 try
140166 {
141- project = solution . AddProject ( solutionRelativeProjectPath , null , solutionFolder ) ;
167+ project = solution . AddProject ( solutionRelativeProjectPath , projectTypeGuid , solutionFolder ) ;
142168 }
143- catch ( SolutionArgumentException ex ) when ( ex . ParamName == "projectTypeName" )
169+ catch ( SolutionArgumentException ex ) when ( ex . Type == SolutionErrorType . InvalidProjectTypeReference )
144170 {
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 ;
153178 }
179+
154180 // Add settings based on existing project instance
155181 string projectInstanceId = projectInstance . GetProjectId ( ) ;
182+
156183 if ( ! string . IsNullOrEmpty ( projectInstanceId ) && serializer is ISolutionSerializer < SlnV12SerializerSettings > )
157184 {
158185 project . Id = new Guid ( projectInstanceId ) ;
@@ -164,7 +191,7 @@ private static void AddProject(SolutionModel solution, string solutionRelativePr
164191 foreach ( var solutionPlatform in solution . Platforms )
165192 {
166193 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 ( ) ) ;
168195 project . AddProjectConfigurationRule ( new ConfigurationRule ( BuildDimension . Platform , "*" , solutionPlatform , projectPlatform ) ) ;
169196 }
170197
@@ -174,6 +201,7 @@ private static void AddProject(SolutionModel solution, string solutionRelativePr
174201 buildType => buildType . Replace ( " " , string . Empty ) == solutionBuildType . Replace ( " " , string . Empty ) , projectInstanceBuildTypes . FirstOrDefault ( ) ) ;
175202 project . AddProjectConfigurationRule ( new ConfigurationRule ( BuildDimension . BuildType , solutionBuildType , "*" , projectBuildType ) ) ;
176203 }
204+
177205 Reporter . Output . WriteLine ( CliStrings . ProjectAddedToTheSolution , solutionRelativeProjectPath ) ;
178206 }
179207}
0 commit comments