diff --git a/cdist/core/cdist_object.py b/cdist/core/cdist_object.py
index bb3a65bd..b963c7cf 100644
--- a/cdist/core/cdist_object.py
+++ b/cdist/core/cdist_object.py
@@ -239,9 +239,9 @@ class CdistObject:
lambda obj: os.path.join(obj.absolute_path, "state"))
source = fsproperty.FileListProperty(
lambda obj: os.path.join(obj.absolute_path, "source"))
- code_local = fsproperty.FileStringProperty(
+ code_local = fsproperty.FileScriptProperty(
lambda obj: os.path.join(obj.base_path, obj.code_local_path))
- code_remote = fsproperty.FileStringProperty(
+ code_remote = fsproperty.FileScriptProperty(
lambda obj: os.path.join(obj.base_path, obj.code_remote_path))
typeorder = fsproperty.FileListProperty(
lambda obj: os.path.join(obj.absolute_path, 'typeorder'))
diff --git a/cdist/util/filesystem.py b/cdist/util/filesystem.py
new file mode 100644
index 00000000..507f343a
--- /dev/null
+++ b/cdist/util/filesystem.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+#
+# 2023 Tabulon (dev-cdist at tabulon.net)
+#
+# This file is part of cdist.
+#
+# cdist 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.
+#
+# cdist 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 cdist. If not, see .
+#
+#
+
+import os
+import stat
+
+def ensure_file_is_executable_by_all(path):
+ """Ensure (and if needed, add) execute permissions
+ for everyone (user, group, others) on the given file
+ Similar to : chmod a+x
+ """
+ ensure_file_permissions(path, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
+
+def ensure_file_permissions(path, permissions):
+ """Ensure (and if needed, add) the given permissions
+ for the given filesystem object.
+ Similar to using '+' with chmod
+ """
+ perm = os.stat(path).st_mode & 0o777 # only the last 3 bits relate to permissions
+
+ # If desired permissions were already set, don't meddle.
+ if ( (perm & permissions ) != permissions ):
+ os.chmod(path, perm | permissions)
+
+ # return a mask of desired permissions that are/were actually set
+ return os.stat(path).st_mode & 0o777 & permissions
+
+def file_has_shebang(path):
+ """Does the given file start with a shebang ?
+ """
+ return read_from_file(path, size=2) == '#!'
+
+def read_from_file(path, size=-1, ignore=None):
+ """Read and return a number of bytes from the given file.
+ If size is '-1' (the default) the entire contents are returned.
+ """
+ value = ""
+ try:
+ with open(path, "r") as fd:
+ value = fd.read(size)
+ except ignore:
+ pass
+ finally:
+ fd.close()
+ return value
+
+def slurp_file(path, ignore=None):
+ """Read and return the entire contents of a given file
+ """
+ return read_from_file(path, size=-1, ignore=ignore)
diff --git a/cdist/util/fsproperty.py b/cdist/util/fsproperty.py
index 6bf935e8..b373f49b 100644
--- a/cdist/util/fsproperty.py
+++ b/cdist/util/fsproperty.py
@@ -23,7 +23,7 @@ import os
import collections
import cdist
-
+import cdist.util.filesystem as fs
class AbsolutePathRequiredError(cdist.Error):
def __init__(self, path):
@@ -319,3 +319,26 @@ class FileStringProperty(FileBasedProperty):
os.remove(path)
except EnvironmentError:
pass
+
+class FileScriptProperty(FileStringProperty):
+ """A property specially tailored for script text,
+ which stores its value in a file.
+ """
+ # Descriptor Protocol
+ def __set__(self, instance, value):
+ super().__set__(instance, value)
+
+ # -------------------------------------------------------------------
+ # NOTE [tabulon@2023-03-31]: If the file starts with a shebang (#!),
+ # mark it as an executable (chmod a+x), so that exec.(local|remote)
+ # can decide to invoke it directly (instead of feeding it to /bin/sh)
+ # -------------------------------------------------------------------
+ # NOTE that this enables cdist to become completely language-agnostic,
+ # even with regard to code generated via (gencode-*) that end up being
+ # stored as a `FileScriptProperty`; since most Unix/Linux systems are
+ # able to detect the **shebang**
+ # -------------------------------------------------------------------
+ if value:
+ path = self._get_path(instance)
+ if fs.file_has_shebang(path):
+ fs.ensure_file_is_executable_by_all(path)