liminix

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

development.adoc (23801B)


      1 = For Developers
      2 
      3 In any Nix-based system the line between "configuration"
      4 and "development" is less of a line and more of a continuum.
      5 This section covers some topics further towards the latter end.
      6 
      7 == Writing modules and services
      8 
      9 It helps here to know NixOS! Liminix uses the NixOS module
     10 infrastructure code, meaning that everything that has been written for
     11 NixOS about the syntax, the type system, and the rules for combining
     12 configuration values from different sources is just as applicable
     13 here.
     14 
     15 === Services
     16 
     17 For the most part, for common use cases, we hope that Liminix modules
     18 provide service templates for all the services you will need, and you
     19 will only have to pass the right parameters to `+build+`.
     20 
     21 But if you're reading this then our hopes are in vain.  To create a
     22 custom service of your own devising, use the [.title-ref]#oneshot# or
     23 [.title-ref]#longrun# functions:
     24 
     25 * a "longrun" service is the "normal" service concept: it has a `+run+`
     26 action which describes the process to start, and it watches that process
     27 to restart it if it exits. The process should not attempt to daemonize
     28 or "background" itself, otherwise s6-rc will think it died. Whatever it
     29 prints to standard output/standard error will be logged.
     30 
     31 [source,nix]
     32 ----
     33 config.services.cowsayd = pkgs.liminix.services.longrun {
     34   name = "cowsayd";
     35   run = "${pkgs.cowsayd}/bin/cowsayd --port 3001 --breed hereford";
     36   # don't start this until the lan interface is ready
     37   dependencies = [ config.services.lan ];
     38 }
     39 ----
     40 
     41 * a "oneshot" service doesn't have a process attached. It consists of
     42 `+up+` and `+down+` actions which are bits of shell script that are run
     43 at the appropriate points in the service lifecycle
     44 
     45 [source,nix]
     46 ----
     47 config.services.greenled = pkgs.liminix.services.oneshot {
     48   name = "greenled";
     49   up = ''
     50     echo 17 > /sys/class/gpio/export
     51     echo out > /sys/class/gpio/gpio17/direction
     52     echo 0   > /sys/class/gpio/gpio17/value
     53   '';
     54   down = ''
     55     echo 0   > /sys/class/gpio/gpio17/value
     56   '';
     57 }
     58 ----
     59 
     60 Services may have dependencies: as you see above in the `+cowsayd+`
     61 example, it depends on some service called `+config.services.lan+`,
     62 meaning that it won't be started until that other service is up.
     63 
     64 ===== Service outputs
     65 
     66 Outputs are a mechanism by which a service can provide data which may be
     67 required by other services. For example:
     68 
     69 * the DHCP client service can expect to receive nameserver address
     70 information as one of the fields in the response from the DHCP server:
     71 we provide that as an output which a dependent service for a stub name
     72 resolver can use to configure its upstream servers.
     73 * a service that creates a new network interface (e.g. ppp) will provide
     74 the name of the interface (`+ppp0+`, or `+ppp1+` or `+ppp7+`) as an
     75 output so that a dependent service can reference it to set up a route,
     76 or to configure firewall rules.
     77 
     78 A service `+myservice+` should write its outputs as files in
     79 `+/run/services/outputs/myservice+`: you can look around this directory
     80 on a running Liminix system to see how it's used currently. Usually we
     81 use the `+in_outputs+` shell function in the `+up+` or `+run+`
     82 attributes of the service:
     83 
     84 [source,shell]
     85 ----
     86 (in_outputs ${name}
     87  for i in lease mask ip router siaddr dns serverid subnet opt53 interface ; do
     88    (printenv $i || true) > $i
     89  done)
     90 ----
     91 
     92 The outputs are just files, so technically you can read them using
     93 anything that can read a file. Liminix has two "preferred" mechanisms,
     94 though:
     95 
     96 ===== One-off lookups
     97 
     98 In any context that ends up being evaluated by the shell, use `+output+`
     99 to print the value of an output
    100 
    101 [source,nix]
    102 ----
    103 services.defaultroute4 = svc.network.route.build {
    104   via = "$(output ${services.wan} address)";
    105   target = "default";
    106   dependencies = [ services.wan ];
    107 };
    108 ----
    109 
    110 ===== Continuous updates
    111 
    112 The downside of using shell functions in downstream service startup
    113 scripts is that they only run when the service starts up: if a service
    114 output _changes_, the downstream service would have to be restarted to
    115 notice the change. Sometimes this is OK but other times the downstream
    116 has no other need to restart, if it can only get its new data.
    117 
    118 For this case, there is the `+anoia.svc+` Fennel library, which allows
    119 you to write a simple loop which is iterated over whenever a service's
    120 outputs change. This code is from
    121 `+modules/dhcp6c/acquire-wan-address.fnl+`
    122 
    123 [source,fennel]
    124 ----
    125 (fn update-addresses [wan-device addresses new-addresses exec]
    126   ;; run some appropriate "ip address [add|remove]" commands
    127   )
    128 
    129 (fn run []
    130   (let [[state-directory wan-device] arg
    131         dir (svc.open state-directory)]
    132     (accumulate [addresses []
    133                  v (dir:events)]
    134       (update-addresses wan-device addresses
    135 			(or (v:output "address") []) system))))
    136 ----
    137 
    138 The `+output+` method seen here accepts a filename (relative to the
    139 service's output directory), or a directory name. It returns the first
    140 line of that file, or for directories it returns a table (Lua's
    141 key/value datastructure, similar to a hash/dictionary) of the outputs in
    142 that directory.
    143 
    144 ===== Design considerations for outputs
    145 
    146 For preference, outputs should be short and simple, and not require
    147 downstream services to do complicated parsing in order to use them.
    148 Shell commands in Liminix are run using the Busybox shell which doesn't
    149 have the niceties evel of an advanced shell like Bash, let alone those of a
    150 real programming language.
    151 
    152 Note also that the Lua `+svc+` library only reads the first line of each
    153 output.
    154 
    155 
    156 === Modules
    157 
    158 Modules in Liminix conventionally live in
    159 `+modules/somename/default.nix+`. If you want or need to write your own,
    160 you may wish to refer to the examples there in conjunction with reading
    161 this section.
    162 
    163 A module is a function that accepts `+{lib, pkgs, config, ... }+` and
    164 returns an attrset with keys `+imports, options, config+`.
    165 
    166 * `+imports+` is a list of paths to the other modules required by this
    167 one
    168 * `+options+` is a nested set of option declarations
    169 * `+config+` is a nested set of option definitions
    170 
    171 The NixOS manual section
    172 https://nixos.org/manual/nixos/stable/#sec-writing-modules[Writing NixOS
    173 Modules] is a quite comprehensive reference to writing NixOS modules,
    174 which is also mostly applicable to Liminix except that it doesn't cover
    175 service templates.
    176 
    177 
    178 ==== Service templates
    179 
    180 Although you can define services "ad hoc" using `longrun` or `oneshot`
    181 <<_writing_services,as above>>, this approach has limitations if
    182 you're writing code intended for wider use. Services in the
    183 modules bundled with Liminix are implemented following a pattern we
    184 call "service templates": functions that accept a _type-checked_
    185 attrset and return an appropriately configured service that can be
    186 assigned by the caller to a key in ``config.services``.
    187 
    188 To expose a service template in a module, it needs the following:
    189 
    190 * an option declaration for `+system.service.myservicename+` with the
    191 type of `+liminix.lib.types.serviceDefn+`
    192 
    193 [source,nix]
    194 ----
    195 options = {
    196   system.service.cowsay = mkOption {
    197     type = liminix.lib.types.serviceDefn;
    198   };
    199 };
    200 ----
    201 
    202 * an option definition for the same key, which specifies where to import
    203 the service template from (often `+./service.nix+`) and the types of its
    204 parameters.
    205 
    206 [source,nix]
    207 ----
    208 config.system.service.cowsay = config.system.callService ./service.nix {
    209   address = mkOption {
    210     type = types.str;
    211     default = "0.0.0.0";
    212     description = "Listen on specified address";
    213     example = "127.0.0.1";
    214   };
    215   port = mkOption {
    216     type = types.port;
    217     default = 22;
    218     description = "Listen on specified TCP port";
    219   };
    220   breed = mkOption {
    221     type = types.str;
    222     default = "British Friesian"
    223     description = "Breed of the cow";
    224   };
    225 };
    226 ----
    227 
    228 Then you need to provide the service template itself, probably in
    229 `+./service.nix+`:
    230 
    231 [source,nix]
    232 ----
    233 {
    234   # any nixpkgs package can be named here
    235   liminix
    236 , cowsayd
    237 , serviceFns
    238 , lib
    239 }:
    240 # these are the parameters declared in the callService invocation
    241 { address, port, breed} :
    242 let
    243   inherit (liminix.services) longrun;
    244   inherit (lib.strings) escapeShellArg;
    245 in longrun {
    246   name = "cowsayd";
    247   run = "${cowsayd}/bin/cowsayd --address ${address} --port ${builtins.toString port} --breed ${escapeShellArg breed}";
    248 }
    249 ----
    250 
    251 TIP: Not relevant to module-based services specifically, but a common gotcha
    252 when specifiying services is forgetting to transform "rich" parameter
    253 values into text when composing a command for the shell to execute. Note
    254 here that the port number, an integer, is stringified with `+toString+`,
    255 and the name of the breed, which may contain spaces, is escaped with
    256 `+escapeShellArg+`
    257 
    258 === Types
    259 
    260 All of the NixOS module types are available in Liminix. These
    261 Liminix-specific types also exist in `+pkgs.liminix.lib.types+`:
    262 
    263 * `+service+`: an s6-rc service
    264 * `+interface+`: an s6-rc service which specifies a network interface
    265 * `+serviceDefn+`: a service "template" definition
    266 
    267 In the future it is likely that we will extend this to include other
    268 useful types in the networking domain: for example; IP address, network
    269 prefix or netmask, protocol family and others as we find them.
    270 
    271 
    272 === Emulated devices
    273 
    274 Unless your changes depend on particular hardware devices, you may
    275 want to test your new/changed module with one of the emulated
    276 "devices" which runn on your build machine using the free
    277 http://www.qemu.org[QEMU machine emulator]. They are
    278 
    279 * `qemu`(MIPS)
    280 * `qemu-armv7l`(32 bit ARM)
    281 * `qemu-aarch64` (64 bit ARM)
    282 
    283 This means you don't need to keep flashing or messing with U-Boot: it
    284 also enables testing against emulated network peers using
    285 https://wiki.qemu.org/Documentation/Networking#Socket[QEMU socket
    286 networking], which may be preferable to letting Liminix loose on your
    287 actual LAN. To build,
    288 
    289 [source,console]
    290 ----
    291 nix-build -I liminix-config=path/to/your/configuration.nix --arg device "import ./devices/qemu" -A outputs.default
    292 ----
    293 
    294 This creates a `+result/+` directory containing a `+vmlinux+` and a
    295 `+rootfs+`, and a shell script `+run.sh+` which invokes QEMU to run
    296 that kernel with that filesystem. It connects the Liminix serial console
    297 and the https://www.qemu.org/docs/master/system/monitor.html[QEMU
    298 monitor] to stdin/stdout. Use `^P` (not `^A`) to switch to the monitor.
    299 
    300 // FIXME should add a `connect.sh` script instead of requiring nix-shell here
    301 
    302 If you run with `+--background /path/to/some/directory+` as the first
    303 parameter, it will fork into the background and open Unix sockets in
    304 that directory for console and monitor. Use `+nix-shell --run
    305 connect-vm+` to connect to either of these sockets, and ^O to
    306 disconnect.
    307 
    308 [[qemu-networking]]
    309 ===== Networking
    310 
    311 VMs can network with each other using QEMU socket networking. We observe
    312 these conventions, so that we can run multiple emulated instances and
    313 have them wired up to each other in the right way:
    314 
    315 * multicast 230.0.0.1:1234 : access (interconnect between router and
    316 "isp")
    317 * multicast 230.0.0.1:1235 : lan
    318 * multicast 230.0.0.1:1236 : world (the internet)
    319 
    320 Any VM started by a `+run.sh+` script is connected to "lan" and
    321 "access". The emulated upstream (see below) runs PPPoE and is
    322 connected to "access" and "world".
    323 
    324 ===== Upstream connection
    325 
    326 In pkgs/routeros there is a derivation to install and configure
    327 https://mikrotik.com/software[Mikrotik RouterOS] as a PPPoE access
    328 concentrator connected to the `+access+` and `+world+` networks, so that
    329 Liminix PPPoE client support can be tested without actual hardware.
    330 
    331 This is made available as the `+routeros+` command in `+buildEnv+`, so
    332 you can do something like:
    333 
    334 ....
    335 mkdir ros-sockets
    336 nix-shell
    337 nix-shell$ routeros ros-sockets
    338 nix-shell$ connect-vm ./ros-sockets/console
    339 ....
    340 
    341 to start it and connect to it. Note that by default it runs in the
    342 background. It is connected to "access" and "world" virtual networks and
    343 runs a PPPoE service on "access" - so a Liminix VM with a PPPOE client
    344 can connect to it and thus reach the virtual internet. [ check, but
    345 pretty sure this is not the actual internet ]
    346 
    347 [.title-ref]#Liminix does not provide RouterOS licences and it is your
    348 own responsibility if you use this to ensure you're compliant with the
    349 terms of Mikrotik's licencing. It may be supplemented or replaced in
    350 time with configurations for RP-PPPoE and/or Accel PPP.#
    351 
    352 == Hardware hacking/porting to new device
    353 
    354 The steps to port to a new hardware device are largely undocumented at
    355 present (although this hasn't stopped people from figuring it out
    356 already). As an outline I would recommend
    357 
    358 * choose hardware that OpenWrt already supports, otherwise you will
    359 probably spend a lot of time writing kernel code.  The OpenWrt kernel
    360 supports many network interfaces and other hardware for a lot of
    361 hardware boards that might only just about be able to boot Linux on a
    362 serial port if you stick to mainline Linux
    363 
    364 * work out how to get a serial console on it. You are unlikely to get
    365   working networking on your first go at boulding a kernel
    366 
    367 * find the most similar device in Liminiux and copy
    368   `devices/existing-similar-device` to `devices/cool-new-device` as a
    369   starting point
    370 
    371 * use the kernel configuration (`/proc/config.gz`) from OpenWrt as a
    372  reference for the kernel config you'll need to specify
    373  in `devices/cool-new-device/default.nix`
    374 
    375 * break it down into achieveable goals. Your first goal should be
    376   something that can TFTP boot the kernel as far as a running
    377   userland. Networking is harder, Wifi often much harder - it
    378   sometimes also depends on having working flash _even if_ you're TFTP
    379   booting because the driver expects to load wifi firmware or
    380   calibration data from the flash
    381 
    382 * ask on IRC!
    383 
    384 === TFTP
    385 
    386 [[tftpserver]]
    387 How you get your image onto hardware will vary according to the device,
    388 but is likely to involve taking it apart to add wires to serial console
    389 pads/headers, then using U-Boot to fetch images over TFTP. The OpenWrt
    390 documentation has a
    391 https://openwrt.org/docs/techref/hardware/port.serial[good explanation]
    392 of what you may expect to find on the device.
    393 
    394 [[tufted]]
    395 `tufted` is a rudimentary TFTP server which runs from the command
    396 line, has an allowlist for client connections, and follows symlinks,
    397 so you can have your device download images direct from the
    398 `+./result+` directory without exposing `+/nix/store/+` to the
    399 internet or mucking about copying files to `+/tftproot+`. If the
    400 permitted device is to be given the IP address 192.168.8.251 you might
    401 do something like this:
    402 
    403 [source,console]
    404 ----
    405 nix-shell --run "tufted -a 192.168.8.251 result"
    406 ----
    407 
    408 Now add the device and server IP addresses to your configuration:
    409 
    410 [source,nix]
    411 ----
    412 boot.tftp = {
    413   serverip = "192.168.8.111";
    414   ipaddr = "192.168.8.251";
    415 };
    416 ----
    417 
    418 and then build the derivation for `+outputs.default+` or
    419 `+outputs.mtdimage+` (for which it will be an alias on any device where
    420 this is applicable). You should find it has created
    421 
    422 * `+result/firmware.bin+` which is the file you are going to flash
    423 * `+result/flash.scr+` which is a set of instructions to U-Boot to
    424 download the image and write it to flash after erasing the appropriate
    425 flash partition.
    426 
    427 NOTE: TTL serial connections typically have no form of flow control and so
    428 don't always like having massive chunks of text pasted into them - and
    429 U-Boot may drop characters while it's busy. So don't necessarily expect
    430 to copy-paste the whole of `+boot.scr+` into a terminal emulator and
    431 have it work just like that. You may need to paste each line one at a
    432 time, or even retype it.
    433 
    434 === Running from RAM
    435 
    436 For a faster edit-compile-test cycle, you can build a TFTP-bootable
    437 image which boots directly from RAM (using phram) instead of needing
    438 to be flashed first. In your device configuration add
    439 
    440 [source,nix]
    441 ----
    442 imports = [
    443   ./modules/tftpboot.nix
    444 ];
    445 ----
    446 
    447 and then build `+outputs.tftpboot+`. This creates a file `+result/boot.scr+`, which you can copy and paste into U-Boot to
    448 transfer the kernel and filesystem over TFTP and boot the kernel from
    449 RAM.
    450 
    451 [[bng]]
    452 === Networking
    453 
    454 You probably don't want to be testing a device that might serve DHCP,
    455 DNS and routing protocols on the same LAN as you (or your colleagues,
    456 employees, or family) are using for anything else, because it will
    457 interfere. You also might want to test the device against an "upstream"
    458 connection without having to unplug your regular home router from the
    459 internet so you can borrow the cable/fibre/DSL.
    460 
    461 `+bordervm+` is included for this purpose. You will need
    462 
    463 * a Linux machine with a spare (PCI or USB) ethernet device which you
    464 can dedicate to Liminix
    465 * an L2TP service such as https://www.aa.net.uk/broadband/l2tp-service/
    466 
    467 You need to "hide" the Ethernet device from the host so that QEMU has
    468 exclusive use of it. For PCI this means configuring it for VFIO
    469 passthru; for USB you need to unload the module(s) it uses. I have
    470 this segment in my build machine's `configuration.nix` which you may
    471 be able to adapt:
    472 
    473 [source,nix]
    474 ----
    475 boot = {
    476   kernelParams = [ "intel_iommu=on" ];
    477   kernelModules = [
    478     "kvm-intel" "vfio_virqfd" "vfio_pci" "vfio_iommu_type1" "vfio"
    479   ];
    480 
    481   postBootCommands = ''
    482     # modprobe -i vfio-pci
    483     # echo vfio-pci > /sys/bus/pci/devices/0000:01:00.0/driver_override
    484   '';
    485   blacklistedKernelModules = [
    486     "r8153_ecm" "cdc_ether"
    487   ];
    488 };
    489 services.udev.extraRules = ''
    490   SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="8153", OWNER="dan"
    491 '';
    492 ----
    493 
    494 Then you can execute `+run-border-vm+` in a `+buildEnv+` shell, which
    495 starts up QEMU using the NixOS configuration in
    496 `+bordervm-configuration.nix+`.
    497 
    498 Inside the VM
    499 
    500 * your Liminix checkout is mounted under `+/home/liminix/liminix+`
    501 * TFTP is listening on the ethernet device and serving
    502 `+/home/liminix/liminix+`. The server IP address is 10.0.0.1
    503 * a PPPOE-L2TP relay is running on the same ethernet card. When the
    504 connected Liminix device makes PPPoE requests, the relay spawns L2TPv2
    505 Access Concentrator sessions to your specified L2TP LNS. Note that
    506 authentication is expected at the PPP layer not the L2TP layer, so the
    507 PAP/CHAP credentials provided by your L2TP service can be configured
    508 into your test device - bordervm doesn't need to know about them.
    509 
    510 To configure bordervm, you need a file called `+bordervm.conf.nix+`
    511 which you can create by copying and appropriately editing
    512 `+bordervm.conf-example.nix+`
    513 
    514 NOTE: If you make changes to the bordervm configuration after executing
    515 `+run-border-vm+`, you need to remove the `+border.qcow2+` disk image
    516 file otherwise the changes won't get picked up.
    517 
    518 == Contributing
    519 
    520 Patches welcome! Also bug reports, documentation improvements,
    521 experience reports/case studies etc etc all equally as welcome.
    522 
    523 * if you have an obvious bug fix, new package, documentation
    524   improvement or other uncontroversial small patch, send it straight
    525   in.
    526 
    527 * if you have a large new feature or design change in mind, please
    528   please _get in touch_ to talk about it before you commit time to
    529   implementing it. Perhaps it isn't what we were expecting, almost
    530   certainly we will have ideas or advice on what it should do or how
    531   it should be done.
    532 
    533 Liminix development is not tied to Github or any other particular
    534 forge. How to send changes:
    535 
    536 1. Push your Liminix repo with your changes to a git repository
    537 somewhere on the Internet that I can clone from. It can be on Codeberg
    538 or Gitlab or Sourcehut or Forgejo or Gitea or Github or a bare repo in
    539 your own personal web space or any kind of hosting you like.
    540 
    541 2. Email <<_mailing_lists,devel@liminix.org>> with the URL of the repo
    542 and the branch name, and we will take a look.
    543 
    544 If that's not an option, I’m also happy for you to send your changes
    545 direct to the list itself, as an incremental git bundle or using git
    546 format-patch. We'll work it out somehow.
    547 
    548 The main development repo for Liminix is hosted at
    549 <https://gti.telent.net/dan/liminix>, with a read-only mirror at
    550 <https://github.com/telent/liminix>. If you're happy to use Github
    551 then you can fork from the latter to make your changes, but please use
    552 the mailing list one of the approved routes to tell me about your changes because I don't regularly go there to check PRs.
    553 
    554 Remember that the <<_code_of_conduct>> applies to all Liminix spaces,
    555 and anyone who violates it may be sanctioned or expelled from these
    556 spaces at the discretion of the project leadership.
    557 
    558 === Nix language style
    559 
    560 This section describes some Nix language style points that we attempt to
    561 adhere to in this repo. Some are more aspirational than actual.
    562 
    563 * indentation and style is according to `nixfmt-rfc-style`
    564 * favour `+callPackage+` over raw `+import+` for calling derivations or
    565 any function that may generate one - any code that might need `+pkgs+`
    566 or parts of it.
    567 * prefer `+let inherit (quark) up down strange charm+` over
    568 `+with quark+`, in any context where the scope is more than a single
    569 expression or there is more than one reference to `+up+`, `+down+` etc.
    570 `+with pkgs; [ foo bar baz]+` is OK,
    571 `+with lib; stdenv.mkDerivation { ... }+` is usually not.
    572 * `+<liminix>+` is defined only when running tests, so don't refer to it
    573 in "application" code
    574 * the parameters to a derivation are sorted alphabetically, except for
    575 `+lib+`, `+stdenv+` and maybe other non-package "special cases"
    576 * where a `+let+` form defines multiple names, put a newline after the
    577 token `+let+`, and indent each name two characters
    578 * to decide whether some code should be a package or a module? Packages
    579 are self-contained - they live in `+/nix/store/eeeeeee-name+` and don't
    580 directly change system behaviour by their presence or absense. modules
    581 can add to `+/etc+` or `+/bin+` or other global state, create services,
    582 all that side-effecty stuff. Generally it should be a package unless it
    583 can't be.
    584 
    585 === AI policy
    586 
    587 The following is not allowed in Liminix:
    588 
    589 * Submitting contributions fully or in part created by generative AI tools
    590 
    591 * Recommending generative AI tools to other community members for solving problems in the Liminix space.
    592 
    593 That's it, there is no nuance.  if you would like to understand this position
    594 further, I suggest reading the https://docs.postmarketos.org/policies-and-processes/development/ai-policy.html[postmarketOS AI Policy] from which that text was borrowed.
    595 
    596 
    597 === Copyright
    598 
    599 The Nix code in Liminix is MIT-licenced (same as Nixpkgs), but the code
    600 it combines from other places (e.g. Linux, OpenWrt) may have a variety
    601 of licences. Copyright assignment is not expected:
    602 just like when submitting to the Linux kernel you retain the copyright
    603 on the code you contribute.
    604 
    605 === Automated builds
    606 
    607 Automated builds are run on each push to the main branch. This tests
    608 that (among other things)
    609 
    610 * every device image builds
    611 * the build for the “qemu” target is executed with a fake network upstream to test
    612   * PPPoE and DHCP service
    613   * hostap (wireless gateway)
    614 
    615 You can view the build output at https://build.liminix.org . The tests
    616 are defined in ci.nix.
    617 
    618 Unfortunately there's no (easy) way I can make _my_ CI infrastructure
    619 run _your_ code, other than merging it. But see <<_running_tests>>
    620 for how to exercise the same code locally on your machine.
    621 
    622 
    623 == Running tests
    624 
    625 You can run all of the tests by evaluating `+ci.nix+`, which is the
    626 input I use in Hydra.
    627 
    628 [source,console]
    629 ----
    630 nix-build -I liminix=`pwd`  ci.nix -A pppoe # run one job
    631 nix-build -I liminix=`pwd`  ci.nix -A all # run all jobs
    632 ----
    633 
    634 == Troubleshooting
    635 
    636 === Diagnosing unexpectedly large images
    637 
    638 Sometimes you can add a package and it causes the image size to balloon
    639 because it has dependencies on other things you didn't know about. Build
    640 the `+outputs.manifest+` attribute, which is a JSON representation of
    641 the filesystem, and you can run `+nix-store --query+` on it.
    642 
    643 [source,console]
    644 ----
    645 nix-build -I liminix-config=path/to/your/configuration.nix \
    646   --arg device "import ./devices/qemu" -A outputs.manifest \
    647   -o manifest
    648 nix-store -q --tree manifest
    649 ----