diff --git a/elisp/BUILD b/elisp/BUILD index 4e5011d2..317b5906 100644 --- a/elisp/BUILD +++ b/elisp/BUILD @@ -508,6 +508,42 @@ bootstrap( ], ) +bootstrap( + name = "gen-pkg-el", + src = "gen-pkg-el.el", + out = "gen-pkg-el.elc", + visibility = [ + # FIXME: Make private once + # https://github.com/bazelbuild/proposals/blob/main/designs/2019-10-15-tool-visibility.md + # is implemented in all supported Bazel versions. + "//visibility:public", + ], +) + +bootstrap( + name = "gen-metadata", + src = "gen-metadata.el", + out = "gen-metadata.elc", + visibility = [ + # FIXME: Make private once + # https://github.com/bazelbuild/proposals/blob/main/designs/2019-10-15-tool-visibility.md + # is implemented in all supported Bazel versions. + "//visibility:public", + ], +) + +bootstrap( + name = "gen-autoloads", + src = "gen-autoloads.el", + out = "gen-autoloads.elc", + visibility = [ + # FIXME: Make private once + # https://github.com/bazelbuild/proposals/blob/main/designs/2019-10-15-tool-visibility.md + # is implemented in all supported Bazel versions. + "//visibility:public", + ], +) + build_test( name = "compile_test", targets = [":compile"], diff --git a/elisp/defs.bzl b/elisp/defs.bzl index 68609db1..b0b7c83d 100644 --- a/elisp/defs.bzl +++ b/elisp/defs.bzl @@ -63,6 +63,12 @@ additions for this library and all its transitive dependencies. The `depset` uses preorder traversal: entries for libraries closer to the root of the dependency graph come first. The `depset` elements are structures as described in the provider documentation.""", + "package_file": """A `File` object for the -pkg.el file. +None if `enable_package` is False.""", + "metadata_file": """A `File` object for the metadata file. +None if `enable_package` is False.""", + "autoloads_file": """A `File` object for the autoloads file. +None if `enable_package` is False.""", }, ) @@ -77,9 +83,19 @@ def _elisp_library_impl(ctx): tags = ctx.attr.tags, fatal_warnings = ctx.attr.fatal_warnings, ) + extra_out = [] + package_file = None + metadata_file = None + autoloads_file = None + if ctx.attr.enable_package and not ctx.attr.testonly: + pkg = _build_package(ctx, ctx.files.srcs, ctx.files.data) + extra_out += [pkg.package_file, pkg.metadata_file, pkg.autoloads_file] + package_file = pkg.package_file + metadata_file = pkg.metadata_file + autoloads_file = pkg.autoloads_file return [ DefaultInfo( - files = depset(direct = result.outs), + files = depset(direct = result.outs + extra_out), runfiles = result.runfiles, ), coverage_common.instrumented_files_info( @@ -95,6 +111,9 @@ def _elisp_library_impl(ctx): transitive_source_files = result.transitive_srcs, transitive_compiled_files = result.transitive_outs, transitive_load_path = result.transitive_load_path, + package_file = package_file, + metadata_file = metadata_file, + autoloads_file = autoloads_file, ), ] @@ -334,6 +353,34 @@ To add a load path entry for the current package, specify `.` here.""", doc = "List of `elisp_library` dependencies.", providers = [EmacsLispInfo], ), + "enable_package": attr.bool( + doc = """Enable generation of package.el package for this library. +This value is forced to False if testonly is True.""", + default = True, + ), + "emacs_package_name": attr.string( + doc = """The name used for the package.el package. +This attribute is ignored if enable_package is False. +Otherwise, srcs should contain a package description file `-pkg.el`. +If there is no such package description file, then srcs must contain a file +`.el` containing the appropriate package headers. + +If there is only one file in srcs, then the default value is the file basename +with the .el suffix removed. Otherwise, the default is the target label name, +with underscores replaced with dashes.""", + ), + "_gen_pkg_el": attr.label( + default = "//elisp:gen-pkg-el.elc", + allow_single_file = [".elc"], + ), + "_gen_metadata": attr.label( + default = "//elisp:gen-metadata.elc", + allow_single_file = [".elc"], + ), + "_gen_autoloads": attr.label( + default = "//elisp:gen-autoloads.elc", + allow_single_file = [".elc"], + ), }, doc = """Byte-compiles Emacs Lisp source files and makes the compiled output available to dependencies. All sources are byte-compiled. @@ -1226,6 +1273,170 @@ def _resolve_load_path(ctx, dir): ), ) +def _get_emacs_package_name(ctx): + """Returns the package name to use for `elisp_library' rules.""" + if ctx.attr.emacs_package_name: + return ctx.attr.emacs_package_name + if len(ctx.files.srcs) != 1: + return ctx.label.name.replace("_", "-") + basename = ctx.files.srcs[0].basename + if not basename.endswith(".el"): + fail("Suspicious single file when guessing package_name for target", ctx.label) + if basename.endswith("-pkg.el"): + fail("Suspicious package_name derived from single source file for target", ctx.label) + return basename[:-len(".el")] + +def _build_package(ctx, srcs, data): + """Build package files. + + Args: + ctx (ctx): rule context + srcs (list of Files): Emacs Lisp sources files + data (list of Files): data files + + Returns: + A structure with the following fields: + package_file: the File object for the -pkg.el file + metadata_file: the File object containing the package metadata + autoloads_file: the File object for the autoloads file + """ + package_name = _get_emacs_package_name(ctx) + + pkg_file = None + + # Try to find an existing -pkg.el file + expected = package_name + "-pkg.el" + for file in srcs + data: + if file.basename == expected: + pkg_file = file + break + + # Generate a -pkg.el file + if pkg_file == None: + expected = package_name + ".el" + for file in srcs + data: + if file.basename == expected: + pkg_file = _generate_pkg_el(ctx, file) + break + if pkg_file == None: + fail("No package metadata found for target", ctx.label) + + # Try to find an existing autoloads file + autoloads_file = None + expected = package_name + "-autoloads.el" + for file in srcs + data: + if file.basename == expected: + autoloads_file = file + break + if autoloads_file == None: + autoloads_file = _generate_autoloads(ctx, package_name, srcs) + metadata_file = _generate_metadata(ctx, pkg_file) + return struct( + package_file = pkg_file, + metadata_file = metadata_file, + autoloads_file = autoloads_file, + ) + +def _generate_pkg_el(ctx, src): + """Generate -pkg.el file. + + Args: + ctx (ctx): rule context + src (File): Emacs Lisp source file to parse for package metadata + + Returns: + the File object for the -pkg.el file + """ + package_name = src.basename.rsplit(".")[0] + out = ctx.actions.declare_file(paths.join( + _OUTPUT_DIR, + ctx.attr.name, + "{}-pkg.el".format(package_name), + )) + inputs = depset(direct = [src, ctx.file._gen_pkg_el]) + run_emacs( + ctx = ctx, + arguments = [ + "--load=" + ctx.file._gen_pkg_el.path, + "--funcall=elisp/gen-pkg-el-and-exit", + src.path, + out.path, + ], + inputs = inputs, + outputs = [out], + tags = ctx.attr.tags, + mnemonic = "GenPkgEl", + progress_message = "Generating -pkg.el {}".format(out.short_path), + manifest_basename = out.basename, + manifest_sibling = out, + ) + return out + +def _generate_metadata(ctx, package_file): + """Generate metadata file. + + Args: + ctx (ctx): rule context + package_file (File): the File object for the -pkg.el file + + Returns: + The File object for the metadata file + """ + if not package_file.basename.endswith("-pkg.el"): + fail("Unexpected package_file", package_file) + package_name = package_file.basename[:-len("-pkg.el")] + out = ctx.actions.declare_file(paths.join(_OUTPUT_DIR, ctx.attr.name, "{}.json".format(package_name))) + inputs = depset(direct = [package_file, ctx.file._gen_metadata]) + run_emacs( + ctx = ctx, + arguments = [ + "--load=" + ctx.file._gen_metadata.path, + "--funcall=elisp/gen-metadata-and-exit", + package_file.path, + out.path, + ], + inputs = inputs, + outputs = [out], + tags = ctx.attr.tags, + mnemonic = "GenMetadata", + progress_message = "Generating metadata {}".format(out.short_path), + manifest_basename = out.basename, + manifest_sibling = out, + ) + return out + +def _generate_autoloads(ctx, package_name, srcs): + """Generate autoloads file. + + Args: + ctx (ctx): rule context + package_name (string): name of package + srcs (list of Files): Emacs Lisp source files for which to generate autoloads + + Returns: + The generated File. + """ + out = ctx.actions.declare_file(paths.join(_OUTPUT_DIR, ctx.attr.name, "{}-autoloads.el".format(package_name))) + inputs = depset(direct = srcs + [ctx.file._gen_autoloads]) + run_emacs( + ctx = ctx, + arguments = [ + "--load=" + ctx.file._gen_autoloads.path, + "--funcall=elisp/gen-autoloads-and-exit", + out.path, + package_name, + ctx.actions.args().add_all(srcs), + ], + inputs = inputs, + outputs = [out], + tags = ctx.attr.tags, + mnemonic = "GenAutoloads", + progress_message = "Generating autoloads {}".format(out.short_path), + manifest_basename = out.basename, + manifest_sibling = out, + ) + return out + # Directory relative to the current package where to store compiled files. This # is equivalent to _objs for C++ rules. See # https://bazel.build/remote/output-directories#layout-diagram. diff --git a/elisp/gen-autoloads.el b/elisp/gen-autoloads.el new file mode 100644 index 00000000..6bba9e72 --- /dev/null +++ b/elisp/gen-autoloads.el @@ -0,0 +1,78 @@ +;;; gen-autoloads.el --- generate autoloads file -*- lexical-binding: t; -*- + +;; Copyright 2021 Google LLC +;; +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; https://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;;; Commentary: + +;; Generate an autoloads file. +;; +;; Usage: +;; +;; emacs --quick --batch --load=gen-autoloads.el \ +;; --funcall=elisp/gen-autoloads-and-exit DEST PKGNAME SOURCE... +;; +;; Generates an autoloads file DEST for the SOURCE Emacs Lisp files. +;; +;; Exits with a zero status only if successful. + +;;; Code: + +(require 'cl-lib) +(require 'nadvice) +(require 'package) + +(defun elisp/gen-autoloads-and-exit () + "Generate an autoloads file and exit Emacs. +See the file commentary for details." + (unless noninteractive + (error "This function works only in batch mode")) + + ;; Make output deterministic. + ;; The autoload definitions file contains the current time, therefore we + ;; override `current-time'. + (advice-add 'current-time :override (lambda () '(0 0 0 0))) + ;; Return constant file attributes to make builds deterministic. + (advice-add 'file-attributes :around + (lambda (func &rest args) + (let ((ret (apply func args))) + (when ret + (cl-destructuring-bind + (type links _uid _gid _atime _mtime _ctime + size mode _change _inode _dev) + ret + (list type links 0 0 '(0 0 0 0) '(0 0 0 0) '(0 0 0 0) + size mode nil 42 42)))))) + (add-to-list 'ignored-local-variables 'generated-autoload-file) + + (let* ((out (pop command-line-args-left)) + (pkgname (pop command-line-args-left)) + (srcs command-line-args-left) + (workdir (file-name-as-directory (make-temp-file "workdir" :dir))) + ;; Leaving these enabled leads to undefined behavior and doesn’t make + ;; sense in batch mode. + (attempt-stack-overflow-recovery nil) + (attempt-orderly-shutdown-on-fatal-signal nil) + (create-lockfiles nil)) + (dolist (f srcs) + (copy-file f workdir)) + (package-generate-autoloads pkgname workdir) + (copy-file + (expand-file-name (concat workdir (format "%s-autoloads.el" pkgname)) + workdir) + out t) + (kill-emacs 0))) + +(provide 'elisp/gen-autoloads) +;;; gen-autoloads.el ends here diff --git a/elisp/gen-metadata.el b/elisp/gen-metadata.el new file mode 100644 index 00000000..f65bbfb1 --- /dev/null +++ b/elisp/gen-metadata.el @@ -0,0 +1,63 @@ +;;; gen-metadata.el --- generate package info file -*- lexical-binding: t; -*- + +;; Copyright 2021 Google LLC +;; +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; https://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;;; Commentary: + +;; Generate a package metadata file. +;; +;; Usage: +;; +;; emacs --quick --batch --load=gen-metadata.el \ +;; --funcall=elisp/gen-metadata-and-exit SOURCE DEST +;; +;; Generates a metadata file DEST using the SOURCE -pkg.el. +;; This is a JSON file that contains package metadata to allow easier extraction +;; for non-Emacs tools. +;; +;; Exits with a zero status only if successful. + +;;; Code: + +(require 'package) +(require 'json) + +(defun elisp/gen-metadata-and-exit () + "Generate package metadata file and exit Emacs. +See the file commentary for details." + (unless noninteractive + (error "This function works only in batch mode")) + (pcase command-line-args-left + (`(,src ,out) + (let* ( + ;; Leaving these enabled leads to undefined behavior and doesn’t + ;; make sense in batch mode. + (attempt-stack-overflow-recovery nil) + (attempt-orderly-shutdown-on-fatal-signal nil) + (metadata (with-temp-buffer + (insert-file-contents-literally src) + (or (package-process-define-package + (read (current-buffer))) + (error "Can't find define-package in %s" src))))) + (write-region + (json-encode + `((name . ,(package-desc-name metadata)) + (version . ,(package-version-join (package-desc-version metadata))))) + nil out) + (kill-emacs 0))) + (_ (error "Usage: emacs elisp/gen-metadata.el SOURCE DEST")))) + +(provide 'elisp/gen-metadata) +;;; gen-metadata.el ends here diff --git a/elisp/gen-pkg-el.el b/elisp/gen-pkg-el.el new file mode 100644 index 00000000..3225f9e9 --- /dev/null +++ b/elisp/gen-pkg-el.el @@ -0,0 +1,57 @@ +;;; gen-pkg-el.el --- generate -pkg.el file -*- lexical-binding: t; -*- + +;; Copyright 2021 Google LLC +;; +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; https://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;;; Commentary: + +;; Generate a -pkg.el file. +;; +;; Usage: +;; +;; emacs --quick --batch --load=gen-pkg-el.el \ +;; --funcall=elisp/gen-pkg-el-and-exit SOURCE DEST +;; +;; Generates a -pkg.el file DEST, using the headers from the SOURCE Emacs Lisp +;; file. +;; +;; SOURCE should contain the headers as +;; described in Info node `(elisp)Simple Packages'. +;; +;; Exits with a zero status only if successful. + +;;; Code: + +(require 'package) +(require 'lisp-mnt) + +(defun elisp/gen-pkg-el-and-exit () + "Generate a -pkg.el file and exit Emacs. +See the file commentary for details." + (unless noninteractive + (error "This function works only in batch mode")) + (let* ((src (pop command-line-args-left)) + (out (pop command-line-args-left)) + ;; Leaving these enabled leads to undefined behavior and doesn’t make + ;; sense in batch mode. + (attempt-stack-overflow-recovery nil) + (attempt-orderly-shutdown-on-fatal-signal nil) + (pkginfo (with-temp-buffer + (insert-file-contents-literally src) + (package-buffer-info)))) + (package-generate-description-file pkginfo out) + (kill-emacs 0))) + +(provide 'elisp/gen-pkg-el) +;;; gen-pkg-el.el ends here