1

When working on projects which use multiple kinds of build-systems, sometimes I want to run a make command based on the project, without having to manually setup project spesific hooks.

Is there a good way to perform this in Emacs?


To expand on this question to give some context,

I might have many projects on my computer and load a C file from a project that uses GNUMakefiles, then another project that uses CMake, and a third project that uses Meson.

Along with this, I might also have some documentation in reStructuredText or Markdown that has a GNUMakefile, which is useful to run to build the documentation.

Neither major-modes have anything to do with GNU Make, it's just convenient to use a GNUMakefile sometimes.

By knowing the language is C or the document is reStructuredText doesn't give me a hint as to the build system, so I would like a way to detect this.

ideasman42
  • 8,375
  • 1
  • 28
  • 105
  • @Fólkvangr I don't understand this comment. Emacs detects version control - git/subversion/cvs/mecurial etc via the `vcs` module. I'm just looking for a generic way to activate a build which detects the build-system in use - in much the same way the `vcs` module detects version control. – ideasman42 Mar 24 '20 at 09:42
  • @Fólkvangr no, for different projects. For example a C++ project may use any one of multiple build-systems scons / waf / make / jam / cmake, . So by loading a C++ file, I don't automatically know which build command to run - by knowning the language alone. – ideasman42 Mar 24 '20 at 21:28

2 Answers2

2

did you try with .dir-locals.el file? something along the lines

((c++-mode  . ((compile-command  . "make -j 4")
               ;; other customisation
               ))
 ;; other modes               
)

see (info "(emacs)Directory Variables")

  • Not all C/C++ project's use the same build system, there are quite a verity to choose from. Although I suppose I could investigate having project level settings - which I expect is possible. Even so, it's nice if I don't need to create files which then need to be properly ignored - when they can be auto-detected. – ideasman42 Mar 15 '20 at 04:25
  • The `.dir-locals.el` in this answer is local to each directory. So every project would have it's own and have "make" replaced with the appropriate build command for that project. – erikstokes Mar 22 '20 at 23:16
1

This is a solution which searches up the parent directories for common build system filenames, and runs the appropriate build-command.

Posting here since it works, although better methods may exist.

(defun my-compile-context-sensitive--locate-dominating-file-multi (dir compilation-filenames)
  "Search for the compilation file traversing up the directory tree.

DIR the base directory to search.
COMPILATION-FILENAMES a list pairs (id, list-of-names).
Note that the id can be any object, this is intended to identify the kind of group.

Returns a triplet (dir, filename, id) or nil if nothing is found.
"
  ;; Ensure 'test-dir' has a trailing slash.
  (let ((test-dir (file-name-as-directory dir))
        (parent-dir (file-name-directory (directory-file-name dir))))

    (catch 'mk-result
      (while (not (string= test-dir parent-dir))
        (dolist (test-id-and-filenames compilation-filenames)
          (pcase-let ((`(,test-id ,test-filenames) test-id-and-filenames))
            (dolist (test-filename test-filenames)
              (when (file-readable-p (concat test-dir test-filename))
                (throw 'mk-result (list test-dir test-filename test-id))))))
        (setq test-dir parent-dir)
        (setq parent-dir (file-name-directory (directory-file-name parent-dir)))))))

(defun my-compile-context-sensitive ()
  (interactive)
  (let* ((mk-reference-dir (expand-file-name "."))
         (mk-dir-file-id
          (my-compile-context-sensitive--locate-dominating-file-multi
           (directory-file-name mk-reference-dir)
           (list
            '("make" ("Makefile" "makefile" "GNUmakefile"))
            '("ninja" ("build.ninja"))
            '("scons" ("SConstruct"))))))

    (if mk-dir-file-id
        (pcase-let ((`(,dir ,file ,id) mk-dir-file-id))
          ;; Ensure 'compile-command' is used.
          (let ((compilation-read-command nil)
                (compile-command
                 (cond
                  ((string= id "make")
                   (concat "make -C " (shell-quote-argument dir)))
                  ((string= id "ninja")
                   (concat "ninja -C " (shell-quote-argument dir)))
                  ((string= id "scons")
                   (concat "scons -C " (shell-quote-argument dir)))
                  (t
                   (error "Unhandled type (internal error)")))))

            (call-interactively 'compile)))
      (message "No makefile found in %S" mk-reference-dir))))
ideasman42
  • 8,375
  • 1
  • 28
  • 105
  • I haven't studied everything, so please forgive me if I am completely confused ... but, why would you need a `while` test during the `catch`/`throw` statement? It seems that the you could just put a `when` statement ... since there is no indication the test of `(not (string= test-dir parent-dir))` needs to be undertaken more than once. Using a `while` statement implies a loop of sorts such that the statement needs to be evaluated more than once during the course of the function .... – lawlist Mar 09 '20 at 05:30
  • The `while` keeps looking up parent directories, in the case none of the paths are found, it needs to stop looking at the parent of "/" or whatever the root path is. – ideasman42 Mar 09 '20 at 05:43
  • Okay ... thank you. I should know better than to think about loops without a cup of coffee in hand. – lawlist Mar 09 '20 at 05:51
  • I guess you could use `locate-dominating-file` rather than your own loop (so it obeys `locate-dominating-stop-dir-regexp`). – Stefan Mar 24 '20 at 14:53
  • Yes, I would then need to pick the _closest_ path found - for the unlikely case multiple exist (it could be done by splitting by path separator and picking the longest). – ideasman42 Mar 24 '20 at 21:35