;;; admin.el --- utilities for Emacs administration
;; Copyright (C) 2001-2021 Free Software Foundation, Inc.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see .
;;; Commentary:
;; add-release-logs Add ``Version X released'' change log entries.
;; set-version Change Emacs version number in source tree.
;; set-copyright Change Emacs short copyright string (eg as
;; printed by --version) in source tree.
;;; Code:
(defvar add-log-time-format) ; in add-log
(defun add-release-logs (root version &optional date)
"Add \"Version VERSION released.\" change log entries in ROOT.
Also update the etc/HISTORY file.
Root must be the root of an Emacs source tree.
Optional argument DATE is the release date, default today."
(interactive (list (read-directory-name "Emacs root directory: ")
(read-string "Version number: "
(format "%s.%s" emacs-major-version
emacs-minor-version))
(read-string "Release date: "
(progn (require 'add-log)
(funcall add-log-time-format nil t)))))
(setq root (expand-file-name root))
(unless (file-exists-p (expand-file-name "src/emacs.c" root))
(user-error "%s doesn't seem to be the root of an Emacs source tree" root))
(let ((clog (expand-file-name "ChangeLog" root)))
(if (file-exists-p clog)
;; Basic check that a ChangeLog that exists is not your personal one.
;; TODO Perhaps we should move any existing file and unconditionally
;; call make ChangeLog? Or make ChangeLog CHANGELOG=temp and compare
;; with the existing?
(with-temp-buffer
(insert-file-contents clog)
(or (re-search-forward "^[ \t]*Copyright.*Free Software" nil t)
(user-error "ChangeLog looks like a personal one - remove it?")))
(or
(zerop (call-process "make" nil nil nil "-C" root "ChangeLog"))
(error "Problem generating ChangeLog"))))
(require 'add-log)
(or date (setq date (funcall add-log-time-format nil t)))
(let* ((logs (process-lines "find" root "-name" "ChangeLog"))
(entry (format "%s %s <%s>\n\n\t* Version %s released.\n\n"
date
(or add-log-full-name (user-full-name))
(or add-log-mailing-address user-mail-address)
version)))
(dolist (log logs)
(find-file log)
(goto-char (point-min))
(insert entry)))
(let ((histfile (expand-file-name "etc/HISTORY" root)))
(unless (file-exists-p histfile)
(error "%s not present" histfile))
(find-file histfile)
(goto-char (point-max))
(search-backward "")
(insert (format "GNU Emacs %s (%s) emacs-%s\n\n" version date version))))
(defun set-version-in-file (root file version rx)
"Subroutine of `set-version' and `set-copyright'."
(find-file (expand-file-name file root))
(goto-char (point-min))
(setq version (format "%s" version))
(unless (re-search-forward rx nil :noerror)
(user-error "Version not found in %s" file))
(if (not (equal version (match-string 1)))
(replace-match version nil nil nil 1)
(kill-buffer)
(message "No need to update `%s'" file)))
(defun set-version (root version)
"Set Emacs version to VERSION in relevant files under ROOT.
Root must be the root of an Emacs source tree."
(interactive (list
(read-directory-name "Emacs root directory: " source-directory)
(read-string "Version number: " emacs-version)))
(unless (file-exists-p (expand-file-name "src/emacs.c" root))
(user-error "%s doesn't seem to be the root of an Emacs source tree" root))
(message "Setting version numbers...")
;; There's also a "version 3" (standing for GPLv3) at the end of
;; `README', but since `set-version-in-file' only replaces the first
;; occurrence, it won't be replaced.
(set-version-in-file root "README" version
(rx (and "version" (1+ space)
(submatch (1+ (in "0-9."))))))
(set-version-in-file root "configure.ac" version
(rx (and "AC_INIT" (1+ (not (in ?,)))
?, (0+ space)
(submatch (1+ (in "0-9."))))))
(set-version-in-file root "nt/README.W32" version
(rx (and "version" (1+ space)
(submatch (1+ (in "0-9."))))))
;; TODO: msdos could easily extract the version number from
;; configure.ac with sed, rather than duplicating the information.
(set-version-in-file root "msdos/sed2v2.inp" version
(rx (and bol "/^#undef " (1+ not-newline)
"define PACKAGE_VERSION" (1+ space) "\""
(submatch (1+ (in "0-9."))))))
;; Major version only.
(when (string-match "\\([0-9]\\{2,\\}\\)" version)
(let ((newmajor (match-string 1 version)))
(set-version-in-file root "src/msdos.c" newmajor
(rx (and "Vwindow_system_version" (1+ not-newline)
?\( (submatch (1+ (in "0-9"))) ?\))))
(set-version-in-file root "etc/refcards/ru-refcard.tex" newmajor
"\\\\newcommand{\\\\versionemacs}\\[0\\]\
{\\([0-9]\\{2,\\}\\)}.+%.+version of Emacs")))
(let* ((oldversion
(with-temp-buffer
(insert-file-contents (expand-file-name "README" root))
(if (re-search-forward "version \\([0-9.]*\\)" nil t)
(version-to-list (match-string 1)))))
(oldmajor (if oldversion (car oldversion)))
(newversion (version-to-list version))
(newmajor (car newversion))
(newshort (format "%s.%s" newmajor
(+ (cadr newversion)
(if (eq 2 (length newversion)) 0 1))))
(majorbump (and oldversion (not (equal oldmajor newmajor))))
(minorbump (and oldversion (not majorbump)
(or (not (equal (cadr oldversion) (cadr newversion)))
;; Eg 26.2 -> 26.2.50.
(and (> (length newversion)
(length oldversion))))))
(newsfile (expand-file-name "etc/NEWS" root))
(oldnewsfile (expand-file-name (format "etc/NEWS.%s" oldmajor) root)))
(unless (> (length newversion) 2) ; pretest or release candidate?
(with-temp-buffer
(insert-file-contents newsfile)
(when (re-search-forward "^\\* [^\n]*\n+" nil t)
(display-warning 'admin
"NEWS file contains empty sections - remove them?"))
(goto-char (point-min))
(if (re-search-forward "^\\(\\+\\+\\+ *$\\|--- *$\\|Temporary note:\\)" nil t)
(display-warning 'admin
"NEWS file still contains temporary markup.
Documentation changes might not have been completed!"))))
(when (and majorbump
(not (file-exists-p oldnewsfile)))
(rename-file newsfile oldnewsfile)
(find-file oldnewsfile) ; to prompt you to commit it
(copy-file oldnewsfile newsfile)
(with-temp-buffer
(insert-file-contents newsfile)
(re-search-forward "is about changes in Emacs version \\([0-9]+\\)")
(replace-match (number-to-string newmajor) nil nil nil 1)
(re-search-forward "^See files \\(NEWS\\)")
(unless (save-match-data
(when (looking-at "\\(\\..*\\), \\(\\.\\.\\.\\|…\\)")
(replace-match
(format ".%s, NEWS.%s" oldmajor (1- oldmajor))
nil nil nil 1)
t))
(replace-match (format "NEWS.%s, NEWS" oldmajor) nil nil nil 1)
(let ((start (line-beginning-position)))
(search-forward "in older Emacs versions")
(or (equal start (line-beginning-position))
(fill-region start (line-beginning-position 2)))))
(re-search-forward "^$")
(forward-line -1)
(let ((start (point)))
(goto-char (point-max))
(re-search-backward "^$" nil nil 2)
(delete-region start (line-beginning-position 0)))
(write-region nil nil newsfile)))
(when (or majorbump minorbump)
(find-file newsfile)
(goto-char (point-min))
(if (re-search-forward (format "^\\* .*in Emacs %s" newshort) nil t)
(progn
(kill-buffer)
(message "No need to update etc/NEWS"))
(goto-char (point-min))
(re-search-forward "^$")
(forward-line -1)
(dolist (s '("Installation Changes" "Startup Changes" "Changes"
"Editing Changes"
"Changes in Specialized Modes and Packages"
"New Modes and Packages"
"Incompatible Lisp Changes"
"Lisp Changes"))
(insert (format "\n\n* %s in Emacs %s\n" s newshort)))
(insert (format "\n\n* Changes in Emacs %s on \
Non-Free Operating Systems\n" newshort)))
;; Because we skip "bump version" commits when merging between branches.
;; Probably doesn't matter in practice, because NEWS changes
;; will only happen on master anyway.
(message "Commit any NEWS changes separately")))
(message "Setting version numbers...done"))
;; Note this makes some assumptions about form of short copyright.
(defun set-copyright (root copyright)
"Set Emacs short copyright to COPYRIGHT in relevant files under ROOT.
Root must be the root of an Emacs source tree."
(interactive (list
(read-directory-name "Emacs root directory: " nil nil t)
(read-string
"Short copyright string: "
(format "Copyright (C) %s Free Software Foundation, Inc."
(format-time-string "%Y")))))
(unless (file-exists-p (expand-file-name "src/emacs.c" root))
(user-error "%s doesn't seem to be the root of an Emacs source tree" root))
(message "Setting copyrights...")
(set-version-in-file root "configure.ac" copyright
(rx (and bol "copyright" (0+ (not (in ?\")))
?\" (submatch (1+ (not (in ?\")))) ?\")))
(set-version-in-file root "msdos/sed2v2.inp" copyright
(rx (and bol "/^#undef " (1+ not-newline)
"define COPYRIGHT" (1+ space)
?\" (submatch (1+ (not (in ?\")))) ?\")))
(set-version-in-file root "lib-src/rcs2log" copyright
(rx (and "Copyright" (0+ space) ?= (0+ space)
?\' (submatch (1+ nonl)))))
(when (string-match "\\([0-9]\\{4\\}\\)" copyright)
(setq copyright (match-string 1 copyright))
(set-version-in-file root "etc/refcards/ru-refcard.tex" copyright
"\\\\newcommand{\\\\cyear}\\[0\\]\
{\\([0-9]\\{4\\}\\)}.+%.+copyright year")
(set-version-in-file root "etc/refcards/emacsver.tex.in" copyright
"\\\\def\\\\year\
{\\([0-9]\\{4\\}\\)}.+%.+copyright year"))
(message "Setting copyrights...done"))
;;; Various bits of magic for generating the web manuals
(defun manual-misc-manuals (root)
"Return doc/misc manuals as list of strings.
ROOT should be the root of an Emacs source tree."
;; Similar to `make -C doc/misc echo-info', but works if unconfigured,
;; and for INFO_TARGETS rather than INFO_INSTALL.
(with-temp-buffer
(insert-file-contents (expand-file-name "doc/misc/Makefile.in" root))
;; Should really use expanded value of INFO_TARGETS.
(search-forward "INFO_COMMON = ")
(let ((start (point)))
(end-of-line)
(while (and (looking-back "\\\\")
(zerop (forward-line 1)))
(end-of-line))
(append (split-string (replace-regexp-in-string
"\\(\\\\\\|\\.info\\)" ""
(buffer-substring start (point))))
'("efaq-w32")))))
;; TODO report the progress
(defun make-manuals (root &optional type)
"Generate the web manuals for the Emacs webpage.
ROOT should be the root of an Emacs source tree.
Interactively with a prefix argument, prompt for TYPE.
Optional argument TYPE is type of output (nil means all)."
(interactive (let ((root
(if noninteractive
(or (pop command-line-args-left)
default-directory)
(read-directory-name "Emacs root directory: "
source-directory nil t))))
(list root
(if current-prefix-arg
(completing-read
"Type: "
(append
'("misc" "pdf" "ps")
(let (res)
(dolist (i '("emacs" "elisp" "eintr") res)
(dolist (j '("" "-mono" "-node" "-ps" "-pdf"))
(push (concat i j) res))))
(manual-misc-manuals root)))))))
(let* ((dest (expand-file-name "manual" root))
(html-node-dir (expand-file-name "html_node" dest))
(html-mono-dir (expand-file-name "html_mono" dest))
(ps-dir (expand-file-name "ps" dest))
(pdf-dir (expand-file-name "pdf" dest))
(emacs (expand-file-name "doc/emacs/emacs.texi" root))
(emacs-xtra (expand-file-name "doc/emacs/emacs-xtra.texi" root))
(elisp (expand-file-name "doc/lispref/elisp.texi" root))
(eintr (expand-file-name "doc/lispintro/emacs-lisp-intro.texi" root))
(misc (manual-misc-manuals root)))
;; TODO this makes it non-continuable.
;; Instead, delete the individual dest directory each time.
(when (file-directory-p dest)
(if (y-or-n-p (format "Directory %s exists, delete it first? " dest))
(delete-directory dest t)
(user-error "Aborted")))
(if (member type '(nil "emacs" "emacs-node"))
(manual-html-node emacs (expand-file-name "emacs" html-node-dir)))
(if (member type '(nil "emacs" "emacs-mono"))
(manual-html-mono emacs (expand-file-name "emacs.html" html-mono-dir)))
(when (member type '(nil "emacs" "emacs-pdf" "pdf"))
(manual-pdf emacs (expand-file-name "emacs.pdf" pdf-dir))
;; emacs-xtra exists only in pdf/ps format.
;; In other formats it is included in the Emacs manual.
(manual-pdf emacs-xtra (expand-file-name "emacs-xtra.pdf" pdf-dir)))
(when (member type '(nil "emacs" "emacs-ps" "ps"))
(manual-ps emacs (expand-file-name "emacs.ps" ps-dir))
(manual-ps emacs-xtra (expand-file-name "emacs-xtra.ps" ps-dir)))
(if (member type '(nil "elisp" "elisp-node"))
(manual-html-node elisp (expand-file-name "elisp" html-node-dir)))
(if (member type '(nil "elisp" "elisp-mono"))
(manual-html-mono elisp (expand-file-name "elisp.html" html-mono-dir)))
(if (member type '(nil "elisp" "elisp-pdf" "pdf"))
(manual-pdf elisp (expand-file-name "elisp.pdf" pdf-dir)))
(if (member type '(nil "elisp" "elisp-ps" "ps"))
(manual-ps elisp (expand-file-name "elisp.ps" ps-dir)))
(if (member type '(nil "eintr" "eintr-node"))
(manual-html-node eintr (expand-file-name "eintr" html-node-dir)))
(if (member type '(nil "eintr" "eintr-node"))
(manual-html-mono eintr (expand-file-name "eintr.html" html-mono-dir)))
(if (member type '(nil "eintr" "eintr-pdf" "pdf"))
(manual-pdf eintr (expand-file-name "eintr.pdf" pdf-dir)))
(if (member type '(nil "eintr" "eintr-ps" "ps"))
(manual-ps eintr (expand-file-name "eintr.ps" ps-dir)))
;; Misc manuals
(dolist (manual misc)
(if (member type `(nil ,manual "misc"))
(manual-misc-html manual root html-node-dir html-mono-dir)))
(message "Manuals created in %s" dest)))
(defconst manual-doctype-string
"\n\n")
(defconst manual-meta-string
"
\n\n")
(defconst manual-style-string "\n")
(defun manual-misc-html (name root html-node-dir html-mono-dir)
;; Hack to deal with the cases where .texi creates a different .info.
;; Blech. TODO Why not just rename the .texi (or .info) files?
(let* ((texiname (cond ((equal name "ccmode") "cc-mode")
(t name)))
(texi (expand-file-name (format "doc/misc/%s.texi" texiname) root)))
(manual-html-node texi (expand-file-name name html-node-dir))
(manual-html-mono texi (expand-file-name (concat name ".html")
html-mono-dir))))
(defvar manual-makeinfo (or (getenv "MAKEINFO") "makeinfo")
"The `makeinfo' program to use.")
(defvar manual-texi2pdf (or (getenv "TEXI2PDF") "texi2pdf")
"The `texi2pdf' program to use.")
(defvar manual-texi2dvi (or (getenv "TEXI2DVI") "texi2dvi")
"The `texi2dvi' program to use.")
(defun manual-html-mono (texi-file dest)
"Run Makeinfo on TEXI-FILE, emitting mono HTML output to DEST.
This function also edits the HTML files so that they validate as
HTML 4.01 Transitional, and pulls in the gnu.org stylesheet using
the @import directive."
(make-directory (or (file-name-directory dest) ".") t)
(call-process manual-makeinfo nil nil nil
"-D" "WWW_GNU_ORG"
"-I" (expand-file-name "../emacs"
(file-name-directory texi-file))
"-I" (expand-file-name "../misc"
(file-name-directory texi-file))
"--html" "--no-split" texi-file "-o" dest)
(with-temp-buffer
(insert-file-contents dest)
(setq buffer-file-name dest)
(manual-html-fix-headers)
(manual-html-fix-index-1)
(manual-html-fix-index-2 t)
(manual-html-fix-node-div)
(goto-char (point-max))
(re-search-backward "