{
  buildDotnetModule,
  darwin,
  dotnetCorePackages,
  fetchFromGitHub,
  fetchpatch,
  git,
  glibc,
  glibcLocales,
  lib,
  nixosTests,
  stdenv,
  which,
  buildPackages,
  runtimeShell,
  # List of Node.js runtimes the package should support
  nodeRuntimes ? [ "node20" ],
  nodejs_20,
}:

# Node.js runtimes supported by upstream
assert builtins.all (x: builtins.elem x [ "node20" ]) nodeRuntimes;

buildDotnetModule rec {
  pname = "github-runner";
  version = "2.321.0";

  src = fetchFromGitHub {
    owner = "actions";
    repo = "runner";
    rev = "v${version}";
    hash = "sha256-KZ072v5kYlD78RGQl13Aj05DGzj2+r2akzyZ1aJn93A=";
    leaveDotGit = true;
    postFetch = ''
      git -C $out rev-parse --short HEAD > $out/.git-revision
      rm -rf $out/.git
    '';
  };

  # The git commit is read during the build and some tests depend on a git repo to be present
  # https://github.com/actions/runner/blob/22d1938ac420a4cb9e3255e47a91c2e43c38db29/src/dir.proj#L5
  unpackPhase = ''
    cp -r $src $TMPDIR/src
    chmod -R +w $TMPDIR/src
    cd $TMPDIR/src
    (
      export PATH=${buildPackages.git}/bin:$PATH
      git init
      git config user.email "root@localhost"
      git config user.name "root"
      git add .
      git commit -m "Initial commit"
      git checkout -b v${version}
    )
    mkdir -p $TMPDIR/bin
    cat > $TMPDIR/bin/git <<EOF
    #!${runtimeShell}
    if [ \$# -eq 1 ] && [ "\$1" = "rev-parse" ]; then
      echo $(cat $TMPDIR/src/.git-revision)
      exit 0
    fi
    exec ${buildPackages.git}/bin/git "\$@"
    EOF
    chmod +x $TMPDIR/bin/git
    export PATH=$TMPDIR/bin:$PATH
  '';

  patches = [
    # Replace some paths that originally point to Nix's read-only store
    ./patches/host-context-dirs.patch
    # Use GetDirectory() to obtain "diag" dir
    ./patches/use-get-directory-for-diag.patch
    # Don't try to install service
    ./patches/dont-install-service.patch
    # Access `.env` and `.path` relative to `$RUNNER_ROOT`, if set
    ./patches/env-sh-use-runner-root.patch
    # Fix FHS path: https://github.com/actions/runner/pull/2464
    (fetchpatch {
      name = "ln-fhs.patch";
      url = "https://github.com/actions/runner/commit/5ff0ce1.patch";
      hash = "sha256-2Vg3cKZK3cE/OcPDZkdN2Ro2WgvduYTTwvNGxwCfXas=";
    })
    # Fix source path discovery in tests
    ./patches/test-getsrcpath.patch
  ];

  postPatch = ''
    # Ignore changes to src/Runner.Sdk/BuildConstants.cs
    substituteInPlace src/dir.proj \
      --replace-fail 'git update-index --assume-unchanged ./Runner.Sdk/BuildConstants.cs' \
                     'true'
  '';

  DOTNET_SYSTEM_GLOBALIZATION_INVARIANT = isNull glibcLocales;
  LOCALE_ARCHIVE = lib.optionalString (
    !DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
  ) "${glibcLocales}/lib/locale/locale-archive";

  postConfigure = ''
    # Generate src/Runner.Sdk/BuildConstants.cs
    dotnet msbuild \
      -t:GenerateConstant \
      -p:ContinuousIntegrationBuild=true \
      -p:Deterministic=true \
      -p:PackageRuntime="${dotnetCorePackages.systemToDotnetRid stdenv.hostPlatform.system}" \
      -p:RunnerVersion="${version}" \
      src/dir.proj
  '';

  nativeBuildInputs =
    [
      which
      git
    ]
    ++ lib.optionals (stdenv.hostPlatform.isDarwin && stdenv.hostPlatform.isAarch64) [
      darwin.autoSignDarwinBinariesHook
    ];

  buildInputs = [ (lib.getLib stdenv.cc.cc) ];

  dotnet-sdk = dotnetCorePackages.sdk_8_0;
  dotnet-runtime = dotnetCorePackages.runtime_8_0;

  dotnetFlags = [
    "-p:PackageRuntime=${dotnetCorePackages.systemToDotnetRid stdenv.hostPlatform.system}"
  ];

  # As given here: https://github.com/actions/runner/blob/0befa62/src/dir.proj#L33-L41
  projectFile = [
    "src/Sdk/Sdk.csproj"
    "src/Runner.Common/Runner.Common.csproj"
    "src/Runner.Listener/Runner.Listener.csproj"
    "src/Runner.Worker/Runner.Worker.csproj"
    "src/Runner.PluginHost/Runner.PluginHost.csproj"
    "src/Runner.Sdk/Runner.Sdk.csproj"
    "src/Runner.Plugins/Runner.Plugins.csproj"
  ];
  nugetDeps = ./deps.nix;

  doCheck = true;

  __darwinAllowLocalNetworking = true;

  # Fully qualified name of disabled tests
  disabledTests =
    [
      "GitHub.Runner.Common.Tests.Listener.SelfUpdaterL0.TestSelfUpdateAsync"
      "GitHub.Runner.Common.Tests.ProcessInvokerL0.OomScoreAdjIsInherited"
    ]
    ++ map (x: "GitHub.Runner.Common.Tests.Listener.SelfUpdaterL0.TestSelfUpdateAsync_${x}") [
      "Cancel_CloneHashTask_WhenNotNeeded"
      "CloneHash_RuntimeAndExternals"
      "DownloadRetry"
      "FallbackToFullPackage"
      "NoUpdateOnOldVersion"
      "NotUseExternalsRuntimeTrimmedPackageOnHashMismatch"
      "UseExternalsRuntimeTrimmedPackage"
      "UseExternalsTrimmedPackage"
      "ValidateHash"
    ]
    ++ map (x: "GitHub.Runner.Common.Tests.Listener.SelfUpdaterV2L0.${x}") [
      "TestSelfUpdateAsync_DownloadRetry"
      "TestSelfUpdateAsync_ValidateHash"
      "TestSelfUpdateAsync"
    ]
    ++ map (x: "GitHub.Runner.Common.Tests.Worker.ActionManagerL0.PrepareActions_${x}") [
      "CompositeActionWithActionfile_CompositeContainerNested"
      "CompositeActionWithActionfile_CompositePrestepNested"
      "CompositeActionWithActionfile_MaxLimit"
      "CompositeActionWithActionfile_Node"
      "DownloadActionFromGraph"
      "NotPullOrBuildImagesMultipleTimes"
      "RepositoryActionWithActionYamlFile_DockerHubImage"
      "RepositoryActionWithActionfileAndDockerfile"
      "RepositoryActionWithActionfile_DockerHubImage"
      "RepositoryActionWithActionfile_Dockerfile"
      "RepositoryActionWithActionfile_DockerfileRelativePath"
      "RepositoryActionWithActionfile_Node"
      "RepositoryActionWithDockerfile"
      "RepositoryActionWithDockerfileInRelativePath"
      "RepositoryActionWithDockerfilePrepareActions_Repository"
      "RepositoryActionWithInvalidWrapperActionfile_Node"
      "RepositoryActionWithWrapperActionfile_PreSteps"
    ]
    ++ map (x: "GitHub.Runner.Common.Tests.DotnetsdkDownloadScriptL0.${x}") [
      "EnsureDotnetsdkBashDownloadScriptUpToDate"
      "EnsureDotnetsdkPowershellDownloadScriptUpToDate"
    ]
    ++ [ "GitHub.Runner.Common.Tests.Listener.RunnerL0.TestRunOnceHandleUpdateMessage" ]
    # Tests for trimmed runner packages which aim at reducing the update size. Not relevant for Nix.
    ++ map (x: "GitHub.Runner.Common.Tests.PackagesTrimL0.${x}") [
      "RunnerLayoutParts_CheckExternalsHash"
      "RunnerLayoutParts_CheckDotnetRuntimeHash"
    ]
    ++ lib.optionals (stdenv.hostPlatform.system == "aarch64-linux") [
      # "JavaScript Actions in Alpine containers are only supported on x64 Linux runners. Detected Linux Arm64"
      "GitHub.Runner.Common.Tests.Worker.StepHostL0.DetermineNodeRuntimeVersionInAlpineContainerAsync"
      "GitHub.Runner.Common.Tests.Worker.StepHostL0.DetermineNode20RuntimeVersionInAlpineContainerAsync"
    ]
    ++ lib.optionals DOTNET_SYSTEM_GLOBALIZATION_INVARIANT [
      "GitHub.Runner.Common.Tests.ProcessExtensionL0.SuccessReadProcessEnv"
      "GitHub.Runner.Common.Tests.Util.StringUtilL0.FormatUsesInvariantCulture"
      "GitHub.Runner.Common.Tests.Worker.VariablesL0.Constructor_SetsOrdinalIgnoreCaseComparer"
      "GitHub.Runner.Common.Tests.Worker.WorkerL0.DispatchCancellation"
      "GitHub.Runner.Common.Tests.Worker.WorkerL0.DispatchRunNewJob"
      "GitHub.Runner.Common.Tests.ProcessExtensionL0.SuccessReadProcessEnv"
    ];

  testProjectFile = [ "src/Test/Test.csproj" ];

  preCheck =
    ''
      mkdir -p _layout/externals
    ''
    + lib.optionalString (lib.elem "node20" nodeRuntimes) ''
      ln -s ${nodejs_20} _layout/externals/node20
    '';

  postInstall =
    ''
      mkdir -p $out/bin

      install -m755 src/Misc/layoutbin/runsvc.sh                 $out/lib/github-runner
      install -m755 src/Misc/layoutbin/RunnerService.js          $out/lib/github-runner
      install -m755 src/Misc/layoutroot/run.sh                   $out/lib/github-runner
      install -m755 src/Misc/layoutroot/run-helper.sh.template   $out/lib/github-runner/run-helper.sh
      install -m755 src/Misc/layoutroot/config.sh                $out/lib/github-runner
      install -m755 src/Misc/layoutroot/env.sh                   $out/lib/github-runner

      # env.sh is patched to not require any wrapping
      ln -sr "$out/lib/github-runner/env.sh" "$out/bin/"

      substituteInPlace $out/lib/github-runner/config.sh \
        --replace './bin/Runner.Listener' "$out/bin/Runner.Listener"
    ''
    + lib.optionalString stdenv.hostPlatform.isLinux ''
      substituteInPlace $out/lib/github-runner/config.sh \
        --replace 'command -v ldd' 'command -v ${glibc.bin}/bin/ldd' \
        --replace 'ldd ./bin' '${glibc.bin}/bin/ldd ${dotnet-runtime}/shared/Microsoft.NETCore.App/${dotnet-runtime.version}/' \
        --replace '/sbin/ldconfig' '${glibc.bin}/bin/ldconfig'
    ''
    + ''
      # Remove uneeded copy for run-helper template
      substituteInPlace $out/lib/github-runner/run.sh --replace 'cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh' ' '
      substituteInPlace $out/lib/github-runner/run-helper.sh --replace '"$DIR"/bin/' '"$DIR"/'

      # Make paths absolute
      substituteInPlace $out/lib/github-runner/runsvc.sh \
        --replace './externals' "$out/lib/externals" \
        --replace './bin/RunnerService.js' "$out/lib/github-runner/RunnerService.js"

      # The upstream package includes Node and expects it at the path
      # externals/node$version. As opposed to the official releases, we don't
      # link the Alpine Node flavors.
      mkdir -p $out/lib/externals
    ''
    + lib.optionalString (lib.elem "node20" nodeRuntimes) ''
      ln -s ${nodejs_20} $out/lib/externals/node20
    ''
    + ''
      # Install Nodejs scripts called from workflows
      install -D src/Misc/layoutbin/hashFiles/index.js $out/lib/github-runner/hashFiles/index.js
      mkdir -p $out/lib/github-runner/checkScripts
      install src/Misc/layoutbin/checkScripts/* $out/lib/github-runner/checkScripts/
    ''
    + lib.optionalString stdenv.hostPlatform.isLinux ''
      # Wrap explicitly to, e.g., prevent extra entries for LD_LIBRARY_PATH
      makeWrapperArgs=()

      # We don't wrap with libicu
      substituteInPlace $out/lib/github-runner/config.sh \
        --replace '$LDCONFIG_COMMAND -NXv ''${libpath//:/ }' 'echo libicu'
    ''
    + ''
      # XXX: Using the corresponding Nix argument does not work as expected:
      #      https://github.com/NixOS/nixpkgs/issues/218449
      # Common wrapper args for `executables`
      makeWrapperArgs+=(
        --run 'export RUNNER_ROOT="''${RUNNER_ROOT:-"$HOME/.github-runner"}"'
        --run 'mkdir -p "$RUNNER_ROOT"'
        --chdir "$out"
      )
    '';

  # List of files to wrap
  executables = [
    "config.sh"
    "Runner.Listener"
    "Runner.PluginHost"
    "Runner.Worker"
    "run.sh"
    "runsvc.sh"
  ];

  doInstallCheck = true;
  installCheckPhase = ''
    runHook preInstallCheck

    export RUNNER_ROOT="$TMPDIR"

    $out/bin/config.sh --help >/dev/null
    $out/bin/Runner.Listener --help >/dev/null

    version=$($out/bin/Runner.Listener --version)
    if [[ "$version" != "${version}" ]]; then
      printf 'Unexpected version %s' "$version"
      exit 1
    fi

    commit=$($out/bin/Runner.Listener --commit)
    if [[ "$commit" != "$(git rev-parse HEAD)" ]]; then
      printf 'Unexpected commit %s' "$commit"
      exit 1
    fi

    runHook postInstallCheck
  '';

  passthru = {
    tests.smoke-test = nixosTests.github-runner;
    updateScript = ./update.sh;
  };

  meta = with lib; {
    changelog = "https://github.com/actions/runner/releases/tag/v${version}";
    description = "Self-hosted runner for GitHub Actions";
    homepage = "https://github.com/actions/runner";
    license = licenses.mit;
    maintainers = with maintainers; [
      veehaitch
      kfollesdal
      aanderse
      zimbatm
    ];
    platforms = [
      "x86_64-linux"
      "aarch64-linux"
      "x86_64-darwin"
      "aarch64-darwin"
    ];
    sourceProvenance = with sourceTypes; [ binaryNativeCode ];
  };
}