{
  lib,
  stdenv,
  callPackage,
  runCommand,
  writeText,
  pub2nix,
  dartHooks,
  makeWrapper,
  dart,
  nodejs,
  darwin,
  jq,
  yq,
}:

{
  src,
  sourceRoot ? "source",
  packageRoot ? (lib.removePrefix "/" (lib.removePrefix "source" sourceRoot)),
  gitHashes ? { },
  sdkSourceBuilders ? { },
  customSourceBuilders ? { },

  sdkSetupScript ? "",
  extraPackageConfigSetup ? "",

  # Output type to produce. Can be any kind supported by dart
  # https://dart.dev/tools/dart-compile#types-of-output
  # If using jit, you might want to pass some arguments to `dartJitFlags`
  dartOutputType ? "exe",
  dartCompileCommand ? "dart compile",
  dartCompileFlags ? [ ],
  # These come at the end of the command, useful to pass flags to the jit run
  dartJitFlags ? [ ],

  # Attrset of entry point files to build and install.
  # Where key is the final binary path and value is the source file path
  # e.g. { "bin/foo" = "bin/main.dart";  }
  # Set to null to read executables from pubspec.yaml
  dartEntryPoints ? null,
  # Used when wrapping aot, jit, kernel, and js builds.
  # Set to null to disable wrapping.
  dartRuntimeCommand ?
    if dartOutputType == "aot-snapshot" then
      "${dart}/bin/dartaotruntime"
    else if (dartOutputType == "jit-snapshot" || dartOutputType == "kernel") then
      "${dart}/bin/dart"
    else if dartOutputType == "js" then
      "${nodejs}/bin/node"
    else
      null,

  runtimeDependencies ? [ ],
  extraWrapProgramArgs ? "",

  autoPubspecLock ? null,
  pubspecLock ?
    if autoPubspecLock == null then
      throw "The pubspecLock argument is required. If import-from-derivation is allowed (it isn't in Nixpkgs), you can set autoPubspecLock to the path to a pubspec.lock instead."
    else
      assert lib.assertMsg (builtins.pathExists autoPubspecLock)
        "The pubspec.lock file could not be found!";
      lib.importJSON (
        runCommand "${lib.getName args}-pubspec-lock-json" {
          nativeBuildInputs = [ yq ];
        } ''yq . '${autoPubspecLock}' > "$out"''
      ),
  ...
}@args:

let
  generators = callPackage ./generators.nix { inherit dart; } { buildDrvArgs = args; };

  pubspecLockFile = builtins.toJSON pubspecLock;
  pubspecLockData = pub2nix.readPubspecLock {
    inherit
      src
      packageRoot
      pubspecLock
      gitHashes
      customSourceBuilders
      ;
    sdkSourceBuilders = {
      # https://github.com/dart-lang/pub/blob/e1fbda73d1ac597474b82882ee0bf6ecea5df108/lib/src/sdk/dart.dart#L80
      "dart" =
        name:
        runCommand "dart-sdk-${name}" { passthru.packageRoot = "."; } ''
          for path in '${dart}/pkg/${name}'; do
            if [ -d "$path" ]; then
              ln -s "$path" "$out"
              break
            fi
          done

          if [ ! -e "$out" ]; then
            echo 1>&2 'The Dart SDK does not contain the requested package: ${name}!'
            exit 1
          fi
        '';
    }
    // sdkSourceBuilders;
  };
  packageConfig = generators.linkPackageConfig {
    inherit pubspecLock;
    packageConfig = pub2nix.generatePackageConfig {
      pname = if args.pname != null then "${args.pname}-${args.version}" else null;

      dependencies =
        # Ideally, we'd only include the main dependencies and their transitive
        # dependencies.
        #
        # The pubspec.lock file does not contain information about where
        # transitive dependencies come from, though, and it would be weird to
        # include the transitive dependencies of dev and override dependencies
        # without including the dev and override dependencies themselves.
        builtins.concatLists (builtins.attrValues pubspecLockData.dependencies);

      inherit (pubspecLockData) dependencySources;
    };
    extraSetupCommands = extraPackageConfigSetup;
  };

  inherit (dartHooks.override { inherit dart; })
    dartConfigHook
    dartBuildHook
    dartInstallHook
    dartFixupHook
    ;

  baseDerivation = stdenv.mkDerivation (
    finalAttrs:
    (removeAttrs args [
      "gitHashes"
      "sdkSourceBuilders"
      "pubspecLock"
      "customSourceBuilders"
    ])
    // {
      inherit
        pubspecLockFile
        packageConfig
        sdkSetupScript
        dartCompileCommand
        dartOutputType
        dartRuntimeCommand
        dartCompileFlags
        dartJitFlags
        ;

      outputs = [
        "out"
        "pubcache"
      ]
      ++ args.outputs or [ ];

      dartEntryPoints =
        if (dartEntryPoints != null) then
          writeText "entrypoints.json" (builtins.toJSON dartEntryPoints)
        else
          null;

      runtimeDependencies = map lib.getLib runtimeDependencies;

      nativeBuildInputs =
        (args.nativeBuildInputs or [ ])
        ++ [
          dart
          dartConfigHook
          dartBuildHook
          dartInstallHook
          dartFixupHook
          makeWrapper
          jq
        ]
        ++ lib.optionals stdenv.hostPlatform.isDarwin [
          darwin.sigtool
        ]
        ++
          # Ensure that we inherit the propagated build inputs from the dependencies.
          builtins.attrValues pubspecLockData.dependencySources;

      preConfigure = args.preConfigure or "" + ''
        ln -sf "$pubspecLockFilePath" pubspec.lock
      '';

      # When stripping, it seems some ELF information is lost and the dart VM cli
      # runs instead of the expected program. Don't strip if it's an exe output.
      dontStrip = args.dontStrip or (dartOutputType == "exe");

      passAsFile = [ "pubspecLockFile" ];

      passthru = {
        pubspecLock = pubspecLockData;
      }
      // (args.passthru or { });

      meta = (args.meta or { }) // {
        platforms = args.meta.platforms or dart.meta.platforms;
      };
    }
  );
in
assert
  !(builtins.isString dartOutputType && dartOutputType != "")
  -> throw "dartOutputType must be a non-empty string";
baseDerivation
