@@ -29,8 +29,8 @@ namespace JD.Efcpt.Build.Tasks;
2929/// </item>
3030/// <item>
3131/// <description>
32- /// On .NET 10.0 or later, if dnx is available, the task runs <c> dnx <ToolPackageId></c>
33- /// to execute the tool without requiring installation.
32+ /// When the project targets .NET 10.0 or later, the .NET 10+ SDK is installed, and dnx is available,
33+ /// the task runs <c>dnx <ToolPackageId></c> to execute the tool without requiring installation.
3434/// </description>
3535/// </item>
3636/// <item>
@@ -77,6 +77,11 @@ namespace JD.Efcpt.Build.Tasks;
7777/// </remarks>
7878public sealed class RunEfcpt : Task
7979{
80+ /// <summary>
81+ /// Timeout in milliseconds for external process operations (SDK checks, dnx availability).
82+ /// </summary>
83+ private const int ProcessTimeoutMs = 5000 ;
84+
8085 /// <summary>
8186 /// Controls how the efcpt dotnet tool is resolved.
8287 /// </summary>
@@ -119,9 +124,10 @@ public sealed class RunEfcpt : Task
119124 /// </value>
120125 /// <remarks>
121126 /// <para>
122- /// On .NET 10.0 or later, tool restoration is skipped even when this property is <c>true</c>
123- /// because the <c>dnx</c> command handles tool execution directly without requiring prior
124- /// installation. The tool is fetched and run on-demand by the dotnet SDK.
127+ /// When the project targets .NET 10.0 or later and the .NET 10+ SDK is installed, tool restoration
128+ /// is skipped even when this property is <c>true</c> because the <c>dnx</c> command handles tool
129+ /// execution directly without requiring prior installation. The tool is fetched and run on-demand
130+ /// by the dotnet SDK.
125131 /// </para>
126132 /// </remarks>
127133 public string ToolRestore { get ; set ; } = "true" ;
@@ -224,6 +230,15 @@ public sealed class RunEfcpt : Task
224230 /// </value>
225231 public string Provider { get ; set ; } = "mssql" ;
226232
233+ /// <summary>
234+ /// Target framework of the project being built (e.g., "net8.0", "net9.0", "net10.0").
235+ /// </summary>
236+ /// <value>
237+ /// Used to determine whether to use dnx for tool execution on .NET 10+ projects.
238+ /// If empty or not specified, falls back to runtime version detection.
239+ /// </value>
240+ public string TargetFramework { get ; set ; } = "" ;
241+
227242 private readonly record struct ToolResolutionContext (
228243 string ToolPath ,
229244 string ToolMode ,
@@ -234,6 +249,7 @@ private readonly record struct ToolResolutionContext(
234249 string ToolPackageId ,
235250 string WorkingDir ,
236251 string Args ,
252+ string TargetFramework ,
237253 BuildLog Log
238254 ) ;
239255
@@ -255,6 +271,7 @@ private readonly record struct ToolRestoreContext(
255271 string ToolPath ,
256272 string ToolPackageId ,
257273 string ToolVersion ,
274+ string TargetFramework ,
258275 BuildLog Log
259276 ) ;
260277
@@ -267,7 +284,7 @@ BuildLog Log
267284 Args : ctx . Args ,
268285 Cwd : ctx . WorkingDir ,
269286 UseManifest : false ) )
270- . When ( ( in ctx ) => IsDotNet10OrLater ( ) && IsDnxAvailable ( ctx . DotNetExe ) )
287+ . When ( ( in ctx ) => IsDotNet10OrLater ( ctx . TargetFramework ) && IsDotNet10SdkInstalled ( ctx . DotNetExe ) && IsDnxAvailable ( ctx . DotNetExe ) )
271288 . Then ( ( in ctx )
272289 => new ToolInvocation (
273290 Exe : ctx . DotNetExe ,
@@ -297,29 +314,30 @@ private static bool ToolIsAutoOrManifest(ToolResolutionContext ctx) =>
297314 private static readonly Lazy < ActionStrategy < ToolRestoreContext > > ToolRestoreStrategy = new ( ( ) =>
298315 ActionStrategy < ToolRestoreContext > . Create ( )
299316 // Manifest restore: restore tools from local manifest
300- // Skip on .NET 10+ because dnx handles tool execution without installation
301- . When ( static ( in ctx ) => ctx is { UseManifest : true , ShouldRestore : true } && ! IsDotNet10OrLater ( ) )
317+ // Skip when: dnx will be used OR no manifest directory exists
318+ . When ( ( in ctx ) => ctx is { UseManifest : true , ShouldRestore : true , ManifestDir : not null }
319+ && ! ( IsDotNet10OrLater ( ctx . TargetFramework ) && IsDotNet10SdkInstalled ( ctx . DotNetExe ) && IsDnxAvailable ( ctx . DotNetExe ) ) )
302320 . Then ( ( in ctx ) =>
303321 {
304322 var restoreCwd = ctx . ManifestDir ?? ctx . WorkingDir ;
305323 ProcessRunner . RunOrThrow ( ctx . Log , ctx . DotNetExe , "tool restore" , restoreCwd ) ;
306324 } )
307325 // Global restore: update global tool package
308- // Skip on .NET 10+ because dnx handles tool execution without installation
309- . When ( static ( in ctx )
326+ // Skip only when dnx will be used (all three conditions: .NET 10+ target, SDK installed, dnx available)
327+ . When ( ( in ctx )
310328 => ctx is
311329 {
312330 UseManifest : false ,
313331 ShouldRestore : true ,
314332 HasExplicitPath : false ,
315333 HasPackageId : true
316- } && ! IsDotNet10OrLater ( ) )
334+ } && ! ( IsDotNet10OrLater ( ctx . TargetFramework ) && IsDotNet10SdkInstalled ( ctx . DotNetExe ) && IsDnxAvailable ( ctx . DotNetExe ) ) )
317335 . Then ( ( in ctx ) =>
318336 {
319337 var versionArg = string . IsNullOrWhiteSpace ( ctx . ToolVersion ) ? "" : $ " --version \" { ctx . ToolVersion } \" ";
320338 ProcessRunner . RunOrThrow ( ctx . Log , ctx . DotNetExe , $ "tool update --global { ctx . ToolPackageId } { versionArg } ", ctx . WorkingDir ) ;
321339 } )
322- // Default: no restoration needed (includes .NET 10+ with dnx )
340+ // Default: no restoration needed (dnx will be used OR no manifest for manifest mode )
323341 . Default ( static ( in _ ) => { } )
324342 . Build ( ) ) ;
325343
@@ -392,7 +410,7 @@ private bool ExecuteCore(TaskExecutionContext ctx)
392410 // Use the Strategy pattern to resolve tool invocation
393411 var context = new ToolResolutionContext (
394412 ToolPath , mode , manifestDir , forceManifestOnNonWindows ,
395- DotNetExe , ToolCommand , ToolPackageId , workingDir , args , log ) ;
413+ DotNetExe , ToolCommand , ToolPackageId , workingDir , args , TargetFramework , log ) ;
396414
397415 var invocation = ToolResolutionStrategy . Value . Execute ( in context ) ;
398416
@@ -418,6 +436,7 @@ private bool ExecuteCore(TaskExecutionContext ctx)
418436 ToolPath : ToolPath ,
419437 ToolPackageId : ToolPackageId ,
420438 ToolVersion : ToolVersion ,
439+ TargetFramework : TargetFramework ,
421440 Log : log
422441 ) ;
423442
@@ -429,12 +448,106 @@ private bool ExecuteCore(TaskExecutionContext ctx)
429448 }
430449
431450
432- private static bool IsDotNet10OrLater ( )
451+ /// <summary>
452+ /// Checks if the target framework is .NET 10.0 or later.
453+ /// </summary>
454+ /// <param name="targetFramework">The target framework string (e.g., "net8.0", "net10.0").</param>
455+ /// <returns>True if the target framework is .NET 10.0 or later; otherwise false.</returns>
456+ private static bool IsDotNet10OrLater ( string targetFramework )
457+ {
458+ if ( string . IsNullOrWhiteSpace ( targetFramework ) )
459+ return false ;
460+
461+ try
462+ {
463+ // Parse target framework to get major version (e.g., "net8.0" -> 8, "net10.0" -> 10)
464+ if ( ! targetFramework . StartsWith ( "net" , StringComparison . OrdinalIgnoreCase ) )
465+ return false ;
466+
467+ var versionPart = targetFramework [ 3 ..] ;
468+
469+ // Trim at the first '.' or '-' after "net" to handle formats like:
470+ // - "net10.0" -> "10"
471+ // - "net10.0-windows" -> "10"
472+ // - "net10-windows" -> "10"
473+ var dotIndex = versionPart . IndexOf ( '.' ) ;
474+ var hyphenIndex = versionPart . IndexOf ( '-' ) ;
475+
476+ var cutIndex = ( dotIndex >= 0 , hyphenIndex >= 0 ) switch
477+ {
478+ ( true , true ) => Math . Min ( dotIndex , hyphenIndex ) ,
479+ ( true , false ) => dotIndex ,
480+ ( false , true ) => hyphenIndex ,
481+ _ => - 1
482+ } ;
483+
484+ if ( cutIndex > 0 )
485+ versionPart = versionPart [ ..cutIndex ] ;
486+
487+ if ( int . TryParse ( versionPart , out var version ) )
488+ return version >= 10 ;
489+
490+ return false ;
491+ }
492+ catch
493+ {
494+ return false ;
495+ }
496+ }
497+
498+ /// <summary>
499+ /// Checks if .NET SDK version 10 or later is installed.
500+ /// </summary>
501+ /// <param name="dotnetExe">Path to the dotnet executable.</param>
502+ /// <returns>True if .NET 10+ SDK is installed; otherwise false.</returns>
503+ private static bool IsDotNet10SdkInstalled ( string dotnetExe )
433504 {
434505 try
435506 {
436- var version = Environment . Version ;
437- return version . Major >= 10 ;
507+ var psi = new ProcessStartInfo
508+ {
509+ FileName = dotnetExe ,
510+ Arguments = "--list-sdks" ,
511+ RedirectStandardOutput = true ,
512+ RedirectStandardError = true ,
513+ UseShellExecute = false ,
514+ CreateNoWindow = true
515+ } ;
516+
517+ using var p = Process . Start ( psi ) ;
518+ if ( p is null ) return false ;
519+
520+ // Check if process completed within timeout
521+ if ( ! p . WaitForExit ( ProcessTimeoutMs ) )
522+ return false ;
523+
524+ if ( p . ExitCode != 0 )
525+ return false ;
526+
527+ var output = p . StandardOutput . ReadToEnd ( ) ;
528+
529+ // Parse output like "10.0.100 [C:\Program Files\dotnet\sdk]"
530+ // Check if any line starts with "10." or higher
531+ foreach ( var line in output . Split ( new [ ] { "\r \n " , "\n " } , StringSplitOptions . RemoveEmptyEntries ) )
532+ {
533+ var trimmed = line . Trim ( ) ;
534+ if ( string . IsNullOrEmpty ( trimmed ) )
535+ continue ;
536+
537+ // Extract version number (first part before space or bracket)
538+ var spaceIndex = trimmed . IndexOf ( ' ' ) ;
539+ var versionStr = spaceIndex >= 0 ? trimmed . Substring ( 0 , spaceIndex ) : trimmed ;
540+
541+ // Parse major version
542+ var dotIndex = versionStr . IndexOf ( '.' ) ;
543+ if ( dotIndex > 0 && int . TryParse ( versionStr . Substring ( 0 , dotIndex ) , out var major ) )
544+ {
545+ if ( major >= 10 )
546+ return true ;
547+ }
548+ }
549+
550+ return false ;
438551 }
439552 catch
440553 {
@@ -459,7 +572,9 @@ private static bool IsDnxAvailable(string dotnetExe)
459572 using var p = Process . Start ( psi ) ;
460573 if ( p is null ) return false ;
461574
462- p . WaitForExit ( 5000 ) ; // 5 second timeout
575+ if ( ! p . WaitForExit ( ProcessTimeoutMs ) )
576+ return false ;
577+
463578 return p . ExitCode == 0 ;
464579 }
465580 catch
0 commit comments