[container] name = "frq-flutter-dev" description = "the Flutter desktop build, incremental, in a nix devShell" base = "arch-nix" # A Sandbox, not a Function: runs on a real VM, and the command is # the sandbox's own process so it dies when the command does. runtime = "sandbox" [build] # The container lives inside the repo it builds, so the copy is rooted two # levels up and `.` is the whole tree. context = "../.." include = ["."] # `dev` is the shell you actually want. There is no default shell in this # flake any more -- the one that used to be there belonged to the retired # libcosmic frontend -- so a bare `nix develop` here fails rather than # resolving to the wrong tree. # The devShell, baked in rather than entered. `print-dev-env` writes the whole # environment out as shell -- PATH, the compiler, every variable mkShell sets # -- and realises its inputs on the way, so the closure becomes an image layer # instead of a fetch every container pays for. Sourcing it from .bashrc means a # shell attached to this container *is* the devShell: no `nix develop`, no # clone of the flake's git inputs, no wait. # # `dev` stays for the case where the baked env is stale against a flake edit. commands = [ "nix print-dev-env /app#flutter-desktop --accept-flake-config --extra-substituters file:///nix-cache > /etc/devshell.sh", "echo '. /etc/devshell.sh' >> /root/.bashrc", "printf '#!/bin/sh\\nexec nix develop /app#flutter-desktop \"$@\"\\n' > /usr/local/bin/dev && chmod +x /usr/local/bin/dev", ] # The build state a local checkout carries: 395MB of a 441MB repo, uploaded on # every start and wanted by nothing out there. Flutter builds into a volume of # its own, and the clojure caches are this machine's. ignore = [ "flutter/build", "flutter/.home", "flutter/.dart_tool", "flutter/.clojuredart", "flutter/.cpcache", ".cpcache", "result", "build", ".git", ] # Two volumes doing two different jobs. `nix-cache` is the binary cache every # container here reads from and writes back to. `devshell` is the working # state of a `nix develop` loop, and it is shared by every container that has # one -- each gets its own directory under it, named for the devShell it # belongs to, so two projects (or two shells of one project) never write the # same tree. Modal Volumes have no locking, so the directories are the only # thing keeping them apart, and two runs of the *same* devshell must not # overlap. [volumes] nix-cache = "/nix-cache" devshell = "/devshell" [resources] cpu = 8 memory = 16384 timeout = 3600 [run] workdir = "/app" # Nix for the dependencies, the ordinary toolchain for the build. `nix build` # cannot do this: a derivation is all-or-nothing, so any edit is a fresh # sandbox and a fresh compile of everything. Here the devShell supplies the # compiler and the libraries, and `flutter build` decides what is stale -- # which is the whole reason `just flutter-desktop` exists as the working-tree # loop rather than as another `nix build`. # # rsync and not cp, with --checksum and not mtimes: Modal copies the source in # with fresh timestamps on every run, so a plain copy would look entirely new # to Flutter and rebuild the lot. --checksum compares content, leaves the # unchanged files' timestamps alone, and lets the incremental build work. # # The excludes are the state that must NOT be overwritten from /app -- it is # what we are here to keep. `just flutter-desktop` seeds those caches only # when they are missing, so finding them warm is all it takes. command = """ set -e # This container's own directory on the shared devshell volume, named for the # devShell it keeps the state of. Anything else using this volume picks its # own name and the two never meet. SHELL_DIR=/devshell/frq-flutter-desktop mkdir -p "$SHELL_DIR" "$SHELL_DIR/.cache" # A worktree's `.git` is a *file* naming a gitdir back on the machine that # copied it in, and nix believes it and goes looking for a path that is not # here. It has to go before any flake reference to /app. rm -rf /app/.git echo "sync: /app -> $SHELL_DIR" # `nix shell --command` and not `nix profile install`: a profile install puts # rsync in ~/.nix-profile/bin, which is not on the PATH of the shell already # running, so the very next line said `rsync: command not found`. # # rsync and not cp, with --checksum and not mtimes: Modal copies the source in # with fresh timestamps every run, so a plain copy looks entirely new to # Flutter and rebuilds the lot. --checksum compares content and leaves the # unchanged files' timestamps alone, which is the whole basis of the # incremental build. # # The excludes are the state we are here to keep -- overwriting them from /app # would defeat the volume. `just flutter-desktop` seeds those caches only when # they are missing, so finding them warm is all it takes. nix shell nixpkgs#rsync --accept-flake-config \ --extra-substituters file:///nix-cache --command \ rsync -a --checksum --delete \ --exclude 'flutter/.home/' \ --exclude 'flutter/.clojuredart/' \ --exclude 'flutter/build/' \ --exclude 'flutter/.dart_tool/' \ --exclude '.git' \ /app/ "$SHELL_DIR/" cd "$SHELL_DIR" echo "state carried over:" du -sh flutter/.home flutter/.clojuredart flutter/build 2>/dev/null \ || echo " (none yet -- first run)" # Nix for the dependencies, the ordinary toolchain for the build. `nix build` # cannot do this: a derivation is all-or-nothing, so any edit is a fresh # sandbox and a fresh compile of everything. Here the devShell supplies the # compiler and the libraries and `flutter build` decides what is stale. # Evaluated from /app and built in the volume. Both halves matter: /app is the # pristine copy, so nix stores a source tree of the repo rather than one # carrying gigabytes of flutter/build, while the recipe still runs where the # state it reuses lives -- `just -f` is what puts it there, since the recipe # cds to its own justfile's directory. nix develop /app#flutter-desktop --accept-flake-config \ --extra-substituters file:///nix-cache \ --max-jobs auto --command just -f "$SHELL_DIR/justfile" flutter-desktop echo "built:" du -sh flutter/build # The devShell's closure is gigabytes of Flutter, Dart, clang and GTK, and the # store it landed in belongs to the image rather than to a volume -- so # without this every run re-fetches it from upstream. Written back, the next # run substitutes it from file:///nix-cache instead. if [ -f /nix-cache/nix-cache-info ]; then echo "cache: writing the devShell closure back" nix copy --no-check-sigs --all --to file:///nix-cache fi """ # Nix's own cache, on the volume rather than in the container. Without it # every Sandbox starts empty and `nix develop` re-clones the flake's git # inputs -- nixgl and its transitives -- # because flake.lock pins which revision to fetch, not whether it is already # on disk. Set here rather than in the command so an interactive shell into # this container gets it too. env = { XDG_CACHE_HOME = "/devshell/frq-flutter-desktop/.cache" } [nix] # Every nix command in the container reads the mounted cache, including one # typed by hand in a shell. Passing --extra-substituters per command only ever # covered the scripts. substituters = ["file:///nix-cache"] # No devShell warming at image build time: this enters `nix develop` at run # time, on the VM, where the cache answers for its closure. The ptyshim that # warming would need under gVisor is deprecated and does not come back. flake = false shim = false # [experimental] overrides the sandbox default of vm_runtime = true.