diff --git a/elisp/BUILD b/elisp/BUILD index f6ebb288..4e1a25b1 100644 --- a/elisp/BUILD +++ b/elisp/BUILD @@ -25,6 +25,12 @@ package( licenses(["notice"]) +exports_files([ + "gen-pkg-el.el", + "gen-metadata.el", + "gen-autoloads.el", +]) + toolchain_type( name = "toolchain_type", visibility = ["//visibility:public"], diff --git a/elisp/defs.bzl b/elisp/defs.bzl index 69f1bb81..91d5a9b1 100644 --- a/elisp/defs.bzl +++ b/elisp/defs.bzl @@ -58,6 +58,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.""", }, ) @@ -81,9 +87,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( @@ -99,6 +115,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, ), ] @@ -394,6 +413,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.el", + allow_single_file = [".el"], + ), + _gen_metadata = attr.label( + default = "//elisp:gen-metadata.el", + allow_single_file = [".el"], + ), + _gen_autoloads = attr.label( + default = "//elisp:gen-autoloads.el", + allow_single_file = [".el"], + ), ), doc = """Byte-compiles Emacs Lisp source files and makes the compiled output available to dependencies. All sources are byte-compiled. @@ -1026,6 +1073,169 @@ def _load_directory_for_actions(directory): # map_each above. return check_relative_filename(directory.for_actions) +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: + 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, + 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..8d941a64 --- /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 SOURCE... +;; +;; Generates an autoloads file DEST for the SOURCE Emacs Lisp files. +;; +;; Exits with a zero status only if successful. + +;;; Code: + +(require 'autoload) +(require 'cl-lib) +(require 'nadvice) + +(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)) + (srcs 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) + (create-lockfiles nil) + (workdir (file-name-as-directory (make-temp-file "workdir" :dir))) + (generated-autoload-file (expand-file-name (file-name-nondirectory out) workdir))) + (unwind-protect + (progn + (write-region (autoload-rubric out "package" :feature) nil generated-autoload-file) + (dolist (f srcs) + (copy-file f workdir)) + (update-directory-autoloads workdir) + (copy-file generated-autoload-file out t)) + (delete-directory workdir :recursive)) + (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..02cb4001 --- /dev/null +++ b/elisp/gen-metadata.el @@ -0,0 +1,59 @@ +;;; 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")) + (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) + (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))) + +(provide 'elisp/gen-metadata) +;;; gen-metadata.el ends here diff --git a/elisp/gen-metadata.elc b/elisp/gen-metadata.elc new file mode 100644 index 00000000..3bf62796 Binary files /dev/null and b/elisp/gen-metadata.elc differ 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 diff --git a/elisp/proto/BUILD b/elisp/proto/BUILD index 3678f02c..0de24e7a 100644 --- a/elisp/proto/BUILD +++ b/elisp/proto/BUILD @@ -128,6 +128,7 @@ elisp_library( "@platforms//os:macos": ["module.so"], "@platforms//os:windows": ["module.dll"], }), + enable_package = False, ) cc_binary( diff --git a/elisp/runfiles/runfiles.el b/elisp/runfiles/runfiles.el index f3d6bd6f..dbda8d75 100644 --- a/elisp/runfiles/runfiles.el +++ b/elisp/runfiles/runfiles.el @@ -14,6 +14,8 @@ ;; See the License for the specific language governing permissions and ;; limitations under the License. +;; Version: 0.1.0 + ;;; Commentary: ;; This library implements support for Bazel runfiles. diff --git a/examples/BUILD b/examples/BUILD index ada7b0e1..8e985288 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -48,6 +48,7 @@ elisp_library( srcs = [ "lib-2-a.el", "lib-2-b.el", + "lib-2-pkg.el", ] + select({ "@platforms//os:linux": ["module.so"], "@platforms//os:macos": ["module.so"], diff --git a/examples/lib-1.el b/examples/lib-1.el index 9c39ff6b..63df7e73 100644 --- a/examples/lib-1.el +++ b/examples/lib-1.el @@ -14,6 +14,8 @@ ;; See the License for the specific language governing permissions and ;; limitations under the License. +;; Version: 0.1.0 + ;;; Commentary: ;; Example library. diff --git a/examples/lib-2-pkg.el b/examples/lib-2-pkg.el new file mode 100644 index 00000000..06374183 --- /dev/null +++ b/examples/lib-2-pkg.el @@ -0,0 +1,15 @@ +;; Copyright 2023 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. + +(define-package "lib-2" "0.1.0") diff --git a/tests/test-lib.el b/tests/test-lib.el index ddb06643..7b28fba0 100644 --- a/tests/test-lib.el +++ b/tests/test-lib.el @@ -14,6 +14,8 @@ ;; See the License for the specific language governing permissions and ;; limitations under the License. +;; Version: 0.1.0 + ;;; Commentary: ;; Example functions to test coverage reporting. This has to be in a separate