;;; helm-make.el --- Select a Makefile target with helm ;; Copyright (C) 2014 Oleh Krehel ;; Author: Oleh Krehel ;; URL: https://github.com/abo-abo/helm-make ;; Package-Version: 20160807.1756 ;; Version: 0.2.0 ;; Package-Requires: ((helm "1.5.3") (projectile "0.11.0")) ;; Keywords: makefile ;; This file is not part of GNU Emacs ;; This file 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, or (at your option) ;; any later version. ;; This program 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. ;; For a full copy of the GNU General Public License ;; see . ;;; Commentary: ;; ;; A call to `helm-make' will give you a `helm' selection of this directory ;; Makefile's targets. Selecting a target will call `compile' on it. ;;; Code: (require 'helm) (require 'helm-multi-match) (declare-function ivy-read "ext:ivy") (defgroup helm-make nil "Select a Makefile target with helm." :group 'convenience) (defcustom helm-make-do-save nil "If t, save all open buffers visiting files from Makefile's directory." :type 'boolean :group 'helm-make) (defcustom helm-make-build-dir "" "Specify a build directory for an out of source build. The path should be relative to the project root. When non-nil `helm-make-projectile' will first look in that directory for a makefile." :type '(string) :group 'helm-make) (make-variable-buffer-local 'helm-make-build-dir) (defcustom helm-make-sort-targets nil "Whether targets shall be sorted. If t, targets will be sorted as a final step before calling the completion method. HINT: If you are facing performance problems set this to nil. This might be the case, if there are thousand of targets." :type 'boolean :group 'helm-make) (defcustom helm-make-cache-targets nil "Whether to cache the targets or not. If t, cache targets of Makefile. If `helm-make' or `helm-make-projectile' gets called for the same Makefile again, and the Makefile hasn't changed meanwhile, i.e. the modification time is `equal' to the cached one, reuse the cached targets, instead of recomputing them. If nil do nothing. You can reset the cache by calling `helm-make-reset-db'." :type 'boolean :group 'helm-make) (defcustom helm-make-executable "make" "Store the name of make executable." :type 'string :group 'helm-make) (defcustom helm-make-require-match t "When non-nil, don't allow selecting a target that's not on the list." :type 'boolean) (defcustom helm-make-named-buffer nil "When non-nil, name compilation buffer based on make target." :type 'boolean) (defcustom helm-make-comint nil "When non-nil, run helm-make in Comint mode instead of Compilation mode." :type 'boolean) (defvar helm-make-command nil "Store the make command.") (defvar helm-make-target-history nil "Holds the recently used targets.") (defvar helm-make-makefile-names '("Makefile" "makefile" "GNUmakefile") "List of Makefile names which make recognizes. An exception is \"GNUmakefile\", only GNU make unterstand it.") (defun helm--make-action (target) "Make TARGET." (let* ((make-command (format helm-make-command target)) (compile-buffer (compile make-command helm-make-comint))) (when helm-make-named-buffer (helm--make-rename-buffer compile-buffer target)))) (defun helm--make-rename-buffer (buffer target) "Rename the compilation BUFFER based on the make TARGET." (let ((buffer-name (format "*compilation (%s)*" target))) (when (get-buffer-window buffer-name) (delete-window (get-buffer-window buffer-name))) (when (get-buffer buffer-name) (kill-buffer buffer-name)) (with-current-buffer buffer (rename-buffer buffer-name)))) (defcustom helm-make-completion-method 'helm "Method to select a candidate from a list of strings." :type '(choice (const :tag "Helm" helm) (const :tag "Ido" ido) (const :tag "Ivy" ivy))) ;;;###autoload (defun helm-make (&optional arg) "Call \"make -j ARG target\". Target is selected with completion." (interactive "p") (setq helm-make-command (format "%s -j%d %%s" helm-make-executable arg)) (let ((makefile (helm--make-makefile-exists default-directory))) (if makefile (helm--make makefile) (error "No Makefile in %s" default-directory)))) (defun helm--make-target-list-qp (makefile) "Return the target list for MAKEFILE by parsing the output of \"make -nqp\"." (let ((default-directory (file-name-directory (expand-file-name makefile))) targets target) (with-temp-buffer (insert (shell-command-to-string "make -nqp __BASH_MAKE_COMPLETION__=1 .DEFAULT 2>/dev/null")) (goto-char (point-min)) (unless (re-search-forward "^# Files" nil t) (error "Unexpected \"make -nqp\" output")) (while (re-search-forward "^\\([^%$:#\n\t ]+\\):\\([^=]\\|$\\)" nil t) (setq target (match-string 1)) (unless (or (save-excursion (goto-char (match-beginning 0)) (forward-line -1) (looking-at "^# Not a target:")) (string-match "^\\([/a-zA-Z0-9_. -]+/\\)?\\." target)) (push target targets)))) targets)) (defun helm--make-target-list-default (makefile) "Return the target list for MAKEFILE by parsing it." (let (targets) (with-temp-buffer (insert-file-contents makefile) (goto-char (point-min)) (while (re-search-forward "^\\([^: \n]+\\):" nil t) (let ((str (match-string 1))) (unless (string-match "^\\." str) (push str targets))))) targets)) (defcustom helm-make-list-target-method 'default "Method of obtaining the list of Makefile targets." :type '(choice (const :tag "Default" default) (const :tag "make -qp" qp))) (defun helm--make-makefile-exists (base-dir &optional dir-list) "Check if one of `helm-make-makefile-names' exist in BASE-DIR. Returns the absolute filename to the Makefile, if one exists, otherwise nil. If DIR-LIST is non-nil, also search for `helm-make-makefile-names'." (let* ((default-directory (file-truename base-dir)) (makefiles (progn (unless (and dir-list (listp dir-list)) (setq dir-list (list ""))) (let (result) (dolist (dir dir-list) (dolist (makefile helm-make-makefile-names) (push (expand-file-name makefile dir) result))) (reverse result))))) (cl-find-if 'file-exists-p makefiles))) (defvar helm-make-db (make-hash-table :test 'equal) "An alist of Makefile and corresponding targets.") (cl-defstruct helm-make-dbfile targets modtime sorted) (defun helm--make-cached-targets (makefile) "Return cached targets of MAKEFILE. If there are no cached targets for MAKEFILE, the MAKEFILE modification time has changed, or `helm-make-cache-targets' is nil, parse the MAKEFILE, and cache targets of MAKEFILE, if `helm-make-cache-targets' is t." (let* ((att (file-attributes makefile 'integer)) (modtime (if att (nth 5 att) nil)) (entry (gethash makefile helm-make-db nil)) (new-entry (make-helm-make-dbfile)) (targets (cond ((and helm-make-cache-targets entry (equal modtime (helm-make-dbfile-modtime entry)) (helm-make-dbfile-targets entry)) (helm-make-dbfile-targets entry)) (t (delete-dups (if (eq helm-make-list-target-method 'default) (helm--make-target-list-default makefile) (helm--make-target-list-qp makefile))))))) (when helm-make-sort-targets (unless (and helm-make-cache-targets entry (helm-make-dbfile-sorted entry)) (setq targets (sort targets 'string<))) (setf (helm-make-dbfile-sorted new-entry) t)) (when helm-make-cache-targets (setf (helm-make-dbfile-targets new-entry) targets (helm-make-dbfile-modtime new-entry) modtime) (puthash makefile new-entry helm-make-db)) targets)) ;;;###autoload (defun helm-make-reset-cache () "Reset cache, see `helm-make-cache-targets'." (interactive) (clrhash helm-make-db)) (defun helm--make (makefile) "Call make for MAKEFILE." (when helm-make-do-save (let* ((regex (format "^%s" default-directory)) (buffers (cl-remove-if-not (lambda (b) (let ((name (buffer-file-name b))) (and name (string-match regex (expand-file-name name))))) (buffer-list)))) (mapc (lambda (b) (with-current-buffer b (save-buffer))) buffers))) (let ((targets (helm--make-cached-targets makefile)) (default-directory (file-name-directory makefile))) (delete-dups helm-make-target-history) (cl-case helm-make-completion-method (helm (helm :sources `((name . "Targets") (candidates . ,targets) (action . helm--make-action)) :history 'helm-make-target-history :preselect (when helm-make-target-history (format "^%s$" (car helm-make-target-history))))) (ivy (ivy-read "Target: " targets :history 'helm-make-target-history :preselect (car helm-make-target-history) :action 'helm--make-action :require-match helm-make-require-match)) (ido (let ((target (ido-completing-read "Target: " targets nil nil nil 'helm-make-target-history))) (when target (helm--make-action target))))))) ;;;###autoload (defun helm-make-projectile (&optional arg) "Call `helm-make' for `projectile-project-root'. ARG specifies the number of cores. By default `helm-make-projectile' will look in `projectile-project-root' followed by `projectile-project-root'/build, for a makefile. You can specify an additional directory to search for a makefile by setting the buffer local variable `helm-make-build-dir'." (interactive "p") (require 'projectile) (setq helm-make-command (format "%s -j%d %%s" helm-make-executable arg)) (let ((makefile (helm--make-makefile-exists (projectile-project-root) (if (and (stringp helm-make-build-dir) (not (string-match-p "\\`[ \t\n\r]*\\'" helm-make-build-dir))) `(,helm-make-build-dir "" "build") `(,@helm-make-build-dir "" "build"))))) (if makefile (helm--make makefile) (error "No Makefile found for project %s" (projectile-project-root))))) (provide 'helm-make) ;;; helm-make.el ends here