[
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\n\non:\n  push:\n  pull_request:\n  schedule:\n  - cron: '0 16 * * *'  # UTC 16:00 daily\n\njobs:\n  format:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: DeterminateSystems/determinate-nix-action@v3\n        with:\n          extra-conf: |\n            lazy-trees = true\n            eval-cores = 0\n      - run: nix run nixpkgs#nixfmt-tree -- --ci\n\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        # TODO: re-enable aarch64-linux once github native runner supports nested virtualization.\n        system: [x86_64-linux]\n        version:\n        - nixpkgs: nixos-25.11\n          home-manager: release-25.11\n        - nixpkgs: nixos-unstable\n          home-manager: master\n\n    steps:\n    - name: checkout\n      uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n\n    - uses: DeterminateSystems/determinate-nix-action@v3\n      with:\n        extra-conf: |\n          lazy-trees = true\n          eval-cores = 0\n\n    - uses: endersonmenezes/free-disk-space@v3\n      with:\n        remove_android: true\n        remove_dotnet: true\n        remove_haskell: true\n        rm_cmd: rmz\n        rmz_version: 3.1.1\n\n    - uses: cachix/cachix-action@v16\n      env:\n        CACHIX_AUTH_TOKEN_PRESENT: ${{ secrets.CACHIX_AUTH_TOKEN != '' }}\n      if: ${{ env.CACHIX_AUTH_TOKEN_PRESENT == 'true' }}\n      with:\n        name: quadlet-nix\n        authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n\n    - name: Run tests\n      run: >\n        nix flake check\n        --keep-going\n        --all-systems\n        --override-input nixpkgs 'github:NixOS/nixpkgs/${{ matrix.version.nixpkgs }}'\n        --override-input home-manager 'github:nix-community/home-manager/${{ matrix.version.home-manager }}'\n        --override-input test-config \"path:$(pwd)/tests/${{ matrix.system }}\"\n        ./tests\n\n    - name: Build docs\n      run: |\n        nix build ./docs#book\n\n    - name: Upload docs\n      if: matrix.system == 'x86_64-linux' && matrix.version.nixpkgs == 'nixos-unstable'\n      uses: actions/upload-pages-artifact@v3\n      with:\n        path: ./result\n\n  pass:\n    needs: [format, test]\n    runs-on: ubuntu-slim\n    steps:\n    - run: true\n\n  publish-docs:\n    if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)\n    needs: pass\n    runs-on: ubuntu-latest\n    permissions:\n      pages: write\n      id-token: write\n    steps:\n    - uses: actions/deploy-pages@v4\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 SEIAROTg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# quadlet-nix\n\nManages Podman containers, networks, pods, etc. on NixOS via [Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html).\n\n## Features\n\n- Supports Podman containers, networks, pods, volumes, etc.\n- Supports declarative update and deletion of networks.\n- Supports rootful and rootless (via [Home Manager](https://github.com/nix-community/home-manager)) resources behind the same interface.\n- Supports [Podman auto-update][podman-auto-update].\n- Supports cross-referencing between resources in Nix language.\n- Full quadlet options support, typed and properly escaped.\n- Reliability through effective testing.\n- Simplicity.\n- Whatever offered by Nix or Quadlet.\n\n[podman-auto-update]: https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html\n\n## Motivation\n\nThis project was started in Aug 2023, as a result of [the author's frustration on some relatively simple container management needs](https://seiarotg.me/post/tidy-up-homelab-containers/), where then available technologies are either overly restrictive, or overly complex that requires non-trivial but pointless investment ad-hoc domain knowledge.\n\n`quadlet-nix` is designed to be a simple tool that just works. Quadlet options are directly mapped into Nix, allowing users to effectively manage their Podman resources in the Nix language, without having to acquire domain knowledge in yet another tool. Prior knowledge and documentation of Podman continue to apply.\n\n## Comparison\n\nBelow are comparisons with several alternatives for declaratively managing Podman containers on NixOS, effective as of May 2025.\n\n<details>\n<summary><a href=\"https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/oci-containers.nix\" target=\"_blank\">NixOS <code>virtualisation.oci-containers</code></a></summary>\n\n- 👍 Part of NixOS, no additional dependencies.\n- 👍 Rootless container support without additional dependencies.\n- 👍 Supports Docker.\n- 😐 Compatible with podman auto-update (requires external setup).\n- 👎 Limited options.\n- 👎 Lack of support for networks, pods, etc.\n\n</details>\n\n<details>\n<summary><a href=\"https://github.com/hercules-ci/arion\" target=\"_blank\"><code>arion</code></a></summary>\n\n- 👍 Supports Docker.\n- 😐 More indirection and moving parts.\n- 👎 Limited options.\n- 👎 Incompatible with podman auto-update.\n\n</details>\n\n<details>\n<summary><a href=\"https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html\"  target=\"_blank\">Vanilla Podman Quadlet</a></summary>\n\n- 👍 Even less indirection.\n- 😐 Compatible with podman auto-update (requires external setup).\n- 😐 Requires more work to set up.\n- 👎 Not integrated with rest of Nix configuration.\n\n</details>\n\n<details>\n<summary><a href=\"https://nix-community.github.io/home-manager/options.xhtml#opt-services.podman.enable\" target=\"_blank\">Home Manager <code>services.podman</code></a></summary>\n\n- 👍 Part of Home Manager, no additional dependencies if you are already using it.\n- 👎 Lack of rootful container support.\n\n</details>\n\n<details>\n<summary><a href=\"https://github.com/aksiksi/compose2nix\" target=\"_blank\"><code>compose2nix</code></a></summary>\n\n- 👍 Supports Docker.\n- 😐 Compatible with podman auto-update (requires external setup).\n- 😐 More indirection and moving parts.\n- 👎 Less maintainable Nix files due to generated boilerplate.\n- 👎 Manual regeneration is required.\n- 👎 Lack of rootless container support.\n- 👎 Limited options.\n- 👎 Fragmented configuration with source of truth being outside of Nix.\n\n</details>\n\n## How\n\nSee [seiarotg.github.io/quadlet-nix](https://seiarotg.github.io/quadlet-nix) for all options.\n\n## Recipes\n\n<details open>\n<summary>Rootful containers</summary>\n\n#### `flake.nix`\n\n```nix\n{\n    inputs = {\n        nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n        quadlet-nix.url = \"github:SEIAROTg/quadlet-nix\";\n    };\n    outputs = { nixpkgs, quadlet-nix, ... }@attrs: {\n        nixosConfigurations.machine = nixpkgs.lib.nixosSystem {\n            system = \"x86_64-linux\";\n            modules = [\n                ./configuration.nix\n                quadlet-nix.nixosModules.quadlet\n            ];\n        };\n    };\n}\n```\n\n#### `configuration.nix`\n\n```nix\n{ config, ... }: {\n    # ...\n    virtualisation.quadlet = let\n        inherit (config.virtualisation.quadlet) networks pods;\n    in {\n        containers = {\n            nginx.containerConfig.image = \"docker.io/library/nginx:latest\";\n            nginx.containerConfig.networks = [ \"podman\" networks.internal.ref ];\n            nginx.containerConfig.pod = pods.foo.ref;\n            nginx.serviceConfig.TimeoutStartSec = \"60\";\n        };\n        networks = {\n            internal.networkConfig.subnets = [ \"10.0.123.1/24\" ];\n        };\n        pods = {\n            foo = { };\n        };\n    };\n}\n```\n\n</details>\n\n<details>\n<summary>Rootless containers (via Home Manager)</summary>\n\n#### `flake.nix`\n\n```nix\n{\n    inputs = {\n        nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n        home-manager.url = \"github:nix-community/home-manager\";\n        home-manager.inputs.nixpkgs.follows = \"nixpkgs\";\n        quadlet-nix.url = \"github:SEIAROTg/quadlet-nix\";\n    };\n    outputs = { nixpkgs, quadlet-nix, home-manager, ... }@attrs: {\n        nixosConfigurations.machine = nixpkgs.lib.nixosSystem {\n            system = \"x86_64-linux\";\n            modules = [\n                ./configuration.nix\n                home-manager.nixosModules.home-manager\n                # to enable podman & podman systemd generator\n                quadlet-nix.nixosModules.quadlet\n            ];\n        };\n    };\n}\n```\n\n#### `configuration.nix`\n\n```nix\n{\n    # ...\n    # to enable podman & podman systemd generator\n    virtualisation.quadlet.enable = true;\n    users.users.alice = {\n        # ...\n        # required for auto start before user login\n        linger = true;\n        # required for rootless container with multiple users\n        autoSubUidGidRange = true;\n    };\n    home-manager.users.alice = { pkgs, config, ... }: {\n        # ...\n        imports = [ inputs.quadlet-nix.homeManagerModules.quadlet ];\n        virtualisation.quadlet.containers = {\n            echo-server = {\n                autoStart = true;\n                serviceConfig = {\n                    RestartSec = \"10\";\n                    Restart = \"always\";\n                };\n                containerConfig = {\n                    image = \"docker.io/mendhak/http-https-echo:31\";\n                    publishPorts = [ \"127.0.0.1:8080:8080\" ];\n                    userns = \"keep-id\";\n                };\n            };\n        };\n    };\n}\n```\n\n</details>\n\n<details>\n<summary>Rootless containers (in system systemd)</summary>\n\n⚠️ Not officially supported by Podman. Use at your own risk and expect breaking changes.\n\n```nix\n{ config, ... }: {\n    users.users.alice = {\n        uid = 1234;\n        # required for auto start before user login\n        linger = true;\n        # required for rootless container with multiple users\n        autoSubUidGidRange = true;\n    };\n    virtualisation.quadlet.containers.nginx = {\n        rootlessConfig.uid = config.users.users.alice.uid;\n        containerConfig.image = \"docker.io/library/nginx:latest\";\n    };\n}\n```\n\n</details>\n\n<details>\n<summary>Volumes</summary>\n\n```nix\n{ config, ... }: {\n    # ...\n    virtualisation.quadlet = let\n        inherit (config.virtualisation.quadlet) volumes;\n    in {\n        containers.nginx.containerConfig.image = \"docker.io/library/nginx:latest\";\n        containers.nginx.containerConfig.volumes = [\n            \"${volumes.nginx-config.ref}:/etc/nginx\"\n        ];\n        volumes.nginx-config.volumeConfig = {\n            type = \"bind\";\n            device = \"/path/to/host/directory\";\n        };\n    };\n}\n```\n\n</details>\n\n<details>\n<summary>Build (inlined <code>Containerfile</code>)</summary>\n\n```nix\n{ pkgs, config, ... }: {\n    # ...\n    virtualisation.quadlet = let\n        inherit (config.virtualisation.quadlet) builds;\n        containerfile = pkgs.writeText \"Containerfile\" ''\n          FROM docker.io/library/nginx:latest\n          # ...\n        '';\n    in {\n        containers.nginx.containerConfig.image = builds.nginx.ref;\n        builds.nginx.buildConfig.file = containerfile.outPath;\n    };\n}\n```\n\n</details>\n\n<details>\n<summary>Build (git repository)</summary>\n\n```nix\n{ config, ... }: {\n    # ...\n    virtualisation.quadlet = let\n        inherit (config.virtualisation.quadlet) builds;\n        src = builtins.fetchGit {\n          url = \"https://github.com/alpinelinux/docker-alpine.git\";\n          rev = \"4dc13cbc7caffe03c98aa99f28e27c2fb6f7e74d\";\n        };\n    in {\n        containers.example.containerConfig = {\n          image = builds.alpine.ref;\n          entrypoint = \"/bin/sh\";\n          exec = \"-c 'echo 123'\";\n        };\n        containers.example.serviceConfig.RemainAfterExit = true;\n        builds.alpine.buildConfig = {\n          tag = \"alpine:3.22\";\n          workdir = \"${src}/x86_64\";\n        };\n    };\n}\n```\n\nAlternatively, git integration of Podman can be used through `workdir = \"https://github.com/nginx/docker-nginx.git\"`. However, it will be users' responsibility to make binaries such as `git` available to the build service via `PATH`.\n\n</details>\n\n<details>\n<summary>Image</summary>\n\n```nix\n{ config, ... }: {\n    # ...\n    virtualisation.quadlet = let\n        inherit (config.virtualisation.quadlet) images;\n    in {\n        containers.nginx.containerConfig.image = images.nginx.ref;\n        images.nginx.imageConfig.image = \"docker-archive:/path/to/local/image\";\n        images.nginx.imageConfig.tag = \"docker.com/library/nginx:latest\";\n    };\n}\n```\n\n</details>\n\n<details>\n<summary>Install raw Quadlet files</summary>\n\nIf you wish to write raw Quadlet files instead of using the Nix options, you may do so with `rawConfig`. Using this will cause all other options (except `autoStart`) to be ignored though.\n\n```nix\n{ config, ... }: {\n    # ...\n    virtualisation.quadlet = let\n        inherit (config.virtualisation.quadlet) networks pods;\n    in {\n        containers = {\n            nginx.rawConfig = ''\n                [Container]\n                Image=docker.io/library/nginx:latest\n                Network=podman\n                Network=${networks.internal.ref}\n                Pod=${pods.foo.ref}\n                [Service]\n                TimeoutStartSec=60\n            '';\n        };\n        networks = {\n            internal.networkConfig.subnets = [ \"10.0.123.1/24\" ];\n        };\n        pods = {\n            foo = { };\n        };\n    };\n}\n```\n</details>\n\n<details>\n<summary>Work with <code>pkgs.dockerTools</code></summary>\n\nPodman natively supports multiple transport, including `docker-archive` that can be used with `pkgs.dockerTools`.\n\n```nix\n{ pkgs, ... }: let\n    image = pkgs.dockerTools.buildImage {\n        # ...\n    };\nin {\n    virtualisation.quadlet.containers = {\n        foo.containerConfig.image = \"docker-archive:${image}\";\n    };\n}\n```\n\nSee: https://docs.podman.io/en/v5.5.0/markdown/podman-run.1.html#image\n\n</details>\n\n<details>\n<summary>Podman DNS not working?</summary>\n\nTo use Podman DNS, it needs to be enabled and allowed by your firewall.\n\nFor the default network, below sets up both for you:\n\n```nix\nvirtualisation.podman.defaultNetwork.settings.dns_enabled = true;\n```\n\nOr if you manage firewall separately, allow UDP port 53 on the input chain on host interface \"podman0\" and set:\n\n```nix\nvirtualisation.podman.defaultNetwork.settings.dns_enabled = true;\nvirtualisation.podman.defaultNetwork.settings.network_interface = \"podman0\";\n```\n\nFor custom networks managed by Quadlet, Podman DNS is enabled by default, unless `disableDns` is set. To set up the firewall rules:\n\n```nix\nvirtualisation.quadlet.networks.foo.networkConfig.interfaceName = \"br-foo\";\nnetworking.firewall.interfaces.br-foo.allowedUDPPorts = [ 53 ];\n```\n\nTo apply this on all custom networks:\n\n#### `enable-dns.nix`\n\n```nix\n{ config, lib, ... }: {\n  options.virtualisation.quadlet.networks = lib.mkOption {\n    type = lib.types.attrsOf (lib.types.submodule ( { name, ... }: {\n      networkConfig.driver = \"bridge\";\n      networkConfig.interfaceName = \"br-${name}\";\n    }));\n  };\n  config.networking.firewall.interfaces = lib.mapAttrs' (name: _: {\n    name = \"br-${name}\";\n    value.allowedUDPPorts = [ 53 ];\n  }) config.virtualisation.quadlet.networks;\n}\n```\n\n#### `configuration.nix`\n\n```nix\n{\n  imports = [\n    ./enable-dns.nix\n  ];\n  # ...\n}\n```\n\n</details>\n\n<details>\n<summary>Dependencies</summary>\n\nObvious dependencies such as those between containers and their networks are automatically set up by Quadlet, and thus no additional configuration is needed.\n\nExtra dependencies can be set up in systemd unit config. Note that `.ref` syntax is only valid in quadlet and does not work from regular systemd units.\n\n```nix\n{ config, ... }: {\n    # ...\n    virtualisation.quadlet = let\n        inherit (config.virtualisation.quadlet) containers;\n    in {\n        containers = {\n            database = {\n                # ...\n            };\n            server = {\n               # ...\n               unitConfig.Requires = [ containers.database.ref \"network-online.target\" ];\n               unitConfig.After = [ containers.database.ref \"network-online.target\" ];\n            };\n        };\n    };\n}\n```\n\n</details>\n\n<details>\n<summary>Debug & log access</summary>\n\n`quadlet-nix` tries to put containers into full management under systemd. This means once a container crashes, it will be fully deleted and debugging mechanisms like `podman ps -a` or `podman logs` will not work.\n\nHowever, status and logs are still accessible through systemd, namely, `systemctl status <service name>` and `journalctl -u <service name>`, where `<service name>` is container name, `<network name>-network`, `<pod name>-pod`, or similar. These names are the names as appeared in `virtualisation.quadlet.containers.<container name>`, rather than podman container name, in case it's different.\n\n</details>\n\n<details>\n<summary>The option I need is not available</summary>\n\nCheck if that option is supported by Podman Quadlet here: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html.\n\nIf it exists, please create an issue or send a PR to add.\n\nOtherwise, please use `PodmanArgs` and `GlobalArgs` to insert additional command line arguments as `quadlet-nix` does not intend to support options beyond what Quadlet offers.\n\n</details>\n"
  },
  {
    "path": "build.nix",
    "content": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils) encoders;\n\n  buildOpts = {\n    annotations = quadletOptions.mkOption {\n      type = types.oneOf [\n        (types.listOf types.str)\n        (types.attrsOf types.str)\n      ];\n      default = { };\n      example = {\n        annotation = \"value\";\n      };\n      cli = \"--annotation\";\n      property = \"Annotation\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    arch = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"aarch64\";\n      cli = \"--arch\";\n      property = \"Arch\";\n    };\n\n    authFile = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/etc/registry/auth.json\";\n      cli = \"--authfile\";\n      property = \"AuthFile\";\n    };\n\n    buildArgs = quadletOptions.mkOption {\n      type = types.oneOf [\n        (types.listOf types.str)\n        (types.attrsOf types.str)\n      ];\n      default = { };\n      example = {\n        foo = \"bar\";\n      };\n      cli = \"--build-arg\";\n      property = \"BuildArg\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    modules = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/etc/nvd.conf\" ];\n      cli = \"--module\";\n      property = \"ContainersConfModule\";\n    };\n\n    dns = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"192.168.55.1\" ];\n      cli = \"--dns\";\n      property = \"DNS\";\n    };\n\n    dnsSearch = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"foo.com\" ];\n      cli = \"--dns-search\";\n      property = \"DNSSearch\";\n    };\n\n    dnsOption = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"ndots:1\" ];\n      cli = \"--dns-option\";\n      property = \"DNSOption\";\n    };\n\n    environments = quadletOptions.mkOption {\n      type = types.attrsOf types.str;\n      default = { };\n      example = {\n        foo = \"bar\";\n      };\n      cli = \"--env\";\n      property = \"Environment\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    file = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/path/to/Containerfile\";\n      cli = \"--file\";\n      property = \"File\";\n    };\n\n    forceRm = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--force-rm\";\n      property = \"ForceRM\";\n    };\n\n    globalArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--log-level=debug\" ];\n      description = \"Additional command line arguments to insert between `podman` and `build`\";\n      property = \"GlobalArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    addGroups = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"keep-groups\" ];\n      cli = \"--group-add\";\n      property = \"GroupAdd\";\n    };\n\n    ignoreFile = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/path/to/.customignore\";\n      cli = \"--ignorefile\";\n      property = \"IgnoreFile\";\n    };\n\n    tag = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"localhost/imagename\";\n      cli = \"--tag\";\n      property = \"ImageTag\";\n    };\n\n    labels = quadletOptions.mkOption {\n      type = types.oneOf [\n        (types.listOf types.str)\n        (types.attrsOf types.str)\n      ];\n      default = { };\n      example = {\n        foo = \"bar\";\n      };\n      cli = \"--label\";\n      property = \"Label\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    networks = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"host\" ];\n      cli = \"--net\";\n      property = \"Network\";\n    };\n\n    podmanArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--add-host foobar\" ];\n      description = \"Additional command line arguments to insert after `podman build`\";\n      property = \"PodmanArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    pull = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"never\";\n      cli = \"--pull\";\n      property = \"Pull\";\n    };\n\n    retry = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 5;\n      cli = \"--retry\";\n      property = \"Retry\";\n    };\n\n    retryDelay = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"5s\";\n      cli = \"--retry-delay\";\n      property = \"RetryDelay\";\n    };\n\n    secrets = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"secret[,opt=opt …]\" ];\n      cli = \"--secret\";\n      property = \"Secret\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    workdir = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"file\";\n      description = \"Sets WorkingDirectory of systemd unit file\";\n      property = \"SetWorkingDirectory\";\n    };\n\n    target = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"my-app\";\n      cli = \"--target\";\n      property = \"Target\";\n    };\n\n    tlsVerify = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--tls-verify\";\n      property = \"TLSVerify\";\n    };\n\n    variant = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"arm/v7\";\n      cli = \"--variant\";\n      property = \"Variant\";\n    };\n\n    volumes = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/source:/dest\" ];\n      cli = \"--volume\";\n      property = \"Volume\";\n    };\n  };\n\n  serviceConfigDefault = {\n    TimeoutStartSec = 900;\n  };\nin\n{\n  options = quadletOptions.mkObjectOptions \"build\" {\n    buildConfig = buildOpts;\n  };\n\n  config =\n    let\n      buildTag = if config.buildConfig.tag != null then config.buildConfig.tag else \"localhost/${name}\";\n      buildConfig = config.buildConfig // {\n        tag = buildTag;\n      };\n      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;\n      unitConfig = {\n        Unit = {\n          Description = \"Podman build ${name}\";\n        }\n        // config.unitConfig;\n        Build = quadletUtils.configToProperties buildConfig buildOpts;\n        Service = serviceConfigDefault // config.serviceConfig;\n      }\n      // (if quadlet == { } then { } else { Quadlet = quadlet; });\n    in\n    lib.pipe\n      {\n        _serviceName = \"${name}-build\";\n        _configText =\n          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;\n        _autoStart = config.autoStart;\n        _autoEscapeRequired = quadletUtils.autoEscapeRequired buildConfig buildOpts;\n        ref = \"${name}.build\";\n      }\n      [\n        (quadletOptions.applyRootlessConfig config)\n      ];\n}\n"
  },
  {
    "path": "container.nix",
    "content": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils) encoders;\n\n  containerOpts = {\n    addCapabilities = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"NET_ADMIN\" ];\n      cli = \"--cap-add\";\n      property = \"AddCapability\";\n      encoders.scalar = encoders.scalar.quotedUnescaped;\n    };\n\n    addHosts = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"hostname:192.168.10.11\" ];\n      cli = \"--add-host\";\n      property = \"AddHost\";\n    };\n\n    devices = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/dev/foo\" ];\n      cli = \"--device\";\n      property = \"AddDevice\";\n      encoders.scalar = encoders.scalar.quotedUnescaped;\n    };\n\n    annotations = quadletOptions.mkOption {\n      type = types.oneOf [\n        (types.listOf types.str)\n        (types.attrsOf types.str)\n      ];\n      default = { };\n      example = {\n        annotation = \"value\";\n      };\n      cli = \"--annotation\";\n      property = \"Annotation\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    autoUpdate = quadletOptions.mkOption {\n      type = types.nullOr (\n        types.enum [\n          \"registry\"\n          \"local\"\n        ]\n      );\n      default = null;\n      example = \"registry\";\n      cli = \"--label \\\"io.containers.autoupdate=...\\\"\";\n      property = \"AutoUpdate\";\n    };\n\n    appArmor = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"alternate-profile\";\n      cli = \"--security-opt apparmor=...\";\n      property = \"AppArmor\";\n    };\n\n    cgroupsMode = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"no-conmon\";\n      cli = \"--cgroups\";\n      property = \"CgroupsMode\";\n    };\n\n    name = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"name\";\n      cli = \"--name\";\n      property = \"ContainerName\";\n    };\n\n    modules = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/etc/nvd.conf\" ];\n      cli = \"--module\";\n      property = \"ContainersConfModule\";\n    };\n\n    dns = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"192.168.55.1\" ];\n      cli = \"--dns\";\n      property = \"DNS\";\n    };\n\n    dnsSearch = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"foo.com\" ];\n      cli = \"--dns-search\";\n      property = \"DNSSearch\";\n    };\n\n    dnsOption = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"ndots:1\" ];\n      cli = \"--dns-option\";\n      property = \"DNSOption\";\n    };\n\n    dropCapabilities = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"NET_ADMIN\" ];\n      cli = \"--cap-drop\";\n      property = \"DropCapability\";\n      encoders.scalar = encoders.scalar.quotedUnescaped;\n    };\n\n    entrypoint = quadletOptions.mkOption {\n      type = types.nullOr (\n        types.oneOf [\n          types.str\n          (types.listOf types.str)\n        ]\n      );\n      default = null;\n      example = \"/foo.sh\";\n      cli = \"--entrypoint\";\n      property = \"Entrypoint\";\n      encoders.raw = encoders.scalar.raw;\n      encoders.list = encoders.list.json;\n    };\n\n    environments = quadletOptions.mkOption {\n      type = types.attrsOf types.str;\n      default = { };\n      example = {\n        foo = \"bar\";\n      };\n      cli = \"--env\";\n      property = \"Environment\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    environmentFiles = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/tmp/env\" ];\n      cli = \"--env-file\";\n      property = \"EnvironmentFile\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    environmentHost = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--env-host\";\n      property = \"EnvironmentHost\";\n    };\n\n    exec = quadletOptions.mkOption {\n      type = types.nullOr (\n        types.oneOf [\n          types.str\n          (types.listOf types.str)\n        ]\n      );\n      default = null;\n      example = \"/usr/bin/command\";\n      description = \"Command after image specification\";\n      property = \"Exec\";\n      # CAVEAT: doesn't prevent systemd environment variable substitution, but probably a quadlet problem?\n      encoders.scalar = encoders.scalar.raw;\n      encoders.list = encoders.list.oneLine encoders.scalar.quotedEscaped;\n    };\n\n    exposePorts = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"50-59\" ];\n      cli = \"--expose\";\n      property = \"ExposeHostPort\";\n    };\n\n    gidMaps = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"0:10000:10\" ];\n      cli = \"--gidmap\";\n      property = \"GIDMap\";\n      encoders.scalar = encoders.scalar.quotedUnescaped;\n    };\n\n    globalArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--log-level=debug\" ];\n      description = \"Additional command line arguments to insert between `podman` and `run`\";\n      property = \"GlobalArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    group = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"1234\";\n      cli = \"--user UID:...\";\n      property = \"Group\";\n    };\n\n    addGroups = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"keep-groups\" ];\n      cli = \"--group-add\";\n      property = \"GroupAdd\";\n    };\n\n    healthCmd = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/usr/bin/command\";\n      cli = \"--health-cmd\";\n      property = \"HealthCmd\";\n    };\n\n    healthInterval = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"2m\";\n      cli = \"--health-interval\";\n      property = \"HealthInterval\";\n    };\n\n    healthLogDestination = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/foo/log\";\n      cli = \"--health-log-destination\";\n      property = \"HealthLogDestination\";\n    };\n\n    healthMaxLogCount = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 5;\n      cli = \"--health-max-log-count\";\n      property = \"HealthMaxLogCount\";\n    };\n\n    healthMaxLogSize = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 500;\n      cli = \"--health-max-log-size\";\n      property = \"HealthMaxLogSize\";\n    };\n\n    healthOnFailure = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"kill\";\n      cli = \"--health-on-failure\";\n      property = \"HealthOnFailure\";\n    };\n\n    healthRetries = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 5;\n      cli = \"--health-retries\";\n      property = \"HealthRetries\";\n    };\n\n    healthStartPeriod = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"1m\";\n      cli = \"--health-start-period\";\n      property = \"HealthStartPeriod\";\n    };\n\n    healthStartupCmd = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/usr/bin/command\";\n      cli = \"--health-startup-cmd\";\n      property = \"HealthStartupCmd\";\n    };\n\n    healthStartupInterval = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"1m\";\n      cli = \"--health-startup-interval\";\n      property = \"HealthStartupInterval\";\n    };\n\n    healthStartupRetries = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 8;\n      cli = \"--health-startup-retries\";\n      property = \"HealthStartupRetries\";\n    };\n\n    healthStartupSuccess = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 2;\n      cli = \"--health-startup-success\";\n      property = \"HealthStartupSuccess\";\n    };\n\n    healthStartupTimeout = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"1m33s\";\n      cli = \"--health-startup-timeout\";\n      property = \"HealthStartupTimeout\";\n    };\n\n    healthTimeout = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"20s\";\n      cli = \"--health-timeout\";\n      property = \"HealthTimeout\";\n    };\n\n    hostname = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"new-host-name\";\n      cli = \"--hostname\";\n      property = \"HostName\";\n    };\n\n    httpProxy = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--http-proxy\";\n      property = \"HttpProxy\";\n    };\n\n    image = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"docker.io/library/nginx:latest\";\n      description = \"Image specification\";\n      property = \"Image\";\n    };\n\n    ip = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"192.5.0.1\";\n      cli = \"--ip\";\n      property = \"IP\";\n    };\n\n    ip6 = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"fd46:db93:aa76:ac37::10\";\n      cli = \"--ip6\";\n      property = \"IP6\";\n    };\n\n    labels = quadletOptions.mkOption {\n      type = types.oneOf [\n        (types.listOf types.str)\n        (types.attrsOf types.str)\n      ];\n      default = { };\n      example = {\n        foo = \"bar\";\n      };\n      cli = \"--label\";\n      property = \"Label\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    logDriver = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"journald\";\n      cli = \"--log-driver\";\n      property = \"LogDriver\";\n    };\n\n    logOptions = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"path=/var/log/mykube.json\" ];\n      cli = \"--log-opt\";\n      property = \"LogOpt\";\n      encoders.scalar = encoders.scalar.quotedUnescaped;\n    };\n\n    mask = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/proc/sys/foo:/proc/sys/bar\";\n      cli = \"--security-opt mask=...\";\n      property = \"Mask\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    memory = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"20g\";\n      cli = \"--memory\";\n      property = \"Memory\";\n    };\n\n    mounts = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"type=...\" ];\n      cli = \"--mount\";\n      property = \"Mount\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    networks = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"host\" ];\n      cli = \"--net\";\n      property = \"Network\";\n    };\n\n    networkAliases = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"name\" ];\n      cli = \"--network-alias\";\n      property = \"NetworkAlias\";\n    };\n\n    noNewPrivileges = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--security-opt no-new-privileges\";\n      property = \"NoNewPrivileges\";\n    };\n\n    notify = quadletOptions.mkOption {\n      type = types.enum [\n        null\n        true\n        false\n        \"healthy\"\n      ];\n      default = null;\n      cli = \"--sdnotify container\";\n      property = \"Notify\";\n    };\n\n    pidsLimit = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 10000;\n      cli = \"--pids-limit\";\n      property = \"PidsLimit\";\n    };\n\n    pod = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      cli = \"--pod\";\n      property = \"Pod\";\n    };\n\n    podmanArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--add-host foobar\" ];\n      description = \"Additional command line arguments to insert after `podman run`\";\n      property = \"PodmanArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    publishPorts = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"50-59\" ];\n      cli = \"--publish\";\n      property = \"PublishPort\";\n    };\n\n    pull = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"never\";\n      cli = \"--pull\";\n      property = \"Pull\";\n    };\n\n    readOnly = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--read-only\";\n      property = \"ReadOnly\";\n    };\n\n    readOnlyTmpfs = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--read-only-tmpfs\";\n      property = \"ReadOnlyTmpfs\";\n    };\n\n    reloadCmd = quadletOptions.mkOption {\n      type = types.nullOr (\n        types.oneOf [\n          types.str\n          (types.listOf types.str)\n        ]\n      );\n      default = null;\n      description = \"Adds ExecReload and run exec with the value\";\n      example = \"/usr/bin/command\";\n      property = \"ReloadCmd\";\n      encoders.scalar = encoders.scalar.raw;\n      encoders.list = encoders.list.oneLine encoders.scalar.quotedEscaped;\n    };\n\n    reloadSignal = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      description = \"Add ExecReload and run kill with the signal\";\n      example = \"SIGHUP\";\n      property = \"ReloadSignal\";\n    };\n\n    retry = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 5;\n      cli = \"--retry\";\n      property = \"Retry\";\n    };\n\n    retryDelay = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"5s\";\n      cli = \"--retry-delay\";\n      property = \"RetryDelay\";\n    };\n\n    rootfs = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/var/lib/rootfs\";\n      cli = \"--rootfs\";\n      property = \"Rootfs\";\n    };\n\n    runInit = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--init\";\n      property = \"RunInit\";\n    };\n\n    seccompProfile = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/tmp/s.json\";\n      cli = \"--security-opt seccomp=...\";\n      property = \"SeccompProfile\";\n    };\n\n    secrets = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"secret[,opt=opt …]\" ];\n      cli = \"--secret\";\n      property = \"Secret\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    securityLabelDisable = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--security-opt label=disable\";\n      property = \"SecurityLabelDisable\";\n    };\n\n    securityLabelFileType = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"usr_t\";\n      cli = \"--security-opt label=filetype:...\";\n      property = \"SecurityLabelFileType\";\n    };\n\n    securityLabelLevel = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"s0:c1,c2\";\n      cli = \"--security-opt label=level:s0:c1,c2\";\n      property = \"SecurityLabelLevel\";\n    };\n\n    securityLabelNested = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--security-opt label=nested\";\n      property = \"SecurityLabelNested\";\n    };\n\n    securityLabelType = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"spc_t\";\n      cli = \"--security-opt label=type:...\";\n      property = \"SecurityLabelType\";\n    };\n\n    shmSize = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"100m\";\n      cli = \"--shm-size\";\n      property = \"ShmSize\";\n    };\n\n    startWithPod = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      description = \"If pod is defined, container is started by pod\";\n      property = \"StartWithPod\";\n    };\n\n    stopSignal = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"SIGINT\";\n      cli = \"--stop-signal\";\n      property = \"StopSignal\";\n    };\n\n    stopTimeout = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 20;\n      cli = \"--stop-timeout\";\n      property = \"StopTimeout\";\n    };\n\n    subGIDMap = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"gtest\";\n      cli = \"--subgidname\";\n      property = \"SubGIDMap\";\n    };\n\n    subUIDMap = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"utest\";\n      cli = \"--subuidname\";\n      property = \"SubUIDMap\";\n    };\n\n    sysctl = quadletOptions.mkOption {\n      type = types.attrsOf types.str;\n      default = { };\n      example = {\n        name = \"value\";\n      };\n      cli = \"--sysctl\";\n      property = \"Sysctl\";\n      encoders.scalar = encoders.scalar.quotedUnescaped;\n    };\n\n    timezone = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"local\";\n      cli = \"--tz\";\n      property = \"Timezone\";\n    };\n\n    tmpfses = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/work\" ];\n      cli = \"--tmpfs\";\n      property = \"Tmpfs\";\n    };\n\n    uidMaps = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"0:10000:10\" ];\n      cli = \"--uidmap\";\n      property = \"UIDMap\";\n      encoders.scalar = encoders.scalar.quotedUnescaped;\n    };\n\n    ulimits = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"nofile=1000:10000\" ];\n      cli = \"--ulimit\";\n      property = \"Ulimit\";\n    };\n\n    unmask = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"ALL\";\n      cli = \"--security-opt unmask=...\";\n      property = \"Unmask\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    user = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"bin\";\n      cli = \"--user\";\n      property = \"User\";\n    };\n\n    userns = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"keep-id:uid=200,gid=210\";\n      cli = \"--userns\";\n      property = \"UserNS\";\n    };\n\n    volumes = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/source:/dest\" ];\n      cli = \"--volume\";\n      property = \"Volume\";\n    };\n\n    workdir = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"$HOME\";\n      cli = \"--workdir\";\n      property = \"WorkingDir\";\n    };\n  };\n\n  serviceConfigDefault = {\n    Restart = \"always\";\n    TimeoutStartSec = 900;\n  };\nin\n{\n  options = quadletOptions.mkObjectOptions \"container\" {\n    containerConfig = containerOpts;\n  };\n\n  config =\n    let\n      containerName = if config.containerConfig.name != null then config.containerConfig.name else name;\n      containerConfig = config.containerConfig // {\n        name = containerName;\n      };\n      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;\n      unitConfig = {\n        Unit = {\n          Description = \"Podman container ${name}\";\n        }\n        // config.unitConfig;\n        Container = quadletUtils.configToProperties containerConfig containerOpts;\n        Service = serviceConfigDefault // config.serviceConfig;\n      }\n      // (if quadlet == { } then { } else { Quadlet = quadlet; });\n    in\n    lib.pipe\n      {\n        _serviceName = name;\n        _configText =\n          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;\n        _autoStart = config.autoStart;\n        _autoEscapeRequired = quadletUtils.autoEscapeRequired containerConfig containerOpts;\n        ref = \"${name}.container\";\n\n        # quadlet default is \"split\" which does not work rootless under system systemd.\n        containerConfig.cgroupsMode = lib.mkIf config._rootless (lib.mkDefault \"enabled\");\n      }\n      [\n        (quadletOptions.applyRootlessConfig config)\n      ];\n}\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Docs\n\nTo generate the documentation, run:\n\n```sh\nnix build './docs#book'\n```\n"
  },
  {
    "path": "docs/flake.nix",
    "content": "{\n  description = \"quadlet-nix docs\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n    quadlet-nix.url = \"path:..\";\n  };\n\n  outputs =\n    {\n      nixpkgs,\n      quadlet-nix,\n      self,\n      ...\n    }:\n    let\n      allSystems = [\n        \"x86_64-linux\"\n        \"aarch64-linux\"\n      ];\n      perSystem = f: nixpkgs.lib.genAttrs allSystems f;\n    in\n    {\n      packages = perSystem (\n        system:\n        let\n          pkgs = import nixpkgs { inherit system; };\n          lib = pkgs.lib;\n          buildDocs =\n            module:\n            let\n              moduleFn = import module;\n              # filters out assertions, config, etc. that cause problems.\n              filteredModuleFn = args: { inherit (moduleFn args) options; };\n              eval = lib.evalModules {\n                modules = [\n                  { _module.args.pkgs = pkgs; }\n                  (lib.mirrorFunctionArgs moduleFn filteredModuleFn)\n                ];\n              };\n              options = lib.filterAttrs (name: _: name != \"_module\") eval.options;\n            in\n            pkgs.nixosOptionsDoc { inherit options; };\n\n          pages = {\n            nixosModules.quadlet = buildDocs quadlet-nix.nixosModules.quadlet;\n            homeManagerModules.quadlet = buildDocs quadlet-nix.homeManagerModules.quadlet;\n          };\n\n        in\n        {\n          inherit pages;\n\n          book = pkgs.stdenv.mkDerivation {\n            pname = \"quadlet-nix-docs-book\";\n            version = \"0.1\";\n            src = self;\n\n            nativeBuildInputs = [\n              pkgs.mdbook\n            ];\n\n            dontConfigure = true;\n            dontFixup = true;\n\n            buildPhase = ''\n              runHook preBuild\n              cp ${quadlet-nix}/README.md src/introduction.md\n              cp ${pages.nixosModules.quadlet.optionsCommonMark} src/nixos-options.md\n              cp ${pages.homeManagerModules.quadlet.optionsCommonMark} src/home-manager-options.md\n              mdbook build\n              runHook postBuild\n            '';\n\n            installPhase = ''\n              runHook preInstall\n              mv book $out\n              runHook postInstall\n            '';\n          };\n        }\n      );\n    };\n}\n"
  },
  {
    "path": "docs/src/SUMMARY.md",
    "content": "# Contents\n\n- [Introduction](./introduction.md)\n- [NixOS Options](./nixos-options.md)\n- [Home Manager Options](./home-manager-options.md)\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"NixOS and home-manager module for Podman Quadlets\";\n\n  outputs =\n    { self }:\n    {\n      nixosModules.quadlet = ./nixos-module.nix;\n      homeManagerModules.quadlet = ./home-manager-module.nix;\n    };\n}\n"
  },
  {
    "path": "home-manager-module.nix",
    "content": "{\n  config,\n  osConfig ? { },\n  lib,\n  pkgs,\n  ...\n}:\nlet\n  inherit (lib) mergeAttrsList mkIf getExe;\n\n  cfg = config.virtualisation.quadlet;\n  quadletUtils = import ./utils.nix {\n    inherit pkgs lib;\n    inherit (import (pkgs.path + \"/nixos/lib/utils.nix\") { inherit lib config pkgs; }) systemdUtils;\n    podmanPackage = osConfig.virtualisation.podman.package or pkgs.podman;\n    autoEscape = config.virtualisation.quadlet.autoEscape;\n  };\n  quadletOptions = import ./options.nix {\n    supportRootless = false;\n    inherit lib quadletUtils;\n  };\n  activationScript = lib.hm.dag.entryBefore [ \"reloadSystemd\" ] ''\n    mkdir -p '${config.xdg.configHome}/quadlet-nix/'\n    ln -sf \"''${XDG_RUNTIME_DIR:-/run/user/$UID}/systemd/generator/\" '${config.xdg.configHome}/quadlet-nix/out'\n  '';\nin\n{\n  options.virtualisation.quadlet = quadletOptions.mkTopLevelOptions { };\n  config =\n    let\n      allObjects = quadletOptions.getAllObjects cfg;\n      enable = cfg.enable == true || (cfg.enable == null && allObjects != [ ]);\n    in\n    mkIf enable {\n      assertions = quadletOptions.mkAssertions [ ] cfg;\n      warnings =\n        (quadletUtils.assertionsToWarnings [\n          {\n            assertion = enable -> (osConfig.virtualisation.quadlet.enable or true == true);\n            message = ''\n              The `virtualisation.quadlet.enable` in **NixOS config** is not set to true.\n              The NixOS module is required to set up Podman and explicit enablement will be required in the future.\n            '';\n          }\n        ])\n        ++ (quadletOptions.mkWarnings [ ] cfg);\n\n      home.activation.quadletNix = mkIf (lib.length allObjects > 0) activationScript;\n\n      xdg.configFile =\n        let\n          configPathLink =\n            (pkgs.linkFarm \"quadlet-out-path\" [\n              {\n                name = \"quadlet-nix\";\n                path = \"${config.xdg.configHome}/quadlet-nix\";\n              }\n            ])\n            + \"/quadlet-nix\";\n        in\n        mergeAttrsList (\n          map (p: {\n            # Install the .container, .network, etc files\n            \"containers/systemd/${p.ref}\" = {\n              text = p._configText;\n            };\n            # Import quadlet-generated unit as a dropin override.\n            \"systemd/user/${p._serviceName}.service.d/override.conf\" = {\n              source = \"${configPathLink}/out/${p._serviceName}.service\";\n            };\n          }) allObjects\n        )\n        // {\n          # `systemctl`, `sleep`, etc. not found\n          \"systemd/user/podman-user-wait-network-online.service.d/override.conf\" = {\n            text = quadletUtils.unitConfigToText {\n              Service.ExecSearchPath = [ \"/run/current-system/sw/bin/\" ];\n            };\n          };\n        };\n\n      systemd.user.services =\n        mergeAttrsList (\n          map (p: {\n            # Inject hash for the activation process to detect changes.\n            # Must be in the main file as it's the only thing home-manager switch process looks at.\n            # WantedBy must be set through `systemd.user.services` which generates .targets.wants symlinks.\n            # sd-switch only starts new services with those symlinks.\n            ${p._serviceName} = {\n              Unit.X-QuadletNixConfigHash = builtins.hashString \"sha256\" p._configText;\n              Install.WantedBy = if p._autoStart then [ \"default.target\" ] else [ ];\n            };\n          }) allObjects\n        )\n        // {\n          # TODO: link from ${pkgs.podman}/share/systemd/user/podman-auto-update.service\n          # when https://github.com/containers/podman/issues/24637 is fixed.\n          podman-auto-update = mkIf cfg.autoUpdate.enable {\n            Unit = {\n              Description = \"Podman auto-update service\";\n              Documentation = \"man:podman-auto-update(1)\";\n            };\n            Service = {\n              Type = \"oneshot\";\n              ExecStart = \"${getExe quadletUtils.podmanPackage} auto-update\";\n              ExecStartPost = \"${getExe quadletUtils.podmanPackage} image prune -f\";\n              TimeoutStartSec = \"900s\";\n              TimeoutStopSec = \"10s\";\n            };\n          };\n        };\n\n      systemd.user.timers.podman-auto-update = mkIf cfg.autoUpdate.enable {\n        Unit = {\n          Description = \"Podman auto-update timer\";\n          Documentation = \"man:podman-auto-update(1)\";\n        };\n        Timer = {\n          OnCalendar = cfg.autoUpdate.calendar;\n          Persistent = true;\n        };\n        Install.WantedBy = [ \"timers.target\" ];\n      };\n    };\n}\n"
  },
  {
    "path": "image.nix",
    "content": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils) encoders;\n\n  imageOpts = {\n    allTags = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--all-tags\";\n      property = \"AllTags\";\n    };\n\n    arch = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"aarch64\";\n      cli = \"--arch\";\n      property = \"Arch\";\n    };\n\n    authFile = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/etc/registry/auth.json\";\n      cli = \"--authfile\";\n      property = \"AuthFile\";\n    };\n\n    certDir = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/etc/registry/certs\";\n      cli = \"--cert-dir\";\n      property = \"CertDir\";\n    };\n\n    modules = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/etc/nvd.conf\" ];\n      cli = \"--module\";\n      property = \"ContainersConfModule\";\n    };\n\n    creds = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"myname:mypassword\";\n      cli = \"--creds\";\n      property = \"Creds\";\n    };\n\n    decryptionKey = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"/etc/registry.key\";\n      cli = \"--decryption-key\";\n      property = \"DecryptionKey\";\n    };\n\n    globalArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--log-level=debug\" ];\n      description = \"Additional command line arguments to insert between `podman` and `pull`\";\n      property = \"GlobalArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    image = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"docker.io/library/nginx:latest\";\n      description = \"Image specification\";\n      property = \"Image\";\n    };\n\n    tag = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"localhost/imagename\";\n      description = \"FQIN of the referenced Image. Only meaningful when source is a file or directory archive. Used when resolving .image references.\";\n      property = \"ImageTag\";\n    };\n\n    os = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"windows\";\n      cli = \"--os\";\n      property = \"OS\";\n    };\n\n    podmanArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--add-host foobar\" ];\n      description = \"Additional command line arguments to insert after `podman pull`\";\n      property = \"PodmanArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    policy = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"always\";\n      cli = \"--policy\";\n      property = \"Policy\";\n    };\n\n    retry = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 5;\n      cli = \"--retry\";\n      property = \"Retry\";\n    };\n\n    retryDelay = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"5s\";\n      cli = \"--retry-delay\";\n      property = \"RetryDelay\";\n    };\n\n    tlsVerify = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--tls-verify\";\n      property = \"TLSVerify\";\n    };\n\n    variant = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"arm/v7\";\n      cli = \"--variant\";\n      property = \"Variant\";\n    };\n  };\n\n  serviceConfigDefault = {\n    TimeoutStartSec = 900;\n  };\nin\n{\n  options = quadletOptions.mkObjectOptions \"image\" {\n    imageConfig = imageOpts;\n  };\n\n  config =\n    let\n      imageConfig = config.imageConfig;\n      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;\n      unitConfig = {\n        Unit = {\n          Description = \"Podman image ${name}\";\n        }\n        // config.unitConfig;\n        Image = quadletUtils.configToProperties imageConfig imageOpts;\n        Service = serviceConfigDefault // config.serviceConfig;\n      }\n      // (if quadlet == { } then { } else { Quadlet = quadlet; });\n    in\n    lib.pipe\n      {\n        _serviceName = \"${name}-image\";\n        _configText =\n          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;\n        _autoStart = config.autoStart;\n        _autoEscapeRequired = quadletUtils.autoEscapeRequired imageConfig imageOpts;\n        ref = \"${name}.image\";\n      }\n      [\n        (quadletOptions.applyRootlessConfig config)\n      ];\n}\n"
  },
  {
    "path": "network.nix",
    "content": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types getExe;\n  inherit (quadletUtils) encoders;\n\n  networkOpts = {\n    modules = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/etc/nvd.conf\" ];\n      cli = \"--module\";\n      property = \"ContainersConfModule\";\n    };\n\n    disableDns = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--disable-dns\";\n      property = \"DisableDNS\";\n    };\n\n    dns = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"192.168.55.1\" ];\n      cli = \"--dns\";\n      property = \"DNS\";\n    };\n\n    driver = quadletOptions.mkOption {\n      type = types.nullOr (\n        types.enum [\n          \"bridge\"\n          \"macvlan\"\n          \"ipvlan\"\n        ]\n      );\n      default = null;\n      example = \"bridge\";\n      cli = \"--driver\";\n      property = \"Driver\";\n    };\n\n    gateways = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"192.168.55.3\" ];\n      cli = \"--gateway\";\n      property = \"Gateway\";\n    };\n\n    globalArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--log-level=debug\" ];\n      description = \"Additional command line arguments to insert between `podman` and `network create`\";\n      property = \"GlobalArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    interfaceName = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"enp1\";\n      cli = \"--interface-name\";\n      property = \"InterfaceName\";\n    };\n\n    internal = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--internal\";\n      property = \"Internal\";\n    };\n\n    ipamDriver = quadletOptions.mkOption {\n      type = types.nullOr (\n        types.enum [\n          \"host-local\"\n          \"dhcp\"\n          \"none\"\n        ]\n      );\n      default = null;\n      example = \"dhcp\";\n      cli = \"--ipam-driver\";\n      property = \"IPAMDriver\";\n    };\n\n    ipRanges = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"192.168.55.128/25\" ];\n      cli = \"--ip-range\";\n      property = \"IPRange\";\n    };\n\n    ipv6 = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--ipv6\";\n      property = \"IPv6\";\n    };\n\n    labels = quadletOptions.mkOption {\n      type = types.oneOf [\n        (types.listOf types.str)\n        (types.attrsOf types.str)\n      ];\n      default = { };\n      example = {\n        foo = \"bar\";\n      };\n      cli = \"--label\";\n      property = \"Label\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    name = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"foo\";\n      description = \"Network name as in `podman network create foo`\";\n      property = \"NetworkName\";\n    };\n\n    networkDeleteOnStop = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      description = \"When set to true the network is deleted when the service is stopped\";\n      property = \"NetworkDeleteOnStop\";\n    };\n\n    options = quadletOptions.mkOption {\n      # TODO: drop string support and remove warning.\n      type = types.oneOf [\n        types.str\n        (types.listOf types.str)\n        (types.attrsOf types.str)\n      ];\n      default = { };\n      example = {\n        isolate = \"true\";\n      };\n      cli = \"--opt\";\n      property = \"Options\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    podmanArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--dns=192.168.55.1\" ];\n      description = \"Additional command line arguments to insert after `podman network create`\";\n      property = \"PodmanArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    subnets = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"192.5.0.0/16\" ];\n      cli = \"--subnet\";\n      property = \"Subnet\";\n    };\n  };\nin\n{\n  options = quadletOptions.mkObjectOptions \"network\" {\n    networkConfig = networkOpts;\n  };\n\n  config =\n    let\n      networkName = if config.networkConfig.name != null then config.networkConfig.name else name;\n      networkConfig = config.networkConfig // {\n        name = networkName;\n      };\n      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;\n      unitConfig = {\n        Unit = {\n          Description = \"Podman network ${name}\";\n        }\n        // config.unitConfig;\n        Network = quadletUtils.configToProperties networkConfig networkOpts;\n        Service = {\n          # TODO: switches to NetworkDeleteOnStop once podman in stable nixpkgs supports it\n          ExecStop = \"${getExe quadletUtils.podmanPackage} network rm ${networkName}\";\n        }\n        // config.serviceConfig;\n      }\n      // (if quadlet == { } then { } else { Quadlet = quadlet; });\n    in\n    lib.pipe\n      {\n        _serviceName = \"${name}-network\";\n        _configText =\n          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;\n        _autoStart = config.autoStart;\n        _autoEscapeRequired = quadletUtils.autoEscapeRequired networkConfig networkOpts;\n        ref = \"${name}.network\";\n      }\n      [\n        (quadletOptions.applyRootlessConfig config)\n      ];\n}\n"
  },
  {
    "path": "nixos-module.nix",
    "content": "{\n  config,\n  lib,\n  pkgs,\n  ...\n}:\nlet\n  inherit (lib) mergeAttrsList mkIf;\n\n  cfg = config.virtualisation.quadlet;\n  quadletUtils = import ./utils.nix {\n    inherit pkgs lib;\n    inherit (import (pkgs.path + \"/nixos/lib/utils.nix\") { inherit lib config pkgs; }) systemdUtils;\n    podmanPackage = config.virtualisation.podman.package;\n    autoEscape = config.virtualisation.quadlet.autoEscape;\n  };\n  quadletOptions = import ./options.nix {\n    supportRootless = true;\n    inherit lib quadletUtils;\n  };\nin\n{\n  options.virtualisation.quadlet = quadletOptions.mkTopLevelOptions { };\n\n  config =\n    let\n      allObjects = quadletOptions.getAllObjects cfg;\n      # TODO: switch to `cfg.enable == true || (cfg.enable == null && allObjects != [])`\n      # when home-manager users set `enable` explicitly.\n      enable = cfg.enable == true || cfg.enable == null;\n    in\n    mkIf enable {\n      assertions = quadletOptions.mkAssertions [ ] cfg;\n      warnings = quadletOptions.mkWarnings [ ] cfg;\n\n      virtualisation.podman.enable = true;\n      environment.etc = mergeAttrsList (\n        map (p: {\n          \"containers/systemd/${p.ref}\" = {\n            text = p._configText;\n            mode = \"0600\";\n          };\n        }) allObjects\n      );\n      # The symlinks are not necessary for the services to be honored by systemd,\n      # but necessary for NixOS activation process to pick them up for updates.\n      systemd.packages = [\n        (pkgs.linkFarm \"quadlet-service-symlinks\" (\n          map (p: {\n            name = \"etc/systemd/system/${p._serviceName}.service\";\n            path = \"/run/systemd/generator/${p._serviceName}.service\";\n          }) allObjects\n        ))\n      ];\n      # Inject X-RestartIfChanged=${hash} for NixOS to detect changes.\n      systemd.services = mergeAttrsList (\n        map (p: {\n          ${p._serviceName} = {\n            overrideStrategy = \"asDropin\";\n            unitConfig.X-QuadletNixConfigHash = builtins.hashString \"sha256\" p._configText;\n            # systemd recommends multi-user.target over default.target.\n            # https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html#default.target\n            wantedBy = if p._autoStart then [ \"multi-user.target\" ] else [ ];\n          }\n          // p._overrides;\n        }) allObjects\n      );\n\n      systemd.timers.podman-auto-update = mkIf cfg.autoUpdate.enable {\n        timerConfig.OnCalendar = [\n          \"\"\n          cfg.autoUpdate.calendar\n        ];\n        wantedBy = [ \"timers.target\" ];\n        overrideStrategy = \"asDropin\";\n      };\n    };\n}\n"
  },
  {
    "path": "options.nix",
    "content": "{\n  lib,\n  quadletUtils,\n  supportRootless,\n}:\nlet\n  mkOption =\n    {\n      property,\n      cli ? null,\n      description ? null,\n      encoders ? null,\n      ...\n    }@attrs:\n    let\n      descForDesc = if description == null then \"\" else description + \"\\n\\n\";\n      descForCli = if cli == null then \"\" else \"and command line argument `${cli}`\";\n    in\n    (lib.mkOption (\n      lib.filterAttrs (\n        name: _:\n        !(builtins.elem name [\n          \"property\"\n          \"cli\"\n          \"encoders\"\n        ])\n      ) attrs\n    ))\n    // {\n      inherit property;\n      inherit encoders;\n      description = \"${descForDesc}Maps to quadlet option `${property}`${descForCli}.\";\n    };\n\n  quadletOpts = {\n    defaultDependencies = mkOption {\n      type = lib.types.nullOr lib.types.bool;\n      default = null;\n      description = \"Add Quadlet’s default network dependencies to the unit\";\n      property = \"DefaultDependencies\";\n    };\n  };\n\n  mkCommonObjectOptions =\n    objectType:\n    {\n      quadletConfig = quadletOpts;\n\n      autoStart = lib.mkOption {\n        type = lib.types.bool;\n        default = true;\n        description = \"When enabled, this ${objectType} is automatically started on boot.\";\n      };\n\n      unitConfig = lib.mkOption {\n        type = lib.types.attrsOf quadletUtils.unitOption;\n        default = { };\n        description = \"systemd unit config passed through to [Unit] section.\";\n      };\n\n      serviceConfig = lib.mkOption {\n        type = lib.types.attrsOf quadletUtils.unitOption;\n        default = { };\n        description = \"systemd service config passed through to [Service] section.\";\n      };\n\n      rawConfig = lib.mkOption {\n        type = lib.types.nullOr lib.types.str;\n        default = null;\n        description = ''\n          Raw quadlet config text. Using this will cause all other options\n          contributing to quadlet files to be ignored. autoStart is not affected.\n        '';\n      };\n\n      _serviceName = lib.mkOption {\n        internal = true;\n        description = \"Name of the systemd service unit, without the .service suffix.\";\n      };\n\n      _configText = lib.mkOption {\n        internal = true;\n        description = \"Generated quadlet config text\";\n      };\n\n      _autoStart = lib.mkOption {\n        internal = true;\n        description = \"Whether the service is automatically started on boot.\";\n      };\n\n      _autoEscapeRequired = lib.mkOption {\n        internal = true;\n        description = ''\n          Whether `autoEscape` needs to be switched on for correct encoding.\n          This is false if already on.\n        '';\n      };\n\n      _rootless = lib.mkOption {\n        internal = true;\n        default = false;\n        description = ''\n          Whether to run rootless under system systemd.\n        '';\n      };\n\n      _overrides = lib.mkOption {\n        internal = true;\n        default = { };\n        description = ''\n          Overrides to apply on systemd.services.<name>. Not applicable to user systemd.\n        '';\n      };\n\n      ref = lib.mkOption {\n        readOnly = true;\n        description = ''\n          Reference to this ${objectType} from other quadlets.\n\n          Quadlet resolves this to object (e.g. container) names and sets up appropriate systemd dependencies.\n\n          This is recognized for most quadlet native options, but not by Podman command line.\n          Using this inside `podmanArgs` will therefore unlikely to work.\n        '';\n      };\n    }\n    // (\n      if !supportRootless then\n        { }\n      else\n        {\n          rootlessConfig = {\n            uid = lib.mkOption {\n              type = lib.types.nullOr lib.types.int;\n              default = null;\n              description = \"User ID to run rootless podman as\";\n            };\n          };\n        }\n    );\n\n  commonTopLevelOptions =\n    let\n      submoduleArgs = {\n        inherit quadletUtils;\n        quadletOptions = self;\n      };\n    in\n    {\n      builds = lib.mkOption {\n        type = lib.types.attrsOf (lib.types.submodule (import ./build.nix submoduleArgs));\n        default = { };\n        description = \"Image builds\";\n      };\n      containers = lib.mkOption {\n        type = lib.types.attrsOf (lib.types.submodule (import ./container.nix submoduleArgs));\n        default = { };\n        description = \"Containers\";\n      };\n      images = lib.mkOption {\n        type = lib.types.attrsOf (lib.types.submodule (import ./image.nix submoduleArgs));\n        default = { };\n        description = \"Image pulls\";\n      };\n      networks = lib.mkOption {\n        type = lib.types.attrsOf (lib.types.submodule (import ./network.nix submoduleArgs));\n        default = { };\n        description = \"Networks\";\n      };\n      pods = lib.mkOption {\n        type = lib.types.attrsOf (lib.types.submodule (import ./pod.nix submoduleArgs));\n        default = { };\n        description = \"Pods\";\n      };\n      volumes = lib.mkOption {\n        type = lib.types.attrsOf (lib.types.submodule (import ./volume.nix submoduleArgs));\n        default = { };\n        description = \"Volumes\";\n      };\n      enable = lib.mkOption {\n        type = lib.types.nullOr lib.types.bool;\n        default = null;\n        description = \"Enables quadlet-nix\";\n      };\n      autoEscape = lib.mkOption {\n        type = lib.types.bool;\n        default = true;\n        description = ''\n          Enables appropriate quoting / escaping.\n        '';\n      };\n      autoUpdate = {\n        enable = lib.mkOption {\n          type = lib.types.bool;\n          default = false;\n          description = \"Enables podman auto update.\";\n        };\n        calendar = lib.mkOption {\n          type = lib.types.str;\n          default = \"*-*-* 00:00:00\";\n          description = \"Schedule for podman auto update. See `systemd.time(7)` for details.\";\n        };\n      };\n    };\n\n  getAllObjects =\n    config:\n    builtins.concatLists (\n      map lib.attrValues [\n        config.builds\n        config.containers\n        config.images\n        config.networks\n        config.pods\n        config.volumes\n      ]\n    );\n\n  self = {\n    inherit mkOption quadletOpts;\n\n    mkObjectOptions =\n      objectType: extraOptions:\n      lib.attrsets.unionOfDisjoint (mkCommonObjectOptions objectType) extraOptions;\n\n    mkTopLevelOptions = extraOptions: lib.attrsets.unionOfDisjoint commonTopLevelOptions extraOptions;\n\n    inherit getAllObjects;\n\n    applyRootlessConfig =\n      prev: cfg:\n      let\n        isEnabled = supportRootless && prev.rootlessConfig.uid != null;\n        ifEnabled = lib.mkIf isEnabled;\n        userService = \"user@${toString prev.rootlessConfig.uid}.service\";\n      in\n      quadletUtils.unionOfDisjointRecursive cfg {\n        _rootless = isEnabled;\n        serviceConfig.User = ifEnabled prev.rootlessConfig.uid;\n        unitConfig.Wants = ifEnabled (lib.mkAfter [ \"linger-users.service\" ]);\n        unitConfig.Requires = ifEnabled (lib.mkAfter [ userService ]);\n        unitConfig.After = ifEnabled (\n          lib.mkAfter [\n            \"linger-users.service\"\n            userService\n          ]\n        );\n      };\n\n    mkAssertions =\n      extraAssertions: config:\n      let\n        containerPodConflicts = lib.lists.intersectLists (lib.attrNames config.containers) (\n          lib.attrNames config.pods\n        );\n        nullImageArchiveTags = lib.attrNames (\n          lib.filterAttrs (\n            _: image:\n            lib.strings.hasPrefix \"docker-archive:\" image.imageConfig.image && image.imageConfig.tag == null\n          ) config.images\n        );\n      in\n      [\n        {\n          assertion = containerPodConflicts == [ ];\n          message = ''\n            The container/pod names should be unique!\n            See: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#podname\n            The following names are not unique: ${lib.concatStringsSep \" \" containerPodConflicts}\n          '';\n        }\n        {\n          assertion = !(builtins.any (p: p._autoEscapeRequired) (getAllObjects config));\n          message = ''\n            `virtualisation.quadlet.autoEscape = true` is required because this configuration contains characters that require quoting or escaping.\n\n            If you have manual quoting or escaping in place, please undo those and enable `autoEscape`.\n          '';\n        }\n        {\n          assertion = nullImageArchiveTags == [ ];\n          message = ''\n            The following images using `docker-archive:` must have the fully qualified name (FQDN) specified as a tag: ${lib.concatStringsSep \" \" nullImageArchiveTags}\n          '';\n        }\n      ]\n      ++ extraAssertions;\n\n    mkWarnings =\n      extraWarnings: config:\n      (quadletUtils.assertionsToWarnings [\n        {\n          # TODO: drop string support and remove.\n          assertion =\n            !(builtins.any (p: builtins.isString p.networkConfig.options) (\n              builtins.attrValues config.networks\n            ));\n          message = \"String value in `virtualisation.quadlet.networks.*.networkConfig.options` is deprecated. Make it a list or attrset instead.\";\n        }\n      ])\n      ++ extraWarnings;\n  };\nin\nself\n"
  },
  {
    "path": "pod.nix",
    "content": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils) encoders pkgs;\n\n  podOpts = {\n    name = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"name\";\n      cli = \"--name\";\n      property = \"PodName\";\n    };\n\n    addHosts = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"hostname:192.168.10.11\" ];\n      cli = \"--add-host\";\n      property = \"AddHost\";\n    };\n\n    modules = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/etc/nvd.conf\" ];\n      cli = \"--module\";\n      property = \"ContainersConfModule\";\n    };\n\n    dns = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"192.168.55.1\" ];\n      cli = \"--dns\";\n      property = \"DNS\";\n    };\n\n    dnsOptions = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"ndots:1\" ];\n      cli = \"--dns-option\";\n      property = \"DNSOption\";\n    };\n\n    dnsSearches = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"foo.com\" ];\n      cli = \"--dns-search\";\n      property = \"DNSSearch\";\n    };\n\n    exitPolicy = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"stop\";\n      cli = \"--exit-policy\";\n      property = \"ExitPolicy\";\n    };\n\n    gidMaps = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"0:10000:10\" ];\n      cli = \"--gidmap\";\n      property = \"GIDMap\";\n      encoders.scalar = encoders.scalar.quotedUnescaped;\n    };\n\n    globalArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--log-level=debug\" ];\n      description = \"Additional command line arguments to insert between `podman` and `pod create`\";\n      property = \"GlobalArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    hostname = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"new-host-name\";\n      cli = \"--hostname\";\n      property = \"HostName\";\n    };\n\n    ip = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"192.5.0.1\";\n      cli = \"--ip\";\n      property = \"IP\";\n    };\n\n    ip6 = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"2001:db8::1\";\n      cli = \"--ip6\";\n      property = \"IP6\";\n    };\n\n    labels = quadletOptions.mkOption {\n      type = types.oneOf [\n        (types.listOf types.str)\n        (types.attrsOf types.str)\n      ];\n      default = { };\n      example = {\n        foo = \"bar\";\n      };\n      cli = \"--label\";\n      property = \"Label\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    networks = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"host\" ];\n      cli = \"--network\";\n      property = \"Network\";\n    };\n\n    networkAliases = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"name\" ];\n      cli = \"--network-alias\";\n      property = \"NetworkAlias\";\n    };\n\n    podmanArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--cpus=2\" ];\n      description = \"Additional command line arguments to insert after `podman pod create`\";\n      property = \"PodmanArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    publishPorts = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"50-59\" ];\n      cli = \"--publish\";\n      property = \"PublishPort\";\n    };\n\n    # ServiceName not supported as custom service names can make quadlet-nix lost.\n\n    shmSize = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"100m\";\n      cli = \"--shm-size\";\n      property = \"ShmSize\";\n    };\n\n    stopTimeout = quadletOptions.mkOption {\n      type = types.nullOr types.int;\n      default = null;\n      example = 20;\n      cli = \"--time\";\n      property = \"StopTimeout\";\n    };\n\n    subGIDMap = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"gtest\";\n      cli = \"--subgidname\";\n      property = \"SubGIDMap\";\n    };\n\n    subUIDMap = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"utest\";\n      cli = \"--subuidname\";\n      property = \"SubUIDMap\";\n    };\n\n    uidMaps = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"0:10000:10\" ];\n      cli = \"--uidmap\";\n      property = \"UIDMap\";\n      encoders.scalar = encoders.scalar.quotedUnescaped;\n    };\n\n    userns = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"keep-id:uid=200,gid=210\";\n      cli = \"--userns\";\n      property = \"UserNS\";\n    };\n\n    volumes = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/source:/dest\" ];\n      cli = \"--volume\";\n      property = \"Volume\";\n    };\n  };\nin\n{\n  options = quadletOptions.mkObjectOptions \"pod\" {\n    podConfig = podOpts;\n  };\n\n  config =\n    let\n      serviceConfigDefault = {\n        Restart = \"always\";\n        TimeoutStartSec = 900;\n      };\n      podName = if config.podConfig.name != null then config.podConfig.name else name;\n      podConfig = config.podConfig // {\n        name = podName;\n      };\n      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;\n      unitConfig = {\n        Unit = {\n          Description = \"Podman pod ${name}\";\n        }\n        // config.unitConfig;\n        Pod = quadletUtils.configToProperties podConfig podOpts;\n        Service = serviceConfigDefault // config.serviceConfig;\n      }\n      // (if quadlet == { } then { } else { Quadlet = quadlet; });\n      rootlessPidFilePath = \"/run/user/${toString config.rootlessConfig.uid}/%N.pid\";\n    in\n    lib.pipe\n      {\n        _serviceName = \"${name}-pod\";\n        _configText =\n          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;\n        _autoEscapeRequired = quadletUtils.autoEscapeRequired podConfig podOpts;\n        _autoStart = config.autoStart;\n        ref = \"${name}.pod\";\n\n        # [rootless hack]\n        # Quadlet manages pod infra container with PIDFile= at %t/%N.pid, which\n        # rootless process has no access to.\n        # Simply pointing PIDFile= at another location does not help as system\n        # systemd does not like PIDFIle= owned by unprivileged user while the\n        # process is out of the service.\n        # We therefore make it a Type=simple and wait for the pid in service.\n        podConfig.podmanArgs = lib.mkIf config._rootless (\n          lib.mkAfter [ \"--infra-conmon-pidfile=${rootlessPidFilePath}\" ]\n        );\n        serviceConfig.Type = lib.mkIf config._rootless (lib.mkDefault \"simple\");\n        serviceConfig.ExecStart = lib.mkIf config._rootless (\n          lib.mkDefault \"/bin/sh -c \\\"${quadletUtils.podmanPackage}/bin/podman pod start ${podName} && (read pid < ${rootlessPidFilePath}; exec ${pkgs.coreutils}/bin/tail -f --pid \\${pid:?})\\\"\"\n        );\n        serviceConfig.ExecStopPost = lib.mkIf config._rootless (\n          lib.mkAfter [\n            \"${pkgs.coreutils}/bin/rm -f ${rootlessPidFilePath}\"\n          ]\n        );\n\n        # some options conflict with stock quadlet and thus need force overriding\n        _overrides =\n          quadletUtils.unionOfDisjointRecursive\n            (\n              # Type= as a singular field will be overwritten by Quadlet\n              if builtins.hasAttr \"Type\" config.serviceConfig then\n                { serviceConfig.Type = config.serviceConfig.Type; }\n              else\n                { }\n            )\n            (\n              # Type=simple does not support multiple ExecStart\n              if builtins.hasAttr \"ExecStart\" config.serviceConfig then\n                {\n                  serviceConfig.ExecStart = [\n                    \"\"\n                    config.serviceConfig.ExecStart\n                  ];\n                }\n              else\n                { }\n            );\n      }\n      [\n        (quadletOptions.applyRootlessConfig config)\n      ];\n}\n"
  },
  {
    "path": "tests/README.md",
    "content": "# Tests\n\nTo run all tests:\n\n```sh\nnix flake check \\\n    --override-input nixpkgs 'github:NixOS/nixpkgs/nixos-unstable' \\\n    --override-input home-manager 'github:nix-community/home-manager/master' \\\n    --override-input test-config \"path:$(pwd)/tests/x86_64-linux\" \\\n    ./tests\n```\n\nTo run individual test (e.g. `basic-rootful`):\n\n```sh\nnix run \\\n  --override-input nixpkgs 'github:NixOS/nixpkgs/nixos-unstable' \\\n  --override-input home-manager 'github:nix-community/home-manager/master' \\\n  --override-input test-config \"path:$(pwd)/tests/x86_64-linux\" \\\n  './tests#checks.x86_64-linux.basic-rootful.driver'\n```\n"
  },
  {
    "path": "tests/aarch64-linux/flake.nix",
    "content": "{\n  outputs = _: {\n    system = \"aarch64-linux\";\n  };\n}\n"
  },
  {
    "path": "tests/basic.nix",
    "content": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.nginx = {\n          containerConfig.image = \"docker-archive:${pkgs.dockerTools.examples.nginx}\";\n          containerConfig.publishPorts = [ \"8080:80\" ];\n          serviceConfig.TimeoutStartSec = \"60\";\n        }\n        // extraConfig;\n      };\n    };\n  testScript = ''\n    machine.wait_for_unit(\"nginx.service\", user=systemd_user, timeout=30)\n\n    html = machine.succeed(\"curl http://127.0.0.1:8080\")\n    assert \"nginx\" in html.lower()\n  '';\n}\n"
  },
  {
    "path": "tests/build.nix",
    "content": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let\n          inherit (config.virtualisation.quadlet) builds;\n        in\n        {\n          builds.hello = {\n            buildConfig = {\n              file = \"${pkgs.writeText \"Containerfile\" ''\n                FROM docker-archive:${pkgs.dockerTools.examples.bash}\n                CMD bash -c 'echo \"Success\" > /output/result.txt'\n              ''}\";\n            };\n          }\n          // extraConfig;\n\n          containers.hello = {\n            containerConfig = {\n              image = builds.hello.ref;\n              volumes = [ \"/tmp:/output\" ];\n            };\n            serviceConfig = {\n              RemainAfterExit = true;\n            };\n          }\n          // extraConfig;\n        };\n    };\n\n  testScript = ''\n    machine.wait_for_unit(\"hello.service\", user=systemd_user, timeout=30)\n\n    assert machine.succeed(\"cat /tmp/result.txt\").strip() == 'Success'\n  '';\n}\n"
  },
  {
    "path": "tests/container.nix",
    "content": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.nginx = {\n          containerConfig.image = \"docker-archive:${pkgs.dockerTools.examples.nginx}\";\n          containerConfig.publishPorts = [ \"8080:80\" ];\n          serviceConfig.TimeoutStartSec = \"60\";\n          serviceConfig.Restart = \"on-failure\";\n        }\n        // extraConfig;\n      };\n    };\n  testScript = ''\n    machine.wait_for_unit(\"nginx.service\", user=systemd_user, timeout=30)\n    assert 'nginx' in machine.succeed(\"curl http://127.0.0.1:8080\").lower()\n    containers = get_containers()\n    assert containers.keys() == {\"nginx\"}\n    if podman_user is not None:\n      assert not get_containers(user=None)\n\n    machine.stop_job(\"nginx\", user=systemd_user)\n    machine.fail(\"curl http://127.0.0.1:8080\")\n    assert not get_containers()\n\n    machine.start_job(\"nginx\", user=systemd_user)\n    machine.wait_for_unit(\"nginx.service\", user=systemd_user, timeout=30)\n    assert 'nginx' in machine.succeed(\"curl http://127.0.0.1:8080\").lower()\n    containers = get_containers()\n    assert containers.keys() == {\"nginx\"}\n\n    run_as(\"podman stop nginx\", user=podman_user)\n    wait_for_unit_inactive(\"nginx.service\", user=systemd_user, timeout=10)\n  '';\n}\n"
  },
  {
    "path": "tests/escaping.nix",
    "content": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.write1 = {\n          containerConfig = {\n            image = \"docker-archive:${pkgs.dockerTools.examples.bash}\";\n            # quotedUnescaped\n            addCapabilities = [ \"SYS_NICE\" ];\n            entrypoint = \"bash\";\n            # quotedEscaped\n            environments = {\n              FOO = \"aaa bbb $ccc \\\"ddd\\n\\n \";\n              bar = \"\\\"aaa\\\"\";\n              ONLY_SPACES = \"aaa bbb\";\n            };\n            # raw\n            exec = \"-c 'echo -n \\\"$FOO\\\" > /tmp/foo.txt; echo -n \\\"$bar\\\" > /tmp/bar.txt; echo -n \\\"$ONLY_SPACES\\\" > /tmp/only_spaces.txt'\";\n            volumes = [\n              \"/tmp:/tmp\"\n            ];\n          };\n          serviceConfig = {\n            RemainAfterExit = true;\n          };\n        }\n        // extraConfig;\n        containers.write2 = {\n          containerConfig = {\n            image = \"docker-archive:${pkgs.dockerTools.examples.bash}\";\n            environments = {\n              BAZ = \"aaa\";\n            };\n            entrypoint = \"bash\";\n            # oneLine\n            exec = [\n              \"-c\"\n              \"echo $@ $0 $BAZ > /tmp/baz.txt\"\n              \"bbb\"\n              \"ccc\"\n            ];\n            volumes = [\n              \"/tmp:/tmp\"\n            ];\n          };\n          serviceConfig = {\n            RemainAfterExit = true;\n          };\n        }\n        // extraConfig;\n        containers.write3 =\n          let\n            scriptName = \"aaa bbb \\n $ccc\";\n            scriptDir = toString (pkgs.writeTextDir scriptName \"echo 8439b333258ba90e > /tmp/write3.txt\");\n          in\n          {\n            containerConfig = {\n              image = \"docker-archive:${pkgs.dockerTools.examples.bash}\";\n              entrypoint = [\n                \"bash\"\n                \"/test/${scriptName}\"\n              ];\n              volumes = [\n                \"/tmp:/tmp\"\n                \"${scriptDir}:/test/\"\n              ];\n            };\n            serviceConfig = {\n              RemainAfterExit = true;\n            };\n          }\n          // extraConfig;\n      };\n    };\n  testScript = ''\n    machine.wait_for_unit(\"write1.service\", user=systemd_user, timeout=30)\n    machine.wait_for_unit(\"write2.service\", user=systemd_user, timeout=30)\n    machine.wait_for_unit(\"write3.service\", user=systemd_user, timeout=30)\n\n    machine.wait_for_file(\"/tmp/foo.txt\", timeout=10)\n    assert machine.succeed(\"cat /tmp/foo.txt\") == 'aaa bbb $ccc \"ddd\\n\\n '\n\n    machine.wait_for_file(\"/tmp/bar.txt\", timeout=10)\n    assert machine.succeed(\"cat /tmp/bar.txt\") == '\"aaa\"'\n\n    machine.wait_for_file(\"/tmp/baz.txt\", timeout=10)\n    assert machine.succeed(\"cat /tmp/baz.txt\") == 'ccc bbb aaa\\n'\n\n    machine.wait_for_file(\"/tmp/only_spaces.txt\", timeout=10)\n    assert machine.succeed(\"cat /tmp/only_spaces.txt\") == 'aaa bbb'\n\n    machine.wait_for_file(\"/tmp/write3.txt\", timeout=10)\n    assert machine.succeed(\"cat /tmp/write3.txt\") == '8439b333258ba90e\\n'\n  '';\n\n}\n"
  },
  {
    "path": "tests/flake.nix",
    "content": "# this is a separate flake to so home-manager isn't made a compulsory input.\n\n{\n  description = \"quadlet-nix tests\";\n\n  # inputs path to be set in --override-input\n  inputs = {\n    nixpkgs.url = \"path:/dev/null\";\n\n    quadlet-nix.url = \"path:..\";\n\n    home-manager.url = \"path:/dev/null\";\n    home-manager.inputs.nixpkgs.follows = \"nixpkgs\";\n\n    test-config.url = \"path:/dev/null\";\n  };\n\n  outputs =\n    {\n      test-config,\n      nixpkgs,\n      home-manager,\n      quadlet-nix,\n      ...\n    }:\n    let\n      system = test-config.system;\n      makeTestScript =\n        {\n          podmanUser,\n          systemdUser,\n          testScript,\n        }:\n        { nodes, ... }:\n        ''\n          import json\n          from typing import Any, Optional\n\n          podman_user = ${podmanUser}\n          systemd_user = ${systemdUser}\n\n          def run_as(command: str, *, user: Optional[str]) -> str:\n            if user is not None:\n              command = f\"sudo -u {user} -- {command}\"\n            return machine.succeed(command)\n\n          def wait_for_unit_inactive(unit: str, *, user: Optional[str], timeout: int) -> None:\n            def check_active(_last_try: bool) -> bool:\n              state = machine.get_unit_property(unit, \"ActiveState\", user)\n\n              if state == \"inactive\":\n                return True\n              if state in (\"active\", \"deactivating\"):\n                return False\n              assert False, f\"{unit} reached state {state}\"\n\n            with machine.nested(\n              f\"waiting for unit {unit}\"\n              + (f\" with user {user}\" if user is not None else \"\")\n              + \" to be inactive\"\n            ):\n              retry(check_active, timeout)\n\n          def get_containers(*, user: Optional[str] = podman_user) -> dict[str, dict[str, Any]]:\n            containers = json.loads(run_as(\"podman ps --format=json\", user=user))\n            return {name: container for container in containers for name in container[\"Names\"]}\n\n          def get_networks(*, user: Optional[str] = podman_user) -> dict[str, dict[str, Any]]:\n            networks = json.loads(run_as(\"podman network ls --format=json\", user=user))\n            return {network[\"name\"]: network for network in networks}\n\n          def get_pods(*, user: Optional[str] = podman_user) -> dict[str, dict[str, Any]]:\n            pods = json.loads(run_as(\"podman pod ls --format=json\", user=user))\n            return {pod[\"Name\"]: pod for pod in pods}\n\n          def switch_to_specialisation(specialisation: str) -> str:\n            return machine.succeed(f\"${nodes.machine.system.build.toplevel}/specialisation/{specialisation}/bin/switch-to-configuration test\")\n\n          machine.wait_for_unit(\"default.target\", user=None)\n          if systemd_user is not None:\n            machine.wait_for_unit(\"default.target\", user=systemd_user)\n\n          ${testScript}\n        '';\n\n      makeTestCase = template: args: if builtins.isFunction template then template args else template;\n\n      runRootfulTest =\n        {\n          name,\n          template,\n          pkgs,\n        }:\n        let\n          testCase = makeTestCase template {\n            extraConfig = { };\n            isHomeManager = false;\n            home = \"/root\";\n          };\n          testConfig = testCase.testConfig;\n          testScript = testCase.testScript;\n          specialisation = testCase.specialisation or (_: { });\n        in\n        {\n          name = name + \"-rootful\";\n          testScript = makeTestScript {\n            systemdUser = \"None\";\n            podmanUser = \"None\";\n            inherit testScript;\n          };\n\n          node.specialArgs.testType = \"rootful\";\n          nodes.machine =\n            { pkgs, ... }@attrs:\n            {\n              imports = [\n                quadlet-nix.nixosModules.quadlet\n                testConfig\n              ];\n              environment.systemPackages = [ pkgs.curl ];\n              specialisation = builtins.mapAttrs (name: value: { configuration = value; }) (specialisation attrs);\n            };\n        };\n\n      runRootlessTest =\n        {\n          name,\n          template,\n          pkgs,\n        }:\n        let\n          testCase = makeTestCase template {\n            extraConfig = {\n              rootlessConfig.uid = 1357;\n            };\n            isHomeManager = false;\n            home = \"/home/alice\";\n          };\n          testConfig = testCase.testConfig;\n          testScript = testCase.testScript;\n          specialisation = testCase.specialisation or (_: { });\n        in\n        {\n          name = name + \"-rootless\";\n          testScript = makeTestScript {\n            systemdUser = \"None\";\n            podmanUser = \"\\\"alice\\\"\";\n            inherit testScript;\n          };\n\n          node.specialArgs.testType = \"rootless\";\n          nodes.machine =\n            { pkgs, ... }@attrs:\n            {\n              imports = [\n                quadlet-nix.nixosModules.quadlet\n                testConfig\n              ];\n              environment.systemPackages = [ pkgs.curl ];\n              specialisation = builtins.mapAttrs (name: value: { configuration = value; }) (specialisation attrs);\n\n              users.users.alice = {\n                uid = 1357;\n                group = \"alice\";\n                linger = true;\n                autoSubUidGidRange = true;\n                isNormalUser = true;\n              };\n              users.groups.alice = {\n                gid = 2468;\n              };\n            };\n        };\n\n      runHomeManagerTest =\n        {\n          name,\n          template,\n          pkgs,\n        }:\n        let\n          testCase = makeTestCase template {\n            extraConfig = { };\n            isHomeManager = true;\n            home = \"/home/alice\";\n          };\n          testConfig = testCase.testConfig;\n          testScript = testCase.testScript;\n          specialisation = testCase.specialisation or (_: { });\n        in\n        {\n          name = name + \"-home-manager\";\n          testScript = makeTestScript {\n            systemdUser = \"\\\"alice\\\"\";\n            podmanUser = \"\\\"alice\\\"\";\n            inherit testScript;\n          };\n\n          nodes.machine =\n            { lib, pkgs, ... }@attrs:\n            {\n              imports = [\n                quadlet-nix.nixosModules.quadlet\n                home-manager.nixosModules.home-manager\n              ];\n              virtualisation.quadlet.enable = true;\n              environment.systemPackages = [ pkgs.curl ];\n\n              # brings up network-online.target\n              systemd.targets.test-network = {\n                wants = [ \"network-online.target\" ];\n                wantedBy = [ \"multi-user.target\" ];\n              };\n\n              users.users.alice = {\n                group = \"alice\";\n                linger = true;\n                autoSubUidGidRange = true;\n                isNormalUser = true;\n              };\n              users.groups.alice = { };\n\n              home-manager.extraSpecialArgs.testType = \"home-manager\";\n              home-manager.users.alice = lib.mkDefault (\n                { config, ... }:\n                {\n                  imports = [\n                    quadlet-nix.homeManagerModules.quadlet\n                    testConfig\n                  ];\n                  home.stateVersion = config.home.version.release;\n                }\n              );\n\n              specialisation = builtins.mapAttrs (name: value: {\n                configuration = {\n                  home-manager.users.alice = (\n                    { config, ... }:\n                    {\n                      imports = [\n                        quadlet-nix.homeManagerModules.quadlet\n                        testConfig\n                        value\n                      ];\n                      home.stateVersion = config.home.version.release;\n                    }\n                  );\n                };\n              }) (specialisation attrs);\n            };\n        };\n\n      genTest =\n        pkgs: runTest: template:\n        let\n          name = pkgs.lib.removeSuffix \".nix\" (builtins.baseNameOf template);\n          test = pkgs.testers.runNixOSTest (runTest {\n            template = import template;\n            inherit name pkgs;\n          });\n        in\n        {\n          name = test.config.name;\n          value = test;\n        };\n\n    in\n    {\n      checks =\n        let\n          pkgs = import nixpkgs { inherit system; };\n          lib = pkgs.lib;\n          tests = builtins.listToAttrs (\n            map ({ runner, template }: genTest pkgs runner template) (\n              lib.cartesianProduct {\n                template = [\n                  ./basic.nix\n                  ./build.nix\n                  ./container.nix\n                  ./image.nix\n                  ./network.nix\n                  ./pod.nix\n                  ./volume.nix\n                  ./switch.nix\n                  ./raw.nix\n                  ./health.nix\n                  ./escaping.nix\n                  ./overriding.nix\n                ];\n                runner = [\n                  runRootfulTest\n                  runRootlessTest\n                  runHomeManagerTest\n                ];\n              }\n            )\n          );\n        in\n        {\n          \"${system}\" = tests;\n        };\n    };\n}\n"
  },
  {
    "path": "tests/health.nix",
    "content": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.good = {\n          containerConfig = {\n            image = \"docker-archive:${pkgs.dockerTools.examples.redis}\";\n            healthCmd = \"redis-cli ping || exit 1\";\n            healthRetries = 1;\n          };\n          serviceConfig.TimeoutStartSec = 60;\n        }\n        // extraConfig;\n        containers.bad = {\n          containerConfig = {\n            image = \"docker-archive:${pkgs.dockerTools.examples.nginx}\";\n            healthCmd = \"exit 1\";\n            healthRetries = 1;\n          };\n          serviceConfig.TimeoutStartSec = 60;\n        }\n        // extraConfig;\n      };\n    };\n\n  testScript = ''\n    machine.wait_for_unit(\"good.service\", user=systemd_user, timeout=30)\n    machine.wait_for_unit(\"bad.service\", user=systemd_user, timeout=30)\n    machine.sleep(2)  # wait for health command cycles\n\n    containers = get_containers()\n    assert containers.keys() == {\"good\", \"bad\"}\n    assert \"(healthy)\" in containers[\"good\"][\"Status\"]\n    assert \"(unhealthy)\" in containers[\"bad\"][\"Status\"]\n  '';\n}\n"
  },
  {
    "path": "tests/image.nix",
    "content": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let\n          inherit (config.virtualisation.quadlet) images;\n        in\n        {\n          images.hello =\n            let\n              test-bash-image = pkgs.dockerTools.buildImage {\n                name = \"whatever.com/test-bash\";\n                tag = \"latest\";\n                fromImage = pkgs.dockerTools.examples.bash;\n              };\n            in\n            {\n              imageConfig = {\n                image = \"docker-archive:${test-bash-image}\";\n                tag = \"whatever.com/test-bash:latest\";\n              };\n            }\n            // extraConfig;\n\n          containers.hello = {\n            containerConfig = {\n              image = images.hello.ref;\n              volumes = [ \"/tmp:/output\" ];\n              entrypoint = \"bash\";\n              exec = [\n                \"-c\"\n                \"echo \\\"Success\\\" > /output/result.txt\"\n              ];\n            };\n            serviceConfig = {\n              RemainAfterExit = true;\n            };\n          }\n          // extraConfig;\n        };\n    };\n\n  testScript = ''\n    machine.wait_for_unit(\"hello.service\", user=systemd_user, timeout=30)\n\n    assert machine.succeed(\"cat /tmp/result.txt\").strip() == 'Success'\n  '';\n}\n"
  },
  {
    "path": "tests/network.nix",
    "content": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let\n          inherit (config.virtualisation.quadlet) networks;\n        in\n        {\n          containers.nginx = {\n            containerConfig = {\n              image = \"docker-archive:${pkgs.dockerTools.examples.nginx}\";\n              publishPorts = [ \"8080:80\" ];\n              networks = [\n                networks.foo.ref\n                networks.bar.ref\n              ];\n            };\n          }\n          // extraConfig;\n          networks.foo = {\n            networkConfig.options.isolate = \"true\";\n          }\n          // extraConfig;\n          networks.bar = { } // extraConfig;\n        };\n    };\n  testScript = ''\n    machine.wait_for_unit(\"nginx.service\", user=systemd_user, timeout=30)\n    assert \"nginx\" in machine.succeed(\"curl http://127.0.0.1:8080\").lower()\n\n    containers = get_containers()\n    assert containers.keys() == {\"nginx\"}\n    networks = get_networks()\n    assert networks.keys() == {\"foo\", \"bar\", \"podman\"}\n    assert set(containers[\"nginx\"][\"Networks\"]) == {\"foo\", \"bar\"}\n    assert networks[\"foo\"][\"options\"][\"isolate\"] == \"true\"\n    if podman_user is not None:\n      assert not get_containers(user=None)\n      assert get_networks(user=None).keys() == {\"podman\"}\n\n    machine.stop_job(\"foo-network\", user=systemd_user)\n    machine.fail(\"curl http://127.0.0.1:8080\")\n    assert not get_containers()\n    networks = get_networks()\n    assert networks.keys() == {\"bar\", \"podman\"}\n\n    machine.start_job(\"nginx\", user=systemd_user)\n    assert \"nginx\" in machine.succeed(\"curl http://127.0.0.1:8080\").lower()\n    containers = get_containers()\n    assert containers.keys() == {\"nginx\"}\n    networks = get_networks()\n    assert networks.keys() == {\"foo\", \"bar\", \"podman\"}\n  '';\n}\n"
  },
  {
    "path": "tests/overriding.nix",
    "content": "{ extraConfig, isHomeManager, ... }:\n{\n  testConfig =\n    { pkgs, lib, ... }:\n    let\n      execStartPre = \"${pkgs.bash}/bin/bash -c 'echo ef1e835e0ae5 > /tmp/foo.txt'\";\n      nixosOverrides = {\n        systemd.services.nginx.serviceConfig.ExecStartPre = execStartPre;\n      };\n      homeManagerOverrides = {\n        systemd.user.services.nginx.Service.ExecStartPre = execStartPre;\n      };\n      overrides = if isHomeManager then homeManagerOverrides else nixosOverrides;\n    in\n    {\n      virtualisation.quadlet = {\n        containers.nginx = {\n          containerConfig.image = \"docker-archive:${pkgs.dockerTools.examples.nginx}\";\n          containerConfig.publishPorts = [ \"8080:80\" ];\n          serviceConfig.TimeoutStartSec = \"60\";\n        }\n        // extraConfig;\n      };\n    }\n    // overrides;\n  testScript = ''\n    machine.wait_for_unit(\"nginx.service\", user=systemd_user, timeout=30)\n\n    html = machine.succeed(\"curl http://127.0.0.1:8080\")\n    assert \"nginx\" in html.lower()\n    assert machine.succeed(\"cat /tmp/foo.txt\").strip() == \"ef1e835e0ae5\"\n  '';\n}\n"
  },
  {
    "path": "tests/pod.nix",
    "content": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let\n          inherit (config.virtualisation.quadlet) pods;\n        in\n        {\n          containers.nginx = {\n            containerConfig = {\n              image = \"docker-archive:${pkgs.dockerTools.examples.nginx}\";\n              pod = pods.foo.ref;\n            };\n            serviceConfig.Restart = \"on-failure\";\n          }\n          // extraConfig;\n          containers.redis = {\n            containerConfig = {\n              image = \"docker-archive:${pkgs.dockerTools.examples.redis}\";\n              pod = pods.foo.ref;\n            };\n            serviceConfig.Restart = \"on-failure\";\n          }\n          // extraConfig;\n          pods.foo = {\n            podConfig = {\n              publishPorts = [ \"8080:80\" ];\n            };\n          }\n          // extraConfig;\n        };\n    };\n  testScript = ''\n    machine.wait_for_unit(\"nginx.service\", user=systemd_user, timeout=30)\n    machine.wait_for_unit(\"redis.service\", user=systemd_user, timeout=30)\n    assert \"nginx\" in machine.succeed(\"curl http://127.0.0.1:8080\").lower()\n\n    containers = get_containers()\n    assert len(containers) == 3\n    assert containers.keys() >= {\"nginx\", \"redis\"}\n    pods = get_pods()\n    assert pods.keys() == {\"foo\"}\n    assert set(c[\"Id\"] for c in pods[\"foo\"][\"Containers\"]) == {c[\"Id\"] for c in containers.values()}\n    if podman_user is not None:\n      assert not get_containers(user=None)\n      assert not get_pods(user=None)\n\n    machine.stop_job(\"foo-pod\", user=systemd_user)\n    machine.fail(\"curl http://127.0.0.1:8080\")\n    assert not get_containers()\n    assert not get_pods()\n\n    machine.start_job(\"nginx\", user=systemd_user)\n    assert \"nginx\" in machine.succeed(\"curl http://127.0.0.1:8080\").lower()\n    machine.wait_for_unit(\"foo-pod.service\", user=systemd_user, timeout=30)\n    machine.wait_for_unit(\"nginx.service\", user=systemd_user, timeout=30)\n    machine.wait_for_unit(\"redis.service\", user=systemd_user, timeout=30)\n    containers = get_containers()\n    assert len(containers) == 3\n    assert containers.keys() >= {\"nginx\", \"redis\"}\n    pods = get_pods()\n    assert pods.keys() == {\"foo\"}\n\n    run_as(\"podman pod stop foo\", user=podman_user)\n    wait_for_unit_inactive(\"foo-pod.service\", user=systemd_user, timeout=10)\n    wait_for_unit_inactive(\"nginx.service\", user=systemd_user, timeout=10)\n    wait_for_unit_inactive(\"redis.service\", user=systemd_user, timeout=10)\n  '';\n}\n"
  },
  {
    "path": "tests/raw.nix",
    "content": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.nginx = {\n          rawConfig = ''\n            [Container]\n            Image=docker-archive:${pkgs.dockerTools.examples.nginx}\n            PublishPort=8080:80\n            [Service]\n            TimeoutStartSec=60\n          '';\n        }\n        // extraConfig;\n      };\n    };\n  testScript = ''\n    machine.wait_for_unit(\"nginx.service\", user=systemd_user, timeout=30)\n\n    html = machine.succeed(\"curl http://127.0.0.1:8080\")\n    assert \"nginx\" in html.lower()\n  '';\n}\n"
  },
  {
    "path": "tests/switch.nix",
    "content": "{ extraConfig, ... }:\nlet\n  makeQuadletConfig = pkgs: networks: {\n    containers.nginx = {\n      containerConfig = {\n        image = \"docker-archive:${pkgs.dockerTools.examples.nginx}\";\n        publishPorts = [ \"8080:80\" ];\n        networks = map (x: \"${x}.network\") networks;\n      };\n    }\n    // extraConfig;\n    networks = builtins.listToAttrs (\n      map (x: {\n        name = x;\n        value = {\n          networkConfig.name = x;\n        }\n        // extraConfig;\n      }) networks\n    );\n  };\nin\n{\n  testConfig =\n    { lib, pkgs, ... }:\n    {\n      virtualisation.quadlet = lib.mkDefault (makeQuadletConfig pkgs [ \"foo\" ]);\n    };\n\n  specialisation =\n    { pkgs, ... }:\n    {\n      step1Add.virtualisation.quadlet = makeQuadletConfig pkgs [\n        \"foo\"\n        \"bar\"\n      ];\n      step2Remove.virtualisation.quadlet = makeQuadletConfig pkgs [ \"bar\" ];\n      step3AddRemove.virtualisation.quadlet = makeQuadletConfig pkgs [ \"baz\" ];\n    };\n\n  testScript = ''\n    def check(expected_networks: set[str]) -> None:\n      assert \"nginx\" in machine.succeed(\"curl http://127.0.0.1:8080\").lower()\n      containers = get_containers()\n      assert containers.keys() == {\"nginx\"}\n      networks = get_networks()\n      assert networks.keys() == expected_networks | {\"podman\"}\n      assert set(containers[\"nginx\"][\"Networks\"]) == expected_networks\n\n    check({\"foo\"})\n\n    switch_to_specialisation(\"step1Add\")\n    check({\"foo\", \"bar\"})\n\n    switch_to_specialisation(\"step2Remove\")\n    check({\"bar\"})\n\n    switch_to_specialisation(\"step3AddRemove\")\n    check({\"baz\"})\n  '';\n}\n"
  },
  {
    "path": "tests/volume.nix",
    "content": "{ extraConfig, home, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let\n          inherit (config.virtualisation.quadlet) volumes;\n        in\n        {\n          containers.write = {\n            containerConfig = {\n              image = \"docker-archive:${pkgs.dockerTools.examples.bash}\";\n              entrypoint = \"bash\";\n              exec = \"-c 'echo 262c837a9160 > /mnt/foo/bar.txt'\";\n              volumes = [\n                \"${volumes.foo.ref}:/mnt/foo\"\n              ];\n            };\n            serviceConfig = {\n              RemainAfterExit = true;\n            };\n          }\n          // extraConfig;\n          volumes.foo = {\n            volumeConfig = {\n              type = \"bind\";\n              device = home;\n            };\n          }\n          // extraConfig;\n        };\n    };\n  testScript = ''\n    machine.wait_for_unit(\"write.service\", user=systemd_user, timeout=30)\n\n    path = \"${home}/bar.txt\"\n    machine.wait_for_file(path, timeout=10)\n    assert machine.succeed(f\"cat {path}\").strip() == \"262c837a9160\"\n  '';\n}\n"
  },
  {
    "path": "tests/x86_64-linux/flake.nix",
    "content": "{\n  outputs = _: {\n    system = \"x86_64-linux\";\n  };\n}\n"
  },
  {
    "path": "utils.nix",
    "content": "{\n  pkgs,\n  lib,\n  systemdUtils,\n  podmanPackage,\n  autoEscape,\n}:\n\nlet\n  # encodes value based on how podman parses them\n  # see: https://github.com/containers/podman/blob/main/pkg/systemd/quadlet/quadlet.go\n  encoders =\n    let\n      # wraps a scalar encoder so it tries not escaping if possible\n      makePassive =\n        f: x:\n        let\n          raw = systemdUtils.lib.toOption x;\n          encoded = f x;\n          canSkip = encoded == raw || (builtins.match \".*[ \\t\\n\\r].*\" raw == null && \"\\\"${raw}\\\"\" == encoded);\n        in\n        if canSkip then raw else encoded;\n\n    in\n    {\n      scalar.legacy = systemdUtils.lib.toOption;\n\n      # Lookup, LookupAll, LookupLast, LookupAllRaw, LookupLastRaw\n      scalar.raw =\n        x:\n        let\n          ret = systemdUtils.lib.toOption x;\n        in\n        if builtins.match \".*[\\r\\n].*\" ret == null then\n          ret\n        else\n          throw \"quadlet-nix internal error: unsafe value for scalar.raw option: ${ret}\";\n\n      # LookupAllArgs, LookupAllKeyVal\n      # same as systemdUtils.lib.serviceToUnit\n      scalar.quotedEscaped = makePassive builtins.toJSON;\n\n      # LookupAllStrv\n      scalar.quotedUnescaped = makePassive (\n        x:\n        let\n          escaped = builtins.toJSON x;\n          unescaped = \"\\\"${systemdUtils.lib.toOption x}\\\"\";\n        in\n        if escaped == unescaped then\n          unescaped\n        else\n          throw \"quadlet-nix internal error: unsafe value for scalar.quotedUnescaped option: ${escaped}\"\n      );\n\n      list.default = fScalar: x: map fScalar x;\n\n      # LookupLastArgs\n      list.oneLine = fScalar: x: builtins.concatStringsSep \" \" (map fScalar x);\n\n      list.json = builtins.toJSON;\n\n      attrs.default = fScalar: x: lib.mapAttrsToList (k: v: \"${k}=${fScalar v}\") x;\n    };\n\n  encode =\n    encoders: value:\n    if builtins.isString value || builtins.isInt value || builtins.isBool value then\n      encoders.scalar value\n    else if builtins.isList value then\n      encoders.list value\n    else if builtins.isAttrs value then\n      encoders.attrs value\n    else\n      throw \"quadlet-nix internal error: unexpected type for encoder\";\n\n  finalizeEncoders =\n    autoEscape: optionEncoders:\n    let\n      effEncoders = if autoEscape then optionEncoders else { scalar = encoders.scalar.legacy; };\n      scalar = effEncoders.scalar or encoders.scalar.raw;\n      list = effEncoders.list or (encoders.list.default scalar);\n      attrs = effEncoders.attrs or (encoders.attrs.default scalar);\n    in\n    {\n      inherit scalar list attrs;\n    };\n\n  configToProperties =\n    autoEscape: config: options:\n    let\n      nonNullConfig = lib.filterAttrs (_: value: value != null) config;\n      encodeEntry =\n        name: value:\n        lib.nameValuePair options.${name}.property (\n          encode (finalizeEncoders autoEscape options.${name}.encoders) value\n        );\n    in\n    lib.mapAttrs' encodeEntry nonNullConfig;\n\n  unionOfDisjointRecursive =\n    x: y:\n    let\n      intersectionY = builtins.intersectAttrs x y;\n      intersectionX = builtins.mapAttrs (n: _: x.${n}) intersectionY;\n      mergeFn =\n        name: values:\n        let\n          x = builtins.elemAt values 0;\n          y = builtins.elemAt values 1;\n        in\n        if builtins.isAttrs x && builtins.isAttrs y then\n          unionOfDisjointRecursive x y\n        else if x == y then\n          x\n        else\n          throw \"unionOfDisjointRecursive: collision on ${name}\";\n      merged = builtins.zipAttrsWith mergeFn [\n        intersectionX\n        intersectionY\n      ];\n    in\n    x // y // merged;\n\nin\n{\n  configToProperties = config: options: configToProperties autoEscape config options;\n  autoEscapeRequired =\n    config: options:\n    configToProperties autoEscape config options != configToProperties true config options;\n\n  unitConfigToText =\n    unitConfig:\n    builtins.concatStringsSep \"\\n\\n\" (\n      lib.mapAttrsToList (\n        name: section: \"[${name}]\\n${systemdUtils.lib.attrsToSection section}\"\n      ) unitConfig\n    );\n\n  assertionsToWarnings =\n    asssertions: map (x: x.message) (builtins.filter (x: !x.assertion) asssertions);\n\n  inherit (systemdUtils.unitOptions) unitOption;\n  inherit\n    pkgs\n    podmanPackage\n    encoders\n    unionOfDisjointRecursive\n    ;\n}\n"
  },
  {
    "path": "volume.nix",
    "content": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils) encoders;\n\n  volumeOpts = {\n    name = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"foo\";\n      description = \"Volume name as in `podman volume create foo`\";\n      property = \"VolumeName\";\n    };\n\n    copy = quadletOptions.mkOption {\n      type = types.nullOr types.bool;\n      default = null;\n      cli = \"--opt copy\";\n      property = \"Copy\";\n    };\n\n    device = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"tmpfs\";\n      cli = \"--opt device=...\";\n      property = \"Device\";\n    };\n\n    driver = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"image\";\n      cli = \"--driver\";\n      property = \"Driver\";\n    };\n\n    globalArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--log-level=debug\" ];\n      description = \"Additional command line arguments to insert between `podman` and `volume create`\";\n      property = \"GlobalArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    group = quadletOptions.mkOption {\n      type = types.nullOr (\n        types.oneOf [\n          types.int\n          types.str\n        ]\n      );\n      default = null;\n      example = 192;\n      cli = \"--opt group=...\";\n      property = \"Group\";\n    };\n\n    image = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      example = \"quay.io/centos/centos:latest\";\n      cli = \"--opt image=...\";\n      property = \"Image\";\n    };\n\n    labels = quadletOptions.mkOption {\n      type = types.oneOf [\n        (types.listOf types.str)\n        (types.attrsOf types.str)\n      ];\n      default = { };\n      example = {\n        foo = \"bar\";\n      };\n      cli = \"--label\";\n      property = \"Label\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    modules = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"/etc/nvd.conf\" ];\n      cli = \"--module\";\n      property = \"ContainersConfModule\";\n    };\n\n    options = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      cli = \"--opt o=...\";\n      property = \"Options\";\n    };\n\n    podmanArgs = quadletOptions.mkOption {\n      type = types.listOf types.str;\n      default = [ ];\n      example = [ \"--driver=image\" ];\n      description = \"Additional command line arguments to insert after `podman volume create`\";\n      property = \"PodmanArgs\";\n      encoders.scalar = encoders.scalar.quotedEscaped;\n    };\n\n    type = quadletOptions.mkOption {\n      type = types.nullOr types.str;\n      default = null;\n      cli = \"--opt type=...\";\n      description = \"Filesystem type of `device`\";\n      property = \"Type\";\n    };\n\n    user = quadletOptions.mkOption {\n      type = types.nullOr (\n        types.oneOf [\n          types.int\n          types.str\n        ]\n      );\n      default = null;\n      example = 123;\n      cli = \"--opt uid=...\";\n      property = \"User\";\n    };\n  };\nin\n{\n  options = quadletOptions.mkObjectOptions \"volume\" {\n    volumeConfig = volumeOpts;\n  };\n\n  config =\n    let\n      volumeName = if config.volumeConfig.name != null then config.volumeConfig.name else name;\n      volumeConfig = config.volumeConfig // {\n        name = volumeName;\n      };\n      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;\n      unitConfig = {\n        Unit = {\n          Description = \"Podman volume ${name}\";\n        }\n        // config.unitConfig;\n        Volume = quadletUtils.configToProperties volumeConfig volumeOpts;\n        Service = config.serviceConfig;\n      }\n      // (if quadlet == { } then { } else { Quadlet = quadlet; });\n    in\n    lib.pipe\n      {\n        _serviceName = \"${name}-volume\";\n        _configText =\n          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;\n        _autoStart = config.autoStart;\n        _autoEscapeRequired = quadletUtils.autoEscapeRequired volumeConfig volumeOpts;\n        ref = \"${name}.volume\";\n      }\n      [\n        (quadletOptions.applyRootlessConfig config)\n      ];\n}\n"
  }
]