├── .gitattributes ├── .gitignore ├── Makefile ├── README.md ├── install.sh ├── man ├── ik-update-release.1.md ├── ik.1.md ├── index.ini ├── isolatekit.7.md ├── isolatekit.index.7.md ├── isolatekit.rc.5.md └── isolatekit.tutorial.7.md ├── misc ├── com.refi64.isolatekit.policy ├── ik-update-release └── isolatekit-tmpfiles.conf ├── share └── isolatekit │ ├── bin │ ├── aria2c │ ├── bsdtar │ ├── pv │ └── rc │ ├── sbin │ ├── ikextract │ └── ikget │ └── scripts │ ├── createunit.rc │ ├── queryunit.rc │ ├── rununit.rc │ └── utils.rc ├── src └── main.vala └── units ├── alpine.rc ├── alpine ├── sdk.rc └── sdk │ ├── gnome-static.rc │ └── gnome.rc ├── build ├── isolatekit.rc └── isolatekit │ ├── aria2c.rc │ ├── bsdtar.rc │ ├── pv.rc │ └── rc.rc ├── centos.rc ├── fedora.rc └── ubuntu.rc /.gitattributes: -------------------------------------------------------------------------------- 1 | share/isolatekit/bin/* filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | !share/isolatekit/bin/ 3 | man/out/ 4 | man/html/ 5 | config.mk 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | $(shell mkdir -p bin man/out man/html) 2 | 3 | VALAC=valac 4 | override VALAFLAGS += \ 5 | --pkg gio-2.0 \ 6 | --pkg linux \ 7 | --pkg posix \ 8 | --pkg gee-0.8 \ 9 | -X -D_GNU_SOURCE \ 10 | -g 11 | 12 | MRKD=mrkd 13 | 14 | DESTDIR= 15 | PREFIX=/usr 16 | 17 | -include config.mk 18 | 19 | export CC 20 | 21 | override MAN=$(patsubst man/%.md,man/out/%,$(wildcard man/*.md)) 22 | override HTML=$(patsubst man/%.md,man/html/%.html,$(wildcard man/*.md)) 23 | 24 | .PHONY: all c clean install man 25 | 26 | all: bin/ik 27 | 28 | bin/ik: src/*.vala 29 | $(VALAC) $(VALAFLAGS) -o $@ $< 30 | 31 | c: 32 | rm -rf bin/*.vala 33 | cp src/*.vala bin 34 | $(VALAC) $(VALAFLAGS) -C bin/*.vala 35 | 36 | man: $(MAN) 37 | html: $(HTML) 38 | 39 | $(MAN): man/out/%: man/%.md 40 | $(MRKD) -index man/index.ini $^ $@ 41 | 42 | $(HTML): man/html/%.html: man/%.md 43 | $(MRKD) -index man/index.ini -format html $^ $@ 44 | 45 | clean: 46 | rm -rf bin man/out man/html 47 | 48 | install: bin/ik 49 | @sh install.sh $(DESTDIR)$(PREFIX) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IsolateKit 2 | 3 | ## What is IsolateKit? 4 | 5 | IsolateKit is a lightweight Docker/rkt/containerization alternative, designed 6 | specifically for creating reproducible development environment, vs Docker and rkt's 7 | general-purpose usage. 8 | 9 | ## What's wrong with Docker? 10 | 11 | Docker is great, but it wasn't designed for containers. For instance: 12 | 13 | - The only way to use multiple images at once is through multi-stage builds, which, 14 | though great, make it difficult to install multiple dependencies. Have a project that 15 | needs to be built on CentOS and depends on devtoolset-7, Python, and Ruby in one 16 | build stage? Tough luck; you'll have to pick one image and then install everything 17 | else onto it manually. 18 | - It's a bit unintuitive to run an image directly using a temporary container. Want to 19 | run the Alpine image just once? Try `docker run -t -i --rm alpine ash`. Ouch. 20 | - The new mount syntax makes bind mounts (which is what you'll almost always be using 21 | for building binaries) painful: `--mount type=bind,source=xyz,destination=xyz`. 22 | - You can only depend on already-built images, not unbuilt Dockerfiles. This makes 23 | creating multiple images harder than it needs to be. 24 | 25 | ## What's wrong with rkt? 26 | 27 | - acbuild combines everything wrong with state machines and everything wrong with 28 | declarative build formats into one. 29 | - AppC is dead, and rkt doesn't support OCI yet. Oh, as as a result of OCI, 30 | acbuild is unmaintained. 31 | 32 | ## What's wrong with Rootbox? 33 | 34 | [Rootbox](https://project-rootbox.github.io), IsolateKit's predecessor, was great, but 35 | it had a lot of problems: 36 | 37 | - I wrote it in Bash. Seemed like a good idea at the time, ended up being an epic 38 | disaster. 39 | - There was no concept of proper dependency management, so creating new boxes required 40 | running all of the factories it depended on...even if there was no need. 41 | - It was hard-coded to Alpine Linux and wasn't designed in a way to make it easily 42 | extensible. Alpine is great for building static binaries, but you can't use it to 43 | build other things like AppImages. 44 | 45 | ## IsolateKit Highlights 46 | 47 | - Lightweight. GLib is the only runtime dependency, and Vala is the only built-time 48 | dependency. 49 | - Built on top of systemd-nspawn. 50 | - Designed to make generating environments from source scripts insanely easy. IsolateKit 51 | in its current state isn't really designed around using binary images. 52 | 53 | ## Examples 54 | 55 | ```bash 56 | # Create a new target that depends on the Alpine unit. 57 | $ ik target set test -a ik:alpine.rc 58 | # Run the new target. 59 | $ ik target run test 60 | # Run the new target, but also add the SDK unit. 61 | $ ik target run test -a ik:alpine/sdk.rc 62 | # Run the Alpine unit, but with no target. 63 | # This will discard any changes made (think docker run --rm). 64 | $ ik target run null -a ik:alpine.rc,alpine/sdk.rc 65 | ``` 66 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | PREFIX="$1" 6 | cd "`dirname "$0"`" 7 | 8 | inst() { 9 | case "$1" in 10 | exec) local mode=755 ;; 11 | data) local mode=644 ;; 12 | *) echo "Invalid mode: $mode"; exit 1 ;; 13 | esac 14 | 15 | if [ -n "$3" ]; then 16 | target="$PREFIX/$3/`basename $2`" 17 | else 18 | target="$PREFIX/$2" 19 | fi 20 | echo " inst ($mode) $2 -> $target" 21 | install -Dm $mode "$2" "$target" 22 | } 23 | 24 | if touch "$PREFIX" >/dev/null 2>&1; then 25 | inst exec bin/ik 26 | inst exec misc/ik-update-release bin 27 | inst data misc/isolatekit-tmpfiles.conf lib/tmpfiles.d 28 | inst data misc/com.refi64.isolatekit.policy share/polkit-1/actions 29 | for dir in share/isolatekit/*; do 30 | base=`basename $dir` 31 | if [ "$base" = "bin" ] || [ "$base" = "sbin" ]; then 32 | mode=exec 33 | else 34 | mode=data 35 | fi 36 | 37 | for file in $dir/*; do 38 | inst $mode $file 39 | done 40 | done 41 | 42 | if [ -d man/out ]; then 43 | for file in man/out/*; do 44 | sect=`echo $file | sed 's/.*\.//'` 45 | inst data $file share/man/man$sect 46 | done 47 | fi 48 | else 49 | exec pkexec sh "`realpath "$0"`" "$PREFIX" 50 | fi 51 | -------------------------------------------------------------------------------- /man/ik-update-release.1.md: -------------------------------------------------------------------------------- 1 | # ik-update-release -- Helper for updating IsolateKit unit script releases 2 | 3 | ## SYNOPSIS 4 | 5 | **ik-update-release** [files...] 6 | 7 | ## DESCRIPTION 8 | 9 | **ik-update-release** is a helper script that updates the releases in IsolateKit unit 10 | scripts. As mentioned in isolatekit.rc(5), releases are ISO 8061 dates, containing 11 | the 4-digit year, month, day, hour, minute, and second, respectively. 12 | 13 | When called with no arguments, **ik-update-release** will print the current time in the 14 | proper format. 15 | 16 | When called with a list of file arguments, **ik-update-release** will search for the 17 | rel= variable assignment in each file and replace it with the properly-formatted 18 | current time. 19 | -------------------------------------------------------------------------------- /man/ik.1.md: -------------------------------------------------------------------------------- 1 | # ik -- Create isolated development environments 2 | 3 | ## SYNOPSIS 4 | 5 | **ik** [options...] command 6 | 7 | ## DESCRIPTION 8 | 9 | IsolateKit is a tool that lets you easily create isolated development environments. 10 | For more information, see isolatekit(7). 11 | 12 | ## COMMON OPTIONS 13 | 14 | The following options are understood by all commands: 15 | 16 | **-h, --help** 17 | 18 | > Show a help screen. 19 | 20 | **-R, --resolve** 21 | 22 | > Any relative **file:** unit paths will be resolved relative to the given directory 23 | instead of the current directory. 24 | 25 | ## TARGET COMMANDS 26 | 27 | **target set** [target] 28 | 29 | > Set the units used by a target. You may either add units using **--add** or remove units 30 | using **--remove**. If the target does not exist, you may only use **--add**, and the 31 | first unit added *must* be a base unit. All the other units passed must be 32 | builder units. The **--bind-ro** and **--bind-rw** options are ignored in this form. 33 | 34 | **target run** [target] 35 | 36 | > Run a target. If **--add** and/or **--remove** are passed, the units described by those 37 | commands will be added or removed prior to running the target. Passing **null** as the 38 | target name will simply run the units passed to **--add** inside a temporary target that 39 | will be removed once the command exits. 40 | 41 | The following options are understood by the above two commands: 42 | 43 | **-a, --add** 44 | 45 | > A comma-separated list of unit paths to add to the target before executing the 46 | command. If running on either a nonexistent target or the null target, then the first 47 | unit must be a base, and all others must be builder. Otherwise, they all must be 48 | builders. 49 | 50 | **-r, --remove** 51 | 52 | > A comma-separated list of unit paths to remove from the target before executing the 53 | command. You cannot remove the base unit from a target. It is invalid to use this on 54 | nonexistent targets or the null target. 55 | 56 | **-b, --bind-ro** 57 | 58 | > A comma-separated list of read-only bind mounts to add to the target when running it. 59 | The format of each bind mount is *local-path:target-path*, where *local-path* is the host 60 | operating sytem path, and *target-path* is the target mountpoint inside of the target. 61 | Any colons in either path may be escaped by prefixing the colon with a backslash. 62 | 63 | **-B, --bind-rw** 64 | 65 | > Same as above, except the bind mount is read-write instead of read-only. 66 | 67 | ## INFORMATION COMMANDS 68 | 69 | **info target** [target] 70 | 71 | **info unit** [unit] 72 | 73 | > Show information on the given targets or units. 74 | 75 | **list all** 76 | 77 | **list targets** 78 | 79 | **list units** 80 | 81 | > List everything, targets, or units, respectively. 82 | 83 | The following options are understood by the above two commands: 84 | 85 | **-t, --terse** 86 | 87 | > Show terse output. 88 | 89 | ## TARGET AND UNIT COMMANDS 90 | 91 | **update** [units...] 92 | 93 | > Checks for any updates for the given units. If no units are passed, then all the units 94 | will be checked for updates. 95 | 96 | **remove targets** [targets...] 97 | 98 | **remove units** [units...] 99 | 100 | > Remove the given targets or units. 101 | 102 | ## SEE ALSO 103 | 104 | isolatekit.index(7) 105 | -------------------------------------------------------------------------------- /man/index.ini: -------------------------------------------------------------------------------- 1 | [Index] 2 | isolatekit.index(7)=isolatekit.index.7.html 3 | isolatekit(7)=isolatekit.7.html 4 | isolatekit.tutorial(7)=isolatekit.tutorial.7.html 5 | ik(1)=ik.1.html 6 | ik-update-release(1)=ik-update-release.1.html 7 | isolatekit.rc(5)=isolatekit.rc.5.html 8 | 9 | rc(1)=http://manpages.ubuntu.com/manpages/xenial/man1/rc.1.html 10 | -------------------------------------------------------------------------------- /man/isolatekit.7.md: -------------------------------------------------------------------------------- 1 | # isolatekit -- Guide on IsolateKit basic concepts 2 | 3 | ## DESCRIPTION 4 | 5 | IsolateKit is a tool that lets you easily create isolated development environments. 6 | 7 | ## TERMINOLOGY 8 | 9 | An **isolate** is a runnable "container", for lack of a better analogy. It consists of a 10 | set of layered **units**, each describing one portion of the isolate. 11 | 12 | There are two types of units: bases and builders. Base units are a base Linux root, 13 | and builder units "build" on top of that base to add more functionality. 14 | 15 | For example, two of the units available for IsolateKit are the alpine and alpine/sdk 16 | units. The former is a base unit that creates a minimal Alpine Linux installation root, 17 | and the latter is a builder unit that installs the alpine-sdk package. 18 | 19 | When any of these units are run, the changes they make to the root filesystem are 20 | stored individually as layers, like Docker does. That way, if the alpine/sdk unit is 21 | updated, the alpine unit won't have to be re-run. 22 | 23 | **targets** are a combination of an isolate (a set of units) and a working directory 24 | where any changes made to the root filesystem will be saved to. These are the build 25 | environments that can be created with IsolateKit. 26 | 27 | All these units are built using **unit scripts**, which are just rc(1) scripts that 28 | create the units. These end in .rc. For more information, see isolatekit.rc(5). 29 | 30 | Unit scripts are referenced via **unit paths**, which describe the path to a unit script. 31 | 32 | 33 | ## UNIT PATHS 34 | 35 | There are 4 types of unit paths: 36 | 37 | **file:**file-path 38 | 39 | > A path to a file on the local file system. Example: *file:my-unit.rc* 40 | 41 | **git:**git-repo//file-path 42 | 43 | > A path to a Git repo, and a file within that repo. If git-repo is looks like a 44 | repository (e.g. myuser/myrepo), then this does the same thing as github:. You can drop 45 | the .rc suffix on file-path. 46 | 47 | **github:**git-repo//file-path 48 | 49 | > Same as the above, but shorthand for GitHub repos. Again, the the .rc suffix on the file 50 | path can be dropped. Example: *git:kirbyfan64/isolatekit//units/alpine*, or 51 | *git:kirbyfan64/isolatekit//units/alpine.rc*. 52 | 53 | **ik:**file-path 54 | 55 | > *ik:xyz* is shorthand for *github:kirbyfan64/isolatekit//units/xyz*. This is a shortcut for 56 | using units included with the IsolateKit repository. 57 | 58 | ## SEE ALSO 59 | 60 | isolatekit.index(7), isolatekit.tutorial(7), ik(1) 61 | -------------------------------------------------------------------------------- /man/isolatekit.index.7.md: -------------------------------------------------------------------------------- 1 | # isolatekit.index -- IsolateKit documentation index 2 | 3 | ## TUTORIAL 4 | 5 | isolatekit(7) - Guide on IsolateKit basic concepts 6 | 7 | isolatekit.tutorial(7) - A brief tutorial on using IsolateKit 8 | 9 | ## REFERENCE 10 | 11 | ik(1) - Create isolated development environments 12 | 13 | ik-update-release(1) - Helper for updating IsolateKit unit script releases 14 | 15 | isolatekit.rc(5) - IsolateKit unit syntax 16 | -------------------------------------------------------------------------------- /man/isolatekit.rc.5.md: -------------------------------------------------------------------------------- 1 | # isolatekit.rc -- IsolateKit unit syntax 2 | 3 | ## SYNOPSIS 4 | 5 | *unit*.rc 6 | 7 | ## DESCRIPTION 8 | 9 | Unit files in IsolateKit are simply shell scripts written for the rc(1) shell. They 10 | define variables regarding the unit, as well as functions that are run to set up the 11 | unit. 12 | 13 | ## SYNTAX 14 | 15 | Te variant of rc used by IsolateKit is close in spirit to the original Plan 9 rc, 16 | with a few minor changes. Therefore, you can mostly use the 17 | [original rc manual](http://doc.cat-v.org/plan_9/4th_edition/papers/rc), with some 18 | minor exceptions. These are documented in rc(1) but repeated here for clarity: 19 | 20 | - **if not** has been replaced with **else**. 21 | - The $" operator is now the $^ operator. 22 | 23 | ## UNITS 24 | 25 | There are two types of units: 26 | 27 | **base units** 28 | 29 | > These units are distro bases. They will download and create a minimal OS image. 30 | For instance, the alpine unit is a base unit. 31 | 32 | **builder units** 33 | 34 | > These units extend/add functionality on top of base units. For instance, the alpine/sdk 35 | unit extends the alpine unit to add the alpine-sdk package. 36 | 37 | ## VARIABLES 38 | 39 | Both unit types must define the following variables: 40 | 41 | **name** 42 | 43 | > A unique name for the unit. 44 | 45 | **type** 46 | 47 | > The type of the unit; either *base* or *builder*. 48 | 49 | **rel** 50 | 51 | > The release version of the unit. This is an ISO 8061-formatted date, in UTC time, 52 | containing: 53 | 54 | > - 4-digit year (e.g. 2000) 55 | - A dash. 56 | - 2-digit, 1-indexed month (e.g. 01 for January) 57 | - A dash. 58 | - 2-digit, 1-indexed day (e.g. 11 for the 11th) 59 | - The letter T. 60 | - 2-digit hour. 61 | - A colon. 62 | - 2-digit minute. 63 | - A colon. 64 | - 2-digit second. 65 | 66 | > Example: *2018-03-09T22:08:07*. This value can be retrieved and/or updated via 67 | ik-update-release(1). 68 | 69 | In addition, the following variables are optional: 70 | 71 | **props** 72 | 73 | > A list of words, each defining a property that may be passed to the unit file on the 74 | ik(1) command line. The value passed to a prop can be retrieved from within the 75 | functions in the unit file using the variable **prop_NAME**. As props are only assigned 76 | *after* the unit file is loaded but before any functions are called, default values 77 | may be given by assigning to the prop variables in the top-level script code. 78 | 79 | Builder units must also define the following variable: 80 | 81 | **deps** 82 | 83 | > A list of units that the builder depends on. The first *must* be a base unit. 84 | 85 | ## FUNCTIONS 86 | 87 | Base units must define the following functions: 88 | 89 | **create** 90 | 91 | > Called from the host operating system to create a unit. The following vaiables are 92 | already defined inside this function: 93 | 94 | > - **$target** 95 | 96 | > > The target directory where the unit operating system data should be placed. 97 | 98 | **setup** 99 | 100 | > Called from inside the isolate to install packages/set anything else up. 101 | 102 | Builder units must define the following functions: 103 | 104 | **run** 105 | 106 | > Called from inside the isolate to install packages or set anything up that should be 107 | for this unit. 108 | 109 | ## EXAMPLES 110 | 111 | See **units/alpine.rc** and **units/alpine/sdk.rc** for two simple examples of a base unit 112 | and a builder unit, respectively. 113 | 114 | ## SEE ALSO 115 | 116 | isolatekit.index(7), isolatekit(7) 117 | -------------------------------------------------------------------------------- /man/isolatekit.tutorial.7.md: -------------------------------------------------------------------------------- 1 | # isolatekit.tutorial -- A brief tutorial on using IsolateKit 2 | 3 | ## INTRODUCTION 4 | 5 | IsolateKit is a tool that lets you easily create reproducible development environments. 6 | This man page is a tutorial to quickly get you up-to-speed. 7 | 8 | ## BASICS 9 | 10 | Read isolatekit(7) first. That will explain some basic terminology and concepts behind 11 | using IsolateKit. 12 | 13 | As an example, let's say you want to create a build environment to statically compile 14 | aria2c binaries. You can create a target named aria2c-build, that contains the units 15 | alpine and alpine/sdk. (Alpine is great for building static binaries). Then, when you 16 | want to work in your build environment, you just "run" the target. 17 | 18 | ## COMMAND LINE USAGE 19 | 20 | The ik(1) tool is the main tool used for working with IsolateKit. Re-iterating the above 21 | use case of an aria2c target, you could try something like this: 22 | 23 | ```bash 24 | # Create a target named aria2c, containing the alpine and alpine/sdk units. 25 | # -a/--add adds units to a target, creating it if it doesn't already exist. 26 | # Note that the base unit (ik:alpine) must ALWAYS come first. 27 | $ ik target set aria2c -a ik:alpine,ik:alpine/sdk 28 | # Actually, maybe we don't want the alpine/sdk unit. 29 | # -r/--remove removes units from a target. 30 | $ ik target set aria2c -r ik:alpine/sdk 31 | # Wait, I take that back. 32 | $ ik target set aria2c -a ik:alpine/sdk 33 | # Now run the target. This will open up a shell inside the target. 34 | $ ik target run aria2c 35 | # Maybe we want to run it without alpine/sdk. 36 | # The -a/--add and -r/--remove flags can be passed to 'target run', too. 37 | $ ik target run aria2c -r ik:alpine/sdk 38 | # Run the target, but pass a read-only bind mount, mounting $PWD as /workspace. 39 | $ ik target run aria2c -b $PWD:/workspace 40 | # Same as above, but with a read-write bind mount. 41 | $ ik target run aria2c -B $PWD:/workspace 42 | # List info about the target we just created. 43 | $ ik info target aria2c 44 | # List all the targets and units we've downloaded. 45 | $ ik list all 46 | $ ik list targets 47 | # Delete the target. 48 | $ ik remove aria2c 49 | 50 | # If you want to play around with units without actually creating a target, just 51 | # run the 'null' target: 52 | $ ik run null -a ik:alpine,ik:alpine/sdk 53 | ``` 54 | 55 | ## CREATING UNIT SCRIPTS 56 | 57 | See isolatekit.rc(5). 58 | 59 | ## SEE ALSO 60 | 61 | isolatekit.index(7), isolatekit(7), ik(1) 62 | -------------------------------------------------------------------------------- /misc/com.refi64.isolatekit.policy: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | re:fi.64 7 | https://github.com/kirbyfan64/isolatekit 8 | 9 | 10 | Run IsolateKit 11 | Authentication is required to use IsolateKit 12 | audio-x-generic 13 | 14 | no 15 | no 16 | auth_admin_keep 17 | 18 | /usr/bin/ik 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /misc/ik-update-release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then 4 | echo 'usage: ik-update-release []' 5 | exit 6 | fi 7 | 8 | date=`date -u +'%Y-%m-%dT%H:%M:%S'` 9 | 10 | if [ -n "$1" ]; then 11 | for arg in "$@"; do 12 | sed -i "s/^rel=.*/rel='$date'/" "$arg" 13 | done 14 | else 15 | echo "$date" 16 | fi 17 | -------------------------------------------------------------------------------- /misc/isolatekit-tmpfiles.conf: -------------------------------------------------------------------------------- 1 | f /run/isolatekit/script 0600 root root 2 | d /run/isolatekit/tmp 0600 root root 3 | d /run/isolatekit/data 0600 root root 4 | -------------------------------------------------------------------------------- /share/isolatekit/bin/aria2c: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7895cbba4a1fad1077b400d636e3d6657a0c69345c5b172608312602239b5d3c 3 | size 87200504 4 | -------------------------------------------------------------------------------- /share/isolatekit/bin/bsdtar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1f5303c34c1784b810d94ff3ca4e3ccec15c4db0f8de25a7da9855014f3c7396 3 | size 4692720 4 | -------------------------------------------------------------------------------- /share/isolatekit/bin/pv: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:332eb87ecc267b702fe2d31a904435dbadfc231122fa30714ea01904491f8e79 3 | size 593776 4 | -------------------------------------------------------------------------------- /share/isolatekit/bin/rc: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d0daff32bf732871466ac57a9f2e9e628898bc59ce83d632e7d78e1c2ea12602 3 | size 417792 4 | -------------------------------------------------------------------------------- /share/isolatekit/sbin/ikextract: -------------------------------------------------------------------------------- 1 | #!/run/isolatekit/data/bin/rc 2 | 3 | usage='Usage: ikextract [-u|-f] []' 4 | 5 | fn error { 6 | echo $* >[1=2] 7 | exit 1 8 | } 9 | 10 | if (~ $1 -u) { 11 | unnest=1 12 | shift 13 | } else if (~ $1 -f) { 14 | flatten=1 15 | shift 16 | } 17 | 18 | if (~ $#* [0-1]) { 19 | error $usage 20 | } 21 | 22 | if (~ $1 -h --help) { 23 | echo $usage 24 | exit 25 | } 26 | 27 | file=$1 28 | out=$2 29 | shift 2 30 | 31 | for (pattern) { 32 | args=($args --include=$pattern) 33 | } 34 | 35 | if (~ $unnest 1) { 36 | args=($args --strip-components=1) 37 | } else if (~ $flatten 1) { 38 | args=($args -s '|^.*/||') 39 | } 40 | 41 | path=(`{dirname $0}^/../bin $path) #` 42 | 43 | echo Extracting $file... 44 | pv $file | bsdtar -C $out -xf - $args 45 | -------------------------------------------------------------------------------- /share/isolatekit/sbin/ikget: -------------------------------------------------------------------------------- 1 | #!/run/isolatekit/data/bin/rc 2 | 3 | usage='Usage: ikget []' 4 | 5 | fn error { 6 | echo $* >[1=2] 7 | exit 1 8 | } 9 | 10 | switch ($#*) { 11 | case 0 12 | error $usage 13 | case 1 14 | if (~ $1 -h --help) { 15 | echo $usage 16 | exit 17 | } 18 | url=$1 19 | case 2 20 | url=$1 21 | output=$2 22 | } 23 | 24 | path=(`{dirname $0}^/../bin $path) 25 | flags=(-x16 --download-result=hide) 26 | 27 | if (~ $#output 0) { 28 | aria2c $flags $url 29 | } else { 30 | aria2c $flags -o $output $url 31 | } 32 | -------------------------------------------------------------------------------- /share/isolatekit/scripts/createunit.rc: -------------------------------------------------------------------------------- 1 | . `{dirname $0}^/utils.rc #` 2 | 3 | . $1 4 | target=$2 5 | shift 2 6 | 7 | while (! ~ $#* 0) { 8 | prop_$1=$2 9 | shift 2 10 | } 11 | 12 | _ret=0 13 | path=($path /run/isolatekit/data/sbin) create 14 | -------------------------------------------------------------------------------- /share/isolatekit/scripts/queryunit.rc: -------------------------------------------------------------------------------- 1 | . `{dirname $0}^/utils.rc #` 2 | 3 | _out=$2 4 | 5 | echo -n >$_out 6 | 7 | fn _print { 8 | echo -n $* >>$_out 9 | } 10 | 11 | fn _println { 12 | echo $* >>$_out 13 | } 14 | 15 | fn _error { 16 | _println [Error] 17 | _println Message=$^* 18 | exit 19 | } 20 | 21 | fn _check_present { 22 | if (! _vtest $1 $2) { 23 | _error ''''$2'''' must be defined. 24 | } 25 | } 26 | 27 | . $1 28 | 29 | _check_present v name 30 | _check_present v type 31 | _check_present v rel 32 | 33 | if (~ $type base) { 34 | _check_present f create 35 | _check_present f setup 36 | if (_vtest v deps) _error base units cannot have deps. 37 | } else if (~ $type builder) { 38 | _check_present f run 39 | } else { 40 | _error type must be either base or builder, not $type. 41 | } 42 | 43 | if (! ~ $rel [0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-9][0-9]) { 44 | _error rel value $rel must be a date in the format: \ 45 | 'YYYY-MM-DDHH:MM:SS' (e.g. '2000-01-01T00:00:00') 46 | } 47 | 48 | _println [Result] 49 | _println Name=$name 50 | _println Type=$type 51 | _println Rel=$rel 52 | 53 | _print Deps= 54 | for (_dep in $deps) { 55 | _print $_dep^';' 56 | } 57 | _println 58 | 59 | _print Props= 60 | for (_prop in $props) { 61 | _print $_prop^';' 62 | } 63 | _println 64 | -------------------------------------------------------------------------------- /share/isolatekit/scripts/rununit.rc: -------------------------------------------------------------------------------- 1 | . /run/isolatekit/data/scripts/utils.rc 2 | . /run/isolatekit/script 3 | 4 | _func=$1 5 | shift 6 | 7 | while (! ~ $#* 0) { 8 | prop_$1=$2 9 | shift 2 10 | } 11 | 12 | cd /tmp 13 | $_func 14 | exit 15 | -------------------------------------------------------------------------------- /share/isolatekit/scripts/utils.rc: -------------------------------------------------------------------------------- 1 | fn : {} 2 | 3 | fn _vtest { whatis -$1 $2 >[1=] >[2=] } 4 | 5 | fn ikerror { 6 | echo $* 7 | exit 1 8 | } 9 | -------------------------------------------------------------------------------- /src/main.vala: -------------------------------------------------------------------------------- 1 | using Gee; 2 | 3 | const string BOLD = "\033[1m"; 4 | const string RED = "\033[31m"; 5 | const string GREEN = "\033[32m"; 6 | const string YELLOW = "\033[33m"; 7 | const string MAGENTA = "\033[35m"; 8 | const string CYAN = "\033[36m"; 9 | const string RESET = "\033[0m"; 10 | 11 | private Regex color_matcher = null; 12 | 13 | string join(string[] items, string sep) { 14 | if (items.length == 0) { 15 | return ""; 16 | } else if (items.length == 1) { 17 | return items[0]; 18 | } else { 19 | var res = items[0]; 20 | foreach (var item in items[1:items.length]) { 21 | res += @"$sep$item"; 22 | } 23 | return res; 24 | } 25 | } 26 | 27 | void vprint(string message, va_list args) { 28 | if (color_matcher == null) { 29 | color_matcher = /!\[(.+?)\]/; 30 | } 31 | try { 32 | message = color_matcher.replace_eval(message, -1, 0, 0, 33 | (m, res) => { 34 | switch (m.fetch(1)) { 35 | case "bold": res.append(BOLD); break; 36 | case "red": res.append(RED); break; 37 | case "green": res.append(GREEN); break; 38 | case "yellow": res.append(YELLOW); break; 39 | case "magenta": res.append(MAGENTA); break; 40 | case "cyan": res.append(CYAN); break; 41 | case "reset": case "/": res.append(RESET); break; 42 | } 43 | return false; 44 | }); 45 | stdout.vprintf(@"$message$RESET", args); 46 | } catch (RegexError e) { 47 | stdout.printf("INTERNAL ERROR: RegexError: %s\n", e.message); 48 | Process.exit(1); 49 | } 50 | } 51 | 52 | void print(string message, ...) { 53 | var args = va_list(); 54 | vprint(message, args); 55 | } 56 | 57 | void blankln() { 58 | stdout.putc('\n'); 59 | } 60 | 61 | void println(string message, ...) { 62 | var args = va_list(); 63 | vprint(message, args); 64 | blankln(); 65 | } 66 | 67 | void warn(string message, ...) { 68 | var args = va_list(); 69 | vprint(@"![bold]![magenta]Warning: ![reset]$message", args); 70 | blankln(); 71 | } 72 | 73 | [NoReturn] 74 | void fail(string message, ...) { 75 | var args = va_list(); 76 | vprint(@"![bold]![red]Error: ![reset]$message", args); 77 | blankln(); 78 | Process.exit(1); 79 | } 80 | 81 | string sha256(string s) { 82 | var sha_builder = new Checksum(ChecksumType.SHA256); 83 | sha_builder.update((uchar[])s, s.length); 84 | return sha_builder.get_string(); 85 | } 86 | 87 | abstract class UnitPath { 88 | public HashMap props { get; protected set; } 89 | public string path { get; protected set; } 90 | 91 | private File retrieve_cache = null; 92 | 93 | public static UnitPath[] parse_paths(string paths) { 94 | UnitPath[] result = {}; 95 | foreach (var item in paths.split(",")) { 96 | result += UnitPath.parse(item); 97 | } 98 | return result; 99 | } 100 | 101 | public static UnitPath parse(string path_, string? relative_to_ = null) { 102 | var path = path_; 103 | var props = new HashMap(); 104 | var relative_to = relative_to_ ?? Environment.get_current_dir(); 105 | 106 | if ("{" in path) { 107 | var re_props = /^(.+){(.+)}$/; 108 | var re_pair = /^([a-zA-Z0-9_]+)=(.+)$/; 109 | 110 | MatchInfo match_props; 111 | if (!re_props.match(path, 0, out match_props)) { 112 | fail("Invalid unit path: %s", path); 113 | } 114 | 115 | path = match_props.fetch(1); 116 | foreach (var pair in match_props.fetch(2).split(":")) { 117 | MatchInfo match_pair; 118 | if (!re_pair.match(pair, 0, out match_pair)) { 119 | fail("Invalid argument pair in unit path: %s, %s", pair, path); 120 | } 121 | 122 | props[match_pair.fetch(1)] = match_pair.fetch(2); 123 | } 124 | } 125 | 126 | if (path.has_prefix("file:")) { 127 | var file_path = path[5:path.length]; 128 | if (relative_to != null && !Path.is_absolute(file_path)) { 129 | file_path = @"$relative_to/$file_path"; 130 | } 131 | return new FileUnitPath(props, @"file:$file_path", file_path); 132 | } else if (path.has_prefix("git:") || path.has_prefix("github:")) { 133 | var colon = path.index_of_char(':'); 134 | var prefix = path[0:colon]; 135 | var url_and_file = path[colon + 1:path.length]; 136 | 137 | var slash = url_and_file.index_of("//"); 138 | if (slash == -1) { 139 | fail("Unit path %s must use // to separate repository and file.", path); 140 | } 141 | var url = url_and_file[0:slash]; 142 | var file = url_and_file[slash + 2:url_and_file.length]; 143 | 144 | MatchInfo match; 145 | var is_repo = /^([^\/]+)\/([^\/]+)$/.match(url, 0, out match); 146 | if (prefix != "git" && !is_repo) { 147 | fail("Invalid repo in unit path: %s", path); 148 | } 149 | 150 | if (is_repo) { 151 | url = @"https://github.com/$url"; 152 | } 153 | return new GitUnitPath(props, path, url, file); 154 | } else if (path.has_prefix("ik:")) { 155 | var file_path = path[3:path.length]; 156 | return new GitUnitPath(props, path, "https://github.com/kirbyfan64/isolatekit", 157 | @"units/$file_path"); 158 | } 159 | 160 | fail("Invalid unit path: %s", path); 161 | } 162 | 163 | protected abstract File internal_retrieve(bool update); 164 | 165 | public File retrieve(bool update = false) { 166 | if (retrieve_cache == null || update) { 167 | retrieve_cache = internal_retrieve(update); 168 | } 169 | return retrieve_cache; 170 | } 171 | } 172 | 173 | class FileUnitPath : UnitPath { 174 | public File file { get; protected set; } 175 | 176 | public FileUnitPath(HashMap props, string path, string file) { 177 | this.props = props; 178 | this.path = path; 179 | this.file = File.new_for_path(file); 180 | } 181 | 182 | protected override File internal_retrieve(bool update) { 183 | if (!file.query_exists()) { 184 | fail("Unit file %s does not exist.", file.get_path()); 185 | } 186 | 187 | return file; 188 | } 189 | } 190 | 191 | class GitUnitPath : UnitPath { 192 | private string url; 193 | private string file; 194 | 195 | static HashSet already_cloned = new HashSet(); 196 | 197 | public GitUnitPath(HashMap props, string path, string url, 198 | string file) { 199 | this.props = props; 200 | this.path = path; 201 | this.url = url; 202 | this.file = file; 203 | } 204 | 205 | protected override File internal_retrieve(bool update) { 206 | var repo_storage = SystemProvider.get_storage_dir().get_child("repo"); 207 | var id = sha256(url); 208 | var repo = repo_storage.get_child(id); 209 | var test = repo.get_child(".isolatekit-test"); 210 | 211 | if (!test.query_exists()) { 212 | println("Running ![cyan]git clone![/] ![magenta]%s![/]...", url); 213 | 214 | if (repo.query_exists()) { 215 | SystemProvider.recursive_remove(repo); 216 | } 217 | SystemProvider.mkdir_p(repo); 218 | int status; 219 | 220 | try { 221 | string[] command = {"git", "clone", url, repo.get_path()}; 222 | Process.spawn_sync(Environment.get_current_dir(), command, Environ.get(), 223 | SpawnFlags.SEARCH_PATH, null, null, null, out status); 224 | } catch (SpawnError e) { 225 | fail("Failed to spawn git: %s", e.message); 226 | } 227 | 228 | if (status != 0) { 229 | fail("git failed with exit status %d.", status); 230 | } 231 | 232 | try { 233 | var os = test.create(FileCreateFlags.PRIVATE); 234 | os.close(); 235 | } catch (Error e) { 236 | fail("Failed to write test file %s: %s", test.get_path(), e.message); 237 | } 238 | } else if (update && !(repo.get_path() in already_cloned)) { 239 | int status; 240 | 241 | println("Running ![cyan]git fetch --all![/] for ![yellow]%s![/]...", url); 242 | try { 243 | string[] command = {"git", "fetch", "--all"}; 244 | Process.spawn_sync(repo.get_path(), command, Environ.get(), 245 | SpawnFlags.SEARCH_PATH, null, null, null, out status); 246 | } catch (SpawnError e) { 247 | fail("Failed to spawn git: %s", e.message); 248 | } 249 | 250 | if (status != 0) { 251 | fail("git failed with exit status %d.", status); 252 | } 253 | 254 | println("Running ![cyan]git reset --hard origin/master![/] for " + 255 | "![yellow]%s![/]...", url); 256 | try { 257 | string[] command = {"git", "reset", "--hard", "origin/master"}; 258 | Process.spawn_sync(repo.get_path(), command, Environ.get(), 259 | SpawnFlags.SEARCH_PATH, null, null, null, out status); 260 | } catch (SpawnError e) { 261 | fail("Failed to spawn git: %s", e.message); 262 | } 263 | 264 | if (status != 0) { 265 | fail("git failed with exit status %d.", status); 266 | } 267 | } 268 | 269 | already_cloned.add(repo.get_path()); 270 | 271 | var child = File.new_for_path(@"$(repo.get_path())/$file"); 272 | if (child.query_file_type(FileQueryInfoFlags.NONE) != FileType.REGULAR) { 273 | child = File.new_for_path(@"$(child.get_path()).rc"); 274 | if (child.query_file_type(FileQueryInfoFlags.NONE) != FileType.REGULAR) { 275 | fail("Failed to locate unit %s inside %s.", file, url); 276 | } 277 | } 278 | 279 | return child; 280 | } 281 | } 282 | 283 | class SystemProvider { 284 | private static File self_cache = null; 285 | 286 | public static File get_storage_dir() { 287 | return File.new_for_path("/var/lib/isolatekit"); 288 | } 289 | 290 | public static File get_self() { 291 | if (self_cache == null) { 292 | try { 293 | var contents = FileUtils.read_link("/proc/self/exe"); 294 | var utf8 = Filename.to_utf8(contents, -1, null, null); 295 | self_cache = File.new_for_path(utf8); 296 | } catch (FileError e) { 297 | fail("Failed to read /proc/self/exe: %s", e.message); 298 | } catch (ConvertError e) { 299 | fail("Failed to convert /proc/self/exe link to UTF-8: %s", e.message); 300 | } 301 | } 302 | 303 | return self_cache; 304 | } 305 | 306 | public static File get_data_dir() { 307 | return get_self().get_parent().get_parent() 308 | .get_child("share").get_child("isolatekit"); 309 | } 310 | 311 | public static File get_rc() { 312 | var result = get_data_dir().get_child("bin").get_child("rc"); 313 | if (!result.query_exists()) { 314 | fail("rc executable at %s does not exist.", result.get_path()); 315 | } 316 | return result; 317 | } 318 | 319 | public static File get_script(string name) { 320 | var result = get_data_dir().get_child("scripts").get_child(@"$name.rc"); 321 | if (!result.query_exists()) { 322 | fail("Script %s does not exist.", result.get_path()); 323 | } 324 | return result; 325 | } 326 | 327 | public static File get_temporary_dir(string desc) { 328 | var suffix = desc == null ? "" : @"-$(desc.replace("/", "_"))"; 329 | try { 330 | return File.new_for_path(DirUtils.make_tmp(@"XXXXXX$suffix")); 331 | } catch (FileError e) { 332 | fail("Failed to get temporary directory: %s", e.message); 333 | } 334 | } 335 | 336 | public static File get_temporary_file(string desc) { 337 | FileIOStream ios; 338 | var suffix = desc == null ? "" : @"-$(desc.replace("/", "_"))"; 339 | try { 340 | var tmp = File.new_tmp(@"XXXXXX$suffix", out ios); 341 | ios.close(); 342 | return tmp; 343 | } catch (Error e) { 344 | fail("Failed to get temporary file: %s", e.message); 345 | } 346 | } 347 | 348 | public static void mkdir_p(File dir) { 349 | if (DirUtils.create_with_parents(dir.get_path(), 0600) == -1) { 350 | fail("Failed to create directory %s: %s", dir.get_path(), strerror(errno)); 351 | } 352 | } 353 | 354 | public static File[] list(File path, FileType? file_type = null) { 355 | FileEnumerator enumerator = null; 356 | File[] files = {}; 357 | 358 | try { 359 | enumerator = path.enumerate_children("standard::*", 360 | FileQueryInfoFlags.NOFOLLOW_SYMLINKS); 361 | } catch (Error e) { 362 | warn("Failed to list %s: %s", path.get_path(), e.message); 363 | return files; 364 | } 365 | 366 | while (true) { 367 | FileInfo info = null; 368 | 369 | try { 370 | info = enumerator.next_file(); 371 | } catch (Error e) { 372 | warn("Failed to enumerate next file inside %s: %s", path.get_path(), e.message); 373 | break; 374 | } 375 | 376 | if (info == null) { 377 | break; 378 | } 379 | 380 | if (file_type == null || info.get_file_type() == file_type) { 381 | files += path.get_child(info.get_name()); 382 | } else { 383 | warn("Unexpected file %s of type %s inside %s.", info.get_name(), 384 | info.get_file_type().to_string(), path.get_path()); 385 | } 386 | } 387 | 388 | return files; 389 | } 390 | 391 | public static void recursive_remove(File path) { 392 | FileEnumerator enumerator = null; 393 | 394 | try { 395 | enumerator = path.enumerate_children("standard::*", 396 | FileQueryInfoFlags.NOFOLLOW_SYMLINKS); 397 | } catch (Error e) { 398 | warn("Failed to enumerate files inside %s for deletion: %s", path.get_path(), 399 | e.message); 400 | return; 401 | } 402 | 403 | while (true) { 404 | FileInfo info = null; 405 | 406 | try { 407 | info = enumerator.next_file(); 408 | } catch (Error e) { 409 | warn("Failed to enumerate next file inside %s: %s", path.get_path(), e.message); 410 | break; 411 | } 412 | 413 | if (info == null) { 414 | break; 415 | } 416 | 417 | var child = path.resolve_relative_path(info.get_name()); 418 | if (info.get_file_type() == FileType.DIRECTORY) { 419 | recursive_remove(child); 420 | } else { 421 | try { 422 | child.delete(); 423 | } catch (Error e) { 424 | warn("Failed to delete %s while deleting %s: %s", path.get_path(), 425 | child.get_path(), e.message); 426 | } 427 | } 428 | } 429 | 430 | try { 431 | path.delete(); 432 | } catch (Error e) { 433 | warn("Failed to delete %s: %s", path.get_path(), e.message); 434 | } 435 | } 436 | 437 | public static void mount(string source, string target, string type, 438 | Linux.MountFlags flags = 0, string options = "") { 439 | if (Linux.mount(source, target, type, flags, options) == -1) { 440 | fail("Failed to mount %s: %s", target, strerror(errno)); 441 | } 442 | } 443 | 444 | public static void bindmount(string source, string target, bool ro = true) { 445 | var flags = Linux.MountFlags.BIND; 446 | if (ro) { 447 | flags |= Linux.MountFlags.RDONLY; 448 | } 449 | SystemProvider.mount(source, target, "", flags); 450 | } 451 | 452 | public static void umount(string target) { 453 | if (Linux.umount(target) == -1) { 454 | fail("Failed to unmount %s: %s", target, strerror(errno)); 455 | } 456 | } 457 | } 458 | 459 | enum UnitType { BASE, BUILDER } 460 | 461 | struct UnitStorageData { 462 | string id; 463 | File base; 464 | File root; 465 | File config; 466 | File script; 467 | 468 | public static UnitStorageData new_for_unit_id(string id) { 469 | var storage = SystemProvider.get_storage_dir().get_child("unit").get_child(id); 470 | 471 | return UnitStorageData() { 472 | id = id, 473 | base = storage, 474 | root = storage.get_child("root"), 475 | config = storage.get_child("config"), 476 | script = storage.get_child("script.rc") 477 | }; 478 | } 479 | 480 | public static UnitStorageData new_for_unit_name(string name, 481 | HashMap given_props) { 482 | var sha_builder = new Checksum(ChecksumType.SHA256); 483 | sha_builder.update((uchar[])name, name.length); 484 | foreach (var entry in given_props.entries) { 485 | sha_builder.update((uchar[])";", 1); 486 | sha_builder.update((uchar[])entry.key, entry.value.length); 487 | sha_builder.update((uchar[])"=", 1); 488 | sha_builder.update((uchar[])entry.value, entry.value.length); 489 | } 490 | 491 | var id = sha_builder.get_string(); 492 | return new_for_unit_id(id); 493 | } 494 | } 495 | 496 | struct Unit { 497 | string name; 498 | UnitType type; 499 | string rel; 500 | bool dirty; 501 | Unit[] deps; 502 | string[] expected_props; 503 | HashMap given_props; 504 | string path; 505 | File script; 506 | UnitStorageData storage; 507 | } 508 | 509 | class UnitArray { 510 | private Unit[] data = {}; 511 | 512 | public UnitArray() {} 513 | 514 | public int length { 515 | get { 516 | return data.length; 517 | } 518 | } 519 | 520 | public Unit @get(int index) { 521 | return data[index]; 522 | } 523 | 524 | public void add(Unit item) { 525 | data += item; 526 | } 527 | 528 | public Unit[] take() { 529 | return data; 530 | } 531 | } 532 | 533 | DateTime rel_to_time(string rel) { 534 | var time = Time.gm(0); 535 | time.strptime(rel, "%Y-%m-%dT%H:%M:%S"); 536 | return new DateTime.utc(time.year + 1900, time.month, time.day, time.hour, 537 | time.minute, time.second); 538 | } 539 | 540 | enum ReadUnitRequirements { ANY, ALL_BUILDERS, ALL_BUILDERS_FIRST_BASE } 541 | 542 | Unit[] read_units_from_paths_i(UnitPath[] unit_paths, ReadUnitRequirements requirements, 543 | UnitArray units, HashMap name_index_map, 544 | HashMap path_index_map) { 545 | var local_units = new Unit[]{}; 546 | 547 | var rc = SystemProvider.get_rc(); 548 | var queryunit = SystemProvider.get_script("queryunit"); 549 | var temp = SystemProvider.get_temporary_file("queryunit"); 550 | 551 | foreach (var unit_path in unit_paths) { 552 | var unit_script = unit_path.retrieve(); 553 | 554 | string[] command = {rc.get_path(), "-e", queryunit.get_path(), 555 | unit_path.retrieve().get_path(), temp.get_path()}; 556 | int status = 0; 557 | 558 | try { 559 | Process.spawn_sync(Environment.get_current_dir(), command, Environ.get(), 0, 560 | null, null, null, out status); 561 | } catch (SpawnError e) { 562 | fail("Spawning queryunit for %s failed: %s", unit_path.path, e.message); 563 | } 564 | 565 | if (status != 0) { 566 | fail("queryunit of %s failed.", unit_path.path); 567 | } 568 | 569 | Unit unit; 570 | 571 | if (path_index_map.has_key(unit_script.get_path())) { 572 | unit = units[path_index_map[unit_script.get_path()]]; 573 | } { 574 | var kf = new KeyFile(); 575 | try { 576 | kf.load_from_file(temp.get_path(), KeyFileFlags.NONE); 577 | 578 | if (kf.has_group("Error")) { 579 | fail("%s: %s", unit_path.path, kf.get_string("Error", "Message")); 580 | } 581 | 582 | string name = kf.get_string("Result", "Name"); 583 | UnitType type = kf.get_string("Result", "Type") == "base" ? UnitType.BASE : 584 | UnitType.BUILDER; 585 | UnitPath[] dep_paths = {}; 586 | foreach (var dep_string in kf.get_string_list("Result", "Deps")) { 587 | dep_paths += UnitPath.parse(dep_string, 588 | unit_path.retrieve().get_parent().get_path()); 589 | } 590 | 591 | var deps = read_units_from_paths_i(dep_paths, 592 | ReadUnitRequirements.ALL_BUILDERS_FIRST_BASE, 593 | units, name_index_map, path_index_map); 594 | 595 | unit = Unit() { 596 | name = name, 597 | type = type, 598 | rel = kf.get_string("Result", "Rel"), 599 | dirty = false, 600 | deps = deps, 601 | expected_props = kf.get_string_list("Result", "Props"), 602 | given_props = unit_path.props, 603 | path = unit_path.path, 604 | script = unit_script, 605 | storage = UnitStorageData.new_for_unit_name(name, unit_path.props) 606 | }; 607 | } catch (Error e) { 608 | fail("Failed to load key-value queryunit data for %s from %s: %s", 609 | unit_path.path, temp.get_path(), e.message); 610 | } 611 | } 612 | 613 | if (name_index_map.has_key(unit.name)) { 614 | var other_unit = units[name_index_map[unit.name]]; 615 | if (unit.type != other_unit.type || unit.rel != other_unit.rel) { 616 | fail("Unit %s is self-inconsistent.", unit.name); 617 | } 618 | 619 | foreach (var entry in unit.given_props.entries) { 620 | if (other_unit.given_props.has_key(entry.key)) { 621 | warn("Unit %s has inconsistent props between two instances: %s={%s,%s}", 622 | unit.name, entry.key, other_unit.given_props[entry.key], entry.value); 623 | } else { 624 | other_unit.given_props[entry.key] = entry.value; 625 | } 626 | } 627 | } else { 628 | name_index_map[unit.name] = units.length; 629 | path_index_map[unit_script.get_path()] = units.length; 630 | units.add(unit); 631 | } 632 | 633 | local_units += unit; 634 | } 635 | 636 | if (local_units.length != 0) { 637 | int start = 0; 638 | if (requirements == ReadUnitRequirements.ALL_BUILDERS_FIRST_BASE) { 639 | if (local_units[0].type != UnitType.BASE) { 640 | fail("First unit %s must be a base.", local_units[0].name); 641 | } 642 | start = 1; 643 | } 644 | if (requirements != ReadUnitRequirements.ANY) { 645 | foreach (var unit in local_units[start:local_units.length]) { 646 | if (unit.type != UnitType.BUILDER) { 647 | if (requirements == ReadUnitRequirements.ALL_BUILDERS_FIRST_BASE) { 648 | fail("%s is not the first unit, and therefore should not be a base.", 649 | unit.name); 650 | } else if (requirements == ReadUnitRequirements.ALL_BUILDERS) { 651 | fail("%s must not be a base.", unit.name); 652 | } 653 | } 654 | } 655 | } 656 | } 657 | 658 | return local_units; 659 | } 660 | 661 | Unit[] read_units_from_paths(UnitPath[] unit_paths, ReadUnitRequirements requirements) { 662 | var units = new UnitArray(); 663 | var name_index_map = new HashMap(); 664 | var path_index_map = new HashMap(); 665 | read_units_from_paths_i(unit_paths, requirements, units, name_index_map, 666 | path_index_map); 667 | return units.take(); 668 | } 669 | 670 | Unit[] read_units_from_ids_i(string[] unit_ids, UnitArray units, 671 | HashMap index_map) { 672 | Unit[] local_units = {}; 673 | 674 | foreach (var id in unit_ids) { 675 | if (index_map.has_key(id)) { 676 | local_units += units[index_map[id]]; 677 | continue; 678 | } 679 | 680 | var storage = UnitStorageData.new_for_unit_id(id); 681 | if (!storage.base.query_exists()) { 682 | fail("Unit with id %s does not exist.", id); 683 | } 684 | 685 | var kf = new KeyFile(); 686 | try { 687 | kf.load_from_file(storage.config.get_path(), KeyFileFlags.NONE); 688 | 689 | var name = kf.get_string("Unit", "Name"); 690 | var type = kf.get_string("Unit", "Type") == "base" ? UnitType.BASE 691 | : UnitType.BUILDER; 692 | 693 | Unit[] deps = read_units_from_ids_i(kf.get_string_list("Unit", "Deps"), units, 694 | index_map); 695 | 696 | var given_props = new HashMap(); 697 | if (kf.has_group("GivenProps")) { 698 | foreach (var key in kf.get_keys("GivenProps")) { 699 | given_props[key] = kf.get_string("GivenProps", key); 700 | } 701 | } 702 | 703 | var unit = Unit() { 704 | name = name, 705 | type = type, 706 | rel = kf.get_string("Unit", "Rel"), 707 | dirty = false, 708 | deps = deps, 709 | expected_props = kf.get_string_list("Unit", "ExpectedProps"), 710 | given_props = given_props, 711 | path = kf.get_string("Unit", "Path"), 712 | script = storage.script, 713 | storage = storage 714 | }; 715 | 716 | index_map[id] = units.length; 717 | units.add(unit); 718 | } catch (Error e) { 719 | fail("Failed to load unit config for id %s: %s", id, e.message); 720 | } 721 | } 722 | 723 | return local_units; 724 | } 725 | 726 | Unit[] read_units_from_ids(string[] unit_ids) { 727 | var units = new UnitArray(); 728 | var index_map = new HashMap(); 729 | read_units_from_ids_i(unit_ids, units, index_map); 730 | return units.take(); 731 | } 732 | 733 | struct BindMount { 734 | bool rw; 735 | string source; 736 | string dest; 737 | 738 | public static BindMount[] parse(string binds_string, bool rw) { 739 | BindMount[] binds = {}; 740 | 741 | var builder = new StringBuilder(); 742 | var escape = false; 743 | string source = null; 744 | string dest = null; 745 | 746 | var index = 0; 747 | unichar c = 0; 748 | while (binds_string.get_next_char(ref index, out c)) { 749 | if (escape) { 750 | builder.append_unichar(c); 751 | escape = false; 752 | } else if (c == '\\') { 753 | escape = true; 754 | } else if (c == ':') { 755 | if (source != null) { 756 | fail("Bind mount %s should only have one : (use \\: to escape).", 757 | binds_string); 758 | } 759 | source = builder.str; 760 | builder.truncate(); 761 | } else if (c == ',') { 762 | dest = builder.str; 763 | builder.truncate(); 764 | 765 | binds += BindMount() { 766 | rw = rw, 767 | source = source ?? dest, 768 | dest = dest 769 | }; 770 | source = dest = null; 771 | } else { 772 | builder.append_unichar(c); 773 | } 774 | } 775 | 776 | dest = builder.str; 777 | if (dest.length != 0) { 778 | binds += BindMount() { 779 | rw = rw, 780 | source = source ?? dest, 781 | dest = dest 782 | }; 783 | } 784 | 785 | return binds; 786 | } 787 | } 788 | 789 | int run_in_unit(string name, File? storage_base_, Unit[] layers, string[] run_command, 790 | BindMount[] binds = {}) 791 | requires (storage_base_ != null || layers.length != 0) { 792 | var storage_base = storage_base_ ?? 793 | SystemProvider.get_temporary_dir(@"$name-null-storage"); 794 | 795 | var rootdir = storage_base.get_child("root"); 796 | SystemProvider.mkdir_p(rootdir); 797 | 798 | File mountroot = null; 799 | 800 | if (layers.length != 0) { 801 | var workdir = storage_base.get_child("work"); 802 | if (workdir.query_exists()) { 803 | SystemProvider.recursive_remove(workdir); 804 | } 805 | SystemProvider.mkdir_p(workdir); 806 | 807 | mountroot = SystemProvider.get_temporary_dir(@"$name-overlay-mountroot"); 808 | 809 | string[] options = {}; 810 | string[] lower = {}; 811 | 812 | for (var i = layers.length - 1; i >= 0; i--) { 813 | lower += layers[i].storage.root.get_path().replace(":", "\\:"); 814 | } 815 | 816 | options += @"lowerdir=$(join(lower, ":"))"; 817 | options += @"upperdir=$(rootdir.get_path())"; 818 | options += @"workdir=$(workdir.get_path())"; 819 | 820 | SystemProvider.mount("overlay", mountroot.get_path(), "overlay", 0, 821 | join(options, ",")); 822 | } else { 823 | mountroot = rootdir; 824 | } 825 | 826 | SystemProvider.mkdir_p(mountroot.get_child("run").get_child("isolatekit") 827 | .get_child("data")); 828 | 829 | // XXX: using string[] command = {...} here causes a C compilation error. 830 | var command = new string[]{"systemd-nspawn", 831 | "--bind-ro=/run/isolatekit/data", 832 | "--bind-ro=/run/isolatekit/script", 833 | "-D", mountroot.get_path(), "--chdir=/root", "-q", 834 | "-M", name.replace("/", "_"), "-u", "0", 835 | "-E", "ikthreads=2", 836 | "-E", "PATH=/run/isolatekit/data/sbin:/usr/local/sbin:" + 837 | "/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}; 838 | 839 | foreach (var bind in binds) { 840 | var arg = bind.rw ? "" : "-ro"; 841 | var source = File.new_for_path(bind.source.replace(":", "\\:")) 842 | .resolve_relative_path("").get_path(); 843 | var dest = bind.dest.replace(":", "\\:"); 844 | command += @"--bind$arg=$source:$dest"; 845 | } 846 | 847 | foreach (var item in run_command) { 848 | command += item; 849 | } 850 | 851 | int status; 852 | try { 853 | Process.spawn_sync(Environment.get_current_dir(), command, Environ.get(), 854 | SpawnFlags.SEARCH_PATH | SpawnFlags.CHILD_INHERITS_STDIN, null, 855 | null, null, out status); 856 | } catch (SpawnError e) { 857 | fail("Spawning %s for %s failed: %s", join(run_command, " "), name, e.message); 858 | } 859 | 860 | return Process.exit_status(status); 861 | } 862 | 863 | void ensure_units(Unit[] units, HashSet? dirty_ = null) { 864 | var createunit = SystemProvider.get_script("createunit"); 865 | var rc = SystemProvider.get_rc(); 866 | 867 | var dirty = dirty_ ?? new HashSet(); 868 | 869 | foreach (var unit in units) { 870 | ensure_units(unit.deps, dirty); 871 | 872 | if (unit.storage.base.query_exists()) { 873 | if (unit.storage.config.query_exists()) { 874 | try { 875 | var kf = new KeyFile(); 876 | kf.load_from_file(unit.storage.config.get_path(), KeyFileFlags.NONE); 877 | 878 | var current_rel = unit.rel; 879 | var new_rel = kf.get_string("Unit", "Rel"); 880 | var cmp = rel_to_time(current_rel).compare(rel_to_time(new_rel)); 881 | if (cmp == 1) { 882 | unit.dirty = true; 883 | } else if (cmp == -1) { 884 | warn("Unit %s current rel %s is newer than %s; not marking as dirty.", 885 | unit.name, current_rel, new_rel); 886 | } 887 | } catch (Error e) { 888 | warn("Error reading %s storage config: %s", unit.name, e.message); 889 | } 890 | } else { 891 | unit.dirty = true; 892 | } 893 | 894 | if (!unit.dirty) { 895 | foreach (var dep in unit.deps) { 896 | if (dep.storage.id in dirty) { 897 | unit.dirty = true; 898 | break; 899 | } 900 | } 901 | } 902 | 903 | if (!unit.dirty) { 904 | continue; 905 | } else { 906 | dirty.add(unit.storage.id); 907 | } 908 | 909 | SystemProvider.recursive_remove(unit.storage.base); 910 | } 911 | 912 | println("Processing unit ![cyan]%s![/]... ", unit.name); 913 | 914 | SystemProvider.mkdir_p(unit.storage.root); 915 | 916 | string[] command = {rc.get_path(), "-e", createunit.get_path(), 917 | unit.script.get_path(), unit.storage.root.get_path()}; 918 | foreach (var entry in unit.given_props.entries) { 919 | var found = false; 920 | foreach (var prop in unit.expected_props) { 921 | if (prop == entry.key) { 922 | found = true; 923 | command += entry.key; 924 | command += entry.value; 925 | break; 926 | } 927 | } 928 | 929 | if (!found) { 930 | fail("Unknown prop: %s{%s=%s}", unit.path, entry.key, entry.value); 931 | } 932 | } 933 | 934 | var tmp = SystemProvider.get_temporary_dir("createunit"); 935 | 936 | if (unit.type == UnitType.BASE) { 937 | int status = 0; 938 | try { 939 | Process.spawn_sync(tmp.get_path(), command, Environ.get(), 0, null, null, null, 940 | out status); 941 | } catch (SpawnError e) { 942 | fail("Spawning createunit for %s failed: %s", unit.name, e.message); 943 | } 944 | 945 | if (status != 0) { 946 | fail("createunit of %s failed.", unit.name); 947 | } 948 | } 949 | 950 | try { 951 | unit.script.copy(unit.storage.script, 0); 952 | } catch (Error e) { 953 | fail("Error saving script file at %s to %s: %s", unit.script.get_path(), 954 | unit.storage.script.get_path(), e.message); 955 | } 956 | 957 | Unit[] layers = {}; 958 | string func = "setup"; 959 | if (unit.type == UnitType.BUILDER) { 960 | layers += units[0]; 961 | func = "run"; 962 | } 963 | foreach (var dep in unit.deps) { 964 | layers += dep; 965 | } 966 | 967 | string[] run_command = {"/run/isolatekit/data/bin/rc", "-e", 968 | "/run/isolatekit/data/scripts/rununit.rc", func}; 969 | foreach (var entry in unit.given_props.entries) { 970 | run_command += entry.key; 971 | run_command += entry.value; 972 | } 973 | 974 | SystemProvider.bindmount(unit.script.get_path(), "/run/isolatekit/script"); 975 | var ret = run_in_unit(unit.name, unit.storage.base, layers, run_command); 976 | SystemProvider.umount("/run/isolatekit/script"); 977 | 978 | if (ret != 0) { 979 | fail("Unit script failed with exit status %d.", ret); 980 | } 981 | 982 | string[] dep_strings = {}; 983 | foreach (var dep in unit.deps) { 984 | dep_strings += dep.storage.id; 985 | } 986 | 987 | var kf = new KeyFile(); 988 | kf.set_string("Unit", "Name", unit.name); 989 | kf.set_string("Unit", "Type", unit.type == UnitType.BASE ? "base" : "builder"); 990 | kf.set_string("Unit", "Rel", unit.rel); 991 | kf.set_string("Unit", "Path", unit.path); 992 | kf.set_string_list("Unit", "ExpectedProps", unit.expected_props); 993 | kf.set_string_list("Unit", "Deps", dep_strings); 994 | 995 | foreach (var entry in unit.given_props.entries) { 996 | kf.set_string("GivenProps", entry.key, entry.value); 997 | } 998 | 999 | try { 1000 | kf.save_to_file(unit.storage.config.get_path()); 1001 | } catch (FileError e) { 1002 | fail("Failed to save unit config to %s: %s", unit.storage.config.get_path(), 1003 | e.message); 1004 | } 1005 | } 1006 | } 1007 | 1008 | struct Target { 1009 | string name; 1010 | string id; 1011 | Unit[] units; 1012 | File base; 1013 | File config; 1014 | File root; 1015 | 1016 | public static Target read(string name, out bool present) { 1017 | var id = sha256(name); 1018 | var storage = SystemProvider.get_storage_dir().get_child("target").get_child(id); 1019 | 1020 | present = false; 1021 | 1022 | string[] unit_ids = {}; 1023 | 1024 | var config = storage.get_child("config"); 1025 | if (config.query_exists()) { 1026 | var kf = new KeyFile(); 1027 | try { 1028 | kf.load_from_file(config.get_path(), KeyFileFlags.NONE); 1029 | foreach (var unit_id in kf.get_string_list("Target", "Units")) { 1030 | unit_ids += unit_id; 1031 | } 1032 | present = true; 1033 | } catch (Error e) { 1034 | warn("Failed to read target %s: %s", name, e.message); 1035 | } 1036 | } 1037 | 1038 | return Target() { 1039 | name = name, 1040 | id = id, 1041 | units = read_units_from_ids(unit_ids), 1042 | base = storage, 1043 | config = config, 1044 | root = storage.get_child("root") 1045 | }; 1046 | } 1047 | 1048 | public void save() { 1049 | SystemProvider.mkdir_p(root); 1050 | 1051 | string[] unit_ids = {}; 1052 | foreach (var unit in units) { 1053 | unit_ids += unit.storage.id; 1054 | } 1055 | 1056 | var kf = new KeyFile(); 1057 | try { 1058 | kf.set_string("Target", "Name", name); 1059 | kf.set_string_list("Target", "Units", unit_ids); 1060 | kf.save_to_file(config.get_path()); 1061 | } catch (Error e) { 1062 | fail("Failed to save target %s: %s", name, e.message); 1063 | } 1064 | } 1065 | } 1066 | 1067 | delegate void StorageMapDelegate(string name, File path, KeyFile? kf); 1068 | 1069 | string map_storage(string dirname, StorageMapDelegate dl, bool ignore_fail = false) { 1070 | var sect = dirname == "target" ? "Target" : "Unit"; 1071 | 1072 | var dir = SystemProvider.get_storage_dir().get_child(dirname); 1073 | if (!dir.query_exists()) { 1074 | return sect; 1075 | } 1076 | 1077 | foreach (var path in SystemProvider.list(dir, FileType.DIRECTORY)) { 1078 | var config = path.get_child("config"); 1079 | 1080 | var kf = new KeyFile(); 1081 | string name = null; 1082 | try { 1083 | kf.load_from_file(config.get_path(), KeyFileFlags.NONE); 1084 | name = kf.get_string(sect, "Name"); 1085 | } catch (Error e) { 1086 | if (ignore_fail) { 1087 | name = path.get_basename(); 1088 | kf = null; 1089 | } else { 1090 | warn("Failed to load %s %s: %s", dirname, path.get_basename(), e.message); 1091 | continue; 1092 | } 1093 | } 1094 | 1095 | dl(name, path, kf); 1096 | } 1097 | 1098 | return sect; 1099 | } 1100 | 1101 | abstract class Command { 1102 | public abstract string name { get; } 1103 | public abstract string usage { get; } 1104 | public abstract string description { get; } 1105 | public abstract void run(string[] args); 1106 | } 1107 | 1108 | class TargetCommand : Command { 1109 | public override string name { 1110 | get { return "target"; } 1111 | } 1112 | public override string usage { 1113 | get { return "set|run ![yellow]![/]"; } 1114 | } 1115 | public override string description { 1116 | get { return "Manipulate or run a target."; } 1117 | } 1118 | 1119 | private static string arg_add; 1120 | private static string arg_remove; 1121 | private static string arg_bind_ro = null; 1122 | private static string arg_bind_rw = null; 1123 | 1124 | public const OptionEntry[] options = { 1125 | {"add", 'a', 0, OptionArg.STRING, ref arg_add, "Add units to this target.", 1126 | "UNITS"}, 1127 | {"remove", 'r', 0, OptionArg.STRING, ref arg_remove, 1128 | "Remove units from this target.", "UNITS"}, 1129 | {"bind-ro", 'b', 0, OptionArg.STRING, ref arg_bind_ro, 1130 | "Bind mount the given directory in the running isolate (read-only).", "BIND"}, 1131 | {"bind-rw", 'B', 0, OptionArg.STRING, ref arg_bind_rw, 1132 | "Bind mount the given directory in the running isolate (read-write).", "BIND"}, 1133 | }; 1134 | 1135 | public override void run(string[] args) { 1136 | if (args.length != 2) { 1137 | fail("Expected 2 arguments, got %d.", args.length); 1138 | } else if (args[0] != "set" && args[0] != "run") { 1139 | fail("Expected 'set' or 'run', got '%s'.", args[0]); 1140 | } 1141 | 1142 | Target? target = null; 1143 | bool target_present = false; 1144 | 1145 | if (args[1] == "null") { 1146 | if (args[0] == "set") { 1147 | fail("Cannot set null target."); 1148 | } else if (arg_add == null) { 1149 | fail("Cannot run a null target without -a/--add."); 1150 | } 1151 | } else { 1152 | target = Target.read(args[1], out target_present); 1153 | if (args[0] == "run" && !target_present) { 1154 | fail("Cannot run a non-existent target."); 1155 | } 1156 | } 1157 | 1158 | if (args[0] == "set" && (arg_bind_ro != null || arg_bind_rw != null)) { 1159 | fail("-b/--bind-ro or -B/--bind-rw can only be passed to 'run'."); 1160 | } 1161 | 1162 | BindMount[] binds = {}; 1163 | foreach (var bind in BindMount.parse(arg_bind_rw ?? "", true)) { 1164 | binds += bind; 1165 | } 1166 | foreach (var bind in BindMount.parse(arg_bind_ro ?? "", false)) { 1167 | binds += bind; 1168 | } 1169 | 1170 | var requirements = target_present ? ReadUnitRequirements.ALL_BUILDERS 1171 | : ReadUnitRequirements.ALL_BUILDERS_FIRST_BASE; 1172 | var add_units = read_units_from_paths(UnitPath.parse_paths(arg_add ?? ""), 1173 | requirements); 1174 | var remove_units = read_units_from_paths(UnitPath.parse_paths(arg_remove ?? ""), 1175 | ReadUnitRequirements.ALL_BUILDERS); 1176 | 1177 | Unit[] units = {}; 1178 | 1179 | var ignore = new HashSet(); 1180 | foreach (var unit in remove_units) { 1181 | ignore.add(unit.storage.id); 1182 | } 1183 | 1184 | for (int i = 0; i < 2; i++) { 1185 | Unit[] unit_source; 1186 | if (i == 0) { 1187 | if (target == null) { 1188 | continue; 1189 | } 1190 | unit_source = target.units; 1191 | } else { 1192 | unit_source = add_units; 1193 | } 1194 | foreach (var unit in unit_source) { 1195 | if (!(unit.storage.id in ignore)) { 1196 | units += unit; 1197 | ignore.add(unit.storage.id); 1198 | } 1199 | } 1200 | } 1201 | 1202 | ensure_units(units); 1203 | 1204 | if (args[0] == "set") { 1205 | target.units = units; 1206 | target.save(); 1207 | } else if (args[0] == "run") { 1208 | string[] command = {"/run/isolatekit/data/bin/rc", "-l", "/.isolatekit-enter"}; 1209 | var storage_base = target != null ? target.base : null; 1210 | Process.exit(run_in_unit("null", storage_base, units, command, binds)); 1211 | } 1212 | } 1213 | } 1214 | 1215 | class UpdateCommand : Command { 1216 | public override string name { 1217 | get { return "update"; } 1218 | } 1219 | public override string usage { 1220 | get { return "![yellow]![/]"; } 1221 | } 1222 | public override string description { 1223 | get { return "Update units."; } 1224 | } 1225 | 1226 | public const OptionEntry[] options = {}; 1227 | 1228 | public override void run(string[] args) { 1229 | var update_all = args.length == 0; 1230 | var to_update = new HashSet(); 1231 | foreach (var arg in args) { 1232 | to_update.add(arg); 1233 | } 1234 | 1235 | var passed = new HashSet(); 1236 | Unit[] units = {}; 1237 | 1238 | map_storage("unit", (name, _, kf) => { 1239 | UnitPath path; 1240 | 1241 | if (!update_all && !(name in to_update)) { 1242 | return; 1243 | } 1244 | 1245 | try { 1246 | path = UnitPath.parse(kf.get_string("Unit", "Path")); 1247 | } catch (KeyFileError e) { 1248 | fail("Failed to retrieve unit data for %s: %s", name, e.message); 1249 | } 1250 | 1251 | path.retrieve(true); 1252 | UnitPath[] paths = {path}; 1253 | foreach (var unit in read_units_from_paths(paths, ReadUnitRequirements.ANY)) { 1254 | if (!(unit.storage.id in passed)) { 1255 | units += unit; 1256 | passed.add(unit.storage.id); 1257 | } 1258 | } 1259 | }); 1260 | 1261 | ensure_units(units); 1262 | } 1263 | } 1264 | 1265 | class ListCommand : Command { 1266 | public override string name { 1267 | get { return "list"; } 1268 | } 1269 | public override string usage { 1270 | get { return "all|targets|units"; } 1271 | } 1272 | public override string description { 1273 | get { return "List all targets and/or units."; } 1274 | } 1275 | 1276 | private static bool arg_terse; 1277 | 1278 | public const OptionEntry[] options = { 1279 | {"terse", 't', 0, OptionArg.NONE, ref arg_terse, "Show terse output.", null}, 1280 | }; 1281 | 1282 | public override void run(string[] args) { 1283 | if (args.length != 1) { 1284 | fail("Expected 1 argument, got %d.", args.length); 1285 | } else if (args[0] != "all" && args[0] != "targets" && args[0] != "units") { 1286 | fail("Expected 'all', 'targets', or 'units', got '%s'.", args[0]); 1287 | } 1288 | 1289 | string[] dirs = {}; 1290 | if (args[0] == "all" || args[0] == "targets") { 1291 | dirs += "target"; 1292 | } 1293 | if (args[0] == "all" || args[0] == "units") { 1294 | dirs += "unit"; 1295 | } 1296 | 1297 | foreach (var dirname in dirs) { 1298 | var items = new ArrayList(); 1299 | var sect = map_storage(dirname, (name, path, kf) => { 1300 | items.add(name); 1301 | }); 1302 | 1303 | if (items.size == 0) { 1304 | continue; 1305 | } 1306 | items.sort(); 1307 | 1308 | if (!arg_terse) { 1309 | println("![yellow]%ss:", sect); 1310 | foreach (var item in items) { 1311 | println(" - %s", item); 1312 | } 1313 | } else { 1314 | foreach (var item in items) { 1315 | if (args[0] == "all") { 1316 | println("%s: %s", dirname, item); 1317 | } else { 1318 | println("%s", item); 1319 | } 1320 | } 1321 | } 1322 | } 1323 | } 1324 | } 1325 | 1326 | class InfoCommand : Command { 1327 | public override string name { 1328 | get { return "info"; } 1329 | } 1330 | public override string usage { 1331 | get { return "target|unit ![yellow]![/]"; } 1332 | } 1333 | public override string description { 1334 | get { return "Get information about targets or units."; } 1335 | } 1336 | 1337 | private static bool arg_terse; 1338 | 1339 | public const OptionEntry[] options = { 1340 | {"terse", 't', 0, OptionArg.NONE, ref arg_terse, "Show terse output.", null}, 1341 | }; 1342 | 1343 | public override void run(string[] args) { 1344 | if (args.length != 2) { 1345 | fail("Expected 2 arguments, got %d.", args.length); 1346 | } else if (args[0] != "target" && args[0] != "unit") { 1347 | fail("Expected 'target' or 'unit', got %s.", args[0]); 1348 | } 1349 | 1350 | var dirname = args[0]; 1351 | map_storage(dirname, (name, path, _) => { 1352 | if (name != args[1]) { 1353 | return; 1354 | } 1355 | 1356 | if (dirname == "target") { 1357 | bool present; 1358 | var target = Target.read(name, out present); 1359 | assert(present); 1360 | 1361 | if (arg_terse) { 1362 | println("name: %s", name); 1363 | println("id: %s", target.id); 1364 | print("units:"); 1365 | foreach (var unit in target.units) { 1366 | print(" %s", unit.name); 1367 | } 1368 | blankln(); 1369 | } else { 1370 | println("![yellow]Name:![/] %s", name); 1371 | println("![yellow]Id:![/] %s", target.id); 1372 | println("![yellow]Units:![/]"); 1373 | foreach (var unit in target.units) { 1374 | println(" · ![cyan]%s![/]", unit.name); 1375 | } 1376 | } 1377 | } else if (dirname == "unit") { 1378 | string[] unit_ids = {path.get_basename()}; 1379 | var units = read_units_from_ids(unit_ids); 1380 | var unit = units[units.length - 1]; 1381 | var type = unit.type == UnitType.BASE ? "base" : "builder"; 1382 | 1383 | if (arg_terse) { 1384 | println("name: %s", name); 1385 | println("id: %s", unit.storage.id); 1386 | println("type: %s", type); 1387 | println("rel: %s", unit.rel); 1388 | println("path: %s", unit.path); 1389 | 1390 | if (unit.deps.length > 0) { 1391 | print("deps:"); 1392 | foreach (var dep in unit.deps) { 1393 | print(" %s", dep.name); 1394 | } 1395 | blankln(); 1396 | } 1397 | 1398 | if (unit.expected_props.length > 0) { 1399 | print("properties:"); 1400 | foreach (var prop in unit.expected_props) { 1401 | print(" %s", prop); 1402 | } 1403 | blankln(); 1404 | } 1405 | 1406 | if (unit.given_props.size > 0) { 1407 | foreach (var entry in unit.given_props.entries) { 1408 | println("property %s: %s", entry.key, entry.value); 1409 | } 1410 | } 1411 | } else { 1412 | var time = Time.gm(0); 1413 | time.strptime(unit.rel, "%Y-%m-%dT%H:%M:%S"); 1414 | var dt = rel_to_time(unit.rel); 1415 | 1416 | var fmt = "%B %d, %Y %H:%M:%S"; 1417 | var rel_utc = dt.format(fmt); 1418 | var rel_local = dt.to_local().format(fmt); 1419 | var rel_tz_name = dt.to_local().format("%Z"); 1420 | var rel_tz_offset = dt.to_local().format("%z"); 1421 | 1422 | println("![yellow]Name:![/] %s", name); 1423 | println("![yellow]Id:![/] %s", unit.storage.id); 1424 | println("![yellow]Type:![/] %s", type); 1425 | println("![yellow]Release: ![/]![cyan]UTC![/] %s +0000 / ![cyan]%s![/] %s %s", 1426 | rel_utc, rel_tz_name, rel_local, rel_tz_offset); 1427 | println("![yellow]Unit path:![/] %s", unit.path); 1428 | 1429 | if (unit.deps.length > 0) { 1430 | println("![yellow]Dependencies:![/]"); 1431 | foreach (var dep in unit.deps) { 1432 | println(" · ![cyan]%s![/]", dep.name); 1433 | } 1434 | } 1435 | 1436 | if (unit.expected_props.length > 0) { 1437 | println("![yellow]Properties:![/]"); 1438 | foreach (var prop in unit.expected_props) { 1439 | print(" · ![cyan]%s![/]", prop); 1440 | if (unit.given_props.has_key(prop)) { 1441 | print("![cyan]: ![/]%s", unit.given_props[prop]); 1442 | } 1443 | blankln(); 1444 | } 1445 | } 1446 | } 1447 | } 1448 | 1449 | Process.exit(0); 1450 | }); 1451 | 1452 | fail("Failed to find %s with name %s.", args[0], args[1]); 1453 | } 1454 | } 1455 | 1456 | class RemoveCommand : Command { 1457 | public override string name { 1458 | get { return "remove"; } 1459 | } 1460 | public override string usage { 1461 | get { return "targets|units ![yellow]![/]"; } 1462 | } 1463 | public override string description { 1464 | get { return "Remove targets or units."; } 1465 | } 1466 | 1467 | public const OptionEntry[] options = {}; 1468 | 1469 | public override void run(string[] args) { 1470 | if (args.length == 1) { 1471 | fail("Expected at least 2 arguments."); 1472 | } else if (args[0] != "targets" && args[0] != "units") { 1473 | fail("Expected 'targets' or 'units', got '%s'.", args[0]); 1474 | } 1475 | 1476 | var dirname = args[0][0:args[0].length - 1]; 1477 | 1478 | var to_remove = new HashSet(); 1479 | foreach (var arg in args[1:args.length]) { 1480 | to_remove.add(arg); 1481 | } 1482 | 1483 | map_storage(dirname, (name, path, kf) => { 1484 | if (name in to_remove) { 1485 | println("Removing ![cyan]%s![/]...", name); 1486 | SystemProvider.recursive_remove(path); 1487 | to_remove.remove(name); 1488 | } 1489 | }); 1490 | 1491 | foreach (var name in to_remove) { 1492 | warn("Failed to locate %s: %s", dirname, name); 1493 | } 1494 | } 1495 | } 1496 | 1497 | class GcCommand : Command { 1498 | public override string name { 1499 | get { return "gc"; } 1500 | } 1501 | public override string usage { 1502 | get { return "all|targets|units"; } 1503 | } 1504 | public override string description { 1505 | get { return "Clean up old, invalid units and targets."; } 1506 | } 1507 | 1508 | public const OptionEntry[] options = {}; 1509 | 1510 | public override void run(string[] args) { 1511 | if (args.length != 1) { 1512 | fail("Expected 1 argument, got %d.", args.length); 1513 | } else if (args[0] != "all" && args[0] != "targets" && args[0] != "units") { 1514 | fail("Expected 'all', 'targets', or 'units', got '%s'.", args[0]); 1515 | } 1516 | 1517 | string[] dirs = {}; 1518 | if (args[0] == "all" || args[0] == "targets") { 1519 | dirs += "target"; 1520 | } 1521 | if (args[0] == "all" || args[0] == "units") { 1522 | dirs += "unit"; 1523 | } 1524 | 1525 | foreach (var dirname in dirs) { 1526 | map_storage(dirname, (name, path, kf) => { 1527 | if (kf == null) { 1528 | println("Deleting ![magenta]%s![/] ![cyan]%s![/]...", dirname, name); 1529 | SystemProvider.recursive_remove(path); 1530 | } 1531 | }, true); 1532 | } 1533 | } 1534 | } 1535 | 1536 | class Main : Object { 1537 | private static string arg_resolve; 1538 | private static bool arg_help; 1539 | 1540 | const OptionEntry[] common_options = { 1541 | {"help", 'h', 0, OptionArg.NONE, ref arg_help, "Show this screen.", null}, 1542 | {"resolve", 'R', 0, OptionArg.STRING, ref arg_resolve, 1543 | "Resolve paths relative to the given directory.", "DIRECTORY"}, 1544 | }; 1545 | 1546 | private static OptionEntry[] all_options = {}; 1547 | 1548 | private static void print_short_options() { 1549 | foreach (var opt in all_options) { 1550 | if (opt.long_name == null || opt.long_name == "") { 1551 | continue; 1552 | } 1553 | 1554 | print(" ![green][-%c --%s", opt.short_name, opt.long_name); 1555 | if (opt.arg_description != null) { 1556 | print(@"![green]=<$(opt.arg_description)>"); 1557 | } 1558 | print("![green]]"); 1559 | } 1560 | } 1561 | 1562 | private static void print_usage(Command? command) { 1563 | if (command == null) { 1564 | print("![cyan]Usage:![/] ik ![yellow]"); 1565 | print_short_options(); 1566 | blankln(); 1567 | blankln(); 1568 | println(" IsolateKit allows you to create isolated development environments."); 1569 | blankln(); 1570 | 1571 | println("![cyan]Commands:"); 1572 | blankln(); 1573 | 1574 | Command[] commands = {new TargetCommand(), new UpdateCommand(), 1575 | new ListCommand(), new InfoCommand(), new RemoveCommand(), 1576 | new GcCommand()}; 1577 | foreach (var cmd in commands) { 1578 | println("![yellow] %-8s![/]%s", cmd.name, cmd.description); 1579 | } 1580 | } else { 1581 | print(@"![cyan]Usage:![/] ik $(command.name) $(command.usage)"); 1582 | print_short_options(); 1583 | blankln(); 1584 | blankln(); 1585 | println(" %s", command.description); 1586 | } 1587 | 1588 | blankln(); 1589 | println("![cyan]Options:"); 1590 | blankln(); 1591 | 1592 | foreach (var opt in all_options) { 1593 | if (opt.long_name == null || opt.long_name == "") { 1594 | continue; 1595 | } 1596 | 1597 | println(" ![green]-%c --%-9s![/]%s", opt.short_name, opt.long_name, 1598 | opt.description); 1599 | } 1600 | } 1601 | 1602 | private static void append_options(OptionEntry[] options) { 1603 | foreach (var opt in options) { 1604 | all_options += opt; 1605 | } 1606 | } 1607 | 1608 | public static int main(string[] args) { 1609 | #if IK_STATIC 1610 | Environment.set_variable("GIO_MODULE_DIR", "", true); 1611 | #endif 1612 | 1613 | string[] pkexec_args = {"pkexec", SystemProvider.get_self().get_path()}; 1614 | foreach (var arg in args[1:args.length]) { 1615 | pkexec_args += arg; 1616 | } 1617 | 1618 | append_options((OptionEntry[])common_options); 1619 | 1620 | Command? command = null; 1621 | 1622 | if (args.length > 1) { 1623 | switch (args[1]) { 1624 | case "target": 1625 | command = new TargetCommand(); 1626 | append_options((OptionEntry[])TargetCommand.options); 1627 | break; 1628 | case "update": 1629 | command = new UpdateCommand(); 1630 | append_options((OptionEntry[])UpdateCommand.options); 1631 | break; 1632 | case "list": 1633 | command = new ListCommand(); 1634 | append_options((OptionEntry[])ListCommand.options); 1635 | break; 1636 | case "info": 1637 | command = new InfoCommand(); 1638 | append_options((OptionEntry[])InfoCommand.options); 1639 | break; 1640 | case "remove": 1641 | command = new RemoveCommand(); 1642 | append_options((OptionEntry[])RemoveCommand.options); 1643 | break; 1644 | case "gc": 1645 | command = new GcCommand(); 1646 | append_options((OptionEntry[])GcCommand.options); 1647 | break; 1648 | } 1649 | } 1650 | 1651 | all_options += OptionEntry() { long_name = null }; 1652 | 1653 | var opt = new OptionContext(); 1654 | opt.add_main_entries((OptionEntry[])all_options, null); 1655 | opt.set_help_enabled(false); 1656 | 1657 | try { 1658 | opt.parse(ref args); 1659 | } catch (OptionError e) { 1660 | fail("%s.", e.message); 1661 | } 1662 | 1663 | if (arg_help) { 1664 | print_usage(command); 1665 | return 0; 1666 | } 1667 | 1668 | if (command == null) { 1669 | fail("No command given."); 1670 | } 1671 | 1672 | if (Posix.access("/", Posix.W_OK) == -1) { 1673 | pkexec_args += "-R"; 1674 | if (arg_resolve == null) { 1675 | pkexec_args += Environment.get_current_dir(); 1676 | } else { 1677 | pkexec_args += File.new_for_path(arg_resolve).resolve_relative_path("") 1678 | .get_path(); 1679 | } 1680 | Posix.execvp("pkexec", pkexec_args); 1681 | fail("pkexec execvp failed: %s", strerror(errno)); 1682 | } 1683 | 1684 | if (arg_resolve != null) { 1685 | if (Posix.chdir(arg_resolve) == -1) { 1686 | fail("Failed to change to resolve directory %s: %s", arg_resolve, 1687 | strerror(errno)); 1688 | } 1689 | } 1690 | 1691 | if (Linux.unshare(Linux.CloneFlags.NEWNS) == -1) { 1692 | fail("Failed to unshare mount namespace: %s", strerror(errno)); 1693 | } 1694 | 1695 | SystemProvider.mount("none", "/", "", 1696 | Linux.MountFlags.PRIVATE | Linux.MountFlags.REC, ""); 1697 | SystemProvider.mkdir_p(File.new_for_path("/run/isolatekit/tmp")); 1698 | SystemProvider.mkdir_p(File.new_for_path("/run/isolatekit/data")); 1699 | SystemProvider.mount("tmpfs", "/run/isolatekit/tmp", "tmpfs", 0, ""); 1700 | SystemProvider.bindmount(SystemProvider.get_data_dir().get_path(), 1701 | "/run/isolatekit/data"); 1702 | 1703 | Environment.set_variable("TMPDIR", "/run/isolatekit/tmp", true); 1704 | 1705 | command.run(args[2:args.length]); 1706 | 1707 | return 0; 1708 | } 1709 | } 1710 | -------------------------------------------------------------------------------- /units/alpine.rc: -------------------------------------------------------------------------------- 1 | name=alpine 2 | type=base 3 | props=(mirror version) 4 | rel='2018-03-09T22:08:07' 5 | 6 | prop_mirror=http://nl.alpinelinux.org/alpine 7 | prop_version=3.7 8 | 9 | fn create { 10 | ikget $prop_mirror/v$prop_version/main/x86_64/APKINDEX.tar.gz 11 | ikextract -u APKINDEX.tar.gz . APKINDEX 12 | 13 | apktools_ver=`{grep -C1 apk-tools-static APKINDEX | tail -1 | cut -d: -f2} #` 14 | ikget $prop_mirror/v$prop_version/main/x86_64/apk-tools-static-$apktools_ver.apk \ 15 | apktools.apk 16 | ikextract apktools.apk . 17 | 18 | path=($path .) sbin/apk.static -X $prop_mirror/v$prop_version/main -U \ 19 | --allow-untrusted --root $target --initdb add alpine-base tzdata 20 | } 21 | 22 | fn setup { 23 | cat >/etc/apk/repositories < /.isolatekit-enter 33 | } 34 | -------------------------------------------------------------------------------- /units/alpine/sdk.rc: -------------------------------------------------------------------------------- 1 | name=alpine/sdk 2 | type=builder 3 | props=() 4 | rel='2018-03-16T15:04:45' 5 | deps=(file:../alpine.rc) 6 | 7 | fn run { 8 | apk add alpine-sdk bash \ 9 | cmake extra-cmake-modules@testing linux-headers ninja \ 10 | autoconf automake libtool 11 | } 12 | -------------------------------------------------------------------------------- /units/alpine/sdk/gnome-static.rc: -------------------------------------------------------------------------------- 1 | name=alpine/sdk/gnome-static 2 | type=builder 3 | props=() 4 | rel='2018-03-12T16:57:29' 5 | deps=(file:../../alpine.rc file:../sdk.rc file:gnome.rc) 6 | 7 | fn run { 8 | apk add glib-static m4 9 | 10 | ikget https://github.com/GNOME/libgee/archive/0.20.1.tar.gz libgee.tar.xz 11 | mkdir libgee 12 | ikextract -u libgee.tar.xz libgee 13 | cd libgee 14 | 15 | ./autogen.sh --enable-static --disable-shared 16 | sed -i 's/-l $(libgee_dlname)/#/' gee/Makefile 17 | make -j$ikthreads install 18 | cd .. 19 | } 20 | -------------------------------------------------------------------------------- /units/alpine/sdk/gnome.rc: -------------------------------------------------------------------------------- 1 | name=alpine/sdk/gnome 2 | type=builder 3 | props=() 4 | rel='2018-03-16T20:24:08' 5 | deps=(file:../../alpine.rc file:../sdk.rc) 6 | 7 | fn run { 8 | apk add clutter-dev gobject-introspection-dev gtk+2.0-dev gtk+3.0-dev gnome-common \ 9 | libgee-dev libnotify-dev librsvg-dev libsecret-dev meson vala 10 | apk add --force-overwrite autoconf-archive@testing 11 | } 12 | -------------------------------------------------------------------------------- /units/build/isolatekit.rc: -------------------------------------------------------------------------------- 1 | name=isolatekit 2 | type=builder 3 | props=() 4 | rel='2018-03-17T00:40:39' 5 | deps=(file:../alpine.rc file:../alpine/sdk.rc file:../alpine/sdk/gnome.rc 6 | file:../alpine/sdk/gnome-static.rc) 7 | 8 | fn run { 9 | apk add vala 10 | 11 | git clone https://github.com/kirbyfan64/isolatekit.git 12 | cd isolatekit 13 | sed -i 's/warn(/_warn(/' src/main.vala 14 | make VALAFLAGS='-D IK_STATIC -X -static -X -lpcre -X -lffi -X -lgmodule-2.0 \ 15 | -X -lglib-2.0 -X -lz -X -lmount -X -lblkid -X -luuid -X -O2' 16 | 17 | cp bin/ik $HOME 18 | } 19 | -------------------------------------------------------------------------------- /units/build/isolatekit/aria2c.rc: -------------------------------------------------------------------------------- 1 | name=isolatekit/aria2c 2 | type=builder 3 | props=(version) 4 | rel='2018-03-16T17:43:14' 5 | deps=(file:../../alpine.rc file:../../alpine/sdk.rc) 6 | 7 | prop_version=1.33.1 8 | 9 | fn run { 10 | apk add libressl-dev 11 | 12 | ikget https://github.com/aria2/aria2/releases/download/release-$prop_version/aria2-$prop_version.tar.xz \ 13 | aria2c.tar.xz 14 | ikextract -u aria2c.tar.xz . 15 | 16 | ./configure --enable-shared=no --enable-static=yes ARIA2_STATIC=yes 17 | make -j$ikthreads 18 | cp src/aria2c $HOME 19 | } 20 | -------------------------------------------------------------------------------- /units/build/isolatekit/bsdtar.rc: -------------------------------------------------------------------------------- 1 | name=isolatekit/bsdtar 2 | type=builder 3 | props=(version) 4 | rel='2018-03-16T17:52:50' 5 | deps=(file:../../alpine.rc file:../../alpine/sdk.rc) 6 | 7 | prop_version=3.3.2 8 | 9 | fn run { 10 | apk add bzip2-dev expat-dev lz4-dev@edge nettle-dev xz-dev zlib-dev 11 | 12 | git clone https://github.com/libarchive/libarchive -b v$prop_version --depth=1 13 | cd libarchive 14 | 15 | path=($path .) build/autogen.sh 16 | ./configure --disable-shared --enable-bsdtar=static 17 | make -j$ikthreads bsdtar LDFLAGS=-all-static 18 | cp bsdtar $HOME 19 | } 20 | -------------------------------------------------------------------------------- /units/build/isolatekit/pv.rc: -------------------------------------------------------------------------------- 1 | name=isolatekit/pv 2 | type=builder 3 | props=(version) 4 | rel='2018-03-16T01:17:39' 5 | deps=(file:../../alpine.rc file:../../alpine/sdk.rc) 6 | 7 | prop_version=1.6.6 8 | 9 | fn run { 10 | ikget http://www.ivarch.com/programs/sources/pv-$prop_version.tar.bz2 pv.tar.bz2 11 | ikextract -u pv.tar.bz2 . 12 | 13 | ./configure CFLAGS=-static 14 | make -j$ikthreads 15 | cp pv $HOME 16 | } 17 | -------------------------------------------------------------------------------- /units/build/isolatekit/rc.rc: -------------------------------------------------------------------------------- 1 | name=isolatekit/rc 2 | type=builder 3 | props=(commit) 4 | rel='2018-03-16T01:17:39' 5 | deps=(file:../../alpine.rc file:../../alpine/sdk.rc) 6 | 7 | prop_commit=a32879322714f53ec044dabe4d20c44cb85be116 8 | 9 | fn run { 10 | apk add bison 11 | 12 | git clone https://github.com/rakitzis/rc 13 | cd rc 14 | git checkout $prop_commit 15 | 16 | ./bootstrap 17 | ./configure CFLAGS=-static 18 | make -j$ikthreads 19 | cp rc $HOME 20 | } 21 | 22 | -------------------------------------------------------------------------------- /units/centos.rc: -------------------------------------------------------------------------------- 1 | name=centos 2 | type=base 3 | props=(version) 4 | rel='2018-03-07T01:35:27' 5 | 6 | prop_version=7 7 | 8 | fn create { 9 | url=https://github.com/CentOS/sig-cloud-instance-images/raw/CentOS-$prop_version/docker/centos-$prop_version-docker.tar.xz 10 | ikget $url centos.txz 11 | 12 | ikextract -u centos.txz $target 13 | } 14 | 15 | fn setup { 16 | /usr/bin/yum update -y 17 | echo 'exec /bin/bash' > /.isolatekit-enter 18 | } 19 | -------------------------------------------------------------------------------- /units/fedora.rc: -------------------------------------------------------------------------------- 1 | name=fedora 2 | type=base 3 | props=(version) 4 | rel='2018-05-01T21:48:09' 5 | 6 | prop_version=28 7 | 8 | fn create { 9 | dir=Container 10 | if (~ $prop_version 2[1-7]) dir=Docker 11 | 12 | url=https://dl.fedoraproject.org/pub/fedora/linux/releases/$prop_version/$dir/x86_64/images 13 | ikget $url images.html 14 | files=`{grep -o 'Fedora-'$dir'-Base-'$prop_version'-[^"<]*' images.html} # ` 15 | ikget $url/$files(1) fedora.txz 16 | 17 | ikextract -u fedora.txz . '*/layer.tar' 18 | ikextract layer.tar $target 19 | } 20 | 21 | fn setup { 22 | dnf upgrade -y --refresh 23 | echo 'exec /bin/bash' > /.isolatekit-enter 24 | } 25 | -------------------------------------------------------------------------------- /units/ubuntu.rc: -------------------------------------------------------------------------------- 1 | name=ubuntu 2 | type=base 3 | props=(release date) 4 | rel='2018-03-27T22:51:20' 5 | 6 | prop_release=bionic 7 | prop_date=current 8 | 9 | fn create { 10 | url=https://partner-images.canonical.com/core/$prop_release/$prop_date/ubuntu-$prop_release-core-cloudimg-amd64-root.tar.gz 11 | ikget $url ubuntu.tgz 12 | 13 | ikextract ubuntu.tgz $target 14 | } 15 | 16 | fn setup { 17 | DEBIAN_FRONTEND=noninteractive 18 | 19 | apt update 20 | apt upgrade 21 | apt install tzdata 22 | echo 'exec /bin/bash' > /.isolatekit-enter 23 | } 24 | --------------------------------------------------------------------------------