]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tmpfiles: Implement L? to only create symlinks if source exists
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 4 Nov 2024 11:21:21 +0000 (12:21 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 4 Nov 2024 18:04:21 +0000 (19:04 +0100)
This allows a single tmpfiles snippet with lines to symlink directories
from /usr/share/factory to be shared across many different configurations
while making sure symlinks only get created if the source actually exists.

man/tmpfiles.d.xml
src/tmpfiles/tmpfiles.c
test/units/TEST-22-TMPFILES.21.sh [new file with mode: 0755]

index a721c1e66d4efee14d789d8021fde678d85efffb..4e6652dc019b9f306148e920540eed45c3537d10 100644 (file)
@@ -299,15 +299,13 @@ L     /tmp/foobar -    -    -     -   /dev/null</programlisting>
         <varlistentry>
           <term><varname>L</varname></term>
           <term><varname>L+</varname></term>
-          <listitem><para>Create a symlink if it does not exist
-          yet. If suffixed with <varname>+</varname> and a file or
-          directory already exists where the symlink is to be created,
-          it will be removed and be replaced by the symlink. If the
-          argument is omitted, symlinks to files with the same name
-          residing in the directory
-          <filename>/usr/share/factory/</filename> are created. Note
-          that permissions on symlinks are ignored.
-          </para></listitem>
+          <term><varname>L?</varname></term>
+          <listitem><para>Create a symlink if it does not exist yet. If suffixed with <varname>+</varname>
+          and a file or directory already exists where the symlink is to be created, it will be removed and
+          be replaced by the symlink. If suffixed with <varname>?</varname> and the source path does not
+          exist, the symlink is not created. If the argument is omitted, symlinks to files with the same name
+          residing in the directory <filename>/usr/share/factory/</filename> are created. Note that
+          permissions on symlinks are ignored.</para></listitem>
         </varlistentry>
 
         <varlistentry>
index c4e032d8d76c58b09f269b807863dc93be30f08b..86bf16356dc1b8872955bac8146f4cf5c21ff377 100644 (file)
@@ -172,6 +172,8 @@ typedef struct Item {
 
         bool purge:1;
 
+        bool ignore_if_target_missing:1;
+
         OperationMask done;
 } Item;
 
@@ -440,6 +442,10 @@ static bool takes_ownership(ItemType t) {
                       RECURSIVE_REMOVE_PATH);
 }
 
+static bool supports_ignore_if_target_missing(ItemType t) {
+        return t == CREATE_SYMLINK;
+}
+
 static struct Item* find_glob(OrderedHashmap *h, const char *match) {
         ItemArray *j;
 
@@ -2400,6 +2406,17 @@ static int create_symlink(Context *c, Item *i) {
         assert(c);
         assert(i);
 
+        if (i->ignore_if_target_missing) {
+                r = chase(i->argument, arg_root, CHASE_SAFE|CHASE_PREFIX_ROOT|CHASE_NOFOLLOW, /*ret_path=*/ NULL, /*ret_fd=*/ NULL);
+                if (r == -ENOENT) {
+                        /* Silently skip over lines where the source file is missing. */
+                        log_info("Symlink source path '%s%s' does not exist, skipping line.", strempty(arg_root), i->argument);
+                        return 0;
+                }
+                if (r < 0)
+                        return log_error_errno(r, "Failed to check if symlink source path '%s%s' exists: %m", strempty(arg_root), i->argument);
+        }
+
         r = path_extract_filename(i->path, &bn);
         if (r < 0)
                 return log_error_errno(r, "Failed to extract filename from path '%s': %m", i->path);
@@ -3593,7 +3610,8 @@ static int parse_line(
         ItemArray *existing;
         OrderedHashmap *h;
         bool append_or_force = false, boot = false, allow_failure = false, try_replace = false,
-                unbase64 = false, from_cred = false, missing_user_or_group = false, purge = false;
+                unbase64 = false, from_cred = false, missing_user_or_group = false, purge = false,
+                ignore_if_target_missing = false;
         int r;
 
         assert(fname);
@@ -3661,6 +3679,8 @@ static int parse_line(
                         from_cred = true;
                 else if (action[pos] == '$' && !purge)
                         purge = true;
+                else if (action[pos] == '?' && !ignore_if_target_missing)
+                        ignore_if_target_missing = true;
                 else {
                         *invalid_config = true;
                         return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
@@ -3678,6 +3698,7 @@ static int parse_line(
         i.allow_failure = allow_failure;
         i.try_replace = try_replace;
         i.purge = purge;
+        i.ignore_if_target_missing = ignore_if_target_missing;
 
         r = specifier_printf(path, PATH_MAX-1, specifier_table, arg_root, NULL, &i.path);
         if (ERRNO_IS_NEG_NOINFO(r))
@@ -3838,6 +3859,12 @@ static int parse_line(
                                   "Purge flag '$' combined with line type '%c' which does not support purging.", (char) i.type);
         }
 
+        if (i.ignore_if_target_missing && !supports_ignore_if_target_missing(i.type)) {
+                *invalid_config = true;
+                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
+                                  "Modifier '?' combined with line type '%c' which does not support this modifier.", (char) i.type);
+        }
+
         if (!should_include_path(i.path))
                 return 0;
 
@@ -3861,6 +3888,7 @@ static int parse_line(
                         if (!i.argument)
                                 return log_oom();
                 }
+
                 break;
 
         case COPY_FILES:
diff --git a/test/units/TEST-22-TMPFILES.21.sh b/test/units/TEST-22-TMPFILES.21.sh
new file mode 100755 (executable)
index 0000000..ffdaf36
--- /dev/null
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# shellcheck disable=SC2235
+set -eux
+
+# Test L?
+
+rm -rf /tmp/tmpfiles
+
+root="/tmp/tmpfiles"
+mkdir "$root"
+touch "$root/abc"
+
+SYSTEMD_LOG_LEVEL=debug systemd-tmpfiles --create - --root=$root <<EOF
+L? /i-dont-exist - - - - /def
+L? /i-do-exist - - - - /abc
+EOF
+
+(! test -L "$root/i-dont-exist")
+test -L "$root/i-do-exist"