The nice thing about the open-source build system justbuild is that it is all about computing a value, not about manipulating the file system to get it into a certain state. Of course, it uses an action cache in the file system, but there is no output directory that can be messed up by concurrent builds (possibly in different build configurations). Even better: as source files get picked up very early during a build, continuing editing and starting a second build does not mess up the first one.
Moreover, a build output (e.g., a test log, a lint report) can even be requested on stdout through the options -p and -P. So results can be obtained by simple piping—no waiting and picking up from the file system. That makes it very easy to integrate into an editing workflow to get quick feedback; I use the following emacs minor mode (which I enable manually on selected source-file buffers) to trigger a build (with a custom, buffer-local, command) whenever I save the buffer. The result of the last-completed build is then associated (through buffer-local variables) with that buffer; the status is shown in the status line and there is a key binding to quickly have a look at the result. All buffer local and "fire and forget" as builds don't interfere.
;; Provide a minor mode to start a build process in the background, collect its ;; outputs, and associate them with the current buffer. ;; ;; Copyright 2025 Klaus Aehlig, License: Apache License, Version 2.0 ;; Configuration: the command to run bufbuild, becomes buffer-local. ;; Set this to some appropriate value before enabling bufbuild-mode. (defcustom bufbuild-cmd '("just-mr" "build" "-p") "The command to run the bufbuild target for the current project. Has to be a list of strings describing argv. Is used by the `after-save-hook' set by `bufbuild-mode'; usually set before enabling this minor mode.") (make-variable-buffer-local 'bufbuild-cmd) ;; Result variables, set on the bufer that requested the build. ;; Used by bufbuild-show (bound to f6 in bufbuild-mode) to fill ;; the result buffer. (defvar bufbuild-status nil "A short status string if the last bufbuild run for the current buffer did not succeed, nil otherwise. Used by the bufbuild minor mode as lighter.") (make-variable-buffer-local 'bufbuild-status) (defvar bufbuild-msg nil "The full bufbuild message for this buffer, if any, nil otherwise.") (make-variable-buffer-local 'bufbuild-msg) ;; Variables of the process buffer; used by bufbuild keep track of what ;; to do once the build process finishes. (defvar bufbuild-err-buffer nil "The buffer for bufbuild stderr of the process for which this is stdout") (make-variable-buffer-local 'bufbuild-err-buffer) (defvar bufbuild-initiating-buffer nil "The buffer that requested bufbuild run") (make-variable-buffer-local 'bufbuild-initiating-buffer) (defun bufbuild-sentinel (proc msg) "Sentinel for the bufbuild process. Upon completion of the process, set the buffer-local variables `bufbuild-status' and `bufbuild-msg' on the buffer that requested the build. Afterwards, clean up the buffers used for collecting stdout and stderr of the build process." (unless (process-live-p proc) (let* ((status (process-status proc)) (code (process-exit-status proc)) (buffer (process-buffer proc)) (err-buffer (with-current-buffer buffer bufbuild-err-buffer)) (req-buffer (with-current-buffer buffer bufbuild-initiating-buffer)) ) (message "Building finished for %s, %s %s" req-buffer status code) (cond ((not (eq status 'exit)) (let ((stderr (with-current-buffer err-buffer (buffer-string)))) (message "Bufbuilding %s did not terminate regularly:\n%s" proc stderr) (when (buffer-live-p req-buffer) (with-current-buffer req-buffer (setq bufbuild-status "PROC") (setq bufbuild-msg stderr))))) ((eq code 0) (when (buffer-live-p req-buffer) (let ((report (with-current-buffer buffer (buffer-string)))) (with-current-buffer req-buffer (setq bufbuild-status nil) (setq bufbuild-msg report))))) ((eq code 2) (let ((report (with-current-buffer buffer (buffer-string)))) (message "Bufbuilding found test:\n%s" report) (when (buffer-live-p req-buffer) (with-current-buffer req-buffer (setq bufbuild-status "TEST") (setq bufbuild-msg report))) )) ((eq code 1) (let ((stderr (with-current-buffer err-buffer (buffer-string)))) (message "Bufbuilding had build errors:\n%s" stderr) (when (buffer-live-p req-buffer) (setq bufbuild-status "BUILD") (setq bufbuild-msg stderr)))) (t (let ((stderr (with-current-buffer err-buffer (buffer-string)))) (message "Failed to bufbuild:\n%s" stderr) (when (buffer-live-p req-buffer) (with-current-buffer req-buffer (setq bufbuild-status "BUILD-internal") (setq bufbuild-msg stderr)))))) (kill-buffer err-buffer) (kill-buffer buffer)))) (defun bufbuild-start () "Start a build process with the command set by the buffer-local variable `bufbuild-cmd'. Usually triggered by the `after-save-hook' set by the `bufbuild-mode' minor mode." (interactive) (let ((out-buffer (generate-new-buffer "*bufbuild* report")) (err-buffer (generate-new-buffer "*bufbuild* progress")) (req-buffer (current-buffer)) (cmd bufbuild-cmd) ) (with-current-buffer out-buffer (setq bufbuild-err-buffer err-buffer) (setq bufbuild-initiating-buffer req-buffer) ) (message "Starting build for %s" req-buffer) (make-process :name "bufbuilding" :command cmd :buffer out-buffer :stderr err-buffer :sentinel #'bufbuild-sentinel))) (defun bufbuild-show () "Switch to the bufbuild result buffer *BUFBUILD* and set its content to the build message associated with current buffer. f6 buries." (interactive) (let ((out (get-buffer-create "*BUFBUILD*")) (status bufbuild-status) (msg bufbuild-msg)) (with-current-buffer out (erase-buffer) (when status (insert status)) (when msg (insert "\n\n" msg)) (use-local-map (copy-keymap global-map)) (local-set-key [f6] 'bury-buffer) ) (switch-to-buffer out))) ;; Minor mode to provide a key binidng to show the last build result (define-minor-mode bufbuild-mode "Minor mode to provide a build status well as a key binding to show result. If enabled, a local `after-save-hook' triggers a build, with the build command taken from `bufbuild-cmd'. The status of the last-finished build for this buffer is shown in the status line; the result as reported on stdout (exit code 0 or 2), or failure message (stderr, otherwise) can be requested by `bufbuild-show' which is bound to f6; f6 also buries the result buffer." :lighter (:eval (if bufbuild-status (propertize (format " %s" bufbuild-status) 'face '(:foreground "red" :weight bold)) " BB")) :keymap (let ((map (make-sparse-keymap))) (define-key map [f6] 'bufbuild-show) map)) (add-hook 'bufbuild-mode-hook (lambda () (if bufbuild-mode (add-hook 'after-save-hook #'bufbuild-start nil 'local) (remove-hook 'after-save-hook #'bufbuild-start 'local))))