Subject: bug#79172: [PATCH] Add SPDX support to copyright.el

From: Antero Mejr <[email-masked]>
Date: Mon, 04 Aug 2025 21:36:54 +0000

Tags: patch

This patch updates copyright.el to auto-insert SPDX- declarations. There
is one new user-facing command 'copyright-spdx' and a few new
customizable variables.

It also makes the 'copyright' command use 'comment-add' so that the
comment marker ;; is correct in Lisp modes, for both the SPDX and
default formats.

While working on this I also noticed the copyright skeletons in
autoinsert.el are inconsistent - they have an extra space relative to
those of copyright.el. IMO autoinsert.el should be adjusted to use the
copyright.el code instead of reimplementing it.

In GNU Emacs 31.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version
 3.24.33, cairo version 1.16.0) of 2025-05-04 built on pop-os
Repository revision: 159e3a981ed5482393182b036e38818d42405c90
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12101004
System Description: Pop!_OS 22.04 LTS

>From 8b630420d40e094ccd84f12ba0235c80f4c7a57f Mon Sep 17 00:00:00 2001
From: Antero Mejr <[email-masked]>
Date: Mon, 4 Aug 2025 20:38:59 +0000
Subject: [PATCH] Add SPDX support to copyright.el

* lisp/emacs-lisp/copyright.el (copyright): Use SPDX format if
`copyright-spdx-enable' is t.
(copyright): Respect `comment-add' variable and add space.
(copyright-spdx): New autoloaded command.
(copyright-spdx-insert): New procedure.
(copyright-spdx-enable, copyright-spdx-filecopyrighttext-regexp,
 copyright-spdx-default-license): New custom-set variables.
* etc/NEWS: Announce.
* .dir-locals.el (nil): Set `copyright-spdx-enable' to nil.
* doc/misc/autotype.texi (Copyrights): Mention SPDX command and
variables.
---
 .dir-locals.el               |  3 +-
 doc/misc/autotype.texi       | 16 ++++++++
 etc/NEWS                     | 17 +++++++++
 lisp/emacs-lisp/copyright.el | 72 +++++++++++++++++++++++++++++++-----
 4 files changed, 97 insertions(+), 11 deletions(-)

diff --git a/.dir-locals.el b/.dir-locals.el
index af92eac5bba..bd43c3fc6e9 100644
--- a/.dir-locals.el
+++ b/.dir-locals.el
@@ -15,7 +15,8 @@
              "/[ \t]*DEFVAR_[A-Z_ \t(]+\"[^\"]+\",[ \t]\\([A-Za-z0-9_]+\\)/\\1/"))))
          (etags-regen-ignores . ("test/manual/etags/"))
          (vc-prepare-patches-separately . nil)
-         (vc-default-patch-addressee . "[email-masked]")))
+         (vc-default-patch-addressee . "[email-masked]")
+         (copyright-spdx-enable . nil)))
  (c-mode . ((c-file-style . "GNU")
             (c-noise-macro-names . ("INLINE" "NO_INLINE" "ATTRIBUTE_NO_SANITIZE_UNDEFINED"
                                     "ATTRIBUTE_NO_SANITIZE_ADDRESS"
diff --git a/doc/misc/autotype.texi b/doc/misc/autotype.texi
index c53be54d0af..1735952efec 100644
--- a/doc/misc/autotype.texi
+++ b/doc/misc/autotype.texi
@@ -385,6 +385,22 @@ Copyrights
 such as in the @code{before-save-hook} feature mentioned above.  Otherwise
 you are always queried.
 
+@vindex copyright-spdx-enable
+  The variable @code{copyright-spdx-enable} controls whether to use the
+@uref{https://spdx.dev/, SPDX} format when inserting copyright
+declarations. When this variable is set to @code{t}, the
+@code{SPDX-FileCopyrightText} declaration format will be used.
+
+@findex copyright-spdx
+@vindex copyright-spdx-default-license
+  Some projects use the @code{SPDX-License-Identifier: } declaration at
+the top of each source code file. The @code{copyright-spdx} command will
+automatically insert that declaration, using the correct comment syntax
+if applicable. If the variable @code{copyright-spdx-default-license} is
+set, it will use that @uref{https://spdx.org/licenses/, SDPX license
+identifier} to populate its field instead of prompting for an identifier
+string.
+

 @node Executables
diff --git a/etc/NEWS b/etc/NEWS
index e12a9dcb127..998cbe86b91 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -676,6 +676,23 @@ linefeeds".
 You can now use 'C-u C-x .' to clear the fill prefix, similarly to how
 you could already use 'C-u C-x C-n' to clear the goal column.
 
+** New command `copyright-spdx' and variable `copyright-spdx-enable'
+The 'copyright-spdx' command inserts an "SPDX-License-Identifier" at
+point. If 'copyright-spdx-default-license' is set, it will autofill with
+its value, otherwise it will prompt for a license identifier string.
+
+The `copyright-spdx-enable' variable, if set to t, indicates that the
+'copyright' command should use the "SPDX-FileCopyrightText: " format.
+
++++
+** 'copyright' command now respects 'comment-add'
+Previously, the 'copyright' command would insert a single comment mark
+and no additional spaces, even when 'comment-add' was non-zero (such as
+in lisp-derived modes). Now 'copyright' will insert the correct number
+of comment marks along with a space, so that the text is correctly
+formatted.
+
+---
 
 * Changes in Specialized Modes and Packages in Emacs 31.1
 
diff --git a/lisp/emacs-lisp/copyright.el b/lisp/emacs-lisp/copyright.el
index 59a2b166a9d..9eb404cec72 100644
--- a/lisp/emacs-lisp/copyright.el
+++ b/lisp/emacs-lisp/copyright.el
@@ -144,7 +144,9 @@ copyright-find-copyright
   (with-demoted-errors "Can't update copyright: %s"
     ;; (1) Need the extra \\( \\) around copyright-regexp because we
     ;; goto (match-end 1) below. See note (2) below.
-    (let ((regexp (concat "\\(" copyright-regexp
+    (let ((regexp (concat "\\(" (if copyright-spdx-enable
+                                    copyright-spdx-filecopyrighttext-regexp
+                                  copyright-regexp)
 			  "\\)\\([ \t]*\n\\)?.*\\(?:"
 			  copyright-names-regexp "\\)")))
       (when (copyright-re-search regexp (copyright-limit) t)
@@ -343,16 +345,27 @@ copyright-fix-years
     (message "No copyright message")))
 
 ;;;###autoload
-(define-skeleton copyright
+(defun copyright ()
   "Insert a copyright by $ORGANIZATION notice at cursor."
-  "Company: "
-  comment-start
-  "Copyright (C) " `(format-time-string "%Y") " by "
-  (or (getenv "ORGANIZATION")
-      str)
-  '(if (copyright-offset-too-large-p)
-       (message "Copyright extends beyond `copyright-limit' and won't be updated automatically."))
-  comment-end \n)
+  (interactive)
+  (let ((org (or (getenv "ORGANIZATION")
+                 (read-string "Company: "))))
+    (if copyright-spdx-enable
+        (copyright-spdx-insert org)
+      (copyright-insert org))))
+
+(defun copyright-comment-start ()
+  "Get a comment-start string repeated the correct number of times."
+  (apply #'concat (mapcar (lambda (x)
+                            comment-start)
+                          (number-sequence 0 comment-add))))
+
+(defun copyright-insert (org)
+    (insert (copyright-comment-start)
+            " Copyright (C) " (format-time-string "%Y") " by " org
+            comment-end "\n")
+  (when (copyright-offset-too-large-p)
+    (message "Copyright extends beyond `copyright-limit' and won't be updated automatically.")))
 
 ;; TODO: recurse, exclude COPYING etc.
 ;;;###autoload
@@ -374,6 +387,45 @@ copyright-update-directory
       (save-buffer)
       (kill-buffer (current-buffer)))))
 
+;; SPDX features
+
+;; TODO: It may be possible to accurately infer SPDX usage.
+(defcustom copyright-spdx-enable nil
+  "Non-nil means copyright notices are expected to be in SPDX format."
+  :type 'boolean
+  :safe #'booleanp)
+
+(defcustom copyright-spdx-filecopyrighttext-regexp
+  "\\(SPDX-FileCopyrightText\\s *:?\\s *\\(?:(C)\\)?\
+\\|SPDX-FileCopyrightText\\s *:?\\s *©\\)\
+\\s *[^0-9\n]*\\s *\
+\\([1-9]\\([-0-9, ';/*%#\n\t–]\\|\\s<\\|\\s>\\)*[0-9]+\\)"
+  "Regex to find the SPDX-FileCopyrightText: declaration"
+  :type 'regexp
+  :safe 'stringp)
+
+(defcustom copyright-spdx-default-license nil
+  "If non-nil, specify SPDX-License-Identifier to use by default."
+  :type 'stringp
+  :safe 'stringp)
+
+;;;###autoload
+(defun copyright-spdx (&optional identifier)
+  "Insert an SPDX-License-Identifier: at cursor.
+Will prompt unless `copyright-spdx-default-license' is set."
+  (interactive
+   (list (or copyright-spdx-default-license
+             (read-string "SPDX License Identifier: "))))
+  (insert (copyright-comment-start) " SPDX-License-Identifier: "
+          identifier comment-end "\n"))
+
+(defun copyright-spdx-insert (org)
+  "Insert an SPDX-FileCopyrightText: declaration."
+  (insert (copyright-comment-start) " SPDX-FileCopyrightText: "
+          (format-time-string "%Y") " " org comment-end "\n")
+  (when (copyright-offset-too-large-p)
+    (message "Copyright extends beyond `copyright-limit' and won't be updated automatically.")))
+
 (provide 'copyright)
 
 ;;; copyright.el ends here
-- 
2.34.1