├── Makefile ├── README.md └── helm-make.el /Makefile: -------------------------------------------------------------------------------- 1 | emacs ?= emacs 2 | 3 | .PHONY: compile clean 4 | 5 | compile: 6 | $(emacs) -batch --eval '(byte-compile-file "helm-make.el")' 7 | 8 | clean: 9 | rm -f *.elc 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | A call to `helm-make` will give you a `helm` selection of this directory 4 | Makefile's targets. Selecting a target will call `compile` on it. You can cancel 5 | as usual with `C-g`. Support is provided for the various flavors of Make tools, 6 | as well as the Ninja build tool. 7 | 8 | ### Install 9 | 10 | Just get it from [MELPA](http://melpa.org/). 11 | 12 | The functions that this package provides are auto-loaded, so no 13 | additional setup is required. Unless you want to bind the functions to 14 | a key. 15 | 16 | ### Additional stuff 17 | 18 | #### `helm-make-do-save` 19 | 20 | If this is set to `t`, the currently visited files from Makefile's 21 | directory will be saved. 22 | 23 | #### `helm-make-projectile` 24 | 25 | This is a `helm-make` called with `(projectile-project-root)` as base directory. 26 | 27 | #### `helm-make-list-target-method` 28 | 29 | What method should be used to parse the Makefile. The default value is 30 | `default`, which is a pure elisp solution, but falls a bit short when the 31 | Makefile includes other Makefile's. The second option is `qp`, it is much more 32 | accurate, as it uses the database produced by make to extract the targets. But 33 | could be a bit slower when the database produced by make is large. 34 | 35 | #### `helm-make-build-dir` 36 | 37 | An additional directory, relative to `projectile-project-root`, where 38 | `helm-make-projectile` will search for a valid Makefile. A valid Makefile is 39 | one that GNU make looks after, i.e. the name of the Makefile must be one of 40 | Makefile, makefile or GNUmakefile to be valid. 41 | 42 | #### `helm-make-sort-targets` 43 | 44 | If this is set to `t`, sort the targets before calling the completion method. 45 | By default it is set to nil, if you are setting it to `t`, and you encounter 46 | longer delays before the targets are displayed, try to set this back to nil. 47 | This, however, might only be the case, if the Makefile contains thousand of 48 | targets. 49 | 50 | #### `helm-make-cache-targets` 51 | 52 | If this is set to `t`, cache the targets. Next time when you call 53 | `helm-make(-projectile)` for the same Makefile, and the modification time of 54 | the Makefile has not changed meanwhile, reuse the cached targets. 55 | It is set to `nil` by default. 56 | 57 | #### `helm-make-executable` 58 | 59 | You can customize executable of make command by changing this variable. Helpful 60 | for implementing remote compiling. 61 | 62 | #### `helm-make-ninja-executable` 63 | 64 | You can customize executable of ninja command by changing this variable. Helpful 65 | for implementing remote compiling. 66 | 67 | #### `helm-make-arguments` 68 | 69 | Customizes arguments which are passed to the make executable when building. To 70 | include the universal argument, use `%d`. Default is `-j%d`. 71 | 72 | #### `helm-make-named-buffer` 73 | 74 | When setting helm-make-named-buffer to `t` all make buffers will be named 75 | based on their make target and `default-directory`. e.g. \*compilation in 76 | ~/emacs (all)\* for `make all` in /home/USER/emacs. This is useful if you want 77 | to run multiple compilations at the same time. 78 | 79 | #### `helm-make-niceness` 80 | 81 | When set to a non-zero value, invocations of make or ninja will run at this 82 | niceness level. Default is 0, i.e. don't nice make commands. 83 | 84 | 85 | #### `helm-make-comint` 86 | 87 | When setting helm-make-comint to `t` helm-make will use Comint mode instead of 88 | Compilation mode. This is useful if you want to interact with the make buffer. 89 | 90 | #### `helm-make-fuzzy-matching` 91 | 92 | When this variable is non-nil, fuzzy matching will be enabled helm make 93 | targets buffer. 94 | 95 | #### `helm-make-nproc` 96 | 97 | Number passed to the '-j' flag. 98 | 99 | When this variable is set to `0`, we try to automatically retrieve available 100 | number of processing units using `helm--make-get-nproc`. 101 | 102 | Regardless of the value of this variable, it can be bypassed by 103 | passing an universal prefix to `helm-make` or `helm-make-projectile`. 104 | -------------------------------------------------------------------------------- /helm-make.el: -------------------------------------------------------------------------------- 1 | ;;; helm-make.el --- Select a Makefile target with helm 2 | 3 | ;; Copyright (C) 2014-2019 Oleh Krehel 4 | 5 | ;; Author: Oleh Krehel 6 | ;; URL: https://github.com/abo-abo/helm-make 7 | ;; Version: 0.2.0 8 | ;; Keywords: makefile 9 | 10 | ;; This file is not part of GNU Emacs 11 | 12 | ;; This file is free software; you can redistribute it and/or modify 13 | ;; it under the terms of the GNU General Public License as published by 14 | ;; the Free Software Foundation; either version 3, or (at your option) 15 | ;; any later version. 16 | 17 | ;; This program is distributed in the hope that it will be useful, 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | ;; GNU General Public License for more details. 21 | 22 | ;; For a full copy of the GNU General Public License 23 | ;; see . 24 | 25 | ;;; Commentary: 26 | ;; 27 | ;; A call to `helm-make' will give you a `helm' selection of this directory 28 | ;; Makefile's targets. Selecting a target will call `compile' on it. 29 | 30 | ;;; Code: 31 | 32 | (require 'subr-x) 33 | (require 'cl-lib) 34 | (eval-when-compile 35 | (require 'helm-source nil t)) 36 | 37 | (declare-function helm "ext:helm") 38 | (declare-function helm-marked-candidates "ext:helm") 39 | (declare-function helm-build-sync-source "ext:helm") 40 | (declare-function ivy-read "ext:ivy") 41 | (declare-function projectile-project-root "ext:projectile") 42 | 43 | (defgroup helm-make nil 44 | "Select a Makefile target with helm." 45 | :group 'convenience) 46 | 47 | (defcustom helm-make-do-save nil 48 | "If t, save all open buffers visiting files from Makefile's directory." 49 | :type 'boolean 50 | :group 'helm-make) 51 | 52 | (defcustom helm-make-build-dir "" 53 | "Specify a build directory for an out of source build. 54 | The path should be relative to the project root. 55 | 56 | When non-nil `helm-make-projectile' will first look in that directory for a 57 | makefile." 58 | :type '(string) 59 | :group 'helm-make) 60 | (make-variable-buffer-local 'helm-make-build-dir) 61 | 62 | (defcustom helm-make-sort-targets nil 63 | "Whether targets shall be sorted. 64 | If t, targets will be sorted as a final step before calling the 65 | completion method. 66 | 67 | HINT: If you are facing performance problems set this to nil. 68 | This might be the case, if there are thousand of targets." 69 | :type 'boolean 70 | :group 'helm-make) 71 | 72 | (defcustom helm-make-cache-targets nil 73 | "Whether to cache the targets or not. 74 | 75 | If t, cache targets of Makefile. If `helm-make' or `helm-make-projectile' 76 | gets called for the same Makefile again, and the Makefile hasn't changed 77 | meanwhile, i.e. the modification time is `equal' to the cached one, reuse 78 | the cached targets, instead of recomputing them. If nil do nothing. 79 | 80 | You can reset the cache by calling `helm-make-reset-db'." 81 | :type 'boolean 82 | :group 'helm-make) 83 | 84 | (defcustom helm-make-executable "make" 85 | "Store the name of make executable." 86 | :type 'string 87 | :group 'helm-make) 88 | 89 | (defcustom helm-make-ninja-executable "ninja" 90 | "Store the name of ninja executable." 91 | :type 'string 92 | :group 'helm-make) 93 | 94 | (defcustom helm-make-niceness 0 95 | "When non-zero, run make jobs at this niceness level." 96 | :type 'integer 97 | :group 'helm-make) 98 | 99 | (defcustom helm-make-arguments "-j%d" 100 | "Pass these arguments to `helm-make-executable' or 101 | `helm-make-ninja-executable'. If `%d' is included, it will be substituted 102 | with the universal argument." 103 | :type 'string 104 | :group 'helm-make) 105 | 106 | (defcustom helm-make-require-match t 107 | "When non-nil, don't allow selecting a target that's not on the list." 108 | :type 'boolean) 109 | 110 | (defcustom helm-make-named-buffer nil 111 | "When non-nil, name compilation buffer based on make target." 112 | :type 'boolean) 113 | 114 | (defcustom helm-make-comint nil 115 | "When non-nil, run helm-make in Comint mode instead of Compilation mode." 116 | :type 'boolean) 117 | 118 | (defcustom helm-make-fuzzy-matching nil 119 | "When non-nil, enable fuzzy matching in helm make target(s) buffer." 120 | :type 'boolean) 121 | 122 | (defcustom helm-make-completion-method 'helm 123 | "Method to select a candidate from a list of strings." 124 | :type '(choice 125 | (const :tag "Helm" helm) 126 | (const :tag "Ido" ido) 127 | (const :tag "Ivy" ivy))) 128 | 129 | (defcustom helm-make-nproc 1 130 | "Use that many processing units to compile the project. 131 | 132 | If `0', automatically retrieve available number of processing units 133 | using `helm--make-get-nproc'. 134 | 135 | Regardless of the value of this variable, it can be bypassed by 136 | passing an universal prefix to `helm-make' or `helm-make-projectile'." 137 | :type 'integer) 138 | 139 | (defvar helm-make-command nil 140 | "Store the make command.") 141 | 142 | (defvar helm-make-target-history nil 143 | "Holds the recently used targets.") 144 | 145 | (defvar helm-make-makefile-names '("Makefile" "makefile" "GNUmakefile") 146 | "List of Makefile names which make recognizes. 147 | An exception is \"GNUmakefile\", only GNU make understands it.") 148 | 149 | (defvar helm-make-ninja-filename "build.ninja" 150 | "Ninja build filename which ninja recognizes.") 151 | 152 | (defun helm--make-get-nproc () 153 | "Retrieve available number of processing units on this machine. 154 | 155 | If it fails to do so, `1' will be returned. 156 | " 157 | (cond 158 | ((member system-type '(gnu gnu/linux gnu/kfreebsd cygwin)) 159 | (if (executable-find "nproc") 160 | (string-to-number (string-trim (shell-command-to-string "nproc"))) 161 | (warn "Can not retrieve available number of processing units, \"nproc\" not found") 162 | 1)) 163 | ;; What about the other systems '(darwin windows-nt aix berkeley-unix hpux usg-unix-v)? 164 | (t 165 | (warn "Retrieving available number of processing units not implemented for system-type %s" system-type) 166 | 1))) 167 | 168 | (defvar helm-make--last-item nil) 169 | 170 | (defun helm--make-action (target) 171 | "Make TARGET." 172 | (setq helm-make--last-item target) 173 | (let* ((targets (and (eq helm-make-completion-method 'helm) 174 | (or (> (length (helm-marked-candidates)) 1) 175 | ;; Give single marked candidate precedence over current selection. 176 | (unless (equal (car (helm-marked-candidates)) target) 177 | (setq target (car (helm-marked-candidates))) nil)) 178 | (mapconcat 'identity (helm-marked-candidates) " "))) 179 | (make-command (format helm-make-command (or targets target))) 180 | (compile-buffer (compile make-command helm-make-comint))) 181 | (when helm-make-named-buffer 182 | (helm--make-rename-buffer compile-buffer (or targets target))))) 183 | 184 | (defun helm--make-rename-buffer (buffer target) 185 | "Rename the compilation BUFFER based on the make TARGET." 186 | (let ((buffer-name (format "*compilation in %s (%s)*" 187 | (abbreviate-file-name default-directory) 188 | target))) 189 | (when (get-buffer buffer-name) 190 | (kill-buffer buffer-name)) 191 | (with-current-buffer buffer 192 | (rename-buffer buffer-name)))) 193 | 194 | (defvar helm--make-build-system nil 195 | "Will be 'ninja if the file name is `build.ninja', 196 | and if the file exists 'make otherwise.") 197 | 198 | (defun helm--make-construct-command (arg file) 199 | "Construct the `helm-make-command'. 200 | 201 | ARG should be universal prefix value passed to `helm-make' or 202 | `helm-make-projectile', and file is the path to the Makefile or the 203 | ninja.build file." 204 | (format (concat "%s%s -C %s " helm-make-arguments " %%s") 205 | (if (= helm-make-niceness 0) 206 | "" 207 | (format "nice -n %d " helm-make-niceness)) 208 | (cond 209 | ((equal helm--make-build-system 'ninja) 210 | helm-make-ninja-executable) 211 | (t 212 | helm-make-executable)) 213 | (replace-regexp-in-string 214 | "^/\\(scp\\|ssh\\).+?:.+?:" "" 215 | (shell-quote-argument (file-name-directory file))) 216 | (let ((jobs (abs (if arg (prefix-numeric-value arg) 217 | (if (= helm-make-nproc 0) (helm--make-get-nproc) 218 | helm-make-nproc))))) 219 | (if (> jobs 0) jobs 1)))) 220 | 221 | (defcustom helm-make-directory-functions-list 222 | '(helm-make-current-directory helm-make-project-directory helm-make-dominating-directory) 223 | "Functions that return Makefile's directory, sorted by priority." 224 | :type 225 | '(repeat 226 | (choice 227 | (const :tag "Default directory" helm-make-current-directory) 228 | (const :tag "Project directory" helm-make-project-directory) 229 | (const :tag "Dominating directory with makefile" helm-make-dominating-directory) 230 | (function :tag "Custom function")))) 231 | 232 | ;;;###autoload 233 | (defun helm-make (&optional arg) 234 | "Call \"make -j ARG target\". Target is selected with completion." 235 | (interactive "P") 236 | (let ((makefile nil)) 237 | (cl-find-if 238 | (lambda (fn) (setq makefile (helm--make-makefile-exists (funcall fn)))) 239 | helm-make-directory-functions-list) 240 | (if (not makefile) 241 | (error "No build file in %s" default-directory) 242 | (setq helm-make-command (helm--make-construct-command arg makefile)) 243 | (helm--make makefile)))) 244 | 245 | (defconst helm--make-ninja-target-regexp "^\\(.+\\): " 246 | "Regexp to identify targets in the output of \"ninja -t targets\".") 247 | 248 | (defun helm--make-target-list-ninja (makefile) 249 | "Return the target list for MAKEFILE by parsing the output of \"ninja -t targets\"." 250 | (let ((default-directory (file-name-directory (expand-file-name makefile))) 251 | (ninja-exe helm-make-ninja-executable) ; take a copy in case buffer-local 252 | targets) 253 | (with-temp-buffer 254 | (call-process ninja-exe nil t t "-f" (file-name-nondirectory makefile) 255 | "-t" "targets" "all") 256 | (goto-char (point-min)) 257 | (while (re-search-forward helm--make-ninja-target-regexp nil t) 258 | (push (match-string 1) targets)) 259 | targets))) 260 | 261 | (defun helm--make-target-list-qp (makefile) 262 | "Return the target list for MAKEFILE by parsing the output of \"make -nqp\"." 263 | (let ((default-directory (file-name-directory 264 | (expand-file-name makefile))) 265 | targets target) 266 | (with-temp-buffer 267 | (insert 268 | (shell-command-to-string 269 | (format "make -f %s -nqp __BASH_MAKE_COMPLETION__=1 .DEFAULT 2>/dev/null" 270 | makefile))) 271 | (goto-char (point-min)) 272 | (unless (re-search-forward "^# Files" nil t) 273 | (error "Unexpected \"make -nqp\" output")) 274 | (while (re-search-forward "^\\([^%$:#\n\t ]+\\):\\([^=]\\|$\\)" nil t) 275 | (setq target (match-string 1)) 276 | (unless (or (save-excursion 277 | (goto-char (match-beginning 0)) 278 | (forward-line -1) 279 | (looking-at "^# Not a target:")) 280 | (string-match "^\\([/a-zA-Z0-9_. -]+/\\)?\\." target)) 281 | (push target targets)))) 282 | targets)) 283 | 284 | (defun helm--make-target-list-default (makefile) 285 | "Return the target list for MAKEFILE by parsing it." 286 | (let (targets) 287 | (with-temp-buffer 288 | (insert-file-contents makefile) 289 | (goto-char (point-min)) 290 | (while (re-search-forward "^\\([^: \n]+\\) *:\\(?: \\|$\\)" nil t) 291 | (let ((str (match-string 1))) 292 | (unless (string-match "^\\." str) 293 | (push str targets))))) 294 | (nreverse targets))) 295 | 296 | (defcustom helm-make-list-target-method 'default 297 | "Method of obtaining the list of Makefile targets. 298 | 299 | For ninja build files there exists only one method of obtaining the list of 300 | targets, and hence no `defcustom'." 301 | :type '(choice 302 | (const :tag "Default" default) 303 | (const :tag "make -qp" qp))) 304 | 305 | (defun helm--make-makefile-exists (base-dir &optional dir-list) 306 | "Check if one of `helm-make-makefile-names' and `helm-make-ninja-filename' 307 | exist in BASE-DIR. 308 | 309 | Returns the absolute filename to the Makefile, if one exists, 310 | otherwise nil. 311 | 312 | If DIR-LIST is non-nil, also search for `helm-make-makefile-names' and 313 | `helm-make-ninja-filename'." 314 | (let* ((default-directory (file-truename base-dir)) 315 | (makefiles 316 | (progn 317 | (unless (and dir-list (listp dir-list)) 318 | (setq dir-list (list ""))) 319 | (let (result) 320 | (dolist (dir dir-list) 321 | (dolist (makefile `(,@helm-make-makefile-names ,helm-make-ninja-filename)) 322 | (push (expand-file-name makefile dir) result))) 323 | (reverse result)))) 324 | (makefile (cl-find-if 'file-exists-p makefiles))) 325 | (when makefile 326 | (cond 327 | ((string-match "build\.ninja$" makefile) 328 | (setq helm--make-build-system 'ninja)) 329 | (t 330 | (setq helm--make-build-system 'make)))) 331 | makefile)) 332 | 333 | (defvar helm-make-db (make-hash-table :test 'equal) 334 | "An alist of Makefile and corresponding targets.") 335 | 336 | (cl-defstruct helm-make-dbfile 337 | targets 338 | modtime 339 | sorted) 340 | 341 | (defun helm--make-cached-targets (makefile) 342 | "Return cached targets of MAKEFILE. 343 | 344 | If there are no cached targets for MAKEFILE, the MAKEFILE modification 345 | time has changed, or `helm-make-cache-targets' is nil, parse the MAKEFILE, 346 | and cache targets of MAKEFILE, if `helm-make-cache-targets' is t." 347 | (let* ((att (file-attributes makefile 'integer)) 348 | (modtime (if att (nth 5 att) nil)) 349 | (entry (gethash makefile helm-make-db nil)) 350 | (new-entry (make-helm-make-dbfile)) 351 | (targets (cond 352 | ((and helm-make-cache-targets 353 | entry 354 | (equal modtime (helm-make-dbfile-modtime entry)) 355 | (helm-make-dbfile-targets entry)) 356 | (helm-make-dbfile-targets entry)) 357 | (t 358 | (delete-dups 359 | (cond 360 | ((equal helm--make-build-system 'ninja) 361 | (helm--make-target-list-ninja makefile)) 362 | ((equal helm-make-list-target-method 'qp) 363 | (helm--make-target-list-qp makefile)) 364 | (t 365 | (helm--make-target-list-default makefile)))))))) 366 | (when helm-make-sort-targets 367 | (unless (and helm-make-cache-targets 368 | entry 369 | (helm-make-dbfile-sorted entry)) 370 | (setq targets (sort targets 'string<))) 371 | (setf (helm-make-dbfile-sorted new-entry) t)) 372 | 373 | (when helm-make-cache-targets 374 | (setf (helm-make-dbfile-targets new-entry) targets 375 | (helm-make-dbfile-modtime new-entry) modtime) 376 | (puthash makefile new-entry helm-make-db)) 377 | targets)) 378 | 379 | ;;;###autoload 380 | (defun helm-make-reset-cache () 381 | "Reset cache, see `helm-make-cache-targets'." 382 | (interactive) 383 | (clrhash helm-make-db)) 384 | 385 | (defun helm--make (makefile) 386 | "Call make for MAKEFILE." 387 | (when helm-make-do-save 388 | (let* ((regex (format "^%s" default-directory)) 389 | (buffers 390 | (cl-remove-if-not 391 | (lambda (b) 392 | (let ((name (buffer-file-name b))) 393 | (and name 394 | (string-match regex (expand-file-name name))))) 395 | (buffer-list)))) 396 | (mapc 397 | (lambda (b) 398 | (with-current-buffer b 399 | (save-buffer))) 400 | buffers))) 401 | (let ((targets (helm--make-cached-targets makefile)) 402 | (default-directory (file-name-directory makefile))) 403 | (delete-dups helm-make-target-history) 404 | (cl-case helm-make-completion-method 405 | (helm 406 | (require 'helm) 407 | (helm :sources (helm-build-sync-source "Targets" 408 | :header-name (lambda (name) (format "%s (%s):" name makefile)) 409 | :candidates 'targets 410 | :fuzzy-match helm-make-fuzzy-matching 411 | :action 'helm--make-action) 412 | :history 'helm-make-target-history 413 | :preselect helm-make--last-item)) 414 | (ivy 415 | (unless (window-minibuffer-p) 416 | (ivy-read "Target: " 417 | targets 418 | :history 'helm-make-target-history 419 | :preselect (car helm-make-target-history) 420 | :action 'helm--make-action 421 | :require-match helm-make-require-match))) 422 | (ido 423 | (let ((target (ido-completing-read 424 | "Target: " targets 425 | nil nil nil 426 | 'helm-make-target-history))) 427 | (when target 428 | (helm--make-action target))))))) 429 | 430 | ;;;###autoload 431 | (defun helm-make-projectile (&optional arg) 432 | "Call `helm-make' for `projectile-project-root'. 433 | ARG specifies the number of cores. 434 | 435 | By default `helm-make-projectile' will look in `projectile-project-root' 436 | followed by `projectile-project-root'/build, for a makefile. 437 | 438 | You can specify an additional directory to search for a makefile by 439 | setting the buffer local variable `helm-make-build-dir'." 440 | (interactive "P") 441 | (require 'projectile) 442 | (let ((makefile (helm--make-makefile-exists 443 | (projectile-project-root) 444 | (if (and (stringp helm-make-build-dir) 445 | (not (string-match-p "\\`[ \t\n\r]*\\'" helm-make-build-dir))) 446 | `(,helm-make-build-dir "" "build") 447 | `(,@helm-make-build-dir "" "build"))))) 448 | (if (not makefile) 449 | (error "No build file found for project %s" (projectile-project-root)) 450 | (setq helm-make-command (helm--make-construct-command arg makefile)) 451 | (helm--make makefile)))) 452 | 453 | (defvar project-roots) 454 | 455 | (defun helm-make-project-directory () 456 | "Return the current project root directory if found." 457 | (if (and (fboundp 'project-current) (project-current)) 458 | (car (project-roots (project-current))) 459 | nil)) 460 | 461 | (defun helm-make-current-directory() 462 | "Return the current directory." 463 | default-directory) 464 | 465 | (defun helm-make-dominating-directory () 466 | "Return the dominating directory that contains a Makefile if found" 467 | (locate-dominating-file default-directory 'helm--make-makefile-exists)) 468 | 469 | (provide 'helm-make) 470 | 471 | ;;; helm-make.el ends here 472 | --------------------------------------------------------------------------------