all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
* [PROPOSAL] Builder, a build system integration for Emacs
@ 2023-05-21 10:21 BTuin
  2023-05-21 12:11 ` Philip Kaludercic
                   ` (2 more replies)
  0 siblings, 3 replies; 100+ messages in thread
From: BTuin @ 2023-05-21 10:21 UTC (permalink / raw)
  To: emacs-devel

[-- Attachment #1: Type: text/plain, Size: 10851 bytes --]

Table of Contents
─────────────────

1. The problem it tries to solve
2. How it works
.. 1. Build system detection
.. 2. Configure and compile
.. 3. Project only or not?
.. 4. Command formatting
.. 5. `function-modification'
.. 6. Configuration
3. Open questions
.. 1. Local configuration
.. 2. Build directory name
.. 3. Build system detection
.. 4. Tramp support
.. 5. Flexibility
.. 6. Adding a build system
.. 7. Other
4. Conclusion


Hello, I would like to propose a new package to integrate in Emacs,
"Builder".  It provides integration with build systems, to ease the
configuration and the compilation process, by automatically detecting
the build system and proposing relevant commands.

Big disclaimer: I do not know many build systems, I do not have a lot of
experience in programming, I do not know (E)lisp very well, and English
is not my first language.  Now that you are definitely sold on it, let's
have a deeper explanation!

As far as I know, Emacs does not provide a convenient way to use build
systems, like CMake, Meson, Autotools, Cargo, Dune, and so on.  I know
your can configure the compilation command with local variables, but:
• You cannot have a choice, like `cmake .. -DCMAKE_BUILD_TYPE=Release'
   or `cmake .. -DCMAKE_BUILD_TYPE=Debug'
• It requires to set it up manually
• There is no (convenient?) way to have a configuration phase and a
   compilation phase.  (like `cmake ..' followed by `make')

With Builder, you just run `builder-configure' which set up the build
system, and `builder-compile' to compile the project.
`builder-configure' allow you to easily switch between release and
debug.

You can try it with the `builder-demo/' directory found inside this git
repository: <https://gitlab.com/btuin2/builder.git>

There are more explanations in the README.org.


1 The problem it tries to solve
═══════════════════════════════

   Builder tries to solve two issues at the same time: being convenient
   and being easy to configure.  Independently they are not *that*
   difficult to solve, but together it gets really complex.  For example,
   some build systems need to run their command /outside/ the build
   directory, while others need to run it /inside/.  Also, some commands
   need to be modified according to the state of the project: Meson needs
   the flag `--reconfigure' if it was already configured.


2 How it works
══════════════

2.1 Build system detection
──────────────────────────

   The first step is the detection of the build system.  It simply checks
   in a list of files which one exists and it returns a list of build
   system identifiers.  For instance if both CMake and Meson are
   available, it returns the list `("cmake" "meson")'.

   Notice that the detection is completely separated from build systems
   addition.  This is a deliberate design choice to make it more
   versatile, for example to filter results or have a cache system (both
   not implemented).


2.2 Configure and compile
─────────────────────────

   The configuration is a first step for some build systems (Autotools,
   CMake, Meson)… Indeed they are not actually build systems, but build
   system /generator/ (not that it really matters).  `builder-configure'
   and `builder-compile' respectively provide, well, configuration and
   compilation.  They both do what you would expect by proposing relevant
   commands and running the selected one at the right place.


2.3 Project only or not?
────────────────────────

   You will quickly notice that many functions have the argument
   `variable'.  I am not really sure about this one, but it would allow
   to use a build system not located at the root of a project.  I guess
   it is useful for monorepo projects?  I did minimal testing on it and
   it seems to works.


2.4 Command formatting
──────────────────────

   Commands pass though the function `builder-format-command' before
   being run.  It uses the function `format-spec` with predefined
   specifiers, such as `%n` for the number of cores, `%b` for the build
   directory name…  For example "mycommand -proc %n –dir %b" becomes
   "mycommand -proc 4 –dir build".


2.5 `function-modification'
───────────────────────────

   A pretty neat feature is `function-modification': before running, the
    command is modified by a function.  Its power is showcased with the
    Dune build system and CMake Presets, because you can chose a specific
    target from a list, and the command is modified as needed!  Some
    details need to be investigated though: should the command pass
    through `builder-format-command' before being send to the function?
    After?  Or let the function take care of it?


2.6 Configuration
─────────────────

   The configuration of a build system is hierarchical: at toplevel, it
   affects every instruction from the build system, but you can configure
   each instruction independently.  For example, you don't need to
   specify manually that each CMake command need to run inside the
   directory, but you can still customize a specific command.

   In the following example, both "debug" and "fast" commands are
   executed inside the build directory, and "release" is executed at the
   root of the project.

   ┌────
   │ (builder-add-build-system
   │  :build-system-id "gcc"
   │  :compile '(:inside-directory t ;; Affects all instructions
   │             :instructions
   │             ((:name "debug"
   │               :command "gcc main.c -g")
   │              (:name "release"
   │               :command "gcc main.c -O2"
   │               ;; Overrides the higher level :inside-directory
   │               :inside-directory nil)
   │              (:name "fast"
   │               :command "gcc main.c -Ofast"))))
   └────


3 Open questions
════════════════

   This is a list of problems I don't have a definitive answer to.
   Suggestions are most welcome.


3.1 Local configuration
───────────────────────

   It's necessary to have a way to locally configure a project to add
   commands, specify the build directory…  But I don't really know how to
   do that.  Should it affect the whole project?  Only a specific
   directory (e.g. only `project-root/subdir')?  I know that local
   variables are a thing in Emacs, but they are not a good solution for
   this since they are actually *buffer* variables.  So changing the
   local configuration would require to reload each local variable even
   unrelated one that could have been modified on a per buffer basis.
   Also, the commands cannot be considered as safe, so opening a file
   trigger a warning each time (unless you mark the values as safe).

   It would be a good idea to discuss with the Projectile package devs
   about it, to agree on a common solution if they wish.

   I have implemented an alternative, dir-val.el.  The variables are
   stored in a tree, each node corresponding to a directory.  The
   variables are "hierarchical".  If you have a directory
   `/dirA/dirB/dirC' and you set a variable to `dirB', then `dirC' will
   also access it.  But you can also change the value only for `dirC'.

   I'm wondering if this is actually useful.  Would project variables be
   enough?


3.2 Build directory name
────────────────────────

   This is harder to solve than it seems.  I would like to have a
   mechanism to change it easily, so you can have `build-release' and
   `build-debug' directories, allowing you to conveniently switch between
   release and debug without having to recompile the whole project each
   time you switch.  BUT: some tools have specific expectations about
   this name.  `clangd' expects it to be named `build', and I'm pretty
   sure that the OCaml ecosystem expect it to be named `_build'.  Also,
   how to know which directory to use to execute a command, as
   "configure" and "compile" may have different names?  With a cache?
   With a regex to select candidates?  Local configuration only?

   I really would like to have this feature implemented.  Currently you
   can chose to modify the build directory name according to the current
   git branch (which is pretty reliable), but it would be really useful
   to have `build-release' and `build-debug' directories.


3.3 Build system detection
──────────────────────────

   Currently there are few build systems added to the list.  But there
   could be hundreds.  How well would it scale?  Especially on slow file
   systems?


3.4 Tramp support
─────────────────

   I don't use Tramp and have never used it.  I don't think it would be
   too hard to support it, but the detection could be a problem if there
   are too many build systems.


3.5 Flexibility
───────────────

   Is Builder good enough to support every build system?  To accommodate
   most use cases?  As I said I don't have a lot of experience in build
   systems, so suggestions are welcome.


3.6 Adding a build system
─────────────────────────

   A build system is added with `builder-add-build-system'.  Is it good
   enough?  I fear it is a bit too error prone because it's easy to mess
   with parenthesis and keywords location.  How to solve that?  With a
   macro?  EIEIO?


3.7 Other
─────────

   Some other are listed in the README and there are probably many other
   issues I did not think about.


4 Conclusion
════════════

   I hope you will find this package useful.  Please tell me if this is
   unclear, needs more documentation, or if you have issues.

   Currently only a few build systems are supported.  You can already use
   it to build Emacs with the build system `autotools'.
   `builder-configure' and `builder-compile' are respectively bind to
   `C-x p C' and `C-x p c'.  `builder-configure' will create a directory
   named "build" and run `../configure' from it, and `builder-compile'
   will run `make -jN' with N the number of cores on your processor (the
   result of `num-processor').

   I think this package is far from ready, but I need to know if the
   design is sound enough and future proof.

   Obviously I will need to sign the copyright assignment.

[-- Attachment #2: builder.el --]
[-- Type: text/x-emacs-lisp, Size: 28927 bytes --]

;;; builder.el --- Build a project and run it -*- lexical-binding: t; -*-

;;; Copyright (C) 2022  BTuin
;;; Version: 0.1
;;; Package-Requires: ((emacs "28.1"))
;;; Homepage: https://gitlab.com/btuin2/builder

;; This program 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 of the License, 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.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;; This package provides functions to ease the use of build systems with Emacs.

;;; Code:

(require 'project)
(require 'json)
(require 'cl-lib)

;; only used to get the current branch name to create the build dir
(require 'vc-git)

;; Global variables

(defgroup builder nil
  "Customizations for Builder."
  :version 2
  :group 'tools)

(defvar builder--infos (make-hash-table :test 'equal)
  "Hash table storing the informations about the build systems.
The key is the build systemd ID, a (lowercase?) string corresponding to the
name of the build system.  For example, the key of CMake is \"cmake\".

The value for each key is a plist with multiple keys.
Those keys are symbols, and currently are `:compile',
`:configure' and `:priority'.

For `:compile' and `:configure', the attached values are
also plists.  Those plist, called instructions, have keys that are also
symbols.  Those symbols are:
`:inside-directory': whether the instructions should be executed inside
the build directory or inside its parent directory.
`:function-modification': a function with two arguments, COMMAND and DIRECTORY.
COMMAND, a string, is the command to execute inside a shell.  Those functions
exist because some build systems need to add arguments in different situations.
For example, Meson needs the flag \"--reconfigure\" if it was already
configured once before.
At this level, `:function-modification' applies to every instructions.
`:instructions': a list of instructions. This is the deepest level. Detailled
below.

An instruction is a plist containing a command, a name, and optionnaly some
parameters. The values are:
`:name': a string used to display when asking the user.
`:command': a string, the command to execute in a shell. Before execution,
it is formatted by the function `#'builder-format-command'. See its
documentation for more informations.
`:function-modification': see `:function-modification' detailled upwards. At
this level, only applies to this instruction.

`:priority' is an integer. It is used to change the display order when
asking the user. Greater value means greater priority.")

(defcustom builder-dir-name "build"
  "Default build directory name."
  :type 'string
  :version "28.1"
  :group 'builder)

(defcustom builder-cache-file (locate-user-emacs-file "projects-builder")
  "Cache file.
Currently unused."
  :type 'file
  :version "28.1"
  :group 'builder)

(defcustom builder-enable-multiple-build-directories nil
  "Currently unused.
If set to true, use one directory per build system configuration.
For example, configuring CMake in debug mode will use the directory
\"build-cmake-debug\"."
  :type 'boolean
  :safe t
  :version "29.1"
  :group 'builder)

(defcustom builder-build-dir-name-function #'builder-get-build-dir-name-default
  "A function that return the name of the build directory."
  :type 'function
  :safe t
  :version "29.1"
  :group 'builder)


(defvar builder-build-system-files
  '(("cmake" . "CMakeLists.txt")
    ("cmake-presets" . "CMakePresets.json")
    ("meson" . "meson.build")
    ("autotools". ("configure" "configure.ac"))
    ("make" . "Makefile")
    ("cargo" . "Cargo.toml")
    ("dune" . "dune-project")))


(define-key project-prefix-map "C" 'builder-configure)
(define-key project-prefix-map "c" 'builder-compile)

\f
;; General functions


(cl-defun builder-compilation-buffer-name-function-creator (&key build-system-id name type)
  "Return a *function* which return the name of the compilation buffer.
BUILD-SYSTEM-ID is the name of the build system (e.g. \"cmake\", \"make\"...).
NAME is the type of build, such as \"debug\", \"release\"...
TYPE is the type of command, such as \"compile\", \"configure\"..."
  (lambda (mode) (concat "*" (downcase mode) " "
                         " <"
                         (project-name (project-current))
                         "> "
                         type
                         build-system-id " "
                         name
                         "*")))

(defun builder-get-build-dir-name-default (&rest _)
  "Return the build dir name given by the customizable variable BUILD-DIR-NAME."
  (let ((build-dir-name (or builder-dir-name
                            "build")))
    build-dir-name))

(defun builder-get-build-dir-name-vc-branch (&rest _)
  "Return BUILD-DIR-NAME concatenated with the current branch.
For example, if the project is set on the branch \"bugfix\", the returned
name is \"build-bugfix\"."
  (let ((build-dir-name (or builder-dir-name
                            "build"))
        (current-vc-backend (vc-responsible-backend "." t)))
    (concat
     build-dir-name "-"
     (cond ((string= current-vc-backend "Git")
            (car (vc-git-branches)))
           (t (message "Could not determine branch name from version control"))))))

(defun builder-get-build-dir-name-with-build-configuration (configuration &rest _)
  "Return BUILD-DIR-NAME concatenated with CONFIGURATION (WARNING!!! BROKEN).
WARNING!!!  The implementation is actually completely broken, beacause the
configuration and the compilation instructions need to have exactly the same
name.  This needs to be rethought.  Maybe use a some kind of regex to get
the name of the directory? Like \"build(\\-.+)?\"?
Original documentation below.
CONFIGURATION is the build system configuration.  For example, if you want to
build the project in release mode, the result will be \"build-release\".
This is useful if you want to quickly switch between release and debug,
as some build systems will need a complete recompilation."
  (let ((build-dir-name (or builder-dir-name
                            "build")))
    (concat
     build-dir-name "-" configuration)))

(defun builder--get-build-dir-parent (&optional directory)
  "Return the absolute path of the build directory's parent.
DIRECTORY is the parent of the build directory, relative to
the project root or can an asbolute path.  If nil, the
project root is returned."
  (if directory
      (expand-file-name directory (project-root (project-current)))
    (project-root (project-current))))


(defun builder--get-build-dir-name-absolute (&optional directory)
  "Return the absolute path of the build directory.
If set, DIRECTORY is the parent directory of the build dir.
DIRECTORY is relative to the project root, or can be absolute."
  (expand-file-name
   (funcall builder-build-dir-name-function)
   (builder--get-build-dir-parent directory)))


(defun builder--get-path-relative-to-project (directory)
  "Return DIRECTORY relative to the project root."
  (file-relative-name directory (project-root (project-current))))


(defun builder-reload-dir-locals-project ()
  "Update buffer variables set by the file \"dir-locals.el\".
Currently unused, was created to experiment with dir-locals.el to
create project-local instructions."
  (interactive)
  (let ((dir (project-root (project-current)))
        (enable-local-variables t))
    (dolist (buffer (buffer-list))
      (with-current-buffer buffer
        (when (equal default-directory dir)
          (hack-dir-local-variables-non-file-buffer))))))


(defun builder--detect-build-systems-list (directory)
  "Return a list of detected build systems inside DIRECTORY.
DIRECTORY is either absolute or relative to the root of the project."
  (let ((directory (builder--get-build-dir-parent directory))
        (detected-build-systems (list)))
    (dolist (system-file-list builder-build-system-files)
      (dolist (system-file (ensure-list (cdr system-file-list)))
        (when (file-exists-p (expand-file-name system-file directory))
          (cl-pushnew (car system-file-list) detected-build-systems :test #'equal))))
      detected-build-systems))


(defun builder-detect-build-system (&optional directory)
  "Return the identifier of one detected and selected build system.
The detection occurs inside DIRECTORY if set, otherwise at the root
of the project.  If there are multiple build systems, asks the user
to select one.
Return nil if no build system is detected."
  (let ((build-system-list (builder--detect-build-systems-list
                            (or directory
                                (builder--get-build-dir-parent)))))
    (cond ((zerop (length build-system-list))
           nil)
          ((length= build-system-list 1)
           (message (car build-system-list)))
          (t
           (completing-read "Select build system: " build-system-list nil t)))))


(cl-defun builder-add-build-system (&key build-system-id
                                          configure
                                          compile
                                          priority)
  "Add a build system to the hash table BUILDER--INFOS.
BUILD-SYSTEM-ID is a string indentifying the build system.
CONFIGURE is a plist containing the instructions for the configuration.
COMPILE is a plist containing the instructions for the configuration.
PRIORITY is an integer used to chose the display order of the build system"
  (let ((build-system-infos nil))
    (setq build-system-infos
          (plist-put build-system-infos :configure configure))
    (setq build-system-infos
          (plist-put build-system-infos :compile compile))
    (setq build-system-infos
          (plist-put build-system-infos :priority (or priority
                                                      0)))
    (puthash build-system-id build-system-infos builder--infos)))


(cl-defun builder-add-instruction (&key build-system-id
                                         type
                                         instruction)
  "Add an instruction to the global table.
BUILD-SYSTEM-ID is the identifier of the build system (a string).
TYPE is either :configure or :compile.
INSTRUCTION is a plist with a key :name, a key :command and other
optional keys."
  (let* ((build-infos (gethash build-system-id builder--infos))
         (type-infos (plist-get build-infos type ))
         (instructions-list (plist-get type-infos  :instructions)))
    (cl-pushnew instruction instructions-list
                :test (lambda (x y) (string-equal (plist-get x :name)
                                                  (plist-get y :name))))
    (setq build-infos (plist-put build-infos type
                                 (plist-put type-infos :instructions instructions-list)))
    (puthash build-system-id build-infos builder--infos)))


(cl-defun builder-remove-instruction (&key build-system-id
                                            type
                                            name)
  "Remove an instruction from the global table.
BUILD-SYSTEM-ID is the identifier of the build system (a string).
TYPE is either :configure or :compile.
NAME is the name of the instruction to remove."
  (let* ((build-infos (gethash build-system-id builder--infos))
         (type-infos (plist-get build-infos type ))
         (instructions-list (plist-get type-infos  :instructions)))
    (setq instructions-list
          (cl-remove-if (lambda (x) (string-equal (plist-get x :name)
                                                  name)) instructions-list))
    (setq build-infos (plist-put build-infos type
                                 (plist-put type-infos :instructions instructions-list)))
    (puthash build-system-id build-infos builder--infos)))


(defun builder--get-instruction-priority (instruction)
  "Get the priority of an instruction.
INSTRUCTION is a plist that may contain the key `:priority',
linking to an integer.
If the key is not present, the priority is 0."
  (let ((priority (plist-get instruction :priority)))
    (if priority
        priority
      0)))

(defun builder--compare-instructions-priority (x y)
  "Compare the priority of the instructions X and Y.
Returns true if X has a lower priority than Y."
  (< (builder--get-instruction-priority x)
     (builder--get-instruction-priority y)))


(defun builder--get-instruction-list (build-system-id instruction-type)
  "Return a list of instructions for the build system.
BUILD-SYSTEM-ID is a string identifying the build system,
such as \"cmake\" or \"meson\".
INSTRUCTION-TYPE is a symbol, such as `:config' or `:compile'."
  (let* ((instructions ())
         (build-system-data (gethash build-system-id builder--infos))
         (global-instruction-type-data (plist-get build-system-data instruction-type)))
    (dolist (new-instruction (plist-get global-instruction-type-data :instructions))
      (cl-pushnew
       (dolist (symbol '(:inside-directory
                         :function-modification)
                       new-instruction)
         (let ((value (plist-get global-instruction-type-data symbol)))
           (if (plist-member new-instruction symbol)
               new-instruction
             (setq new-instruction (plist-put new-instruction symbol value)))))
       instructions
       :test (lambda (x y)
               (string-equal
                (plist-get x :name)
                (plist-get y :name)))))
    ;; Not sure if the priority system actually works, nor if it is desirable...
    (sort instructions #'builder--compare-instructions-priority)))


(defun builder--find-in-list-of-plist (plist-list value key)
  "In a list of plist, return the plist matching the value VALUE for the key KEY.
PLIST-LIST is a list of plist, such as
`((:name \"name1\" ...) (:name \"name2\"...))'"
  (cl-find value plist-list :test (lambda (str pl)
                                    (string-equal str (plist-get pl key)))))


(defun builder--select-instruction (instruction-list message)
  "Ask the user to select an instruction from INSTRUCTION-LIST.
INSTRUCTION-LIST is a list of plist, and the function displays
the `:name' value of each plist to make the choice.
It also displays the string MESSAGE to the user."
  (cond ((length= instruction-list 1)
         (plist-get (car instruction-list) :name))
        ((length> instruction-list 1)
         (completing-read
          message
          (cl-map 'list (lambda (x) (plist-get x :name)) instruction-list)))))

(defun builder-format-command (command &optional directory)
  "Format the string COMMAND by replacing format specifications.
It uses the function `format-spec'.
DIRECTORY is the parent of the build directory.

There are multiple format specifications.
`%b' is the build directory name, quotted with `shell-quote-argument'.
`%n' is the number of processors (core) availables.  Provided by the
function `num-processors'.
`%p' is the absolute path of the parent of the build directory, quotted
with `shell-quote-argument'
`%f' is the absolute path of the build directory, quotted with
`shell-quote-argument'

For example, if you want a \"make\" command that use all of your cores, you
can use the string \"cmake -j%n\".  If your processor has 4 cores, it will
be transformed to \"cmake -j4\"."
  (format-spec
   (or command "")
   `((?b . ,(shell-quote-argument (funcall builder-build-dir-name-function)))
     (?n . ,(num-processors))
     (?p . ,(shell-quote-argument (builder--get-build-dir-parent directory)))
     (?f . ,(shell-quote-argument (builder--get-build-dir-name-absolute directory))))))


(cl-defun builder--execute-command (&key instruction directory)
  "Execute the command from INSTRUCTION.
INSTRUCTION is a plist with the key `:command', with a string command as
a value.
MESSAGE is displayed to the user when asked to confirm the command.
DIRECTORY the parent of the build directory.
MODIFICATION-FUNC is a function modifying the command."
  (let* ((build-dir-absolute-directory
          (builder--get-build-dir-name-absolute directory))
         (formatted-command
          (builder-format-command (plist-get instruction :command) directory))
         (work-directory nil)
         (inside-directory (plist-get instruction :inside-directory)))
    (when (not (file-directory-p build-dir-absolute-directory))
      ;; Should we ask before creating the directory?
      (make-directory build-dir-absolute-directory))
    (if inside-directory
        (setq work-directory build-dir-absolute-directory)
      (setq work-directory (builder--get-build-dir-parent directory)))
    (let ((default-directory work-directory))
      (compile (read-string
                (concat
                 (when inside-directory
                   (concat "[in \""
                           (builder--get-path-relative-to-project work-directory)
                           "\"] "))
                 "Command: ")
                (if (plist-get instruction :function-modification)
                    (funcall (plist-get instruction :function-modification)
                             formatted-command
                             directory)
                  formatted-command))))))


;;;###autoload
(defun builder-configure (&optional directory)
  "Configure the build system.

The configuration of a build system is a step happening before compilation.
Some \"build systems\", such as Autotools or CMake are not actually build
systems. They are in reality build systems generator. Their goal is to
create Makefiles (or equivalent), which will then be used to compile the
project.
During this phase, some options are set, such as whether the project is
compiled in debug or release mode, which files need to be compiled,
or compile-time values.

This function detects which build systems are available at the project root,
or at DIRECTORY if set.  DIRECTORY is either absolute or relative to the
project root.
It then asks to select a build system, and finally to chose an available
instruction from the build system."
  (interactive)
  (let* ((build-system-id
          (builder-detect-build-system directory))
         (selected-name nil)
         (instruction-list
          (builder--get-instruction-list build-system-id :configure))
         (instruction nil))
    (if build-system-id
        (progn (setq selected-name (builder--select-instruction
                                    instruction-list
                                    (concat "Select type of configuration for "
                                            build-system-id ":")))
               (setq instruction (builder--find-in-list-of-plist
                                  instruction-list selected-name
                                  :name))
               (let ((compilation-buffer-name-function
                      ;; How does this even work??
                      ;; Isn't lexical binding supposed to prevent that?
                      (builder-compilation-buffer-name-function-creator
                       :type "configure"
                       :build-system-id build-system-id
                       :name selected-name)))
                 (builder--execute-command
                  :instruction instruction
                  :directory directory)))
      (message "No known build system detected"))))


;; Should builder-compile and builder-configure exists?
;; Should they be one function with one parameter? Why? Why not?
;; Arguements for keeping them separated:
;; - Can change one without affecting the other
;; - There is not a lot of logic inside anyway, would merging them be actually useful?
;; Arguments againts:
;; - Duplication of logic
;; - Creating a new one require to copy the code. For instance, projectile has 5
;;   relevant functions, configure, compile, package, install, test (and run,
;;   but this is a special case). Keeping five copies of virtually the same
;;   function seems a bad idea.
;;;###autoload
(defun builder-compile (&optional directory)
  "Compile the project.
This function detects which build systems are available at the project root,
or at DIRECTORY if non-nil.
It then asks to select a build system, and then to chose an available
instruction from the build system."
  (interactive)
  (let* ((build-system-id
          (builder-detect-build-system directory))
         (selected-name nil)
         (instruction-list
          (builder--get-instruction-list build-system-id :compile))
         (instruction nil))
    (if build-system-id
        (progn(setq selected-name (builder--select-instruction
                                   instruction-list
                                   (concat "Select type of compilation for "
                                           build-system-id ":")))
              (setq instruction (builder--find-in-list-of-plist
                                 instruction-list selected-name
                                 :name))
              (let ((compilation-buffer-name-function
                     ;; How does this even work??
                     ;; Isn't lexical binding supposed to prevent that?
                     (builder-compilation-buffer-name-function-creator
                      :type "compile"
                      :build-system-id build-system-id
                      :name selected-name)))
                (builder--execute-command
                 :instruction instruction
                 :directory directory)))
      (message "No known build system detected"))))

\f
;; Build system specific functions

(cl-defun builder--meson-configure-modify-command (command directory)
  "If Meson is already configured, append \"--reconfigure\" to COMMAND.
It checks in the build directory, the child of DIRECTORY, if the file
\"meson-private\" exists.  DIRECTORY is either absolute or relative
to the project root."
  (let ((build-dir-absolute-name (builder--get-build-dir-name-absolute directory)))
    (if (file-directory-p (expand-file-name "meson-private" build-dir-absolute-name))
        (concat command " --reconfigure")
      command)))


(defun builder--cmake-presets-modify-command (command directory)
  "Ask the user to chose a preset from the file \"CMakePresets.json\".
It appends \"--preset=PRESET\" to COMMAND, with PRESET being the
selected preset.
It looks for the file \"CmakePresets.json\" at DIRECTORY.  DIRECTORY is either
 absolute or relative to the project root."
  (let
      ((cmake-presets-directory
        (expand-file-name
         "CMakePresets.json"
         (or directory
             (project-root (project-current)))))
       (selectioned-preset nil))
    (setq selectioned-preset
          (when
              (and
               (json-available-p)
               (file-exists-p cmake-presets-directory))
            (let
                ((presets-content
                  (with-temp-buffer
                    (insert-file-contents cmake-presets-directory)
                    (goto-char
                     (point-min))
                    (json-read)))
                 (presets-table (make-hash-table)))
              (seq-doseq
                  (preset-name
                   (cdr
                    (assoc 'configurePresets presets-content)))
                (puthash
                 (cdr (assoc 'name preset-name))
                 (cdr (assoc 'displayName preset-name))
                 presets-table))
              (when (not (hash-table-empty-p presets-table))
                (completing-read "Select preset: " presets-table nil t)))))
    (if selectioned-preset
        (concat command " --preset=" selectioned-preset)
      (error "No preset selected"))))


(defun builder--dune-modify-command-compile (command directory)
  "Ask user to chose a dune target.
It recursively reads the dune files inside \"bin\" located inside DIRECTORY to
 find the name of the executable.  Each dune file contains can contain at most
one parameter \"executable\" xor (exclusive or) \"executables\".
It then reads the value of the paramter \"name\" (resp. \"names\").
COMMAND is the command to modify.
TODO: add a setting to change the directory, as \"bin\" may not be
the right directory."
  (let* ((working-directory (builder--get-build-dir-parent
                             directory))
         (source-dir (expand-file-name
                      "bin"
                      working-directory))
         (candidates ()))
    (dolist (dune-file (directory-files-recursively source-dir "^dune$"))
      (with-temp-buffer
        (insert "(progn ")
        (insert-file-contents dune-file)
        (goto-char (point-max))
        (insert ")")
        (goto-char (point-min))
        (let* ((content (cdr (read (current-buffer))))
               (sexp nil)
               (res nil))
          (while (and (setq sexp (caar content)) (not res))
            (cond
             ((eq 'executable sexp)
              (cl-pushnew
               (file-relative-name
                (file-name-concat
                 (file-name-parent-directory dune-file)
                 (concat
                  (symbol-name (car (alist-get 'name (cdar content))))
                  ".exe"))
                working-directory)
               candidates))
             ((eq 'executables sexp)
              (setq candidates
                    (append candidates
                            (mapcar
                             (lambda (name)
                               (file-relative-name
                                (file-name-concat
                                 (file-name-parent-directory dune-file)
                                 (concat
                                  name
                                  ".exe"))
                                working-directory))
                             (mapcar #'symbol-name
                                     (alist-get 'names (cdar content))))))))
            (setq content (cdr content))))))
    (concat
     command
     (when candidates
       (let ((choice
              (shell-quote-argument
               (completing-read "Select target:"
                                (cons "all" candidates)
                                nil t))))
         (if (not (string-equal "all" choice))
             (concat " " choice)))))))



\f
;; Add build systems

(builder-add-build-system
 :build-system-id "cmake"
 :configure '(:inside-directory t
              :instructions
              ((:name "release"
                :command "cmake .. -DCMAKE_BUILD_TYPE=release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON")
              (:name "debug"
               :command "cmake .. -DCMAKE_BUILD_TYPE=debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON")))
 :compile '(:instructions
            ((:name "parallel"
              :command "cmake --build %b --parallel %n"))))

(builder-add-build-system
 :build-system-id "meson"
 :configure '(:instructions
              ((:name "release"
                :command "meson setup %b --buildtype=release")
               (:name "debug"
                :command "meson setup %b --buildtype=debug"))
              :function-modification builder--meson-configure-modify-command)
 :compile '(:instructions
            ((:name "default"
              :command "meson compile -C %b"))))


(builder-add-build-system
 :build-system-id "cmake-presets"
 :configure '(:instructions
              ((:name "default"
                :command "cmake"))
              :function-modification builder--cmake-presets-modify-command)
 :compile '(:instructions
            ((:name "parallel"
              :command "cmake --build %b --parallel %n"))))


(builder-add-build-system
 :build-system-id "autotools"
 :configure '(:instructions
              ((:name "configure"
                :command "../configure"
                :inside-directory t)
               (:name "autoconf"
                :command "autoconf")))
 :compile '(:instructions
            ((:name "parallel"
              :command "make -j%n"))
            :inside-directory t))


(builder-add-build-system
 :build-system-id "make"
 :compile '(:instructions
            ((:name "parallel"
              :command "make -j%n"))))

(builder-add-build-system
 :build-system-id "cargo"
 :compile '(:instructions
            ((:name "release"
              :command "cargo build --release")
             (:name "debug"
              :command "cargo build"))))

(builder-add-build-system
 :build-system-id "dune"
 :compile '(:instructions
            ((:name "compile"
              :command "dune build"
              :function-modification builder--dune-modify-command-compile))))


(provide 'builder)
;;; builder.el ends here

[-- Attachment #3: dir-var.el --]
[-- Type: text/x-emacs-lisp, Size: 8744 bytes --]

;;; dir-var.el --- Directory variables -*- lexical-binding: t; -*-

;;; Copyright (C) 2023  BTuin
;;; Version: 0.1
;;; Package-Requires: ((emacs "28.1"))
;;; Homepage: https://gitlab.com/btuin2/builder

;; This program 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 of the License, 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.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;; This package provides project variables.

;;; Code:


(require 'project)
(require 'map)
(require 'cl-lib)


(defvar dir-var--table (make-hash-table :test 'equal))

(defcustom dir-var-file-name "emacs-project-variables-cache.el"
  "Name of file in which project variable are written."
  :type 'file
  :group 'builder)



(defvar dir-var--tree '("" nil))

;; node structure : (name list-of-subnodes plist-of-variables-with-values)

(defun dir-var--make-node (id &optional subnodes variables)
  "Create a node.
ID is the identifier of the node, a string.
SUBNODES if a list of nodes.
VARIABLES is a plist, the properties are the name of the variable (a symbol)
 and the values are their value."
  (cons id (cons subnodes variables)))

(defun dir-var--get-node-subnodes (node)
  "Return the subnodes of NODE."
  (cadr node))

(defun dir-var--get-node-name (node)
  "Return the name of NODE (its ID)."
  (car node))

(defun dir-var--get-node-variables (node)
  "Return the plist containing the variables of NODE."
  (caddr node))

(defun dir-var--node-equal-p (node1 node2)
  "Check if the nodes NODE1 and NODE2 are equal.
Two nodes are equal when their names are equal."
  (string-equal (dir-var--get-node-name node1)
                (dir-var--get-node-name node2)))

(defun dir-var--find-subnode (node name)
  "Find a subnode in the list of subnodes of NODE.
NAME is the name of the subnode to find."
  (seq-find (lambda (x)
              (string-equal (dir-var--get-node-name x)
                            name))
            (dir-var--get-node-subnodes node)))


(defun dir-var--insert-node-in-parent (parent-node node)
  "Insert NODE in PARENT-NODE subnodes list."
  (let ((subnodes (dir-var--get-node-subnodes parent-node)))
    (let ((subnodes (if node
                        (cons
                         node
                         (cl-remove-if (lambda (x) (dir-var--node-equal-p x node)) subnodes))
                      subnodes)))
      (setf parent-node (dir-var--make-node
                         (dir-var--get-node-name parent-node)
                         subnodes
                         (dir-var--get-node-variables parent-node))))))


(defun dir-var--set-node-variable (node variable value)
  "Return NODE with the variable VARIABLE set to VALUE."
  (let ((variable-plist (dir-var--get-node-variables node)))
    (dir-var--make-node
     (dir-var--get-node-name node)
     (dir-var--get-node-subnodes node)
     (list (plist-put variable-plist variable value)))))


(defun dir-var--get-node-variable-value (node variable)
  "Get the value of VARIABLE stored in NODE."
  (plist-get (dir-var--get-node-variables node) variable))


(defun dir-var--node-variable-exists-p (node variable)
  "Return non-nil if VARIABLE is stored in NODE."
  (plist-member (dir-var--get-node-variables node) variable))

(defun dir-var--remove-node-variable (node variable)
  "Return NODE without the variable VARIABLE."
  (let ((variable-plist (dir-var--get-node-variables node)))
    (dir-var--make-node
     (dir-var--get-node-name node)
     (dir-var--get-node-subnodes node)
    (list (map-delete variable-plist variable)))))


(defun dir-var--insert-variable (root dir-split variable value)
  "Return a new tree with the variable inserted.
ROOT is the root node of the tree.
DIR-SPLIT is a list of string.  It is the full path of the directory
splitted a each file separator.  For example, on UNIX systems, the
corresponding splitted path of \"/home/user/project/source\" is
the list (\"home\" \"user\" \"project\" \"source\").
VARIABLE is the symbol to refer to the variable.
VALUE is the value of the variable."
  (if (car dir-split)
      (let ((subnode (or
                      (dir-var--find-subnode root (car dir-split))
                      (dir-var--make-node (car dir-split)))))
        (dir-var--insert-node-in-parent
         root
         (dir-var--insert-variable subnode (cdr dir-split) variable value)))
    (dir-var--set-node-variable root variable value)))


(defun dir-var--variable-get-value (root dir-split variable)
  "Return the value of VARIABLE.

ROOT is the root node of the tree
DIR-SPLIT is a list of string.  It is the full path of the directory
splitted a each file separator.  For example, on UNIX systems, the
corresponding splitted path of \"/home/user/project/source\" is
the list (\"home\" \"user\" \"project\" \"source\")."
  (let ((value nil)
        (node root))
    (while (and dir-split node)
      (setq node (dir-var--find-subnode node (car dir-split)))
      (setq value (or (dir-var--get-node-variable-value node variable)
                      value))
      (setq dir-split (cdr dir-split)))
    value))


(defun dir-var--remove-empty-subnodes (node)
  "Return NODE without its empty subnodes.
A subnode is empty if it does not contain any variable or subnode."
  (dir-var--make-node
   (dir-var--get-node-name node)
   (cl-remove-if-not
    (lambda (node)(or (dir-var--get-node-subnodes node)
                      (dir-var--get-node-variables node)))
    (dir-var--get-node-subnodes node))
   (dir-var--get-node-variables node)))


(defun dir-var--remove-variable (root dir-split variable)
  "Return the tree without the variable VARIABLE stored at DIR-SPLIT.
ROOT is the root node of the tree.
DIR-SPLIT is a list of string.  It is the full path of the directory splitted
at each file separator.  For example, on UNIX systems, the corresponding
splitted path of \"/home/user/project/source\" is the list
 (\"home\" \"user\" \"project\" \"source\")."
  (if (car dir-split)
      (let ((subnode (or
                      (dir-var--find-subnode root (car dir-split))
                      (dir-var--make-node (car dir-split)))))
        (dir-var--remove-empty-subnodes
         (dir-var--insert-node-in-parent
          root
          (dir-var--remove-variable subnode (cdr dir-split) variable))))
    (dir-var--remove-node-variable root variable)))


(defun dir-var--split-directory (&optional directory)
  "Split the full path of DIRECTORY in a list.
If DIRECTORY is nil, the variable DEFAULT-DIRECTORY is used instead.
If DIRECTORY is not an absolute path, it is relative to DEFAULT-DIRECTORY."
  (cl-remove-if
   #'string-empty-p
   (file-name-split (expand-file-name (or directory
                                          default-directory)))))

(defun dir-var-insert (variable value &optional directory)
  "Set a variable for DIRECTORY and its subdirectories.
VARIABLE is the name of a variable, a symbol.
VALUE is the value of the variable, it can be anything.
DIRECTORY is an optional parameter.  If it is not set, the current directory
 is used."
  (let ((splitted-path (dir-var--split-directory directory)))
    (setq dir-var--tree
          (dir-var--insert-variable dir-var--tree splitted-path variable value))))

(defun dir-var-get (variable &optional directory)
  "Get the value of VARIABLE at DIRECTORY.
DIRECTORY is an optional parameter.  If it is not set, the current directory
 is used."
  (let ((splitted-path (dir-var--split-directory directory)))
    (dir-var--variable-get-value dir-var--tree splitted-path variable)))


(defun dir-var-variable-exists-p (variable &optional directory)
  "Return non-nil if the variable VARIABLE is defined in DIRECTORY or parents.
DIRECTORY is an optional parameter.  If it is not set, the current directory
is used.

For exemple, with the path \"/home/user/project/source\", if the
variable \"var1\" is set in the directory \"project\", this function returns
non-nil for the directory \"source\"."
  (let ((dir-split (dir-var--split-directory directory))
        (node dir-var--tree)
        (exists nil))
    (while (and (not exists) dir-split node)
      (setq exists (dir-var--node-variable-exists-p node variable))
      (setq node (dir-var--find-subnode node (car dir-split)))
      (setq dir-split (cdr dir-split)))))


(provide 'dir-var)
;;; dir-var.el ends here

[-- Attachment #4: dir-var-test.el --]
[-- Type: text/x-emacs-lisp, Size: 3071 bytes --]

;;; dir-var-test.el --- Directory variables -*- lexical-binding: t; -*-

;;; Copyright (C) 2023  BTuin
;;; Homepage: https://gitlab.com/btuin2/builder

;; This program 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 of the License, 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.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;; This package provides tests for dir-var-el.

;;; Code:

(require 'ert)

;; "require" does not work for some reason, "dir-var.el" is not found.
;; (require 'dir-var)


(ert-deftest dir-var-set-variable-test ()
  (should
   (equal
    (dir-var--set-node-variable
     (dir-var--set-node-variable
      (dir-var--make-node "name")
      'var "test")
     'var "value")
    '("name" nil (var "value")))))


(ert-deftest dir-var-insert-variable-test ()
  (should
   (equal
    (dir-var--insert-variable
     (dir-var--insert-variable
      (dir-var--make-node "test")
      (list "1" "2" "3" "4")
      'var1 "value1")
     (list "1" "2" "5")
     'var2 "value2")
   '("test"
     (("1"
       (("2"
	 (("5" nil
	   (var2 "value2"))
	  ("3"
	   (("4" nil
	     (var1 "value1")))))))))))))

(ert-deftest dir-var-remove-variable-test ()
  (should
   (equal
    (dir-var--remove-variable
     (dir-var--insert-variable
      (dir-var--insert-variable
       (dir-var--make-node "test")
       (list "1" "2" "3" "4")
       'var1 "value1")
      (list "1" "2" "5")
      'var2 "value2")
     (list "1" "2" "3" "4")
     'var1)
    '("test"
      (("1"
        (("2"
	  (("5" nil
	    (var2 "value2")))))))))))


(ert-deftest dir-var-get-variable-test ()
  (let ((tree (dir-var--make-node "test")))
    (setq tree (dir-var--insert-variable
                tree
                (list "1" "2" "3" "4")
                'var1 "value1"))
    (setq tree (dir-var--insert-variable
                tree
                (list "1" "2" "5")
                'var2 "value2"))
    (setq tree (dir-var--insert-variable
                tree
                (list "1" "2" "3")
                'var3 "value3"))
    (should
     (equal
      (dir-var--variable-get-value
       tree
       (list "1" "2" "3" "4")
       'var1)
      "value1"))
    (should
     (equal
      (dir-var--variable-get-value
       tree
       (list "1" "2" "5")
       'var2)
      "value2"))
    (should
     (equal
      (dir-var--variable-get-value
       tree
       (list "1" "2" "3" "4")
       'var3)
      "value3"))
    (should
     (equal
      (dir-var--variable-get-value
       tree
       (list "1" "2" "5")
       'var3)
      nil))))


(provide 'dir-var-test)
;;; dir-var-test.el ends here

[-- Attachment #5: README.org --]
[-- Type: text/org, Size: 10575 bytes --]

* Introduction

Simple Emacs package to ease the use of build systems.  By default it will use the directory ~build~ at the root of the project.
It uses Emacs' built-in project.el.

*THIS IS A WORK IN PROGRESS!*
This package is not and should not be considered as stable.  Expect unexpected changes at any time.

** Presentation
Project compilation with Emacs is often tedious: there is no built-in system to easily run CMake, Meson or others with a single function, meaning you need to keep a terminal open to run the commands.

*Builder* aims to solve this problem by automatically detecting available build systems for the project, and by proposing a command that will run in a compilation buffer.

There is currently no way to have project local configuration, but this is a planned feature.

* Usage

By default it will use or create the directory ~build~ at the root of a project to generate the build system.  You can change the name globally with the customizable variable ~builder-dir-name~.

To run, just launch the function ~builder-configure~ or ~builder-compile~.

There are demos in the ~builder-demo~ directory.

** Add a build system
*** Detection
You must first make the system detectable.  To do this, you add an element to the alist ~builder--build-system-files~.  The car of an element is the identifier of the system (a string, such as "make", "cargo", "cmake"...), and the cdr is the name of the characteristic file of the build system, located at the project's root.  For "make" it is a file named "Makefile", for "cmake" it is "CMakeLists.txt" and so on.
You can add an element to the list like this:
#+BEGIN_SRC elisp
(add-to-list builder-build-system-files '("make" "Makefile"))
#+END_SRC

*** Add the information
Once the build system is detectable, you need to add the information about the instructions. It works like that:

#+BEGIN_SRC elisp
(builder-add-build-system
 :build-system-id "meson"
 :configure '(:instructions
              ((:name "release"
                :command "meson setup %b --buildtype=release")
               (:name "debug"
                :command "meson setup %b --buildtype=debug"))
              :function-modification builder--meson-configure-modify-command)
 :compile '(:instructions
            ((:name "default"
              :command "meson compile -C %b"))))
#+END_SRC

There are many things to note:
- ~:build-system-id "meson"~: the build system identifier.  Be careful to not mistype it and to check the case!
- ~:configure~ and ~:compile~: currently there a two type of instructions. /configure/ and /compile/.  They both take the same kind of arguments, a ~plist~ with those keys:
  + ~:instructions~: a list of /instructions/, a ~plist~ including a /command/, a name and other optional parameters
    - ~:name~: mandatory, the name of the instruction (a string)
    - ~:command~: mandatory, the command to run (a string). Before execution, it is formatted by ~builder-format-command~.  For example, if the build directory name is "build" on a computer with 4 processor cores, "cmake --build %b --parallel %n" is transformed to "cmake --build \"build\" --parallel 4".  See the doctring of ~builder-format-command~ for more information.
    - ~function--modification~: a function that take in argument a command (a string) and a directory.  The returned value is the command to execute.  Very useful if the command needs information only available at runtime. For example, to reconfigure /meson/ the flag ~--reconfigure~ needs to be added at the end of the command. ~:function-modification builder--meson-configure-modify-command~ automates this.  ~function--modification~ is either instruction specific or global to the whole list of instructions (see below).
  + ~function--modification~: at this level, it affects all instructions of the instruction list.  Same behaviour as described above.
- ~:inside-directory t~ (not demonstrated here): whether the command needs to run inside or outside the build directory (a boolean).  Like ~function--modification~, it can be instruction specific or not.  False by default.
      

** Features
*** Main features
**** Build system detection
Automatically detects your build system and only show relevant commands!

**** Modification of the command on the fly
Some build systems have specific needs.  For instance, CMake's presets are stored into a JSon file.  Builder automatically reads this file, and propose you available targets.
Meson needs the flag ~--reconfigure~ if it was already configured. /Builder/ automatically appends it to the command when needed.

** All features
*** Build directory
- [X] Allow to change the default name
- [ ] Multiple out-of-tree builds (removed because it was utterly broken)
- [X] Possibility to have one build directory per branch
- [X] Chose whether to run the command inside or outside the directory
*** Detection
- [X] Detect the build system
- [X] Change the location of the search
- [X] In case of multiple build systems, allow to select one
- [ ] Override the detection locally
*** Configure/compilation
- [X] Chose between debug/release (or others)
- [ ] *Easily* customize the command locally or globally
- [X] Edit the command on the fly
- [ ] Per project history
- [X] Command formatting with format strings
*** Program execution
- [ ] Run the result of the compilation from Emacs


* Design
** Definitions
/Builder/ makes the artificial distinction between a command and an instruction:
- Command : the plain text sent to the shell.  For example, "make -j2" is a command.
- Instruction : a structure containing a command and other information.  For example, the following is an instruction:
#+BEGIN_SRC elisp
(:name "default" :command "cmake" :function-modification #'my-function)
#+END_SRC



** Build system detection
In the current implementation, build systems are detected by checking if specific files exist.  For example, CMake is detected if a file ~CMakeLists.txt~ exists in the project directory.
Then, the user is asked to select the build system from the list.  It then returns a string, the identifier of the build system.
You may notice that the detection is completely independent from the instructions.  This is a deliberate choice, to allow better flexibility. For example, you could implement rules to only show a build system if some conditions are true; or to not display a build system if an alternative is also present.

** Build system table
Information about build systems are stored in the variable ~builder--infos~.  This is a hash table with build system ids as keys (e.g. "cmake", "meson"...).  See the related doctring for more information.


** Instruction specific vs global
Some settings can be set for a specific instruction, or for all instructions of the instruction list (globally).  This is the case for ~function--modification~ for example.  If it is set globally it can still be overridden on a per instruction basis.  This is extremely useful if you want to exclude some instructions from the function without too much hassle.  For example you can set this parameter to ~nil~ for a specific instruction, while all of the other while use your function.

** Project variables
Currently there is no system to customize commands on a per project basis.  The existing Emacs directory variables could be used, but there are some issues with that.  The variables are actually *buffer* local, so changing the command would require to reload *every* open buffer, which would cause issues if some variables were modified.
Also, the security system is great, but since commands cannot be safe, a warning would appear every time you open a buffer unless you set the values as safe (how much of a problem is that?).

* Open questions
- Should ~:function-modification~ be executed before ~builder-format-command~?  After?  Should ~builder-format-command~ not being called at all if ~function-modification~ is set?
- Force build systems to use ~builder-dir-name~?  Let them use their own defaults?  For example OCaml uses ~_build~ by default.
- Is the design sound enough?  Could it theoretically support every past, present, and future build systems?
- Is ~:function-modification~ well designed?
- Are directory variable useful? Would project variable be sufficient?
- More specifiers in ~builder-format-command~?
- How to implement local variables?  Leverage the existing infrastructure?
- Should ~builder-compile~ and ~builder-configure~ be merged?  Do they have virtually any difference?
- Is a priority system useful (to rank available commands)?
- Is the detection system able to handle hundreds of build systems?  If it is not, what should be done?
- ~builder-get-build-dir-name-with-build-configuration~ is kind of broken (and now removed), because the name of the configuration is only known at build time...  Which means the function does not work correctly if it is not called from ~builder-compile~ or ~builder-configure~...  It means that the configuration instruction and the compilation instruction need to have exactly the same name... Is there a better way to implement this functionality?

* Troubleshooting
** My build system is not detected!
Make sure you added a file to ~builder-build-system-files~ (and that there is not typo in the file name)!
** No commands are proposed!
SO your build system is detected, but there is an error when you selected it. Did you use the function ~builder-add-build-system~?  Are you sure you used the same identifier as for ~builder-build-system-files~?  They both need to have the same case.
** Some configuration doesn't work!
So some configurations such as ~:function-modification~ or ~:inside-directory~ have no effect?  Be careful with the parenthesis:
#+BEGIN_SRC elisp
  (builder-add-build-system
   :build-system-id "autotools"
   :configure '(:instructions
                ((:name "configure"
                  :command "../configure")
                 :inside-directory t)))
#+END_SRC
This example is incorrect: ~:inside-directory~ should be in the same context as ~:name~ (notice the parenthesis after ~:command "../configure"~).
This is the correct version:
#+BEGIN_SRC elisp
  (builder-add-build-system
   :build-system-id "autotools"
   :configure '(:instructions
                ((:name "configure"
                  :command "../configure"
                  :inside-directory t))))
#+END_SRC

* Contributing
I plan to propose this package to Emacs upstream, so if you want to contribute you have to sign the [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Copyright-Assignment.html][copyright assignment]].

^ permalink raw reply	[flat|nested] 100+ messages in thread

end of thread, other threads:[~2023-06-17  3:34 UTC | newest]

Thread overview: 100+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-05-21 10:21 [PROPOSAL] Builder, a build system integration for Emacs BTuin
2023-05-21 12:11 ` Philip Kaludercic
2023-05-21 14:56   ` Augustin Chéneau (BTuin)
2023-05-21 17:24 ` Jim Porter
2023-05-21 19:33   ` Augustin Chéneau (BTuin)
2023-05-21 19:58     ` Jim Porter
2023-05-21 21:36       ` John Yates
2023-05-22 16:35       ` Augustin Chéneau (BTuin)
2023-05-23  0:44         ` Po Lu
2023-05-23 21:10           ` Augustin Chéneau (BTuin)
2023-05-23 21:17             ` Óscar Fuentes
2023-05-24  6:09             ` Dirk-Jan C. Binnema
2023-05-24 21:35               ` Richard Stallman
2023-05-24 23:32                 ` Jim Porter
2023-05-25  0:11                 ` Gregory Heytings
2023-05-25  4:00                   ` tomas
2023-05-25  6:53                     ` Gregory Heytings
2023-05-25  7:48                       ` Eli Zaretskii
2023-05-25  9:33                         ` Gregory Heytings
2023-05-25 11:28                           ` Eli Zaretskii
2023-05-25 11:53                             ` Gregory Heytings
2023-05-25 13:09                               ` Eli Zaretskii
2023-05-25 14:36                                 ` Gregory Heytings
2023-05-25 16:20                                   ` Eli Zaretskii
2023-05-25 16:40                                     ` tomas
2023-05-25 19:23                                     ` Gregory Heytings
2023-05-26  0:57                                       ` Po Lu
2023-05-27  0:24                                         ` Gregory Heytings
2023-05-26  5:57                                       ` Eli Zaretskii
2023-05-26 21:16                                       ` Richard Stallman
2023-05-27  0:25                                         ` Gregory Heytings
2023-05-28 10:20                                           ` Madhu
2023-05-28 12:38                                             ` Po Lu
2023-05-29 22:03                                               ` Gregory Heytings
2023-05-29 22:42                                                 ` Po Lu
2023-05-30  7:26                                                   ` Gregory Heytings
2023-05-30 12:54                                                     ` Po Lu
2023-05-30 15:08                                                       ` Gregory Heytings
2023-05-30 16:50                                                         ` chad
2023-05-31  1:14                                                         ` Po Lu
2023-06-05  1:09                                                           ` Gregory Heytings
2023-06-05  5:29                                                             ` Po Lu
2023-06-05  8:17                                                               ` Gregory Heytings
2023-06-05  9:06                                                                 ` Po Lu
2023-06-17  3:34                                                                   ` Yilkal Argaw
2023-05-27 14:55                                         ` Brian Cully via Emacs development discussions.
2023-05-26  0:54                                   ` Po Lu
2023-05-26 21:16                                   ` Richard Stallman
2023-05-27  0:26                                     ` Gregory Heytings
2023-05-27  2:37                                       ` Ruijie Yu via Emacs development discussions.
2023-05-28 21:48                                       ` Richard Stallman
2023-05-29 22:05                                         ` Gregory Heytings
2023-05-30 13:01                                           ` Po Lu
2023-05-30 15:08                                             ` Gregory Heytings
2023-05-31  1:16                                               ` Po Lu
2023-06-02 21:38                                                 ` Richard Stallman
2023-06-05  1:10                                                 ` Gregory Heytings
2023-06-05  5:19                                                   ` Po Lu
2023-06-05  8:17                                                     ` Gregory Heytings
2023-06-05  9:00                                                       ` Po Lu
2023-05-31 22:28                                               ` Richard Stallman
2023-05-30 21:52                                           ` Richard Stallman
2023-05-28 21:48                                       ` Richard Stallman
2023-05-25 13:16                         ` chad
2023-05-25 19:38                           ` Augustin Chéneau (BTuin)
2023-05-26 21:32                           ` Richard Stallman
2023-05-27  9:45                             ` Yuri Khan
2023-05-28 21:48                               ` Richard Stallman
2023-05-29  8:03                                 ` Yuri Khan
2023-05-30 21:47                                   ` Richard Stallman
2023-05-25  7:55                       ` tomas
2023-05-25  8:44                         ` Gregory Heytings
2023-05-25 10:38                       ` Po Lu
2023-05-25 11:44                         ` Gregory Heytings
2023-05-25 12:02                           ` Po Lu
2023-05-25 12:08                             ` Gregory Heytings
2023-05-26  0:52                               ` Po Lu
2023-05-26 21:16                           ` Richard Stallman
2023-05-27  0:26                             ` Gregory Heytings
2023-05-28 21:47                               ` Richard Stallman
2023-05-29 22:05                                 ` Gregory Heytings
2023-05-30 13:03                                   ` Po Lu
2023-05-31 22:29                                     ` Richard Stallman
2023-05-26 22:59                       ` Lynn Winebarger
2023-05-28 21:22                         ` Björn Bidar
2023-05-29 22:38                           ` Richard Stallman
2023-05-29 22:38                           ` Richard Stallman
2023-05-30  4:28                             ` tomas
2023-05-25 10:42             ` Po Lu
2023-05-25 19:36               ` Augustin Chéneau (BTuin)
2023-05-22 22:00 ` Richard Stallman
2023-05-23  8:36   ` Philip Kaludercic
2023-05-23 11:18     ` Eli Zaretskii
2023-05-23 12:13       ` Po Lu
2023-05-23 18:46       ` Augustin Chéneau (BTuin)
2023-05-24  6:32         ` Juri Linkov
2023-05-24 20:09           ` Augustin Chéneau (BTuin)
2023-05-24  3:34     ` David Masterson
2023-05-24 10:26       ` Philip Kaludercic
2023-05-28 21:17     ` Björn Bidar

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.