>Looks good, except that it does not apply onto the latest main. I rebase pulled the main and created the patch by `git format-patch -1`. Please check the patch to see if the issue is gone.
>Could you also change it to your name for correct attribution while you >are on it? Done Lei Zhe On Thu, Mar 5, 2026 at 3:26 AM Ihor Radchenko <[email protected]> wrote: > > Ihor Radchenko <[email protected]> writes: > > > Lei Zhe <[email protected]> writes: > > > >> Sorry for the late reply. > >> Please check the updated patch. > > Thanks! > Looks good, except that it does not apply onto the latest main. Could > you please rebase? > > Also, I see > > From: llcc <[email protected]> > > Could you also change it to your name for correct attribution while you > are on it? > > -- > Ihor Radchenko // yantar92, > Org mode maintainer, > Learn more about Org mode at <https://orgmode.org/>. > Support Org development at <https://liberapay.com/org-mode>, > or support my work at <https://liberapay.com/yantar92>
From d4cd5eefcfc49673dae0b0b8cfb3457125751048 Mon Sep 17 00:00:00 2001 From: Lei Zhe <[email protected]> Date: Fri, 4 Apr 2025 20:47:11 +0800 Subject: [PATCH] ob-tangle.el: Support tangling a source block to multiple targets * lisp/ob-tangle.el (org-babel-tangle--compute-targets): New function to compute multiple target file paths for a source block, supporting the new :tangle-directory header argument. (org-babel-tangle-collect-blocks): Refactor to handle the new nested list structure that supports multiple tangle targets per block. (org-babel-tangle-single-block): Modify to return a list of file-block pairs instead of a single pair, enabling multiple target support. (org-babel-tangle--unbracketed-link): Add guard for stringp check to handle list vaulues in :tangle parameter. * testing/lisp/test-ob-tangle.el (ob-tangle/collect-blocks): New test helper function and variable. (ob-tangle/collect-blocks): Extend tests to cover :tangle-directory header and multiple tangle targets. --- doc/org-manual.org | 46 ++++++++++++++++-- etc/ORG-NEWS | 54 +++++++++++++++++++++ lisp/ob-tangle.el | 87 ++++++++++++++++++++++++---------- testing/lisp/test-ob-tangle.el | 57 ++++++++++++++++++---- 4 files changed, 206 insertions(+), 38 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 9c4c27877..601eabdc1 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -19653,10 +19653,48 @@ to source file(s). - {{{var(FILENAME)}}} :: - Export the code block to source file whose file name is derived from - any string passed to the =tangle= header argument. Org derives the - file name as being relative to the directory of the Org file's - location. Example: =:tangle FILENAME=. + Export the code block to source file(s) whose file name(s) are + derived from the value passed to the =tangle= header argument. Org + derives the file names as being relative to the directory of the Org + file's location, or relative to the directory specified by the + =tangle-directory= header argument if provided. + + The FILENAME can take several forms: + - A single file name (string) :: + + Example: =:tangle script.py=. + + - A list of file names :: + + Tangle the block to multiple files. Example: =:tangle + '("src/main.py" "src/utils.py")=. + + - A variable or function call :: + + Evaluate to get the target file name(s). Example: =:tangle + 'tangle-targets= or =:tangle (get-tangle-targets)=. + + When both =:tangle-directory= and multiple =:tangle= files are + specified, the block is tangled to all combinations of directories + and files. + + Example: + #+begin_example + ,#+begin_src emacs-lisp :tangle '("config.el" "backup.el") :tangle-directory '("~/.config" "/backup") + (message "Tangling to multiple targets specified by :tangle and :tangle-directory") + ,#+end_src + #+end_example + + This tangles the block to four files: + - ~/.config/config.el + - ~/.config/backup.el + - /backup/config.el + - /backup/backup.el + +#+cindex: @samp{tangle-directory}, header argument +The =tangle-directory= header argument specifies one or more base +directories for relative tangle file paths. This works similarly to +=tangle= and accepts the same types of values. #+cindex: comment trees Additionally, code blocks inside commented subtrees (see [[*Comment diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 40fa1e6aa..b1e464dc4 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -30,6 +30,18 @@ Please send Org bug reports to mailto:[email protected]. The "Tags and Priorities" section of the global context menu is split into a "Tags" section and a "Priorities" section. +*** ~org-babel-tangle-single-block~ may return multiple target files + +When called with non-nil ONLY-THIS-BLOCK, +~org-babel-tangle-single-block~ can now return multiple target file +entries if a source block tangles to more than one file. + +Previously, the returned list always contained exactly one element. +The return value structure is unchanged, but callers should no longer +assume that the outer list has length one, which means ~(car +(org-babel-tangle-single-block ...))~ is not guaranteed to return +the correct ~(FILE . ...)~ cons. + ** New features # We list the most important features, and the features that may @@ -40,6 +52,40 @@ into a "Tags" section and a "Priorities" section. Priorities can now be increased, decreased, set to the default, and set interactively from the priority context menus. +*** ob-tangle.el now supports tangling to multiple targets + +Source blocks can now be tangled to multiple target files using +the ~:tangle~ header argument. The following forms are now supported: + +- =:tangle "file1.el"= (existing behavior) +- =:tangle '("file1.el" "file2.el")= (new: list of files) +- =:tangle (get-tangle-targets)= (new: function returning paths) +- =:tangle 'tangle-targets= (new: variable returning paths) + +Additionally, the new ~:tangle-directory~ header argument specifies +base directories for relative tangle paths: + +- =:tangle-directory "/tmp"= (single directory) +- =:tangle-directory '("/tmp/a" "/tmp/b")= (multiple directories) +- =:tangle-directory (get-tangle-dirs)= (function returning directories) +- =:tangle-directory 'tangle-dirs= (variable returning directories) + +When both ~:tangle-directory~ and multiple ~:tangle~ files are specified, +the block is tangled to all combinations of directories and files. + +Example: +#+begin_example +,#+begin_src emacs-lisp :tangle '("config.el" "backup.el") :tangle-directory '("~/.config" "/backup") +(message "Tangling to multiple targets specified by :tangle and :tangle-directory") +,#+end_src +#+end_example + +This tangles the block to four files: +- ~/.config/config.el +- ~/.config/backup.el +- /backup/config.el +- /backup/backup.el + ** New and changed options # Changes dealing with changing default values of customizations, @@ -76,6 +122,14 @@ showed =0%= when a non-zero amount have been completed, e.g. 1/101. Those cases now show =1%= instead. The same is true for the checkbox status summary type =X%= when defining columns for column view. +*** ~org-babel-tangle-use-relative-file-links~ is ignored during tangling when =:tangle= specifies multiple targets. + +Previously, when ~org-babel-tangle-use-relative-file-links~ was +non-nil, Org Babel attempted to generate relative commented links in +tangled files. This behavior no longer applies when =:tangle= +specifies multiple targets; in that case, absolute links are always +used and the variable is ignored. + * Version 9.8 ** Important announcements and breaking changes diff --git a/lisp/ob-tangle.el b/lisp/ob-tangle.el index 6fe553469..6e4ae07da 100644 --- a/lisp/ob-tangle.el +++ b/lisp/ob-tangle.el @@ -72,8 +72,12 @@ then the name of the language is used." :safe t) (defcustom org-babel-tangle-use-relative-file-links t - "Use relative path names in links from tangled source back the Org file." + "Use relative path names in links from tangled source back the Org file. + +Note that relative links are not used when a code block is tangled into +multiple target files." :group 'org-babel-tangle + :package-version '(Org . "10.0") :type 'boolean) (defcustom org-babel-post-tangle-hook nil @@ -490,7 +494,6 @@ source code blocks by languages matching a regular expression. Optional argument TANGLE-FILE can be used to limit the collected code blocks by target file." (let ((counter 0) - (buffer-fn (buffer-file-name (buffer-base-buffer))) last-heading-pos blocks) (org-babel-map-src-blocks (buffer-file-name) (let ((current-heading-pos @@ -504,26 +507,22 @@ code blocks by target file." (setq last-heading-pos current-heading-pos))) (unless (or (org-in-commented-heading-p) (org-in-archived-heading-p)) - (let* ((info (org-babel-get-src-block-info 'no-eval)) - (src-lang (nth 0 info)) - (src-tfile (cdr (assq :tangle (nth 2 info))))) - (unless (or (string= src-tfile "no") - ;; src block without lang - (and (not src-lang) (string= src-tfile "yes")) - (and tangle-file (not (equal tangle-file src-tfile))) - ;; lang-re but either no lang or lang doesn't match - (and lang-re - (or (not src-lang) - (not (string-match-p lang-re src-lang))))) - ;; Add the spec for this block to blocks under its tangled - ;; file name. - (let* ((block (org-babel-tangle-single-block counter)) - (src-tfile (cdr (assq :tangle (nth 4 block)))) - (file-name (org-babel-effective-tangled-filename - buffer-fn src-lang src-tfile)) - (by-fn (assoc file-name blocks))) - (if by-fn (setcdr by-fn (cons (cons src-lang block) (cdr by-fn))) - (push (cons file-name (list (cons src-lang block))) blocks))))))) + (dolist (block (org-babel-tangle-single-block counter t)) + (let ((src-file (car block)) + (src-lang (caadr block))) + (unless (or (not src-file) + ;; src block without lang + (and (not src-lang) src-file) + (and tangle-file (not (equal tangle-file src-file))) + ;; lang-re but either no lang or lang doesn't match + (and lang-re + (or (not src-lang) + (not (string-match-p lang-re src-lang))))) + (setq blocks + (mapcar (lambda (group) + (cons (car group) + (apply #'append (mapcar #'cdr (cdr group))))) + (seq-group-by #'car (push block blocks))))))))) ;; Ensure blocks are in the correct order. (mapcar (lambda (b) (cons (car b) (nreverse (cdr b)))) (nreverse blocks)))) @@ -540,6 +539,7 @@ The PARAMS are the 3rd element of the info for the same src block." (match-string 1 l)))) (when bare (if (and org-babel-tangle-use-relative-file-links + (stringp (cdr (assq :tangle params))) (string-match org-link-types-re bare) (string= (match-string 1 bare) "file")) (concat "file:" @@ -549,6 +549,41 @@ The PARAMS are the 3rd element of the info for the same src block." bare)))))) (defvar org-outline-regexp) ; defined in lisp/org.el + +(defun org-babel-tangle--compute-targets (buffer-fn info) + "Compute the list of target file paths for tangling a source block. + +BUFFER-FN is the absolute file name of the source buffer. INFO is the +source block information, as returned by `org-babel-get-src-block-info'." + (let* ((params (nth 2 info)) + (lang (nth 0 info)) + (tangle-dir-raw (cdr (assq :tangle-directory params))) + (tangle-targets (cdr (assq :tangle params))) + (tangle-dirs (ensure-list tangle-dir-raw)) + (tangle-files + (cond + ((and (stringp tangle-targets) (string= tangle-targets "yes")) + ;; Default to buffer name if :tangle yes + (list (file-name-nondirectory + (org-babel-effective-tangled-filename buffer-fn lang tangle-targets)))) + ((and (stringp tangle-targets) (string= tangle-targets "no")) nil) + (t (ensure-list tangle-targets))))) + + (when tangle-files + (setq tangle-files + (cl-loop for file in tangle-files append + (if (file-name-absolute-p file) + (list file) ;; absolute paths stay as is + (if tangle-dirs + (mapcar (lambda (dir) (expand-file-name file dir)) tangle-dirs) + (list file)))))) + + ;; Normalize final paths + (cl-remove-duplicates + (mapcar (lambda (file) + (org-babel-effective-tangled-filename buffer-fn lang file)) + tangle-files)))) + (defun org-babel-tangle-single-block (block-counter &optional only-this-block) "Collect the tangled source for current block. Return the list of block attributes needed by @@ -616,7 +651,6 @@ non-nil, return the full association list to be used by (match-end 0) (point-min)))) (point))))) - (src-tfile (cdr (assq :tangle params))) (result (list start-line (if org-babel-tangle-use-relative-file-links @@ -629,9 +663,10 @@ non-nil, return the full association list to be used by (org-trim (org-remove-indentation body))) comment))) (if only-this-block - (let* ((file-name (org-babel-effective-tangled-filename - file src-lang src-tfile))) - (list (cons file-name (list (cons src-lang result))))) + (let* ((file-names (org-babel-tangle--compute-targets file info))) + (mapcar (lambda (file-name) + (cons file-name (list (cons src-lang result)))) + file-names)) result))) (defun org-babel-tangle-comment-links (&optional info) diff --git a/testing/lisp/test-ob-tangle.el b/testing/lisp/test-ob-tangle.el index 640d910b5..a65b1f758 100644 --- a/testing/lisp/test-ob-tangle.el +++ b/testing/lisp/test-ob-tangle.el @@ -579,6 +579,14 @@ another block (set-buffer-modified-p nil)) (kill-buffer buffer)))) +(defun ob-tangle/tangle-targets () + "Return tangle targets for testing." + '("relative.el" "/tmp/absolute.el")) + +(defvar ob-tangle/tangle-targets + '("relative.el" "/tmp/absolute.el") + "Tangle targets variable for testing.") + (ert-deftest ob-tangle/collect-blocks () "Test block collection into groups for tangling." (org-test-with-temp-text-in-file "" ; filled below, it depends on temp file name @@ -628,6 +636,18 @@ another block \"H1: no language and inherited :tangle relative.el in properties\" #+end_src +#+begin_src emacs-lisp :tangle '(\"relative.el\" \"/tmp/absolute.el\") +\"H1: :tangle relative.el and /tmp/absolute.el\" +#+end_src + +#+begin_src emacs-lisp :tangle 'ob-tangle/tangle-targets +\"H1: :tangle relative.el and /tmp/absolute.el\" +#+end_src + +#+begin_src emacs-lisp :tangle (ob-tangle/tangle-targets) +\"H1: :tangle relative.el and /tmp/absolute.el\" +#+end_src + * H2 without :tangle in properties #+begin_src emacs-lisp @@ -664,7 +684,19 @@ another block #+begin_src \"H2: without language and thus without :tangle\" -#+end_src" +#+end_src + +* H3 with :tangle-directory + +#+begin_src emacs-lisp :tangle-directory /tmp/a :tangle '(\"foo.el\" \"bar.el\") :mkdirp yes +\"H3: :tangle /tmp/foo.el and /tmp/bar.el\" +#+end_src + +#+begin_src emacs-lisp :tangle-directory '(\"/tmp/a\" \"/tmp/b\") :tangle '(\"foo.el\" \"bar.el\") :mkdirp yes +\"H3: :tangle /tmp/a/foo.el, /tmp/a/bar.el, /tmp/b/foo.el and /tmp/b/bar.el\" +#+end_src + +" `((?a . ,el-file-abs) (?r . ,el-file-rel)))) ;; We check the collected blocks to tangle by counting equal @@ -687,10 +719,15 @@ another block ;; From `org-babel-tangle-collect-blocks'. collected-blocks))))) (should (equal (funcall normalize-expected-targets-alist - `(("/tmp/absolute.el" . 4) - ("relative.el" . 6) + `(("/tmp/absolute.el" . 7) + ("/tmp/a/foo.el" . 2) + ("/tmp/a/bar.el" . 2) + ("/tmp/b/foo.el" . 1) + ("/tmp/b/bar.el" . 1) + ("relative.el" . 8) ;; file name differs between tests - (,el-file-abs . 4))) + (,el-file-abs . 4) + )) (funcall count-blocks-in-target-files (org-babel-tangle-collect-blocks)))) ;; Simulate TARGET-FILE to test as `org-babel-tangle' and @@ -702,12 +739,16 @@ another block (list (cons :tangle el-file-abs))))) (should (equal (funcall normalize-expected-targets-alist - `(("/tmp/absolute.el" . 4) - ("relative.el" . 6) + `(("/tmp/absolute.el" . 7) + ("/tmp/a/foo.el" . 2) + ("/tmp/a/bar.el" . 2) + ("/tmp/b/foo.el" . 1) + ("/tmp/b/bar.el" . 1) + ("relative.el" . 8) ;; Default :tangle header now also ;; points to the file name derived from the name of - ;; the Org file, so 6 blocks should go there. - (,el-file-abs . 6))) + ;; the Org file, so 5 blocks should go there. + (,el-file-abs . 5))) (funcall count-blocks-in-target-files (org-babel-tangle-collect-blocks))))))))) -- 2.50.1 (Apple Git-155)
