| The window opens, and a devShell that remembers what it built c20643a nandi 5d ago | 1 | [container] |
| 2 | name = "frq-flutter-dev" |
| 3 | description = "the Flutter desktop build, incremental, in a nix devShell" |
| 4 | base = "arch-nix" |
| 5 | # A Sandbox, not a Function: runs on a real VM, and the command is |
| 6 | # the sandbox's own process so it dies when the command does. |
| 7 | runtime = "sandbox" |
| 8 | |
| 9 | [build] |
| 10 | # The container lives inside the repo it builds, so the copy is rooted two |
| 11 | # levels up and `.` is the whole tree. |
| 12 | context = "../.." |
| 13 | include = ["."] |
| Stop shipping the build tree, and let a shell be the devShell 013945f nandi 5d ago | 14 | # `dev` is the shell you actually want. Bare `nix develop` here resolves to the |
| 15 | # flake's default shell -- libcosmic, jolt-native, a thousand crates of Rust -- |
| 16 | # which is the wrong tree and an expensive way to find out. |
| 17 | # The devShell, baked in rather than entered. `print-dev-env` writes the whole |
| 18 | # environment out as shell -- PATH, the compiler, every variable mkShell sets |
| 19 | # -- and realises its inputs on the way, so the closure becomes an image layer |
| 20 | # instead of a fetch every container pays for. Sourcing it from .bashrc means a |
| 21 | # shell attached to this container *is* the devShell: no `nix develop`, no |
| 22 | # clone of the flake's git inputs, no wait. |
| 23 | # |
| 24 | # `dev` stays for the case where the baked env is stale against a flake edit. |
| 25 | commands = [ |
| A shell that is already the devShell, and two recipes to reach it e853593 nandi 5d ago | 26 | "nix print-dev-env /app#flutter-desktop --accept-flake-config --extra-substituters file:///nix-cache > /etc/devshell.sh", |
| Stop shipping the build tree, and let a shell be the devShell 013945f nandi 5d ago | 27 | "echo '. /etc/devshell.sh' >> /root/.bashrc", |
| 28 | "printf '#!/bin/sh\\nexec nix develop /app#flutter-desktop \"$@\"\\n' > /usr/local/bin/dev && chmod +x /usr/local/bin/dev", |
| 29 | ] |
| 30 | # The build state a local checkout carries: 395MB of a 441MB repo, uploaded on |
| 31 | # every start and wanted by nothing out there. Flutter builds into a volume of |
| 32 | # its own, and the jolt and clojure caches are this machine's. |
| 33 | ignore = [ |
| 34 | "flutter/build", "flutter/.home", "flutter/.dart_tool", |
| 35 | "flutter/.clojuredart", "flutter/.cpcache", |
| 36 | ".jolt", ".cpcache", "result", "build", ".git", |
| 37 | ] |
| The window opens, and a devShell that remembers what it built c20643a nandi 5d ago | 38 | |
| 39 | # Two volumes doing two different jobs. `nix-cache` is the binary cache every |
| 40 | # container here reads from and writes back to. `devshell` is the working |
| 41 | # state of a `nix develop` loop, and it is shared by every container that has |
| 42 | # one -- each gets its own directory under it, named for the devShell it |
| 43 | # belongs to, so two projects (or two shells of one project) never write the |
| 44 | # same tree. Modal Volumes have no locking, so the directories are the only |
| 45 | # thing keeping them apart, and two runs of the *same* devshell must not |
| 46 | # overlap. |
| 47 | [volumes] |
| 48 | nix-cache = "/nix-cache" |
| 49 | devshell = "/devshell" |
| 50 | |
| 51 | [resources] |
| 52 | cpu = 8 |
| 53 | memory = 16384 |
| 54 | timeout = 3600 |
| 55 | |
| 56 | [run] |
| 57 | workdir = "/app" |
| 58 | # Nix for the dependencies, the ordinary toolchain for the build. `nix build` |
| 59 | # cannot do this: a derivation is all-or-nothing, so any edit is a fresh |
| 60 | # sandbox and a fresh compile of everything. Here the devShell supplies the |
| 61 | # compiler and the libraries, and `flutter build` decides what is stale -- |
| 62 | # which is the whole reason `just flutter-desktop` exists as the working-tree |
| 63 | # loop rather than as another `nix build`. |
| 64 | # |
| 65 | # rsync and not cp, with --checksum and not mtimes: Modal copies the source in |
| 66 | # with fresh timestamps on every run, so a plain copy would look entirely new |
| 67 | # to Flutter and rebuild the lot. --checksum compares content, leaves the |
| 68 | # unchanged files' timestamps alone, and lets the incremental build work. |
| 69 | # |
| 70 | # The excludes are the state that must NOT be overwritten from /app -- it is |
| 71 | # what we are here to keep. `just flutter-desktop` seeds those caches only |
| 72 | # when they are missing, so finding them warm is all it takes. |
| 73 | command = """ |
| 74 | set -e |
| 75 | # This container's own directory on the shared devshell volume, named for the |
| 76 | # devShell it keeps the state of. Anything else using this volume picks its |
| 77 | # own name and the two never meet. |
| 78 | SHELL_DIR=/devshell/frq-flutter-desktop |
| Stop shipping the build tree, and let a shell be the devShell 013945f nandi 5d ago | 79 | mkdir -p "$SHELL_DIR" "$SHELL_DIR/.cache" |
| The window opens, and a devShell that remembers what it built c20643a nandi 5d ago | 80 | |
| 81 | # A worktree's `.git` is a *file* naming a gitdir back on the machine that |
| 82 | # copied it in, and nix believes it and goes looking for a path that is not |
| 83 | # here. It has to go before any flake reference to /app. |
| 84 | rm -rf /app/.git |
| 85 | |
| 86 | echo "sync: /app -> $SHELL_DIR" |
| 87 | # `nix shell --command` and not `nix profile install`: a profile install puts |
| 88 | # rsync in ~/.nix-profile/bin, which is not on the PATH of the shell already |
| 89 | # running, so the very next line said `rsync: command not found`. |
| 90 | # |
| 91 | # rsync and not cp, with --checksum and not mtimes: Modal copies the source in |
| 92 | # with fresh timestamps every run, so a plain copy looks entirely new to |
| 93 | # Flutter and rebuilds the lot. --checksum compares content and leaves the |
| 94 | # unchanged files' timestamps alone, which is the whole basis of the |
| 95 | # incremental build. |
| 96 | # |
| 97 | # The excludes are the state we are here to keep -- overwriting them from /app |
| 98 | # would defeat the volume. `just flutter-desktop` seeds those caches only when |
| 99 | # they are missing, so finding them warm is all it takes. |
| 100 | nix shell nixpkgs#rsync --accept-flake-config \ |
| 101 | --extra-substituters file:///nix-cache --command \ |
| 102 | rsync -a --checksum --delete \ |
| 103 | --exclude 'flutter/.home/' \ |
| 104 | --exclude 'flutter/.clojuredart/' \ |
| 105 | --exclude 'flutter/build/' \ |
| 106 | --exclude 'flutter/.dart_tool/' \ |
| 107 | --exclude '.git' \ |
| 108 | /app/ "$SHELL_DIR/" |
| 109 | |
| 110 | cd "$SHELL_DIR" |
| 111 | echo "state carried over:" |
| 112 | du -sh flutter/.home flutter/.clojuredart flutter/build 2>/dev/null \ |
| 113 | || echo " (none yet -- first run)" |
| 114 | |
| 115 | # Nix for the dependencies, the ordinary toolchain for the build. `nix build` |
| 116 | # cannot do this: a derivation is all-or-nothing, so any edit is a fresh |
| 117 | # sandbox and a fresh compile of everything. Here the devShell supplies the |
| 118 | # compiler and the libraries and `flutter build` decides what is stale. |
| 119 | # Evaluated from /app and built in the volume. Both halves matter: /app is the |
| 120 | # pristine copy, so nix stores a source tree of the repo rather than one |
| 121 | # carrying gigabytes of flutter/build, while the recipe still runs where the |
| 122 | # state it reuses lives -- `just -f` is what puts it there, since the recipe |
| 123 | # cds to its own justfile's directory. |
| 124 | nix develop /app#flutter-desktop --accept-flake-config \ |
| 125 | --extra-substituters file:///nix-cache \ |
| 126 | --max-jobs auto --command just -f "$SHELL_DIR/justfile" flutter-desktop |
| 127 | |
| 128 | echo "built:" |
| 129 | du -sh flutter/build |
| 130 | |
| 131 | # The devShell's closure is gigabytes of Flutter, Dart, clang and GTK, and the |
| 132 | # store it landed in belongs to the image rather than to a volume -- so |
| 133 | # without this every run re-fetches it from upstream. Written back, the next |
| 134 | # run substitutes it from file:///nix-cache instead. |
| 135 | if [ -f /nix-cache/nix-cache-info ]; then |
| 136 | echo "cache: writing the devShell closure back" |
| 137 | nix copy --no-check-sigs --all --to file:///nix-cache |
| 138 | fi |
| 139 | """ |
| Stop shipping the build tree, and let a shell be the devShell 013945f nandi 5d ago | 140 | # Nix's own cache, on the volume rather than in the container. Without it |
| 141 | # every Sandbox starts empty and `nix develop` re-clones the flake's git |
| 142 | # inputs -- jolt-src, jolt-native, glimmer, nixgl and their transitives -- |
| 143 | # because flake.lock pins which revision to fetch, not whether it is already |
| 144 | # on disk. Set here rather than in the command so an interactive shell into |
| 145 | # this container gets it too. |
| 146 | env = { XDG_CACHE_HOME = "/devshell/frq-flutter-desktop/.cache" } |
| The window opens, and a devShell that remembers what it built c20643a nandi 5d ago | 147 | |
| 148 | [nix] |
| Stop shipping the build tree, and let a shell be the devShell 013945f nandi 5d ago | 149 | # Every nix command in the container reads the mounted cache, including one |
| 150 | # typed by hand in a shell. Passing --extra-substituters per command only ever |
| 151 | # covered the scripts. |
| 152 | substituters = ["file:///nix-cache"] |
| The window opens, and a devShell that remembers what it built c20643a nandi 5d ago | 153 | # No devShell warming at image build time: this enters `nix develop` at run |
| 154 | # time, on the VM, where the cache answers for its closure. The ptyshim that |
| 155 | # warming would need under gVisor is deprecated and does not come back. |
| 156 | flake = false |
| 157 | shim = false |
| 158 | |
| 159 | # [experimental] overrides the sandbox default of vm_runtime = true. |