[
  {
    "path": "CHANGELOG.md",
    "content": "# 0.14 (2025-12-19)\n- Enhancements\n  - Support NixOS unstable\n# 0.13 (2024-12-12)\n- Enhancements\n  - Support NixOS option `containers.<name>.autoStart`.\n    This allows creating containers that are automatically started on system boot.\n  - Support NixOS 24.11, NixOS unstable\n# 0.12 (2023-06-15)\n- Enhancements\n  - Flake: Allow accessing built container configs.\n    Example:\n    ```bash\n    nix eval ./examples/flake --apply 'sys: sys.containers.demo.config.networking.hostName'\n    ```\n  - Add compatibility with NixOS 23.05, NixOS unstable\n- Fixes\n  - Fix `extra-container destroy --all` when more than one container is installed\n# 0.11 (2022-10-22)\n- Enhancements\n  - Support building containers via Flakes (see [examples/flake](./examples/flake)).\n  - Support destroying containers from container definitions:\\\n    If you have installed containers with command `extra-container create ./mycontainers.nix`,\n    you can now destroy these containers with the analogous command\n    `extra-container destroy ./mycontainers.nix`.\n- Fixes\n  - Fix incomplete container state directory path in `help()` message\n# 0.10 (2022-06-26)\n- Enhancements\n  - Support NixOS 22.05\n# 0.9 (2022-04-11)\n- Enhancements\n  - Support NixOS unstable\n- Fixes\n  - Fix command `destroy` for nested declarative containers\n# 0.8 (2021-09-30)\n- Enhancements\n  - Support NixOS unstable\n- Fixes\n  - Fix flake\n# 0.7 (2021-08-03)\n- Enhancements\n  - Support NixOS 21.05 and unstable\n  - Add basic [Nix flake](https://nixos.wiki/wiki/Flakes) support\n    for installing and developing.\\\n    `extra-container` itself still uses `nix-build` internally.\n# 0.6 (2021-02-05)\n- Fixes\n  - Add compatibility with current NixOS unstable.\n  - `extra.exposeLocalhost`: don't fail when iptables lock can't be obtained immediately.\n  - Fix `PATH` not being preserved in container shells.\n# 0.5 (2020-11-01)\n- Enhancements. (See the [README](README.md) for full documentation.)\n  - Add generic support for systemd-based Linux distros.\n  - Add command `shell`.\n  - Add extra container options:\\\n    `extra.enableWAN`\\\n    `extra.exposeLocalhost`\\\n    `extra.firewallAllowHost`\\\n    See [eval-config.nix](eval-config.nix) for descriptions.\n  - Add option `--ssh`.\n  - Add option `--expr|-E`.\n  - Append `pwd` to `NIX_PATH` to allow accessing the working dir in non-file configs.\n  - Support nixpkgs versions > 20.03.\n  - Automatically run as root via `sudo`.\n- Fixes\n  - Don't copy local nixpkgs sources provided via `--nixpkgs` to the nix store.\n\n# 0.4 (2020-09-25)\n- Enhancements\n  - Significantly speed up container evaluation.\\\n    Use a reduced module set for evaluating the container host system derivation.\n  - Speed up container destruction.\\\n    Kill the container process instead of a clean shutdown.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 The extra-container developers\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."
  },
  {
    "path": "Makefile",
    "content": "all: test\n\ntest: runTests checkFlake\n\nrunTests:\n\tnix shell -c sudo ./run-tests-in-container.sh\n\ncheckFlake:\n\tnix flake check\n\ndoc:\n\tnix run .#updateReadme\n\n.PHONY: runTests checkFlake test doc\n"
  },
  {
    "path": "README.md",
    "content": "# extra-container\n\nManage declarative NixOS containers like imperative containers, without system\nrebuilds.\n\nEach declarative container adds a full system module evaluation to every NixOS rebuild,\nwhich can be prohibitively slow for systems with many containers or when experimenting\nwith single containers.\n\nOn the other hand, the faster imperative containers lack the full range of options of declarative containers.\nThis tool brings you the best of both worlds.\n\n## Example\n\n```bash\n\nsudo extra-container create --start <<'EOF'\n{\n  containers.demo = {\n    privateNetwork = true;\n    hostAddress = \"10.250.0.1\";\n    localAddress = \"10.250.0.2\";\n\n    config = { pkgs, ... }: {\n      systemd.services.hello = {\n        wantedBy = [ \"multi-user.target\" ];\n        script = ''\n          while true; do\n            echo hello | ${pkgs.netcat}/bin/nc -lN 50\n          done\n        '';\n      };\n      networking.firewall.allowedTCPPorts = [ 50 ];\n    };\n  };\n}\nEOF\n\ncurl --http0.9 10.250.0.2:50 # Returns 'hello' from the container\n\n# Now change the 'hello' string in the container definition to something\n# else and re-run the `extra-container create --start` command.\n# The container is automatically updated via NixOS' `switch-to-configuration`.\n\n# The container is a regular container that can be controlled\n# with nixos-container\nnixos-container status demo\n\n# Remove the container\nsudo extra-container destroy demo\n```\n\n#### Run command in a container and exit\n\n```bash\ncfg='{\n  containers.demo.config = {\n    networking.hostName = \"hello\";\n  };\n}'\nextra-container shell -E \"$cfg\" --run c hostname # => hello\n```\n\n## Changelog\n\n [`CHANGELOG.md`](CHANGELOG.md)\n\n## Install\n\n### On NixOS\n\n##### NixOS ≥ 21.11\n\nAdd `programs.extra-container.enable = true` to your configuration.\n\n##### Any NixOS with `flake` support\nImport `extra-container.nixosModules.default` in your configuration.\n\n### On other systemd-based Linux distros\n\n```bash\ngit clone https://github.com/erikarvstedt/extra-container\n# Calls sudo during install\nextra-container/util/install.sh\n```\n[`install.sh`](util/install.sh) installs `extra-container` to the root nix user profile\nand edits `/etc/sudoers` to enable running `extra-container` with sudo.\n\n## More features\n\n### Shell\n\nCommand `shell` starts a container shell session.\nThe shell provides helper functions for interacting with the container. The container is destroyed when exiting the shell.\n\nThis config uses `extra` options that are [explained below](#private-network-helper).\n```bash\nread -d '' src <<'EOF' || :\n{\n  containers.demo = {\n    extra.addressPrefix = \"10.250.0\"; # Sets up a private network.\n    extra.enableWAN = true;\n  };\n}\nEOF\n# Provide container config via `-E` instead of stdin because the shell's stdin\n# should be connected to the terminal.\nextra-container shell -E \"$src\" --ssh\n```\n\n`extra-container` automatically runs itself via `sudo` when called as a non-root user.\n\nAn example shell session\n```\n...\nStarting shell.\nEnter \"h\" for documentation.\n\n$ h\nContainer address: 10.250.0.2 ($ip)\nContainer filesystem: /var/lib/nixos-containers/demo\n\nRun \"c COMMAND\" to execute a command in the container\nRun \"c\" to start a shell session inside the container\nRun \"cssh\" for SSH\n\n# Container internet access, enabled via option `extra.enableWAN`\n$ c curl example.com\n<!doctype html>\n<html>\n...\n\n# Connect with SSH, enabled by `--ssh`\n$ cssh hostname\ndemo\n```\n\n#### Run commands\n\nRun a command in a shell session and exit. The container is destroyed afterwards.\n```bash\ncfg='{ containers.demo = {}; }'\nextra-container shell -E \"$cfg\" --run c hostname\n# => demo\n```\n\nStart a shell inside the container.\n```bash\ncfg='{ containers.demo = {}; }'\nextra-container shell -E \"$cfg\" --run c\n```\n\n#### Repeated calls to `extra-container shell`\nWhen `extra-container shell` detects that it is already running in a container shell\nsession, it updates the running container instead of destroying and restarting it and\nstarting a new shell.\\\nTo prevent `sudo` from clearing the environment variables that are needed for shell\ndetection, call `extra-container` without `sudo`.\\\n`extra-container` will automatically run itself via `sudo` only when it is first\ncalled as a non-root user outside of a shell session.\n\nTo force container destruction inside a shell session, use `extra-container shell --destroy|-d`.\n\n#### Disable auto-destruction\nBy default, `shell` destroys the shell container before starting and before exiting.\nThis ensures that containers start with no leftover filesystem state from\nprevious runs and that containers do not consume system resources after use.\\\nTo disable auto-destructing containers, run\n`extra-container shell --no-destroy|-n`\n\n\n### Private network helper\n\nContainer options `extra.*` are defined by `extra-container` and help with setting up private network containers.\\\nSee [eval-config.nix](./eval-config.nix) for full option descriptions.\n```nix\ncontainers.demo = {\n  extra = {\n    # Sets\n    # privateNetwork = true\n    # hostAddress = \"${addressPrefix}.1\"\n    # localAddress = \"${addressPrefix}.2\"\n    addressPrefix = \"10.250.0\";\n\n    # Enable internet access for the container\n    enableWAN = true;\n    # Always allow connections from hostAddress\n    firewallAllowHost = true;\n    # Make the container's localhost reachable via localAddress\n    exposeLocalhost = true;\n  }\n};\n```\n\n### Access working dir in non-file configs\n\n`extra-container` appends `pwd` to `NIX_PATH` to allow configs given via `--expr|-E`\nor via stdin to access the working directory.\n```bash\nextra-container create -E '{ imports = [ <pwd/myfile.nix> ]; ... }'\n```\n\n## Define containers via Flakes\n\nSee [examples/flake](./examples/flake).\n\n## Usage\n```\nextra-container create <container-config-file>\n                       [--attr|-A attrPath]\n                       [--nixpkgs-path|--nixos-path path]\n                       [--start|-s | --restart-changed|-r]\n                       [--ssh]\n                       [--build-args arg...]\n\n    <container-config-file> is a NixOS config file with container\n    definitions like 'containers.mycontainer = { ... }'\n\n    --attr | -A attrPath\n      Select an attribute from the config expression\n\n    --nixpkgs-path\n      A nix expression that returns a path to the nixpkgs source\n      to use for building the containers\n\n    --nixos-path\n      Like '--nixpkgs-path', but for directly specifying the NixOS source\n\n    --start | -s\n      Start all created containers\n      Update running containers that have changed or restart them if '--restart-changed' was specified\n\n    --update-changed | -u\n      Update running containers with a changed system configuration by running\n      'switch-to-configuration' inside the container.\n      Restart containers with a changed container configuration\n\n    --restart-changed | -r\n      Restart running containers that have changed\n\n    --ssh\n      Generate SSH keys in /tmp and enable container SSH access.\n      The key files remain after exit and are reused on subsequent runs.\n      Unlocks the function 'cssh' in 'extra-container shell'.\n      Requires container option 'privateNetwork = true'.\n\n    --build-args arg...\n      All following args are passed to nix-build.\n\n    Example:\n      extra-container create mycontainers.nix --restart-changed\n\n      extra-container create mycontainers.nix --nixpkgs-path \\\n        'fetchTarball https://nixos.org/channels/nixos-unstable/nixexprs.tar.xz'\n\n      extra-container create mycontainers.nix --start --build-args --builders 'ssh://worker - - 8'\n\necho <container-config> | extra-container create\n    Read the container config from stdin\n\n    Example:\n      extra-container create --start <<EOF\n        { containers.hello = { enableTun = true; config = {}; }; }\n      EOF\n\nextra-container create --expr|-E <container-config>\n    Provide container config as an argument\n\nextra-container create <store-path>\n    Create containers from <store-path>/etc\n\n    Examples:\n      Create from nixos system derivation\n      extra-container create /nix/store/9h..27-nixos-system-foo-18.03\n\n      Create from nixos etc derivation\n      extra-container create /nix/store/32..9j-etc\n\nextra-container shell ...\n    Start a container shell session.\n    See the README for a complete documentation.\n    Supports all arguments from 'create'\n\n    Extra arguments:\n      --run <cmd> <arg>...\n        Run command in shell session and exit\n        Must be the last option given\n      --no-destroy|-n\n        Do not destroy shell container before and after running\n      --destroy|-d\n        If running inside an existing shell session, force container to\n        be destroyed before and after running\n\n    Example:\n      extra-container shell -E '{ containers.demo.config = {}; }'\n\nextra-container build ...\n    Build the container config and print the resulting NixOS system etc path\n\n    This command can be used like 'create', but options related\n    to starting are not supported\n\nextra-container list\n    List all extra containers\n\nextra-container restart <container>...\n    Fixes the broken restart command of nixos-container (nixpkgs issue #43652)\n\nextra-container destroy <container-name>...\n    Destroy containers\n\nextra-container destroy <args for create/shell>...\n    Destroy the containers defined by the args for command `create` or `shell` (see above).\n    For this to work, the first arg after `destroy` must start with one of the\n    following three characters: ./-\n\n    Example:\n      extra-container destroy ./containers.nix\n\nextra-container destroy --all|-a\n    Destroy all extra containers\n\nextra-container <cmd> <arg>...\n    All other commands are forwarded to nixos-container\n```\n\n## Implementation\n\nThe script works like this: Take a NixOS config with container definitions and build\nthe system's `config.system.build.etc` derivation. Because we're not building a full\nsystem we can use a reduced module set (`eval-config.nix`) to improve evaluation\nperformance.\n\nNow link the container files from the etc derivation to the main system, like so:\n```\nnixos-system/etc/systemd/system/container@CONTAINER.service -> /etc/systemd-mutable/system\nnixos-system/etc/containers/CONTAINER.conf -> /etc/containers       (system.stateVersion < 22.05)\n                                           -> /etc/nixos-containers (system.stateVersion ≥ 22.05)\n```\nFinally, add gcroots pointing to the linked files.\n\n\n## Developing\nAll contributions and suggestions are welcome, even if they're minor or cosmetic.\n\n### Development workflow\n\nRun `nix develop` in the project root directory to start a development shell.\\\nWithin the shell, you can run extra-container from the [local\nsource](./extra-container) via command `extra-container`.\n\nWhen changing the `Usage` documentation in `extra-container`, run `make doc` to copy\nthese changes to `README.md`.\n\n#### Tests\n\nRun `make` to run the tests.\\\nThe following tests are executed:\n\n- Main test suite\n\n  Can be run manually via `sudo run-tests-in-container.sh`.\\\n  This script creates a temporary container named `test-extra-container` in which\n  the main test script [`test.sh`](./test.sh) is run.\\\n  `test.sh` adds and removes temporary containers named `test-*` on the host system.\n  It can also be called directly, but wrapping it with `run-tests-in-container.sh`\n  helps reducing interference with your main system.\n\n- VM test\n\n  Can be run manually via `nix build .#test`.\\\n  This is a basic test using the [NixOS VM test\n  framework](https://github.com/NixOS/nixpkgs/blob/master/nixos/lib/testing-python.nix).\n  It is built as a Nix derivation, which makes it independent from the system\n  environment.\n\n  For debugging the VM, run `nix run .#debugTest` to start a Python test driver shell\n  inside the VM.\n\n- `nix flake check`\n\n  Evaluates all flake outputs and builds the VM test.\n\n#### VM\n\nRun `nix run .#vm` to start a VM where `extra-container` is installed.\\\nThis provides an isolated and reproducible testing environment.\n"
  },
  {
    "path": "default.nix",
    "content": "{ stdenv, lib, nixos-container, openssh, glibcLocales\n, pkgSrc ? lib.cleanSource ./.\n}:\n\nstdenv.mkDerivation rec {\n  pname = \"extra-container\";\n  version = \"0.14\";\n\n  src = pkgSrc;\n\n  buildCommand = ''\n    install -D $src/extra-container $out/bin/extra-container\n    patchShebangs $out/bin\n    share=$out/share/extra-container\n    install $src/eval-config.nix -Dt $share\n\n    # Use existing PATH for systemctl and machinectl\n    scriptPath=\"export PATH=${lib.makeBinPath [ openssh ]}:\\$PATH\"\n\n    sed -i \"\n      s|evalConfig=.*|evalConfig=$share/eval-config.nix|\n      s|LOCALE_ARCHIVE=.*|LOCALE_ARCHIVE=${glibcLocales}/lib/locale/locale-archive|\n      2i$scriptPath\n      2inixosContainer=${nixos-container}/bin\n    \" $out/bin/extra-container\n  '';\n\n  meta = with lib; {\n    description = \"Run declarative containers without full system rebuilds\";\n    homepage = \"https://github.com/erikarvstedt/extra-container\";\n    changelog = \"https://github.com/erikarvstedt/extra-container/blob/master/CHANGELOG.md\";\n    license = licenses.mit;\n    platforms = platforms.linux;\n    maintainers = [ maintainers.erikarvstedt ];\n  };\n}\n"
  },
  {
    "path": "eval-config.nix",
    "content": "{ nixosPath\n, systemConfig\n, legacyInstallDirs\n, system ? builtins.currentSystem\n}:\n\nlet\n  # A minimal module set for evaluating container configs.\n  # This significantly reduces extra-container evaluation overhead (total eval time - container eval time)\n  # Compatible with nixpkgs >= 16.09\n  baseModules = [\n    (nixosPath + \"/modules/misc/assertions.nix\")\n    (nixosPath + \"/modules/misc/nixpkgs.nix\")\n    (nixosPath + \"/modules/misc/extra-arguments.nix\")\n    (nixosPath + \"/modules/system/activation/top-level.nix\")\n    (nixosPath + \"/modules/system/etc/etc.nix\")\n    (nixosPath + \"/modules/system/boot/systemd.nix\")\n    nixosContainerModule\n    dummyOptions\n  ];\n\n  nixosContainerModule = let\n    new = nixosPath + \"/modules/virtualisation/nixos-containers.nix\";\n    old = nixosPath + \"/modules/virtualisation/containers.nix\"; # For nixpkgs < 20.09)\n  in\n    if builtins.pathExists new then new else old;\n\n  dummyOptions = { pkgs, lib, options, ... }: let\n    optionValue = default: lib.mkOption { inherit default; };\n    dummy = optionValue [];\n  in {\n    options = {\n      boot.kernel.sysctl = dummy;\n      boot.kernelModules = dummy;\n      boot.kernelPackages.kernel.version = optionValue \"\";\n      boot.kernelParams = dummy;\n      boot.loader.systemd-boot.bootCounting.enable = optionValue false;\n      environment.systemPackages = dummy;\n      networking.dhcpcd.denyInterfaces = dummy;\n      networking.hosts = dummy;\n      networking.extraHosts = dummy;\n      networking.proxy.envVars = optionValue {};\n      nix.package = optionValue pkgs.nix;\n      security = dummy;\n      services = {\n        dbus = dummy;\n        logrotate = dummy;\n        udev = dummy;\n        rsyslogd.enable = optionValue false;\n        syslog-ng.enable = optionValue false;\n      };\n      system.activationScripts = dummy;\n      system.fsPackages = dummy;\n      system.nssDatabases = dummy;\n      system.nssModules = dummy;\n      system.path = optionValue \"\";\n      system.requiredKernelConfig = dummy;\n      system.stateVersion = optionValue (if legacyInstallDirs then \"21.11\" else \"22.05\");\n      systemd.oomd = dummy;\n      systemd.user.generators = optionValue {};\n      ids.gids.keys = dummy;\n      ids.uids.systemd-coredump = dummy;\n      ids.gids.systemd-journal = dummy;\n      ids.gids.systemd-journal-gateway = dummy;\n      ids.uids.systemd-journal-gateway = dummy;\n      ids.gids.systemd-network = dummy;\n      ids.uids.systemd-network = dummy;\n      ids.uids.systemd-resolve = dummy;\n      ids.gids.systemd-resolve = dummy;\n      users.users.systemd-coredump = dummy;\n      users.users.systemd-network.group = dummy;\n      users.users.systemd-network.uid = dummy;\n      users.users.systemd-resolve.group = dummy;\n      users.users.systemd-resolve.uid = dummy;\n      users.users.systemd-journal-gateway.group = dummy;\n      users.users.systemd-journal-gateway.uid = dummy;\n      users.groups.systemd-coredump = dummy;\n      users.groups.systemd-network.gid = dummy;\n      users.groups.systemd-resolve.gid = dummy;\n      users.groups.keys.gid = dummy;\n      users.groups.systemd-journal.gid = dummy;\n      users.groups.systemd-journal-gateway.gid = dummy;\n    };\n\n    config = {\n      systemd.timers = lib.mkForce {};\n      systemd.targets = lib.mkForce {};\n    } // lib.optionalAttrs (options.systemd ? managerEnvironment) {\n      systemd.managerEnvironment = lib.mkForce {};\n    };\n  };\n\n  containerAssert = cond: name: msg: value:\n    if cond then value\n    else throw \"container '${name}': ${msg}'\";\n\n  assertNonNull = var:\n    containerAssert (var != null);\n\n  extraModule = { config, pkgs, lib, ... }: with lib; {\n    options = {\n      containers = mkOption {\n        type = types.attrsOf (types.submodule (\n          { config, name, ... }: {\n            options = {\n              extra = {\n                addressPrefix = mkOption {\n                  type = with types; nullOr str;\n                  default = null;\n                  description = ''\n                    Enable privateNetwork and set\n                    hostAddress = <addressPrefix>.1\n                    localAddress = <addressPrefix>.2\n                  '';\n                };\n                enableWAN = mkOption {\n                  type = types.bool;\n                  default = false;\n                  description = ''\n                    Enable WAN access inside the container by rewriting container traffic\n                    to use the host's address (NAT).\n\n                    Only active when privateNetwork == true.\n                  '';\n                };\n                exposeLocalhost = mkOption {\n                  type = types.bool;\n                  default = false;\n                  description = ''\n                    Forward requests from the container's external interface\n                    to the container's localhost.\n                    Useful to test internal services from outside the container.\n\n                    WARNING: This exposes the container's localhost to all users.\n                    Only use in a trusted environment.\n\n                    Only active when privateNetwork == true.\n                  '';\n                };\n                firewallAllowHost = mkOption {\n                  type = types.bool;\n                  default = false;\n                  description = ''\n                    Always allow connections from the container host.\n\n                    Only active when privateNetwork == true.\n                  '';\n                };\n                enableSSH = mkOption {\n                  type = types.bool;\n                  default = builtins.getEnv(\"extraContainerSSH\") == \"1\";\n                  description = ''\n                    Enable SSH access with an automatically generated key.\n                    This enables the 'cssh' comand in extra-container shell.\n\n                    Requires privateNetwork == true.\n                  '';\n                };\n              };\n            };\n\n            config = mkMerge [\n              (\n                let\n                  prefix = config.extra.addressPrefix;\n                in mkIf (prefix != null) {\n                  privateNetwork = true;\n                  hostAddress = \"${prefix}.1\";\n                  localAddress = \"${prefix}.2\";\n                }\n              )\n              {\n                config = ({ pkgs, ... }@moduleArgs: mkMerge [\n                  {\n                    systemd.services.forward-to-localhost = mkIf (config.extra.exposeLocalhost && config.privateNetwork) {\n                      wantedBy = [ \"network.target\" ];\n                      script = assertNonNull config.localAddress name\n                        ''\n                          option extra.exposeLocalhost requires localAddress to be non-null.\n                        ''\n                        ''\n                          ${pkgs.procps}/bin/sysctl -w net.ipv4.conf.all.route_localnet=1\n                          ${pkgs.iptables}/bin/iptables -w -t nat -I PREROUTING -p tcp \\\n                            -d ${config.localAddress} ! --dport 80 -j DNAT --to-destination 127.0.0.1\n                        '';\n                    };\n                    networking.firewall.extraCommands = mkIf (config.extra.firewallAllowHost && config.privateNetwork) (\n                      assertNonNull config.hostAddress name\n                        ''\n                          option extra.exposeLocalhost requires hostAddress to be non-null.\n                        ''\n                        ''\n                          iptables -w -A nixos-fw -s ${config.hostAddress} -j ACCEPT\n                        ''\n                    );\n                    # Silence system state warning\n                    system.stateVersion = lib.mkDefault moduleArgs.config.system.nixos.release;\n                  }\n                  (mkIf config.extra.enableSSH {\n                     services.openssh.enable = containerAssert config.privateNetwork name ''\n                       option extra.enableSSH requires privateNetwork to be enabled.\n                     '' true;\n                     users.users.root.openssh.authorizedKeys.keyFiles = [\n                       /tmp/extra-container-ssh/key.pub\n                     ];\n                  })\n                ]);\n              }\n            ];\n          }\n        ));\n      };\n    };\n\n    config = {\n      systemd.services = let\n        WANContainers = builtins.filter (c:\n                          let cfg = config.containers.${c};\n                          in cfg.privateNetwork && cfg.extra.enableWAN\n                        ) (builtins.attrNames config.containers);\n        iptables = \"${pkgs.iptables}/bin/iptables\";\n        serviceCfg = c: let\n          containerAddress = config.containers.${c}.localAddress;\n        in\n          assertNonNull containerAddress c\n          ''\n            option extra.enableWAN requires localAddress to be non-null\n          ''\n          {\n            preStart = \"${iptables} -w -t nat -A POSTROUTING -s ${containerAddress} -j MASQUERADE\";\n            postStop = \"${iptables} -w -t nat -D POSTROUTING -s ${containerAddress} -j MASQUERADE || true\";\n          };\n      in\n        listToAttrs (map (c: nameValuePair \"container@${c}\" (serviceCfg c)) WANContainers);\n    };\n  };\nin\nimport (nixosPath + \"/lib/eval-config.nix\") {\n  inherit baseModules system;\n  modules = [ extraModule systemConfig ];\n}\n"
  },
  {
    "path": "examples/flake/flake.nix",
    "content": "# See how this flake is used in ./usage.sh\n{\n  inputs.extra-container.url = \"github:erikarvstedt/extra-container\";\n  inputs.nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n\n  outputs = { extra-container, ... }@inputs:\n    extra-container.lib.eachSupportedSystem (system: {\n      packages.default = extra-container.lib.buildContainers {\n        # The system of the container host\n        inherit system;\n\n        # Only set this if the `system.stateVersion` of your container\n        # host is < 22.05\n        # legacyInstallDirs = true;\n\n        # Optional: Set nixpkgs.\n        # If unset, the nixpkgs input of extra-container flake is used\n        nixpkgs = inputs.nixpkgs;\n\n        # Set this to disable `nix run` support\n        # addRunner = false;\n\n        config = {\n          containers.demo = {\n            extra.addressPrefix = \"10.250.0\";\n\n            # `specialArgs` is available in nixpkgs > 22.11\n            # This is useful for importing flakes from modules (see nixpkgs/lib/modules.nix).\n            # specialArgs = { inherit inputs; };\n\n            config = { pkgs, ... }: {\n              systemd.services.hello = {\n                wantedBy = [ \"multi-user.target\" ];\n                script = ''\n                  while true; do\n                    echo hello | ${pkgs.netcat}/bin/nc -lN 50\n                  done\n                '';\n              };\n              networking.firewall.allowedTCPPorts = [ 50 ];\n            };\n          };\n        };\n      };\n    });\n}\n"
  },
  {
    "path": "examples/flake/usage.sh",
    "content": "# Usage via `nix run`\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n# Container lifecycle\n\n# Create and start container defined by ./flake.nix\nnix run . -- create --start\n# You can use the same command to update the (running) container,\n# after changing the container configuration.\n#\n# The arguments after `--` are passed to the `extra-container` binary in PATH,\n# while the flake is used for the container definitions.\n\n# Use `nixos-container` to control the running container\nsudo nixos-container run demo -- hostname\nsudo nixos-container root-login demo\n\n# Destroy container\nnix run . -- destroy\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n# Container shell\n# Start an interactive shell in an ephemeral container\nnix run . -- shell\nnix run . # equivalent, because `shell` is used as the default command\n\n# Run a single command in the container.\n# The container is destroyed afterwards.\nnix run . -- --run c hostname\nnix run . -- shell --run c hostname # equivalent\nnix run . -- --run bash -c 'curl --http0.9 $ip:50'\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n# Usage via `nix build`\n# 1. Build container\nnix build . --out-link /tmp/container\n# 2. Run container\nextra-container shell /tmp/container\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n# Inspect container configs\nnix eval . --apply 'sys: sys.containers.demo.config.networking.hostName'\n"
  },
  {
    "path": "extra-container",
    "content": "#!/usr/bin/env bash\n\nset -eo pipefail\nshopt -s nullglob\n\n# This script uses systemctl, machinectl and nsenter from the existing environment\n\nshowHelp() {\n    echo \"Usage:\"\n    echo\n    echo \"extra-container create <container-config-file>\"\n    echo \"                       [--attr|-A attrPath]\"\n    echo \"                       [--nixpkgs-path|--nixos-path path]\"\n    echo \"                       [--start|-s | --restart-changed|-r]\"\n    echo \"                       [--ssh]\"\n    echo \"                       [--build-args arg...]\"\n    echo\n    echo \"    <container-config-file> is a NixOS config file with container\"\n    echo \"    definitions like 'containers.mycontainer = { ... }'\"\n    echo\n    echo \"    --attr | -A attrPath\"\n    echo \"      Select an attribute from the config expression\"\n    echo\n    echo \"    --nixpkgs-path\"\n    echo \"      A nix expression that returns a path to the nixpkgs source\"\n    echo \"      to use for building the containers\"\n    echo\n    echo \"    --nixos-path\"\n    echo \"      Like '--nixpkgs-path', but for directly specifying the NixOS source\"\n    echo\n    echo \"    --start | -s\"\n    echo \"      Start all created containers\"\n    echo \"      Update running containers that have changed or restart them if '--restart-changed' was specified\"\n    echo\n    echo \"    --update-changed | -u\"\n    echo \"      Update running containers with a changed system configuration by running\"\n    echo \"      'switch-to-configuration' inside the container.\"\n    echo \"      Restart containers with a changed container configuration\"\n    echo\n    echo \"    --restart-changed | -r\"\n    echo \"      Restart running containers that have changed\"\n    echo\n    echo \"    --ssh\"\n    echo \"      Generate SSH keys in /tmp and enable container SSH access.\"\n    echo \"      The key files remain after exit and are reused on subsequent runs.\"\n    echo \"      Unlocks the function 'cssh' in 'extra-container shell'.\"\n    echo \"      Requires container option 'privateNetwork = true'.\"\n    echo\n    echo \"    --build-args arg...\"\n    echo \"      All following args are passed to nix-build.\"\n    echo\n    echo \"    Example:\"\n    echo \"      extra-container create mycontainers.nix --restart-changed\"\n    echo\n    echo \"      extra-container create mycontainers.nix --nixpkgs-path \\\\\"\n    echo \"        'fetchTarball https://nixos.org/channels/nixos-unstable/nixexprs.tar.xz'\"\n    echo\n    echo \"      extra-container create mycontainers.nix --start --build-args --builders 'ssh://worker - - 8'\"\n    echo\n    echo \"echo <container-config> | extra-container create\"\n    echo \"    Read the container config from stdin\"\n    echo\n    echo \"    Example:\"\n    echo \"      extra-container create --start <<EOF\"\n    echo \"        { containers.hello = { enableTun = true; config = {}; }; }\"\n    echo \"      EOF\"\n    echo\n    echo \"extra-container create --expr|-E <container-config>\"\n    echo \"    Provide container config as an argument\"\n    echo\n    echo \"extra-container create <store-path>\"\n    echo \"    Create containers from <store-path>/etc\"\n    echo\n    echo \"    Examples:\"\n    echo \"      Create from nixos system derivation\"\n    echo \"      extra-container create /nix/store/9h..27-nixos-system-foo-18.03\"\n    echo\n    echo \"      Create from nixos etc derivation\"\n    echo \"      extra-container create /nix/store/32..9j-etc\"\n    echo\n    echo \"extra-container shell ...\"\n    echo \"    Start a container shell session.\"\n    echo \"    See the README for a complete documentation.\"\n    echo \"    Supports all arguments from 'create'\"\n    echo\n    echo \"    Extra arguments:\"\n    echo \"      --run <cmd> <arg>...\"\n    echo \"        Run command in shell session and exit\"\n    echo \"        Must be the last option given\"\n    echo \"      --no-destroy|-n\"\n    echo \"        Do not destroy shell container before and after running\"\n    echo \"      --destroy|-d\"\n    echo \"        If running inside an existing shell session, force container to\"\n    echo \"        be destroyed before and after running\"\n    echo\n    echo \"    Example:\"\n    echo \"      extra-container shell -E '{ containers.demo.config = {}; }'\"\n    echo\n    echo \"extra-container build ...\"\n    echo \"    Build the container config and print the resulting NixOS system etc path\"\n    echo\n    echo \"    This command can be used like 'create', but options related\"\n    echo \"    to starting are not supported\"\n    echo\n    echo \"extra-container list\"\n    echo \"    List all extra containers\"\n    echo\n    echo \"extra-container restart <container>...\"\n    echo \"    Fixes the broken restart command of nixos-container (nixpkgs issue #43652)\"\n    echo\n    echo \"extra-container destroy <container-name>...\"\n    echo \"    Destroy containers\"\n    echo\n    echo \"extra-container destroy <args for create/shell>...\"\n    echo \"    Destroy the containers defined by the args for command \\`create\\` or \\`shell\\` (see above).\"\n    echo \"    For this to work, the first arg after \\`destroy\\` must start with one of the\"\n    echo \"    following three characters: ./-\"\n    echo\n    echo \"    Example:\"\n    echo \"      extra-container destroy ./containers.nix\"\n    echo\n    echo \"extra-container destroy --all|-a\"\n    echo \"    Destroy all extra containers\"\n    echo\n    echo \"extra-container <cmd> <arg>...\"\n    echo \"    All other commands are forwarded to nixos-container\"\n    exit 0\n}\n\ncase $1 in\n    help|-h|--help)\n        showHelp\n        ;;\nesac\n\n# If a container build output is given ($EXTRA_CONTAINER_ETC) and no command is\n# specified in $1, use `shell` as the default command\nif [[ $EXTRA_CONTAINER_ETC && ($# == 0 || $1 == -*) ]]; then\n    set -- shell \"$@\"\nelif [[ $# == 0 ]]; then\n     showHelp\nfi\n\n# Run as root if needed\n#------------------------------------------------------------------------------\n\nif [[ $EUID != 0 ]]; then\n    exec sudo PATH=\"$PATH\" NIX_PATH=\"$NIX_PATH\" EXTRA_CONTAINER_ETC=\"$EXTRA_CONTAINER_ETC\" \"${BASH_SOURCE[0]}\" \"$@\"\nfi\n\n# Operating system specific setup\n#------------------------------------------------------------------------------\n\nerrEcho() {\n    >&2 echo \"$@\"\n}\n\n[[ -e /run/booted-system/nixos-version ]] && isNixOS=1 || isNixOS=\n\nif [[ $isNixOS ]]; then\n    mutableServicesDir=/etc/systemd-mutable/system\nelse\n    # This is the canonical way to check for systemd\n    # https://www.freedesktop.org/software/systemd/man/sd_booted.html\n    if [[ ! -e /run/systemd/system ]]; then\n        errEcho \"extra-container requires systemd\"\n        exit 1\n    fi\n    if ! type -P machinectl > /dev/null; then\n        errEcho \"extra-container requires machinectl to be installed\"\n        exit 1\n    fi\n    if [[ ! -e /nix/var/nix/profiles/default ]]; then\n        errEcho \"extra-container requires a multi-user nix installation\"\n        exit 1\n    fi\n\n    mutableServicesDir=/usr/lib/systemd/system\n\n    # For nixos-container on non-NixOS systems https://github.com/NixOS/nix/issues/599#issuecomment-153885553\n    # The value of 'LOCALE_ARCHIVE' is inserted by the builder (./default.nix)\n    if [[ ! -v LOCALE_ARCHIVE ]]; then\n        export LOCALE_ARCHIVE=\n    fi\n\n    # Work around https://github.com/NixOS/nixpkgs/issues/28833\n    createOsReleaseFile() {\n        if [[ ! -f /etc/static/os-release ]]; then\n            mkdir -p /etc/static\n            echo \"# added by extra-container\" > /etc/static/os-release\n        fi\n    }\nfi\n\n# Use existing `nixos-container` in PATH by default because on NixOS the container\n# install location differs depending on `system.stateVersion` and is hardcoded in\n# the `nixos-container` binary.\n# See also: function `getInstallDirs`.\nif ! type -P nixos-container > /dev/null; then\n    PATH=$nixosContainer:$PATH\nfi\n\n# Parse and run command\n#------------------------------------------------------------------------------\n\ntrap 'echo \"Error at ${BASH_SOURCE[0]}:$LINENO\"' ERR\n\ntmpDir=\nonlyBuild=\nshell=\nlist=\nrestart=\ndestroy=\n\ncase $1 in\n    create|add)\n        shift\n        ;;\n    build)\n        onlyBuild=1\n        shift\n        ;;\n    shell)\n        shell=1\n        shift\n        ;;\n    list)\n        list=1\n        ;;\n    restart)\n        restart=1\n        shift\n        ;;\n    destroy)\n        destroy=1\n        shift\n        ;;\n    *)\n        exec nixos-container \"$@\"\n        ;;\nesac\n\n# Value is replaced by builder (./default.nix)\nevalConfig=$(cd \"${BASH_SOURCE[0]%/*}\" && pwd)/eval-config.nix\n\nbuildContainers() {\n    local containerCfg=$1\n    local tmpDir=$2\n    local nixosPath=$3\n\n    local attrExpr=\n    if [[ $attr ]]; then\n        attrExpr=\".\\${''$attr''}\"\n    fi\n    NIX_PATH=$NIX_PATH:pwd=$PWD nix-build --out-link $tmpDir/result \"${buildArgs[@]}\" -E \\\n    \" let cfg = ($containerCfg)$attrExpr;\n      in (import $evalConfig {\n         nixosPath = $nixosPath;\n         legacyInstallDirs = $legacyInstallDirs;\n         systemConfig = cfg;\n      }).config.system.build.etc\n    \" >/dev/null\n}\n\nrestartContainers() {\n    local services=$(getServiceNames $*)\n\n    # `systemctl restart <container>` is broken (https://github.com/NixOS/nixpkgs/issues/43652),\n    # so use a workaraound\n    systemctl stop $services\n    # Retry terminating the container machines until the command succeeds\n    # or the machines have disappeared\n    for ((i = 1;; i++)); do\n        local failed=\n        local output\n        output=$(machinectl terminate $* 2>&1) || failed=1\n        if [[ ! $failed || $output == *no*machine*known* ]]; then\n            break\n        fi\n        echo $output\n        if ((i == 20)); then\n            errEcho \"Failed to stop containers.\"\n            exit 1\n        fi\n        sleep 0.001\n    done\n    systemctl start $services\n}\n\nupdateContainers() {\n    for container in $*; do\n        local confFile=$configDirectory/$container.conf\n        local systemPath=$(grep -ohP \"(?<=^SYSTEM_PATH=).*\" $confFile)\n        # Shift output 2 spaces to the right\n        echo \"  Updating $container\"\n        nixos-container run $container -- bash -lc \"${systemPath}/bin/switch-to-configuration test\" |& sed 's/^/  /' || true\n        echo\n    done\n}\n\ngetContainers() {\n    for service in $mutableServicesDir/container@?*.service; do\n        getContainerName $service\n    done\n}\n\ngetContainerName() {\n    [[ $1 =~ container@(.+)\\.service ]]\n    echo ${BASH_REMATCH[1]}\n}\n\ngetServiceNames() {\n    for container in $*; do\n        echo \"container@$container.service \"\n    done\n}\n\nmakeGCRootsPath() {\n    # Use gcroots/auto instead of gcroots/per-user/root because\n    # stale links in per-user are not automatically removed.\n    # This avoids cluttering the gcroots dir in case containers\n    # are manually removed.\n    echo /nix/var/nix/gcroots/auto/extra-container-$1\n}\n\n# The key is reused between extra-container sessions\nmakeSSHKey() {\n    local keyDir=/tmp/extra-container-ssh\n    sshKey=$keyDir/key\n    if [[ ! (-d $keyDir && $(stat -c '%a%U' $keyDir) == 700root &&\n                 -f $sshKey && -f $sshKey.pub) ]]; then\n        if [[ -e $keyDir ]]; then\n            rm -rf $keyDir\n        fi\n        mkdir -m 700 $keyDir\n        ssh-keygen -t ed25519 -P '' -C none -f $sshKey > /dev/null\n    fi\n    chmod 600 $sshKey\n}\n\nmakeTmpDir() {\n    if [[ ! $tmpDir ]]; then\n        tmpDir=$(mktemp -d /tmp/extra-container.XXX)\n    fi\n}\n\nconfigDirectory=\ngetInstallDirs() {\n    if [[ ! $configDirectory ]]; then\n        nixosContainer=$(type -p nixos-container)\n        if grep -q '\"/etc/nixos-containers\"' \"$nixosContainer\"; then\n            # NixOS ≥22.05\n            configDirectory=/etc/nixos-containers\n            configDirectoryName=nixos-containers\n            otherConfigDirectoryName=containers\n            stateDirectory=/var/lib/nixos-containers\n            legacyInstallDirs=false\n        else\n            # NixOS <22.05\n            configDirectory=/etc/containers\n            configDirectoryName=containers\n            otherConfigDirectoryName=nixos-containers\n            stateDirectory=/var/lib/containers\n            legacyInstallDirs=true\n        fi\n    fi\n}\n\n# This fn is used by commands `create`, `shell`, `destroy`.\n# To simplify the implementation, this fn includes the whole arg parsing for commands\n# `create`, `shell`. These args are not by `destroy`.\ngetContainersFromDefinition() {\n    start=\n    update=\n    restart=\n    ssh=\n    containerExpr=\n    attr=\n    nixpkgsPath=\n    nixosPath=\n    buildArgs=()\n    runCommand=()\n\n    destroy=1\n    forceDestroy=\n\n    args=()\n    while [[ $# > 0 ]]; do\n        arg=\"$1\"\n        shift\n        case $arg in\n            --start|-s)\n                start=1\n                ;;\n            --update-changed|-u)\n                update=1\n                ;;\n            --restart-changed|-r)\n                restart=1\n                ;;\n            --ssh)\n                ssh=1\n                ;;\n            --expr|-E)\n                containerExpr=\"$1\"\n                shift\n                ;;\n            --attr|-A)\n                attr=\"$1\"\n                shift\n                ;;\n            --nixpkgs-path)\n                nixpkgsPath=\"$1\"\n                shift\n                ;;\n            --nixos-path)\n                nixosPath=\"$1\"\n                shift\n                ;;\n            --build-args)\n                buildArgs=()\n                # Add all following args until --run\n                while [[ $# > 0 && $1 != --run ]]; do\n                    buildArgs+=(\"$1\")\n                    shift\n                done\n                ;;\n            --run)\n                runCommand=(\"$@\")\n                break\n                ;;\n            --no-destroy|-n)\n                destroy=\n                ;;\n            --destroy|-d)\n                destroy=1\n                forceDestroy=1\n                ;;\n            *)\n                args+=(\"$arg\")\n                ;;\n        esac\n    done\n    set -- \"${args[@]}\"\n\n    if [[ ! $containerExpr ]]; then\n        arg=${1:-}\n        shift || true\n    fi\n    if [[ $# > 0 ]]; then\n        errEcho \"Error: Unhandled arguments: $*\"\n        exit 1\n    fi\n\n    ## 1. Build containers if needed\n\n    if [[ $ssh ]]; then\n        makeSSHKey\n    fi\n    # This env var is read by ./eval-config.nix while building\n    export extraContainerSSH=$ssh\n\n    getInstallDirs\n\n    needToBuildContainers() {\n        if [[ $containerExpr ]]; then\n            return 0;\n        fi\n        if [[ $arg ]]; then\n            if [[ -f $arg || -e $arg/default.nix ]]; then\n                return 0\n            fi\n        elif [[ ! $EXTRA_CONTAINER_ETC ]]; then\n            return 0\n        fi\n        return 1\n    }\n\n    if needToBuildContainers; then\n        if [[ ! $containerExpr ]]; then\n            if [[ ! $arg || $arg == - ]]; then\n                # Read container cfg from stdin\n                if [[ $shell ]]; then\n                    errEcho \"Reading container config from STDIN is not supported for command 'shell'\"\n                    exit 1\n                fi\n                read -d '' containerExpr || true\n            else\n                containerExpr=\"import ''$(realpath \"$arg\")''\"\n            fi\n        fi\n        if [[ ! $nixosPath ]]; then\n            if [[ $nixpkgsPath ]]; then\n                nixosPath=\"\\\"\\${toString ($nixpkgsPath)}/nixos\\\"\"\n            else\n                nixosPath=\"<nixpkgs/nixos>\"\n            fi\n        fi\n        makeTmpDir\n        errEcho \"Building containers...\"\n        buildContainers \"$containerExpr\" $tmpDir \"$nixosPath\"\n        nixosSystemEtc=$tmpDir/result/etc\n\n        if [[ $onlyBuild ]]; then\n            realpath $tmpDir/result\n            exit 0\n        fi\n    else\n        if [[ $arg ]]; then\n            EXTRA_CONTAINER_ETC=$arg\n        fi\n        if [[ ! $EXTRA_CONTAINER_ETC ]]; then\n            errEcho \"No containers specified\"\n            exit 1\n        fi\n        nixosSystemEtc=\"$EXTRA_CONTAINER_ETC/etc\"\n        if [[ ! -e $nixosSystemEtc ]]; then\n            errEcho \"$nixosSystemEtc doesn't exist\"\n            exit 1\n        fi\n    fi\n\n    ## 2. Gather containers\n\n    services=$(echo $nixosSystemEtc/systemd/system/container@?*.service)\n    if [[ ! $services ]]; then\n        errEcho \"No container services in $nixosSystemEtc/systemd/system\"\n        exit 0\n    fi\n    allContainers=()\n    for service in $services; do\n        allContainers+=($(getContainerName $service))\n    done\n    allContainers=($(printf '%s\\n' ${allContainers[@]} | sort))\n}\n\natExit() {\n    origExitCode=$?\n    set +e\n    if [[ $startShellEnv && $destroy ]]; then\n        [[ $runCommand ]] || echo \"Destroying container.\"\n        destroyContainers $shellContainer\n    fi\n    if [[ $tmpDir ]]; then\n        rm -rf $tmpDir\n    fi\n    exit $origExitCode\n}\ntrap atExit EXIT\n\n# Command 'list extra containers'\n#------------------------------------------------------------------------------\n\nif [[ $list ]]; then\n    getContainers\n    exit 0\nfi\n\n# Command 'restart container'\n#------------------------------------------------------------------------------\n\nif [[ $restart ]]; then\n    restartContainers $*\n    exit 0\nfi\n\n# Command 'destroy containers'\n#------------------------------------------------------------------------------\n\nsystemctlIgnoreNotLoaded() {\n    local failed=\n    output=$(systemctl \"$@\" 2>&1) || failed=1\n    if [[ $failed ]]; then\n        if [[ $output == *not\\ loaded* ]]; then\n            unitLoaded=\n        else\n            return 1\n        fi\n    fi\n}\n\ndestroyContainers() {\n    getInstallDirs\n\n    if [[ ! -e \"$configDirectory\" ]]; then\n        # No containers installed. The containers dir is needed by the code below.\n        return\n    fi\n\n    local needDaemonReload=\n\n    for container in \"$@\"; do\n        service=container@${container}.service\n        serviceFile=${mutableServicesDir}/$service\n        confFile=\"$configDirectory/$container.conf\"\n\n        # Signal 'stop' before killing so that the killed container doesn't restart\n        local output\n        local unitLoaded=1\n        if ! systemctlIgnoreNotLoaded stop --no-block $service; then\n            errEcho \"Stopping $service failed with: $output\"\n        fi\n        if [[ $unitLoaded ]]; then\n            if ! systemctlIgnoreNotLoaded kill $service; then\n                errEcho \"Killing $service failed with: $output\"\n            fi\n        fi\n\n        rm -f \"$mutableServicesDir/machines.target.wants/$service\"\n\n        if [[ -L $serviceFile ]]; then\n            rm $serviceFile\n            needDaemonReload=1\n        fi\n        local gcRootsPath=$(makeGCRootsPath $container)\n        rm -f $gcRootsPath\n        rm -f $gcRootsPath.conf\n\n        # Remove declarative container confFile, otherwise `nixos-container` fails\n        # with 'cannot destroy declarative container'\n        rm -f $confFile\n        # Create dummy confFile, otherwise `nixos-container` stops before\n        # destroying the container completely\n        touch $confFile\n\n        # Remove immutable attribute from nested container var/empty files so that\n        # the container directory can be deleted\n        for varempty in \"$stateDirectory\"/$container/var/lib/*containers/*/var/empty; do\n            chattr -i $varempty\n        done\n\n        nixos-container destroy $container || true\n    done\n    if [[ $needDaemonReload ]]; then\n        systemctl daemon-reload\n    fi\n}\n\nif [[ $destroy ]]; then\n    if  [[ $1 == --all || $1 == -a ]]; then\n        getInstallDirs\n        containers=($(getContainers))\n    # If the first letter of the first arg is one of ./-\n    # or no arg is given and EXTRA_CONTAINER_ETC is defined\n    elif (($# > 0)) && [[ ${1:0:1} == [./-] ]] || [[ $EXTRA_CONTAINER_ETC ]]; then\n        # Get containers to be destroyed from the container definition\n        getContainersFromDefinition \"$@\"\n        containers=(\"${allContainers[@]}\")\n        echo \"Destroying containers:\"\n        printf '%s\\n' \"${containers[@]}\"\n    else\n        if (($# == 0)); then\n            errEcho \"No container name specified\"\n            exit 1\n        fi\n        containers=(\"$@\")\n    fi\n\n    destroyContainers \"${containers[@]}\"\n    exit 0\nfi\n\n# Commands 'create', 'shell'\n#------------------------------------------------------------------------------\ngetContainersFromDefinition \"$@\"\n\n## Shell related setup\n\nstartShellEnv=\nmakeStartInterruptible=\nif [[ $shell ]]; then\n    if (( ${#allContainers[@]} > 1 )); then\n        errEcho \"Command 'shell' requires only one container to be defined\"\n        exit 1\n    fi\n    shellContainer=${allContainers[0]}\n\n    [[ $shellContainer == $runningShellContainer ]] && insideContainerShell=1 || insideContainerShell=\n\n    if [[ $runCommand || ! $insideContainerShell ]]; then\n        startShellEnv=1\n    fi\n    if [[ $insideContainerShell && ! $forceDestroy ]]; then\n        destroy=\n    fi\n    if [[ $startShellEnv && ! $runCommand ]]; then\n        makeStartInterruptible=1\n    fi\n    if [[ $destroy ]]; then\n        destroyContainers $shellContainer\n    fi\n    start=1\nfi\n\n## 4. Install containers\n\nconfWithoutSystem() {\n    sed /^SYSTEM_PATH=/d $1\n}\n\nisContainerUnchanged() {\n    [[ -e $serviceDest && \\\n       -e $confDest && \\\n       $(realpath $serviceFile) == $(realpath $serviceDest) ]] || return 1\n\n    if [[ $(realpath $confFile) != $(realpath $confDest) ]]; then\n        if [[ $(confWithoutSystem $confFile) == $(confWithoutSystem $confDest) ]]; then\n            onlySystemChangedContainers+=($container)\n        fi\n        return 1\n    fi\n}\n\ncheckInstallationSuccess() {\n    if [[ $isNixOS ]]; then\n        service=container@$1.service\n        # If ExecStart points to the generic 'container_-start' script of the\n        # 'container@.service' service template, the installation of the\n        # container service file failed.\n        if [[ $(systemctl show -p ExecStart $service) == */bin/container_-start*  ]]; then\n            errEcho\n            errEcho 'Container service installation failed.'\n            errEcho 'Please add the following to your NixOS configuration'\n            errEcho 'to enable dynamically installing systemd units:'\n            errEcho 'boot.extraSystemdUnitPaths = [ \"/etc/systemd-mutable/system\" ];'\n            errEcho\n            errEcho 'See also: https://github.com/erikarvstedt/extra-container/#install'\n            exit 1\n        fi\n    fi\n}\n\necho\necho \"Installing containers:\"\n\nmkdir -p $mutableServicesDir \"$configDirectory\" /nix/var/nix/gcroots/auto/\nif [[ ! $isNixOS ]]; then createOsReleaseFile; fi\n\nchangedContainers=()\nonlySystemChangedContainers=()\nfor container in ${allContainers[@]} ; do\n    service=container@$container.service\n    serviceFile=$nixosSystemEtc/systemd/system/$service\n    serviceDest=$mutableServicesDir/$service\n    confFile=$nixosSystemEtc/$configDirectoryName/$container.conf\n    confDest=\"$configDirectory/$(basename $confFile)\"\n\n    if isContainerUnchanged; then\n        echo \"$container (unchanged, skipped)\"\n    else\n        echo \"$container\"\n        changedContainers+=($container)\n\n        if [[ ! -e $confFile ]]; then\n            alternativeConfFile=$nixosSystemEtc/$otherConfigDirectoryName/$container.conf\n            if [[ -e $alternativeConfFile ]]; then\n                errEcho\n                errEcho    'Error:'\n                errEcho    'To be compatible with this container host, the container'\n                errEcho -n 'must be built with `legacyInstallDirs = '\n                if [[ $legacyInstallDirs == true ]]; then\n                    errEcho 'true`.'\n                else\n                    errEcho 'false` (default).'\n                fi\n                errEcho\n            else\n                errEcho \"Error: $confFile doesn't exist\"\n            fi\n            exit 1\n        fi\n\n        ln -sf $(realpath $serviceFile) $serviceDest\n        ln -sf $(realpath $confFile) $confDest\n        gcRootsPath=$(makeGCRootsPath $container)\n        ln -sf $serviceDest $gcRootsPath\n        ln -sf $confDest $gcRootsPath.conf\n\n        if grep -q AUTO_START=1 \"$confFile\"; then\n            wantsDir=$mutableServicesDir/machines.target.wants\n            mkdir -p \"$wantsDir\"\n            ln -sfn \"../$service\" \"$wantsDir\"\n        fi\n    fi\ndone\n\nif [[ $changedContainers ]]; then\n    systemctl daemon-reload\n    checkInstallationSuccess ${changedContainers[0]}\nfi\n\n## 3. Setup shell\n\nif [[ $makeStartInterruptible ]]; then\n    # When starting a shell, continue the script when container starting gets interrupted.\n    # This way, the user can interrupt waiting for the startup of all services\n    # and interact with the starting container.\n    trap handleShell SIGINT\nfi\n\nhandleShell() {\n    trap - SIGINT\n\n    export containerIp=$(nixos-container show-ip $shellContainer 2> /dev/null) || containerIp=\n    export runningShellContainer=$shellContainer\n    export containerStateDirectory=$stateDirectory\n    extraContainerCmd() {\n        if [[ $# > 0 ]]; then\n            nixos-container run $name -- \"$@\" | cat;\n        else\n            nixos-container root-login $name\n        fi\n    }\n    if [[ $extraContainerSSH ]]; then\n        makeTmpDir\n        export sshKey\n        export sshControlPath=$tmpDir/ssh-connection\n        extraContainerSSHCmd() {\n            ssh -i $sshKey -o ConnectTimeout=2 \\\n                -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \\\n                -o ControlMaster=auto -o ControlPath=\"$sshControlPath\" -o ControlPersist=60 \\\n                root@$ip \"$@\"\n        }\n        export -f extraContainerSSHCmd\n\n    fi\n    extraContainerHelp() {\n        if [[ $ip ]]; then\n            echo \"Container address: $ip (\\$ip)\"\n        fi\n        echo \"Container filesystem: $containerStateDirectory/$name\"\n        echo\n        echo 'Run \"c COMMAND\" to execute a command in the container'\n        echo 'Run \"c\" to start a shell session inside the container'\n        if [[ $extraContainerSSH ]]; then\n            echo 'Run \"cssh\" for SSH'\n        fi\n    }\n    export -f extraContainerCmd extraContainerHelp\n\n    defineShortcuts='\n      function h() { extraContainerHelp; }\n      function c() { extraContainerCmd \"$@\"; }\n      export -f h c\n      if [[ $extraContainerSSH ]]; then\n        function cssh() { extraContainerSSHCmd \"$@\"; }\n        export -f cssh\n      fi\n      export name=$runningShellContainer\n      export ip=$containerIp\n    '\n\n    if [[ $runCommand ]]; then\n        eval \"$defineShortcuts\"\n        echo \"Running command.\"\n        \"${runCommand[0]}\" \"${runCommand[@]:1}\"\n    else\n        echo 'Starting shell.'\n        echo 'Enter \"h\" for documentation.'\n        # Start interactive, non-login shell.\n        # Contrary to what is stated in the manual, bash --rcfile sources\n        # /etc/profile (and consequently .bashrc) before evaluating the custom rcfile.\n        # This allows us to start a shell with prompt and aliases set up and add extra\n        # features via --rcfile.\n        bash --rcfile <(\n            # Prevent aliases from overriding the helper functions\n            echo \"unalias h c cssh 2>/dev/null; $defineShortcuts\"\n            # Use PATH from the current calling environment\n            printf 'export PATH=%q' \"$PATH\"\n        )\n    fi\n\n    # Needed when called from the SIGINT handler\n    exit 0\n}\n\n## 4. Start/restart containers\n\necho\n\nif [[ $start || $update || $restart ]]; then\n    toStart=()\n    toRestart=()\n    # systemctl is-active fails when some containers are not active\n    statuses=($(systemctl is-active $(getServiceNames ${allContainers[@]}))) || true\n    for i in ${!statuses[@]}; do\n        if [[ ${statuses[$i]} == active ]]; then\n            runningContainers+=(${allContainers[$i]})\n        else\n            toStart+=(${allContainers[$i]})\n        fi\n    done\n\n    if [[ $start && ((${#toStart[@]} != 0)) ]]; then\n        echo \"Starting containers:\"\n        printf '%s\\n' ${toStart[@]}\n        echo\n        systemctl start $(getServiceNames ${toStart[@]})\n    fi\n\n    # Update or restart changed containers that are running\n    toRestart=($(comm -12 <(printf '%s\\n' ${runningContainers[@]} | sort) \\\n                          <(printf '%s\\n' ${changedContainers[@]})))\n    if ((${#toRestart[@]} != 0)); then\n        if [[ ! $restart ]]; then\n            toUpdate=($(comm -12 <(printf '%s\\n' ${toRestart[@]}) \\\n                                 <(printf '%s\\n' ${onlySystemChangedContainers[@]})))\n            toRestart=($(comm -23 <(printf '%s\\n' ${toRestart[@]}) \\\n                                  <(printf '%s\\n' ${toUpdate[@]})))\n            if ((${#toUpdate[@]} != 0)); then\n                echo \"Updating containers:\"\n                printf '%s\\n' ${toUpdate[@]}\n                echo\n                updateContainers ${toUpdate[@]}\n            fi\n        fi\n        if ((${#toRestart[@]} != 0)); then\n            echo \"Restarting containers:\"\n            printf '%s\\n' ${toRestart[@]}\n            echo\n            restartContainers ${toRestart[@]}\n        fi\n    fi\nfi\n\nif [[ $startShellEnv ]]; then\n    handleShell\nfi\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"Run declarative NixOS containers without full system rebuilds\";\n\n  inputs.nixpkgs.url = \"github:NixOS/nixpkgs/nixos-25.11\";\n  inputs.flake-utils.url = \"github:numtide/flake-utils\";\n\n  outputs = { self, nixpkgs, flake-utils }@inputs:\n    let\n      supportedSystems = [ \"x86_64-linux\" \"i686-linux\" \"aarch64-linux\" ];\n      eachSupportedSystem = flake-utils.lib.eachSystem supportedSystems;\n      pkg = pkgs: pkgs.callPackage ./. { pkgSrc = ./.; };\n    in\n    {\n      nixosModules.default = { pkgs, ... }: {\n        environment.systemPackages = [ (pkg pkgs) ];\n        boot.extraSystemdUnitPaths = [ \"/etc/systemd-mutable/system\" ];\n      };\n\n      overlays.default = final: prev: { extra-container = pkg final; };\n\n      lib = {\n        inherit supportedSystems eachSupportedSystem;\n\n        buildContainers = {\n          system\n          , config\n          , nixpkgs ? inputs.nixpkgs\n          , legacyInstallDirs ? false\n          , addRunner ? true\n        }: let\n          containers = self.lib.evalContainers { inherit system config nixpkgs legacyInstallDirs; };\n          etc = containers.config.system.build.etc;\n          withRunner = etc.overrideAttrs (old: {\n            name = \"container\";\n            buildCommand = old.buildCommand + \"\\n\" + ''\n              install -D -m700 <(printf '${''\n                #!/usr/bin/env bash\n                if ! type -p extra-container >/dev/null; then\n                  >&2 echo \"Error: extra-container is not installed\"\n                  >&2 echo \"Docs: https://github.com/erikarvstedt/extra-container?tab=readme-ov-file#install\"\n                  exit 1\n                fi\n                EXTRA_CONTAINER_ETC=%s exec extra-container \"$@\"\n              ''}' \"$out\") $out/bin/container\n            '';\n          });\n        in\n          (if addRunner then withRunner else etc) // {\n            inherit (containers) config;\n            inherit (containers.config) containers;\n          };\n\n        evalContainers = {\n          system\n          , config\n          , nixpkgs ? inputs.nixpkgs\n          , legacyInstallDirs ? false\n        }: import ./eval-config.nix {\n          inherit\n            system\n            legacyInstallDirs;\n          nixosPath = nixpkgs + \"/nixos\";\n          systemConfig = config;\n        };\n      };\n\n    } // (eachSupportedSystem (system:\n      let\n        pkgs = import nixpkgs { inherit system; };\n        inherit (nixpkgs) lib;\n      in\n      rec {\n        packages.default = pkg pkgs;\n\n        # This dev shell allows running the `extra-container` command directly from the local\n        # source (./extra-container), for quick edit/test cycles.\n        # This only works when `nix develop` is started from the repo root directory.\n        devShells.default = let\n          # Extra PATH, as defined in ./default.nix\n          path = lib.makeBinPath (with pkgs; [\n            openssh\n          ]);\n        in pkgs.stdenv.mkDerivation {\n          name = \"shell\";\n\n          shellHook =  ''\n            PATH=\"${path}''${PATH:+:}$PATH\"\n\n            # Enable calling the local source (./extra-container) with command `extra-container`\n            PATH=\"$(realpath .):$PATH\"\n\n            # Use the pinned nixpkgs for building containers when running `extra-container`\n            export NIX_PATH=\"nixpkgs=${nixpkgs}''${NIX_PATH:+:}$NIX_PATH\"\n\n            # See comment in ./extra-container for an explanation\n            export LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive\n          '';\n        };\n\n        packages = {\n          # Run a basic extra-container test in a NixOS VM\n          test = pkgs.testers.nixosTest {\n            name = \"extra-container\";\n\n            nodes.machine = { config, ... }: {\n              imports = [ self.nixosModules.default ];\n              # memorySize = 1024 needed for evaluating the container system\n              # memorySize = 1200 needed to avoid error during boot:\n              #   'agetty[817]: failed to open credentials directory'\n              virtualisation.memorySize = 1200;\n              nix.nixPath = [ \"nixpkgs=${nixpkgs}\" ];\n              system.stateVersion = config.system.nixos.release;\n              # Pre-build the container used by testScript\n              system.extraDependencies = let\n                basicContainer = import ./eval-config.nix {\n                  nixosPath = \"${nixpkgs}/nixos\";\n                  legacyInstallDirs = false;\n                  inherit system;\n                  systemConfig = {\n                    containers.test.config.environment.etc.testFile.text = \"testSuccess\";\n                  };\n                };\n              in [ basicContainer.config.system.build.etc ];\n            };\n\n            testScript = ''\n              config = '{ containers.test.config.environment.etc.testFile.text = \"testSuccess\"; }'\n              output = machine.succeed(\n                f\"extra-container shell -E '{config}' --run c cat /etc/testFile\"\n              )\n              if not \"testSuccess\" in output:\n                print(f\"Test failed. Output:\\n{output}\")\n            '';\n          };\n\n          # Used by apps.vm\n          vm = (import \"${nixpkgs}/nixos\" {\n            inherit system;\n            configuration = { config, pkgs, lib, modulesPath, ... }: with lib; {\n              imports = [\n                self.nixosModules.default\n                \"${modulesPath}/virtualisation/qemu-vm.nix\"\n              ];\n              virtualisation.graphics = false;\n              services.getty.autologinUser = \"root\";\n              nix.nixPath = [ \"nixpkgs=${nixpkgs}\" ];\n              system.stateVersion = config.system.nixos.release;\n              documentation.enable = false;\n              # Power off VM when the user exits the shell\n              systemd.services.\"serial-getty@\".preStop = ''\n                echo o >/proc/sysrq-trigger\n              '';\n              # Pre-build a minimal container\n              system.extraDependencies = let\n                basicContainer = import ./eval-config.nix {\n                  nixosPath = \"${nixpkgs}/nixos\";\n                  legacyInstallDirs = false;\n                  inherit system;\n                  systemConfig = {};\n                };\n              in [ basicContainer.config.system.build.etc ];\n            };\n          }).config.system.build.vm;\n\n          runVM = pkgs.writers.writeBash \"run-vm\" ''\n            set -euo pipefail\n            export NIX_DISK_IMAGE=/tmp/extra-container-vm-img\n            rm -f $NIX_DISK_IMAGE\n            trap \"rm -f $NIX_DISK_IMAGE\" EXIT\n\n            export QEMU_OPTS=\"-smp $(nproc) -m 2000\"\n            ${packages.vm}/bin/run-*-vm\n          '';\n\n          debugTest = pkgs.writers.writeBash \"run-debug-test\" ''\n            set -euo pipefail\n            export TMPDIR=$(mktemp -d)\n            trap \"rm -rf $TMPDIR\" EXIT\n\n            export QEMU_OPTS=\"-smp $(nproc) -m 2000\"\n            ${packages.test.driver}/bin/nixos-test-driver <(\n              echo \"start_all(); import code; code.interact(local=globals())\"\n            )\n          '';\n\n          updateReadme = pkgs.writers.writeBash \"update-readme\" ''\n            exec ${pkgs.ruby}/bin/ruby ./util/update-readme.rb\n          '';\n        };\n\n        apps = {\n          # Run a NixOS VM where extra-container is installed\n          vm = {\n            type = \"app\";\n            program = toString packages.runVM;\n          };\n\n          # Run a Python test driver shell inside the test VM\n          debugTest = {\n            type = \"app\";\n            program = toString packages.debugTest;\n          };\n\n          updateReadme = {\n            type = \"app\";\n            program = toString packages.updateReadme;\n          };\n        };\n\n        checks = { inherit (packages) test; };\n      }\n    ));\n}\n"
  },
  {
    "path": "run-tests-in-container.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\nshopt -s nullglob\n\nscriptDir=\"$(dirname \"$(readlink -f \"$0\")\")\"\nPATH=$scriptDir:$PATH\n\ncleanup() {\n    # clean immutable files inside the container\n    for f in /var/lib/*containers/test-extra-container/var/lib/*containers/*/var/empty; do\n        chattr -i -a \"$f\"\n        rm -rf \"$f\"\n    done\n    extra-container destroy test-extra-container || true\n}\ntrap \"cleanup\" EXIT\n\ntrap \"echo \\\"Error at $(realpath ${BASH_SOURCE[0]}):\\$LINENO\\\"\" ERR\n\ncleanup\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n\nnixpkgs=$(nix-instantiate --eval -E '(toString <nixpkgs>)' | tr -d '\"')\n\nextra-container create -s <<EOF\n{ config, pkgs, lib, ... }:\n{\n  containers.test-extra-container = {\n    bindMounts.\"/extra-container\".hostPath = \"$scriptDir\";\n    bindMounts.\"/nixpkgs\".hostPath = \"$nixpkgs\";\n    config = { options, ... }: {\n      environment = {\n        systemPackages = [ pkgs.nixos-container ];\n        variables.NIX_PATH = lib.mkForce \"nixpkgs=/nixpkgs\";\n      };\n      boot = lib.optionalAttrs (options.boot ? extraSystemdUnitPaths) {\n        extraSystemdUnitPaths = [ \"/etc/systemd-mutable/system\" ];\n      };\n    };\n  };\n}\nEOF\n\necho \"Running tests\"\necho\nnixos-container run test-extra-container -- '/extra-container/test.sh'\n"
  },
  {
    "path": "test.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\nshopt -s nullglob\n\nscriptDir=\"$(dirname \"$(readlink -f \"$0\")\")\"\nPATH=$scriptDir:$PATH\n\ncleanup() {\n    set +e\n    for container in $(extra-container list | grep ^test-); do\n        extra-container destroy $container\n    done\n    set -e\n}\ntrap \"cleanup\" EXIT\n\ntrap \"echo \\\"Error at $(realpath ${BASH_SOURCE[0]}):\\$LINENO\\\"\" ERR\n\ntestMatches() {\n    actual=\"$1\"\n    expected=\"$2\"\n    if [[ $actual != $expected ]]; then\n        echo\n        echo 'Pattern does not match'\n        echo 'Expected:'\n        echo \"$expected\"\n        echo\n        echo 'Actual:'\n        echo \"$actual\"\n        echo\n        return 1\n    fi\n}\n\n# This significantly reduces eval time. Not needed for NixOS ≥ 20.03\nbaseConfig='config = { documentation.nixos.enable = false; }'\n\ncleanup\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\necho \"Test attr arg and container starting \"\n\noutput=$(extra-container create -A 'a b' --start <<EOF\n{\n  \"a b\" = { config, pkgs, ... }: {\n    containers.test-1 = {\n      $baseConfig;\n    };\n  };\n}\nEOF\n)\n\ntestMatches \"$output\" \"*Installing*test-1*Starting*test-1*\"\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\necho \"Test starting and updating\"\n\noutput=$(extra-container create -s <<EOF\n{ config, pkgs, ... }:\n{\n  containers.test-1 = {\n    $baseConfig // { environment.variables.foo = \"a\"; };\n  };\n  containers.test-2 = {\n    $baseConfig;\n  };\n}\nEOF\n)\n\ntestMatches \"$output\" \"*Starting*test-2*Updating*test-1*\"\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\necho \"Test unchanged\"\n\noutput=$(extra-container create -s <<EOF\n{ config, pkgs, ... }:\n{\n  containers.test-1 = {\n    $baseConfig // { environment.variables.foo = \"a\"; };\n  };\n}\nEOF\n)\n\ntestMatches \"$output\" \"*test-1 (unchanged, skipped)*\"\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\necho \"Test updating and restarting\"\n\noutput=$(extra-container create -u <<EOF\n{ config, pkgs, ... }:\n{\n  containers.test-1 = {\n    $baseConfig // { environment.variables.foo = \"b\"; };\n  };\n  containers.test-2 = {\n    privateNetwork = true;\n    $baseConfig;\n  };\n}\nEOF\n)\n\ntestMatches \"$output\" \"*Updating*test-1*Restarting*test-2*\"\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\necho \"Test list\"\n\noutput=$(extra-container list | grep ^test- || true)\ntestMatches \"$output\" \"test-1*test-2\"\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\necho \"Test destroy\"\n\n[[ $(echo /var/lib/*containers/test-*) ]]\ncleanup\noutput=$(extra-container list | grep ^test- || true)\ntestMatches \"$output\" \"\"\n[[ ! $(echo /var/lib/*containers/test-*) ]]\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\necho \"Test shell run\"\n\nread -d '' src <<EOF || true\n{ config, pkgs, ... }:\n{\n  containers.test-1 = {\n    $baseConfig;\n  };\n}\nEOF\noutput=$(extra-container shell -E \"$src\" --run c uname -a)\ntestMatches \"$output\" \"*Linux test*\"\n\n# Container should be destroyed after running\n[[ ! -e /var/lib/*containers/test-1 ]]\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\necho \"Test manual build\"\n\nstorePath=$(extra-container build <<EOF\n{ config, pkgs, ... }:\n{\n  containers.test-1 = {\n    $baseConfig;\n  };\n  containers.test-2 = {\n    $baseConfig;\n  };\n}\nEOF\n)\n\ntestMatches \"$storePath\" \"/nix/store/*\"\n\noutput=$(extra-container create -s $storePath)\ntestMatches \"$output\" \"*Starting*test-1*test-2*\"\n\n#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\necho \"Test destroy from container definition\"\nextra-container destroy $storePath\n[[ ! -e /var/lib/*containers/test-1 ]]\n[[ ! -e /var/lib/*containers/test-2 ]]\n\n# TODO: Add flake tests when flakes have stabilized\n"
  },
  {
    "path": "util/generate-sudoers.rb",
    "content": "# 1. Add $HOME/.nix-profile/bin to secure_path (Defaults)\n# 2. Add NIX_PATH to env_keep (Defaults)\n#\ndef make_sudoers(old_sudoers, extra_secure_paths)\n  secure_path_set = false\n\n  sudoers = old_sudoers.sub(/(Defaults\\s+secure_path\\s*=\\s*\"?)([^\"\\s]+)(.*)/) do |_|\n    secure_path_set = true\n    prefix, path, suffix = $1, $2, $3\n    paths = path.split(':')\n    extra_paths = extra_secure_paths.split(':')\n    new_path = (paths + extra_paths).uniq.join(':')\n    \"#{prefix}#{new_path}#{suffix}\"\n  end\n\n  env_keep_statement = \"Defaults\tenv_keep += NIX_PATH\"\n\n  lines_to_add = []\n  lines_to_add << %(Defaults\tsecure_path = \"#{extra_secure_paths}\") if !secure_path_set\n  lines_to_add << env_keep_statement if !old_sudoers.include?(env_keep_statement)\n\n  if !lines_to_add.empty?\n    sudoers << lines_to_add.join(\"\\n\") << \"\\n\"\n  end\n\n  sudoers == old_sudoers ? nil : sudoers\nend\n\nif __FILE__ == $0\n  puts make_sudoers(STDIN.read, ARGV.first)\nend\n"
  },
  {
    "path": "util/install.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Install extra-container on a non-NixOS systemd-based system for use with sudo.\n# Requires a multi-user nix installation, because extra-container runs nix-build as root.\n# This script is idempotent.\n\n[[ -e /run/booted-system/nixos-version ]] && isNixos=1 || isNixos=\n[[ -e /run/systemd/system ]] && hasSystemd=1 || hasSystemd=\nscriptDir=$(cd \"${BASH_SOURCE[0]%/*}\" && pwd)\n\nif [[ $EUID == 0 ]]; then\n    echo \"This script should NOT be run as root.\"\n    exit 1\nfi\nif [[ $isNixos ]]; then\n    echo \"This install script is not needed on NixOS. See the README for installation instructions.\"\n    exit 1\nfi\nif [[ ! $hasSystemd ]]; then\n    echo \"extra-container requires systemd.\"\n    exit 1\nfi\nif [[ ! -e /nix/var/nix/profiles/default ]]; then\n    echo \"extra-container requires a multi-user nix installation.\"\n    exit 1\nfi\n\n## 1. Build extra-container\ntmpDir=$(mktemp -d)\ntrap \"rm -rf $tmpDir\" EXIT\nnix-build --out-link $tmpDir/extra-container -E \"(import <nixpkgs> {}).callPackage ''$scriptDir/..'' {}\"\n\n## 2. Install to root user profile\nsudo $(type -P nix-env) -i $tmpDir/extra-container\n\n## 3. Edit /etc/sudoers to enable running extra-container via sudo\n# See ./edit-sudoers.rb for more details\nif ! type -P ruby > /dev/null; then\n    nix-build --out-link $tmpDir/ruby '<nixpkgs>' -A ruby > /dev/null\n    export PATH=\"$tmpDir/ruby/bin${PATH:+:}$PATH\"\nfi\n\nextraSecurePaths=/root/.nix-profile/bin:/nix/var/nix/profiles/default/bin\n\nnewSudoersContent=$(sudo cat /etc/sudoers | ruby \"$scriptDir\"/generate-sudoers.rb \"$extraSecurePaths\")\nif [[ $newSudoersContent ]]; then\n    echo \"$newSudoersContent\" | sudo EDITOR=\"tee\" visudo >/dev/null\nfi\n"
  },
  {
    "path": "util/update-readme.rb",
    "content": "#!/usr/bin/env ruby\n\ndef without_warnings\n  original_verbosity = $VERBOSE\n  $VERBOSE = nil\n  result = yield\n  $VERBOSE = original_verbosity\n  result\nend\n\nDir.chdir(File.join(__dir__, '..'))\nreadme = File.read('README.md')\n# hide `warning: Insecure world writable dir {extra-container-src-dir}`\nusage = without_warnings { `extra-container`.sub(/\\A.*?Usage:\\s*/m, '') }\nupdated = readme.sub(/(?<=^## Usage\\n```\\n).*?(?=^```)/m, usage)\ncurrent = File.read('README.md')\nif current != updated\n  File.write('README.md', updated)\n  puts \"Updated README.md\"\nend\n"
  }
]