Dependencies

Single units can be useful, but the goal of Sysunit is to allow them to be easily composed to express complex desired system states. This is mostly accomplished by defining dependencies that your unit requires.

Let's use our package installation unit from the previous section, and invoke it with various arguments to set up a development environment for writing Python:

# dev_env.sh

deps() {
    dep pkg.sh name=git
    dep pkg.sh name=python3
    dep pkg.sh name=tmux
}

When we run this with sysunit apply dev_env.sh, sysunit will use the deps hook to determine that before dev_env.sh can be considered applied, three instances of the pkg.sh unit must be applied first, each with the provided name argument. Each of these dependencies will have their check hook run before applying, and apply will only be run if it does not indicate that the unit is already applied.

Note that this unit does not provide an apply hook, so Sysunit will do nothing by default after the dependencies are satisfied, units like this are only used as a sort of manifest to define a set of units that should be applied together.

Unit Path

By default, Sysunit will look for units in the current working directory, but you can specify colon-separated paths in which units can be found either with the --path or p flag, or by setting SYSUNIT_PATH in your environment.

Dependency Resolution

Unit instances are resolved into a Directed Acyclic Graph (DAG) before execution. A unit instance is differentiated by a combination of its target (explained in another section) and its arguments, and an instance is executed only once per sysunit invocation. Let's see how this works in practice:

#foo_file.sh

deps() { dep scratch_dir.sh };

check() {
    if [ -f /tmp/scratch/foo ]; then
        present
    fi
}

apply() echo "helloooo" > /tmp/scratch/foo;
#cat_pic.sh

check() {
    if [ -f /tmp/scratch/cat.jpg ]; then
        present
    fi
}

apply() curl -o /tmp/scratch/cat.jpg https://placekitten.com/200/300
#scratch_dir.sh

check() {
    if [ -d /tmp/scratch ]; then
        present
    fi
}

apply() mkdir -p /tmp/scratch
#project_files.sh

deps() {
    dep foo_file.sh
    dep cat_pic.sh
}

When we run sysunit apply project_files.sh, Sysunit will run the units in this order:

  • scratch_dir.sh
  • foo_file.sh
  • cat_pic.sh
  • project_files.sh

Note that scratch_dir is only run once, even though two units include it in their deps.

Dynamic Dependencies

When we define parameters that our unit can accept, they are injected prior to running the deps hook. This allows us to define dependencies that are based on parameters of our unit:

#cat_pic.sh

meta() {
    params !url:string
}

deps() {
    if $url | grep -q '^ssh'; then
       dep pkg.sh name=scp 
    else
       dep pkg.sh name=curl
    fi
}

apply() {
    if $url | grep -q '^ssh'; then
        scp $url /tmp/scratch/cat.jpg
    else
        curl -o /tmp/scratch/cat.jpg $url
    fi
}

As with all other hooks, the deps hook can run any arbitrary shell code, and is executed on the target system

Unlike Metadata, deps are intended to be dynamic, and that capability is very powerful. However, some users may not desire the lack of determinism in knowing which dependencies will be run when they invoke a unit. The Execution Plan output intends to provide clarity on what dynamic units have been selected, but use of this feature can be avoided for those who don't desire it.

Dynamic Parameters

We can also pass our dynamic parameters directly down to our dependencies:

#foo_file.sh

meta() {
    params !directory:string
}

deps() {
    dep dir.sh directory="$directory"
}

apply() {
    touch $directory/foo
}
#directory.sh

meta() {
    params !directory:string
}

apply() mkdir -p $directory;

Running this unit with sysunit apply foo_file.sh --arg directory=/tmp/ will first create the /tmp/ directory, and then create the foo file in it.