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)