Initial commit: Nix WSL Wrapper project with DDD, Clean Code, and tests
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
|||||||
|
## Build outputs
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
**/dist/
|
||||||
|
|
||||||
|
## User-specific files
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
## Visual Studio cache and settings
|
||||||
|
.vs/
|
||||||
|
.vs*
|
||||||
|
[Dd]ebug/
|
||||||
|
[Rr]elease/
|
||||||
|
|
||||||
|
## Test results
|
||||||
|
TestResults/
|
||||||
|
*.trx
|
||||||
|
|
||||||
|
## Rider / JetBrains settings
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
## VS Code settings
|
||||||
|
.vscode/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<Solution>
|
||||||
|
<Folder Name="/src/">
|
||||||
|
<Project Path="src/NixWslWrapper.Cli/NixWslWrapper.Cli.csproj" />
|
||||||
|
<Project Path="src/NixWslWrapper.Core/NixWslWrapper.Core.csproj" />
|
||||||
|
<Project Path="src/NixWslWrapper.Infrastructure/NixWslWrapper.Infrastructure.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/tests/">
|
||||||
|
<Project Path="tests/NixWslWrapper.Tests/NixWslWrapper.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
|
</Solution>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Nix WSL Wrapper (nix.exe)
|
||||||
|
|
||||||
|
A robust, transparent Windows executable wrapper designed to intercept native Windows commands and execute them seamlessly inside Nix Subsystem for Linux (WSL). This wrapper is particularly useful for allowing Windows-based MCP servers (like `utensils/mcp-nixos`), IDE integrations, and build tools to run `nix` commands natively on a Windows host without requiring full native Nix compile chains.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
* **Clean Architecture & SOLID**: Built in C# using Domain-Driven Design (DDD) to encapsulate domain validation (e.g. `WslTarget`, `NixArguments`).
|
||||||
|
* **Deadlock-Free I/O Streaming**: Spawns background threads to redirect standard input, standard output, and standard error without blocking host tools.
|
||||||
|
* **Premium CLI UI & Diagnostics**: Integrates `Spectre.Console` to render beautiful diagnostic dashboards, system statuses, and automated validation tests.
|
||||||
|
* **Highly Configuration-driven**: Environment variables allow configuring the WSL distribution name, username, and path to the Nix profile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architectural Layout
|
||||||
|
* **NixWslWrapper.Core**: The domain layer defining value objects (`WslTarget`, `NixArguments`, `ExecutionResult`) and interfaces (`ICommandExecutor`, `IStreamCopier`).
|
||||||
|
* **NixWslWrapper.Infrastructure**: Contains execution strategies (`WslCommandExecutor`), builder patterns (`ProcessBuilder`), and threaded stream piping (`ThreadedStreamCopier`).
|
||||||
|
* **NixWslWrapper.Cli**: Presentation layer resolving services via Dependency Injection (`Microsoft.Extensions.DependencyInjection`), handling command forwarding, and presenting styled dashboard visuals with `Spectre.Console`.
|
||||||
|
* **NixWslWrapper.Tests**: The testing suite compiling xUnit tests and mock assertions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
1. **WSL2** must be active.
|
||||||
|
2. A Linux distribution installed in WSL (e.g. `Debian` or `Ubuntu`).
|
||||||
|
3. **Nix Package Manager** installed inside the WSL distribution.
|
||||||
|
|
||||||
|
### Build and Publish
|
||||||
|
Compile the project to a single self-contained executable for Windows:
|
||||||
|
```powershell
|
||||||
|
dotnet publish src/NixWslWrapper.Cli/NixWslWrapper.Cli.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o ./dist
|
||||||
|
```
|
||||||
|
This will generate `nix.exe` inside the `./dist` folder. Place this executable in your Windows `%PATH%` (e.g., in a WinGet Links directory or your IDE binary directory).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configure the wrapper using the following Windows environment variables:
|
||||||
|
|
||||||
|
| Environment Variable | Description | Default Value |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `NIX_WSL_DISTRO` | Target WSL Linux distribution name. | `Debian` |
|
||||||
|
| `NIX_WSL_USER` | The user to execute WSL commands under. | `root` |
|
||||||
|
| `NIX_WSL_PROFILE` | Path to the nix-daemon profile script inside WSL. | `/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Command Flags
|
||||||
|
|
||||||
|
When running the wrapper, standard arguments are forwarded directly to WSL. Two special flags are intercepted by the wrapper for system administration:
|
||||||
|
|
||||||
|
* `--wsl-status`: Renders a premium diagnostic panel checking your WSL execution and verifying the Nix version inside your distro.
|
||||||
|
* `--wsl-help`: Displays wrapper-specific configuration details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
Run the xUnit test suite from the root of the project:
|
||||||
|
```powershell
|
||||||
|
dotnet test
|
||||||
|
```
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using NixWslWrapper.Core.Domain;
|
||||||
|
using NixWslWrapper.Core.Interfaces;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Cli
|
||||||
|
{
|
||||||
|
public class CliController
|
||||||
|
{
|
||||||
|
private readonly ICommandExecutor _commandExecutor;
|
||||||
|
|
||||||
|
public CliController(ICommandExecutor commandExecutor)
|
||||||
|
{
|
||||||
|
_commandExecutor = commandExecutor ?? throw new ArgumentNullException(nameof(commandExecutor));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Run(string[] args)
|
||||||
|
{
|
||||||
|
// Intercept special configuration/status options
|
||||||
|
if (args.Contains("--wsl-status", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
RenderStatusDashboard();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Contains("--wsl-help", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
RenderHelp();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get configuration from environment variables or use defaults
|
||||||
|
string distro = Environment.GetEnvironmentVariable("NIX_WSL_DISTRO") ?? "Debian";
|
||||||
|
string user = Environment.GetEnvironmentVariable("NIX_WSL_USER") ?? "root";
|
||||||
|
string profile = Environment.GetEnvironmentVariable("NIX_WSL_PROFILE") ?? "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh";
|
||||||
|
|
||||||
|
WslTarget target;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
target = new WslTarget(distro, user, profile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($"[red]Configuration Error:[/] {ex.Message}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nixArgs = new NixArguments(args);
|
||||||
|
var result = _commandExecutor.Execute(nixArgs, target);
|
||||||
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
if (result.ErrorMessage != null)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($"[bold red]Execution Error:[/] {result.ErrorMessage}");
|
||||||
|
}
|
||||||
|
return result.ExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RenderStatusDashboard()
|
||||||
|
{
|
||||||
|
AnsiConsole.Write(
|
||||||
|
new FigletText("Nix WSL Wrapper")
|
||||||
|
.Color(Color.DeepSkyBlue1));
|
||||||
|
|
||||||
|
var panel = new Panel(
|
||||||
|
new Markup(
|
||||||
|
"[bold]Nix WSL Wrapper Diagnostic Dashboard[/]\n" +
|
||||||
|
"Wraps native Windows MCP / CLI tool inputs and redirects them into Nix running inside WSL.\n\n" +
|
||||||
|
$"[grey]Generated at: {DateTime.Now:yyyy-MM-dd HH:mm:ss}[/]"
|
||||||
|
))
|
||||||
|
{
|
||||||
|
Border = BoxBorder.Rounded,
|
||||||
|
Padding = new Padding(1, 1, 1, 1),
|
||||||
|
Header = new PanelHeader("System Info")
|
||||||
|
};
|
||||||
|
|
||||||
|
AnsiConsole.Write(panel);
|
||||||
|
|
||||||
|
// Fetch Environment details
|
||||||
|
string distro = Environment.GetEnvironmentVariable("NIX_WSL_DISTRO") ?? "Debian (Default)";
|
||||||
|
string user = Environment.GetEnvironmentVariable("NIX_WSL_USER") ?? "root (Default)";
|
||||||
|
string profile = Environment.GetEnvironmentVariable("NIX_WSL_PROFILE") ?? "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh (Default)";
|
||||||
|
|
||||||
|
var table = new Table()
|
||||||
|
.Border(TableBorder.Rounded)
|
||||||
|
.BorderColor(Color.Grey)
|
||||||
|
.AddColumn("[bold]Setting[/]")
|
||||||
|
.AddColumn("[bold]Value[/]")
|
||||||
|
.AddColumn("[bold]Source[/]");
|
||||||
|
|
||||||
|
table.AddRow("WSL Target Distro", distro, Environment.GetEnvironmentVariable("NIX_WSL_DISTRO") != null ? "[green]Env Var[/]" : "[yellow]Fallback[/]");
|
||||||
|
table.AddRow("WSL Execution User", user, Environment.GetEnvironmentVariable("NIX_WSL_USER") != null ? "[green]Env Var[/]" : "[yellow]Fallback[/]");
|
||||||
|
table.AddRow("Nix Profile Path", profile, Environment.GetEnvironmentVariable("NIX_WSL_PROFILE") != null ? "[green]Env Var[/]" : "[yellow]Fallback[/]");
|
||||||
|
|
||||||
|
AnsiConsole.Write(new Rule("[yellow]Configured Parameters[/]") { Justification = Justify.Left });
|
||||||
|
AnsiConsole.Write(table);
|
||||||
|
|
||||||
|
// Run WSL check
|
||||||
|
AnsiConsole.Write(new Rule("[yellow]WSL Integration Status[/]") { Justification = Justify.Left });
|
||||||
|
|
||||||
|
AnsiConsole.Status()
|
||||||
|
.Spinner(Spinner.Known.Dots)
|
||||||
|
.Start("Verifying WSL and Nix installation...", ctx =>
|
||||||
|
{
|
||||||
|
bool wslOk = false;
|
||||||
|
string wslVersion = "Unknown";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo("wsl.exe", "--status")
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
using var p = Process.Start(psi);
|
||||||
|
p?.WaitForExit(3000);
|
||||||
|
wslOk = p?.ExitCode == 0;
|
||||||
|
wslVersion = wslOk ? "WSL Active" : "WSL Error";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
wslVersion = "wsl.exe not found in PATH";
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status("Checking Nix inside WSL Distro...");
|
||||||
|
bool nixOk = false;
|
||||||
|
string nixVersion = "Not Found";
|
||||||
|
if (wslOk)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string targetDistro = Environment.GetEnvironmentVariable("NIX_WSL_DISTRO") ?? "Debian";
|
||||||
|
string targetUser = Environment.GetEnvironmentVariable("NIX_WSL_USER") ?? "root";
|
||||||
|
string targetProfile = Environment.GetEnvironmentVariable("NIX_WSL_PROFILE") ?? "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh";
|
||||||
|
|
||||||
|
var psi = new ProcessStartInfo("wsl.exe", $"-d {targetDistro} -u {targetUser} -- sh -c \". {targetProfile} && nix --version\"")
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
using var p = Process.Start(psi);
|
||||||
|
p?.WaitForExit(5000);
|
||||||
|
if (p?.ExitCode == 0)
|
||||||
|
{
|
||||||
|
nixOk = true;
|
||||||
|
nixVersion = p.StandardOutput.ReadToEnd().Trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nixVersion = "Nix execution failed inside WSL";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
nixVersion = $"Error querying: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Checks
|
||||||
|
var grid = new Grid().AddColumn().AddColumn();
|
||||||
|
grid.AddRow(new Markup("wsl.exe Executable Status:"), wslOk ? new Markup("[green]✔ OK[/]") : new Markup($"[red]✘ FAILED ({wslVersion})[/]"));
|
||||||
|
grid.AddRow(new Markup("Nix Engine in WSL Distro:"), nixOk ? new Markup($"[green]✔ OK ({nixVersion})[/]") : new Markup($"[red]✘ FAILED ({nixVersion})[/]"));
|
||||||
|
|
||||||
|
AnsiConsole.Write(new Panel(grid) { Border = BoxBorder.None });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RenderHelp()
|
||||||
|
{
|
||||||
|
AnsiConsole.Write(
|
||||||
|
new FigletText("Nix WSL Wrapper")
|
||||||
|
.Color(Color.DeepSkyBlue1));
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine("[bold]Usage:[/] nix.exe [[nix-arguments-to-forward]]");
|
||||||
|
AnsiConsole.MarkupLine("[bold]Alternative flags (Wrapper specific):[/]");
|
||||||
|
AnsiConsole.MarkupLine(" [yellow]--wsl-status[/] Displays diagnostic status of WSL and Nix integration.");
|
||||||
|
AnsiConsole.MarkupLine(" [yellow]--wsl-help[/] Displays this help menu.");
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
AnsiConsole.MarkupLine("[bold]Configuration Environment Variables:[/]");
|
||||||
|
AnsiConsole.MarkupLine(" [green]NIX_WSL_DISTRO[/] The target WSL distribution name (default: [grey]Debian[/]).");
|
||||||
|
AnsiConsole.MarkupLine(" [green]NIX_WSL_USER[/] The user to execute commands under (default: [grey]root[/]).");
|
||||||
|
AnsiConsole.MarkupLine(" [green]NIX_WSL_PROFILE[/] The path to the nix-daemon profile script (default: [grey]/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh[/]).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\NixWslWrapper.Infrastructure\NixWslWrapper.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Spectre.Console" Version="0.57.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NixWslWrapper.Core.Interfaces;
|
||||||
|
using NixWslWrapper.Infrastructure.IO;
|
||||||
|
using NixWslWrapper.Infrastructure.Executors;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Cli
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static int Main(string[] args)
|
||||||
|
{
|
||||||
|
var serviceProvider = ConfigureServices();
|
||||||
|
var controller = serviceProvider.GetRequiredService<CliController>();
|
||||||
|
return controller.Run(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceProvider ConfigureServices()
|
||||||
|
{
|
||||||
|
return new ServiceCollection()
|
||||||
|
.AddSingleton<IStreamCopier, ThreadedStreamCopier>()
|
||||||
|
.AddSingleton<ICommandExecutor, WslCommandExecutor>()
|
||||||
|
.AddSingleton<CliController>()
|
||||||
|
.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Core.Domain
|
||||||
|
{
|
||||||
|
public class ExecutionResult
|
||||||
|
{
|
||||||
|
public int ExitCode { get; }
|
||||||
|
public bool IsSuccess => ExitCode == 0;
|
||||||
|
public string? ErrorMessage { get; }
|
||||||
|
|
||||||
|
public ExecutionResult(int exitCode, string? errorMessage = null)
|
||||||
|
{
|
||||||
|
ExitCode = exitCode;
|
||||||
|
ErrorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExecutionResult Success() => new ExecutionResult(0);
|
||||||
|
public static ExecutionResult Failure(int exitCode, string errorMessage) => new ExecutionResult(exitCode, errorMessage);
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not ExecutionResult other) return false;
|
||||||
|
return ExitCode == other.ExitCode && string.Equals(ErrorMessage, other.ErrorMessage, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(ExitCode, ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return IsSuccess ? "Success" : $"Failed with Exit Code {ExitCode}. Error: {ErrorMessage}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Core.Domain
|
||||||
|
{
|
||||||
|
public class NixArguments
|
||||||
|
{
|
||||||
|
public string[] RawArguments { get; }
|
||||||
|
|
||||||
|
public NixArguments(string[]? rawArguments)
|
||||||
|
{
|
||||||
|
RawArguments = rawArguments ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToEscapedString()
|
||||||
|
{
|
||||||
|
if (RawArguments.Length == 0) return string.Empty;
|
||||||
|
|
||||||
|
var escapedList = new List<string>();
|
||||||
|
foreach (var arg in RawArguments)
|
||||||
|
{
|
||||||
|
// Escape existing double quotes
|
||||||
|
string escaped = arg.Replace("\"", "\\\"");
|
||||||
|
// Wrap in double quotes to prevent word splitting in bash
|
||||||
|
escapedList.Add($"\"{escaped}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", escapedList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not NixArguments other) return false;
|
||||||
|
if (RawArguments.Length != other.RawArguments.Length) return false;
|
||||||
|
return RawArguments.SequenceEqual(other.RawArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
int hash = 17;
|
||||||
|
foreach (var arg in RawArguments)
|
||||||
|
{
|
||||||
|
hash = hash * 23 + (arg?.GetHashCode() ?? 0);
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Join(" ", RawArguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Core.Domain
|
||||||
|
{
|
||||||
|
public class WslTarget
|
||||||
|
{
|
||||||
|
public string Distribution { get; }
|
||||||
|
public string User { get; }
|
||||||
|
public string NixProfilePath { get; }
|
||||||
|
|
||||||
|
public WslTarget(string distribution, string user, string nixProfilePath = "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(distribution))
|
||||||
|
throw new ArgumentException("WSL Distribution name cannot be null or empty.", nameof(distribution));
|
||||||
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
|
throw new ArgumentException("WSL User cannot be null or empty.", nameof(user));
|
||||||
|
if (string.IsNullOrWhiteSpace(nixProfilePath))
|
||||||
|
throw new ArgumentException("Nix Profile Path cannot be null or empty.", nameof(nixProfilePath));
|
||||||
|
|
||||||
|
Distribution = distribution.Trim();
|
||||||
|
User = user.Trim();
|
||||||
|
NixProfilePath = nixProfilePath.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not WslTarget other) return false;
|
||||||
|
return string.Equals(Distribution, other.Distribution, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(User, other.User, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(NixProfilePath, other.NixProfilePath, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(
|
||||||
|
Distribution.ToLowerOrdinal(),
|
||||||
|
User.ToLowerOrdinal(),
|
||||||
|
NixProfilePath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"WSL Distro: {Distribution}, User: {User}, Profile: {NixProfilePath}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class StringExtensions
|
||||||
|
{
|
||||||
|
public static string ToLowerOrdinal(this string str) => str.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using NixWslWrapper.Core.Domain;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Core.Interfaces
|
||||||
|
{
|
||||||
|
public interface ICommandExecutor
|
||||||
|
{
|
||||||
|
ExecutionResult Execute(NixArguments arguments, WslTarget target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Core.Interfaces
|
||||||
|
{
|
||||||
|
public interface IStreamCopier
|
||||||
|
{
|
||||||
|
void Copy(Stream input, Stream output);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Infrastructure.Executors
|
||||||
|
{
|
||||||
|
public class ProcessBuilder
|
||||||
|
{
|
||||||
|
private readonly ProcessStartInfo _startInfo = new();
|
||||||
|
|
||||||
|
public ProcessBuilder SetFileName(string fileName)
|
||||||
|
{
|
||||||
|
_startInfo.FileName = fileName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProcessBuilder SetArguments(string arguments)
|
||||||
|
{
|
||||||
|
_startInfo.Arguments = arguments;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProcessBuilder RedirectInput(bool redirect = true)
|
||||||
|
{
|
||||||
|
_startInfo.RedirectStandardInput = redirect;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProcessBuilder RedirectOutput(bool redirect = true)
|
||||||
|
{
|
||||||
|
_startInfo.RedirectStandardOutput = redirect;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProcessBuilder RedirectError(bool redirect = true)
|
||||||
|
{
|
||||||
|
_startInfo.RedirectStandardError = redirect;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProcessBuilder UseShellExecute(bool useShell = false)
|
||||||
|
{
|
||||||
|
_startInfo.UseShellExecute = useShell;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProcessBuilder CreateNoWindow(bool noWindow = true)
|
||||||
|
{
|
||||||
|
_startInfo.CreateNoWindow = noWindow;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProcessStartInfo Build()
|
||||||
|
{
|
||||||
|
return _startInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Process CreateProcess()
|
||||||
|
{
|
||||||
|
return new Process { StartInfo = _startInfo };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using NixWslWrapper.Core.Domain;
|
||||||
|
using NixWslWrapper.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Infrastructure.Executors
|
||||||
|
{
|
||||||
|
public class WslCommandExecutor : ICommandExecutor
|
||||||
|
{
|
||||||
|
private readonly IStreamCopier _streamCopier;
|
||||||
|
private readonly Stream _hostIn;
|
||||||
|
private readonly Stream _hostOut;
|
||||||
|
private readonly Stream _hostErr;
|
||||||
|
|
||||||
|
public WslCommandExecutor(
|
||||||
|
IStreamCopier streamCopier,
|
||||||
|
Stream? hostIn = null,
|
||||||
|
Stream? hostOut = null,
|
||||||
|
Stream? hostErr = null)
|
||||||
|
{
|
||||||
|
_streamCopier = streamCopier ?? throw new ArgumentNullException(nameof(streamCopier));
|
||||||
|
_hostIn = hostIn ?? Console.OpenStandardInput();
|
||||||
|
_hostOut = hostOut ?? Console.OpenStandardOutput();
|
||||||
|
_hostErr = hostErr ?? Console.OpenStandardError();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExecutionResult Execute(NixArguments arguments, WslTarget target)
|
||||||
|
{
|
||||||
|
if (arguments == null) throw new ArgumentNullException(nameof(arguments));
|
||||||
|
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||||
|
|
||||||
|
string wslCmd = $". {target.NixProfilePath} && nix {arguments.ToEscapedString()}";
|
||||||
|
string wslArgs = $"-d {target.Distribution} -u {target.User} -- sh -c \"{wslCmd.Replace("\"", "\\\"")}\"";
|
||||||
|
|
||||||
|
var processBuilder = new ProcessBuilder()
|
||||||
|
.SetFileName("wsl.exe")
|
||||||
|
.SetArguments(wslArgs)
|
||||||
|
.RedirectInput(true)
|
||||||
|
.RedirectOutput(true)
|
||||||
|
.RedirectError(true)
|
||||||
|
.UseShellExecute(false)
|
||||||
|
.CreateNoWindow(true);
|
||||||
|
|
||||||
|
using (Process process = processBuilder.CreateProcess())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
// Standard streams copy tasks
|
||||||
|
Thread tOut = new(() => _streamCopier.Copy(process.StandardOutput.BaseStream, _hostOut))
|
||||||
|
{
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
Thread tErr = new(() => _streamCopier.Copy(process.StandardError.BaseStream, _hostErr))
|
||||||
|
{
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
Thread tIn = new(() => _streamCopier.Copy(_hostIn, process.StandardInput.BaseStream))
|
||||||
|
{
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
|
||||||
|
tOut.Start();
|
||||||
|
tErr.Start();
|
||||||
|
tIn.Start();
|
||||||
|
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
// Wait up to 1 second for stream copier threads to finalize flushing buffer content
|
||||||
|
tOut.Join(1000);
|
||||||
|
tErr.Join(1000);
|
||||||
|
|
||||||
|
return new ExecutionResult(process.ExitCode);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ExecutionResult.Failure(1, $"Error executing Nix wrapper: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using NixWslWrapper.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Infrastructure.IO
|
||||||
|
{
|
||||||
|
public class ThreadedStreamCopier : IStreamCopier
|
||||||
|
{
|
||||||
|
private readonly int _bufferSize;
|
||||||
|
|
||||||
|
public ThreadedStreamCopier(int bufferSize = 8192)
|
||||||
|
{
|
||||||
|
if (bufferSize <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(bufferSize), "Buffer size must be positive.");
|
||||||
|
_bufferSize = bufferSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Copy(Stream input, Stream output)
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[_bufferSize];
|
||||||
|
int read;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
output.Write(buffer, 0, read);
|
||||||
|
output.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// Clean exit when stream is closed or process terminates
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Clean exit when streams are disposed
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// General catch to ensure thread doesn't crash the host process
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\NixWslWrapper.Core\NixWslWrapper.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using Moq;
|
||||||
|
using NixWslWrapper.Cli;
|
||||||
|
using NixWslWrapper.Core.Domain;
|
||||||
|
using NixWslWrapper.Core.Interfaces;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Tests
|
||||||
|
{
|
||||||
|
public class CliTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CliController_ForwardArguments_ShouldReturnExecutorExitCode()
|
||||||
|
{
|
||||||
|
var mockExecutor = new Mock<ICommandExecutor>();
|
||||||
|
var expectedResult = new ExecutionResult(42);
|
||||||
|
mockExecutor
|
||||||
|
.Setup(m => m.Execute(It.IsAny<NixArguments>(), It.IsAny<WslTarget>()))
|
||||||
|
.Returns(expectedResult);
|
||||||
|
|
||||||
|
var controller = new CliController(mockExecutor.Object);
|
||||||
|
string[] cliArgs = new[] { "run", "nixpkgs#hello" };
|
||||||
|
|
||||||
|
int exitCode = controller.Run(cliArgs);
|
||||||
|
|
||||||
|
Assert.Equal(42, exitCode);
|
||||||
|
mockExecutor.Verify(m => m.Execute(
|
||||||
|
It.Is<NixArguments>(a => a.RawArguments.Length == 2 && a.RawArguments[0] == "run"),
|
||||||
|
It.IsAny<WslTarget>()
|
||||||
|
), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CliController_HelpMenuFlag_ShouldReturnZeroImmediately()
|
||||||
|
{
|
||||||
|
var mockExecutor = new Mock<ICommandExecutor>();
|
||||||
|
var controller = new CliController(mockExecutor.Object);
|
||||||
|
string[] cliArgs = new[] { "--wsl-help" };
|
||||||
|
|
||||||
|
int exitCode = controller.Run(cliArgs);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
mockExecutor.Verify(m => m.Execute(It.IsAny<NixArguments>(), It.IsAny<WslTarget>()), Times.Never);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using NixWslWrapper.Core.Domain;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Tests
|
||||||
|
{
|
||||||
|
public class CoreTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void WslTarget_ValidParameters_ShouldConstructCorrectly()
|
||||||
|
{
|
||||||
|
var target = new WslTarget("Debian", "root", "/some/path");
|
||||||
|
Assert.Equal("Debian", target.Distribution);
|
||||||
|
Assert.Equal("root", target.User);
|
||||||
|
Assert.Equal("/some/path", target.NixProfilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "root", "/path")]
|
||||||
|
[InlineData("Debian", "", "/path")]
|
||||||
|
[InlineData("Debian", "root", "")]
|
||||||
|
[InlineData(null, "root", "/path")]
|
||||||
|
[InlineData("Debian", null, "/path")]
|
||||||
|
[InlineData("Debian", "root", null)]
|
||||||
|
public void WslTarget_InvalidParameters_ShouldThrowArgumentException(string? distro, string? user, string? profile)
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => new WslTarget(distro!, user!, profile!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WslTarget_Equality_ShouldBeTrueForSameValues()
|
||||||
|
{
|
||||||
|
var target1 = new WslTarget("Debian", "root", "/path");
|
||||||
|
var target2 = new WslTarget("debian", "ROOT", "/path"); // case-insensitive for distro/user
|
||||||
|
Assert.Equal(target1, target2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NixArguments_EmptyArgs_ShouldReturnEmptyEscapedString()
|
||||||
|
{
|
||||||
|
var args = new NixArguments(Array.Empty<string>());
|
||||||
|
Assert.Equal(string.Empty, args.ToEscapedString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NixArguments_WithSpecialCharacters_ShouldEscapeDoubleQuotes()
|
||||||
|
{
|
||||||
|
var args = new NixArguments(new[] { "run", "nixpkgs#hello", "--argstr", "msg", "hello \"world\"" });
|
||||||
|
string escaped = args.ToEscapedString();
|
||||||
|
Assert.Equal("\"run\" \"nixpkgs#hello\" \"--argstr\" \"msg\" \"hello \\\"world\\\"\"", escaped);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExecutionResult_Success_ShouldSetCorrectProperties()
|
||||||
|
{
|
||||||
|
var result = ExecutionResult.Success();
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(0, result.ExitCode);
|
||||||
|
Assert.Null(result.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExecutionResult_Failure_ShouldSetCorrectProperties()
|
||||||
|
{
|
||||||
|
var result = ExecutionResult.Failure(5, "Access Denied");
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.Equal(5, result.ExitCode);
|
||||||
|
Assert.Equal("Access Denied", result.ErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using NixWslWrapper.Infrastructure.IO;
|
||||||
|
using NixWslWrapper.Infrastructure.Executors;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace NixWslWrapper.Tests
|
||||||
|
{
|
||||||
|
public class InfrastructureTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ThreadedStreamCopier_ValidStream_ShouldCopyEntireContent()
|
||||||
|
{
|
||||||
|
var content = "Hello, Nix WSL Wrapper! Stream copying should work flawlessly.";
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
using var input = new MemoryStream(bytes);
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
|
||||||
|
var copier = new ThreadedStreamCopier(64); // small buffer to force multiple reads
|
||||||
|
copier.Copy(input, output);
|
||||||
|
|
||||||
|
byte[] resultBytes = output.ToArray();
|
||||||
|
string result = Encoding.UTF8.GetString(resultBytes);
|
||||||
|
|
||||||
|
Assert.Equal(content, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessBuilder_ValidConfiguration_ShouldBuildCorrectProcessStartInfo()
|
||||||
|
{
|
||||||
|
var builder = new ProcessBuilder()
|
||||||
|
.SetFileName("my-app.exe")
|
||||||
|
.SetArguments("--arg1 val1")
|
||||||
|
.RedirectInput(true)
|
||||||
|
.RedirectOutput(true)
|
||||||
|
.RedirectError(true)
|
||||||
|
.UseShellExecute(false)
|
||||||
|
.CreateNoWindow(true);
|
||||||
|
|
||||||
|
var startInfo = builder.Build();
|
||||||
|
|
||||||
|
Assert.Equal("my-app.exe", startInfo.FileName);
|
||||||
|
Assert.Equal("--arg1 val1", startInfo.Arguments);
|
||||||
|
Assert.True(startInfo.RedirectStandardInput);
|
||||||
|
Assert.True(startInfo.RedirectStandardOutput);
|
||||||
|
Assert.True(startInfo.RedirectStandardError);
|
||||||
|
Assert.False(startInfo.UseShellExecute);
|
||||||
|
Assert.True(startInfo.CreateNoWindow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\NixWslWrapper.Core\NixWslWrapper.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\NixWslWrapper.Infrastructure\NixWslWrapper.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\NixWslWrapper.Cli\NixWslWrapper.Cli.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user