<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>The Zanc Report</title><id>https://carlo.zancanaro.id.au/feed.xml</id><subtitle>Recent Posts</subtitle><updated>2026-02-25T10:33:59Z</updated><link href="https://carlo.zancanaro.id.au/feed.xml" rel="self" /><link href="https://carlo.zancanaro.id.au" /><entry><title>Forgejo, AGit, and Pull Request Templates</title><id>https://carlo.zancanaro.id.au/posts/forgejo-agit-and-pull-request-templates.html</id><author><name>Carlo Zancanaro</name><email>carlo@zancanaro.id.au</email></author><updated>2026-02-21T00:00:00Z</updated><link href="https://carlo.zancanaro.id.au/posts/forgejo-agit-and-pull-request-templates.html" rel="alternate" /><content type="html">&lt;p&gt;I've raised a few PRs against &lt;a href=&quot;https://codeberg.org/guix/guix&quot;&gt;the Guix Codeberg repository&lt;/a&gt; recently, and each time I've done so with &lt;a href=&quot;https://forgejo.org/docs/latest/user/agit-support/&quot;&gt;Forgejo's AGit workflow&lt;/a&gt;. This workflow is pretty nice, and allows me to raise a PR entirely from within Emacs. To do that, I've been using this code in my Emacs config to add an extra option to the &lt;code&gt;magit-push&lt;/code&gt; transient to use the AGit flow to push to the upstream branch:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-emacs-lisp&quot;&gt;(transient-define-suffix magit-push-current-agit (source target args)
  :if #'magit-get-current-branch
  :description (lambda () (concat (magit-push--upstream-description) &amp;quot; (agit)&amp;quot;))
  (interactive
   (let ((source (or (magit-get-current-branch)
                     (user-error &amp;quot;No branch is checked out&amp;quot;))))
     (list source
           (magit-get-upstream-branch source)
           (magit-push-arguments))))

  (magit-push-refspecs
   (magit-get &amp;quot;branch&amp;quot; source &amp;quot;remote&amp;quot;)
   (concat source &amp;quot;:&amp;quot;
           ;; Forgejo uses /refs/for/{branch} to target. Locally,
           ;; setting the upstream branch sets the &amp;quot;merge&amp;quot; config item
           ;; to refs/heads/{branch}, so we can easily construct what we need
           (s-replace &amp;quot;/heads/&amp;quot; &amp;quot;/for/&amp;quot;
                      (magit-get &amp;quot;branch&amp;quot; (magit-get-current-branch) &amp;quot;merge&amp;quot;))
           &amp;quot;/&amp;quot; source)
   `(,@args
     ,@(if (or (member &amp;quot;--force&amp;quot; args)
               (member &amp;quot;--force-with-lease&amp;quot; args))
           '(&amp;quot;-o&amp;quot; &amp;quot;force-push=true&amp;quot;)
         '()))))

(transient-insert-suffix 'magit-push
  '(1 0)
  (list &amp;quot;a&amp;quot; #'magit-push-current-agit))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once I've set an upstream branch, this makes it easy to push to Codeberg. Either &lt;code&gt;P a&lt;/code&gt; to create a new PR, or &lt;code&gt;P - f a&lt;/code&gt; to update an existing PR.&lt;/p&gt;&lt;p&gt;One downside to the AGit workflow is that I completely bypass the usual PR template that the Guix project has set up in &lt;code&gt;.forgejo/pull_request_template.md&lt;/code&gt;. Instead, it just uses the commit message of the last commit that's being pushed. That's often &lt;em&gt;fine&lt;/em&gt;, but I'd like to add at least some of my own commentary. With my existing Magit action I would usually have to go and open the PR to change the description to say what I wanted to say.&lt;/p&gt;&lt;p&gt;I've always been a bit uncomfortable about this, particularly because I think it's rude to just ignore the template.&lt;/p&gt;&lt;p&gt;My discomfort finally got strong enough that I solved this problem tonight, with this change to the final &lt;code&gt;if&lt;/code&gt; in the above code snippet:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-emacs-lisp&quot;&gt;,@(if (or (member &amp;quot;--force&amp;quot; args)
          (member &amp;quot;--force-with-lease&amp;quot; args))
      '(&amp;quot;-o&amp;quot; &amp;quot;force-push=true&amp;quot;)
    (let ((info (cz/read-pull-request-from-buffer
                 (concat (magit-gitdir) &amp;quot;../.forgejo/pull_request_template.md&amp;quot;))))
      `(&amp;quot;-o&amp;quot; ,(concat &amp;quot;title={base64}&amp;quot; (base64-encode-string (car info) t))
        &amp;quot;-o&amp;quot; ,(concat &amp;quot;description={base64}&amp;quot; (base64-encode-string (cdr info) t)))))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On a force push, we don't do anything (because we're just changing the commits on an existing PR). If we're &lt;em&gt;not&lt;/em&gt; force pushing then we're creating a new PR, so we prompt for the title and description, base64 encode them, and send them through as options on the push.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;cz/read-pull-request-from-buffer&lt;/code&gt; function, and its associated mode, are not quite so easy. They read out the pull request template, then go into a &lt;a href=&quot;https://www.gnu.org/software/emacs/manual/html_node/emacs/Recursive-Edit.html&quot;&gt;&lt;code&gt;recursive-edit&lt;/code&gt;&lt;/a&gt; in a &lt;code&gt;markdown-mode&lt;/code&gt; buffer with the template's contents (after skipping the Forgejo header). &lt;em&gt;Technically&lt;/em&gt; I'm not using the template correctly, because I should extract the title from the template and add it to the template, but I don't care.&lt;/p&gt;&lt;p&gt;At some point I might extract this out of my config and make a proper package for it. Let me know if that would be helpful to you - it might motivate me to actually do it!&lt;/p&gt;&lt;p&gt;The full relevant section of my config now looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-emacs-lisp&quot;&gt;(define-derived-mode cz/agit-pull-request-mode markdown-mode &amp;quot;AGit PR&amp;quot;)

(defvar-local cz/agit-pull-request-title nil)
(defvar-local cz/agit-pull-request-description nil)

(defun cz/agit-pull-request-submit ()
  (interactive)
  (when (y-or-n-p &amp;quot;Are you sure you want to submit this pull request? &amp;quot;)
    (setq-local cz/agit-pull-request-title
                (save-excursion
                  (beginning-of-buffer)
                  (buffer-substring-no-properties
                   (+ (point) 2)
                   (line-end-position))))
    (setq-local cz/agit-pull-request-description
                (save-excursion
                  (beginning-of-buffer)
                  (next-line)
                  (buffer-substring-no-properties
                   (point)
                   (point-max))))
    (exit-recursive-edit)))

(keymap-set cz/agit-pull-request-mode-map &amp;quot;C-c C-c&amp;quot; #'cz/agit-pull-request-submit)

(defun cz/agit-pull-request-cancel ()
  (interactive)
  (abort-recursive-edit))

(keymap-set cz/agit-pull-request-mode-map &amp;quot;C-c C-k&amp;quot; #'cz/agit-pull-request-cancel)

(defun cz/read-pull-request-from-buffer (template-file)
  (if (get-buffer &amp;quot;*Pull Request*&amp;quot;)
      (user-error &amp;quot;Other pull request already open - aborting this one.&amp;quot;)
    (let ((buffer (get-buffer-create &amp;quot;*Pull Request*&amp;quot;)))
      (with-current-buffer buffer
        (insert &amp;quot;# &amp;quot;)
        (save-excursion
          (insert &amp;quot;\n\n&amp;quot;)
          (when (file-exists-p template-file)
            (let ((start (point)))
              (insert-file-contents-literally template-file)
              (beginning-of-buffer)
              (save-match-data
                (re-search-forward &amp;quot;^---&amp;quot;)
                (re-search-forward &amp;quot;^---&amp;quot;)
                (next-line)
                (beginning-of-line))
              (delete-region start (point)))))
        (cz/agit-pull-request-mode)
        (pop-to-buffer (current-buffer)
                       '(display-buffer-same-window)))
      (unwind-protect
          (progn
            (recursive-edit)
            (with-current-buffer buffer
              (if cz/agit-pull-request-title
                  (cons cz/agit-pull-request-title
                        cz/agit-pull-request-description)
                (user-error &amp;quot;No pull request information&amp;quot;))))
        (with-current-buffer buffer
          (if (get-buffer-window)
              (quit-window 'kill (get-buffer-window))
            (kill-buffer)))))))

;; Define a command for an AGit flow to upstream (this is suitable for
;; opening PRs on Codeberg)
(transient-define-suffix magit-push-current-agit (source target args)
  :if #'magit-get-current-branch
  :description (lambda () (concat (magit-push--upstream-description) &amp;quot; (agit)&amp;quot;))
  (interactive
   (let ((source (or (magit-get-current-branch)
                     (user-error &amp;quot;No branch is checked out&amp;quot;))))
     (list source
           (magit-get-upstream-branch source)
           (magit-push-arguments))))

  (magit-push-refspecs
   (magit-get &amp;quot;branch&amp;quot; source &amp;quot;remote&amp;quot;)
   (concat source &amp;quot;:&amp;quot;
           ;; Forgejo uses /refs/for/{branch} to target. Locally,
           ;; setting the upstream branch sets the &amp;quot;merge&amp;quot; config item
           ;; to refs/heads/{branch}, so we can easily construct what we need
           (s-replace &amp;quot;/heads/&amp;quot; &amp;quot;/for/&amp;quot;
                      (magit-get &amp;quot;branch&amp;quot; (magit-get-current-branch) &amp;quot;merge&amp;quot;))
           &amp;quot;/&amp;quot; source)
   `(,@args
     ,@(if (or (member &amp;quot;--force&amp;quot; args)
               (member &amp;quot;--force-with-lease&amp;quot; args))
           '(&amp;quot;-o&amp;quot; &amp;quot;force-push=true&amp;quot;)
         (let ((info (cz/read-pull-request-from-buffer
                      (concat (magit-gitdir) &amp;quot;../.forgejo/pull_request_template.md&amp;quot;))))
           `(&amp;quot;-o&amp;quot; ,(concat &amp;quot;title={base64}&amp;quot; (base64-encode-string (car info) t))
             &amp;quot;-o&amp;quot; ,(concat &amp;quot;description={base64}&amp;quot; (base64-encode-string (cdr info) t))))))))

(transient-insert-suffix 'magit-push
  '(1 0)
  (list &amp;quot;a&amp;quot; #'magit-push-current-agit))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I'm confident that this code can be improved, but I think it'll solve my problem for now.&lt;/p&gt;&lt;h2&gt;Alternatives&lt;/h2&gt;&lt;p&gt;After writing this post two alternatives have been mentioned:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;a href=&quot;https://codeberg.org/halvin/agitjo&quot;&gt;agitjo&lt;/a&gt;, as a more fully-featured implementation of the AGit workflow, and&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/sarg/dotfiles/blob/a948406b169ae46d76abdfcc0327fa99f206dd0c/emacs/.doom.d/config.org#agit&quot;&gt;sarg's config&lt;/a&gt;, which is similar to the code above, but also stores the branch title/description in the Git branch description.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;There's also a &lt;a href=&quot;https://github.com/magit/magit/discussions/5508&quot;&gt;Magit discussion&lt;/a&gt; about having an AGit implementation upstream, which tarsius seems on board for.&lt;/p&gt;</content></entry><entry><title>Setting Up Cuirass Locally</title><id>https://carlo.zancanaro.id.au/posts/setting-up-cuirass-locally.html</id><author><name>Carlo Zancanaro</name><email>carlo@zancanaro.id.au</email></author><updated>2026-02-15T00:00:00Z</updated><link href="https://carlo.zancanaro.id.au/posts/setting-up-cuirass-locally.html" rel="alternate" /><content type="html">&lt;p&gt;Recently I've been trying to get &lt;a href=&quot;https://codeberg.org/guix/guix/pulls/2796&quot;&gt;a PR merged with some fixes for Lua&lt;/a&gt;. I thought this would be a pretty straightforward thing to merge, but it turns out that modifying the Lua packages leads to 990 packages needing to be rebuilt. This is more than the 300 limit for a merge to &lt;code&gt;master&lt;/code&gt;, so instead of merging my changes directly Andreas has kindly pushed them to a &lt;code&gt;lua-team&lt;/code&gt; branch and queued it up behind &lt;code&gt;go-team&lt;/code&gt;, &lt;code&gt;gnome-team&lt;/code&gt;, and &lt;code&gt;rust-team&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;While I'm waiting for that, I have some more drastic changes that I'd like to try out (e.g. packaging Lua 5.5.0, and using it as the default Lua for building packages). In order to do that, I'd like to be able to test my changes before the &lt;code&gt;lua-team&lt;/code&gt; branch gets to the front of the queue. I'm not a committer to Guix, so instead I figured I'd spin up Cuirass on my local machine to help me try things out.&lt;/p&gt;&lt;h2&gt;Why Cuirass?&lt;/h2&gt;&lt;p&gt;It's worth spending a moment on why I want to use Cuirass, rather than just doing the builds using Guix directly. &lt;code&gt;guix refresh -l&lt;/code&gt; already gives you a list of the packages that you need to build to test your changes, why not just use that with &lt;code&gt;guix build&lt;/code&gt;?&lt;/p&gt;&lt;p&gt;There are two reasons why I want to use Cuirass: live feedback, and tracking changes over time.&lt;/p&gt;&lt;p&gt;For live feedback, Cuirass gives me more information about what's going on that &lt;code&gt;guix build&lt;/code&gt; does. Even though I'm only running on my local machine, seeing the list of scheduled/succeeded/failed builds, and in-progress builds, is helpful for me to keep track of where things are up to. It lets me get started investigating build failures right away, without interrupting the rest of the builds.&lt;/p&gt;&lt;p&gt;For tracking changes over time, Cuirass helps me by reporting not just whether a package has built or not, but whether that has changed since the last build. This tells me whether my changes are breaking things, or whether things were already broken before I started. I need to know this so I know where to spend effort trying to fix things.&lt;/p&gt;&lt;p&gt;Given Cuirass is now a sunk cost for me, we're going to proceed with the assumption that this was a good idea. 🙂&lt;/p&gt;&lt;h2&gt;Initial Setup&lt;/h2&gt;&lt;p&gt;Setting up Cuirass on a Guix system is shockingly easy. I just added these three services to my &lt;code&gt;operating-system&lt;/code&gt;'s &lt;code&gt;services&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(service cuirass-service-type
         (cuirass-configuration
          (specifications
           #~(list (specification
                    (name &amp;quot;lua-team&amp;quot;)
                    (build '(manifests #$(local-file &amp;quot;lua-manifest.scm&amp;quot;)))
                    (channels (list (channel
                                     (name 'guix) ; has to be called guix for pull to work
                                     (url &amp;quot;https://git.sr.ht/~czan/guix&amp;quot;) ; my unlisted fork
                                     (branch &amp;quot;lua-team&amp;quot;)))))))))
(service postgresql-service-type
         (postgresql-configuration
          (postgresql (specification-&amp;gt;package &amp;quot;postgresql@16&amp;quot;))))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This sets up a local instance of the &lt;code&gt;cuirass-service-type&lt;/code&gt; configured to poll the listed channels and build things locally whenever anything changes.&lt;/p&gt;&lt;p&gt;The manifest that I wrote looks for all the packages which (transitively) depend on one of the Lua version. I took inspiration for the code for &lt;code&gt;guix refresh -l&lt;/code&gt;, and this is the result::&lt;/p&gt;&lt;pre&gt;&lt;code&gt;;; lua-manifest.scm
(use-modules (guix)
             (guix packages)
             (gnu packages)
             (guix profiles)
             (guix monads)
             (guix graph)
             (guix scripts graph)
             (ice-9 match)
             (gnu packages lua))

(define packages
  (list lua-5.4 lua lua-5.2 lua-5.1 luajit))

(packages-&amp;gt;manifest
 (with-store store
   (run-with-store store
     (mlet %store-monad ((edges (node-back-edges %bag-node-type
                                                 (package-closure (all-packages)))))
       (return (node-transitive-edges packages edges))))))&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Problem: not authenticated&lt;/h2&gt;&lt;p&gt;The first problem I ran into was that Guix will fail a pull for the &lt;code&gt;guix&lt;/code&gt; channel if it tries to authenticate the commits but can't. Since I'm not a committer, I can't authenticate my commits without doing some more work (setting up a key, adding to the keyring, signing a commit, setting up an introduction, ...). I didn't really want to do that, so I hunted through the Cuirass manual and found the &lt;code&gt;authenticate-channels?&lt;/code&gt; property. Setting it to &lt;code&gt;#f&lt;/code&gt; does an unauthenticated pull. Dangerous in some circumstances, but acceptable for what I'm doing.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(service cuirass-service-type
         (cuirass-configuration
          (specifications
           #~(list (specification
                    ;; ... As above ...
                    ;; I'm not a committer, so we can't authenticate my commits
                    (properties `((authenticate-channels? . #f))))))))&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Problem: evaluations failing&lt;/h2&gt;&lt;p&gt;I got the pulls working, but then the evaluations kept failing. It turns out the path listed in the &lt;code&gt;build&lt;/code&gt; option need to exist &lt;em&gt;in the pulled channels&lt;/em&gt;. In my first attempt I passed a &lt;code&gt;(local-file ...)&lt;/code&gt; call, but this did not work at all.&lt;/p&gt;&lt;p&gt;I had to commit my manifest into the repository and reference it in the &lt;code&gt;build&lt;/code&gt; option:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(service cuirass-service-type
         (cuirass-configuration
          (specifications
           #~(list (specification
                    (name &amp;quot;lua-team&amp;quot;)
                    (build '(manifests &amp;quot;etc/teams/lua/lua-manifest.scm&amp;quot;))
                    ;; ... As above ...
                    )))))&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Problem: no builds running&lt;/h2&gt;&lt;p&gt;The next problem I ran into was that this configuration didn't run any builds. It would poll the &lt;code&gt;git&lt;/code&gt; repository, and successfully pull the changes and build Guix, but it would never get to building any packages. I couldn't see any reason for it not working, but the documentation for the local build mode also had this warning:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;When Cuirass is used to build a large amount of jobs, the remote build mechanism described below should be preferred.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I figured I should try that. Again, configuring this in a Guix system to just use the local machine as a builder is easy. I just added the &lt;code&gt;remote-server&lt;/code&gt; setting to my &lt;code&gt;cuirass-configuration&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(service cuirass-service-type
         (cuirass-configuration
          (remote-server (cuirass-remote-server-configuration))
          (specifications
           ;; ... As above ...
           )))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and I added one more service to my list of services:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(service cuirass-remote-worker-service-type
         (cuirass-remote-worker-configuration))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With that, Cuirass started to build my packages locally. I saw my CPUs get busy, and all was well.&lt;/p&gt;&lt;h2&gt;Problem: killed my machine&lt;/h2&gt;&lt;p&gt;Unfortunately, the default configuration for the Cuirass remote worker is to run a single build, and to let it use all the cores for its build. My machine has an i7-8700 with 6 cores running 12 threads, and 32GiB of RAM. It turns out that building some Guix packages will cause that configuration to lock up (I assume it's swapping, but I didn't investigate at all).&lt;/p&gt;&lt;p&gt;I looked through the Cuirass manual to try to find some way to set the equivalent of &lt;code&gt;--cores&lt;/code&gt; in the builds it runs, but I couldn't find anything. I had a look at the &lt;a href=&quot;https://codeberg.org/guix/cuirass/src/commit/208ecf61a30c070f1b86c0f4833c41595c90887b/src/cuirass/scripts/remote-worker.scm#L332&quot;&gt;Cuirass source where the parameter is set&lt;/a&gt;, which itself comes from &lt;a href=&quot;https://codeberg.org/guix/cuirass/src/commit/208ecf61a30c070f1b86c0f4833c41595c90887b/src/cuirass/scripts/remote-worker.scm#L548&quot;&gt;dividing the number of CPU cores by the number of workers&lt;/a&gt;. This assumes that the Cuirass workers can take up all the cores on the machine (which is not &lt;em&gt;ideal&lt;/em&gt; for the computer that I use as my desktop), but it at least gives me enough control to avoid the builds locking up the whole machine.&lt;/p&gt;&lt;p&gt;Once again, the configuration for this is straightforward:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(service cuirass-remote-worker-service-type
         (cuirass-remote-worker-configuration
          (workers 6)))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now I have six concurrent builds, with each using two threads. This has kept my memory usage under 50% thus far, and builds are continuing unimpeded.&lt;/p&gt;&lt;h2&gt;Final configuration&lt;/h2&gt;&lt;p&gt;The end result of my service configuration looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(service cuirass-service-type
         (cuirass-configuration
           (remote-server (cuirass-remote-server-configuration))
           (specifications
            #~(list (specification
                     (name &amp;quot;lua-team&amp;quot;)
                     (build '(manifests &amp;quot;etc/teams/lua/lua-manifest.scm&amp;quot;))
                     (channels (list (channel
                                       (name 'guix)
                                       (url &amp;quot;https://git.sr.ht/~czan/guix&amp;quot;)
                                       (branch &amp;quot;lua-team&amp;quot;))))
                     (properties `((authenticate-channels? . #f))))))))
(service postgresql-service-type
         (postgresql-configuration
           (postgresql (specification-&amp;gt;package &amp;quot;postgresql@16&amp;quot;))))
(service cuirass-remote-worker-service-type
         (cuirass-remote-worker-configuration
          (workers 6)))&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Want to help?&lt;/h2&gt;&lt;p&gt;If you're interested in helping to maintain Lua packages in Guix (because I have no idea what I'm doing), &lt;a href=&quot;https://lists.gnu.org/archive/html/guix-devel/2026-02/msg00167.html&quot;&gt;get in touch on &lt;code&gt;guix-devel&lt;/code&gt; in the thread I started&lt;/a&gt;. I'm particularly interested in getting a Guix committer on board, so I don't have to keep bothering Andreas. (Thanks Andreas!)&lt;/p&gt;</content></entry><entry><title>Ruby Development Using Guix</title><id>https://carlo.zancanaro.id.au/posts/ruby-development-using-guix.html</id><author><name>Carlo Zancanaro</name><email>carlo@zancanaro.id.au</email></author><updated>2026-02-08T00:00:00Z</updated><link href="https://carlo.zancanaro.id.au/posts/ruby-development-using-guix.html" rel="alternate" /><content type="html">&lt;p&gt;Up until a few months ago, I was working on a Ruby on Rails monolith. In the four years that I worked on the project, we went through a few ways of building a local development environment:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;directly using &lt;a href=&quot;https://bundler.io&quot;&gt;Bundler&lt;/a&gt; on our machine, then&lt;/li&gt;&lt;li&gt;using Bundler inside a Docker image, then&lt;/li&gt;&lt;li&gt;using &lt;a href=&quot;https://nixos.org/&quot;&gt;Nix&lt;/a&gt; and &lt;a href=&quot;https://github.com/nix-community/bundix&quot;&gt;Bundix&lt;/a&gt;, then&lt;/li&gt;&lt;li&gt;back to using Bundler inside a Docker image (not everyone liked Nix).&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;All of these options were &lt;em&gt;fine&lt;/em&gt;, but my package manager of choice is &lt;a href=&quot;https://guix.gnu.org/&quot;&gt;Guix&lt;/a&gt;. I really wanted a way for me to work with my Ruby on Rails project alongside the rest of my team, but using Guix to build my development environment.&lt;/p&gt;&lt;p&gt;In 2015 David Thompson &lt;a href=&quot;https://dthompson.us/posts/ruby-on-guix.html&quot;&gt;posted about using Guix for Ruby development&lt;/a&gt; which describes using Guix packages to build a Ruby development environment. Guix has a lot of Ruby packages, but it doesn't package all of &lt;a href=&quot;https://rubygems.org/&quot;&gt;RubyGems&lt;/a&gt;, nor does it package all the different versions of each Gem. This makes it difficult to use Guix to get precise dependencies that are needed for most real-world Ruby projects.&lt;/p&gt;&lt;p&gt;To make things easier for myself, I built &lt;a href=&quot;https://git.sr.ht/~czan/guix-ruby&quot;&gt;Guix Ruby&lt;/a&gt;. A Guix channel that provides a &lt;code&gt;guix ruby&lt;/code&gt; command which imports a &lt;code&gt;Gemfile.lock&lt;/code&gt; and constructs Guix packages for all the listed dependencies.&lt;/p&gt;&lt;p&gt;To install it, just need to add this to your channels file (&lt;code&gt;~/.config/guix/channels.scm&lt;/code&gt;):&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(cons* (channel
        (name 'guix-ruby)
        (url &amp;quot;https://git.sr.ht/~czan/guix-ruby&amp;quot;)
        (branch &amp;quot;main&amp;quot;))
       %default-channels)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Your next &lt;code&gt;guix pull&lt;/code&gt; will add a new Guix command: &lt;code&gt;guix ruby&lt;/code&gt;!&lt;/p&gt;&lt;h2&gt;Basic Usage&lt;/h2&gt;&lt;p&gt;Running &lt;code&gt;guix ruby&lt;/code&gt; in the root of a Bundler project will read &lt;code&gt;Gemfile&lt;/code&gt; and &lt;code&gt;Gemfile.lock&lt;/code&gt; to produce a new file: &lt;code&gt;Gemfile.lock.scm&lt;/code&gt;. It looks something like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(use-modules ...) ; imports
(lambda* (#:key (ruby ruby) (groups '(default)) (gem-transformers %default-gem-transformers))
  ...) ; gem definitions&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can then &lt;code&gt;load&lt;/code&gt; this file in your &lt;code&gt;manifest.scm&lt;/code&gt; or &lt;code&gt;guix.scm&lt;/code&gt; file, and call the resulting function to get a list of Guix package objects which match your Bundler dependencies.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;;; An example manifest.scm
(define bundler-packages (load &amp;quot;Gemfile.lock.scm&amp;quot;))
(packages-&amp;gt;manifest
 (cons* ruby (bundler-packages #:groups '(default development test))))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Too easy.&lt;/p&gt;&lt;p&gt;When you're first setting up a project, you can ask &lt;code&gt;guix ruby&lt;/code&gt; to create a &lt;code&gt;manifest.scm&lt;/code&gt; file by passing the &lt;code&gt;--init&lt;/code&gt; flag. It will be a bit more complicated than the example above, because it is more explicit about the Ruby version that it uses.&lt;/p&gt;&lt;h3&gt;Adding New Dependencies&lt;/h3&gt;&lt;p&gt;When it's time to add dependencies, you may find that using &lt;code&gt;bundle install&lt;/code&gt; does not work due to a read-only file system. Instead, you can use &lt;code&gt;bundle lock&lt;/code&gt; to update &lt;code&gt;Gemfile.lock&lt;/code&gt;, then run &lt;code&gt;guix ruby&lt;/code&gt; to update &lt;code&gt;Gemfile.lock.scm&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;You can also ask &lt;code&gt;guix ruby&lt;/code&gt; to do this in one step with the &lt;code&gt;--lock&lt;/code&gt; flag.&lt;/p&gt;&lt;h3&gt;Updating Dependencies&lt;/h3&gt;&lt;p&gt;At the moment, &lt;code&gt;guix ruby&lt;/code&gt; doesn't have a shortcut to update dependencies, but &lt;code&gt;bundle lock --update&lt;/code&gt; can still be used.&lt;/p&gt;&lt;h2&gt;Git Dependencies&lt;/h2&gt;&lt;p&gt;Sometimes it's useful to have Bundler clone dependencies directly from Git. This is supported by &lt;code&gt;guix ruby&lt;/code&gt;, which will write an appropriate Guix package definition. However, when using &lt;code&gt;bundle lock&lt;/code&gt; and &lt;code&gt;bundle lock --update&lt;/code&gt;, you may run into issues. Bundler tries to clone the referenced repository in order to get some information about it. Unfortunately, the way &lt;code&gt;Gemfile.lock.scm&lt;/code&gt; defines packages causes this to break (because it defines &lt;code&gt;GEM_HOME&lt;/code&gt; pointing to a read-only directory).&lt;/p&gt;&lt;p&gt;Setting &lt;code&gt;GEM_HOME&lt;/code&gt; to point to some readable directory should be enough to get it working, at the expense of an extra clone of the repos:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ bundle lock --update # no good :(
...
There was an error accessing `/gnu/store/...`
The underlying system error is Errno::EROFS: Read-only file system @ dir_s_mkdir&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ GEM_HOME=/tmp/bundle-cache bundle lock --update # works :)
Fetching https://git.sr.ht/...
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
Writing lockfile to /.../Gemfile.lock&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Gem Transformers&lt;/h2&gt;&lt;p&gt;You may have noticed the &lt;code&gt;gem-transformers&lt;/code&gt; parameter defined in &lt;code&gt;Gemfile.lock.scm&lt;/code&gt;. This provides a mechanism for you to provide &amp;quot;transformations&amp;quot; to the automatically generated packages, to do things like adding inputs that are needed to build extensions. A &lt;a href=&quot;https://git.sr.ht/~czan/guix-ruby/tree/37f411115deddc44049b1d991e8cfa3883b7b6cc/item/guix-ruby/gems.scm#L121&quot;&gt;default set of transformers&lt;/a&gt; is built into Guix Ruby itself, but you can provide your own transformers if a Gem isn't already supported.&lt;/p&gt;&lt;p&gt;I'm keen to support as much as we can in Guix Ruby, though, so if you find you need to define a transformer then let me know on &lt;a href=&quot;mailto:~czan/guix-ruby@lists.sr.ht&quot;&gt;the mailing list&lt;/a&gt; and I can add it to the default set.&lt;/p&gt;&lt;h2&gt;Try it out!&lt;/h2&gt;&lt;p&gt;I've been using &lt;code&gt;guix ruby&lt;/code&gt; for a while to manage the dependencies on my Ruby projects. I've managed to get a few complex projects working using it (including Rails!), and I'm finding it very convenient to use Guix for Ruby development. If you're looking to bring together Ruby and Guix, please try it out. If you run into any issues, let me know on &lt;a href=&quot;mailto:~czan/guix-ruby@lists.sr.ht&quot;&gt;the mailing list&lt;/a&gt; and I'll do my best to help you.&lt;/p&gt;</content></entry><entry><title>Gradual Typing</title><id>https://carlo.zancanaro.id.au/posts/gradual-typing.html</id><author><name>Carlo Zancanaro</name><email>carlo@zancanaro.id.au</email></author><updated>2026-02-06T00:00:00Z</updated><link href="https://carlo.zancanaro.id.au/posts/gradual-typing.html" rel="alternate" /><content type="html">&lt;p&gt;I recently started a PhD at the &lt;a href=&quot;https://anu.edu.au&quot;&gt;Australian National University&lt;/a&gt;, doing research into gradually typed programming languages. Since I'm probably going to have to explain this a lot, I thought I'd write a quick explanation of what gradual typing is. In a future post I will then talk about what specifically I'm doing.&lt;/p&gt;&lt;h2&gt;Static and Dynamic Types&lt;/h2&gt;&lt;p&gt;In programming languages, there has long been a distinction between languages with &lt;em&gt;static&lt;/em&gt; types, and those with &lt;em&gt;dynamic&lt;/em&gt; types. Languages with static types want to know things about the program you're writing (e.g. &lt;code&gt;x&lt;/code&gt; is a number, &lt;code&gt;y&lt;/code&gt; is a &lt;code&gt;Person&lt;/code&gt; record), and they'll refuse to run your code at all if it tries to do something that doesn't make sense (like adding a number to a &lt;code&gt;Person&lt;/code&gt;). Languages with dynamic types, on the other hand, will happily run your program, but will fail part-way through if you do something invalid (like adding a number to a &lt;code&gt;Person&lt;/code&gt;).&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-haskell&quot;&gt;add a b = a + b
data Person = MakePerson
add MakePerson 10 -- Type checking fails, so this won't run at all&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def add(a, b): return a + b
class Person:
  pass
add(Person(), 10) # Fails while the program is running, inside the add function&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These each have their strengths, and their weaknesses.&lt;/p&gt;&lt;p&gt;Static typing is great for catching mistakes before you make them, but it forces particular structures on your programs. It's not enough to just write a correct program, you have to write a program that can be proven correct for all executions within the rules of the type system.&lt;/p&gt;&lt;p&gt;Dynamic typing is great for more exploratory programming, but there are a lot more ways to make mistakes. The flexibility of dynamic typing makes it possible to write programs without having to prove anything about them. During development this is particularly valuable, as it makes it easier to get rapid feedback without having to dot every &amp;quot;i&amp;quot; and cross every &amp;quot;t&amp;quot;.&lt;/p&gt;&lt;h2&gt;Gradual Types&lt;/h2&gt;&lt;p&gt;At a high level, gradual type systems are an attempt to combine the benefits of both static and dynamic types. Systems with gradual types allow parts of your program to have static types, while other parts of your program can use dynamic types.&lt;/p&gt;&lt;p&gt;An example of a gradual type system is &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;, which has static types, but also includes an &lt;code&gt;any&lt;/code&gt; type which behaves like a dynamic type.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const add = (a: any, b: any) =&amp;gt; a + b // any type
class Person { }
add(new Person(), 10) // Passes the type check&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const add = (a: number, b: number) =&amp;gt; a + b // numbers now
class Person { }
add(new Person(), 10) // Type checking fails: a Person is not a number&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These types are called &amp;quot;gradual&amp;quot; because these types allow you to start by using dynamic types during development, and then &lt;em&gt;gradually&lt;/em&gt; add type annotations so the computer can help catch mistakes.&lt;/p&gt;&lt;p&gt;There are a few languages with gradual type systems at the moment (e.g. &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;; &lt;a href=&quot;https://docs.python.org/3/library/typing.html&quot;&gt;Python&lt;/a&gt; with &lt;a href=&quot;https://www.mypy-lang.org/&quot;&gt;mypy&lt;/a&gt;, &lt;a href=&quot;https://github.com/microsoft/pyright&quot;&gt;pyright&lt;/a&gt;, or &lt;a href=&quot;https://github.com/quora/pyanalyze&quot;&gt;pyanalyze&lt;/a&gt;; Ruby with &lt;a href=&quot;https://sorbet.org/&quot;&gt;Sorbet&lt;/a&gt;; &lt;a href=&quot;https://www.php.net/manual/en/language.types.declarations.php&quot;&gt;PHP&lt;/a&gt;; &lt;a href=&quot;https://dart.dev/language/type-system&quot;&gt;Dart&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;In an academic setting, gradual typing is usually discussed in terms of the &lt;a href=&quot;https://jsiek.github.io/home/refined-criteria-gradual.pdf#subsection.4.3&quot;&gt;&lt;em&gt;gradual guarantee&lt;/em&gt;&lt;/a&gt;, which I'll write about another time.&lt;/p&gt;</content></entry><entry><title>Syntax Highlighting in Haunt</title><id>https://carlo.zancanaro.id.au/posts/syntax-highlighting-in-haunt.html</id><author><name>Carlo Zancanaro</name><email>carlo@zancanaro.id.au</email></author><updated>2026-01-10T00:00:00Z</updated><link href="https://carlo.zancanaro.id.au/posts/syntax-highlighting-in-haunt.html" rel="alternate" /><content type="html">&lt;p&gt;I wrote &lt;a href=&quot;/posts/hello-world.html&quot;&gt;a post just under two years ago&lt;/a&gt; about setting up this blog, and then I didn't write anything else! Shame on me.&lt;/p&gt;&lt;p&gt;Part of the issue that got me stuck is that I wanted syntax highlighting. The &amp;quot;standard&amp;quot; option for &lt;a href=&quot;https://dthompson.us/projects/haunt.html&quot;&gt;Haunt&lt;/a&gt; is to use &lt;a href=&quot;https://dthompson.us/projects/guile-syntax-highlight.html&quot;&gt;guile-syntax-highlight&lt;/a&gt;, but it has a pretty minimal set of supported languages. I wasn't satisfied with that.&lt;/p&gt;&lt;p&gt;In the past I've used &lt;a href=&quot;https://pygments.org/&quot;&gt;Pygments&lt;/a&gt;, but I wasn't sure how to get that into Haunt.&lt;/p&gt;&lt;p&gt;Well, it's been two years: I think it's finally time to solve this problem!&lt;/p&gt;&lt;p&gt;I had some trouble finding a way to call an external process, passing something to its &lt;code&gt;stdin&lt;/code&gt; and reading something from its &lt;code&gt;stdout&lt;/code&gt;. In the end, I came up with something like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(define (syntax-highlight-strings strings language)
  (let* ((push (pipe))
         (pull (pipe))
         (pid (spawn &amp;quot;pygmentize&amp;quot; `(&amp;quot;pygmentize&amp;quot; &amp;quot;-f&amp;quot; &amp;quot;html&amp;quot; ,language)
                     #:input (car push)
                     #:output (cdr pull))))
    ;; Write to the subprocess, then close the port
    (close-port (car push))
    (for-each (lambda (b) (display b (cdr push))) strings)
    (close-port (cdr push))

    ;; Read the result back in, then close the port.
    (close-port (cdr pull))
    (let ((result (get-string-all (car pull))))
      (close-port (car pull))
      (waitpid pid)
      result)))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This uses &lt;code&gt;spawn&lt;/code&gt; to start a &lt;code&gt;pygmentize&lt;/code&gt; process, writes a list of strings to it, then reads the result back.&lt;/p&gt;&lt;p&gt;We can then walk the whole tree to find the shape that the &lt;a href=&quot;https://codeberg.org/spritely/guile-commonmark&quot;&gt;guile-commonmark&lt;/a&gt; reader emits for fenced code blocks with a language (which is &lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code class=&amp;quot;language-scheme&amp;quot;&amp;gt;...&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;/code&gt;).&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(define (syntax-highlight sxml-tree)
  (define (language-from-attrs attrs)
    (let ((classname (assoc-ref attrs 'class)))
      (and classname
           (string-prefix? &amp;quot;language-&amp;quot; (car classname))
           (substring (car classname) 9))))

  (match sxml-tree
    (('pre ('code ('@ . attrs) . body))
     (match (html-&amp;gt;shtml (syntax-highlight-strings body (language-from-attrs attrs)))
       (('*TOP* element &amp;quot;\n&amp;quot;)
        `(div (@ ,@attrs) ,element))))
    ((tag ('@ . attrs) . bodies)
     (cons* tag (cons '@ attrs) (map syntax-highlight bodies)))
    ((tag . bodies)
     (cons* tag (map syntax-highlight bodies)))
    (_ sxml-tree)))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then it's just a matter of calling &lt;code&gt;syntax-highlight&lt;/code&gt; on the &lt;code&gt;post-sxml&lt;/code&gt; in our theme's &lt;code&gt;#:post-template&lt;/code&gt; function.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-scheme&quot;&gt;(define my-theme
  (theme #:name &amp;quot;My Made Up Theme&amp;quot;
         #:post-template (lambda (post)
                           `((h1 (,post-title post))
                             (div ,(syntax-highlight (post-sxml post)))))))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This sets up the HTML necessary for colours, but to get actual colours you'll need to generate a stylesheet with the colours you want. The &lt;code&gt;pygmentize&lt;/code&gt; command can do this. This will print the styles to stdout:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pygmentize -S default -f html&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can include these styles in your page (either in a linked &lt;code&gt;.css&lt;/code&gt; file, or inline) and colours should appear.&lt;/p&gt;</content></entry><entry><title>Hello, World!</title><id>https://carlo.zancanaro.id.au/posts/hello-world.html</id><author><name>Carlo Zancanaro</name><email>carlo@zancanaro.id.au</email></author><updated>2024-01-26T00:00:00Z</updated><link href="https://carlo.zancanaro.id.au/posts/hello-world.html" rel="alternate" /><content type="html">&lt;p&gt;As this blog enters the world, let us begin in the customary way: &amp;quot;hello, world!&amp;quot;&lt;/p&gt;&lt;p&gt;This blog will be a place for me to write things. I'm not really trying to achieve anything with this, except to write more words, so keep your expectations low. 🙂&lt;/p&gt;&lt;p&gt;For some reason, I have decided on a slightly odd software stack for this blog: I am using &lt;a href=&quot;https://dthompson.us/projects/haunt.html&quot;&gt;the Haunt static site generator&lt;/a&gt;. It is pretty bare-bones, and requires some up-front setup. Haunt is not much more than a Guile Scheme library for parsing posts and constructing pages from them. It has a few helpers, and a way to define &amp;quot;themes&amp;quot;, but not that much else.&lt;/p&gt;&lt;p&gt;I decided to define a fancy style that I saw in an Org mode exporter time, to put the name of a language on the top-right of a code block.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;pre code:after {
    display: none;
    position: absolute;
    top: 0;
    right: 0;
    padding: 10px;
    outline: 1px solid black;
}
pre:hover code:after {
    display: block;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I need to make sure to define a new style for each language I use, though.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.language-scheme:after { content: &amp;quot;Scheme&amp;quot;; }
.language-css:after { content: &amp;quot;CSS&amp;quot;; }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But I should also add some sort of source highlighting, I guess. I have no idea how hard that is going to be.&lt;/p&gt;&lt;p&gt;Anyway, it's a start!&lt;/p&gt;</content></entry></feed>