# Defining new properties

Defining new properties is like defining new functions: `DEFPROP`, `DEFPROPLIST` and `DEFPROPSPEC` are more like `DEFUN` than anything else. Here is a guide to these three macros; in particular, why you might need to move from the basic `DEFPROPLIST` to either `DEFPROPSPEC` or `DEFPROP`.

## Tutorial conventions

In these tutorials we assume that you have a workstation called `laptop.example.com` where you run the root Lisp. We also assume that Consfigurator knows about your laptop, and that it has a host deployment specified, so that you can use `HOSTDEPLOY-THESE` to deploy properties to the laptop as root. For example,:

    (defhost laptop.example.com
        (:deploy ((:sudo :as "spwhitton@laptop.example.com") :sbcl))
      "Sean's laptop."
      (os:debian-stable "bullseye" :amd64))

We suppose that you’ve already set up sources of prerequisite data to provide sudo passwords and the like. See the introduction if you haven’t set this up yet.

## `DEFPROPLIST` and `DEFPROPSPEC`

These macros allow you to define properties by combining existing properties. Most user properties in consfigs should be defineable using one of these: you should not need to resort to `DEFPROP`. And indeed, most new properties to be added to Consfigurator itself should not need to use `DEFPROP` either.

### `DEFPROPLIST`

`DEFPROPLIST` allows you to define properties very similarly to how you define hosts with `DEFHOST`. You simply supply an unevaluated property application specification. Compare:

    (defproplist setup-test-pure-wayland ()
      "Set up system for testing programs' support for running Wayland-native."
      (apt:installed "sway")
      (apt:removed "xwayland")
      (systemd:disabled "lightdm"))

    (defhost laptop.example.com ()
      (os:debian-stable "bullseye" :amd64)
      #| ... |#
      (setup-test-pure-wayland)
      ;; Previously we had these; now factored out:
      ;; (apt:installed "sway")
      ;; (apt:removed "xwayland")
      ;; (systemd:disabled "lightdm")
      #| ... |#)

You can use parameters just like with plain functions:

    (defproplist setup-test-pure-wayland (compositor)
      "Set up system for testing programs' support for running Wayland-native."
      (apt:installed compositor)
      (apt:removed "xwayland")
      (systemd:disabled "lightdm"))

    (defhost laptop.example.com ()
      (os:debian-stable "bullseye" :amd64)
      #| ... |#
      (setup-test-pure-wayland "sway")
      #| ... #|)

To compute intermediate values, you can use `&aux` parameters. Be aware that code for `&optional` and `&key` default values, and for `&aux` parameters, may be executed more than once per application of the property, so it should usually be side effect-free.

### `DEFPROPSPEC`

Unevaluated property application specifications are not always as expressive as is required. For example, what if, in our example, we want to allow the user to supply a list of packages to install, of arbitrary length? For this we need `DEFPROPSPEC`. The body of this macro is ordinary Lisp code which should return a property application specification. In most cases all you need is a single backquote expression:

    (defpropspec setup-test-pure-wayland :posix (&rest compositor-packages)
      "Set up system for testing programs' support for running Wayland-native."
      `(eseqprops
        (apt:installed ,@compositor-packages)
        (apt:removed "xwayland")
        (systemd:disabled "lightdm")))

    (defhost laptop.example.com ()
      (os:debian-stable "bullseye" :amd64)
      #| ... |#
      (setup-test-pure-wayland "sway" "swayidle" "swaylock")
      #| ... #|)

Use this basic shape, with `ESEQPROPS`, if you want `DEFPROPLIST` with just a little more expressive power – `DEFPROPLIST` has an implicit `ESEQPROPS`.

If you want to include elements of the property application specification conditionally, you will need `DEFPROPSPEC`. For example, perhaps disabling lightdm is not appropriate on all hosts to which we want to apply this property, because not all of them have it installed. So we might use:

    (defpropspec setup-test-pure-wayland :posix (lightdmp &rest compositor-packages)
      "Set up system for testing programs' support for running Wayland-native."
      `(eseqprops
        (apt:installed ,@compositor-packages)
        (apt:removed "xwayland")
        ,@(and lightdmp '((systemd:disabled "lightdm")))))

    (defhost laptop.example.com ()
      (os:debian-stable "bullseye" :amd64)
      #| ... |#
      (setup-test-pure-wayland t "sway" "swayidle" "swaylock")
      #| ... #|)

The expression `,@(and x '((y)))` (or `,@(and x `((,y)))`) is a Lisp idiom for conditional inclusion of sublists of backquoted lists.

One disadvantage of moving to `DEFPROPSPEC` (aside from just being less declarative) is that you can’t use unevaluated property application specification-specific features, such as dotted propapp notation, directly within backquote expressions. This won’t work:

    (defpropspec setup-... :lisp (...)
      `(eseqprops
        #| ... |#
        ;; won't work
        (chroot:os-bootstrapped. nil "/srv/chroot/unstable-amd64"
          (os:debian-unstable :amd64)
          (apt:installed "build-essential"))
        #| ... |#))

However, use of the `PROPAPP` macro makes it possible to temporarily switch back to something more like `DEFPROPLIST`:

    (defpropspec setup-... :lisp (...)
      `(eseqprops
        #| ... |#
        ,(propapp (chroot:os-bootstrapped. nil "/srv/chroot/unstable-amd64"
                    (os:debian-unstable :amd64)
                    (apt:installed "build-essential")))
        #| ... |#))

In all these examples, the body of the `DEFPROPSPEC` has been a single form. Sometimes you will need to wrap binding forms around this, or precede it with other forms, to compute the propspec expression. In some cases you might not use backquote at all; see `INSTALLER:BOOTLOADER-BINARIES-INSTALLED` for an example.

Note that arguments to property applications within backquotes are not evaluated. Whereas you might use:

    (file:contains-lines "/etc/foo" '("bar" "baz"))

within `DEFPROPLIST`, within backquotes within `DEFPROPSPEC` you would need:

    (file:contains-lines "/etc/foo" ("bar" "baz"))

## DEFPROP

Returning to our `SETUP-TEST-PURE-WAYLAND` example, it’s not great that the user has to supply a parameter specifying whether or not lightdm needs to be disabled. We should just check whether lightdm is installed, and disable it only if it’s there (some sort of check is necessary because `SYSTEMD:DISABLED` will fail if there is no service to disable).

`DEFPROP` is more fundamental than both `DEFPROPLIST` and `DEFPROPSPEC`: it allows you to supply arbitrary code for each of the property’s subroutines (see Property subroutines, below). In our example, the crucial difference is that `DEFPROPLIST` and `DEFPROPSPEC` permit you to run code only before any connection to the host is established, which, of course, is too early to check whether lightdm is installed. We need to run the check at `:APPLY` time:

    (defprop %no-lightdm :posix ()
      (:hostattrs (os:required 'os:debianlike))
      (:apply (if (apt:all-installed-p "lightdm")
                  (systemd:disabled "lightdm"))
                  :no-change))

    (defpropspec setup-test-pure-wayland :posix (&rest compositor-packages)
      "Set up system for testing programs' support for running Wayland-native."
      `(eseqprops
        (apt:installed ,@compositor-packages)
        (apt:removed "xwayland")
        (%no-lightdm)))

Here, we can call the ordinary function `APT:ALL-INSTALLED-P` to examine the actual state of the host. We have had to introduce two complexities to account for several implicit features of `DEFPROPLIST` and `DEFPROPSPEC`. Firstly, we have to specify that the property is only applicable to Debian-like hosts (in this case we could get away with not doing that because we apply `%NO-LIGHTDM` only within a `DEFPROPSPEC` that has other properties which are applicable only to Debian-like hosts, but that’s highly contingent). And secondly, we have to take care to return `:NO-CHANGE` in the case that lightdm is not installed. Both of these things are taken care of for us with `DEFPROPLIST` and `DEFPROPSPEC`. Nevertheless, if you need to examine the actual state of the host, only `DEFPROP` will do.

Finally, note how we keep the rest of `SETUP-TEST-PURE-WAYLAND` in a `DEFPROPSPEC`, only dropping down to `DEFPROP` for the part that requires it. This is good practice.