From 59ef353023a300ff87b02dd18d4f259958344fc0 Mon Sep 17 00:00:00 2001 From: Adam Poit Date: Mon, 2 Mar 2026 18:29:26 -0800 Subject: [PATCH] Support multiple SDKs in an installation. --- .github/workflows/ci.yml | 34 +--- AGENTS.md | 10 +- CONTRIBUTING.md | 2 +- README.md | 30 +++- examples/basic/flake.nix | 1 - examples/multi-sdk/flake.nix | 34 ++++ examples/with-workloads/flake.nix | 1 - flake.nix | 67 +++++-- scripts/run-linux-build.sh | 3 +- src/lib.nix | 1 - src/nix-dotnet.nix | 252 +++++++++++++++++++-------- tests/integration-multi-sdk-test.nix | 88 ++++++++++ tests/unit.nix | 118 +++++++++++++ 13 files changed, 506 insertions(+), 135 deletions(-) create mode 100644 examples/multi-sdk/flake.nix create mode 100644 tests/integration-multi-sdk-test.nix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b5d594..71c0f38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: run: nix build .#checks.x86_64-linux.fmt --no-link # Integration tests (actual SDK downloads) - integration-basic: + integration-tests: strategy: matrix: os: [ubuntu-latest, macos-latest] @@ -62,38 +62,12 @@ jobs: mkdir -p ~/.config/nix echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf - - name: Run basic integration test + - name: Run integration tests run: | system=$(nix eval --impure --raw --expr builtins.currentSystem) if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - nix build .#checks.${system}.integration-test --no-link --option sandbox false + nix build .#checks.${system}.integration-tests --no-link --option sandbox false else - nix build .#checks.${system}.integration-test --no-link - fi - timeout-minutes: 15 - - integration-workloads: - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - - - name: Enable Flakes - run: | - mkdir -p ~/.config/nix - echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf - - - name: Run workload integration test - run: | - system=$(nix eval --impure --raw --expr builtins.currentSystem) - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - nix build .#checks.${system}.integration-workload-test --no-link --option sandbox false - else - nix build .#checks.${system}.integration-workload-test --no-link + nix build .#checks.${system}.integration-tests --no-link fi timeout-minutes: 20 diff --git a/AGENTS.md b/AGENTS.md index c7aeb45..7df4e79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,9 +14,9 @@ - Run all checks: `nix flake check` - Unit tests: `nix run github:nix-community/nix-unit -- --flake '.#tests'` - Run single unit test: `nix run github:nix-community/nix-unit -- --flake '.#tests' --filter 'testValidateSdkVersionBasic'` -- Integration test: `nix build .#checks..integration-test --no-link` +- Integration tests: `nix build .#checks..integration-tests --no-link` - Reproduce Linux integration checks in container: `./scripts/run-linux-build.sh` -- Run a specific Linux check in container: `./scripts/run-linux-build.sh .#checks.x86_64-linux.integration-test` +- Run a specific Linux check in container: `./scripts/run-linux-build.sh .#checks.x86_64-linux.integration-tests` - Build basic example: `nix build .#basic-example --no-link` - Build workload example: `nix build .#workload-example --no-link` - Enter dev shell: `nix develop` @@ -73,10 +73,10 @@ buildWorkloadNames = workloads: ``` - **Integration tests** (slow, requires .NET SDK build): - - Run: `nix build .#checks..integration-test --no-link` + - Run: `nix build .#checks..integration-tests --no-link` - Linux CI parity (from macOS): `./scripts/run-linux-build.sh` - - The script uses a `nixos/nix` linux/amd64 container and defaults to both Linux integration checks. - - Optional attrs can be passed (for example: `./scripts/run-linux-build.sh .#checks.x86_64-linux.integration-workload-test`). + - The script uses a `nixos/nix` linux/amd64 container and defaults to the combined Linux integration check. + - Optional attrs can be passed (for example: `./scripts/run-linux-build.sh .#checks.x86_64-linux.integration-tests`). - Location: `tests/integration/` (full .NET project with xUnit tests) - Tests: End-to-end SDK build and test execution - Validates that mkDotnet produces a working .NET SDK diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3337feb..a2fbb5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ nix develop nix run github:nix-community/nix-unit -- --flake '.#tests' # Integration tests -nix build .#checks..integration-test --no-link +nix build .#checks..integration-tests --no-link # All checks nix flake check diff --git a/README.md b/README.md index 6c82923..035a4e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # nix-dotnet -A Nix module that reads your `global.json` and builds a reproducible .NET SDK derivation. +A Nix module that builds reproducible .NET SDK derivations from `global.json` or explicit SDK versions. ## Quick Start @@ -49,18 +49,35 @@ A Nix module that reads your `global.json` and builds a reproducible .NET SDK de ```nix nix-dotnet.lib.${system}.mkDotnet { - globalJsonPath = ./global.json; # Required - workloads = [ "android" "ios" ]; # Optional + globalJsonPath = ./global.json; # Use this for global.json-driven versioning + # sdkVersion = "10.0.103"; # Alternative to globalJsonPath + # workloadVersion = "10.0.100.1"; # Optional with sdkVersion + additionalSdks = [ + { + sdkVersion = "9.0.304"; + } + ]; outputHash = "sha256-..."; # Required for fixed-output } ``` **Parameters:** -- `globalJsonPath` - Path to global.json (required) -- `workloads` - List of workload names (optional, versions from global.json) +- `globalJsonPath` - Path to global.json (optional; mutually exclusive with `sdkVersion`) +- `sdkVersion` - SDK version string like `10.0.103` (optional; mutually exclusive with `globalJsonPath`) +- `workloadVersion` - Workload manifest version when using explicit `sdkVersion` (optional) +- `workloads` - List of workload names (optional, versions from `global.json` or `workloadVersion`) +- `additionalSdks` - Extra SDK configs to merge into the same `dotnet` installation (optional, no per-SDK hash) - `outputHash` - Fixed-output derivation hash (required) +`additionalSdks` lets one `dotnet` command expose multiple SDK versions, so `dotnet --list-sdks` can show entries like .NET 10 and .NET 9 together. + +Use one top-level `outputHash` for the whole combined installation. + +Use `checks..integration-tests` to run all integration checks that are configured for that system. + +_Note_: `additionalSdks` entries do not support installing workloads. + ### Getting the outputHash 1. Use a placeholder hash @@ -74,6 +91,7 @@ nix-dotnet.lib.${system}.mkDotnet { - [`examples/basic/`](./examples/basic/) - SDK without workloads - [`examples/with-workloads/`](./examples/with-workloads/) - SDK with MAUI workloads +- [`examples/multi-sdk/`](./examples/multi-sdk/) - single `dotnet` with both .NET 10 and .NET 9 SDKs ## Supported Platforms @@ -89,7 +107,7 @@ nix-dotnet.lib.${system}.mkDotnet { nix run github:nix-community/nix-unit -- --flake '.#tests' # Integration tests (slow, builds SDK) -nix build .#checks..integration-test --no-link +nix build .#checks..integration-tests --no-link # All checks nix flake check diff --git a/examples/basic/flake.nix b/examples/basic/flake.nix index c2bf914..78878d5 100644 --- a/examples/basic/flake.nix +++ b/examples/basic/flake.nix @@ -5,7 +5,6 @@ }; outputs = { - self, nixpkgs, nix-dotnet, }: let diff --git a/examples/multi-sdk/flake.nix b/examples/multi-sdk/flake.nix new file mode 100644 index 0000000..08afcf1 --- /dev/null +++ b/examples/multi-sdk/flake.nix @@ -0,0 +1,34 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nix-dotnet.url = "github:adampoit/nix-dotnet"; + }; + + outputs = { + nixpkgs, + nix-dotnet, + }: let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + in { + devShells.${system}.default = pkgs.mkShell { + buildInputs = [ + (nix-dotnet.lib.${system}.mkDotnet { + sdkVersion = "10.0.103"; + outputHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + additionalSdks = [ + { + sdkVersion = "9.0.304"; + } + ]; + }) + ]; + + shellHook = '' + echo "Active SDK: $(dotnet --version)" + echo "Installed SDKs:" + dotnet --list-sdks + ''; + }; + }; +} diff --git a/examples/with-workloads/flake.nix b/examples/with-workloads/flake.nix index c7c0282..1a9e53c 100644 --- a/examples/with-workloads/flake.nix +++ b/examples/with-workloads/flake.nix @@ -5,7 +5,6 @@ }; outputs = { - self, nixpkgs, nix-dotnet, }: let diff --git a/flake.nix b/flake.nix index 2517a9f..125b6e4 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Nix module for installing .NET SDKs with specific versions and workloads from global.json"; + description = "Nix module for reproducible .NET SDKs from global.json or explicit versions"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; @@ -8,10 +8,10 @@ }; outputs = { - self, nixpkgs, flake-utils, nix-unit, + ... }: flake-utils.lib.eachDefaultSystem ( @@ -30,17 +30,48 @@ x86_64-darwin = "sha256-gJPf567EoBmF22ANZX9qFEA/wtZHU/+GBd6rbaj9VUQ="; x86_64-linux = "sha256-R6+gCgcfrTv1NYQyI0/3YXmjDRvvooPEOjCR59WSpfk="; }; + multiSdkOutputHashes = { + aarch64-darwin = "sha256-McAAMwZCOfvdOKOCjNRKYmpWbdtUQfz03WdoAJImv5c="; + aarch64-linux = "sha256-olZbB/1rbLs587K9NQXBQ9o2pQH0WSTUjaBORlJVD+4="; + x86_64-darwin = "sha256-5DNjlU1VCJs3Soqemq4gU3YdEFXvIol30qad0udUiuU="; + x86_64-linux = "sha256-3LAIxMUQYqrnEaOAoYA/seSFZuE1pyVL97TjC891VCs="; + }; outputHashFor = hashes: if builtins.hasAttr system hashes then hashes.${system} else throw "No outputHash configured for system ${system}"; + integrationSingleSdkTest = import ./tests/integration-test.nix { + inherit pkgs dotnet; + outputHash = outputHashFor sdkOutputHashes; + }; + integrationWorkloadTest = import ./tests/integration-workload-test.nix { + inherit pkgs dotnet; + outputHash = outputHashFor workloadOutputHashes; + }; + integrationMultiSdkTest = import ./tests/integration-multi-sdk-test.nix { + inherit pkgs dotnet; + outputHash = outputHashFor multiSdkOutputHashes; + }; + integrationTests = + pkgs.runCommand + "integration-tests" + { + buildInputs = [ + integrationSingleSdkTest + integrationWorkloadTest + integrationMultiSdkTest + ]; + } + '' + touch $out + ''; unitTests = import ./tests/unit.nix { lib = dotnet.internal; inherit dotnet; }; unitTestAssertions = - builtins.map + map ( name: let test = unitTests.${name}; @@ -52,18 +83,22 @@ (builtins.attrNames unitTests); in { lib = dotnet; - packages = { - basic-example = dotnet.mkDotnet { + packages = let + basicExample = dotnet.mkDotnet { globalJsonPath = ./global.json; workloads = []; outputHash = outputHashFor sdkOutputHashes; }; - workload-example = dotnet.mkDotnet { + workloadExample = dotnet.mkDotnet { globalJsonPath = ./global.json; workloads = ["android"]; outputHash = outputHashFor workloadOutputHashes; }; + in { + default = basicExample; + basic-example = basicExample; + workload-example = workloadExample; }; devShells.default = pkgs.mkShell { @@ -80,23 +115,15 @@ { buildInputs = [pkgs.alejandra]; } " - alejandra --check ${./.} - touch $out - "; + alejandra --check ${./.} + touch $out + "; unit-tests = builtins.deepSeq unitTestAssertions (pkgs.runCommand "unit-tests" {} '' echo "${toString (builtins.length (builtins.attrNames unitTests))} unit tests passed" > $out ''); - integration-test = import ./tests/integration-test.nix { - inherit pkgs dotnet; - outputHash = outputHashFor sdkOutputHashes; - }; - - integration-workload-test = import ./tests/integration-workload-test.nix { - inherit pkgs dotnet; - outputHash = outputHashFor workloadOutputHashes; - }; + integration-tests = integrationTests; }; } ) @@ -110,6 +137,10 @@ path = ./examples/with-workloads; description = "Example with .NET workloads"; }; + multi-sdk = { + path = ./examples/multi-sdk; + description = "Example with one dotnet exposing multiple SDK versions"; + }; }; tests = let diff --git a/scripts/run-linux-build.sh b/scripts/run-linux-build.sh index c725e98..df29b55 100755 --- a/scripts/run-linux-build.sh +++ b/scripts/run-linux-build.sh @@ -26,8 +26,7 @@ if [[ "$#" -gt 0 ]]; then attrs=("$@") else attrs=( - ".#checks.x86_64-linux.integration-test" - ".#checks.x86_64-linux.integration-workload-test" + ".#checks.x86_64-linux.integration-tests" ) fi diff --git a/src/lib.nix b/src/lib.nix index e653a7b..161ccd9 100644 --- a/src/lib.nix +++ b/src/lib.nix @@ -2,7 +2,6 @@ inherit (pkgs.lib) concatStringsSep - map match hasAttr replaceStrings diff --git a/src/nix-dotnet.nix b/src/nix-dotnet.nix index 05a6a41..c87fe1e 100644 --- a/src/nix-dotnet.nix +++ b/src/nix-dotnet.nix @@ -17,19 +17,99 @@ defaultInstallScriptSha256 = "0hp4gjss641gabh24wf1xsxp9y1vb48fna5vc9ag24rp614nhahh"; dotnetLibraryPath = pkgs.lib.makeLibraryPath [pkgs.stdenv.cc.cc pkgs.zlib pkgs.icu pkgs.openssl]; + finalizeRawSdk = rawSdk: + if pkgs.stdenv.isLinux + then + pkgs.stdenv.mkDerivation { + pname = rawSdk.pname; + inherit (rawSdk) version; + + src = rawSdk; + dontUnpack = true; + + nativeBuildInputs = [pkgs.patchelf]; + + installPhase = '' + runHook preInstall + + mkdir -p "$out" + cp -a "$src" "$out/.runtime" + chmod -R u+w "$out/.runtime" + + while IFS= read -r -d $'\0' file; do + if patchelf --print-rpath "$file" >/dev/null 2>&1; then + if patchelf --print-interpreter "$file" >/dev/null 2>&1; then + patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" "$file" + fi + + currentRpath="$(patchelf --print-rpath "$file" 2>/dev/null || true)" + if [ -n "$currentRpath" ]; then + patchelf --set-rpath "${dotnetLibraryPath}:$currentRpath" "$file" + else + patchelf --set-rpath "${dotnetLibraryPath}" "$file" + fi + fi + done < <(find "$out/.runtime" -type f -print0) + + for entry in "$out/.runtime"/*; do + name="$(basename "$entry")" + if [ "$name" != "dotnet" ]; then + ln -s ".runtime/$name" "$out/$name" + fi + done + + printf '%s\n' \ + '#!${pkgs.bash}/bin/bash' \ + 'export LD_LIBRARY_PATH="${dotnetLibraryPath}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"' \ + 'script_dir="$(cd "$(dirname "$0")" && pwd)"' \ + 'export DOTNET_ROOT="$script_dir/.runtime"' \ + 'exec "$script_dir/.runtime/dotnet" "$@"' \ + > "$out/dotnet" + chmod +x "$out/dotnet" + + chmod -R a-w "$out" + + runHook postInstall + ''; + + passthru = rawSdk.passthru // {inherit rawSdk;}; + meta = rawSdk.meta; + } + else rawSdk; + mkDotnetSdk = { sdkVersion, workloads ? [], + additionalSdkVersions ? [], installScriptUrl ? defaultInstallScriptUrl, installScriptSha256 ? defaultInstallScriptSha256, outputHash, }: let validatedSdkVersion = validateSdkVersion sdkVersion; validatedWorkloads = map validateWorkload workloads; + validatedAdditionalSdkVersions = map validateSdkVersion additionalSdkVersions; validatedOutputHash = validateOutputHash outputHash; workloadNames = buildWorkloadNames validatedWorkloads; workloadPnameSuffix = buildWorkloadPnameSuffix validatedWorkloads; workloadCommands = buildWorkloadCommands validatedWorkloads; + hasAdditionalSdks = validatedAdditionalSdkVersions != []; + additionalSdkPnameSuffix = + if hasAdditionalSdks + then "-with-${pkgs.lib.concatStringsSep "-" validatedAdditionalSdkVersions}" + else ""; + additionalInstallCommands = + if hasAdditionalSdks + then + pkgs.lib.concatStringsSep "\n\n" (map + (version: '' + echo "Installing additional .NET SDK ${version} into $out" + bash ./dotnet-install.sh \ + --version "${version}" \ + --install-dir "$out" \ + --no-path + '') + validatedAdditionalSdkVersions) + else ""; installScript = pkgs.fetchurl { url = installScriptUrl; @@ -37,7 +117,7 @@ }; rawSdk = pkgs.stdenv.mkDerivation { - pname = sanitizePname "dotnet-sdk-${workloadPnameSuffix}"; + pname = sanitizePname "dotnet-sdk-${workloadPnameSuffix}${additionalSdkPnameSuffix}"; version = validatedSdkVersion; src = null; @@ -69,6 +149,8 @@ cp ${installScript} dotnet-install.sh chmod +x dotnet-install.sh + ${additionalInstallCommands} + echo "Installing .NET SDK ${validatedSdkVersion} into $out" bash ./dotnet-install.sh \ --version "${validatedSdkVersion}" \ @@ -96,6 +178,17 @@ mkdir -p "$DOTNET_CLI_HOME" "$NUGET_PACKAGES" "$NUGET_HTTP_CACHE_PATH" + ${pkgs.lib.optionalString hasAdditionalSdks '' + cat > global.json <<'EOF' + { + "sdk": { + "version": "${validatedSdkVersion}", + "rollForward": "disable" + } + } + EOF + ''} + ${pkgs.lib.optionalString pkgs.stdenv.isLinux '' originalDotnetInterp="" if [ "${workloadNames}" != "none" ]; then @@ -117,6 +210,9 @@ fi rm -rf "$DOTNET_CLI_HOME" "$NUGET_PACKAGES" "$NUGET_HTTP_CACHE_PATH" "$HOME" + ${pkgs.lib.optionalString hasAdditionalSdks '' + rm -f global.json + ''} echo "Installation complete" echo "SDK Version: $($out/dotnet --version)" @@ -140,10 +236,14 @@ passthru = { inherit sdkVersion workloads; + additionalSdkVersions = validatedAdditionalSdkVersions; }; meta = with pkgs.lib; { - description = ".NET SDK ${validatedSdkVersion} with workloads [${workloadNames}]"; + description = + if hasAdditionalSdks + then ".NET SDK ${validatedSdkVersion} with workloads [${workloadNames}] and additional SDKs [${pkgs.lib.concatStringsSep ", " validatedAdditionalSdkVersions}]" + else ".NET SDK ${validatedSdkVersion} with workloads [${workloadNames}]"; homepage = "https://dotnet.microsoft.com/"; license = licenses.mit; maintainers = []; @@ -151,87 +251,99 @@ }; }; in - if pkgs.stdenv.isLinux - then - pkgs.stdenv.mkDerivation { - pname = rawSdk.pname; - inherit (rawSdk) version; + finalizeRawSdk rawSdk; - src = rawSdk; - dontUnpack = true; - - nativeBuildInputs = [pkgs.patchelf]; - - installPhase = '' - runHook preInstall - - mkdir -p "$out" - cp -a "$src" "$out/.runtime" - chmod -R u+w "$out/.runtime" - - while IFS= read -r -d $'\0' file; do - if patchelf --print-rpath "$file" >/dev/null 2>&1; then - if patchelf --print-interpreter "$file" >/dev/null 2>&1; then - patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" "$file" - fi - - currentRpath="$(patchelf --print-rpath "$file" 2>/dev/null || true)" - if [ -n "$currentRpath" ]; then - patchelf --set-rpath "${dotnetLibraryPath}:$currentRpath" "$file" - else - patchelf --set-rpath "${dotnetLibraryPath}" "$file" - fi - fi - done < <(find "$out/.runtime" -type f -print0) - - for entry in "$out/.runtime"/*; do - name="$(basename "$entry")" - if [ "$name" != "dotnet" ]; then - ln -s ".runtime/$name" "$out/$name" - fi - done - - printf '%s\n' \ - '#!${pkgs.bash}/bin/bash' \ - 'export LD_LIBRARY_PATH="${dotnetLibraryPath}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"' \ - 'script_dir="$(cd "$(dirname "$0")" && pwd)"' \ - 'export DOTNET_ROOT="$script_dir/.runtime"' \ - 'exec "$script_dir/.runtime/dotnet" "$@"' \ - > "$out/dotnet" - chmod +x "$out/dotnet" - - chmod -R a-w "$out" - - runHook postInstall - ''; - - passthru = rawSdk.passthru // {inherit rawSdk;}; - meta = rawSdk.meta; - } - else rawSdk; - - mkDotnet = { - globalJsonPath, + resolveDotnetConfig = { + globalJsonPath ? null, + sdkVersion ? null, + workloadVersion ? null, workloads ? [], - outputHash, }: let - globalConfig = readGlobalJson globalJsonPath; - sdkVersion = globalConfig.sdkVersion; - workloadVersion = globalConfig.workloadVersion; + usesGlobalJson = globalJsonPath != null; + usesExplicitSdkVersion = sdkVersion != null; + globalConfig = + if usesGlobalJson + then readGlobalJson globalJsonPath + else { + sdkVersion = null; + workloadVersion = null; + }; + resolvedSdkVersion = + if usesGlobalJson + then globalConfig.sdkVersion + else sdkVersion; + resolvedWorkloadVersion = + if usesGlobalJson + then globalConfig.workloadVersion + else workloadVersion; workloadObjects = map (name: { inherit name; - version = workloadVersion; + version = resolvedWorkloadVersion; }) workloads; + validatedWorkloads = map validateWorkload workloadObjects; in - if sdkVersion == null + if usesGlobalJson && usesExplicitSdkVersion + then throw "mkDotnet accepts either globalJsonPath or sdkVersion, not both." + else if !(usesGlobalJson || usesExplicitSdkVersion) + then throw "mkDotnet requires either globalJsonPath or sdkVersion." + else if resolvedSdkVersion == null then throw "Could not read SDK version from ${toString globalJsonPath}. Make sure the file exists and has a 'sdk.version' field." + else { + sdkVersion = validateSdkVersion resolvedSdkVersion; + workloads = validatedWorkloads; + }; + + mkDotnetSingle = config @ {outputHash, ...}: let + resolvedConfig = resolveDotnetConfig (removeAttrs config ["outputHash"]); + in + mkDotnetSdk { + sdkVersion = resolvedConfig.sdkVersion; + workloads = resolvedConfig.workloads; + inherit outputHash; + }; + + mkDotnetWithAdditional = { + primaryConfig, + additionalConfigs, + outputHash, + }: let + resolvedPrimary = resolveDotnetConfig primaryConfig; + resolvedAdditional = map resolveDotnetConfig additionalConfigs; + additionalWithWorkloads = pkgs.lib.filter (config: config.workloads != []) resolvedAdditional; + additionalVersions = map (config: config.sdkVersion) resolvedAdditional; + in + if additionalWithWorkloads != [] + then throw "additionalSdks entries with workloads are not supported yet." else mkDotnetSdk { - inherit sdkVersion outputHash; - workloads = workloadObjects; + sdkVersion = resolvedPrimary.sdkVersion; + workloads = resolvedPrimary.workloads; + additionalSdkVersions = additionalVersions; + inherit outputHash; + }; + + mkDotnet = args @ {additionalSdks ? [], ...}: let + primaryArgs = removeAttrs args ["additionalSdks"]; + normalizedAdditionalConfigs = + map + (config: + if builtins.hasAttr "additionalSdks" config && config.additionalSdks != [] + then throw "Nested additionalSdks are not supported. Add all SDK entries at the top-level additionalSdks list." + else if builtins.hasAttr "outputHash" config + then throw "additionalSdks entries should not set outputHash. Use top-level outputHash for the combined installation." + else removeAttrs config ["additionalSdks" "outputHash"]) + additionalSdks; + in + if additionalSdks == [] + then mkDotnetSingle primaryArgs + else + mkDotnetWithAdditional { + primaryConfig = removeAttrs primaryArgs ["outputHash"]; + additionalConfigs = normalizedAdditionalConfigs; + outputHash = primaryArgs.outputHash; }; in { inherit mkDotnet; diff --git a/tests/integration-multi-sdk-test.nix b/tests/integration-multi-sdk-test.nix new file mode 100644 index 0000000..b16b8d8 --- /dev/null +++ b/tests/integration-multi-sdk-test.nix @@ -0,0 +1,88 @@ +{ + pkgs, + dotnet, + outputHash, +}: let + dotnetSdk = dotnet.mkDotnet { + globalJsonPath = ./integration/global.json; + additionalSdks = [ + { + sdkVersion = "9.0.304"; + } + ]; + inherit outputHash; + }; +in + pkgs.stdenv.mkDerivation { + pname = "dotnet-nix-integration-multi-sdk-test"; + version = "1.0.0"; + + src = ./integration; + + nativeBuildInputs = [ + pkgs.cacert + pkgs.gnugrep + ]; + + DOTNET_ROOT = "${dotnetSdk}"; + + buildPhase = '' + runHook preBuild + + export PATH="${dotnetSdk}:$PATH" + + export DOTNET_CLI_HOME="$PWD/.dotnet-cli-home" + export NUGET_PACKAGES="$PWD/.nuget/packages" + export NUGET_HTTP_CACHE_PATH="$PWD/.nuget/http-cache" + export HOME="$PWD/.home" + + mkdir -p "$DOTNET_CLI_HOME" "$NUGET_PACKAGES" "$NUGET_HTTP_CACHE_PATH" "$HOME" + + echo "=== Integration Test: Multiple SDKs In One dotnet ===" + echo "SDK Version: $(dotnet --version)" + echo "" + + echo "Step 1: Verifying available SDK versions..." + sdk_list="$(dotnet --list-sdks)" + echo "$sdk_list" + + if ! printf '%s\n' "$sdk_list" | grep -qE '^10\.0\.103[[:space:]]'; then + echo "ERROR: 10.0.103 SDK is missing" + exit 1 + fi + + if ! printf '%s\n' "$sdk_list" | grep -qE '^9\.0\.304[[:space:]]'; then + echo "ERROR: 9.0.304 SDK is missing" + exit 1 + fi + + echo "" + echo "Step 2: Restoring NuGet packages in locked mode..." + dotnet restore TestSolution.slnx --locked-mode + + echo "" + echo "Step 3: Building solution..." + dotnet build TestSolution.slnx --configuration Release --no-restore + + echo "" + echo "Step 4: Running tests..." + dotnet test TestApp.Tests/TestApp.Tests.csproj \ + --configuration Release \ + --no-build \ + --verbosity normal + + echo "" + echo "=== Integration Multi-SDK Test Passed ===" + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out + echo "Integration multi-sdk test completed successfully" > $out/result + + runHook postInstall + ''; + } diff --git a/tests/unit.nix b/tests/unit.nix index 6ee4435..cb63100 100644 --- a/tests/unit.nix +++ b/tests/unit.nix @@ -44,6 +44,12 @@ inherit globalJsonPath workloads; inherit outputHash; }; + + mkDotnetFromVersion = sdkVersion: workloadVersion: workloads: outputHash: + dotnet.mkDotnet { + inherit sdkVersion workloadVersion workloads; + inherit outputHash; + }; in { testValidateSdkVersionBasic = { expr = lib.validateSdkVersion "10.0.100"; @@ -289,6 +295,11 @@ in { expected = "10.0.103"; }; + testMkDotnetVersionFromExplicitSdkVersion = { + expr = (mkDotnetFromVersion "9.0.404" null [] validOutputHash).version; + expected = "9.0.404"; + }; + testMkDotnetPnameWithVersionedWorkloads = { expr = (mkDotnetFrom validGlobalJson [ @@ -323,6 +334,44 @@ in { ]; }; + testMkDotnetPassthruWorkloadsFromExplicitWorkloadVersion = { + expr = + (mkDotnetFromVersion "9.0.404" "9.0.100" [ + "android" + ] + validOutputHash).passthru.workloads; + expected = [ + { + name = "android"; + version = "9.0.100"; + } + ]; + }; + + testMkDotnetRequiresVersionSource = { + expr = builtins.tryEval (dotnet.mkDotnet { + workloads = []; + outputHash = validOutputHash; + }); + expected = { + success = false; + value = false; + }; + }; + + testMkDotnetRejectsMixedVersionSources = { + expr = builtins.tryEval (dotnet.mkDotnet { + globalJsonPath = validGlobalJson; + sdkVersion = "9.0.404"; + workloads = []; + outputHash = validOutputHash; + }); + expected = { + success = false; + value = false; + }; + }; + testMkDotnetMissingSdkVersion = { expr = builtins.tryEval (mkDotnetFrom globalJsonMissingSdkVersion [] validOutputHash); expected = { @@ -338,4 +387,73 @@ in { value = false; }; }; + + testMkDotnetAdditionalSdksVersions = { + expr = + (dotnet.mkDotnet { + sdkVersion = "10.0.103"; + outputHash = validOutputHash; + additionalSdks = [ + { + sdkVersion = "9.0.404"; + } + ]; + }).passthru.additionalSdkVersions; + expected = ["9.0.404"]; + }; + + testMkDotnetAdditionalSdksNestedRejected = { + expr = builtins.tryEval (dotnet.mkDotnet { + sdkVersion = "10.0.103"; + outputHash = validOutputHash; + additionalSdks = [ + { + sdkVersion = "9.0.404"; + additionalSdks = [ + { + sdkVersion = "8.0.100"; + } + ]; + } + ]; + }); + expected = { + success = false; + value = false; + }; + }; + + testMkDotnetAdditionalSdksRejectOutputHash = { + expr = builtins.tryEval (dotnet.mkDotnet { + sdkVersion = "10.0.103"; + outputHash = validOutputHash; + additionalSdks = [ + { + sdkVersion = "9.0.404"; + outputHash = validOutputHash; + } + ]; + }); + expected = { + success = false; + value = false; + }; + }; + + testMkDotnetAdditionalSdksRejectWorkloads = { + expr = builtins.tryEval (dotnet.mkDotnet { + sdkVersion = "10.0.103"; + outputHash = validOutputHash; + additionalSdks = [ + { + sdkVersion = "9.0.404"; + workloads = ["android"]; + } + ]; + }); + expected = { + success = false; + value = false; + }; + }; }