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.

  • @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 9: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

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 4: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

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 ()
  (let* ((mk-reference-dir (expand-file-name "."))
           (directory-file-name mk-reference-dir)
            '("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)
                  ((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)))
                   (error "Unhandled type (internal error)")))))

            (call-interactively 'compile)))
      (message "No makefile found in %S" mk-reference-dir))))
  • 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 9 '20 at 5: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 9 '20 at 5:43
  • Okay ... thank you. I should know better than to think about loops without a cup of coffee in hand.
    – lawlist
    Mar 9 '20 at 5: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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.