Hello!
I've been using GNU Guix to build org-mode for a while now. I recently
cleaned up my build file and I'm now ready to share it!
By putting the guix.scm in the root of the org-mode repository and
running "guix build -f guix.scm" (or to enable parallel builds "guix
build --max-jobs=4 -f guix.scm") you will build many org-mode variants.
Guix takes care of setting everything up in a container like system
seperated from everything else so you don't have to worry about
dependencies or cluttering your user environment with emacs installs.
It is guix convention to have the "guix.scm" file in the root of the
repository but I can move it to the "mk" directory if you'd prefer. Let
me know as I'd have to make a change to the file to do that.
The current variants are:
- Build each outgoing commit
- Build with locale set to "C", "en_US", "fr_FR", "de_DE", and "he_IL"
("zh_CN" and "ja_JP" are commented out due to test failures).
- Build with current emacs, emacs 28, emacs 29, and a development
version of emacs (emacs-next)
- Build with TZ set to "Europe/Istanbul" and "America/New_York"
("Asia/Kathmandu" and "Canada/Newfoundland" are commented out due to
test failures)
- There is also support to test at a specific time but those are
commented out due to test failures when testing near a DST transition.
>From 6b3f2d95f3eb16932c6fdb61750ee1be101930f4 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 | 392 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 392 insertions(+)
create mode 100644 guix.scm
diff --git a/guix.scm b/guix.scm
new file mode 100644
index 000000000..b435f2e21
--- /dev/null
+++ b/guix.scm
@@ -0,0 +1,392 @@
+;;; 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.
+
+(define-module (org-mode-package)
+ #:use-module ((guix licenses) #:prefix license:)
+ #:use-module (gnu packages base)
+ #:use-module (gnu packages check)
+ #:use-module (gnu packages emacs)
+ #:use-module (gnu packages texinfo)
+ #:use-module (guix)
+ #:use-module (guix build utils)
+ #:use-module (guix build-system emacs)
+ #:use-module (guix gexp)
+ #:use-module (guix git)
+ #:use-module (guix git-download)
+ #:use-module (guix packages)
+ #:use-module (guix profiles)
+ #:use-module (guix utils)
+ #:use-module (ice-9 popen)
+ #:use-module (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-public 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-public 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-public 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"
+ (("/bin/sh") (search-input-file inputs "/bin/sh")))
+ ;; FIXME: Fails because we don't have
+ ;; `tramp-encoding-shell' set properly
+ (substitute* "../testing/lisp/test-ob-shell.el"
+ (("ob-shell/remote-with-stdin-or-cmdline .*" all)
+ (string-append all " (skip-unless nil)\n"))))))
+ (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
+ (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-public (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-public (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-public (outgoing-commit-pkgs)
+ "Build all commits between HEAD and upstream."
+ (let ((outgoing-string
+ (git-output "log" "--format=%H" "origin/main..HEAD")))
+ (if (string-null? outgoing-string)
+ #f
+ (map
+ emacs-org-at-commit
+ (string-split
+ outgoing-string
+ #\newline)))))
+
+(apply
+ append
+ (delq #f
+ (list
+ (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: 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")
+
+ (transform-emacs-28-org emacs-org)
+ (transform-emacs-29-org emacs-org)
+ (pkg-with-emacs emacs-org emacs-next-minimal)))))
--
2.54.0