From 4556343d95d8d64c7ab9bbbf1564b53940579f7f Mon Sep 17 00:00:00 2001 From: "Michael Hirsch, Ph.D" Date: Sun, 17 Nov 2019 13:04:18 -0500 Subject: fs: add methods as_posix, is_absolute fs: make exception specify method name fs: actually raise exceptions fs: resolve path e.g. /opt/foo/.. => /opt/foo fs: correct behavior of is_symlink --- docs/markdown/Fs-module.md | 41 ++++++++++++++++++++--- mesonbuild/modules/fs.py | 50 +++++++++++++++++++++++++---- test cases/common/227 fs module/meson.build | 22 +++++++++++++ 3 files changed, 101 insertions(+), 12 deletions(-) diff --git a/docs/markdown/Fs-module.md b/docs/markdown/Fs-module.md index 53bf960..36b4c4a 100644 --- a/docs/markdown/Fs-module.md +++ b/docs/markdown/Fs-module.md @@ -10,6 +10,8 @@ current `meson.build` file is. If specified, a leading `~` is expanded to the user home directory. +Where possible, symlinks and parent directory notation are resolved to an absolute path. + ### exists Takes a single string argument and returns true if an entity with that @@ -19,13 +21,12 @@ special entry such as a device node. ### is_dir Takes a single string argument and returns true if a directory with -that name exists on the file system. This method follows symbolic -links. +that name exists on the file system. ### is_file Takes a single string argument and returns true if an file with that -name exists on the file system. This method follows symbolic links. +name exists on the file system. ### is_symlink @@ -34,6 +35,23 @@ by the string is a symbolic link. ## File Parameters +### is_absolute + +Return a boolean indicating if the path string specified is absolute for this computer, WITHOUT expanding `~`. + +Examples: + +```meson +fs.is_absolute('~') # false unless you literally have a path with string name `~` + +home = fs.expanduser('~') +fs.is_absolute(home) # true + +fs.is_absolute(home / 'foo') # true, even if ~/foo doesn't exist + +fs.is_absolute('foo/bar') # false, even if ./foo/bar exists +``` + ### hash The `fs.hash(filename, hash_algorithm)` method returns a string containing @@ -44,7 +62,6 @@ md5, sha1, sha224, sha256, sha384, sha512. ### size The `fs.size(filename)` method returns the size of the file in integer bytes. -Symlinks will be resolved if possible. ### is_samepath @@ -79,7 +96,21 @@ fs.is_samepath(p, s) # false ## Filename modification -The files need not actually exist yet for this method, as it's just string manipulation. +The files need not actually exist yet for these methods, as they are just string manipulation. + +### as_posix + +`fs.as_posix(path)` assumes a Windows path, even if on a Unix-like system. +Thus, all `'\'` or `'\\'` are turned to '/', even if you meant to escape a character. + +Examples + +```meson +fs.as_posix('\\') == '/' # true +fs.as_posix('\\\\') == '/' # true + +fs.as_posix('foo\\bar/baz') == 'foo/bar/baz' # true +``` ### replace_suffix diff --git a/mesonbuild/modules/fs.py b/mesonbuild/modules/fs.py index 523ad2b..9b2cb8b 100644 --- a/mesonbuild/modules/fs.py +++ b/mesonbuild/modules/fs.py @@ -31,18 +31,52 @@ class FSModule(ExtensionModule): super().__init__(interpreter) self.snippets.add('generate_dub_file') + def _absolute_dir(self, state: 'ModuleState', arg: str) -> Path: + """ + make an absolute path from a relative path, WITHOUT resolving symlinks + """ + return Path(state.source_root) / state.subdir / Path(arg).expanduser() + def _resolve_dir(self, state: 'ModuleState', arg: str) -> Path: """ - resolves (makes absolute) a directory relative to calling meson.build, + resolves symlinks and makes absolute a directory relative to calling meson.build, if not already absolute """ - return Path(state.source_root) / state.subdir / Path(arg).expanduser() + path = self._absolute_dir(state, arg) + try: + # accomodate unresolvable paths e.g. symlink loops + path = path.resolve() + except Exception: + # return the best we could do + pass + return path def _check(self, check: str, state: 'ModuleState', args: T.Sequence[str]) -> ModuleReturnValue: if len(args) != 1: raise MesonException('fs.{} takes exactly one argument.'.format(check)) test_file = self._resolve_dir(state, args[0]) - return ModuleReturnValue(getattr(test_file, check)(), []) + val = getattr(test_file, check)() + if isinstance(val, Path): + val = str(val) + return ModuleReturnValue(val, []) + + @stringArgs + @noKwargs + def is_absolute(self, state: 'ModuleState', args: T.Sequence[str], kwargs: dict) -> ModuleReturnValue: + if len(args) != 1: + raise MesonException('fs.is_absolute takes exactly one argument.') + return ModuleReturnValue(PurePath(args[0]).is_absolute(), []) + + @stringArgs + @noKwargs + def as_posix(self, state: 'ModuleState', args: T.Sequence[str], kwargs: dict) -> ModuleReturnValue: + """ + this function assumes you are passing a Windows path, even if on a Unix-like system + and so ALL '\' are turned to '/', even if you meant to escape a character + """ + if len(args) != 1: + raise MesonException('fs.as_posix takes exactly one argument.') + return ModuleReturnValue(PureWindowsPath(args[0]).as_posix(), []) @stringArgs @noKwargs @@ -52,7 +86,9 @@ class FSModule(ExtensionModule): @stringArgs @noKwargs def is_symlink(self, state: 'ModuleState', args: T.Sequence[str], kwargs: dict) -> ModuleReturnValue: - return self._check('is_symlink', state, args) + if len(args) != 1: + raise MesonException('fs.is_symlink takes exactly one argument.') + return ModuleReturnValue(self._absolute_dir(state, args[0]).is_symlink(), []) @stringArgs @noKwargs @@ -68,7 +104,7 @@ class FSModule(ExtensionModule): @noKwargs def hash(self, state: 'ModuleState', args: T.Sequence[str], kwargs: dict) -> ModuleReturnValue: if len(args) != 2: - raise MesonException('method takes exactly two arguments.') + raise MesonException('fs.hash takes exactly two arguments.') file = self._resolve_dir(state, args[0]) if not file.is_file(): raise MesonException('{} is not a file and therefore cannot be hashed'.format(file)) @@ -84,7 +120,7 @@ class FSModule(ExtensionModule): @noKwargs def size(self, state: 'ModuleState', args: T.Sequence[str], kwargs: dict) -> ModuleReturnValue: if len(args) != 1: - raise MesonException('method takes exactly one argument.') + raise MesonException('fs.size takes exactly one argument.') file = self._resolve_dir(state, args[0]) if not file.is_file(): raise MesonException('{} is not a file and therefore cannot be sized'.format(file)) @@ -113,7 +149,7 @@ class FSModule(ExtensionModule): @noKwargs def replace_suffix(self, state: 'ModuleState', args: T.Sequence[str], kwargs: dict) -> ModuleReturnValue: if len(args) != 2: - raise MesonException('method takes exactly two arguments.') + raise MesonException('fs.replace_suffix takes exactly two arguments.') original = PurePath(args[0]) new = original.with_suffix(args[1]) return ModuleReturnValue(str(new), []) diff --git a/test cases/common/227 fs module/meson.build b/test cases/common/227 fs module/meson.build index 25778af..670ffed 100644 --- a/test cases/common/227 fs module/meson.build +++ b/test cases/common/227 fs module/meson.build @@ -33,6 +33,28 @@ assert(not fs.is_dir('nonexisting'), 'Bad path detected as a dir.') assert(fs.is_dir('~'), 'expanduser not working') assert(not fs.is_file('~'), 'expanduser not working') +# -- as_posix +assert(fs.as_posix('/') == '/', 'as_posix idempotent') +assert(fs.as_posix('\\') == '/', 'as_posix simple') +assert(fs.as_posix('\\\\') == '/', 'as_posix simple') +assert(fs.as_posix('foo\\bar/baz') == 'foo/bar/baz', 'as_posix mixed slash') + +# -- is_absolute +winabs = 'q:/foo' +unixabs = '/foo' +if is_windows + assert(fs.is_absolute(winabs), 'is_absolute windows not detected') + assert(not fs.is_absolute(unixabs), 'is_absolute unix false positive') +else + assert(fs.is_absolute(unixabs), 'is_absolute unix not detected') + assert(not fs.is_absolute(winabs), 'is_absolute windows false positive') +endif + +# -- replace_suffix + +original = 'foo' +assert(fs.replace_suffix(original, '') == original, 'replace_suffix idempotent') + original = 'foo.txt' new = fs.replace_suffix(original, '.ini') assert(new == 'foo.ini', 'replace_suffix failed') -- cgit v1.1