Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"path": "sdk.slnx",
"projects": [
"src\\Dotnet.Watch\\dotnet-watch\\dotnet-watch.csproj",
"src\\Cli\\dn\\dn.csproj",
"src\\Cli\\dn\\dn-native-debug.vcxproj",
"src\\Cli\\dotnet\\dotnet.csproj",
"src\\Cli\\Microsoft.DotNet.Cli.Utils\\Microsoft.DotNet.Cli.Utils.csproj",
"test\\dotnet-new.IntegrationTests\\dotnet-new.IntegrationTests.csproj",
Expand Down
3 changes: 3 additions & 0 deletions sdk.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
<Project Path="src/Dotnet.Watch/Web.Middleware/Microsoft.DotNet.HotReload.Web.Middleware.shproj" />
</Folder>
<Folder Name="/src/Cli/">
<Project Path="src/Cli/dn/dn.csproj" />
<Project Path="src/Cli/dn/dn-native-debug.vcxproj" Id="e9a0b5d7-2f4a-4c8e-9d3b-1a6f5e8c7d2a" />
<Project Path="src/Cli/dotnet-aot/dotnet-aot.csproj" />
<Project Path="src/Cli/dotnet/dotnet.csproj" />
<Project Path="src/Cli/Microsoft.DotNet.Cli.CommandLine/Microsoft.DotNet.Cli.CommandLine.csproj" />
<Project Path="src/Cli/Microsoft.DotNet.Cli.CoreUtils/Microsoft.DotNet.Cli.CoreUtils.csproj" />
Expand Down
5 changes: 5 additions & 0 deletions src/Cli/dn/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Generated by PublishAotForDebug MSBuild target with machine-specific paths
Properties/launchSettings.json
debug-dn.cmd
dn-native-debug.vcxproj.user
dn.csproj.Backup.tmp
178 changes: 178 additions & 0 deletions src/Cli/dn/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;

namespace Microsoft.DotNet.Cli;

partial class Program
{
[LibraryImport("dotnet-aot", EntryPoint = "dotnet_execute")]
private static partial int DotnetExecute(
nint hostPath,
nint dotnetRoot,
nint sdkDir,
nint hostfxrPath,
int argc,
nint argv);

static unsafe int Main(string[] args)
{
string hostPath = Environment.ProcessPath!;
string baseDir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
string dotnetRoot = ResolveDotnetRoot();
string sdkDir = baseDir;
string hostfxrPath = ResolveHostfxrPath(dotnetRoot);

// Marshal argv to native platform strings (UTF-16 on Windows, UTF-8 on Unix)
// to match hostfxr's char_t definition used by PlatformStringMarshaller
// in dotnet-aot.dll.
nint* nativeArgv = stackalloc nint[args.Length];
Comment thread
JeremyKuhne marked this conversation as resolved.
try
{
for (int i = 0; i < args.Length; i++)
{
nativeArgv[i] = MarshalStringToNative(args[i]);
}

nint hpNative = MarshalStringToNative(hostPath);
nint drNative = MarshalStringToNative(dotnetRoot);
nint sdNative = MarshalStringToNative(sdkDir);
nint hfNative = MarshalStringToNative(hostfxrPath);

try
{
return DotnetExecute(
hpNative,
drNative,
sdNative,
hfNative,
args.Length,
(nint)nativeArgv);
}
finally
{
Marshal.FreeCoTaskMem(hpNative);
Marshal.FreeCoTaskMem(drNative);
Marshal.FreeCoTaskMem(sdNative);
Marshal.FreeCoTaskMem(hfNative);
}
}
finally
{
for (int i = 0; i < args.Length; i++)
{
if (nativeArgv[i] != 0)
{
Marshal.FreeCoTaskMem(nativeArgv[i]);
}
}
}
}

/// <summary>
/// Resolves the .NET installation root directory, mimicking muxer behavior.
/// </summary>
private static string ResolveDotnetRoot()
{
// Check DOTNET_ROOT first (standard on all platforms)
string? dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT");
if (!string.IsNullOrEmpty(dotnetRoot) && Directory.Exists(dotnetRoot))
{
return dotnetRoot;
}

// On Windows, also check the architecture-specific variant
if (OperatingSystem.IsWindows())
{
string archVar = RuntimeInformation.ProcessArchitecture switch
{
Architecture.X64 => "DOTNET_ROOT(x64)",
Architecture.X86 => "DOTNET_ROOT(x86)",
Architecture.Arm64 => "DOTNET_ROOT(ARM64)",
_ => ""
};

if (!string.IsNullOrEmpty(archVar))
{
dotnetRoot = Environment.GetEnvironmentVariable(archVar);
if (!string.IsNullOrEmpty(dotnetRoot) && Directory.Exists(dotnetRoot))
{
return dotnetRoot;
}
}
}

// Fall back to resolving from the process path
string? processPath = Environment.ProcessPath;
if (processPath is not null)
{
string? processDir = Path.GetDirectoryName(processPath);
if (processDir is not null)
{
// Walk up looking for a directory with dotnet(.exe)
string? candidate = processDir;
while (candidate is not null)
{
if (File.Exists(Path.Combine(candidate, "dotnet" + (OperatingSystem.IsWindows() ? ".exe" : ""))))
{
return candidate;
}
candidate = Path.GetDirectoryName(candidate);
}
}
}

// Last resort: assume relative to AppContext.BaseDirectory
return Path.GetDirectoryName(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar)) ?? AppContext.BaseDirectory;
}

/// <summary>
/// Finds the hostfxr library path under the given .NET root.
/// </summary>
private static string ResolveHostfxrPath(string dotnetRoot)
{
string fxrDir = Path.Combine(dotnetRoot, "host", "fxr");
if (!Directory.Exists(fxrDir))
{
return string.Empty;
}

// Pick the highest version directory by parsing version numbers
string? latestFxr = Directory.GetDirectories(fxrDir)
.Select(path => new
{
Path = path,
Version = Version.TryParse(Path.GetFileName(path), out Version? version) ? version : null
})
.Where(candidate => candidate.Version is not null)
.OrderByDescending(candidate => candidate.Version)
.Select(candidate => candidate.Path)
.FirstOrDefault();

if (latestFxr is null)
{
return string.Empty;
}

string hostfxrName = OperatingSystem.IsWindows()
? "hostfxr.dll"
: OperatingSystem.IsMacOS()
? "libhostfxr.dylib"
: "libhostfxr.so";

string hostfxrPath = Path.Combine(latestFxr, hostfxrName);
return File.Exists(hostfxrPath) ? hostfxrPath : string.Empty;
}

/// <summary>
/// Marshals a string to a native platform string (UTF-16 on Windows, UTF-8 on Unix)
/// to match hostfxr's char_t definition.
/// </summary>
private static nint MarshalStringToNative(string value)
{
return OperatingSystem.IsWindows()
? Marshal.StringToCoTaskMemUni(value)
: Marshal.StringToCoTaskMemUTF8(value);
}
}
164 changes: 164 additions & 0 deletions src/Cli/dn/dn-native-debug.vcxproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Makefile stub project for debugging NativeAOT dn.exe under Visual Studio's
native debugger. This project has NO C++ source files and performs NO C++
compilation. It exists solely to provide an F5 launch target that uses the
native debugger engine (WindowsLocalDebugger), which correctly binds C#
breakpoints in AOT-compiled code via the native PDB.

WHY A SEPARATE PROJECT IS NEEDED:
The .NET project system (dn.csproj) uses launchSettings.json profiles, which
always select the managed debugger as the primary engine. There is no
"commandName" value in launchSettings.json that selects native-only debugging.
The "nativeDebugging" flag enables mixed-mode (managed primary + native addon),
but the managed engine cannot bind C# breakpoints in code that has no IL
(NativeAOT output). Only the native debugger can map C# source lines to native
addresses via the PDB generated by the ILC compiler.

The C++ project system honors DebuggerFlavor=WindowsLocalDebugger which uses
the native debugger as primary, allowing C# breakpoints in AOT code to bind.

ALTERNATIVES CONSIDERED:
- launchSettings.json profile: No commandName selects native-only debugging
- .csproj.user DebuggerFlavor: Ignored by .NET project system
- devenv /debugexe: Opens new VS instance, loses project context
- launch.vs.json: Only works in "Open Folder" mode, not Solution F5

USAGE:
1. Open cli.slnf (or sdk.slnx) in Visual Studio
2. Set "dn-native-debug" as the startup project
3. Set breakpoints in AOT source files (NativeEntryPoint.cs, ManagedHost.cs, etc.)
4. Press F5

For debugging the managed Layer 3 code (dotnet.dll after CLR loads), use the
dn.csproj project with its launchSettings.json profile instead.
-->
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<!--
This project requires the Visual Studio C++ workload (VCTargetsPath) and only
works on Windows. When built outside VS or on non-Windows platforms (e.g., CI),
it becomes a no-op.
-->
<PropertyGroup>
<_HasVCTargets Condition="'$(VCTargetsPath)' != '' and Exists('$(VCTargetsPath)\Microsoft.Cpp.Default.props')">true</_HasVCTargets>
</PropertyGroup>

<!--
Early exit: define empty Build/Clean/Rebuild/Restore targets when C++ tooling
is unavailable. Also define NuGet restore stubs so that solution-level restore
(which calls _IsProjectRestoreSupported on every project) recognizes this project
as valid rather than emitting NU1503 (which TreatWarningsAsErrors promotes to an error).
When VCTargets IS available, Microsoft.Cpp.targets imports NuGet.targets which
redefines these targets with real implementations.
-->
<Target Name="Build" Condition="'$(_HasVCTargets)' != 'true'" />
<Target Name="Clean" Condition="'$(_HasVCTargets)' != 'true'" />
<Target Name="Rebuild" Condition="'$(_HasVCTargets)' != 'true'" />
<Target Name="Restore" Condition="'$(_HasVCTargets)' != 'true'" />
<Target Name="_IsProjectRestoreSupported" Returns="@(_ValidProjectsForRestore)">
<ItemGroup>
<_ValidProjectsForRestore Include="$(MSBuildProjectFullPath)" />
</ItemGroup>
</Target>
<Target Name="_GetRestoreProjectPathItems" />
<Target Name="_GenerateRestoreGraphProjectEntry" Returns="@(_RestoreGraphEntry)" />
<Target Name="_GenerateProjectRestoreGraph" Returns="@(_RestoreGraphEntry)" />

<!-- Everything below is conditional on having the C++ toolset -->

<ItemGroup Label="ProjectConfigurations" Condition="'$(_HasVCTargets)' == 'true'">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>

<PropertyGroup Label="Globals" Condition="'$(_HasVCTargets)' == 'true'">
<ProjectGuid>{E9A0B5D7-2F4A-4C8E-9D3B-1A6F5E8C7D2A}</ProjectGuid>
<RootNamespace>dn-native-debug</RootNamespace>
<ProjectName>dn-native-debug</ProjectName>
<!-- Minimum VS toolset - actual C++ compilation is not performed -->
<VCProjectVersion>17.0</VCProjectVersion>
</PropertyGroup>

<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" Condition="'$(_HasVCTargets)' == 'true'" />

<PropertyGroup Label="Configuration" Condition="'$(_HasVCTargets)' == 'true'">
<!-- Makefile type: no C++ compiler is invoked, only NMake commands run -->
<ConfigurationType>Makefile</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>

<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" Condition="'$(_HasVCTargets)' == 'true'" />

<!--
Compute paths to the repo's dotnet CLI and the published AOT binary.
These use MSBuild path utilities so they work on any developer machine.
-->
<PropertyGroup Condition="'$(_HasVCTargets)' == 'true'">
<_RepoRoot>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', '..', '..', '..'))</_RepoRoot>
<_DotNetTool>$([MSBuild]::NormalizePath('$(_RepoRoot)', '.dotnet', 'dotnet.exe'))</_DotNetTool>
<_DnPublishDir>$([MSBuild]::NormalizePath('$(_RepoRoot)', 'artifacts', 'bin', 'dn', '$(Configuration)', 'net11.0', 'win-x64', 'publish'))</_DnPublishDir>
<_DnExePath>$([MSBuild]::NormalizePath('$(_DnPublishDir)', 'dn.exe'))</_DnExePath>
<_DotnetRootPath>$([MSBuild]::NormalizePath('$(_RepoRoot)', '.dotnet'))</_DotnetRootPath>
</PropertyGroup>

<!--
NMake build commands: publish all three layers (AOT exe, AOT shared lib,
managed DLL) and assemble them in the publish directory.
-->
<PropertyGroup Condition="'$(_HasVCTargets)' == 'true'">
<NMakeBuildCommandLine>"$(_DotNetTool)" publish "$(MSBuildThisFileDirectory)..\dotnet-aot\dotnet-aot.csproj" -r win-x64 -c $(Configuration) &amp;&amp; "$(_DotNetTool)" publish "$(MSBuildThisFileDirectory)dn.csproj" -r win-x64 -c $(Configuration) &amp;&amp; "$(_DotNetTool)" build "$(MSBuildThisFileDirectory)..\dotnet\dotnet.csproj" -c $(Configuration) &amp;&amp; copy /Y "$(_RepoRoot)\artifacts\bin\dotnet-aot\$(Configuration)\net11.0\win-x64\publish\dotnet-aot.dll" "$(_DnPublishDir)\" &amp;&amp; xcopy "$(_RepoRoot)\artifacts\bin\dotnet\$(Configuration)\net11.0\*" "$(_DnPublishDir)\" /S /Y /Q</NMakeBuildCommandLine>
<NMakeCleanCommandLine>if exist "$(_DnPublishDir)" rd /s /q "$(_DnPublishDir)"</NMakeCleanCommandLine>
Comment thread
JeremyKuhne marked this conversation as resolved.
<NMakeReBuildCommandLine>$(NMakeCleanCommandLine) &amp;&amp; $(NMakeBuildCommandLine)</NMakeReBuildCommandLine>
<NMakeOutput>$(_DnExePath)</NMakeOutput>
</PropertyGroup>

<!--
Native debugger launch settings.
DebuggerFlavor=WindowsLocalDebugger tells VS to use the native debugger as
primary. This is the key property that the .NET project system cannot provide.
C# breakpoints set in AOT-compiled source files bind via the native PDB.
-->
<PropertyGroup Condition="'$(_HasVCTargets)' == 'true'">
<LocalDebuggerCommand>$(_DnExePath)</LocalDebuggerCommand>
<LocalDebuggerCommandArguments>--info</LocalDebuggerCommandArguments>
<LocalDebuggerWorkingDirectory>$(_DnPublishDir)</LocalDebuggerWorkingDirectory>
<LocalDebuggerEnvironment>DOTNET_ROOT=$(_DotnetRootPath)</LocalDebuggerEnvironment>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>

<!--
Include AOT source files as "None" items so they appear in Solution Explorer
under this project. This makes it easy to open them and set breakpoints
without navigating to another project. The files are not compiled by this
project (Makefile projects don't compile anything).
-->
<ItemGroup Condition="'$(_HasVCTargets)' == 'true'">
<None Include="..\dotnet-aot\NativeEntryPoint.cs" Link="AOT Sources\NativeEntryPoint.cs" />
<None Include="..\dotnet-aot\ManagedHost.cs" Link="AOT Sources\ManagedHost.cs" />
<None Include="Program.cs" Link="AOT Sources\dn.Program.cs" />
<None Include="..\dotnet\Program.cs" Link="Managed Sources\dotnet.Program.cs" />
<None Include="..\dotnet\CommandLineInfo.cs" Link="AOT Sources\CommandLineInfo.cs" />
<None Include="debug-dn.cmd" />
</ItemGroup>

<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" Condition="'$(_HasVCTargets)' == 'true'" />

<!--
No-op targets for Arcade build phases that don't apply to this debug-only
project. Arcade's Build.proj dispatches Pack, Test, IntegrationTest, and
PerformanceTest to every project in the solution; the C++ project system
doesn't define them, so we must provide empty stubs to avoid MSB4057.
-->
<Target Name="Pack" />
<Target Name="Test" />
<Target Name="IntegrationTest" />
<Target Name="PerformanceTest" />

</Project>
Loading
Loading