diff --git a/.editorconfig b/.editorconfig
index beffa30..4039ff1 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,11 +1,12 @@
root = true
[*]
-indent_style = space
-indent_size = 2
charset = utf-8
-trim_trailing_whitespace = true
+end_of_line = lf
+indent_size = 2
+indent_style = space
insert_final_newline = true
+trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
diff --git a/.github/actions/setup-msbuild/action.yml b/.github/actions/setup-msbuild/action.yml
new file mode 100644
index 0000000..84aa311
--- /dev/null
+++ b/.github/actions/setup-msbuild/action.yml
@@ -0,0 +1,7 @@
+runs:
+ using: 'composite'
+ steps:
+ - name: Setup MSBuild 🧰
+ uses: microsoft/setup-msbuild@v1.1
+ with:
+ vs-version: '[17.2,'
diff --git a/.github/actions/version/action.yml b/.github/actions/version/action.yml
new file mode 100644
index 0000000..a00cffe
--- /dev/null
+++ b/.github/actions/version/action.yml
@@ -0,0 +1,21 @@
+outputs:
+ version:
+ description: 'The generated version number'
+ value: ${{ steps.version.outputs.version }}
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Generate/Set Version 🏷️
+ id: version
+ shell: pwsh
+ run: |
+ $LATEST_TAG = git describe --tags --abbrev=0 # Get the latest tag name
+ $COMMIT_COUNT = git rev-list --use-bitmap-index --count "$(git rev-list --tags --no-walk --max-count=1)..HEAD" # Count all commits since the last tag
+ $NIXOS_WSL_VERSION = "$($LATEST_TAG -replace '(.+)\.(.+)\.(.+)\..+', '$1.$2.$3').${COMMIT_COUNT}" # Compose the NixOS-WSL version number
+ Write-Output "version=${NIXOS_WSL_VERSION}" >> $env:GITHUB_OUTPUT
+
+ Write-Output $NIXOS_WSL_VERSION > ./VERSION
+ Write-Output $(git rev-parse HEAD) >> ./VERSION
+ ((Get-Content -path ./Launcher/Launcher/Launcher.csproj -Raw) -replace '1.3.3.7',$NIXOS_WSL_VERSION) | Set-Content -Path ./Launcher/Launcher/Launcher.csproj
+ ((Get-Content -path ./Launcher/Launcher-Appx/Package.appxmanifest -Raw) -replace '1.3.3.7',$NIXOS_WSL_VERSION) | Set-Content -Path ./Launcher/Launcher-Appx/Package.appxmanifest
diff --git a/.github/workflows/on_push.yml b/.github/workflows/on_push.yml
index 8fc3997..e1739db 100644
--- a/.github/workflows/on_push.yml
+++ b/.github/workflows/on_push.yml
@@ -2,14 +2,23 @@ name: "Push"
on:
push:
- branches: [main]
+ branches: [main, distro-launcher]
pull_request: {}
jobs:
build:
- name: Build Tarballs 🛠️
+ name: Build 🛠️
uses: ./.github/workflows/run_build.yml
+ package:
+ name: Package 📦
+ uses: ./.github/workflows/run_package.yml
+ needs:
+ - build
+ secrets:
+ APPX_SIGNING_CERT: ${{ secrets.APPX_SIGNING_CERT }}
+ CERT_ALGORITHM: ${{ secrets.CERT_ALGORITHM }}
+
checks:
name: Flake Checks 📋
uses: ./.github/workflows/run_checks.yml
diff --git a/.github/workflows/on_release.yml b/.github/workflows/on_release.yml
index 5a6b677..49df902 100644
--- a/.github/workflows/on_release.yml
+++ b/.github/workflows/on_release.yml
@@ -6,23 +6,41 @@ on:
jobs:
build:
- name: Build Tarballs 🛠️
+ name: Build 🛠️
uses: ./.github/workflows/run_build.yml
+ package:
+ name: Package 📦
+ uses: ./.github/workflows/run_package.yml
+ needs:
+ - build
+ secrets:
+ APPX_SIGNING_CERT: ${{ secrets.APPX_SIGNING_CERT }}
+ CERT_ALGORITHM: ${{ secrets.CERT_ALGORITHM }}
+
release:
needs:
- build
+ - package
name: Create Release 📢
runs-on: ubuntu-latest
steps:
- name: Download Tarball 📥
uses: actions/download-artifact@v4
+ - name: Download appx 📥
+ uses: actions/download-artifact@v4
+ with:
+ name: appx
+
+ - name: Rename appx ✏️
+ run: mv */*.msixbundle NixOS-WSL.msixbundle
+
- name: Generate Checksums 🔑
run: |
mv tarball-modern/nixos-wsl.tar.gz nixos-wsl.tar.gz
mv tarball-legacy/nixos-wsl.tar.gz nixos-wsl-legacy.tar.gz
- for x in *.tar.gz; do
+ for x in *.{tar.gz,msixbundle}; do
sha256sum $x > ${x}.sha256
done
@@ -34,5 +52,7 @@ jobs:
nixos-wsl.tar.gz.sha256
nixos-wsl-legacy.tar.gz
nixos-wsl-legacy.tar.gz.sha256
+ NixOS-WSL.msixbundle
+ NixOS-WSL.msixbundle.sha256
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/run_build.yml b/.github/workflows/run_build.yml
index 0c9d90b..f30441e 100644
--- a/.github/workflows/run_build.yml
+++ b/.github/workflows/run_build.yml
@@ -1,11 +1,11 @@
-name: Build Tarballs
+name: Build Tarball
on:
workflow_call: {}
jobs:
- build:
- name: Build 🛠️
+ tar:
+ name: Tarball 🛠️
runs-on: ubuntu-latest
strategy:
matrix:
@@ -22,17 +22,12 @@ jobs:
- name: Install Nix ❄️
uses: ./.github/actions/install-nix
- - name: Generate Version 🏷️
+ - name: Set Version 🏷️
+ uses: ./.github/actions/version
id: version
- shell: pwsh
- run: |
- $LATEST_TAG = git describe --tags --abbrev=0 # Get the latest tag name
- $COMMIT_COUNT = git rev-list --use-bitmap-index --count "$(git rev-list --tags --no-walk --max-count=1)..HEAD" # Count all commits since the last tag
- $NIXOS_WSL_VERSION = "$($LATEST_TAG -replace '(.+)\.(.+)\.(.+)\..+', '$1.$2.$3').${COMMIT_COUNT}" # Compose the NixOS-WSL version number
- echo "version=$NIXOS_WSL_VERSION" >> $env:GITHUB_OUTPUT
- echo $NIXOS_WSL_VERSION > ./VERSION
- echo $(git rev-parse HEAD) >> ./VERSION
+ - name: Summary Headline 📝
+ run: echo '## NixOS-WSL ${{ steps.version.outputs.version }}' >> $GITHUB_STEP_SUMMARY
- name: Build Tarball 🛠️
uses: ./.github/actions/build-wsl-tarball
@@ -45,3 +40,31 @@ jobs:
with:
name: tarball-${{ matrix.config }}
path: nixos-wsl.tar.gz
+
+ launcher:
+ name: Launcher 🛠️
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup MSBuild 🧰
+ uses: ./.github/actions/setup-msbuild
+
+ - name: Set Versions 🏷️
+ uses: ./.github/actions/version
+
+ - name: Build Launcher
+ run: |
+ cd Launcher
+ dotnet publish --self-contained -r win-x64 -c Release -p:ContinuousIntegrationBuild=true -p:Deterministic=true --output Launcher\pkg Launcher
+
+ - name: Upload Launcher 📤
+ uses: actions/upload-artifact@v4
+ with:
+ name: launcher-bin
+ path: |
+ Launcher/Launcher/obj
+ Launcher/Launcher/pkg
diff --git a/.github/workflows/run_package.yml b/.github/workflows/run_package.yml
new file mode 100644
index 0000000..f30430f
--- /dev/null
+++ b/.github/workflows/run_package.yml
@@ -0,0 +1,91 @@
+name: Build Tarball
+
+on:
+ workflow_call:
+ secrets:
+ APPX_SIGNING_CERT: {}
+ CERT_ALGORITHM: {}
+
+jobs:
+ launcher-bundle:
+ name: ZIP 📦
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Download tarball 📥
+ uses: actions/download-artifact@v4
+ with:
+ name: tarball-modern
+
+ - name: Download launcher binaries 📥
+ uses: actions/download-artifact@v4
+ with:
+ name: launcher-bin
+ path: Launcher/Launcher/
+
+ - name: Bundle tarball with launcher 📦
+ run: |
+ Copy-Item nixos-wsl.tar.gz Launcher\Launcher\pkg\
+
+ - name: Upload Launcher 📤
+ uses: actions/upload-artifact@v4
+ with:
+ name: launcher-bundle
+ path: Launcher/Launcher/pkg
+
+ launcher-msix:
+ name: MSIX 📦
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Download tarball 📥
+ uses: actions/download-artifact@v4
+ with:
+ name: tarball-modern
+
+ - name: Download launcher binaries 📥
+ uses: actions/download-artifact@v4
+ with:
+ name: launcher-bin
+ path: Launcher/Launcher/
+
+ - name: Bundle tarball with launcher 📦
+ run: |
+ Copy-Item nixos-wsl.tar.gz Launcher\Launcher-Appx\
+
+ - name: Setup MSBuild 🧰
+ uses: ./.github/actions/setup-msbuild
+
+ - name: Set Versions 🏷️
+ uses: ./.github/actions/version
+
+ - name: Build MSIX package 📦
+ run: |
+ cd Launcher
+ nuget restore
+ msbuild Launcher-Appx\Launcher-Appx.wapproj /p:Configuration=Release /p:Platform=x64 /p:ContinuousIntegrationBuild=true /p:Deterministic=true
+
+ - name: Sign package ✒️
+ env:
+ APPX_SIGNING_CERT: ${{ secrets.APPX_SIGNING_CERT }}
+ CERT_ALGORITHM: ${{ secrets.CERT_ALGORITHM }}
+ if: env.APPX_SIGNING_CERT != ''
+ run: |
+ Write-Output $env:APPX_SIGNING_CERT > appx-cert.b64
+ certutil -decode appx-cert.b64 appx-cert.pfx
+ &"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\signtool.exe" sign /fd $env:CERT_ALGORITHM /a /f appx-cert.pfx $(Get-ChildItem -Recurse -Filter "*.msixbundle")[0].FullName
+ Get-PfxCertificate appx-cert.pfx | Export-Certificate -FilePath "$($(Get-ChildItem -Recurse -Filter "*.msixbundle")[0].Directory.FullName)\certificate.cer" -Type CERT
+
+ - name: Upload MSIX 📤
+ uses: actions/upload-artifact@v4
+ with:
+ name: launcher-msix
+ path: Launcher/Launcher-Appx/AppPackages/
diff --git a/Launcher/.editorconfig b/Launcher/.editorconfig
new file mode 100644
index 0000000..aa814c2
--- /dev/null
+++ b/Launcher/.editorconfig
@@ -0,0 +1,8 @@
+[*.cs]
+indent_size = 4
+csharp_new_line_before_open_brace = none
+csharp_style_var_when_type_is_apparent = true
+csharp_style_var_for_built_in_types = true
+csharp_new_line_before_else = false
+csharp_new_line_before_catch = false
+csharp_new_line_before_finally = false
diff --git a/Launcher/.gitignore b/Launcher/.gitignore
new file mode 100644
index 0000000..0bcb07a
--- /dev/null
+++ b/Launcher/.gitignore
@@ -0,0 +1,12 @@
+bin/
+*.dll
+*.exe
+*.appx
+*.cache
+.vs/
+.idea/
+*.DotSettings.user
+obj/
+AppPackages/
+BundleArtifacts/
+*.pfx
diff --git a/Launcher/Launcher-Appx/Images/LockScreenLogo.scale-200.png b/Launcher/Launcher-Appx/Images/LockScreenLogo.scale-200.png
new file mode 100644
index 0000000..d96f121
Binary files /dev/null and b/Launcher/Launcher-Appx/Images/LockScreenLogo.scale-200.png differ
diff --git a/Launcher/Launcher-Appx/Images/SplashScreen.scale-200.png b/Launcher/Launcher-Appx/Images/SplashScreen.scale-200.png
new file mode 100644
index 0000000..2bcd524
Binary files /dev/null and b/Launcher/Launcher-Appx/Images/SplashScreen.scale-200.png differ
diff --git a/Launcher/Launcher-Appx/Images/Square150x150Logo.scale-200.png b/Launcher/Launcher-Appx/Images/Square150x150Logo.scale-200.png
new file mode 100644
index 0000000..3932231
Binary files /dev/null and b/Launcher/Launcher-Appx/Images/Square150x150Logo.scale-200.png differ
diff --git a/Launcher/Launcher-Appx/Images/Square44x44Logo.scale-200.png b/Launcher/Launcher-Appx/Images/Square44x44Logo.scale-200.png
new file mode 100644
index 0000000..4422495
Binary files /dev/null and b/Launcher/Launcher-Appx/Images/Square44x44Logo.scale-200.png differ
diff --git a/Launcher/Launcher-Appx/Images/Square44x44Logo.targetsize-24_altform-unplated.png b/Launcher/Launcher-Appx/Images/Square44x44Logo.targetsize-24_altform-unplated.png
new file mode 100644
index 0000000..d7a5d2c
Binary files /dev/null and b/Launcher/Launcher-Appx/Images/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/Launcher/Launcher-Appx/Images/StoreLogo.png b/Launcher/Launcher-Appx/Images/StoreLogo.png
new file mode 100644
index 0000000..4e511c8
Binary files /dev/null and b/Launcher/Launcher-Appx/Images/StoreLogo.png differ
diff --git a/Launcher/Launcher-Appx/Images/Wide310x150Logo.scale-200.png b/Launcher/Launcher-Appx/Images/Wide310x150Logo.scale-200.png
new file mode 100644
index 0000000..f66a836
Binary files /dev/null and b/Launcher/Launcher-Appx/Images/Wide310x150Logo.scale-200.png differ
diff --git a/Launcher/Launcher-Appx/Launcher-Appx.wapproj b/Launcher/Launcher-Appx/Launcher-Appx.wapproj
new file mode 100644
index 0000000..6fe2390
--- /dev/null
+++ b/Launcher/Launcher-Appx/Launcher-Appx.wapproj
@@ -0,0 +1,66 @@
+
+
+
+ 15.0
+
+
+
+ Release
+ x64
+
+
+ Release
+ ARM64
+
+
+
+ $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\
+
+
+
+ b2533ea5-9dd5-41bb-bf00-a1d0fdba58b7
+ 10.0.22000.0
+ 10.0.18362.0
+ de-DE
+ False
+ $(NoWarn);NU1702
+ Launcher-Appx_TemporaryKey.pfx
+ False
+ SHA256
+ False
+ True
+ x64
+ 0
+ ..\Launcher\Launcher.csproj
+
+
+ en-US
+ Always
+
+
+ en-US
+ Always
+
+
+
+ Designer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Launcher/Launcher-Appx/Launcher-Appx.wapproj.user b/Launcher/Launcher-Appx/Launcher-Appx.wapproj.user
new file mode 100644
index 0000000..cfa75ec
--- /dev/null
+++ b/Launcher/Launcher-Appx/Launcher-Appx.wapproj.user
@@ -0,0 +1,24 @@
+
+
+
+ AppHostLocalDebugger
+ False
+
+
+ AppHostLocalDebugger
+
+
+ SideloadOnly
+ False
+ x64
+ False
+
+
+ AppHostLocalDebugger
+
+
+ AppHostLocalDebugger
+ False
+ Mixed
+
+
\ No newline at end of file
diff --git a/Launcher/Launcher-Appx/Package.appxmanifest b/Launcher/Launcher-Appx/Package.appxmanifest
new file mode 100644
index 0000000..705d27d
--- /dev/null
+++ b/Launcher/Launcher-Appx/Package.appxmanifest
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+ NixOS
+ nzbr
+ Images\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Launcher/Launcher.sln b/Launcher/Launcher.sln
new file mode 100644
index 0000000..ca7c13b
--- /dev/null
+++ b/Launcher/Launcher.sln
@@ -0,0 +1,52 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.1.32407.343
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Launcher", "Launcher\Launcher.csproj", "{8EB5612C-BFFA-43EF-AAC6-68AB01DA30CA}"
+EndProject
+Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "Launcher-Appx", "Launcher-Appx\Launcher-Appx.wapproj", "{B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{86D47DB8-0919-4929-9841-EC9B8FCB282F}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ .gitignore = .gitignore
+ default.nix = default.nix
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|ARM64 = Debug|ARM64
+ Debug|x64 = Debug|x64
+ Release|ARM64 = Release|ARM64
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {8EB5612C-BFFA-43EF-AAC6-68AB01DA30CA}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {8EB5612C-BFFA-43EF-AAC6-68AB01DA30CA}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {8EB5612C-BFFA-43EF-AAC6-68AB01DA30CA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8EB5612C-BFFA-43EF-AAC6-68AB01DA30CA}.Debug|x64.Build.0 = Debug|Any CPU
+ {8EB5612C-BFFA-43EF-AAC6-68AB01DA30CA}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {8EB5612C-BFFA-43EF-AAC6-68AB01DA30CA}.Release|ARM64.Build.0 = Release|Any CPU
+ {8EB5612C-BFFA-43EF-AAC6-68AB01DA30CA}.Release|x64.ActiveCfg = Release|Any CPU
+ {8EB5612C-BFFA-43EF-AAC6-68AB01DA30CA}.Release|x64.Build.0 = Release|Any CPU
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Debug|ARM64.ActiveCfg = Release|ARM64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Debug|ARM64.Build.0 = Release|ARM64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Debug|ARM64.Deploy.0 = Release|ARM64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Debug|x64.ActiveCfg = Release|x64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Debug|x64.Build.0 = Release|x64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Debug|x64.Deploy.0 = Release|x64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Release|ARM64.ActiveCfg = Release|ARM64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Release|ARM64.Build.0 = Release|ARM64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Release|ARM64.Deploy.0 = Release|ARM64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Release|x64.ActiveCfg = Release|x64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Release|x64.Build.0 = Release|x64
+ {B2533EA5-9DD5-41BB-BF00-A1D0FDBA58B7}.Release|x64.Deploy.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6515FF51-7BEF-41B2-A3ED-1D5025A3BFA4}
+ EndGlobalSection
+EndGlobal
diff --git a/Launcher/Launcher.sln.DotSettings b/Launcher/Launcher.sln.DotSettings
new file mode 100644
index 0000000..d7c541a
--- /dev/null
+++ b/Launcher/Launcher.sln.DotSettings
@@ -0,0 +1,8 @@
+
+ False
+ False
+ False
+ True
+ True
+ True
+ True
\ No newline at end of file
diff --git a/Launcher/Launcher/Launcher.csproj b/Launcher/Launcher/Launcher.csproj
new file mode 100644
index 0000000..770652a
--- /dev/null
+++ b/Launcher/Launcher/Launcher.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+ win-x64;win-arm64
+ NixOS
+ 1.3.3.7
+ nixos.ico
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Launcher/Launcher/nixos.ico b/Launcher/Launcher/nixos.ico
new file mode 100644
index 0000000..9c1db00
Binary files /dev/null and b/Launcher/Launcher/nixos.ico differ
diff --git a/Launcher/Launcher/src/Commands/Install.cs b/Launcher/Launcher/src/Commands/Install.cs
new file mode 100644
index 0000000..3e8adfc
--- /dev/null
+++ b/Launcher/Launcher/src/Commands/Install.cs
@@ -0,0 +1,30 @@
+using System.CommandLine;
+using Launcher.Helpers;
+
+namespace Launcher.Commands;
+
+public static class Install {
+ public static Command GetCommand() {
+ var command = new Command("install") {
+ Description = $"Install {DistributionInfo.DisplayName} if it has not been installed already"
+ };
+ var reinstallOpt = new Option("--reinstall") {
+ Description = "Delete the existing installation and install a fresh copy"
+ };
+ command.Add(reinstallOpt);
+
+ command.SetHandler((bool reinstall) => {
+ if (reinstall) {
+ var result = InstallationHelper.Uninstall();
+ if (result != 0) {
+ Program.result = result;
+ return;
+ }
+ }
+
+ Program.result = InstallationHelper.Install();
+ }, reinstallOpt);
+
+ return command;
+ }
+}
diff --git a/Launcher/Launcher/src/Commands/Registered.cs b/Launcher/Launcher/src/Commands/Registered.cs
new file mode 100644
index 0000000..15a5e9a
--- /dev/null
+++ b/Launcher/Launcher/src/Commands/Registered.cs
@@ -0,0 +1,31 @@
+using System.CommandLine;
+using WSL;
+
+namespace Launcher.Commands;
+
+public static class Registered {
+ public static Command GetCommand() {
+ var command = new Command("registered") {
+ Description = "Check whether or not the distribution is registered"
+ };
+ var quietOpt = new Option(new[] { "--quiet", "-q" }) {
+ Description = "Only return the appropriate exit code, dont write to the console"
+ };
+ command.Add(quietOpt);
+
+ command.SetHandler((bool quiet) => {
+ var registered = WslApiLoader.WslIsDistributionRegistered(DistributionInfo.Name);
+
+ if (!quiet) {
+ if (registered)
+ Console.WriteLine($"{DistributionInfo.Name} is registered");
+ else
+ Console.WriteLine($"{DistributionInfo.Name} is not registered");
+ }
+
+ Program.result = registered ? 0 : 1;
+ }, quietOpt);
+
+ return command;
+ }
+}
diff --git a/Launcher/Launcher/src/Commands/Run.cs b/Launcher/Launcher/src/Commands/Run.cs
new file mode 100644
index 0000000..e4aaf8c
--- /dev/null
+++ b/Launcher/Launcher/src/Commands/Run.cs
@@ -0,0 +1,40 @@
+using System.CommandLine;
+using Launcher.Helpers;
+using WSL;
+
+namespace Launcher.Commands;
+
+public static class Run {
+ public static Command GetCommand() {
+ var command = new Command("run") {
+ Description =
+ "Run a command in the current directory. If no command is specified, the default shell is launched"
+ };
+ var argCmd = new Argument("command") {
+ Arity = ArgumentArity.ZeroOrOne
+ };
+ command.AddArgument(argCmd);
+
+ command.SetHandler((string? cmd) => {
+ uint exitCode;
+
+ if (!WslApiLoader.WslIsDistributionRegistered(DistributionInfo.Name)) {
+ Console.Error.WriteLine($"{DistributionInfo.DisplayName} is not installed!");
+ Program.result = 1;
+ return;
+ }
+
+ try {
+ WslApiLoader.WslLaunchInteractive(DistributionInfo.Name, cmd, true, out exitCode);
+ } catch (WslApiException e) {
+ Console.Error.WriteLine("An error occured when starting the command!");
+ Program.result = e.HResult;
+ return;
+ }
+
+ Program.result = (int)exitCode;
+ }, argCmd);
+
+ return command;
+ }
+}
diff --git a/Launcher/Launcher/src/Commands/Uninstall.cs b/Launcher/Launcher/src/Commands/Uninstall.cs
new file mode 100644
index 0000000..a2a4ba3
--- /dev/null
+++ b/Launcher/Launcher/src/Commands/Uninstall.cs
@@ -0,0 +1,16 @@
+using System.CommandLine;
+using Launcher.Helpers;
+
+namespace Launcher.Commands;
+
+public static class Uninstall {
+ public static Command GetCommand() {
+ var command = new Command("uninstall") {
+ Description = $"Uninstall {DistributionInfo.DisplayName}"
+ };
+
+ command.SetHandler(() => { Program.result = InstallationHelper.Uninstall(); });
+
+ return command;
+ }
+}
diff --git a/Launcher/Launcher/src/DistributionInfo.cs b/Launcher/Launcher/src/DistributionInfo.cs
new file mode 100644
index 0000000..84bbda3
--- /dev/null
+++ b/Launcher/Launcher/src/DistributionInfo.cs
@@ -0,0 +1,9 @@
+namespace Launcher;
+
+internal static class DistributionInfo {
+ public const string DisplayName = "NixOS";
+ public const string WindowTitle = "NixOS";
+
+ // Name that the distro is registered as in WSL. Can be overridden with --distro-name
+ public static string Name = "NixOS";
+}
diff --git a/Launcher/Launcher/src/Helpers/InstallationHelper.cs b/Launcher/Launcher/src/Helpers/InstallationHelper.cs
new file mode 100644
index 0000000..090aded
--- /dev/null
+++ b/Launcher/Launcher/src/Helpers/InstallationHelper.cs
@@ -0,0 +1,92 @@
+using System.Reflection;
+using WSL;
+
+namespace Launcher.Helpers;
+
+public static class InstallationHelper {
+ ///
+ /// Registers the distribution and runs first-time setup
+ ///
+ /// 0 on success, an error code otherwise
+ public static int Install() {
+ Console.WriteLine($"Registering {DistributionInfo.DisplayName}...");
+
+ if (WslApiLoader.WslIsDistributionRegistered(DistributionInfo.Name)) {
+ Console.Error.WriteLine($"{DistributionInfo.DisplayName} is already installed!");
+ return 0;
+ }
+
+ // Determine tarball location
+ var tarPath = FindTarball();
+ if (tarPath == null) {
+ Console.Error.WriteLine("Could not find distro tarball");
+ return 1;
+ }
+
+ try {
+ WslApiLoader.WslRegisterDistribution(
+ DistributionInfo.Name,
+ tarPath
+ );
+ } catch (WslApiException e) {
+ Console.Error.WriteLine("There was an error registering the distribution");
+ return e.HResult;
+ }
+
+ Console.WriteLine("Installation finished successfully");
+ return 0;
+ }
+
+ ///
+ /// Unregister the distribution
+ ///
+ /// 0 on success and an error code otherwise
+ public static int Uninstall() {
+ Console.WriteLine($"Uninstalling {DistributionInfo.DisplayName}...");
+ if (!WslApiLoader.WslIsDistributionRegistered(DistributionInfo.Name)) {
+ Console.Error.WriteLine($"{DistributionInfo.DisplayName} is not installed!");
+ return 1;
+ }
+
+ try {
+ WslApiLoader.WslUnregisterDistribution(DistributionInfo.Name);
+ } catch (WslApiException e) {
+ Console.Error.WriteLine("An error occured when unregistering the distribution!");
+ return e.HResult;
+ }
+
+ Console.WriteLine("Uninstall completed");
+ return 0;
+ }
+
+ ///
+ /// Find the path of the installer tarball
+ ///
+ /// the full path to the tarball or null
+ public static string? FindTarball() {
+ const string tarFileName = "nixos-wsl.tar.gz";
+
+ // Accept a tarball in the current directory when running a debug build
+#if (DEBUG)
+ var pwd = Directory.GetCurrentDirectory();
+ var debugTarPath = Path.Combine(pwd, tarFileName);
+ if (File.Exists(debugTarPath)) return debugTarPath;
+#endif
+
+ // Look for the tarball in the same directory as the executable
+ var assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ if (assemblyPath == null) return null;
+
+ var tarPath = Path.Combine(assemblyPath, tarFileName);
+ if (File.Exists(tarPath)) return tarPath;
+
+ // In the APPX package, the tarball is in the parent directory
+ var parentDirectory = Directory.GetParent(assemblyPath)?.FullName;
+ if (parentDirectory == null) return null;
+
+ tarPath = Path.Combine(parentDirectory, tarFileName);
+ return File.Exists(tarPath)
+ ? tarPath
+ : null;
+ }
+}
diff --git a/Launcher/Launcher/src/Helpers/StartupHelper.cs b/Launcher/Launcher/src/Helpers/StartupHelper.cs
new file mode 100644
index 0000000..0c67886
--- /dev/null
+++ b/Launcher/Launcher/src/Helpers/StartupHelper.cs
@@ -0,0 +1,29 @@
+using WSL;
+
+namespace Launcher.Helpers;
+
+public static class StartupHelper {
+ private static bool booted;
+
+ public static bool BootDistro() {
+ if (booted) return true;
+ try {
+ WslApiLoader.WslLaunchInteractive(
+ DistributionInfo.Name,
+ "/bin/sh -c \"exit\"", // Don't run anything, exit after we get a shell
+ false,
+ out var exitCode
+ );
+ if (exitCode != 0) {
+ Console.Error.WriteLine("An error occured during the distro's startup process");
+ return false;
+ }
+ } catch (WslApiException) {
+ Console.Error.WriteLine("An error occured while trying to run a command in the distro");
+ return false;
+ }
+
+ booted = true;
+ return true;
+ }
+}
diff --git a/Launcher/Launcher/src/Helpers/VersionHelper.cs b/Launcher/Launcher/src/Helpers/VersionHelper.cs
new file mode 100644
index 0000000..3d4422e
--- /dev/null
+++ b/Launcher/Launcher/src/Helpers/VersionHelper.cs
@@ -0,0 +1,60 @@
+using System.Reflection;
+using System.Text.Json;
+using WSL;
+
+namespace Launcher.Helpers;
+
+public static class VersionHelper {
+ public static Version? LauncherVersion => typeof(VersionHelper).Assembly.GetName().Version;
+
+ public static NixosWslVersion? InstalledVersion {
+ get {
+ try {
+ if (StartupHelper.BootDistro()) {
+ var output = WslApiLoader.WslLaunchGetOutput(
+ DistributionInfo.Name,
+ "/bin/sh --login -c \"nixos-wsl-version --json\"",
+ false,
+ out var exitCode,
+ true
+ ).Trim();
+
+ var json = JsonSerializer.Deserialize>(output);
+ var version = json!["release"];
+
+ if (exitCode == 0) return new NixosWslVersion(version);
+ }
+ } catch (Exception) {
+ // ignored
+ }
+
+ return null;
+ }
+ }
+
+ public class NixosWslVersion {
+ private readonly Version _version;
+ private readonly string _versionString;
+
+ internal NixosWslVersion(string versionString) {
+ _versionString = versionString;
+
+ if (Version.TryParse(versionString, out var ver)) {
+ _version = ver;
+ return;
+ }
+
+ _version = versionString == "DEV_BUILD"
+ ? new Version(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue)
+ : new Version(0, 0, 0, 0);
+ }
+
+ public Version AsVersion() {
+ return _version;
+ }
+
+ public override string ToString() {
+ return _versionString;
+ }
+ }
+}
diff --git a/Launcher/Launcher/src/Program.cs b/Launcher/Launcher/src/Program.cs
new file mode 100644
index 0000000..2e8609c
--- /dev/null
+++ b/Launcher/Launcher/src/Program.cs
@@ -0,0 +1,102 @@
+using System.CommandLine;
+using System.CommandLine.Builder;
+using System.CommandLine.Invocation;
+using System.CommandLine.IO;
+using System.CommandLine.Parsing;
+using Launcher.Commands;
+using Launcher.Helpers;
+using WSL;
+
+namespace Launcher;
+
+internal static class Program {
+ public static int result;
+
+ public static async Task Main(string[] args) {
+ // Set title of the console
+ Console.Title = DistributionInfo.WindowTitle;
+
+ // Argparse docs: https://github.com/dotnet/command-line-api
+
+ var rootCommand = new RootCommand(
+ $"Manage the {DistributionInfo.DisplayName} WSL distribution"
+ ) {
+ Run.GetCommand(),
+ Install.GetCommand(),
+ Registered.GetCommand(),
+ Uninstall.GetCommand(),
+ };
+
+ var distroNameOption = new Option("--distro-name") {
+ Description = "WSL distribution name",
+ Arity = ArgumentArity.ZeroOrOne
+ };
+ distroNameOption.SetDefaultValue(DistributionInfo.Name);
+
+ var versionOption = new Option("--version") {
+ Description = "Show version information"
+ };
+
+ // Add the global options to the root command and all subcommands
+ rootCommand.AddOption(distroNameOption);
+ rootCommand.AddOption(versionOption);
+ foreach (var subcommand in rootCommand.Subcommands) {
+ subcommand.AddOption(distroNameOption);
+ subcommand.AddOption(versionOption);
+ }
+
+ rootCommand.SetHandler(() => {
+ if (!WslApiLoader.WslIsDistributionRegistered(DistributionInfo.Name)) {
+ result = InstallationHelper.Install();
+ if (result != 0) return;
+ }
+
+ try {
+ WslApiLoader.WslLaunchInteractive(DistributionInfo.Name, null, true, out var exitCode);
+ result = (int)exitCode;
+ } catch (WslApiException e) {
+ Console.Error.WriteLine("An error occured when starting the shell");
+ result = e.HResult;
+ }
+ });
+
+ var commandLineBuilder = new CommandLineBuilder(rootCommand)
+ .UseHelp()
+ .UseEnvironmentVariableDirective()
+ .UseParseDirective()
+ .UseSuggestDirective()
+ .RegisterWithDotnetSuggest()
+ .UseTypoCorrections()
+ .UseParseErrorReporting()
+ .UseExceptionHandler()
+ .CancelOnProcessTermination();
+
+ // Implement --distro-name option
+ commandLineBuilder.AddMiddleware(async (context, next) => {
+ var distroNameResult = context.ParseResult.FindResultFor(distroNameOption);
+
+ if (distroNameResult is { Tokens.Count: > 0 }) DistributionInfo.Name = distroNameResult.Tokens[0].ToString();
+
+ await next(context);
+ }, (MiddlewareOrder)(-1300)); // Run before --version
+
+ // Implement --version option
+ commandLineBuilder.AddMiddleware(async (context, next) => {
+ var versionResult = context.ParseResult.FindResultFor(versionOption);
+
+ if (versionResult != null) {
+ var vl = VersionHelper.LauncherVersion?.ToString();
+ var vi = VersionHelper.InstalledVersion?.ToString() ?? "UNKNOWN";
+
+ context.Console.Out.WriteLine($"Launcher: {vl}");
+ context.Console.Out.WriteLine($"Module: {vi}");
+ } else {
+ await next(context);
+ }
+ }, (MiddlewareOrder)(-1200)); // Internal value for the builtin version option
+
+ await commandLineBuilder.Build().InvokeAsync(args);
+
+ return result;
+ }
+}
diff --git a/Launcher/Launcher/src/WSL/Kernel32/CloseHandle.cs b/Launcher/Launcher/src/WSL/Kernel32/CloseHandle.cs
new file mode 100644
index 0000000..f4e3fb5
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/Kernel32/CloseHandle.cs
@@ -0,0 +1,12 @@
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Security;
+
+namespace WSL.Kernel32;
+
+public static partial class Kernel32Loader {
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [SuppressUnmanagedCodeSecurity]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool CloseHandle(IntPtr hObject);
+}
diff --git a/Launcher/Launcher/src/WSL/Kernel32/CreatePipe.cs b/Launcher/Launcher/src/WSL/Kernel32/CreatePipe.cs
new file mode 100644
index 0000000..3b21417
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/Kernel32/CreatePipe.cs
@@ -0,0 +1,13 @@
+using System.Runtime.InteropServices;
+
+namespace WSL.Kernel32;
+
+public static partial class Kernel32Loader {
+ [DllImport("kernel32.dll")]
+ public static extern bool CreatePipe(
+ out IntPtr hReadPipe,
+ out IntPtr hWritePipe,
+ ref SECURITY_ATTRIBUTES lpPipeAttributes,
+ uint nSize
+ );
+}
diff --git a/Launcher/Launcher/src/WSL/Kernel32/GetExitCodeProcess.cs b/Launcher/Launcher/src/WSL/Kernel32/GetExitCodeProcess.cs
new file mode 100644
index 0000000..2e56d79
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/Kernel32/GetExitCodeProcess.cs
@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace WSL.Kernel32;
+
+public static partial class Kernel32Loader {
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
+}
diff --git a/Launcher/Launcher/src/WSL/Kernel32/GetProcessId.cs b/Launcher/Launcher/src/WSL/Kernel32/GetProcessId.cs
new file mode 100644
index 0000000..fdcef68
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/Kernel32/GetProcessId.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace WSL.Kernel32;
+
+public static partial class Kernel32Loader {
+ [DllImport("kernel32.dll")]
+ public static extern int GetProcessId(IntPtr handle);
+}
diff --git a/Launcher/Launcher/src/WSL/Kernel32/GetStdHandle.cs b/Launcher/Launcher/src/WSL/Kernel32/GetStdHandle.cs
new file mode 100644
index 0000000..1633235
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/Kernel32/GetStdHandle.cs
@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+
+namespace WSL.Kernel32;
+
+public static partial class Kernel32Loader {
+ public static int STD_INPUT_HANDLE = -10;
+ public static int STD_OUTPUT_HANDLE = -11;
+ public static int STD_ERROR_HANDLE = -12;
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern IntPtr GetStdHandle(int nStdHandle);
+}
diff --git a/Launcher/Launcher/src/WSL/Kernel32/ReadFile.cs b/Launcher/Launcher/src/WSL/Kernel32/ReadFile.cs
new file mode 100644
index 0000000..dff4485
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/Kernel32/ReadFile.cs
@@ -0,0 +1,14 @@
+using System.Runtime.InteropServices;
+
+namespace WSL.Kernel32;
+
+public static partial class Kernel32Loader {
+ [DllImport(@"kernel32.dll", SetLastError = true)]
+ public static extern unsafe bool ReadFile(
+ IntPtr hFile,
+ IntPtr pBuffer,
+ int NumberOfBytesToRead,
+ out int pNumberOfBytesRead,
+ IntPtr lpOverlapped
+ );
+}
diff --git a/Launcher/Launcher/src/WSL/Kernel32/SecurityAttributes.cs b/Launcher/Launcher/src/WSL/Kernel32/SecurityAttributes.cs
new file mode 100644
index 0000000..dab4d00
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/Kernel32/SecurityAttributes.cs
@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+
+namespace WSL.Kernel32;
+
+public static partial class Kernel32Loader {
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SECURITY_ATTRIBUTES {
+ public int nLength;
+ public IntPtr lpSecurityDescriptor;
+ public bool bInheritHandle;
+ }
+}
diff --git a/Launcher/Launcher/src/WSL/Kernel32/WaitForSingleObject.cs b/Launcher/Launcher/src/WSL/Kernel32/WaitForSingleObject.cs
new file mode 100644
index 0000000..ce43674
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/Kernel32/WaitForSingleObject.cs
@@ -0,0 +1,28 @@
+using System.Runtime.InteropServices;
+
+namespace WSL.Kernel32;
+
+public static partial class Kernel32Loader {
+ public const UInt32 INFINITE = 0xFFFFFFFF;
+ const UInt32 WAIT_ABANDONED = 0x00000080;
+ const UInt32 WAIT_OBJECT_0 = 0x00000000;
+ const UInt32 WAIT_TIMEOUT = 0x00000102;
+
+ public static void WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds) {
+ [DllImport("kernel32.dll", SetLastError = true)]
+ // ReSharper disable once LocalFunctionHidesMethod
+ static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
+
+ var result = WaitForSingleObject(hHandle, dwMilliseconds);
+ if (result == WAIT_OBJECT_0) {
+ return;
+ }
+ if (result == WAIT_TIMEOUT) {
+ throw new TimeoutException();
+ }
+ if (result == WAIT_ABANDONED) {
+ throw new AbandonedMutexException();
+ }
+ throw new Exception("WaitForSingleObject failed");
+ }
+}
diff --git a/Launcher/Launcher/src/WSL/WslApiException.cs b/Launcher/Launcher/src/WSL/WslApiException.cs
new file mode 100644
index 0000000..2cfaceb
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslApiException.cs
@@ -0,0 +1,17 @@
+namespace WSL;
+
+public class WslApiException : Exception {
+
+ private long hresult;
+ WslApiException(long hresult) {
+ this.hresult = hresult;
+ }
+
+ public static void checkResult(long hresult) {
+ if (hresult != 0) {
+ throw new WslApiException(hresult);
+ }
+ }
+
+ public override string Message => $"WSL API Error {hresult}";
+}
diff --git a/Launcher/Launcher/src/WSL/WslConfigureDistribution.cs b/Launcher/Launcher/src/WSL/WslConfigureDistribution.cs
new file mode 100644
index 0000000..dcf0cff
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslConfigureDistribution.cs
@@ -0,0 +1,20 @@
+using System.Runtime.InteropServices;
+
+namespace WSL;
+
+public static partial class WslApiLoader {
+ public static void WslConfigureDistribution(
+ string distributionName,
+ ulong defaultUID,
+ WSL_DISTRIBUTION_FLAGS wslDistributionFlags
+ ) {
+ [DllImport("wslapi.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
+ // ReSharper disable once LocalFunctionHidesMethod
+ static extern long WslConfigureDistribution(
+ string distributionName,
+ ulong defaultUID,
+ WSL_DISTRIBUTION_FLAGS wslDistributionFlags
+ );
+ WslApiException.checkResult(WslConfigureDistribution(distributionName, defaultUID, wslDistributionFlags));
+ }
+}
diff --git a/Launcher/Launcher/src/WSL/WslDistributionFlags.cs b/Launcher/Launcher/src/WSL/WslDistributionFlags.cs
new file mode 100644
index 0000000..83c06e3
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslDistributionFlags.cs
@@ -0,0 +1,21 @@
+namespace WSL;
+
+public static partial class WslApiLoader {
+ [Flags]
+ public enum WSL_DISTRIBUTION_FLAGS {
+ WSL_DISTRIBUTION_FLAGS_NONE = 0x0,
+ WSL_DISTRIBUTION_FLAGS_ENABLE_INTEROP = 0x1,
+ WSL_DISTRIBUTION_FLAGS_APPEND_NT_PATH = 0x2,
+ WSL_DISTRIBUTION_FLAGS_ENABLE_DRIVE_MOUNTING = 0x4
+ }
+
+ public static WSL_DISTRIBUTION_FLAGS WSL_DISTRIBUTION_FLAGS_VALID =
+ WSL_DISTRIBUTION_FLAGS.WSL_DISTRIBUTION_FLAGS_ENABLE_INTEROP |
+ WSL_DISTRIBUTION_FLAGS.WSL_DISTRIBUTION_FLAGS_APPEND_NT_PATH |
+ WSL_DISTRIBUTION_FLAGS.WSL_DISTRIBUTION_FLAGS_ENABLE_DRIVE_MOUNTING;
+
+ public static WSL_DISTRIBUTION_FLAGS WSL_DISTRIBUTION_FLAGS_DEFAULT =
+ WSL_DISTRIBUTION_FLAGS.WSL_DISTRIBUTION_FLAGS_ENABLE_INTEROP |
+ WSL_DISTRIBUTION_FLAGS.WSL_DISTRIBUTION_FLAGS_APPEND_NT_PATH |
+ WSL_DISTRIBUTION_FLAGS.WSL_DISTRIBUTION_FLAGS_ENABLE_DRIVE_MOUNTING;
+}
diff --git a/Launcher/Launcher/src/WSL/WslGetDistributionConfiguration.cs b/Launcher/Launcher/src/WSL/WslGetDistributionConfiguration.cs
new file mode 100644
index 0000000..222270e
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslGetDistributionConfiguration.cs
@@ -0,0 +1,63 @@
+using System.Data;
+using System.Runtime.InteropServices;
+
+namespace WSL;
+
+public static partial class WslApiLoader {
+ // Wrap this method, so that we can get a managed string array out of it instead of char***
+ public static unsafe void WslGetDistributionConfiguration(
+ string distributionName,
+ out ulong distributionVersion,
+ out ulong defaultUID,
+ out WslApiLoader.WSL_DISTRIBUTION_FLAGS wslDistributionFlags,
+ out string[] defaultEnvironmentVariables
+ ) {
+ [DllImport("wslapi.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
+ // ReSharper disable once LocalFunctionHidesMethod
+ static extern long WslGetDistributionConfiguration(
+ string distributionName,
+ out ulong distributionVersion,
+ out ulong defaultUID,
+ out WslApiLoader.WSL_DISTRIBUTION_FLAGS wslDistributionFlags,
+ out byte** defaultEnvironmentVariables,
+ out ulong defaultEnvironmentVariableCount
+ );
+
+ byte** envv;
+ ulong envc;
+
+ WslApiException.checkResult(
+ WslGetDistributionConfiguration(
+ distributionName,
+ out distributionVersion,
+ out defaultUID,
+ out wslDistributionFlags,
+ out envv,
+ out envc)
+ );
+
+ defaultEnvironmentVariables = new string[envc];
+
+ var anyNull = false;
+ for (ulong i = 0; i < envc; i++) {
+ var ptr = new IntPtr(*(envv + i)); // Get the pointer to the string at offset i and dereference it
+ var str = Marshal
+ .PtrToStringAnsi(ptr); // Create a managed string from the char* pointer (_needs_ to be ANSI)
+
+ if (str == null) {
+ anyNull = true;
+ continue;
+ }
+
+ Marshal.FreeHGlobal(ptr); // Free the string
+
+ defaultEnvironmentVariables[i] = str;
+ }
+
+ Marshal.FreeHGlobal(new IntPtr(envv)); // Free the array
+
+ if (anyNull)
+ throw new NoNullAllowedException(
+ "One of the strings in the environment array returned by wslapi.dll is null");
+ }
+}
diff --git a/Launcher/Launcher/src/WSL/WslIsDistributionRegistered.cs b/Launcher/Launcher/src/WSL/WslIsDistributionRegistered.cs
new file mode 100644
index 0000000..daf9d86
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslIsDistributionRegistered.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace WSL;
+
+public static partial class WslApiLoader {
+ [DllImport("wslapi.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
+ public static extern bool WslIsDistributionRegistered(string distributionName);
+}
diff --git a/Launcher/Launcher/src/WSL/WslLaunch.cs b/Launcher/Launcher/src/WSL/WslLaunch.cs
new file mode 100644
index 0000000..63cd6cd
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslLaunch.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using WSL.Kernel32;
+
+namespace WSL;
+
+public static partial class WslApiLoader {
+ public static void WslLaunch(
+ string distributionName,
+ string? command,
+ bool useCurrentWorkingDirectory,
+ IntPtr stdIn,
+ IntPtr stdOut,
+ IntPtr stdErr,
+ out IntPtr process
+ ) {
+ [DllImport("wslapi.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
+ // ReSharper disable once LocalFunctionHidesMethod
+ static extern long WslLaunch(
+ string distributionName,
+ string? command,
+ bool useCurrentWorkingDirectory,
+ IntPtr stdIn,
+ IntPtr stdOut,
+ IntPtr stdErr,
+ out IntPtr process
+ );
+
+ WslApiException.checkResult(
+ WslLaunch(
+ distributionName,
+ command,
+ useCurrentWorkingDirectory,
+ stdIn,
+ stdOut,
+ stdErr,
+ out process
+ )
+ );
+ }
+}
diff --git a/Launcher/Launcher/src/WSL/WslLaunchGetOutput.cs b/Launcher/Launcher/src/WSL/WslLaunchGetOutput.cs
new file mode 100644
index 0000000..b3a78ba
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslLaunchGetOutput.cs
@@ -0,0 +1,84 @@
+using System.Runtime.InteropServices;
+using System.Text;
+using WSL.Kernel32;
+
+namespace WSL;
+
+public static partial class WslApiLoader {
+
+ // Contains code from https://github.com/wslhub/wsl-sdk-dotnet/blob/892e8b5564170eae9944649cf8ab424ce0fbce52/src/Wslhub.Sdk/Wsl.cs#L364
+ public static unsafe string WslLaunchGetOutput(
+ string distributionName,
+ string command,
+ bool useCurrentWorkingDirectory,
+ out uint exitCode,
+ bool noStderr = false
+ ) {
+ var stdin = Kernel32Loader.GetStdHandle(Kernel32Loader.STD_INPUT_HANDLE);
+ var stderr = Kernel32Loader.GetStdHandle(Kernel32Loader.STD_ERROR_HANDLE);
+
+ var attributes = new Kernel32Loader.SECURITY_ATTRIBUTES {
+ lpSecurityDescriptor = IntPtr.Zero,
+ bInheritHandle = true,
+ };
+ attributes.nLength = Marshal.SizeOf(attributes);
+
+ if (!Kernel32Loader.CreatePipe(out IntPtr readPipe, out IntPtr writePipe, ref attributes, 0))
+ throw new Exception("Cannot create stdout pipe");
+
+ var stderrRead = IntPtr.Zero;
+ var stderrWrite = IntPtr.Zero;
+ if (noStderr) {
+ if (!Kernel32Loader.CreatePipe(out stderrRead, out stderrWrite, ref attributes, 0))
+ throw new Exception("Cannot create stderr pipe");
+ stderr = stderrWrite;
+ }
+
+ try {
+ WslLaunch(distributionName, command, useCurrentWorkingDirectory, stdin, writePipe, stderr,
+ out var hProcess);
+ Kernel32Loader.WaitForSingleObject(hProcess, Kernel32Loader.INFINITE);
+
+ if (!Kernel32Loader.GetExitCodeProcess(hProcess, out exitCode)) {
+ Kernel32Loader.CloseHandle(hProcess);
+ throw new Exception("Could not get exit code of WSL process");
+ }
+
+ Kernel32Loader.CloseHandle(hProcess);
+ Kernel32Loader.CloseHandle(writePipe); // Make sure the pipe is closed if the spawned process has not done that
+
+ const int bufferLength = 65536;
+ var bufferPointer = Marshal.AllocHGlobal(bufferLength);
+ var outputContents = new StringBuilder();
+ var encoding = new UTF8Encoding(false);
+ var read = 0;
+
+ do {
+ if (!Kernel32Loader.ReadFile(readPipe, bufferPointer, bufferLength, out read, IntPtr.Zero)) {
+ var lastError = Marshal.GetLastWin32Error();
+
+ if (lastError != 0) {
+ Marshal.FreeHGlobal(bufferPointer);
+ throw new Exception("Could not read from pipe");
+ }
+
+ break;
+ }
+
+ outputContents.Append(encoding.GetString((byte*)bufferPointer.ToPointer(), read));
+ } while (read == bufferLength);
+
+ Marshal.FreeHGlobal(bufferPointer);
+
+ return outputContents.ToString();
+ } finally {
+ Kernel32Loader.CloseHandle(readPipe);
+ Kernel32Loader.CloseHandle(writePipe);
+ if (noStderr) {
+ Kernel32Loader.CloseHandle(stderrRead);
+ Kernel32Loader.CloseHandle(stderrWrite);
+ }
+ }
+ }
+
+}
diff --git a/Launcher/Launcher/src/WSL/WslLaunchInteractive.cs b/Launcher/Launcher/src/WSL/WslLaunchInteractive.cs
new file mode 100644
index 0000000..5c85aab
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslLaunchInteractive.cs
@@ -0,0 +1,31 @@
+using System.Runtime.InteropServices;
+
+namespace WSL;
+
+public static partial class WslApiLoader {
+ public static unsafe void WslLaunchInteractive(
+ string distributionName,
+ string? command,
+ bool useCurrentWorkingDirectory,
+ out uint exitCode
+ ) {
+ [DllImport("wslapi.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = true)]
+ // ReSharper disable once LocalFunctionHidesMethod
+ static extern long WslLaunchInteractive(
+ [In] string distributionName,
+ [In] string? command,
+ [In] bool useCurrentWorkingDirectory,
+ [Out, MarshalAs(UnmanagedType.U4)] out uint exitCode
+ );
+
+ WslApiException.checkResult(
+ WslLaunchInteractive(
+ distributionName,
+ command,
+ useCurrentWorkingDirectory,
+ out exitCode
+ )
+ );
+ Console.WriteLine(exitCode);
+ }
+}
diff --git a/Launcher/Launcher/src/WSL/WslRegisterDistribution.cs b/Launcher/Launcher/src/WSL/WslRegisterDistribution.cs
new file mode 100644
index 0000000..d40e55c
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslRegisterDistribution.cs
@@ -0,0 +1,24 @@
+using System.Runtime.InteropServices;
+
+namespace WSL;
+
+public static partial class WslApiLoader {
+ public static void WslRegisterDistribution(
+ string distributionName,
+ string tarGzFilename
+ ) {
+ [DllImport("wslapi.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
+ // ReSharper disable once LocalFunctionHidesMethod
+ static extern long WslRegisterDistribution(
+ string distributionName,
+ string tarGzFilename
+ );
+
+ WslApiException.checkResult(
+ WslRegisterDistribution(
+ distributionName,
+ tarGzFilename
+ )
+ );
+ }
+}
diff --git a/Launcher/Launcher/src/WSL/WslUnregisterDistribution.cs b/Launcher/Launcher/src/WSL/WslUnregisterDistribution.cs
new file mode 100644
index 0000000..e66950e
--- /dev/null
+++ b/Launcher/Launcher/src/WSL/WslUnregisterDistribution.cs
@@ -0,0 +1,19 @@
+using System.Runtime.InteropServices;
+
+namespace WSL;
+
+public static partial class WslApiLoader {
+ public static void WslUnregisterDistribution(
+ string distributionName
+ ) {
+ [DllImport("wslapi.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
+ // ReSharper disable once LocalFunctionHidesMethod
+ static extern long WslUnregisterDistribution(string distributionName);
+
+ WslApiException.checkResult(
+ WslUnregisterDistribution(
+ distributionName
+ )
+ );
+ }
+}
diff --git a/Launcher/default.nix b/Launcher/default.nix
new file mode 100644
index 0000000..781a611
--- /dev/null
+++ b/Launcher/default.nix
@@ -0,0 +1,95 @@
+{ pkgs ? import { }
+, stdenv ? pkgs.stdenv
+, lib ? pkgs.lib
+, dotnet-sdk ? pkgs.dotnet-sdk
+, dotnetPackages ? pkgs.dotnetPackages
+, fetchurl ? pkgs.fetchurl
+, linkFarmFromDrvs ? pkgs.linkFarmFromDrvs
+}:
+
+let
+ fetchNuGet = { pname, version, hash }: fetchurl {
+ name = "${pname}-${version}.nupkg";
+ url = "https://www.nuget.org/api/v2/package/${pname}/${version}";
+ inherit hash;
+ };
+in
+stdenv.mkDerivation rec {
+ name = "NixOS-WSL-Launcher";
+
+ src = ./.;
+
+ nativeBuildInputs = [
+ dotnet-sdk
+ dotnetPackages.Nuget
+ ];
+
+ nugetDeps = linkFarmFromDrvs "${name}-nuget-deps" (map fetchNuGet [
+ { pname = "Microsoft.AspNetCore.App.Runtime.win-x64"; version = "6.0.25"; hash = "sha256-njOrSWL+YJJvnywJ0clMzYdaANYl3rcVaZeZAki1erk="; }
+ { pname = "Microsoft.AspNetCore.App.Runtime.win-arm64"; version = "6.0.25"; hash = "sha256-Jnt9WIxGvvejtBW5mjWCXlYmsUj6fxVI4Xs3WWsr8YE="; }
+ { pname = "Microsoft.NETCore.App.Host.win-x64"; version = "6.0.25"; hash = "sha256-ATiO6e8xcDPrRngmT9k//7Z2Mx07hJhxgmdwUvMYJA0="; }
+ { pname = "Microsoft.NETCore.App.Host.win-arm64"; version = "6.0.25"; hash = "sha256-xvj8CZHY4PyXKroniWj0JIoksKf4pcbberfMf5XZYaE="; }
+ { pname = "Microsoft.NETCore.App.Runtime.win-x64"; version = "6.0.25"; hash = "sha256-Lk6cn0cSQVp4QVf6IznE7UnZ9tYxZmRMYrsTIHq9Vg8="; }
+ { pname = "Microsoft.NETCore.App.Runtime.win-arm64"; version = "6.0.25"; hash = "sha256-eCoXKCHkrteypFVOUctV3ReQk/lFjJ7cI/CVk2tNrZY="; }
+ { pname = "System.CommandLine"; version = "2.0.0-beta3.22114.1"; hash = "sha256-lcqMVesbt2E189e6KfG8HVmJc3hjo2ht48WnH1UaMkY="; }
+ ]);
+
+ unpackPhase = ''
+ cp -r --no-preserve=mode $src/. .
+ '';
+
+ configurePhase = ''
+ runHook preConfigure
+
+ mkdir .home
+ export HOME=$PWD/.home
+
+ export DOTNET_NOLOGO=1
+ export DOTNET_CLI_TELEMETRY_OPTOUT=1
+
+ nuget sources Add -Name nix -Source "$PWD/nix"
+ nuget init "$nugetDeps" "$PWD/nix"
+ dotnet nuget disable source nuget.org
+
+ mkdir -p $HOME/.nuget/NuGet
+ cp $HOME/.config/NuGet/NuGet.Config $HOME/.nuget/NuGet
+
+ dotnet restore --ignore-failed-sources
+
+ runHook postConfigure
+ '';
+
+ buildPhase = ''
+ runHook preBuild
+
+ mkdir -p $out
+
+ ${lib.concatStringsSep "\n" (
+ map (runtime: ''
+ dotnet publish \
+ --self-contained \
+ -p:ContinuousIntegrationBuild=true \
+ -p:Deterministic=true \
+ --packages "$HOME/nuget_pkgs" \
+ -r ${runtime} \
+ -c Release \
+ --output $out/${runtime} \
+ Launcher
+ '') [ "win-x64" "win-arm64" ]
+ )}
+
+ runHook postBuild
+ '';
+
+ dontInstall = true;
+
+ dontStrip = true; # strip breaks the assemblies
+
+ meta = with lib; {
+ description = "WSL Distribution Launcher for NixOS-WSL";
+ homepage = "https://github.com/nix-community/NixOS-WSL";
+ license = licenses.asl20;
+ maintainers = with maintainers; [ nzbr ];
+ platforms = [ "x86_64-linux" "x86_64-windows" ];
+ };
+}
diff --git a/checks/dotnet-format.nix b/checks/dotnet-format.nix
new file mode 100644
index 0000000..9a4bf60
--- /dev/null
+++ b/checks/dotnet-format.nix
@@ -0,0 +1,8 @@
+{ runCommand
+, dotnet-sdk
+, ...
+}:
+runCommand "check-dotnet-format" { nativeBuildInputs = [ dotnet-sdk ]; } ''
+ cd "${./../Launcher}"
+ dotnet format --verbosity detailed --no-restore --verify-no-changes && touch $out
+''
diff --git a/flake.nix b/flake.nix
index d5c3b31..104960e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -89,6 +89,7 @@
args = { inherit inputs; };
in
{
+ dotnet-format = pkgs.callPackage ./checks/dotnet-format.nix args;
nixpkgs-fmt = pkgs.callPackage ./checks/nixpkgs-fmt.nix args;
shfmt = pkgs.callPackage ./checks/shfmt.nix args;
rustfmt = pkgs.callPackage ./checks/rustfmt.nix args;
diff --git a/modules/version.nix b/modules/version.nix
index 7968318..c287741 100644
--- a/modules/version.nix
+++ b/modules/version.nix
@@ -63,7 +63,7 @@ with lib;
'') config.wsl.version
)}
--json)
- echo '${generators.toJSON {} config.wsl.version}' | ${pkgs.jq}/bin/jq # Use jq to pretty-print the JSON
+ echo '${generators.toJSON {} config.wsl.version}' | ${pkgs.jq}/bin/jq -M # Use jq to pretty-print the JSON
exit 0
;;
*)