7d.nz

ob-tangle to multiple destinations

[2024-07-11 jeu.]

  org

In org-mode, exporting a code block to a source file calls a "tangle processor"1knuth-198411 (see org#Extracting Source Code).

The ability to tangle to multiple destinations is a very convenient way to manage cluster configurations, with central configurations dispatched to multiple remote systems. There are many ways to approach this problem, and this one works quite well.

The tangle process is a rather long and complex operation: in short, there are noweb references, variables, langages-specific hooks and possible evaluations. Altogether they open a wide range of possibilities.

The :tangle parameter is either global to the composition of the document, gathering every blocks in order to build one output file, or local to the processed block. That's this second case we're interested in, with many configuration files described in blocks next to comments and explanations.

Here is a modification of org-babel-tangle introducing the argument :tangle-directory that can accept a list.

It's displayed here as a diff not with the intent to be applied as a patch, but to show the very little differences required in order to get it working.

  diff --git a/lisp/ob-tangle.el b/lisp/ob-tangle.el
index c89763efa..c494571dc 100644
--- a/lisp/ob-tangle.el
+++ b/lisp/ob-tangle.el
@@ -269,11 +269,20 @@ matching a regular expression."
       (when (equal arg '(16))
         (or (cdr (assq :tangle (nth 2 (org-babel-get-src-block-info 'no-eval))))
       (user-error "Point is not in a source code block"))))
+            (dirs (cdr (assq :tangle-directory (nth 2 (org-babel-get-src-block-info)))))
      path-collector
-            (source-file buffer-file-name))
-	(mapc ;; map over file-names
+      (source-file buffer-file-name))
+
+        (setq dirs (cl-case (type-of dirs)
+                     (string (list dirs))
+                     (cons dirs)
+                     (symbol '(nil))))
+
+	(dolist (dir dirs) ; iterate the n-tangle group
+          (progn
+	(mapc ; map over directories
   (lambda (by-fn)
-	   (let ((file-name (car by-fn)))
+	   (let ((file-name (concat dir (car by-fn))))
       (when file-name
                (let ((lspecs (cdr by-fn))
         (fnd (file-name-directory file-name))
@@ -354,6 +363,7 @@ matching a regular expression."
   (if (equal arg '(4))
       (org-babel-tangle-single-block 1 t)
     (org-babel-tangle-collect-blocks lang-re tangle-file)))
+        ))
  (message "Tangled %d code block%s from %s" block-counter
     (if (= block-counter 1) "" "s")
     (file-name-nondirectory

Calling org-babel-tangle with the universal argument runs the tangle processor, not on the entire file, but for the current block. The tangled output goes into the designated group.

#+begin_src elisp :tangle-directory '("/tmp" "/-:cadiz:/tmp") :tangle /x/y :mkdirp t
  (org-babel-n-tangle '(4))
#+end_src

In the above example the tangled outputs goes locally to /tmp/x/y and to a remote host, to cadiz:/tmp/x/y using a default protocol.

Additionally, here is an alternative to org-babel-detangle that can be used to start from existing files.

  (defun org-babel-detangle-block ()
  "detangle current block"
  (interactive)
  (save-excursion
    (let ((source-code-file
           (cdr (assoc :tangle (nth 2 (org-babel-get-src-block-info))))))
      (when source-code-file
        (org-babel-update-block-body
         (with-temp-buffer
           (insert-file-contents source-code-file)
           (buffer-string)))))))