From cbd156907033c6164a1f12fdab917a1d65381893 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 5 Oct 2026 05:12:26 -0500 Subject: fix(make): keep elpa bytecode out of clean-compiled - clean-compiled deleted every .elc in the tree, elpa included. - make clean runs it, so each clean wiped package bytecode. - Nothing here rebuilds it, so packages loaded from source. - Source loads skip native-comp JIT and slow startup. - Emacs 31 also warns per cookie-less file loaded from source. - I made the find skip elpa/ and quote its start point. - New tests run the real target against a throwaway home. - A dry run must show the temp path first. --- Makefile | 10 ++- tests/test-makefile-clean-compiled.el | 121 ++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 tests/test-makefile-clean-compiled.el diff --git a/Makefile b/Makefile index fd759f30..920933a7 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ # make telega-image - Build the telega-server container image # make profile - Profile Emacs startup performance # make clean - Remove test artifacts and compiled files -# make clean-compiled - Remove .elc/.eln files only +# make clean-compiled - Remove .elc/.eln files only (keeps elpa/) # make clean-tests - Remove test artifacts only # make reset - Reset to first launch (destructive!) @@ -106,7 +106,7 @@ help: @echo " make telega-image - Build the telega-server container image" @echo " make profile - Profile Emacs startup performance" @echo " make clean - Remove test artifacts and compiled files" - @echo " make clean-compiled - Remove .elc/.eln files only" + @echo " make clean-compiled - Remove .elc/.eln files only (keeps elpa/)" @echo " make clean-tests - Remove test artifacts only" @echo " make reset - Reset to first launch (DESTRUCTIVE!)" @echo "" @@ -494,9 +494,13 @@ task-sorted: clean: clean-tests clean-compiled @echo "✓ Clean complete" +# elpa/ is excluded: package.el owns that bytecode and nothing here rebuilds it. +# Without it every package loads from source, slower and with no native-comp +# JIT, and Emacs 31 warns about each file lacking a lexical-binding cookie. clean-compiled: @echo "Removing compiled files (.elc, .eln)..." - @find $(EMACS_HOME) -type f \( -name "*.eln" -o -name "*.elc" \) -delete + @find "$(EMACS_HOME)" -type f \( -name "*.eln" -o -name "*.elc" \) \ + -not -path "$(patsubst %/,%,$(EMACS_HOME))/elpa/*" -delete @echo "✓ Compiled files removed" clean-tests: diff --git a/tests/test-makefile-clean-compiled.el b/tests/test-makefile-clean-compiled.el new file mode 100644 index 00000000..0deb54f3 --- /dev/null +++ b/tests/test-makefile-clean-compiled.el @@ -0,0 +1,121 @@ +;;; test-makefile-clean-compiled.el --- Tests for the clean-compiled make target -*- lexical-binding: t -*- + +;;; Commentary: +;; `make clean-compiled' (and `make clean', which depends on it) removes the +;; config's own compiled output. It must leave elpa/ alone: package.el owns +;; that bytecode, nothing in this repo regenerates it, and without it Emacs +;; loads every package from source -- slower, no native-comp JIT, and on +;; Emacs 31 a "Missing `lexical-binding' cookie" warning per cookie-less file. +;; +;; Each test runs the real target against a throwaway EMACS_HOME. Before the +;; real run, a dry run (make -n) confirms the override reached the recipe, so a +;; broken override can never sweep the developer's live tree. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(defconst test-makefile-cc--repo + (file-name-directory + (directory-file-name + (file-name-directory (or load-file-name buffer-file-name)))) + "Repository root holding the Makefile under test.") + +(defun test-makefile-cc--touch (root rel) + "Create an empty file REL under ROOT, making parent dirs." + (let ((path (expand-file-name rel root))) + (make-directory (file-name-directory path) t) + (write-region "" nil path) + path)) + +(defun test-makefile-cc--make (home &optional dry-run) + "Run the clean-compiled target with EMACS_HOME=HOME. +With DRY-RUN, pass -n. Return (EXIT . OUTPUT)." + (with-temp-buffer + (let* ((process-environment + (cl-remove-if (lambda (e) (string-match-p "\\`MAKE\\(FLAGS\\|LEVEL\\)=" e)) + process-environment)) + (args (append (and dry-run '("-n")) + (list "--no-print-directory" + "-C" test-makefile-cc--repo "clean-compiled" + (concat "EMACS_HOME=" (directory-file-name home))))) + (exit (apply #'call-process "make" nil t nil args))) + (cons exit (buffer-string))))) + +(defmacro test-makefile-cc--with-home (files &rest body) + "Create a temp EMACS_HOME holding FILES, bind it to HOME, run BODY. +Refuses to run the real target unless a dry run shows the override applied." + (declare (indent 1)) + `(let ((home (file-name-as-directory (make-temp-file "test-makefile-cc-" t)))) + (unwind-protect + (progn + (dolist (f ,files) (test-makefile-cc--touch home f)) + (let ((dry (test-makefile-cc--make home t))) + (should (= 0 (car dry))) + (should (string-match-p (regexp-quote (directory-file-name home)) + (cdr dry))) + (should-not (string-match-p + (regexp-quote (expand-file-name "~/.emacs.d")) + (cdr dry)))) + ,@body) + (delete-directory home t)))) + +(defun test-makefile-cc--exists (home rel) + "Return non-nil when REL exists under HOME." + (file-exists-p (expand-file-name rel home))) + +(ert-deftest test-makefile-clean-compiled-keeps-elpa-bytecode () + "Normal: package bytecode under elpa/ survives; the config's own is removed." + (test-makefile-cc--with-home '("modules/foo.elc" + "init.elc" + "elpa/pkg-1.0/pkg.el" + "elpa/pkg-1.0/pkg.elc" + "elpa/pkg-1.0/sub/inner.elc") + (should (= 0 (car (test-makefile-cc--make home)))) + (should (test-makefile-cc--exists home "elpa/pkg-1.0/pkg.elc")) + (should (test-makefile-cc--exists home "elpa/pkg-1.0/sub/inner.elc")) + (should (test-makefile-cc--exists home "elpa/pkg-1.0/pkg.el")) + (should-not (test-makefile-cc--exists home "modules/foo.elc")) + (should-not (test-makefile-cc--exists home "init.elc")))) + +(ert-deftest test-makefile-clean-compiled-still-removes-native-code () + "Normal: .eln outside elpa/ is still removed, as before." + (test-makefile-cc--with-home '("eln-cache/31.1-abc/foo.eln" + "modules/foo.elc") + (should (= 0 (car (test-makefile-cc--make home)))) + (should-not (test-makefile-cc--exists home "eln-cache/31.1-abc/foo.eln")) + (should-not (test-makefile-cc--exists home "modules/foo.elc")))) + +(ert-deftest test-makefile-clean-compiled-no-elpa-dir () + "Boundary: a home with no elpa/ at all still cleans and succeeds." + (test-makefile-cc--with-home '("modules/foo.elc") + (should (= 0 (car (test-makefile-cc--make home)))) + (should-not (test-makefile-cc--exists home "modules/foo.elc")))) + +(ert-deftest test-makefile-clean-compiled-only-top-level-elpa-spared () + "Boundary: only the top-level elpa/ is spared, not look-alike paths." + (test-makefile-cc--with-home '(".elpa-mirror/x.elc" + "tests/fixtures/elpa/y.elc" + "elpa/pkg-1.0/pkg.elc") + (should (= 0 (car (test-makefile-cc--make home)))) + (should-not (test-makefile-cc--exists home ".elpa-mirror/x.elc")) + (should-not (test-makefile-cc--exists home "tests/fixtures/elpa/y.elc")) + (should (test-makefile-cc--exists home "elpa/pkg-1.0/pkg.elc")))) + +(ert-deftest test-makefile-clean-compiled-missing-home-fails () + "Error: a nonexistent EMACS_HOME makes the target fail, not silently pass." + (let* ((parent (make-temp-file "test-makefile-cc-missing-" t)) + (home (expand-file-name "absent/" parent))) + (unwind-protect + (progn + (should-not (file-exists-p home)) + (let ((dry (cdr (test-makefile-cc--make home t)))) + (should (string-match-p (regexp-quote (directory-file-name home)) dry)) + (should-not (string-match-p + (regexp-quote (expand-file-name "~/.emacs.d")) dry))) + (should-not (= 0 (car (test-makefile-cc--make home))))) + (delete-directory parent t)))) + +(provide 'test-makefile-clean-compiled) +;;; test-makefile-clean-compiled.el ends here -- cgit v1.2.3