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 ;; *)