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"; + }) +]