Hello!

I have been working more on my guix build thingy and I've extended it a little.

While I would love to see this get integrated into some CI system (possibly on
sourcehut), I don't think that should prevent us from accepting this patch.  I
belive this has a lot of utility locally for developers.

When I first created the org-habit test suite it did not pass on Ihor's
computer.  Thankfully, I had previously worked on timezone issues so I
was able to quickly figure out it was timezone related.  Next time there
is a system configuration related issue, it might not be as easy to
figure out.  This file gives all developers a way to run code in the
exact same environment.

Also guix is a GNU project and org-mode is sorta almost a GNU project so
having nice integration makes political sense (just pointing this out.
I do realize this is not a good argument).

I've added instructions for creating new builds from the command line.
ex:
=guix build -e '(begin (load "guix.scm") (pkg-with-lang emacs-org "tok"))'=

I've added instructions for using guix to spawn graphical emacs sessions from
within a guix container

I've added a commented out build for LANG=tok and linked to the relevant bug
report in our system.

I've added builds for all packages in guix that depend on org-mode so in theory
we could test our changes on downstream packages.  However this is not quite as
wonderful as it seems as I've pointed out in a comment

#+begin_example
;; This would be a wonderful way to check if our changes affect
;; downstream packages.  However, this function returns very few
;; packages (only 35 as of 2026-07-14) as most are actually built
;; using the org-mode builtin to emacs.  This is not ideal and
;; upstream knows that.
;; See https://codeberg.org/guix/guix/issues/1055
#+end_example

I've also moved the build using emacs-28 and emacs-29 into a variable named
"expensive-builds".  Upstream guix doesn't have an emacs-28 or emacs-29 package
so we can't download binaries.  We have to build emacs before we can run the
org-mode build.

We might not want to run the emacs-28 or 29 build on a CI server with transient
storage as then it would rebuild full emacs each time.  Doing these builds
locally is fine as the build is cached.

It would probably be possible to convince guix to accept the emacs-28 and 29
packages upstream so we could download the binaries from their build server.
Let me know if you want me to work on that.


>From 0d5491440518ae0802b5d1fd7145d4aa75e77252 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Sat, 20 Jun 2026 12:15:54 -0400
Subject: [PATCH] Add guix.scm file to enable building with GNU Guix

This allows us to use Guix as a sort of CI to aid in development.  See
the top of the file for more commentary.

* guix.scm: New file

guix.scm changes
---
 guix.scm | 518 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 518 insertions(+)
 create mode 100644 guix.scm

diff --git a/guix.scm b/guix.scm
new file mode 100644
index 000000000..f6a434c87
--- /dev/null
+++ b/guix.scm
@@ -0,0 +1,518 @@
+;;; guix.scm  --  Build recipe for GNU Guix.
+
+;; File maintained by Morgan Smith <[email protected]>
+
+;; Much of this code is copied from the GNU Guix project (same
+;; licensing of GPLV3+)
+
+;; GNU Guix has a very useful build system that we can use.  It has
+;; the ability to control the build environment completely.
+;;
+;; If you are not already familiar with the "Make" build system, then
+;; you should do that first.
+;;
+;; By running:
+;;     guix build -f guix.scm
+;;
+;; Guix will build all of the package definitions that are returned by
+;; this file (the list at the bottom).
+;;
+;; If we instead run:
+;;     guix build --max-jobs=4 --keep-going -f guix.scm
+;;
+;; Then Guix will run 4 builds at once and keep going even if a build
+;; fails.  The output of all builds will be mixed together but don't
+;; fret.  Guix will save a log file for each failure that you can open
+;; to see only the output of that build.  Look for lines that start
+;; with "View build log at"
+;;
+;; If Guix has already built a package definition, it is smart enough
+;; to not build it again.  You can force it to build with the
+;; "--check" flag.
+;;
+;; New variants can also be defined on the command-line.  For example:
+;;
+;;     guix build -e '(begin (load "guix.scm") (pkg-with-lang emacs-org "tok"))'
+;;
+;;; Guix Shell
+;;
+;; An interactive shell with packages can be created like so:
+;;
+;;     guix shell -e '(begin (load "guix.scm") (list emacs-org-28 emacs-minimal-28))'
+;;
+;; However, running emacs like this will have strange issues as it
+;; will share your system EMACSLOADPATH environment variable and will
+;; have access to your emacs `user-init-file'.  I recommend running
+;; emacs in a container like this:
+;;
+;;     # Preserve TERM environment variable so emacs can run in the terminal
+;;     guix shell --container --preserve='^TERM$' -e '(begin (load "guix.scm") (list emacs-org-28 emacs-minimal-28))' -- emacs
+;;
+;; Using short flags the same command would look like this:
+;;
+;;     guix shell -C -E '^TERM$' -e '(begin (load "guix.scm") (list emacs-org-28 emacs-minimal-28))' -- emacs
+;;
+;; To run a graphical emacs under X11 try this command:
+;; (the emacs minimal variants don't have graphical capabilities)
+;;
+;;     guix shell -C -E '^DISPLAY$' --share=/tmp -e '(begin (load "guix.scm") (list emacs-org-28 emacs-28))' -- emacs
+;;
+;; To run a graphical emacs under wayland try this command:
+;; (this requires a "pgtk" emacs variant)
+;;
+;;     guix shell -C -E '^WAYLAND_DISPLAY$' -E '^XDG_RUNTIME_DIR$' --expose=$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY -e '(begin (load "guix.scm") (list emacs-org emacs-pgtk))' -- emacs
+
+(use-modules ((guix licenses) #:prefix license:)
+             (gnu packages)
+             (gnu packages base)
+             (gnu packages check)
+             (gnu packages emacs)
+             (gnu packages texinfo)
+             (guix)
+             (guix build utils)
+             (guix build-system emacs)
+             (guix gexp)
+             (guix git)
+             (guix git-download)
+             (guix packages)
+             (guix profiles)
+             (guix utils)
+             (ice-9 popen)
+             (ice-9 rdelim))
+
+(define %source-dir (dirname (current-filename)))
+
+(define vcs-file?
+  ;; Return true if the given file is under version control.
+  (or (git-predicate %source-dir #:recursive? #f)
+      (const #t)))                                ;not in a Git checkout
+
+;; Files that are not pulled into the Guix build.  Modifying these
+;; files will not cause Guix to rebuild so it is useful to exclude any
+;; files that are not needed.
+(define exclude-files
+  (map
+   (lambda (file)
+     (string-append %source-dir "/" file))
+   '("guix.scm" "git-hooks")))
+
+(define (emacs-org-build-file? file stat)
+  (if (member file exclude-files)
+      #f
+      (vcs-file? file stat)))
+
+(define (git-output . args)
+  "Execute 'git ARGS ...' command and return its output without trailing
+newspace."
+  (with-directory-excursion %source-dir
+    (let* ((port   (apply open-pipe* OPEN_READ "git" args))
+           (output (read-string port)))
+      (close-pipe port)
+      (string-trim-right output #\newline))))
+
+(define emacs-minimal-28
+  (package
+    (inherit emacs-minimal)
+    (version "28.2")
+    (source (origin
+              (inherit (package-source emacs-minimal))
+              (method url-fetch)
+              (uri (string-append "mirror://gnu/emacs/emacs-"
+                                  version ".tar.xz"))
+              (sha256
+               (base32
+                "12144dcaihv2ymfm7g2vnvdl4h71hqnsz1mljzf34cpg6ci1h8gf"))
+              (patches '())))
+    (arguments
+     (substitute-keyword-arguments arguments
+       ((#:tests? _ #f) #f)
+       ((#:phases phases)
+        #~(modify-phases #$phases
+            (delete 'fix-tests)
+            (delete 'install-site-start)))
+       ((#:substitutable? _ #f) #f)))))
+
+(define emacs-28
+  (package
+    (inherit emacs)
+    (version (package-version emacs-minimal-28))
+    (source (package-source emacs-minimal-28))
+    (arguments
+     (substitute-keyword-arguments arguments
+       ((#:tests? _ #f) #f)
+       ((#:phases phases)
+        #~(modify-phases #$phases
+            (delete 'fix-tests)
+            (delete 'install-site-start)
+            (delete 'build-trampolines)
+            (delete 'validate-comp-integrity)))
+       ;; This keyword is not supported by glib-or-gtk-build-system even
+       ;; though it would work :/
+       ;; ((#:substitutable? _ #f) #f)
+       ))))
+
+(define emacs-minimal-29
+  (package
+    (inherit emacs-minimal)
+    (version "29.4")
+    (source (origin
+              (inherit (package-source emacs-minimal))
+              (method url-fetch)
+              (uri (string-append "mirror://gnu/emacs/emacs-"
+                                  version ".tar.xz"))
+              (sha256
+               (base32
+                "0dd2mh6maa7dc5f49qdzj7bi4hda4wfm1cvvgq560djcz537k2ds"))
+              (patches '())))
+    (arguments
+     (substitute-keyword-arguments arguments
+       ((#:tests? _ #f) #f)
+       ((#:substitutable? _ #f) #f)))))
+
+(define emacs-29
+  (package
+    (inherit emacs)
+    (version (package-version emacs-minimal-29))
+    (source (package-source emacs-minimal-29))
+    (arguments
+     (substitute-keyword-arguments arguments
+       ((#:tests? _ #f) #f)
+       ((#:phases phases)
+        #~(modify-phases #$phases
+            (delete 'fix-tests)
+            (delete 'install-site-start)
+            (delete 'build-trampolines)
+            (delete 'validate-comp-integrity)))
+       ;; This keyword is not supported by glib-or-gtk-build-system even
+       ;; though it would work :/
+       ;; ((#:substitutable? _ #f) #f)
+       ))))
+
+(define emacs-org
+  (package
+    (name "emacs-org")
+    (version "10.0-pre") ;; FIXME: pull version from org.el header
+    (source (local-file "." "org-mode-checkout"
+                        #:recursive? #t
+                        #:select? emacs-org-build-file?))
+    (build-system emacs-build-system)
+    (arguments
+     (list
+      #:substitutable? #f ;; Don't upstream this
+      ;; For some reason if I don't specify the '#:emacs' argument in
+      ;; this package then `substitute-keyword-arguments' fails to
+      ;; modify it in my transformations
+      #:emacs emacs-minimal
+      #:lisp-directory "lisp"
+      #:tests? #t
+      #:disallowed-references (map cadr (package-native-inputs this-package))
+      #:test-command #~(list "make" "-C" ".." "test-dirty")
+      #:phases
+      #~(modify-phases %standard-phases
+          (add-after 'unpack 'patch-paths
+            (lambda* (#:key inputs #:allow-other-keys)
+              (substitute* "ob-shell.el"
+                (("/usr/bin/env")
+                 (search-input-file inputs "/bin/env")))))
+          (replace 'make-autoloads
+            (lambda _
+              (invoke "make" "-C" ".." "autoloads" (string-append "ORGVERSION=" #$version))))
+          (add-before 'check 'fix-tests
+            (lambda* (#:key tests? inputs #:allow-other-keys)
+              (when tests?
+                (setenv "HOME" (getcwd))
+                ;; These files are modified during testing.
+                (with-directory-excursion "../testing/examples"
+                  (for-each make-file-writable
+                            '("babel.org"
+                              "ob-awk-test.org"
+                              "ob-sed-test.org"))
+                  ;; Specify where sh executable is.
+                  (let ((sh (search-input-file inputs "/bin/sh")))
+                    (substitute* "babel.org"
+                      (("/bin/sh") sh))))
+                ;; FIXME: Fix failure in ob-tangle/collect-blocks.
+                ;; The test assumes that ~/../.. corresponds to /.
+                ;; This isn't true in our case.
+                (substitute* "../testing/lisp/test-ob-tangle.el"
+                  ((" ~/\\.\\./\\.\\./")
+                   (string-append " ~"
+                                  ;; relative path from ${HOME} to / during
+                                  ;; build
+                                  (string-join
+                                   (map-in-order
+                                    (lambda (x)
+                                      (if (equal? x "") "" ".."))
+                                    (string-split (getcwd) #\/)) "/")
+                                  "/")))
+                (substitute* '("../testing/lisp/test-ob-shell.el"
+                               "../testing/org-test.el")
+                  (("/bin/sh") (search-input-file inputs "/bin/sh")))
+                ;; FIXME: tell guix that the upstream guix emacs
+                ;; package should set `tramp-encoding-shell'.
+                (substitute* "../testing/lisp/test-ob-shell.el"
+                  (("ob-shell/remote-with-stdin-or-cmdline .*" all)
+                   (string-append all "  (skip-unless nil)\n")
+                   ;; Substituting this instead works on some of the
+                   ;; package definitions but not others.  I'm not
+                   ;; sure why
+                   ;; (string-append all
+                   ;;                "  (setq tramp-encoding-shell \"sh\")")
+                   )))))
+          (add-after 'install 'install-org-test
+            (lambda _
+              (with-directory-excursion "../testing"
+                (copy-recursively "."
+                                  (string-append (elpa-directory #$output:test))))))
+          (add-after 'install 'install-org-documentation
+            (lambda _
+              ;; FIXME: these files end up in a different location
+              ;; then the upstream package
+              (invoke "make" "-C" ".." "install-info" "install-etc"
+                      (string-append "ORGVERSION=" #$version)
+                      (string-append
+                       "prefix=" #$output))
+              (install-file "../etc/ORG-NEWS"
+                            (string-append #$output "/share/doc/"
+                                           #$name "-" #$version)))))))
+    (native-inputs
+     (list texinfo tzdata-for-tests))
+    (home-page "https://orgmode.org/";)
+    (synopsis "Outline-based notes management and organizer")
+    (description "Org is an Emacs mode for keeping notes, maintaining TODO
+lists, and project planning with a fast and effective lightweight markup
+language.  It also is an authoring system with unique support for literate
+programming and reproducible research.  If you work with the LaTeX output
+capabilities of Org-mode, you may want to install the
+@code{emacs-org-texlive-collection} meta-package, which propagates the TexLive
+components required by the produced @file{.tex} file.")
+    (license license:gpl3+)))
+
+(define* (emacs-org-at-commit commit #:optional (revision "0"))
+  (package
+    (inherit emacs-org)
+    (version (git-version (package-version emacs-org) revision commit))
+    (source (git-checkout
+              (url %source-dir)
+              (commit commit)))))
+
+(define (timezone-transformer timezone)
+  (lambda (pkg)
+    (package
+      (inherit pkg)
+      (name (string-append
+             (package-name pkg)
+             "-TZ=" (string-replace-substring timezone "/" "-")))
+      (arguments
+       (substitute-keyword-arguments arguments
+         ((#:phases phases)
+          #~(modify-phases #$phases
+              (add-after 'unpack 'set-timezone
+                (lambda _
+                  (setenv "TZ" #$timezone))))))))))
+
+(define (pkg-with-timezone pkg timezone)
+  ((timezone-transformer timezone) pkg))
+
+(define (lang-transformer lang)
+  (lambda (pkg)
+    (package
+      (inherit pkg)
+      (name (string-append (package-name pkg) "-LANG=" lang))
+      (arguments
+       (substitute-keyword-arguments arguments
+         ;; This keyword is not supported by emacs-build-system even
+         ;; though it would work :/
+         ;; ((#:locale _ #f) lang)
+         ((#:phases phases)
+          #~(modify-phases #$phases
+              (replace 'install-locale
+                (lambda _
+                  ;; install-locale demotes errors into warnings so I
+                  ;; have to promote them to errors again
+                  (let ((error-string
+                         (with-error-to-string
+                           (lambda ()
+                             ((assoc-ref #$phases 'install-locale) #:locale #$lang)))))
+                    (if (string-prefix? "warning:" error-string)
+                        (error error-string)
+                        (format (current-error-port) error-string)))))))))
+      (native-inputs (modify-inputs native-inputs
+                       (prepend glibc-locales))))))
+
+(define (pkg-with-lang pkg lang)
+  ((lang-transformer lang) pkg))
+
+(define (pkg-at-time pkg time)
+  (package
+    (inherit pkg)
+    (name (string-append (package-name pkg) "-at-time-"
+                         (string-filter
+                          ;; Unfortunately, guix doesn't expose this charset for us to use
+                          ;; (@@ (guix store) %store-item-charset)
+                          (char-set-adjoin char-set:letter+digit #\-)
+                          (string-replace-substring time " " "T"))))
+    (arguments
+     (substitute-keyword-arguments arguments
+       ((#:phases phases)
+        #~(modify-phases #$phases
+            (add-after 'unpack 'use-libfaketime
+              (lambda* (#:key inputs #:allow-other-keys)
+                (substitute* "../mk/default.mk"
+                  ;; FIXME: This fixes the tests erroring with
+                  ;; "File %s changed on disk" but I really should
+                  ;; figure out a better solution
+                  (("^EMACS\t= emacs")
+                   "EMACS\t= emacs --eval='(setq query-about-changed-file nil)'"))
+                (setenv "FAKETIME" #$time)
+                (setenv "FAKETIME_DONT_RESET" "1")
+                (setenv "LD_PRELOAD"
+                        (search-input-file
+                         inputs
+                         "lib/faketime/libfaketimeMT.so.1"))))))))
+    (native-inputs (modify-inputs native-inputs
+                     (prepend libfaketime)))))
+
+(define (emacs-input-transformer emacs)
+  "Return a transformation procedure that replaces package emacs inputs
+with EMACS."
+  (package-input-rewriting/spec
+   (map (lambda (pkg)
+          (cons pkg (const emacs)))
+        (list
+         "emacs-next-pgtk"
+         "emacs"
+         "emacs-minimal"
+         "emacs-no-x"
+         "emacs-no-x-toolkit"))))
+
+(define (pkg-with-emacs pkg emacs)
+  ((emacs-input-transformer emacs) pkg))
+
+(define pkg-with-emacs-28
+  (emacs-input-transformer emacs-minimal-28))
+
+(define pkg-with-emacs-29
+  (emacs-input-transformer emacs-minimal-29))
+
+(define (transform-emacs-28-org org)
+  (pkg-with-emacs-28
+   (package
+     (inherit org)
+     (name "emacs-28-org")
+     (arguments
+      (substitute-keyword-arguments arguments
+        ((#:phases phases)
+         #~(modify-phases #$phases
+             (delete 'ensure-package-description))))))))
+
+(define emacs-org-28 (transform-emacs-28-org emacs-org))
+
+(define (transform-emacs-29-org org)
+  (pkg-with-emacs-29
+   (package
+     (inherit org)
+     (name "emacs-29-org")
+     (arguments
+      (substitute-keyword-arguments arguments
+        ((#:phases phases)
+         #~(modify-phases #$phases
+             (delete 'ensure-package-description))))))))
+
+(define emacs-org-29 (transform-emacs-29-org emacs-org))
+
+(define (outgoing-commit-pkgs)
+  "Build all commits between HEAD and upstream."
+  (let ((outgoing-string
+         (git-output "rev-list" "@{upstream}..")))
+    (if (string-null? outgoing-string)
+        #f
+        (map
+         emacs-org-at-commit
+         (string-split
+          outgoing-string
+          #\newline)))))
+
+(define (get-direct-dependent-packages pkg-name)
+  "Return the list of packages that have a direct dependency named
+PKG-NAME."
+  (fold-packages
+   (lambda (package lst)
+     (if (member
+          pkg-name
+          (map car (package-direct-inputs package)))
+         (cons package lst)
+         lst))
+   (list)))
+
+;; This would be a wonderful way to check if our changes affect
+;; downstream packages.  However, this function returns very few
+;; packages (only 35 as of 2026-07-14) as most are actually built
+;; using the org-mode builtin to emacs.  This is not ideal and
+;; upstream knows that.
+;; See https://codeberg.org/guix/guix/issues/1055
+(define (emacs-org-dependent-packages-with-org org)
+  "Return the list of packages that depend on emacs-org with that
+dependency replaced with ORG."
+  (let ((pkgs (get-direct-dependent-packages "emacs-org"))
+        (transformer
+         (package-input-rewriting/spec
+          (list (cons "emacs-org" (const org))))))
+    (map transformer pkgs)))
+
+(define expensive-builds
+  (cons*
+   ;; There is no upstream package for these emacs so they have
+   ;; to built on the local system
+   (transform-emacs-28-org emacs-org)
+   (transform-emacs-29-org emacs-org)
+
+   (emacs-org-dependent-packages-with-org emacs-org)))
+
+(apply
+ append
+ (delq #f
+       (list
+        expensive-builds
+        (outgoing-commit-pkgs)
+        (map
+         (lambda (lang)
+           (pkg-with-lang emacs-org lang))
+         ;; FIXME: also test non utf8 locales
+         '("C.UTF-8"
+           "en_US.UTF-8"
+           "fr_FR.UTF-8"
+           "de_DE.UTF-8"
+           "he_IL.UTF-8"
+           ;; FIXME: These fail 'test-org-clock/clocktable/lang'
+           ;; "zh_CN.UTF-8"
+           ;; "ja_JP.UTF-8"
+           ;; FIXME: Failure when abbreviated day name contains spaces
+           ;; see: https://list.orgmode.org/ch3pr84mb342413ebc2dd0045ec233792c5...@ch3pr84mb3424.namprd84.prod.outlook.com
+           ;; "tok.UTF-8"
+           ))
+        ;; FIXME: all of these fail
+        ;; Test DST transitions in EST
+        ;; (let ((emacs-org-est (pkg-with-timezone emacs-org "America/New_York")))
+        ;;   (list
+        ;;    ;; (spring forward) [2009-03-08 01:59] -> [2009-03-08 03:00]
+        ;;    (pkg-at-time emacs-org-est "@2009-03-08 01:00:00")
+        ;;    (pkg-at-time emacs-org-est "@2009-03-08 01:30:00")
+        ;;    (pkg-at-time emacs-org-est "@2009-03-08 03:00:00")
+        ;;    (pkg-at-time emacs-org-est "@2009-03-08 03:30:00")
+        ;;    ;; fall back [2009-11-01 01:59] -> [2009-11-01 01:00]
+        ;;    (pkg-at-time emacs-org-est "@2009-11-01 01:00:00")
+        ;;    (pkg-at-time emacs-org-est "@2009-11-01 01:30:00")
+        ;;    (pkg-at-time emacs-org-est "@2009-11-01 02:00:00")
+        ;;    (pkg-at-time emacs-org-est "@2009-11-01 02:30:00")))
+
+        (list
+         emacs-org
+
+         ;; FIXME: test failures when timezone is not an integer number of hours
+         ;; (pkg-with-timezone emacs-org "Asia/Kathmandu")
+         ;; (pkg-with-timezone emacs-org "Canada/Newfoundland")
+         (pkg-with-timezone emacs-org "Europe/Istanbul")
+         (pkg-with-timezone emacs-org "America/New_York")
+
+         (pkg-with-emacs emacs-org emacs-next-minimal)))))
-- 
2.54.0

Reply via email to