@@ -203,6 +203,73 @@ static string JoinArguments (string[] args)
203203 }
204204#endif
205205
206+ /// <summary>
207+ /// Throws <see cref="InvalidOperationException"/> when <paramref name="exitCode"/> is non-zero.
208+ /// Includes stderr/stdout context in the message when available.
209+ /// </summary>
210+ public static void ThrowIfFailed ( int exitCode , string command , string ? stderr = null , string ? stdout = null )
211+ {
212+ if ( exitCode == 0 )
213+ return ;
214+
215+ var message = $ "'{ command } ' failed with exit code { exitCode } .";
216+
217+ if ( ! string . IsNullOrEmpty ( stderr ) )
218+ message += $ " stderr:{ Environment . NewLine } { stderr ! . Trim ( ) } ";
219+ if ( ! string . IsNullOrEmpty ( stdout ) )
220+ message += $ " stdout:{ Environment . NewLine } { stdout ! . Trim ( ) } ";
221+
222+ throw new InvalidOperationException ( message ) ;
223+ }
224+
225+ /// <summary>
226+ /// Validates that <paramref name="value"/> is not null or empty.
227+ /// Throws <see cref="ArgumentNullException"/> for null values and
228+ /// <see cref="ArgumentException"/> for empty strings.
229+ /// </summary>
230+ public static void ValidateNotNullOrEmpty ( string ? value , string paramName )
231+ {
232+ if ( value is null )
233+ throw new ArgumentNullException ( paramName ) ;
234+ if ( value . Length == 0 )
235+ throw new ArgumentException ( "Value cannot be an empty string." , paramName ) ;
236+ }
237+
238+ /// <summary>
239+ /// Searches versioned cmdline-tools directories (descending) and "latest" for a specific tool binary.
240+ /// Falls back to the legacy tools/bin path. Returns null if not found.
241+ /// </summary>
242+ public static string ? FindCmdlineTool ( string sdkPath , string toolName , string extension )
243+ {
244+ var cmdlineToolsDir = Path . Combine ( sdkPath , "cmdline-tools" ) ;
245+
246+ if ( Directory . Exists ( cmdlineToolsDir ) ) {
247+ var subdirs = new List < ( string name , Version ? version ) > ( ) ;
248+ foreach ( var dir in Directory . GetDirectories ( cmdlineToolsDir ) ) {
249+ var name = Path . GetFileName ( dir ) ;
250+ if ( string . IsNullOrEmpty ( name ) || name == "latest" )
251+ continue ;
252+ Version . TryParse ( name , out var v ) ;
253+ subdirs . Add ( ( name , v ?? new Version ( 0 , 0 ) ) ) ;
254+ }
255+ subdirs . Sort ( ( a , b ) => b . version ! . CompareTo ( a . version ) ) ;
256+
257+ // Check versioned directories first (highest version first), then "latest"
258+ foreach ( var ( name , _) in subdirs ) {
259+ var toolPath = Path . Combine ( cmdlineToolsDir , name , "bin" , toolName + extension ) ;
260+ if ( File . Exists ( toolPath ) )
261+ return toolPath ;
262+ }
263+ var latestPath = Path . Combine ( cmdlineToolsDir , "latest" , "bin" , toolName + extension ) ;
264+ if ( File . Exists ( latestPath ) )
265+ return latestPath ;
266+ }
267+
268+ // Legacy fallback: tools/bin/<tool>
269+ var legacyPath = Path . Combine ( sdkPath , "tools" , "bin" , toolName + extension ) ;
270+ return File . Exists ( legacyPath ) ? legacyPath : null ;
271+ }
272+
206273 internal static IEnumerable < string > FindExecutablesInPath ( string executable )
207274 {
208275 var path = Environment . GetEnvironmentVariable ( EnvironmentVariableNames . Path ) ?? "" ;
0 commit comments