First pass at debug adapter support

This commit is contained in:
Joshua "Yoshi" Askharoun
2025-11-30 13:37:39 -06:00
parent 75efccc271
commit 22e78658ce
18 changed files with 901 additions and 12 deletions
@@ -0,0 +1,147 @@
using Microsoft.Iris.Debug;
using Microsoft.Iris.Debug.Data;
using OmniSharp.Extensions.DebugAdapter.Client;
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
using OmniSharp.Extensions.DebugAdapter.Protocol.Models;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using IrisBreakpoint = Microsoft.Iris.Debug.Data.Breakpoint;
namespace Microsoft.Iris.DebugAdapter.Client;
public class IrisDebugAdapterClient : IDebuggerClient, IRemoteDebuggerState, IDisposable
{
private Stream _inputStream;
private Stream _outputStream;
private DebugAdapterClient? _debugAdapter;
private InterpreterCommand _debuggerCommand;
public InterpreterCommand DebuggerCommand
{
get => _debuggerCommand;
set
{
_debuggerCommand = value;
_ = value switch
{
InterpreterCommand.Break => _debugAdapter.RequestPause(new()),
InterpreterCommand.Continue => _debugAdapter.RequestContinue(new()),
InterpreterCommand.Step => _debugAdapter.RequestNext(new()),
_ => Task.CompletedTask
};
}
}
public string ConnectionString { get; }
public event Action<InterpreterCommand> InterpreterStateChanged;
public event EventHandler<InterpreterInstruction> InterpreterDecode;
public event EventHandler<InterpreterEntry> InterpreterExecute;
public event Action<string> DispatcherStep;
public event Action<IDebuggerState, object> Connected;
public IrisDebugAdapterClient(Stream inputStream, Stream outputStream)
{
_inputStream = inputStream;
_outputStream = outputStream;
}
public IrisDebugAdapterClient(string connectionString)
{
ConnectionString = connectionString;
}
public async Task StartAsync()
{
if (ConnectionString is not null)
{
ConnectionStringHelper.CreateFromString(ConnectionString, out _inputStream, out _outputStream);
}
_debugAdapter = await DebugAdapterClient.From(options =>
{
options
.WithInput(_inputStream)
.WithOutput(_outputStream)
.OnInitialize((server, _, cancellationToken) =>
{
return Task.CompletedTask;
})
.OnInitialized((_, _, response, _) =>
{
return Task.CompletedTask;
})
.OnContinued(args =>
{
_debuggerCommand = InterpreterCommand.Continue;
InterpreterStateChanged?.Invoke(_debuggerCommand);
})
.OnStopped(args =>
{
_debuggerCommand = InterpreterCommand.Break;
InterpreterStateChanged?.Invoke(_debuggerCommand);
})
;
}).ConfigureAwait(false);
}
public void Dispose()
{
_debugAdapter?.Dispose();
_inputStream.Dispose();
_outputStream.Dispose();
}
public void UpdateBreakpoint(IrisBreakpoint irisBreakpoint)
{
if (irisBreakpoint.Line > 0)
{
List<SourceBreakpoint> sourceBreakpoints = [
new()
{
Line = irisBreakpoint.Line,
Column = irisBreakpoint.Column > 0
? irisBreakpoint.Column
: null
}
];
_ = _debugAdapter.SetBreakpoints(new()
{
Breakpoints = sourceBreakpoints
});
}
else if (irisBreakpoint.Offset != uint.MaxValue)
{
// Instruction breakpoint
List<InstructionBreakpoint> instructionBreakpoints = [
new()
{
InstructionReference = $"{irisBreakpoint.Uri}@0x{irisBreakpoint.Offset:X}",
//Offset = irisBreakpoint.Offset,
}
];
_ = _debugAdapter.SetInstructionBreakpoints(new()
{
Breakpoints = instructionBreakpoints
});
}
}
public void RequestLineNumberTable(string uri, Action<MarkupLineNumberEntry[]> callback)
{
}
public void Start()
{
StartAsync().RunSynchronously();
Connected?.Invoke(this, EventArgs.Empty);
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net6.0;net6.0-windows10.0.22000</TargetFrameworks>
<LangVersion>12</LangVersion>
<RootNamespace>Microsoft.Iris.DebugAdapter.Client</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OmniSharp.Extensions.DebugAdapter.Client" Version="0.19.9" />
<ProjectReference Include="..\..\libs\MicrosoftIris\UIX\UIX.csproj" />
<ProjectReference Include="..\UIX.DebugAdapter.Shared\UIX.DebugAdapter.Shared.csproj" />
<PackageReference Include="PolySharp" Version="1.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,84 @@
using Microsoft.Iris.Debug;
using Microsoft.Iris.Debug.Data;
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
using System;
namespace Microsoft.Iris.DebugAdapter.Server;
public class DapDebuggerServer : IDebuggerServer, IRemoteDebuggerState
{
private readonly IrisDebugServerOptions _debugAdapterOptions;
private IrisDebugAdapterServer? _debugAdapter;
private InterpreterCommand _debuggerCommand;
public InterpreterCommand DebuggerCommand
{
get => _debuggerCommand;
set
{
_debuggerCommand = value;
if (_debugAdapter?.Server is null)
return;
if (value is InterpreterCommand.Continue)
{
_debugAdapter.Server.SendContinued(new()
{
ThreadId = Environment.CurrentManagedThreadId,
});
}
else if (value is InterpreterCommand.Break)
{
_debugAdapter.Server.SendStopped(new()
{
Reason = new("unknown"),
ThreadId = Environment.CurrentManagedThreadId,
});
}
}
}
public string ConnectionString { get; }
public event Action<IDebuggerState, object>? Connected;
public DapDebuggerServer(string connectionString, IrisDebugServerOptions options)
{
ConnectionString = connectionString;
_debugAdapterOptions = options;
}
public void LogDispatcher(string message)
{
}
public void LogInterpreterDecode(object context, InterpreterInstruction instruction)
{
}
public void LogInterpreterExecute(object context, InterpreterEntry entry)
{
}
public MarkupLineNumberEntry[] OnLineNumberTableRequested(string uri)
{
return [];
}
public void Start()
{
ConnectionStringHelper.CreateFromString(ConnectionString, out var input, out var output);
_debugAdapter = new(input, output, _debugAdapterOptions);
_debugAdapter.StartAsync().RunSynchronously();
Connected?.Invoke(this, EventArgs.Empty);
}
public void WaitForContinue()
{
while (DebuggerCommand is InterpreterCommand.Break) ;
}
}
@@ -0,0 +1,76 @@
using Microsoft.Iris.Debug.Symbols;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using DapBreakpoint = OmniSharp.Extensions.DebugAdapter.Protocol.Models.Breakpoint;
using IrisBreakpoint = Microsoft.Iris.Debug.Data.Breakpoint;
namespace Microsoft.Iris.DebugAdapter.Server.Handlers;
internal class BreakpointHandler(DebugSymbolResolver symbolResolver)
: ISetInstructionBreakpointsHandler, ISetBreakpointsHandler
{
public Task<SetInstructionBreakpointsResponse> Handle(SetInstructionBreakpointsArguments request, CancellationToken cancellationToken)
{
List<DapBreakpoint> dapBreakpoints = [];
foreach (var requestedBreakpoint in request.Breakpoints)
{
}
SetInstructionBreakpointsResponse response = new()
{
Breakpoints = dapBreakpoints
};
return Task.FromResult(response);
}
public Task<SetBreakpointsResponse> Handle(SetBreakpointsArguments request, CancellationToken cancellationToken)
{
// Debug symbols are required for setting source (line:column) breakpoints
var fsym = symbolResolver?.GetForFile(request.Source.Path);
if (fsym is null)
return Task.FromResult(new SetBreakpointsResponse());
List<DapBreakpoint> dapBreakpoints = [];
// TODO: Clear existing breakpoints for file
foreach (var requestedBreakpoint in request.Breakpoints ?? [])
{
SourceMap.Entry location;
if (requestedBreakpoint.Column is not null)
{
SourcePosition position = new(requestedBreakpoint.Line, requestedBreakpoint.Column.Value);
location = fsym.SourceMap.GetLocationFromPosition(position);
}
else
{
// Get first location that contains this line
location = fsym.SourceMap.GetLocationFromLine(requestedBreakpoint.Line);
}
IrisBreakpoint irisBreakpoint = new(fsym.CompiledFileName, location.Offset);
Application.DebugSettings.Breakpoints.Add(irisBreakpoint);
DapBreakpoint dapBreakpoint = new()
{
Line = location.Span.Start.Line,
Column = location.Span.Start.Column,
EndLine = location.Span.End.Line,
EndColumn = location.Span.End.Column,
};
dapBreakpoints.Add(dapBreakpoint);
}
SetBreakpointsResponse response = new()
{
Breakpoints = dapBreakpoints
};
return Task.FromResult(response);
}
}
@@ -0,0 +1,58 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Iris.Debug;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
namespace Microsoft.Iris.DebugAdapter.Server.Handlers;
internal class ContinueHandler : ContinueHandlerBase
{
private readonly IDebuggerServer _debugService;
public ContinueHandler(IDebuggerServer debugService) => _debugService = debugService;
public override Task<ContinueResponse> Handle(ContinueArguments request, CancellationToken cancellationToken)
{
_debugService.DebuggerCommand = Debug.Data.InterpreterCommand.Continue;
return Task.FromResult(new ContinueResponse());
}
}
internal class NextHandler : NextHandlerBase
{
private readonly IDebuggerServer _debugService;
public NextHandler(IDebuggerServer debugService) => _debugService = debugService;
public override Task<NextResponse> Handle(NextArguments request, CancellationToken cancellationToken)
{
_debugService.DebuggerCommand = Debug.Data.InterpreterCommand.Step;
return Task.FromResult(new NextResponse());
}
}
internal class PauseHandler : PauseHandlerBase
{
private readonly IDebuggerServer _debugService;
public PauseHandler(IDebuggerServer debugService) => _debugService = debugService;
public override Task<PauseResponse> Handle(PauseArguments request, CancellationToken cancellationToken)
{
_debugService.DebuggerCommand = Debug.Data.InterpreterCommand.Break;
return Task.FromResult(new PauseResponse());
}
}
internal class StepInHandler : StepInHandlerBase
{
private readonly IDebuggerServer _debugService;
public StepInHandler(IDebuggerServer debugService) => _debugService = debugService;
public override Task<StepInResponse> Handle(StepInArguments request, CancellationToken cancellationToken)
{
_debugService.DebuggerCommand = Debug.Data.InterpreterCommand.Step;
return Task.FromResult(new StepInResponse());
}
}
@@ -0,0 +1,121 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Iris.Debug;
using Microsoft.Iris.DebugAdapter.Server.Handlers;
using OmniSharp.Extensions.DebugAdapter.Server;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.Iris.DebugAdapter.Server;
public class IrisDebugAdapterServer : IDisposable
{
private readonly Stream _inputStream;
private readonly Stream _outputStream;
private readonly TaskCompletionSource<bool> _serverStopped;
public IrisDebugAdapterServer(
Stream inputStream,
Stream outputStream,
IrisDebugServerOptions options)
{
_inputStream = inputStream;
_outputStream = outputStream;
_serverStopped = new();
Options = options;
}
internal IrisDebugServerOptions Options { get; }
internal DebugAdapterServer? Server { get; private set; }
/// <summary>
/// Start the debug server listening.
/// </summary>
/// <returns>A task that completes when the server is ready.</returns>
public async Task StartAsync()
{
Server = await DebugAdapterServer.From(options =>
{
// We need to let the PowerShell Context Service know that we are in a debug session
// so that it doesn't send the powerShell/startDebugger message.
//_psesHost = ServiceProvider.GetService<PsesInternalHost>();
//_psesHost.DebugContext.IsDebugServerActive = true;
options
.WithInput(_inputStream)
.WithOutput(_outputStream)
.WithServices(serviceCollection =>
serviceCollection
.AddOptions()
.AddIrisDebugServices(Options.SymbolDir, Options.SourceDir)
)
// TODO: Consider replacing all WithHandler with AddSingleton
//.WithHandler<AttachHandler>()
//.WithHandler<DisconnectHandler>()
.WithHandler<BreakpointHandler>()
//.WithHandler<ConfigurationDoneHandler>()
//.WithHandler<ThreadsHandler>()
//.WithHandler<StackTraceHandler>()
//.WithHandler<ScopesHandler>()
//.WithHandler<VariablesHandler>()
.WithHandler<ContinueHandler>()
.WithHandler<NextHandler>()
.WithHandler<PauseHandler>()
.WithHandler<StepInHandler>()
//.WithHandler<StepOutHandler>()
//.WithHandler<SourceHandler>()
//.WithHandler<SetVariableHandler>()
//.WithHandler<DebugEvaluateHandler>()
// The OnInitialize delegate gets run when we first receive the _Initialize_ request:
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
.OnInitialize(async (server, _, cancellationToken) =>
{
// Start the host if not already started, and enable debug mode (required
// for remote debugging).
//
// TODO: We might need to fill in HostStartOptions here.
//_startedPses = !await _psesHost.TryStartAsync(new HostStartOptions(), cancellationToken).ConfigureAwait(false);
//_psesHost.DebugContext.EnableDebugMode();
// Clear any existing breakpoints before proceeding.
//BreakpointService breakpointService = server.GetService<BreakpointService>();
//await breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false);
})
// The OnInitialized delegate gets run right before the server responds to the _Initialize_ request:
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
.OnInitialized((_, _, response, _) =>
{
//response.SupportsConditionalBreakpoints = true;
//response.SupportsConfigurationDoneRequest = true;
//response.SupportsFunctionBreakpoints = true;
//response.SupportsHitConditionalBreakpoints = true;
//response.SupportsLogPoints = true;
//response.SupportsSetVariable = true;
//response.SupportsDelayedStackTraceLoading = true;
return Task.CompletedTask;
})
;
}).ConfigureAwait(false);
}
public void Dispose()
{
// Note that the lifetime of the DebugContext is longer than the debug server;
// It represents the debugger on the PowerShell process we're in,
// while a new debug server is spun up for every debugging session
//_psesHost.DebugContext.IsDebugServerActive = false;
Server?.Dispose();
_inputStream.Dispose();
_outputStream.Dispose();
_serverStopped.SetResult(true);
}
public async Task WaitForShutdownAsync() => await _serverStopped.Task.ConfigureAwait(false);
public event EventHandler? SessionEnded;
internal void OnSessionEnded() => SessionEnded?.Invoke(this, EventArgs.Empty);
}
@@ -0,0 +1,8 @@
namespace Microsoft.Iris.DebugAdapter;
public class IrisDebugServerOptions
{
public string SymbolDir { get; set; }
public string? SourceDir { get; set; }
}
@@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Iris.DebugAdapter.Server;
internal static class ServiceCollectionExtensions
{
public static IServiceCollection AddIrisDebugServices(this IServiceCollection services, string symbolDir, string? sourceDir)
{
DebugSymbolResolver symbolResolver = new(symbolDir, sourceDir);
return services
.AddSingleton(symbolResolver);
}
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net6.0;net6.0-windows10.0.22000</TargetFrameworks>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<RootNamespace>Microsoft.Iris.DebugAdapter.Server</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OmniSharp.Extensions.DebugAdapter.Server" Version="0.19.9" />
<ProjectReference Include="..\..\libs\MicrosoftIris\UIX\UIX.csproj" />
<ProjectReference Include="..\UIX.DebugAdapter.Shared\UIX.DebugAdapter.Shared.csproj" />
<PackageReference Include="PolySharp" Version="1.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,20 @@
using System.IO;
using System.IO.Pipes;
namespace Microsoft.Iris.DebugAdapter;
public static class ConnectionStringHelper
{
private const string PIPE_PREFIX = @"\\.\pipe\";
public static void CreateFromString(string connectionString, out Stream input, out Stream output)
{
// TODO: Support other transports
var pipeName = connectionString.StartsWith(PIPE_PREFIX)
? connectionString
: NamedPipeUtils.GenerateValidNamedPipeName();
input = NamedPipeUtils.CreateNamedPipe(pipeName, PipeDirection.InOut);
output = input;
}
}
@@ -0,0 +1,76 @@
using Microsoft.Iris.Debug.Symbols;
using System;
using System.IO;
using System.Linq;
namespace Microsoft.Iris.DebugAdapter;
public class DebugSymbolResolver(string symbolDir, string? sourceDir)
{
private readonly DirectoryInfo _symbolDir = new(symbolDir);
private readonly DirectoryInfo? _sourceDir = sourceDir is not null ? new(sourceDir) : null;
public ApplicationDebugSymbols? GetForApplication(string? application)
{
if (application is null)
return null;
var fileName = $"{application}.asym.json";
var asymFile = FindFile(fileName, _symbolDir);
if (asymFile is null)
return null;
using var stream = asymFile.OpenRead();
using var reader = new StreamReader(stream);
return DebugSymbolsJsonParser.ParseForApplication(reader.ReadToEnd());
}
public FileDebugSymbols? GetForFile(string? file, string? application = null)
{
if (file is null)
return null;
var fileName = $"{file}.fsym.json";
var fsymFile = FindFile(fileName, _symbolDir);
if (fsymFile is not null)
{
using var stream = fsymFile.OpenRead();
using var reader = new StreamReader(stream);
var fsym = DebugSymbolsJsonParser.ParseForFile(reader.ReadToEnd());
var sourceFile = FindFile(file, _sourceDir);
if (sourceFile is not null)
{
using var sourceStream = sourceFile.OpenRead();
using var sourceReader = new StreamReader(sourceStream);
fsym.SetSourceCode(sourceReader.ReadToEnd());
}
return fsym;
}
if (application is null)
return null;
var asym = GetForApplication(application);
if (asym is null)
return null;
return asym.GetForFile(fileName);
}
private static FileInfo? FindFile(string fileName, DirectoryInfo? dir)
{
return dir?
.EnumerateFiles()
.FirstOrDefault(f => AreEquivalentFileNames(f.Name, fileName));
}
private static bool AreEquivalentFileNames(string fileName1, string fileName2)
{
// NOTE: Assumes Windows. Should be trivial to support other filesystems later.
return fileName1.Equals(fileName2, StringComparison.InvariantCultureIgnoreCase);
}
}
@@ -0,0 +1,151 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
#if !NET
using System.Security.Principal;
using System.Security.AccessControl;
#else
using System.Runtime.InteropServices;
#endif
namespace Microsoft.Iris.DebugAdapter;
/// <summary>
/// Utility class for handling named pipe creation in .NET Core and .NET Framework.
/// </summary>
public static class NamedPipeUtils
{
#if !NET
// .NET Framework requires the buffer size to be specified
private const int PipeBufferSize = 1024;
#endif
public static NamedPipeServerStream CreateNamedPipe(
string pipeName,
PipeDirection pipeDirection)
{
#if NET
return new NamedPipeServerStream(
pipeName: pipeName,
direction: pipeDirection,
maxNumberOfServerInstances: 1,
transmissionMode: PipeTransmissionMode.Byte,
options: PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous);
#else
// In .NET Framework, we must manually ACL the named pipes we create
PipeSecurity pipeSecurity = new();
WindowsIdentity identity = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new(identity);
if (principal.IsInRole(WindowsBuiltInRole.Administrator))
{
// Allow the Administrators group full access to the pipe.
pipeSecurity.AddAccessRule(
new PipeAccessRule(
new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null).Translate(typeof(NTAccount)),
PipeAccessRights.FullControl, AccessControlType.Allow));
}
else
{
// Allow the current user read/write access to the pipe.
pipeSecurity.AddAccessRule(new PipeAccessRule(
WindowsIdentity.GetCurrent().User,
PipeAccessRights.ReadWrite, AccessControlType.Allow));
}
return new NamedPipeServerStream(
pipeName,
pipeDirection,
maxNumberOfServerInstances: 1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous,
inBufferSize: PipeBufferSize,
outBufferSize: PipeBufferSize,
pipeSecurity);
#endif
}
/// <summary>
/// Generate a named pipe name known to not already be in use.
/// </summary>
/// <param name="prefixes">Prefix variants of the pipename to test, if any.</param>
/// <returns>A named pipe name or name suffix that is safe to you.</returns>
public static string GenerateValidNamedPipeName(IReadOnlyCollection<string>? prefixes = null)
{
for (int i = 0; i < 10; i++)
{
string pipeName = $"IrisUIX_{Path.GetRandomFileName()}";
// In the simple prefix-less case, just test the pipe name
if (prefixes == null)
{
if (!IsPipeNameValid(pipeName))
{
continue;
}
return pipeName;
}
// If we have prefixes, test that all prefix/pipename combinations are valid
bool allPipeNamesValid = true;
foreach (string prefix in prefixes)
{
string prefixedPipeName = $"IrisUIX_{prefix}_{pipeName}";
if (!IsPipeNameValid(prefixedPipeName))
{
allPipeNamesValid = false;
break;
}
}
if (allPipeNamesValid)
{
return pipeName;
}
}
throw new IOException("Unable to create named pipe; no available names");
}
/// <summary>
/// Validate that a named pipe file name is a legitimate named pipe file name and is not already in use.
/// </summary>
/// <param name="pipeName">The named pipe name to validate. This should be a simple name rather than a path.</param>
/// <returns>True if the named pipe name is valid, false otherwise.</returns>
public static bool IsPipeNameValid(string pipeName)
{
if (string.IsNullOrEmpty(pipeName))
{
return false;
}
return !File.Exists(GetNamedPipePath(pipeName));
}
/// <summary>
/// Get the path of a named pipe given its name.
/// </summary>
/// <param name="pipeName">The simple name of the named pipe.</param>
/// <returns>The full path of the named pipe.</returns>
#pragma warning disable IDE0022
public static string GetNamedPipePath(string pipeName)
{
#if NET
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{pipeName}");
}
#endif
return $@"\\.\pipe\{pipeName}";
}
}
#pragma warning restore IDE0022
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net6.0;net6.0-windows10.0.22000</TargetFrameworks>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<RootNamespace>Microsoft.Iris.DebugAdapter</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\libs\MicrosoftIris\UIX\UIX.csproj" />
<PackageReference Include="PolySharp" Version="1.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>