diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000..c1e4bf5
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "fantomas": {
+ "version": "6.3.8",
+ "commands": [
+ "fantomas"
+ ]
+ }
+ }
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e744fe1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,40 @@
+root = true
+
+[*]
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+
+# ReSharper properties
+resharper_xml_indent_size = 2
+resharper_xml_max_line_length = 100
+resharper_xml_tab_width = 2
+
+[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
+indent_style = space
+indent_size = 2
+
+[*.{fs,fsi}]
+fsharp_bar_before_discriminated_union_declaration = true
+fsharp_space_before_uppercase_invocation = true
+fsharp_space_before_class_constructor = true
+fsharp_space_before_member = true
+fsharp_space_before_colon = true
+fsharp_space_before_semicolon = true
+fsharp_multiline_bracket_style = aligned
+fsharp_newline_between_type_definition_and_members = true
+fsharp_align_function_signature_to_indentation = true
+fsharp_alternative_long_member_definitions = true
+fsharp_multi_line_lambda_closing_newline = true
+fsharp_experimental_keep_indent_in_branch = true
+fsharp_max_value_binding_width = 80
+fsharp_max_record_width = 0
+max_line_length = 120
+end_of_line = lf
+
+[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
+indent_style = space
+indent_size = 2
+tab_width = 2
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.fantomasignore b/.fantomasignore
new file mode 100644
index 0000000..9b42106
--- /dev/null
+++ b/.fantomasignore
@@ -0,0 +1 @@
+.direnv/
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..203a3b5
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,3 @@
+# See: https://help.github.com/articles/about-codeowners/
+
+* @G-Research/rqf @G-Research/gr-oss
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..1d54482
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json
+version: 2
+updates:
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/dotnet.yaml b/.github/workflows/dotnet.yaml
new file mode 100644
index 0000000..6abf99e
--- /dev/null
+++ b/.github/workflows/dotnet.yaml
@@ -0,0 +1,217 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/github-workflow.json
+name: .NET
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
+ NUGET_XMLDOC_MODE: ''
+ DOTNET_MULTILEVEL_LOOKUP: 0
+
+jobs:
+ build-windows:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Test
+ run: dotnet test
+ - name: Run example
+ run: ".\\Example\\bin\\Release\\net8.0\\win-x64\\Example.exe"
+
+ build:
+ strategy:
+ matrix:
+ config:
+ - Release
+ - Debug
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
+ - name: Install Nix
+ uses: cachix/install-nix-action@V27
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Restore dependencies
+ run: nix develop --command dotnet restore
+ - name: Build
+ run: nix develop --command dotnet build --no-restore --configuration ${{matrix.config}}
+ - name: Test
+ run: nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}}
+
+ build-nix:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install Nix
+ uses: cachix/install-nix-action@V27
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Build
+ run: nix build
+
+ check-dotnet-format:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install Nix
+ uses: cachix/install-nix-action@V27
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Run Fantomas
+ run: nix run .#fantomas -- --check .
+
+ check-nix-format:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install Nix
+ uses: cachix/install-nix-action@V27
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Run Alejandra
+ run: nix develop --command alejandra --check .
+
+ linkcheck:
+ name: Check links
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ - name: Install Nix
+ uses: cachix/install-nix-action@V27
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Run link checker
+ run: nix develop --command markdown-link-check README.md
+
+ flake-check:
+ name: Check flake
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ - name: Install Nix
+ uses: cachix/install-nix-action@V27
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Flake check
+ run: nix flake check
+
+ nuget-pack:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
+ - name: Install Nix
+ uses: cachix/install-nix-action@V27
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Restore dependencies
+ run: nix develop --command dotnet restore
+ - name: Build
+ run: nix develop --command dotnet build --no-restore --configuration Release
+ - name: Pack
+ run: nix develop --command dotnet pack --configuration Release
+ - name: Upload NuGet artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: nuget-package
+ path: WoofWare.DotnetRuntimeLocator/bin/Release/WoofWare.DotnetRuntimeLocator.*.nupkg
+
+ expected-pack:
+ needs: [nuget-pack]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Download NuGet artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-package
+ path: packed
+ - name: Check NuGet contents
+ # Verify that there is exactly one nupkg in the artifact that would be NuGet published
+ run: if [[ $(find packed -maxdepth 1 -name 'WoofWare.DotnetRuntimeLocator.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi
+
+ github-release-dry-run:
+ needs: [nuget-pack]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Download NuGet artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-package
+ - name: Tag and release
+ env:
+ DRY_RUN: 1
+ GITHUB_TOKEN: mock-token
+ run: sh .github/workflows/tag.sh
+
+ all-required-checks-complete:
+ needs: [check-dotnet-format, check-nix-format, build, build-nix, linkcheck, flake-check, nuget-pack, expected-pack, github-release-dry-run]
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo "All required checks complete."
+
+ nuget-publish:
+ runs-on: ubuntu-latest
+ if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }}
+ needs: [all-required-checks-complete]
+ environment: main-deploy
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install Nix
+ uses: cachix/install-nix-action@V27
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Download NuGet artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-package
+ path: packed
+ - name: Publish to NuGet
+ run: nix develop --command dotnet nuget push "packed/WoofWare.DotnetRuntimeLocator.*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
+
+ github-release:
+ runs-on: ubuntu-latest
+ if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }}
+ needs: [all-required-checks-complete]
+ environment: main-deploy
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Download NuGet artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-package
+ - name: Tag and release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: sh .github/workflows/tag.sh
diff --git a/.github/workflows/tag.sh b/.github/workflows/tag.sh
new file mode 100644
index 0000000..e2795f1
--- /dev/null
+++ b/.github/workflows/tag.sh
@@ -0,0 +1,120 @@
+#!/bin/bash
+
+echo "Dry-run? $DRY_RUN!"
+
+find . -maxdepth 1 -type f ! -name "$(printf "*\n*")" -name '*.nupkg' | while IFS= read -r file
+do
+ tag=$(basename "$file" .nupkg)
+ git tag "$tag"
+ ${DRY_RUN:+echo} git push origin "$tag"
+done
+
+export TAG
+TAG=$(find . -maxdepth 1 -type f -name 'WoofWare.DotnetRuntimeLocator.*.nupkg' -exec sh -c 'basename "$1" .nupkg' shell {} \; | grep -v Attributes)
+
+case "$TAG" in
+ *"
+"*)
+ echo "Error: TAG contains a newline; multiple plugins found."
+ exit 1
+ ;;
+esac
+
+# target_commitish empty indicates the repo default branch
+curl_body='{"tag_name":"'"$TAG"'","target_commitish":"","name":"'"$TAG"'","draft":false,"prerelease":false,"generate_release_notes":false}'
+
+echo "cURL body: $curl_body"
+
+failed_output=$(cat <<'EOF'
+{
+ "message": "Validation Failed",
+ "errors": [
+ {
+ "resource": "Release",
+ "code": "already_exists",
+ "field": "tag_name"
+ }
+ ],
+ "documentation_url": "https://docs.github.com/rest/releases/releases#create-a-release"
+}
+EOF
+)
+
+success_output=$(cat <<'EOF'
+{
+ "url": "https://api.github.com/repos/Smaug123/WoofWare.DotnetRuntimeLocator/releases/158152116",
+ "assets_url": "https://api.github.com/repos/Smaug123/WoofWare.DotnetRuntimeLocator/releases/158152116/assets",
+ "upload_url": "https://uploads.github.com/repos/Smaug123/WoofWare.DotnetRuntimeLocator/releases/158152116/assets{?name,label}",
+ "html_url": "https://github.com/Smaug123/WoofWare.DotnetRuntimeLocator/releases/tag/WoofWare.DotnetRuntimeLocator.2.1.30",
+ "id": 158152116,
+ "author": {
+ "login": "github-actions[bot]",
+ "id": 41898282,
+ "node_id": "MDM6Qm90NDE4OTgyODI=",
+ "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/github-actions%5Bbot%5D",
+ "html_url": "https://github.com/apps/github-actions",
+ "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
+ "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
+ "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
+ "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
+ "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
+ "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
+ "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
+ "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
+ "type": "Bot",
+ "site_admin": false
+ },
+ "node_id": "RE_kwDOJfksgc4JbTW0",
+ "tag_name": "WoofWare.DotnetRuntimeLocator.2.1.30",
+ "target_commitish": "main",
+ "name": "WoofWare.DotnetRuntimeLocator.2.1.30",
+ "draft": false,
+ "prerelease": false,
+ "created_at": "2024-05-30T11:00:55Z",
+ "published_at": "2024-05-30T11:03:02Z",
+ "assets": [
+
+ ],
+ "tarball_url": "https://api.github.com/repos/Smaug123/WoofWare.DotnetRuntimeLocator/tarball/WoofWare.DotnetRuntimeLocator.2.1.30",
+ "zipball_url": "https://api.github.com/repos/Smaug123/WoofWare.DotnetRuntimeLocator/zipball/WoofWare.DotnetRuntimeLocator.2.1.30",
+ "body": null
+}
+EOF
+)
+
+HANDLE_OUTPUT=''
+handle_error() {
+ ERROR_OUTPUT="$1"
+ exit_message=$(echo "$ERROR_OUTPUT" | jq -r --exit-status 'if .errors | length == 1 then .errors[0].code else null end')
+ if [ "$exit_message" = "already_exists" ] ; then
+ HANDLE_OUTPUT="Did not create GitHub release because it already exists at this version."
+ else
+ echo "Unexpected error output from curl: $(cat curl_output.json)"
+ echo "JQ output: $(exit_message)"
+ exit 2
+ fi
+}
+
+run_tests() {
+ handle_error "$failed_output"
+ if [ "$HANDLE_OUTPUT" != "Did not create GitHub release because it already exists at this version." ]; then
+ echo "Bad output from handler: $HANDLE_OUTPUT"
+ exit 3
+ fi
+ HANDLE_OUTPUT=''
+ echo "Tests passed."
+}
+
+run_tests
+
+if [ "$DRY_RUN" != 1 ] ; then
+ if curl --fail-with-body -L -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GITHUB_TOKEN" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/Smaug123/WoofWare.DotnetRuntimeLocator/releases -d "$curl_body" > curl_output.json; then
+ echo "Curl succeeded."
+ else
+ handle_error "$(cat curl_output.json)"
+ echo "$HANDLE_OUTPUT"
+ fi
+fi
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..38fb0e1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+bin/
+obj/
+/packages/
+riderModule.iml
+/_ReSharper.Caches/
+.idea/
+*.sln.DotSettings.user
+.DS_Store
+result
+.analyzerpackages/
+analysis.sarif
+.direnv/
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..cbf2f3e
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,16 @@
+
+
+ embedded
+ true
+ [UNDEFINED]
+ true
+ true
+ true
+ embedded
+ FS3388,FS3559
+
+
+
+
+
+
diff --git a/Example/Example.fsproj b/Example/Example.fsproj
new file mode 100644
index 0000000..18f0d71
--- /dev/null
+++ b/Example/Example.fsproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net8.0
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/Program.fs b/Example/Program.fs
new file mode 100644
index 0000000..26250a3
--- /dev/null
+++ b/Example/Program.fs
@@ -0,0 +1,21 @@
+namespace Example
+
+open System
+open WoofWare.DotnetRuntimeLocator
+
+module Program =
+ []
+ let main argv =
+ let info = DotnetEnvironmentInfo.Get ()
+ Console.WriteLine info
+ Console.WriteLine ("SDKs:")
+
+ for sdk in info.Sdks do
+ Console.WriteLine $"SDK: %O{sdk}"
+
+ Console.WriteLine ("Frameworks:")
+
+ for f in info.Frameworks do
+ Console.WriteLine $"Framework: %O{f}"
+
+ 0
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..debcfa2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+# WoofWare.DotnetRuntimeLocator
+
+Helpers to locate the .NET runtime and SDKs programmatically.
+(If you're parsing `dotnet --list-runtimes`, you're doing it wrong!)
+
+## Usage
+
+See [the example](Example/Program.fs).
+
+```fsharp
+let info = DotnetEnvironmentInfo.Get ()
+// or, if you already know a path to the `dotnet` executable...
+let info = DotnetEnvironmentInfo.Get "/path/to/dotnet"
+```
diff --git a/WoofWare.DotnetRuntimeLocator.sln b/WoofWare.DotnetRuntimeLocator.sln
new file mode 100644
index 0000000..b293925
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator.sln
@@ -0,0 +1,34 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WoofWare.DotnetRuntimeLocator", "WoofWare.DotnetRuntimeLocator\WoofWare.DotnetRuntimeLocator.csproj", "{6AEC4B30-8AFE-4071-A5FB-DBA6EB2D8194}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Test", "WoofWare.DotnetRuntimeLocator\Test\Test.fsproj", "{7123924E-E6C5-4612-9E2E-2C4B8D14C7B2}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Example", "Example\Example.fsproj", "{C295BC67-F932-4225-9183-7173B26E1F9E}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {6AEC4B30-8AFE-4071-A5FB-DBA6EB2D8194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6AEC4B30-8AFE-4071-A5FB-DBA6EB2D8194}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6AEC4B30-8AFE-4071-A5FB-DBA6EB2D8194}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6AEC4B30-8AFE-4071-A5FB-DBA6EB2D8194}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7123924E-E6C5-4612-9E2E-2C4B8D14C7B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7123924E-E6C5-4612-9E2E-2C4B8D14C7B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7123924E-E6C5-4612-9E2E-2C4B8D14C7B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7123924E-E6C5-4612-9E2E-2C4B8D14C7B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C295BC67-F932-4225-9183-7173B26E1F9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C295BC67-F932-4225-9183-7173B26E1F9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C295BC67-F932-4225-9183-7173B26E1F9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C295BC67-F932-4225-9183-7173B26E1F9E}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/WoofWare.DotnetRuntimeLocator/Boilerplate.cs b/WoofWare.DotnetRuntimeLocator/Boilerplate.cs
new file mode 100644
index 0000000..698fadd
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/Boilerplate.cs
@@ -0,0 +1,28 @@
+namespace System.Runtime.CompilerServices
+{
+ internal class RequiredMemberAttribute : Attribute
+ {
+ }
+
+ internal class CompilerFeatureRequiredAttribute : Attribute
+ {
+ public CompilerFeatureRequiredAttribute(string name)
+ {
+ }
+ }
+}
+
+namespace System.Diagnostics.CodeAnalysis
+{
+ [AttributeUsage(AttributeTargets.Constructor)]
+ internal sealed class SetsRequiredMembersAttribute : Attribute
+ {
+ }
+}
+
+namespace System.Runtime.CompilerServices
+{
+ internal static class IsExternalInit
+ {
+ }
+}
diff --git a/WoofWare.DotnetRuntimeLocator/DotnetEnvironmentFrameworkInfo.cs b/WoofWare.DotnetRuntimeLocator/DotnetEnvironmentFrameworkInfo.cs
new file mode 100644
index 0000000..5909afa
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/DotnetEnvironmentFrameworkInfo.cs
@@ -0,0 +1,29 @@
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace WoofWare.DotnetRuntimeLocator;
+
+///
+/// Information about a single instance of the .NET runtime.
+///
+/// The name of this runtime, e.g. "Microsoft.NETCore.App"
+///
+/// The path to this runtime, e.g. "/usr/bin/dotnet/shared/Microsoft.AspNetCore.App" (I'm guessing at
+/// the prefix here, I use Nix so my paths are all different)
+///
+/// The version of this runtime, e.g. "8.0.5"
+public record DotnetEnvironmentFrameworkInfo(string Name, string Path, string Version)
+{
+ internal static DotnetEnvironmentFrameworkInfo FromNative(
+ InteropStructs.DotnetEnvironmentFrameworkInfoNative native)
+ {
+ if (native.size < 0 || native.size > int.MaxValue)
+ throw new InvalidDataException("size field did not fit in an int");
+
+ var size = (int)native.size;
+ if (size != Marshal.SizeOf(native))
+ throw new InvalidDataException($"size field {size} did not match expected size {Marshal.SizeOf(native)}");
+
+ return new DotnetEnvironmentFrameworkInfo(native.name, native.path, native.version);
+ }
+}
diff --git a/WoofWare.DotnetRuntimeLocator/DotnetEnvironmentInfo.cs b/WoofWare.DotnetRuntimeLocator/DotnetEnvironmentInfo.cs
new file mode 100644
index 0000000..d917d1f
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/DotnetEnvironmentInfo.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace WoofWare.DotnetRuntimeLocator;
+
+///
+/// Information known to `dotnet` about what frameworks and runtimes are available.
+///
+/// Version of the runtime, e.g. "8.0.5"
+///
+/// A commit hash of the .NET runtime (as of this writing, this is probably a hash from
+/// https://github.com/dotnet/runtime).
+///
+/// Collection of .NET SDKs we were able to find.
+/// Collection of .NET runtimes we were able to find.
+public record DotnetEnvironmentInfo(
+ string HostFxrVersion,
+ string HostFxrCommitHash,
+ IReadOnlyList Sdks,
+ IReadOnlyList Frameworks)
+{
+ private static readonly Lazy HostFxr = new(() =>
+ {
+ // First, we might be self-contained: try and find it next to us.
+ var selfContainedAttempt = Directory.GetParent(Assembly.GetExecutingAssembly().Location);
+ if (selfContainedAttempt != null)
+ {
+ var attempt = selfContainedAttempt.EnumerateFiles("*hostfxr*").FirstOrDefault();
+ if (attempt != null) return attempt;
+ }
+
+ var runtimeDir = new DirectoryInfo(RuntimeEnvironment.GetRuntimeDirectory());
+ var parent1 = runtimeDir.Parent ??
+ throw new Exception("Unable to locate the host/fxr directory in the .NET runtime");
+ var parent2 = parent1.Parent ??
+ throw new Exception("Unable to locate the host/fxr directory in the .NET runtime");
+ var parent3 = parent2.Parent ??
+ throw new Exception("Unable to locate the host/fxr directory in the .NET runtime");
+ var fxrDir = new DirectoryInfo(Path.Combine(parent3.FullName, "host", "fxr"));
+ return fxrDir.EnumerateDirectories().First().EnumerateFiles("*hostfxr*").First();
+ });
+
+ private static FileInfo ResolveAllSymlinks(FileInfo f)
+ {
+ while (!ReferenceEquals(null, f.LinkTarget))
+ {
+ var parent = f.Directory ?? new DirectoryInfo("/");
+ f = new FileInfo(Path.Combine(parent.FullName, f.LinkTarget));
+ }
+
+ return f;
+ }
+
+ ///
+ /// Takes a DotnetEnvironmentInfoNative and a return location, which must fit a DotnetEnvironmentInfo.
+ /// Renders the DotnetEnvironmentInfoNative and stores it in the return location.
+ ///
+ private static void StoreResult(IntPtr envInfo, IntPtr retLoc)
+ {
+ var toRet = FromNativeConstructor.FromNative(
+ Marshal.PtrToStructure(envInfo));
+ var handle = GCHandle.FromIntPtr(retLoc);
+ handle.Target = toRet;
+ }
+
+ private static unsafe DotnetEnvironmentInfo CallDelegate(string? dotnetExePath, RuntimeDelegate f)
+ {
+ byte[]? dotnet = null;
+ if (dotnetExePath != null)
+ {
+ dotnet = Encoding.ASCII.GetBytes(dotnetExePath);
+ }
+ fixed (byte* dotnetPath = dotnet)
+ {
+ DotnetEnvironmentInfo? toRet = null;
+ var handle = GCHandle.Alloc(toRet);
+ try
+ {
+ var del = (StoreResultDelegate)StoreResult;
+ var callback = Marshal.GetFunctionPointerForDelegate(del);
+
+ var rc = f.Invoke((IntPtr)dotnetPath, IntPtr.Zero, callback, GCHandle.ToIntPtr(handle));
+ if (rc != 0) throw new Exception($"Could not obtain .NET environment information (exit code: {rc})");
+
+ if (ReferenceEquals(null, handle.Target))
+ throw new NullReferenceException(
+ "Unexpectedly failed to populate DotnetEnvironmentInfo, despite the native call succeeding.");
+ return (DotnetEnvironmentInfo)handle.Target;
+ }
+ finally
+ {
+ handle.Free();
+ }
+ }
+ }
+
+ ///
+ /// Get the environment information that is available to the specified `dotnet` executable.
+ ///
+ /// A `dotnet` (or `dotnet.exe`) executable, e.g. one from /usr/bin/dotnet. Set this to null if you want us to just do our best.
+ /// Information about the environment available to the given executable.
+ /// Throws on any failure; handles nothing gracefully.
+ public static DotnetEnvironmentInfo GetSpecific(FileInfo? dotnetExe)
+ {
+ var hostFxr = HostFxr.Value;
+ var lib = NativeLibrary.Load(hostFxr.FullName);
+ try
+ {
+ var ptr = NativeLibrary.GetExport(lib, "hostfxr_get_dotnet_environment_info");
+ if (ptr == IntPtr.Zero) throw new Exception("Unable to load function from native library");
+
+ var f = Marshal.GetDelegateForFunctionPointer(ptr);
+ string? dotnetParent = null;
+ if (dotnetExe != null)
+ {
+ var dotnetNoSymlinks = ResolveAllSymlinks(dotnetExe);
+ var parent = dotnetNoSymlinks.Directory;
+ if (parent != null)
+ {
+ dotnetParent = parent.FullName;
+ }
+ }
+ return CallDelegate(dotnetParent, f);
+ }
+ finally
+ {
+ NativeLibrary.Free(lib);
+ }
+ }
+
+ private static FileInfo? FindDotnetAbove(DirectoryInfo path)
+ {
+ while (true)
+ {
+ var candidate = Path.Combine(path.FullName, "dotnet");
+ if (File.Exists(candidate)) return new FileInfo(candidate);
+
+ if (ReferenceEquals(path.Parent, null)) return null;
+
+ path = path.Parent;
+ }
+ }
+
+ ///
+ /// Get the environment information that is available to some arbitrary `dotnet` executable we were able to find.
+ ///
+ /// Information about the environment available to `dotnet`.
+ /// Throws on any failure; handles nothing gracefully.
+ public static DotnetEnvironmentInfo Get()
+ {
+ var dotnetExe = FindDotnetAbove(new DirectoryInfo(RuntimeEnvironment.GetRuntimeDirectory()));
+
+ if (ReferenceEquals(dotnetExe, null))
+ {
+ // This can happen! Maybe we're self-contained.
+ return GetSpecific(null);
+ }
+
+ return GetSpecific(dotnetExe);
+ }
+
+ ///
+ /// The signature of hostfxr_get_dotnet_environment_info.
+ /// Its implementation is
+ /// https://github.com/dotnet/runtime/blob/2dba5a3587de19160fb09129dcd3d7a4089b67b5/src/native/corehost/fxr/hostfxr.cpp#L357
+ /// Takes:
+ /// * The ASCII-encoded path to the directory which contains the `dotnet` executable
+ /// * A structure which is reserved for future use and which must currently be null
+ /// * A pointer to a callback which takes two arguments: a DotnetEnvironmentInfoNative
+ /// (https://github.com/dotnet/runtime/blob/2dba5a3587de19160fb09129dcd3d7a4089b67b5/src/native/corehost/hostfxr.h#L311)
+ /// and a context object you supplied.
+ /// This callback is represented by the type `StoreResultDelegate`.
+ /// * A pointer to the context object you want to consume in the callback.
+ /// Returns zero on success.
+ ///
+ internal delegate int RuntimeDelegate(IntPtr pathToDotnetExeDirectory, IntPtr mustBeNull, IntPtr outputCallback,
+ IntPtr outputArg);
+
+ ///
+ /// The callback which you pass to RuntimeDelegate.
+ /// Takes:
+ /// * a DotnetEnvironmentInfoNative
+ /// (https://github.com/dotnet/runtime/blob/2dba5a3587de19160fb09129dcd3d7a4089b67b5/src/native/corehost/hostfxr.h#L311)
+ /// * a context object, which is up to you to define and to pass into the RuntimeDelegate.
+ ///
+ internal delegate void StoreResultDelegate(IntPtr envInfo, IntPtr retLoc);
+}
+
+internal class FromNativeConstructor
+{
+ internal static DotnetEnvironmentInfo FromNative(InteropStructs.DotnetEnvironmentInfoNative native)
+ {
+ if (native.size < 0 || native.size > int.MaxValue)
+ throw new InvalidDataException("size field did not fit in an int");
+ var size = (int)native.size;
+ if (native.framework_count < 0 || native.framework_count > int.MaxValue)
+ throw new InvalidDataException("framework_count field did not fit in an int");
+ var frameworkCount = (int)native.framework_count;
+ if (native.sdk_count < 0 || native.sdk_count > int.MaxValue)
+ throw new InvalidDataException("sdk_count field did not fit in an int");
+ var sdkCount = (int)native.sdk_count;
+
+ if (size != Marshal.SizeOf(native))
+ throw new InvalidDataException($"size field {size} did not match expected size {Marshal.SizeOf(native)}");
+
+ var frameworks = new List((int)native.framework_count);
+ for (var i = 0; i < frameworkCount; i++)
+ {
+ var frameworkInfo = new IntPtr(native.frameworks.ToInt64() +
+ i * Marshal.SizeOf());
+ frameworks.Add(DotnetEnvironmentFrameworkInfo.FromNative(
+ Marshal.PtrToStructure(frameworkInfo)));
+ }
+
+ var sdks = new List((int)native.sdk_count);
+ for (var i = 0; i < sdkCount; i++)
+ {
+ var sdkInfo = new IntPtr(native.sdks.ToInt64() +
+ i * Marshal.SizeOf());
+ sdks.Add(DotnetEnvironmentSdkInfo.FromNative(
+ Marshal.PtrToStructure(sdkInfo)));
+ }
+
+ return new DotnetEnvironmentInfo(native.hostfxr_version, native.hostfxr_commit_hash, sdks, frameworks);
+ }
+}
diff --git a/WoofWare.DotnetRuntimeLocator/DotnetEnvironmentSdkInfo.cs b/WoofWare.DotnetRuntimeLocator/DotnetEnvironmentSdkInfo.cs
new file mode 100644
index 0000000..b76c42b
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/DotnetEnvironmentSdkInfo.cs
@@ -0,0 +1,27 @@
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace WoofWare.DotnetRuntimeLocator;
+
+///
+/// Information about a single instance of the .NET SDK.
+///
+///
+/// The path to this SDK, e.g. "/usr/bin/dotnet/sdk/8.0.300" (I'm guessing at the prefix there, I use
+/// Nix so my paths are different)
+///
+/// e.g. "8.0.300"
+public record DotnetEnvironmentSdkInfo(string Path, string Version)
+{
+ internal static DotnetEnvironmentSdkInfo FromNative(InteropStructs.DotnetEnvironmentSdkInfoNative native)
+ {
+ if (native.size < 0 || native.size > int.MaxValue)
+ throw new InvalidDataException("size field did not fit in an int");
+
+ var size = (int)native.size;
+ if (size != Marshal.SizeOf(native))
+ throw new InvalidDataException($"size field {size} did not match expected size {Marshal.SizeOf(native)}");
+
+ return new DotnetEnvironmentSdkInfo(native.path, native.version);
+ }
+}
diff --git a/WoofWare.DotnetRuntimeLocator/InteropStructs.cs b/WoofWare.DotnetRuntimeLocator/InteropStructs.cs
new file mode 100644
index 0000000..c3a3f85
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/InteropStructs.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace WoofWare.DotnetRuntimeLocator;
+
+internal static class InteropStructs
+{
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ internal struct DotnetEnvironmentSdkInfoNative
+ {
+ public nuint size;
+ public string version;
+ public string path;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ internal struct DotnetEnvironmentFrameworkInfoNative
+ {
+ public nuint size;
+ public string name;
+ public string version;
+ public string path;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ internal struct DotnetEnvironmentInfoNative
+ {
+ public nuint size;
+ public string hostfxr_version;
+ public string hostfxr_commit_hash;
+
+ public nuint sdk_count;
+
+ ///
+ /// Pointer to an array of DotnetEnvironmentSdkInfoNative, of length `sdk_count`
+ ///
+ public IntPtr sdks;
+
+ public nuint framework_count;
+
+ ///
+ /// Pointer to an array of DotnetEnvironmentFrameworkInfoNative, of length `framework_count`
+ ///
+ public IntPtr frameworks;
+ }
+}
diff --git a/WoofWare.DotnetRuntimeLocator/SurfaceBaseline.txt b/WoofWare.DotnetRuntimeLocator/SurfaceBaseline.txt
new file mode 100644
index 0000000..8c34e00
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/SurfaceBaseline.txt
@@ -0,0 +1,47 @@
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo inherit obj, implements WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo System.IEquatable
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo..ctor [constructor]: (string, string, string)
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.$ [method]: unit -> WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.Deconstruct [method]: (System.String&, System.String&, System.String&) -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.get_Name [method]: unit -> string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.get_Path [method]: unit -> string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.get_Version [method]: unit -> string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.Name [property]: string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.op_Equality [static method]: (WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo, WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo) -> bool
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.op_Inequality [static method]: (WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo, WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo) -> bool
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.Path [property]: string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.set_Name [method]: string -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.set_Path [method]: string -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.set_Version [method]: string -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo.Version [property]: string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo inherit obj, implements WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo System.IEquatable
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo..ctor [constructor]: (string, string, WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo System.Collections.Generic.IReadOnlyList, WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo System.Collections.Generic.IReadOnlyList)
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.$ [method]: unit -> WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.Deconstruct [method]: (System.String&, System.String&, System.Collections.Generic.IReadOnlyList, System.Collections.Generic.IReadOnlyList) -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.Frameworks [property]: WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo System.Collections.Generic.IReadOnlyList
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.Get [static method]: unit -> WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.get_Frameworks [method]: unit -> WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo System.Collections.Generic.IReadOnlyList
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.get_HostFxrCommitHash [method]: unit -> string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.get_HostFxrVersion [method]: unit -> string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.get_Sdks [method]: unit -> WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo System.Collections.Generic.IReadOnlyList
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.GetSpecific [static method]: System.IO.FileInfo -> WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.HostFxrCommitHash [property]: string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.HostFxrVersion [property]: string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.op_Equality [static method]: (WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo, WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo) -> bool
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.op_Inequality [static method]: (WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo, WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo) -> bool
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.Sdks [property]: WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo System.Collections.Generic.IReadOnlyList
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.set_Frameworks [method]: WoofWare.DotnetRuntimeLocator.DotnetEnvironmentFrameworkInfo System.Collections.Generic.IReadOnlyList -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.set_HostFxrCommitHash [method]: string -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.set_HostFxrVersion [method]: string -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentInfo.set_Sdks [method]: WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo System.Collections.Generic.IReadOnlyList -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo inherit obj, implements WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo System.IEquatable
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo..ctor [constructor]: (string, string)
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.$ [method]: unit -> WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.Deconstruct [method]: (System.String&, System.String&) -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.get_Path [method]: unit -> string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.get_Version [method]: unit -> string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.op_Equality [static method]: (WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo, WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo) -> bool
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.op_Inequality [static method]: (WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo, WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo) -> bool
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.Path [property]: string
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.set_Path [method]: string -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.set_Version [method]: string -> unit
+WoofWare.DotnetRuntimeLocator.DotnetEnvironmentSdkInfo.Version [property]: string
\ No newline at end of file
diff --git a/WoofWare.DotnetRuntimeLocator/Test/Test.fsproj b/WoofWare.DotnetRuntimeLocator/Test/Test.fsproj
new file mode 100644
index 0000000..cea00be
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/Test/Test.fsproj
@@ -0,0 +1,26 @@
+
+
+
+ net8.0
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WoofWare.DotnetRuntimeLocator/Test/TestDotnetEnvironmentInfo.fs b/WoofWare.DotnetRuntimeLocator/Test/TestDotnetEnvironmentInfo.fs
new file mode 100644
index 0000000..e892c06
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/Test/TestDotnetEnvironmentInfo.fs
@@ -0,0 +1,19 @@
+namespace WoofWare.DotnetRuntimeLocator.Test
+
+open System
+open FsUnitTyped
+open WoofWare.DotnetRuntimeLocator
+open NUnit.Framework
+
+[]
+module TestDotnetEnvironmentInfo =
+
+ []
+ let ``Can locate the runtime`` () =
+ let runtimes = DotnetEnvironmentInfo.Get ()
+
+ // In the test setup, there should be an SDK!
+ runtimes.Sdks |> Seq.length |> shouldBeGreaterThan 0
+ runtimes.Frameworks |> Seq.length |> shouldBeGreaterThan 0
+
+ Console.WriteLine $"%O{runtimes}"
diff --git a/WoofWare.DotnetRuntimeLocator/Test/TestSurface.fs b/WoofWare.DotnetRuntimeLocator/Test/TestSurface.fs
new file mode 100644
index 0000000..cafcd6c
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/Test/TestSurface.fs
@@ -0,0 +1,23 @@
+namespace WoofWare.DotnetRuntimeLocator.Test
+
+open NUnit.Framework
+open ApiSurface
+
+[]
+module TestSurface =
+ let assembly = typeof.Assembly
+
+ []
+ let ``Ensure API surface has not been modified`` () = ApiSurface.assertIdentical assembly
+
+ []
+ let ``Update API surface`` () =
+ ApiSurface.writeAssemblyBaseline assembly
+
+ []
+ let ``Ensure public API is fully documented`` () =
+ DocCoverage.assertFullyDocumented assembly
+
+ []
+ let ``Ensure version is monotonic`` () =
+ MonotonicVersion.validate assembly "WoofWare.DotnetRuntimeLocator"
diff --git a/WoofWare.DotnetRuntimeLocator/WoofWare.DotnetRuntimeLocator.csproj b/WoofWare.DotnetRuntimeLocator/WoofWare.DotnetRuntimeLocator.csproj
new file mode 100644
index 0000000..bb04fe1
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/WoofWare.DotnetRuntimeLocator.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net6.0
+ enable
+ latest
+ false
+ true
+ true
+ Patrick Stevens
+ Copyright (c) Patrick Stevens 2024
+ Helpers to locate the .NET runtime and SDKs
+ git
+ https://github.com/Smaug123/WoofWare.DotnetRuntimeLocator
+ MIT
+ README.md
+ logo.png
+ runtime;locate;sdk;list-runtimes;list-sdks
+ true
+
+
+
+
+
+
+
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
diff --git a/WoofWare.DotnetRuntimeLocator/logo.png b/WoofWare.DotnetRuntimeLocator/logo.png
new file mode 100644
index 0000000..89f4d2f
Binary files /dev/null and b/WoofWare.DotnetRuntimeLocator/logo.png differ
diff --git a/WoofWare.DotnetRuntimeLocator/version.json b/WoofWare.DotnetRuntimeLocator/version.json
new file mode 100644
index 0000000..bb9aa91
--- /dev/null
+++ b/WoofWare.DotnetRuntimeLocator/version.json
@@ -0,0 +1,10 @@
+{
+ "version": "0.1",
+ "publicReleaseRefSpec": null,
+ "pathFilters": [
+ "^Test/",
+ ":/WoofWare.DotnetRuntimeLocator",
+ ":/Directory.Build.props",
+ ":/README.md"
+ ]
+}
\ No newline at end of file
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..f15c20e
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1710146030,
+ "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1717646450,
+ "narHash": "sha256-KE+UmfSVk5PG8jdKdclPVcMrUB8yVZHbsjo7ZT1Bm3c=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "818dbe2f96df233d2041739d6079bb616d3e5597",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..ef41b67
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,66 @@
+{
+ description = "Utilities to help you identify available .NET runtimes";
+
+ inputs = {
+ flake-utils.url = "github:numtide/flake-utils";
+ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+ };
+
+ outputs = {
+ nixpkgs,
+ flake-utils,
+ ...
+ }:
+ flake-utils.lib.eachDefaultSystem (system: let
+ pkgs = nixpkgs.legacyPackages.${system};
+ pname = "WoofWare.DotnetRuntimeLocator";
+ dotnet-sdk = pkgs.dotnet-sdk_8;
+ dotnet-runtime = pkgs.dotnetCorePackages.runtime_8_0;
+ version = "0.1";
+ dotnetTool = dllOverride: toolName: toolVersion: sha256:
+ pkgs.stdenvNoCC.mkDerivation rec {
+ name = toolName;
+ version = toolVersion;
+ nativeBuildInputs = [pkgs.makeWrapper];
+ src = pkgs.fetchNuGet {
+ pname = name;
+ version = version;
+ sha256 = sha256;
+ installPhase = ''mkdir -p $out/bin && cp -r tools/net6.0/any/* $out/bin'';
+ };
+ installPhase = let
+ dll =
+ if isNull dllOverride
+ then name
+ else dllOverride;
+ in ''
+ runHook preInstall
+ mkdir -p "$out/lib"
+ cp -r ./bin/* "$out/lib"
+ makeWrapper "${dotnet-runtime}/bin/dotnet" "$out/bin/${name}" --add-flags "$out/lib/${dll}.dll"
+ runHook postInstall
+ '';
+ };
+ in {
+ packages = {
+ fantomas = dotnetTool null "fantomas" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fantomas.version (builtins.head (builtins.filter (elem: elem.pname == "fantomas") ((import ./nix/deps.nix) {fetchNuGet = x: x;}))).sha256;
+ default = pkgs.buildDotnetModule {
+ inherit pname version dotnet-sdk dotnet-runtime;
+ name = "WoofWare.Myriad.Plugins";
+ src = ./.;
+ projectFile = "./WoofWare.DotnetRuntimeLocator/WoofWare.DotnetRuntimeLocator.csproj";
+ testProjectFile = "./WoofWare.DotnetRuntimeLocator/Test/Test.fsproj";
+ nugetDeps = ./nix/deps.nix; # `nix build .#default.passthru.fetch-deps && ./result` and put the result here
+ doCheck = true;
+ };
+ };
+ devShell = pkgs.mkShell {
+ buildInputs = [dotnet-sdk];
+ packages = [
+ pkgs.alejandra
+ pkgs.nodePackages.markdown-link-check
+ pkgs.shellcheck
+ ];
+ };
+ });
+}
diff --git a/nix/deps.nix b/nix/deps.nix
new file mode 100644
index 0000000..d9caa56
--- /dev/null
+++ b/nix/deps.nix
@@ -0,0 +1,224 @@
+# This file was automatically generated by passthru.fetch-deps.
+# Please dont edit it manually, your changes might get overwritten!
+{fetchNuGet}: [
+ (fetchNuGet {
+ pname = "ApiSurface";
+ version = "4.0.40";
+ sha256 = "1c9z0b6minlripwrjmv4yd5w8zj4lcpak4x41izh7ygx8kgmbvx0";
+ })
+ (fetchNuGet {
+ pname = "fantomas";
+ version = "6.3.8";
+ sha256 = "0qfgx08br57sigb8vmpkx9vzsf5bgl86ax7rv4q373ikx3kyrmkl";
+ })
+ (fetchNuGet {
+ pname = "FSharp.Core";
+ version = "8.0.300";
+ sha256 = "158xxr9hnhz2ibyzzp2d249angvxfc58ifflm4g3hz8qx9zxaq04";
+ })
+ (fetchNuGet {
+ pname = "FsUnit";
+ version = "6.0.0";
+ sha256 = "18q3p0z155znwj1l0qq3vq9nh9wl2i4mlfx4pmrnia4czr0xdkmb";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.AspNetCore.App.Ref";
+ version = "6.0.31";
+ sha256 = "0hki4z9x60vzcg53s8cxnig4g1xnpqcj629r2cg5q1xw0sknfp5d";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.AspNetCore.App.Runtime.linux-arm64";
+ version = "6.0.31";
+ sha256 = "0blf8hl2irl9r9x6f7cih87ps21rcs3b8r09z5wp7jcb5j1cv8fg";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.AspNetCore.App.Runtime.linux-x64";
+ version = "6.0.31";
+ sha256 = "050dzfy49c4jwcm8dfrz2lqbbyhmgnq485zdhpcnc3w08z0ppbs6";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.AspNetCore.App.Runtime.osx-arm64";
+ version = "6.0.31";
+ sha256 = "0w4sab66rjjyar9z139ls6rx29gvgj3rp3cbrsc8z00y9mw2sl22";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.AspNetCore.App.Runtime.osx-x64";
+ version = "6.0.31";
+ sha256 = "13kww7x35926wik32z8cnvzhpqp3dwhazkzb569v87x8yww56n3k";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.CodeCoverage";
+ version = "17.10.0";
+ sha256 = "0s0v7jmrq85n356xv7zixvwa4z94fszjcr5vll8x4im1a2lp00f9";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NET.Test.Sdk";
+ version = "17.10.0";
+ sha256 = "13g8fwl09li8fc71nk13dgkb7gahd4qhamyg2xby7am63nlchhdf";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.App.Host.linux-arm64";
+ version = "6.0.31";
+ sha256 = "05s1c6bd4592xhy0y3w0cjckg11hb4pci729v59k3i3hl0hbad4s";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.App.Host.linux-x64";
+ version = "6.0.31";
+ sha256 = "10s0p30qzfn9zibp1ldnqar87hqs47ni3rwqpvwx4jn3589cl9sn";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.App.Host.osx-arm64";
+ version = "6.0.31";
+ sha256 = "0sah1gf2lccc93n3lmkgiahlz4jwr02cw20bvcwqyikpldy2awds";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.App.Host.osx-x64";
+ version = "6.0.31";
+ sha256 = "0k16h1fwnvhw1gcx8ib01bidhrls5m56fiy6wldk3ajgs5dif8i6";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.App.Ref";
+ version = "6.0.31";
+ sha256 = "19a4ainxj8jxij7ckglbmlnvrjxp72xfgx0r6lbglzh9dhsakwm7";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.App.Runtime.linux-arm64";
+ version = "6.0.31";
+ sha256 = "1wmlwzy9bc1fs38r0vpn3ragp8pkimcq6sicj978yhk7brn52z1p";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.App.Runtime.linux-x64";
+ version = "6.0.31";
+ sha256 = "0pw2n3j6vbmbghda1cvkhi3c39a49xk0a4w059mfya017adl6kac";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.App.Runtime.osx-arm64";
+ version = "6.0.31";
+ sha256 = "115c220p0mbk30biaw0sfqprnaghks7lcvvz6n5rsg0kn4fvy7qs";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.App.Runtime.osx-x64";
+ version = "6.0.31";
+ sha256 = "1cl561dgdk4mj48zw5xwg7a0cafkx8j2wjd4nlv0x3di300k75k5";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.NETCore.Platforms";
+ version = "2.0.0";
+ sha256 = "1fk2fk2639i7nzy58m9dvpdnzql4vb8yl8vr19r2fp8lmj9w2jr0";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.TestPlatform.ObjectModel";
+ version = "17.10.0";
+ sha256 = "07j69cw8r39533w4p39mnj00kahazz38760in3jfc45kmlcdb26x";
+ })
+ (fetchNuGet {
+ pname = "Microsoft.TestPlatform.TestHost";
+ version = "17.10.0";
+ sha256 = "1bl471s7fx9jycr0cc8rylwf34mrvlg9qn1an6l86nisavfcyb7v";
+ })
+ (fetchNuGet {
+ pname = "Nerdbank.GitVersioning";
+ version = "3.6.133";
+ sha256 = "1cdw8krvsnx0n34f7fm5hiiy7bs6h3asvncqcikc0g46l50w2j80";
+ })
+ (fetchNuGet {
+ pname = "Newtonsoft.Json";
+ version = "13.0.1";
+ sha256 = "0fijg0w6iwap8gvzyjnndds0q4b8anwxxvik7y8vgq97dram4srb";
+ })
+ (fetchNuGet {
+ pname = "Newtonsoft.Json";
+ version = "13.0.3";
+ sha256 = "0xrwysmrn4midrjal8g2hr1bbg38iyisl0svamb11arqws4w2bw7";
+ })
+ (fetchNuGet {
+ pname = "NuGet.Common";
+ version = "6.10.0";
+ sha256 = "0nizrnilmlcqbm945293h8q3wfqfchb4xi8g50x4kjn0rbpd1kbh";
+ })
+ (fetchNuGet {
+ pname = "NuGet.Configuration";
+ version = "6.10.0";
+ sha256 = "1aqaknaawnqx4mnvx9qw73wvj48jjzv0d78dzwl7m9zjlrl9myhz";
+ })
+ (fetchNuGet {
+ pname = "NuGet.Frameworks";
+ version = "6.10.0";
+ sha256 = "0hrd8y31zx9a0wps49czw0qgbrakb49zn3abfgylc9xrq990zkqk";
+ })
+ (fetchNuGet {
+ pname = "NuGet.Packaging";
+ version = "6.10.0";
+ sha256 = "18s53cvrf51lihmaqqdf48p2qi6ky1l48jv0hvbp76cxwdg7rba4";
+ })
+ (fetchNuGet {
+ pname = "NuGet.Protocol";
+ version = "6.10.0";
+ sha256 = "0hmv4q0ks9i34mfgpb13l01la9v3jjllfh1qd3aqv105xrqrdxac";
+ })
+ (fetchNuGet {
+ pname = "NuGet.Versioning";
+ version = "6.10.0";
+ sha256 = "1x19njx4x0sw9fz8y5fibi15xfsrw5avir0cx0599yd7p3ykik5g";
+ })
+ (fetchNuGet {
+ pname = "NUnit";
+ version = "4.1.0";
+ sha256 = "0fj6xwgqaxq3mrai86bklclfmjkzf038mrslwfqf4ignaz9f7g5j";
+ })
+ (fetchNuGet {
+ pname = "NUnit3TestAdapter";
+ version = "4.5.0";
+ sha256 = "1srx1629s0k1kmf02nmz251q07vj6pv58mdafcr5dr0bbn1fh78i";
+ })
+ (fetchNuGet {
+ pname = "System.Formats.Asn1";
+ version = "6.0.0";
+ sha256 = "1vvr7hs4qzjqb37r0w1mxq7xql2b17la63jwvmgv65s1hj00g8r9";
+ })
+ (fetchNuGet {
+ pname = "System.IO.Abstractions";
+ version = "4.2.13";
+ sha256 = "0s784iphsmj4vhkrzq9q3w39vsn76w44zclx3hsygsw458zbyh4y";
+ })
+ (fetchNuGet {
+ pname = "System.IO.FileSystem.AccessControl";
+ version = "4.5.0";
+ sha256 = "1gq4s8w7ds1sp8f9wqzf8nrzal40q5cd2w4pkf4fscrl2ih3hkkj";
+ })
+ (fetchNuGet {
+ pname = "System.Reflection.Metadata";
+ version = "1.6.0";
+ sha256 = "1wdbavrrkajy7qbdblpbpbalbdl48q3h34cchz24gvdgyrlf15r4";
+ })
+ (fetchNuGet {
+ pname = "System.Security.AccessControl";
+ version = "4.5.0";
+ sha256 = "1wvwanz33fzzbnd2jalar0p0z3x0ba53vzx1kazlskp7pwyhlnq0";
+ })
+ (fetchNuGet {
+ pname = "System.Security.Cryptography.Pkcs";
+ version = "6.0.4";
+ sha256 = "0hh5h38pnxmlrnvs72f2hzzpz4b2caiiv6xf8y7fzdg84r3imvfr";
+ })
+ (fetchNuGet {
+ pname = "System.Security.Cryptography.ProtectedData";
+ version = "4.4.0";
+ sha256 = "1q8ljvqhasyynp94a1d7jknk946m20lkwy2c3wa8zw2pc517fbj6";
+ })
+ (fetchNuGet {
+ pname = "System.Security.Principal.Windows";
+ version = "4.5.0";
+ sha256 = "0rmj89wsl5yzwh0kqjgx45vzf694v9p92r4x4q6yxldk1cv1hi86";
+ })
+ (fetchNuGet {
+ pname = "System.Text.Encodings.Web";
+ version = "7.0.0";
+ sha256 = "1151hbyrcf8kyg1jz8k9awpbic98lwz9x129rg7zk1wrs6vjlpxl";
+ })
+ (fetchNuGet {
+ pname = "System.Text.Json";
+ version = "7.0.3";
+ sha256 = "0zjrnc9lshagm6kdb9bdh45dmlnkpwcpyssa896sda93ngbmj8k9";
+ })
+]